/* paper.jsx — the real, sendable A4 invoice document (210×297mm proportions).
 * Rendered at a native pixel width and scaled to fit wherever it's shown. This
 * is the artifact that gets downloaded, shared and sent. Includes the payment
 * section (configured methods + scannable pay-QR encoding the outstanding
 * balance, never the full total of a partially paid invoice).
 * → window.A4.{ Invoice, Scaled, Viewer }
 */
(function () {
  const { useState } = React;
  const Q = window.Q, LIB = window.LIB, Icons = window.Icons;
  const { Icon } = Icons;
  const t = window.t;
  const REDUCE = () => window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* Local keyframes for the on-screen document assembly + pay-QR sheen. Kept
     in paper.jsx (not the shared stylesheet) so this file owns its own motion;
     injected once, idempotently. The reduced-motion media reset in index.html
     neutralises these globally too. */
  (function injectPaperCSS() {
    if (document.getElementById("q-paper-css")) return;
    const el = document.createElement("style");
    el.id = "q-paper-css";
    el.textContent =
      "@keyframes q-paper-block{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}" +
      "@keyframes q-paper-sheen{to{transform:translateX(320%) skewX(-16deg)}}" +
      ".q-paper-qr{position:relative;overflow:hidden}" +
      ".q-paper-qr::after{content:'';position:absolute;top:-20%;bottom:-20%;left:0;width:55%;" +
      "transform:translateX(-160%) skewX(-16deg);pointer-events:none;" +
      "background:linear-gradient(100deg,transparent,hsl(38 72% 32% / 0.18),transparent);" +
      "animation:q-paper-sheen .6s cubic-bezier(.32,.72,0,1) .7s 1 both}";
    document.head.appendChild(el);
  })();

  /* Cross-package safety: strings.js keys land in another package. If a key
     has not merged yet, t() echoes the key back — print the fallback instead
     of a raw key on a real document. */
  function tf(key, vars, fb) {
    const s = t(key, vars);
    return s === key ? fb : s;
  }
  /* Short unit label for the qty column ("h", "d", "pc"…) via unit.*.abbr
     keys; falls back to the legacy truncated label until the keys merge. */
  function unitAbbr(unit, qty) {
    const k = "unit." + unit + ".abbr";
    const s = t(k);
    return s === k ? LIB.unitLabel(unit, qty).slice(0, 3) : s;
  }
  /* e-invoice readiness for the profile mention: LIB.einvoiceCheck once it
     lands (boolean, missing[] or {ok} shapes), else the minimal seller-ID
     check the FacturX profile line requires. */
  function einvReady(inv, co, seller) {
    if (typeof LIB.einvoiceCheck === "function") {
      const r = LIB.einvoiceCheck(inv, co);
      if (typeof r === "boolean") return r;
      if (Array.isArray(r)) return r.length === 0;
      if (r && typeof r === "object") return !!(r.ok != null ? r.ok : r.ready);
      return !!r;
    }
    return !!(seller.vat && seller.siret);
  }

  const NATIVE = 820;                 // design width in px for the A4 sheet
  const RATIO = 297 / 210;            // A4 aspect (h/w)
  const PAD = 54;

  /* paper ink palette (a real document — not the app's theme surfaces) */
  const INK = "hsl(35 22% 13%)";
  const INK2 = "hsl(35 14% 40%)";
  const INK3 = "hsl(35 12% 55%)";
  const HAIR = "hsl(35 18% 90%)";
  const RULE = "hsl(35 22% 20%)";
  const GOLD = "hsl(38 72% 32%)";
  const SAGE = "hsl(155 32% 34%)";

  function Meta({ label, value }) {
    return (
      <div style={{ marginBottom: 11 }}>
        <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, letterSpacing: "0.1em", textTransform: "uppercase", color: INK3 }}>{label}</div>
        <div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 500, marginTop: 3, color: INK }}>{value}</div>
      </div>
    );
  }
  function FootMeta({ label, value }) {
    if (!value) return null;
    return (
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: "var(--font-mono)", fontSize: 9.5, letterSpacing: "0.1em", textTransform: "uppercase", color: INK3 }}>{label}</div>
        <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, marginTop: 2, color: INK2, wordBreak: "break-word" }}>{value}</div>
      </div>
    );
  }

  /* ── Payment section (on the A4 page) ─────────────────────────
     `due` is the outstanding balance — the QR must never ask a client to
     pay the full total of a partially paid invoice. */
  function PaySection({ co, inv, due, anim, sheen }) {
    const pm = co.payment || {};
    const qr = LIB.payQR(co, inv, inv.status === "paid" ? 0 : due);
    const methods = [];
    if (pm.bank && pm.bank.enabled && co.iban) methods.push("bank");
    if (pm.card && pm.card.enabled && pm.card.link) methods.push("card");
    if (pm.nita && pm.nita.enabled) methods.push("nita");
    if (methods.length === 0) return null;
    return (
      <div style={{ marginTop: 26, border: `1.4px solid ${HAIR}`, borderRadius: 14, padding: 22, display: "flex", gap: 22, ...anim }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.12em", textTransform: "uppercase", color: INK3, marginBottom: 14 }}>{t("pay.title")}</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
            {methods.includes("bank") && (
              <PayMethod icon="bank" title={t("pay.bank")}>
                <PayLine label={t("pay.iban")} value={co.iban} />
                <PayLine label={t("pay.bic")} value={co.bic} />
                <PayLine label={t("pay.account")} value={co.accountName || co.name} />
                <PayLine label={t("pay.ref")} value={inv.id} />
              </PayMethod>
            )}
            {methods.includes("card") && (
              <PayMethod icon="card" title={t("pay.card")}>
                <PayLine label={t("pay.link")} value={pm.card.link} accent />
              </PayMethod>
            )}
            {methods.includes("nita") && (
              <PayMethod icon="wallet" title={t("pay.nita")} sage>
                <PayLine label={t("pay.nita.to")} value={pm.nita.handle || pm.nita.phone} />
              </PayMethod>
            )}
          </div>
        </div>
        {qr && (
          <div style={{ width: 150, flexShrink: 0, textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center" }}>
            <div className={sheen ? "q-paper-qr" : undefined} style={{ padding: 10, border: `1.4px solid ${HAIR}`, borderRadius: 12, background: "#fff" }}>
              <Q.QR value={qr.value} size={120} color={INK} />
            </div>
            <div style={{ display: "inline-flex", alignItems: "center", gap: 5, marginTop: 10, color: qr.method === "nita" ? SAGE : INK2 }}>
              <Icon name="scan" size={13} />
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.04em", lineHeight: 1.3 }}>{t("pay.scan")}</span>
            </div>
          </div>
        )}
      </div>
    );
  }
  function PayMethod({ icon, title, children, sage }) {
    return (
      <div style={{ display: "flex", gap: 11 }}>
        <div style={{ width: 30, height: 30, borderRadius: 8, background: sage ? "hsl(155 32% 34% / 0.12)" : "hsl(35 18% 92%)", color: sage ? SAGE : INK2, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
          <Icon name={icon} size={16} />
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 12.5, fontWeight: 700, color: INK, marginBottom: 4 }}>{title}</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>{children}</div>
        </div>
      </div>
    );
  }
  function PayLine({ label, value, accent }) {
    if (!value) return null;
    return (
      <div style={{ display: "flex", gap: 8, fontSize: 11.5, lineHeight: 1.5 }}>
        <span style={{ color: INK3, minWidth: 56, fontFamily: "var(--font-mono)", fontSize: 10.5, textTransform: "uppercase", letterSpacing: "0.04em", paddingTop: 1 }}>{label}</span>
        <span style={{ fontFamily: "var(--font-mono)", color: accent ? SAGE : INK, fontWeight: 500, wordBreak: "break-word" }}>{value}</span>
      </div>
    );
  }

  /* ── The A4 document (native size) ───────────────────────────── */
  function Invoice({ api, inv, balance, animate }) {
    const { db } = api;
    const co = db.COMPANY;
    const liveClient = LIB.getClient(db, inv.clientId) || { name: inv.newClientName || "-", address: "", email: "", vat: "" };
    /* Identity blocks: print the snapshot frozen at issue time when present,
       else fall back to the live records (backward compatible with seeded invoices). */
    const seller = inv.seller || { name: co.name, vat: co.vat, siret: co.siret, address: co.address };
    const buyer = inv.buyer || { name: liveClient.name, vat: liveClient.vat, reg: liveClient.reg, address: liveClient.address };
    const tot = LIB.totals(inv);
    /* Credit notes store POSITIVE line rates; the sign is applied at render
       time only (line nets, totals block). fmtMoney renders the minus. */
    const sign = LIB.docSign(inv);
    /* Amount the pay-QR encodes: the caller-supplied balance when provided,
       else the computed outstanding balance, else the total. */
    const due = balance != null ? balance : (tot.balance != null ? tot.balance : tot.total);
    const isQuote = inv.kind === "quote";
    const zeroVat = inv.vatMode === "reverse" || inv.vatMode === "exempt";
    const vat = tot.vatBreakdown.filter(v => v.rate > 0);
    const vatMention = LIB.vatModeMention(inv.vatMode);
    /* e-invoice mention: ON for invoices AND credits (FacturX type 381 avoir),
       suppressed for quotes only */
    const einv = !isQuote && einvReady(inv, co, seller);
    /* "Nita generated this": when shown on screen (animate), the document
       assembles top-down — letterhead, rule, bill-to, items, totals — each
       major block faintly lifting into place. NEVER on the print path
       (animate is unset there) so the PDF/print capture is always solid, and
       gated on reduced-motion. `i` indexes the cascade. */
    const doAnim = animate && !REDUCE();
    const sec = (i) => doAnim
      ? { animation: `q-paper-block .5s cubic-bezier(.32,.72,0,1) ${0.06 + i * 0.07}s both` }
      : null;
    return (
      <div style={{ width: NATIVE, minHeight: NATIVE * RATIO, background: "#fff", color: INK, fontFamily: "var(--font-sans)", padding: `${PAD}px ${PAD}px 44px`, boxSizing: "border-box", display: "flex", flexDirection: "column" }}>
        {/* letterhead */}
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", ...sec(0) }}>
          <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
            {/* The letterhead mark is the SELLER's uploaded logo when set,
                else the typographic monogram — never the app's brand mark. */}
            {co.logo
              ? <img src={co.logo} alt="" style={{ width: 46, height: 46, borderRadius: 10, objectFit: "cover", display: "block" }} />
              : <Q.Avatar name={seller.name} size={46} square />}
            <div>
              <div style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 24, letterSpacing: "-0.01em" }}>{seller.name}</div>
              <div style={{ fontSize: 12.5, color: INK2, whiteSpace: "pre-line", lineHeight: 1.45, marginTop: 3 }}>{seller.address}</div>
            </div>
          </div>
          <div style={{ textAlign: "right" }}>
            <div style={{ fontFamily: "var(--font-serif)", fontSize: 30, fontWeight: 600, letterSpacing: "-0.01em", color: INK }}>{inv.kind === "credit" ? tf("paper.avoir", null, "Credit note") : inv.depositOf ? tf("paper.deposit", null, "Deposit invoice") : isQuote ? tf("paper.quote", null, t("man.type.quote")) : tf("paper.invoice", null, t("man.type.invoice"))}</div>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 600, marginTop: 4, color: GOLD }}>{inv.id}</div>
          </div>
        </div>

        <div style={{ height: 1.4, background: RULE, margin: "26px 0 24px", transformOrigin: "left", ...(doAnim ? { animation: "q-paper-block .5s cubic-bezier(.32,.72,0,1) .13s both" } : null) }} />

        {/* bill-to + meta */}
        <div style={{ display: "flex", gap: 32, ...sec(1) }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, letterSpacing: "0.12em", textTransform: "uppercase", color: INK3, marginBottom: 8 }}>{t("inv.billto")}</div>
            <div style={{ fontSize: 16, fontWeight: 700 }}>{buyer.name}</div>
            {liveClient.contact && <div style={{ fontSize: 13, color: INK2, marginTop: 2 }}>{liveClient.contact}</div>}
            <div style={{ fontSize: 12.5, color: INK2, whiteSpace: "pre-line", lineHeight: 1.5, marginTop: 4 }}>{buyer.address}</div>
            {buyer.vat && <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: INK3, marginTop: 6 }}>{t("cl.taxid")}: {buyer.vat}</div>}
            {buyer.reg && <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: INK3, marginTop: 3 }}>{t("cl.reg")}: {buyer.reg}</div>}
          </div>
          <div style={{ width: 150 }}>
            <Meta label={t("inv.issued")} value={window.fmtDate(inv.issued)} />
            <Meta label={isQuote ? tf("paper.validuntil", null, "Valid until") : t("inv.due")} value={window.fmtDate(inv.due)} />
            {inv.depositOf && <Meta label={tf("paper.deposit.of", { id: inv.depositOf }, "On quote " + inv.depositOf)} value={inv.depositOf} />}
            {inv.creditFor && <Meta label={tf("paper.creditfor", null, "Credits invoice")} value={inv.creditFor} />}
          </div>
        </div>

        {/* items */}
        <div style={{ marginTop: 28, ...sec(2) }}>
          <div style={{ display: "flex", padding: "0 0 9px", borderBottom: `1.4px solid ${RULE}`, fontFamily: "var(--font-mono)", fontSize: 10.5, letterSpacing: "0.06em", textTransform: "uppercase", color: INK3 }}>
            <div style={{ flex: 1 }}>{t("inv.item")}</div>
            <div style={{ width: 70, textAlign: "right" }}>{t("inv.qty")}</div>
            <div style={{ width: 100, textAlign: "right" }}>{t("inv.rate")}</div>
            <div style={{ width: 60, textAlign: "right" }}>{t("inv.vat")}</div>
            <div style={{ width: 110, textAlign: "right" }}>{t("inv.amount")}</div>
          </div>
          {inv.items.map((it, ri) => {
            const rate = zeroVat ? 0 : LIB.lineTax(it, inv);
            const disc = Number(it.discountPct) || 0;
            /* Lines cascade in as the paper draws (entrance only — they're
               static once landed and never touched on the print path). Capped
               so a long invoice doesn't leave the last rows lagging. */
            const rowAnim = doAnim ? { animation: `q-paper-block .42s cubic-bezier(.32,.72,0,1) ${0.3 + Math.min(ri, 12) * 0.028}s both` } : null;
            return (
              <div key={it.id} style={{ display: "flex", padding: "13px 0", borderBottom: `1px solid ${HAIR}`, fontSize: 13.5, alignItems: "baseline", ...rowAnim }}>
                <div style={{ flex: 1, paddingRight: 10 }}>
                  <div style={{ fontWeight: 600 }}>{it.description}</div>
                  {disc > 0 && <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: INK3, marginTop: 2 }}>{tf("paper.linediscount", { pct: disc }, "−" + disc + "% " + t("inv.linedisc").toLowerCase())}</div>}
                </div>
                <div style={{ width: 70, textAlign: "right", fontFamily: "var(--font-mono)", color: INK2 }}>{it.qty}{it.unit && it.unit !== "fixed" ? " " + unitAbbr(it.unit, it.qty) : ""}</div>
                <div style={{ width: 100, textAlign: "right", fontFamily: "var(--font-mono)", color: INK2 }}>{window.fmtMoney(it.rate, inv.currency)}</div>
                <div style={{ width: 60, textAlign: "right", fontFamily: "var(--font-mono)", color: INK3, fontSize: 12 }}>{rate}%</div>
                <div style={{ width: 110, textAlign: "right", fontFamily: "var(--font-mono)", fontWeight: 600 }}>{window.fmtMoney(sign * LIB.lineNet(it), inv.currency)}</div>
              </div>
            );
          })}
        </div>

        {/* totals */}
        <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 20, ...sec(5) }}>
          <div style={{ width: 280 }}>
            <Tot label={t("inv.subtotal")} value={window.fmtMoney(sign * tot.subtotal, inv.currency)} />
            {tot.discount > 0 && <Tot label={`${t("inv.discount")} ${inv.discountPct}%`} value={sign < 0 ? window.fmtMoney(tot.discount, inv.currency) : "−" + window.fmtMoney(tot.discount, inv.currency)} />}
            {vat.map(v => <Tot key={v.name + "_" + v.rate} label={v.name ? `${v.name} (${v.rate}%)` : `${t("inv.vat")} ${v.rate}%`} value={window.fmtMoney(sign * v.tax, inv.currency)} sub />)}
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginTop: 10, paddingTop: 12, borderTop: `1.4px solid ${RULE}` }}>
              <span style={{ fontSize: 14, fontWeight: 700 }}>{t("inv.total")}</span>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 22, fontWeight: 700, color: GOLD }}>{window.fmtMoney(sign * tot.total, inv.currency)}</span>
            </div>
            {inv.status === "partial" && (
              <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 12.5, color: INK2 }}>
                <span>{t("det.balance")}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontWeight: 600 }}>{window.fmtMoney(tot.balance, inv.currency)}</span>
              </div>
            )}
          </div>
        </div>

        {/* payment — only a true invoice is payable: a devis is not payable
            and an avoir must never print a pay-QR */}
        {inv.kind === "invoice" && <PaySection co={co} inv={inv} due={due} anim={doAnim ? sec(6) : null} sheen={doAnim} />}

        {/* notes + legal */}
        <div style={{ marginTop: 24, ...sec(7) }}>
          {(inv.notes) && <div style={{ fontSize: 12.5, color: INK2, lineHeight: 1.55 }}>{inv.notes}</div>}
          {vatMention && <div style={{ fontSize: 11, color: INK3, lineHeight: 1.5, marginTop: 8 }}>{t(vatMention)}</div>}
          {(inv.legal || co.legalMention || t("inv.legal.default")) && <div style={{ fontSize: 11, color: INK3, lineHeight: 1.5, marginTop: 8 }}>{inv.legal || co.legalMention || t("inv.legal.default")}</div>}
          {einv && <div style={{ fontFamily: "var(--font-mono)", fontSize: 9.5, letterSpacing: "0.08em", textTransform: "uppercase", color: INK3, marginTop: 10 }}>{tf("einv.profile", null, "FacturX e-invoice · EN 16931")}</div>}
        </div>

        <div style={{ flex: 1, minHeight: 16 }} />

        {/* seller footer */}
        <div style={{ marginTop: 24, paddingTop: 16, borderTop: `1px solid ${HAIR}`, display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16 }}>
          <FootMeta label={seller.name} value={co.email} />
          <FootMeta label={t("set.taxid")} value={seller.vat} />
          <FootMeta label={tf("paper.siret", null, "SIRET")} value={seller.siret} />
        </div>
      </div>
    );
  }
  function Tot({ label, value, sub }) {
    return (
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 7, fontSize: 12.5 }}>
        <span style={{ color: sub ? INK3 : INK2 }}>{label}</span>
        <span style={{ fontFamily: "var(--font-mono)", fontWeight: 500, color: INK }}>{value}</span>
      </div>
    );
  }

  /* ── Scaled wrapper (fits the doc to a given width) ────────────
     The layered shadow lives HERE, never on the A4 itself, so the document
     stays a pixel-faithful 210:297 sheet wherever it prints. */
  function Scaled({ api, inv, width, onClick, shadow = true, balance, animate }) {
    const ref = React.useRef(null);
    const [h, setH] = useState(NATIVE * RATIO);
    const scale = width / NATIVE;
    React.useLayoutEffect(() => {
      if (ref.current) setH(ref.current.offsetHeight);
    }, [inv, width]);
    return (
      <div onClick={onClick} style={{ width, height: h * scale, position: "relative", borderRadius: "var(--radius-lg)", overflow: "hidden", cursor: onClick ? "pointer" : "default", boxShadow: shadow ? "0 1px 2px rgb(40 30 15 / 0.10), 0 10px 26px rgb(40 30 15 / 0.13), 0 30px 70px rgb(40 30 15 / 0.18), 0 0 0 1px rgb(40 30 15 / 0.05)" : "none" }}>
        <div ref={ref} style={{ width: NATIVE, transform: `scale(${scale})`, transformOrigin: "top left", position: "absolute", top: 0, left: 0 }}>
          <Invoice api={api} inv={inv} balance={balance} animate={animate} />
        </div>
      </div>
    );
  }

  /* ── Fullscreen A4 viewer ────────────────────────────────────── */
  function Viewer({ api, inv, open, onClose, balance }) {
    const [shown, setShown] = useState(false);
    const [mounted, setMounted] = useState(open);
    /* Parallax tilt: as the sheet is scrolled inside the dark viewer it leans
       on its X axis like a physical page you're holding up to the light — the
       top edge tips away as you pull it up, settling flat at rest. Subtle
       (≤3.4°), gated on REDUCE(). */
    const [tilt, setTilt] = useState(0);
    const rootRef = React.useRef(null);
    const scrollRef = React.useRef(null);
    React.useEffect(() => {
      if (open) { setMounted(true); const r = setTimeout(() => setShown(true), 20); return () => clearTimeout(r); }
      else { setShown(false); const tm = setTimeout(() => setMounted(false), 280); return () => clearTimeout(tm); }
    }, [open]);
    /* Freeze the screens behind the fullscreen viewer (same lock as Q.Sheet);
       the dark gradient needs white status-bar glyphs while it is up. */
    Q.useScrollLock(mounted);
    Q.useStatusBarLight(mounted);
    /* Focus containment (V2-3): the viewer portals into #q-frame as a sibling
       of <main>, so app.jsx's composer/manual `inert` pattern never covers it —
       without a trap, Tab reaches the obscured screen controls (a keyboard
       user could invoke Send invisibly behind the dark overlay). Same intent
       as Q.Sheet's trap: cycle Tab through the viewer's own focusables, move
       focus in on open, restore it on close. */
    React.useEffect(() => {
      if (!mounted) return;
      const prev = document.activeElement;
      const focusables = () => {
        const root = rootRef.current;
        if (!root) return [];
        return Array.prototype.filter.call(
          root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
          el => !el.disabled && el.offsetParent !== null
        );
      };
      const first = focusables()[0];
      if (first) first.focus();
      const onKey = (e) => {
        if (e.key !== "Tab") return;
        const f = focusables();
        if (!f.length) return;
        const i = f.indexOf(document.activeElement);
        if (e.shiftKey) {
          if (i <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
        } else if (i === -1 || i === f.length - 1) {
          e.preventDefault(); f[0].focus();
        }
      };
      document.addEventListener("keydown", onKey, true);
      return () => {
        document.removeEventListener("keydown", onKey, true);
        if (prev && prev.focus && document.contains(prev)) prev.focus();
      };
    }, [mounted]);
    /* Reset the lean each time the viewer reopens so it always starts flat. */
    React.useEffect(() => { if (open) setTilt(0); }, [open]);
    const onScroll = () => {
      if (REDUCE()) return;
      const el = scrollRef.current;
      if (!el) return;
      const max = el.scrollHeight - el.clientHeight;
      const p = max > 0 ? Math.min(1, el.scrollTop / max) : 0;
      setTilt(p * 3.4);
    };
    if (!mounted || !inv) return null;
    const W = 358; // inner width within phone
    const overlay = (
      <div ref={rootRef} style={{ position: "absolute", inset: 0, zIndex: 140, display: "flex", flexDirection: "column", background: "linear-gradient(180deg, hsl(35 18% 28%), hsl(35 16% 21%) 55%, hsl(35 18% 15%))", opacity: shown ? 1 : 0, transition: "opacity .26s" }}>
        <div style={{ paddingTop: Q.SAFE_TOP, paddingLeft: 16, paddingRight: 12, paddingBottom: 12, display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "#fff", flex: 1, fontWeight: 600 }}>{inv.id}</span>
          <button onClick={() => api.printInvoice(inv.id)} aria-label={t("prev.download")} style={{ width: 40, height: 40, borderRadius: 999, border: "none", background: "rgb(255 255 255 / 0.16)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}><Icon name="download" size={18} /></button>
          <button onClick={onClose} aria-label={t("g.close")} style={{ width: 40, height: 40, borderRadius: 999, border: "none", background: "rgb(255 255 255 / 0.16)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}><Icon name="x" size={18} /></button>
        </div>
        <div ref={scrollRef} onScroll={onScroll} style={{ flex: 1, overflowY: "auto", padding: "8px 16px 32px", display: "flex", justifyContent: "center", perspective: 1400 }}>
          {/* Outer = entrance spring; inner = live scroll-driven parallax lean.
              Split so the resting tilt animates independently of the open pop. */}
          <div style={{ transform: shown ? "none" : "scale(0.82) translateY(36px)", opacity: shown ? 1 : 0, transition: REDUCE() ? "none" : "transform .38s var(--spring), opacity .24s", transformOrigin: "50% 30%" }}>
            <div style={{ transform: tilt ? `rotateX(${tilt}deg)` : "none", transformOrigin: "50% 0%", transition: REDUCE() ? "none" : "transform .3s ease-out", willChange: "transform" }}>
              <Scaled api={api} inv={inv} width={W} balance={balance} animate />
            </div>
          </div>
        </div>
      </div>
    );
    const frame = document.getElementById("q-frame");
    return frame ? ReactDOM.createPortal(overlay, frame) : overlay;
  }

  window.A4 = { Invoice, Scaled, Viewer, NATIVE, RATIO };
})();
