/* screen-invoices.jsx — Invoices list, detail, A4 PDF preview
 * → window.QScreens.{Invoices, InvoiceDetail, InvoicePreview}
 */
(function () {
  const { useState, useEffect } = React;
  const Q = window.Q, LIB = window.LIB, Icons = window.Icons;
  const { Icon } = Icons;
  const { AppBar, Card, Money, Kicker, Button, StatusPill, Avatar, EmptyState, IconButton, Sparkle, Badge, Toggle, AiKicker, Row, EntityLink, ReceiptThumb, ReceiptViewer } = Q;
  const t = window.t;

  const openManualEdit = (api, inv) => {
    const seed = window.QEditor.draftFromInvoice(inv);
    api.openManual(seed);
  };

  /* ════════════════════════════════════════════════════════════
     IN-APP PAYMENT CARD (mirror of the A4 payment section)
     ════════════════════════════════════════════════════════════ */
  function PaymentCard({ api, inv }) {
    const co = api.db.COMPANY;
    const pm = co.payment || {};
    const tot = LIB.totals(inv);
    const qr = LIB.payQR(co, inv, inv.status === "paid" ? 0 : tot.balance);
    const has = (pm.bank && pm.bank.enabled && co.iban) || (pm.card && pm.card.enabled && pm.card.link) || (pm.nita && pm.nita.enabled);
    if (!has) return null;
    const copy = (val, msg) => { try { navigator.clipboard && navigator.clipboard.writeText(val); } catch (e) {} Q.toast(msg || t("pay.copied"), "copy"); };
    return (
      <div>
        <Kicker style={{ marginBottom: 10, marginTop: 4 }}>{t("pay.title")}</Kicker>
        <Card pad={0}>
          <div style={{ display: "flex", gap: 12, padding: 16 }}>
            <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 14, minWidth: 0 }}>
              {pm.bank && pm.bank.enabled && co.iban && (
                <PMethod icon="bank" title={t("pay.bank")} onClick={() => copy(co.iban.replace(/\s/g, ""))}>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-2)", wordBreak: "break-all" }}>{co.iban}</span>
                </PMethod>
              )}
              {pm.card && pm.card.enabled && pm.card.link && (
                <PMethod icon="card" title={t("pay.card")} onClick={() => copy(pm.card.link)}>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-primary)" }}>{pm.card.link}</span>
                </PMethod>
              )}
              {pm.nita && pm.nita.enabled && (
                <PMethod icon="wallet" title={t("pay.nita")} sage onClick={() => copy(pm.nita.handle || pm.nita.phone)}>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-2)" }}>{pm.nita.handle || pm.nita.phone}</span>
                </PMethod>
              )}
            </div>
            {qr && (
              <div style={{ flexShrink: 0, textAlign: "center" }}>
                <div className="q-qr-flip q-sheen" style={{ padding: 8, background: "#fff", borderRadius: 12, border: "1.4px solid var(--color-border-strong)", boxShadow: "0 1px 2px hsl(35 16% 5% / 0.08), 0 6px 16px hsl(35 16% 5% / 0.10)" }}>
                  <Q.QR value={qr.value} size={92} color="hsl(35 20% 12%)" />
                </div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.04em", color: qr.method === "nita" ? "var(--color-primary)" : "var(--color-text-3)", marginTop: 6, maxWidth: 96, lineHeight: 1.3 }}>{t("pay.scan")}</div>
              </div>
            )}
          </div>
        </Card>
      </div>
    );
  }
  function PMethod({ icon, title, children, onClick, sage }) {
    return (
      <button onClick={onClick} className="q-tap" style={{ display: "flex", gap: 12, alignItems: "flex-start", background: "none", border: "none", padding: 0, textAlign: "left", cursor: "pointer", width: "100%" }}>
        <div style={{ width: 30, height: 30, borderRadius: 8, background: sage ? "var(--color-primary-muted)" : "var(--color-surface-1)", color: sage ? "var(--color-primary)" : "var(--color-text-2)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name={icon} size={15} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: "var(--color-text-1)", display: "flex", alignItems: "center", gap: 6 }}>{title}<Icon name="copy" size={12} color="var(--color-text-3)" /></div>
          <div style={{ marginTop: 2 }}>{children}</div>
        </div>
      </button>
    );
  }

  /* ════════════════════════════════════════════════════════════
     A4 PDF PREVIEW (post-confirm)
     ════════════════════════════════════════════════════════════ */
  function InvoicePreview({ api, id, fresh }) {
    const { db } = api;
    const inv = db.INVOICES.find(i => i.id === id);
    const [gen, setGen] = useState(!!fresh);
    const [viewer, setViewer] = useState(false);
    useEffect(() => { if (fresh) { const t1 = setTimeout(() => setGen(false), 1150); return () => clearTimeout(t1); } }, [fresh]);
    if (!inv) return <div><AppBar title={t("man.type.invoice")} onBack={() => api.back()} /><EmptyState icon="invoice" title={t("g.notfound")} body={t("g.trysearch")} /></div>;
    const tot = LIB.totals(inv);

    if (gen) {
      return (
        <div style={{ height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 18, padding: 40 }}>
          <div style={{ position: "relative", width: 60, height: 60 }}>
            <div style={{ position: "absolute", inset: 0, border: "3px solid var(--color-surface-2)", borderTopColor: "var(--color-primary)", borderRadius: "50%", animation: "q-spin 0.8s linear infinite" }} />
            <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center" }}><Sparkle size={20} /></div>
          </div>
          <div className="q-shimmer-text" style={{ fontSize: 16, fontWeight: 600 }}>{t("prev.generating")}</div>
        </div>
      );
    }

    return (
      <div style={{ paddingBottom: 30 }}>
        <AppBar title={inv.id} onBack={() => api.back()}
          right={<IconButton name="edit" label={t("prev.edit")} onClick={() => openManualEdit(api, inv)} />} />
        {fresh && (
          /* sequence: the paper (q-paper-in, .45s) settles first, THEN this
             "ready" chip scales in — so the document lands and is acknowledged,
             rather than both firing at frame 0 and competing for the eye. */
          <div className="q-scalein" style={{ margin: "0 16px 14px", padding: "12px 16px", background: "var(--color-primary-muted)", border: "1.4px solid var(--color-primary)", borderRadius: 12, display: "flex", alignItems: "center", gap: 10, animationDelay: ".42s" }}>
            <div style={{ width: 30, height: 30, borderRadius: 8, background: "var(--color-primary)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name="check" size={17} color="var(--color-primary-fg)" /></div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 700 }}>{t("prev.ready.kicker")}</div>
              <div style={{ fontSize: 12, color: "var(--color-text-2)" }}>{inv.id} · {window.fmtMoney(tot.total, inv.currency)}</div>
            </div>
          </div>
        )}
        {/* A4 document, scaled (q-paper-* hooks: entrance styling owned by
            index.html). q-paper-stack must sit on the paper wrapper itself:
            on the full-width column its white backing sheets span edge to
            edge and read as broken overlapping panes. */}
        <div style={{ padding: "0 16px", display: "flex", flexDirection: "column", alignItems: "center" }}>
          <div className="q-paper-in q-paper-stack" style={{ position: "relative" }}>
            {window.A4 && <window.A4.Scaled api={api} inv={inv} width={354} onClick={() => setViewer(true)} />}
            {/* 34px visual circle, 40x40 hit area: transparent padding + negative margin (zero layout shift) */}
            <button onClick={() => setViewer(true)} aria-label={t("det.viewpdf")} style={{ position: "absolute", right: 10, bottom: 10, width: 34, height: 34, padding: 3, margin: -3, boxSizing: "content-box", borderRadius: 999, border: "none", background: "rgb(20 16 10 / 0.62)", backgroundClip: "content-box", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", backdropFilter: "blur(4px)" }}><Icon name="expand" size={16} /></button>
          </div>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 10, display: "flex", alignItems: "center", gap: 6 }}><Icon name="file" size={12} />A4 · {inv.id}.pdf · {t("det.viewpdf")}</div>
        </div>
        {/* actions */}
        <div style={{ padding: "18px 16px 0", display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <SquareAction icon="download" label={t("prev.download")} onClick={() => api.printInvoice(inv.id)} />
          <SquareAction icon="link" label={t("prev.copylink")} onClick={() => { try { navigator.clipboard && navigator.clipboard.writeText(LIB.payLink(inv)); } catch (e) {} Q.toast(t("prev.linkcopied"), "link"); }} />
          <SquareAction icon="send" label={t("prev.send")} primary onClick={() => api.openSend(inv.id, "send")} />
        </div>
        <div style={{ padding: "12px 16px 0", display: "flex", flexDirection: "column", gap: 4 }}>
          {api.openPayView && <Button variant="ghost" size="md" full icon="eye" onClick={() => api.openPayView(inv.id)}>{t("payview.open")}</Button>}
          <Button variant="ghost" size="md" full onClick={() => api.switchTab("invoices")}>{t("prev.done")}</Button>
        </div>
        {window.A4 && <window.A4.Viewer api={api} inv={inv} open={viewer} onClose={() => setViewer(false)} />}
      </div>
    );
  }
  function SquareAction({ icon, label, onClick, primary }) {
    const [pressed, setPressed] = useState(false);
    return (
      <button onClick={onClick} className="q-tap"
        onPointerDown={() => setPressed(true)}
        onPointerUp={() => setPressed(false)}
        onPointerLeave={() => setPressed(false)}
        style={{
          display: "flex", flexDirection: "column", alignItems: "center", gap: 8, padding: "16px 6px", cursor: "pointer",
          borderRadius: 12, border: primary ? "none" : "1.4px solid var(--color-border-strong)",
          background: primary ? "var(--color-text-1)" : "var(--color-surface-0)",
          color: primary ? "var(--color-bg)" : "var(--color-text-1)",
          /* press vocabulary: 0.96 scale + a 1-tier shadow lift on the hero
             "send" tile (primary), narrowed transition (no "all" anti-pattern) */
          transform: pressed ? "scale(0.96)" : "scale(1)",
          boxShadow: primary && !pressed ? "0 2px 8px hsl(35 16% 5% / 0.14)" : "none",
          transition: "transform .12s cubic-bezier(.32,.72,0,1), box-shadow .12s ease",
        }}>
        <Icon name={icon} size={20} />
        <span style={{ fontSize: 12, fontWeight: 600 }}>{label}</span>
      </button>
    );
  }

  /* ════════════════════════════════════════════════════════════
     INVOICE DETAIL
     ════════════════════════════════════════════════════════════ */
  function InvoiceDetail({ api, id }) {
    const { db, setDb, today } = api;
    const [receiptView, setReceiptView] = useState(null);
    const [burst, setBurst] = useState(false);
    const [fullTrail, setFullTrail] = useState(false);
    const inv = db.INVOICES.find(i => i.id === id);
    if (!inv) return <div><AppBar title={t("man.type.invoice")} onBack={() => api.back()} /><EmptyState icon="invoice" title={t("g.notfound")} body={t("g.trysearch")} /></div>;
    const client = LIB.getClient(db, inv.clientId) || { name: inv.newClientName || "-", email: "" };
    const tot = LIB.totals(inv);
    const eff = LIB.effectiveStatus(inv, today);
    const isQuote = inv.kind === "quote";
    const isCredit = inv.kind === "credit";

    const markPaid = () => {
      setDb(d => ({
        ...d, INVOICES: d.INVOICES.map(x => x.id === id ? LIB.recordPayment(x, { amount: LIB.totals(x).balance, at: today, method: "other" }) : x),
        ACTIVITY: [LIB.activityEvent("paid", { client: client.name, id: inv.id }, inv.id), ...d.ACTIVITY],
      }));
      Q.toast(t("toast.markedPaid", { id: inv.id }), "checkCircle", "var(--color-success)");
      setBurst(true);
    };
    const duplicate = () => {
      const number = LIB.nextNumber(db.COMPANY, "invoice", today);
      const copy = { ...inv, id: number, kind: "invoice", status: "draft", paid: 0, paidOn: null, issued: today, due: (() => { const d = new Date(today); d.setDate(d.getDate() + LIB.daysBetween(inv.issued, inv.due)); return d.toISOString(); })(), items: inv.items.map(it => ({ ...it, id: "li" + Math.random().toString(36).slice(2, 7) })), audit: [LIB.auditEvent("duplicated", { from: inv.id })] };
      /* duplicates stay linkless: strip every link/state field */
      delete copy.recurrence; delete copy.recurrenceOf; delete copy.chase; delete copy.quoteId; delete copy.depositOf; delete copy.depositPct; delete copy.creditFor; delete copy.creditedBy; delete copy.convertedTo; delete copy.payments;
      setDb(d => ({ ...d, COMPANY: { ...d.COMPANY, nextSeq: d.COMPANY.nextSeq + 1 }, INVOICES: [copy, ...d.INVOICES] }));
      api.go("invoice", { id: number });
      Q.toast(t("toast.duplicatedAs", { number }), "copy");
    };
    const convert = () => {
      const r = LIB.convertQuote(db, inv.id, today);
      setDb(() => r.db);
      Q.toast(t("toast.convertedTo", { number: r.invoiceId }));
      api.go("invoice", { id: r.invoiceId });
    };
    const accept = () => {
      setDb(d => LIB.acceptQuote(d, id, today));
      Q.toast(t("toast.accepted"), "check");
    };
    const doIssueCredit = () => {
      const r = LIB.issueCredit(db, inv.id, today);
      setDb(() => r.db);
      Q.toast(t("toast.creditCreated", { id: r.creditId }));
      api.go("invoice", { id: r.creditId });
    };

    const dueInfo = LIB.dueLabel(inv, today);
    const hasAudit = !!(inv.audit && inv.audit.length);
    const timeline = hasAudit ? LIB.timelineFromAudit(inv) : buildTimeline(inv, client, today);
    /* FacturX readiness — LIB.einvoiceCheck(db, inv) -> { ready, checks:[{ id, ok }] }.
       Owned by lib.jsx; render nothing until it ships. */
    const einv = LIB.einvoiceCheck ? LIB.einvoiceCheck(db, inv) : null;
    const editable = inv.status === "draft" || isQuote;
    const auditHas = (ty) => !!(inv.audit && inv.audit.some(e => e.type === ty));
    const deps = isQuote ? LIB.quoteDeposits(db, inv.id) : [];
    const planned = (inv.kind === "invoice" && inv.chase && inv.chase.enabled) ? LIB.chaseSchedule(inv, db.COMPANY, today).filter(s => !s.sent) : [];
    const canCredit = inv.kind === "invoice" && inv.status !== "draft" && !inv.creditedBy;
    const heroAmount = isCredit ? LIB.docSign(inv) * tot.total : (inv.status === "paid" || isQuote) ? tot.total : tot.balance;

    return (
      <div style={{ paddingBottom: 40 }}>
        <AppBar title={inv.id} onBack={() => api.back()}
          right={<>
            {editable && <IconButton name="edit" label={t("g.edit")} onClick={() => openManualEdit(api, inv)} />}
            <IconButton name="eye" label={t("det.viewpdf")} onClick={() => api.go("invoice-preview", { id })} />
          </>} />

        <div style={{ padding: "4px 16px 0", display: "flex", flexDirection: "column", gap: 12 }}>
          {/* hero amount */}
          <Card pad={20} style={{ position: "relative" }}>
            {burst && <Q.PaidBurst />}
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
              <StatusPill status={eff} solid />
              {/* the pill carries the state; overdue keeps only the magnitude here */}
              {dueInfo && eff !== "paid" && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, color: dueInfo.tone === "accent" ? "var(--color-accent)" : dueInfo.tone === "warning" ? "var(--color-warning)" : "var(--color-text-3)" }}>{eff === "overdue" ? t("det.overdue.days", { d: Math.abs(LIB.daysBetween(today, inv.due)) }) : dueInfo.text}</span>}
            </div>
            <Kicker>{isCredit ? t("man.title.credit") : isQuote ? t("quoteview.amount") : inv.status === "paid" ? t("inv.totalPaid") : t("det.balance")}</Kicker>
            <div style={{ marginTop: 6 }}>
              <Q.AnimatedNumber amount={heroAmount} currency={inv.currency} color="var(--color-gold)" animKey={inv.id}
                style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 44, lineHeight: 1.05, letterSpacing: "-0.02em" }} />
            </div>
            {inv.status === "partial" && <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 6 }}>{t("det.partial", { paid: window.fmtMoney(tot.paid, inv.currency), total: window.fmtMoney(tot.total, inv.currency) })}</div>}
            {inv.status === "paid" && inv.paidOn && <div style={{ fontSize: 13, color: "var(--color-success)", marginTop: 6, fontWeight: 600 }}>{t("det.paid_on", { date: window.fmtDate(inv.paidOn) })}</div>}
            {isQuote && <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 6 }}>{t("quote.validuntil", { date: window.fmtDate(inv.due) })}</div>}
            {isQuote && inv.convertedTo && (
              <div style={{ marginTop: 10 }}>
                <EntityLink kind="invoice" mono name={inv.convertedTo} sub={t("det.convertedto")} onClick={() => api.go("invoice", { id: inv.convertedTo })} />
              </div>
            )}
            {/* milestone stepper: sent / viewed / paid, derived from audit (never sage).
                No pill backgrounds — reached steps carry the tone check, unreached
                steps stay hollow, so "Paid" can never read as the current status. */}
            {inv.kind === "invoice" && inv.status !== "draft" && (
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 12 }}>
                {(() => {
                  /* a paid / partial / non-draft invoice was necessarily SENT —
                     and a paid one was necessarily VIEWED — even when the demo
                     record carries no audit trail. Derive the reached state from
                     status so "Paid" can never light up while "Sent" reads hollow. */
                  const wasSent = auditHas("sent") || eff === "paid" || eff === "partial" || inv.status === "paid" || inv.status === "partial" || inv.status === "sent";
                  const wasPaid = eff === "paid" || auditHas("paid") || inv.status === "paid";
                  const wasViewed = auditHas("viewed") || wasPaid;
                  return [
                  { label: t("st.sent"), on: wasSent, tone: "var(--color-info)" },
                  { label: t("det.t.viewed"), on: wasViewed, tone: "var(--color-info)" },
                  { label: t("st.paid"), on: wasPaid, tone: "var(--color-success)" },
                ];
                })().map((s, i) => (
                  <React.Fragment key={s.label}>
                    {i > 0 && <span aria-hidden="true" style={{ width: 12, height: 1, background: "var(--color-border)", flexShrink: 0 }} />}
                    <span style={{ display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0 }}>
                      {s.on
                        ? <span className="q-pop" style={{ display: "inline-flex", animationDelay: (0.06 + i * 0.05) + "s" }}><Icon name="check" size={12} color={s.tone} /></span>
                        : <span style={{ width: 8, height: 8, borderRadius: "50%", border: "1.4px solid var(--color-border-strong)", boxSizing: "border-box", flexShrink: 0 }} />}
                      <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600, letterSpacing: "0.10em", textTransform: "uppercase", color: s.on ? "var(--color-text-1)" : "var(--color-text-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{s.label}</span>
                    </span>
                  </React.Fragment>
                ))}
              </div>
            )}
            {inv.recurrence && (
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 12, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-2)" }}>
                <Icon name="repeat" size={13} />
                <span>{t("rec.detail." + inv.recurrence.every, { date: window.fmtDate(inv.recurrence.nextRun, "short") })}</span>
              </div>
            )}

            <div onClick={() => inv.clientId && api.go("client", { id: inv.clientId })} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 16, paddingTop: 16, borderTop: "1px solid var(--color-border)", cursor: inv.clientId ? "pointer" : "default" }}>
              <Avatar name={client.name} size={38} square />
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 14, fontWeight: 600 }}>{client.name}</div>
                <div style={{ fontSize: 12, color: "var(--color-text-3)" }}>{client.email || t("det.noemail")}</div>
              </div>
              {inv.clientId && <Icon name="chevronRight" size={18} color="var(--color-text-3)" />}
            </div>
          </Card>

          {/* actions: tier 1 = the ONE 48px ink CTA per state; tier 2 = uniform
              40px outline rows. Convert keeps the AI voice (true Nita action). */}
          {isCredit ? (
            /* credits: never markPaid / Record / Remind */
            inv.status === "draft" ? (
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                <Button variant="primary" size="lg" full icon="send" onClick={() => api.openSend(id, "send")}>{t("det.send")}</Button>
                <Button variant="outline" size="md" full icon="edit" onClick={() => openManualEdit(api, inv)}>{t("g.edit")}</Button>
              </div>
            ) : (
              <Button variant="outline" size="md" full icon="eye" onClick={() => api.go("invoice-preview", { id })}>{t("det.viewpdf")}</Button>
            )
          ) : isQuote ? (
            eff === "quote" ? (
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                <Button variant="primary" size="lg" full icon="send" onClick={() => api.openSend(id, "send")}>{t("det.send")}</Button>
                <div style={{ display: "flex", gap: 10 }}>
                  <Button variant="outline" size="md" full icon="check" onClick={accept}>{t("det.accept")}</Button>
                  {api.openQuoteView && <Button variant="outline" size="md" full icon="eye" onClick={() => api.openQuoteView(id)}>{t("payview.open")}</Button>}
                </div>
              </div>
            ) : eff === "accepted" ? (
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                {!inv.convertedTo && <Button variant="primary" size="lg" full icon="arrowRight" onClick={convert}>{t("det.convert")}</Button>}
                {(!inv.convertedTo || api.openQuoteView) && (
                  <div style={{ display: "flex", gap: 10 }}>
                    {!inv.convertedTo && <Button variant="outline" size="md" full icon="coins" onClick={() => api.openDeposit(id)}>{t("det.requestdeposit")}</Button>}
                    {api.openQuoteView && <Button variant="outline" size="md" full icon="eye" onClick={() => api.openQuoteView(id)}>{t("payview.open")}</Button>}
                  </div>
                )}
              </div>
            ) : (
              /* expired / declined */
              <Button variant="outline" size="md" full icon="copy" onClick={duplicate}>{t("det.duplicate")}</Button>
            )
          ) : inv.status === "paid" ? (
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              <div style={{ display: "flex", gap: 10 }}>
                <Button variant="outline" size="md" full icon="eye" onClick={() => api.go("invoice-preview", { id })}>{t("det.viewpdf")}</Button>
                <Button variant="outline" size="md" full icon="copy" onClick={duplicate}>{t("det.duplicate")}</Button>
              </div>
              {canCredit && <Button variant="outline" size="md" full icon="refresh" onClick={doIssueCredit}>{t("det.issuecredit")}</Button>}
            </div>
          ) : inv.status === "draft" ? (
            /* DRAFT: never sent → the ONE primary is Send. A draft cannot be
               paid (money-integrity: no markPaid / Record on something the
               client has never received). Edit + duplicate stay secondary. */
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              <Button variant="primary" size="lg" full icon="send" onClick={() => api.openSend(id, "send")}>{t("det.send")}</Button>
              <div style={{ display: "flex", gap: 10 }}>
                <Button variant="outline" size="md" full icon="edit" onClick={() => openManualEdit(api, inv)}>{t("g.edit")}</Button>
                <Button variant="outline" size="md" full icon="copy" onClick={duplicate}>{t("det.duplicate")}</Button>
              </div>
            </div>
          ) : (
            /* SENT / PARTIAL / OVERDUE: the primary CTA follows the get-paid
               funnel, not "mark paid". Overdue → chase (Send reminder);
               sent/partial → Record payment. Mark-as-paid is demoted to an
               outline so it can never be the loudest action on a live debt. */
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              {eff === "overdue"
                ? <Button variant="primary" size="lg" full icon="bell" onClick={() => api.openSend(id, "remind")}>{t("det.remind")}</Button>
                : <Button variant="primary" size="lg" full icon="card" onClick={() => api.openPayment(id)}>{t("det.record")}</Button>}
              <div style={{ display: "flex", gap: 10 }}>
                <Button variant="outline" size="md" full icon="check" onClick={markPaid}>{t("det.markpaid")}</Button>
                {eff === "overdue"
                  ? <Button variant="outline" size="md" full icon="card" onClick={() => api.openPayment(id)}>{t("det.record")}</Button>
                  : <Button variant="outline" size="md" full icon="send" onClick={() => api.openSend(id, "send")}>{t("det.send")}</Button>}
              </div>
              <div style={{ display: "flex", gap: 10 }}>
                <Button variant="outline" size="md" full icon="copy" onClick={duplicate}>{t("det.duplicate")}</Button>
                {canCredit && <Button variant="outline" size="md" full icon="refresh" onClick={doIssueCredit}>{t("det.issuecredit")}</Button>}
              </div>
              {api.openPayView && <Button variant="outline" size="md" full icon="eye" onClick={() => api.openPayView(id)}>{t("payview.open")}</Button>}
            </div>
          )}

          {/* autopilot chase toggle (AI feature: sage is correct here) */}
          {inv.kind === "invoice" && (eff === "sent" || eff === "partial" || eff === "overdue") && (
            <Card pad={14}>
              <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                <Sparkle size={16} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 600 }}>{t("chase.toggle")}</div>
                  <div style={{ fontSize: 12, color: "var(--color-text-3)", marginTop: 2, lineHeight: 1.45 }}>{t("chase.toggle.sub")}</div>
                </div>
                <Toggle checked={!!(inv.chase && inv.chase.enabled)} label={t("chase.toggle")} onChange={(next) => {
                  setDb(d => ({
                    ...d,
                    INVOICES: d.INVOICES.map(x => x.id === id
                      ? LIB.pushAudit({ ...x, chase: { enabled: next, step: (x.chase && x.chase.step) || 0 } }, LIB.auditEvent(next ? "chase.on" : "chase.off"))
                      : x),
                  }));
                  Q.toast(t(next ? "chase.on.toast" : "chase.off.toast"), "sparkle");
                }} />
              </div>
            </Card>
          )}

          {/* deposit invoices raised on this quote */}
          {isQuote && deps.length > 0 && (
            <div>
              <Kicker style={{ marginBottom: 10, marginTop: 4 }}>{t("det.deposits")}</Kicker>
              <Card pad={0} style={{ overflow: "hidden" }}>
                {deps.map((dep, i) => <InvoiceRow key={dep.id} api={api} inv={dep} last={i === deps.length - 1} />)}
              </Card>
            </div>
          )}

          {/* PDF mini + document links (credit/source, quote provenance, recurring template) */}
          <Card pad={0} style={{ overflow: "hidden" }}>
            <div onClick={() => api.go("invoice-preview", { id })} style={{ padding: "12px 16px", display: "flex", alignItems: "center", gap: 12, cursor: "pointer" }}>
              <div style={{ width: 38, height: 46, borderRadius: 6, background: "#fff", border: "1px solid var(--color-border-strong)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, boxShadow: "0 2px 6px rgb(40 30 15 / 0.08)" }}>
                <Icon name="file" size={18} color="var(--color-text-3)" />
              </div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 14, fontWeight: 600 }}>{inv.id}.pdf</div>
                <div style={{ fontSize: 12, color: "var(--color-text-3)" }}>A4 · {inv.items.length === 1 ? t("det.pdf.line", { count: inv.items.length }) : t("det.pdf.lines", { count: inv.items.length })} · {t("det.viewpdf")}</div>
              </div>
              <Icon name="chevronRight" size={18} color="var(--color-text-3)" />
            </div>
            {(inv.creditFor || inv.creditedBy || inv.quoteId || inv.recurrenceOf) && (
              <div style={{ borderTop: "1px solid var(--color-border)", padding: "12px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
                {inv.creditFor && <EntityLink kind="invoice" mono name={inv.creditFor} sub={t("det.creditfor")} onClick={() => api.go("invoice", { id: inv.creditFor })} />}
                {inv.creditedBy && <EntityLink kind="invoice" mono name={inv.creditedBy} sub={t("det.creditedby")} onClick={() => api.go("invoice", { id: inv.creditedBy })} />}
                {inv.quoteId && <EntityLink kind="invoice" mono name={inv.quoteId} sub={t("det.convertedfrom")} onClick={() => api.go("invoice", { id: inv.quoteId })} />}
                {inv.recurrenceOf && <EntityLink kind="invoice" mono name={inv.recurrenceOf} sub={t("rec.source")} onClick={() => api.go("invoice", { id: inv.recurrenceOf })} />}
              </div>
            )}
          </Card>

          {/* e-invoicing readiness (FacturX) — semantic state colors only */}
          {einv && Array.isArray(einv.checks) && (
            <Card pad={16}>
              <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: einv.checks.length ? 10 : 0 }}>
                <Icon name={einv.ready ? "checkCircle" : "alert"} size={17} color={einv.ready ? "var(--color-success)" : "var(--color-warning)"} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 600 }}>{t("einv.title")}</div>
                  <div style={{ fontSize: 12, color: "var(--color-text-3)", marginTop: 2 }}>{t(einv.ready ? "einv.ready" : "einv.notready")}</div>
                </div>
              </div>
              <div style={{ display: "flex", flexDirection: "column" }}>
                {einv.checks.map(c => (
                  <div key={c.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 0" }}>
                    <Icon name={c.ok ? "check" : "x"} size={13} color={c.ok ? "var(--color-success)" : "var(--color-warning)"} />
                    <span style={{ fontSize: 12, color: "var(--color-text-2)" }}>{t("einv.check." + c.id)}</span>
                  </div>
                ))}
              </div>
            </Card>
          )}

          {/* payment methods (credits never show a pay path) */}
          {!isCredit && <PaymentCard api={api} inv={inv} />}

          {/* timeline (audit trail when the invoice carries one) — newest 4 shown,
              older events behind a local expander (timeline is newest-last) */}
          <div>
            <Kicker style={{ marginBottom: 10, marginTop: 4 }}>{t(hasAudit ? "det.audit" : "det.timeline")}</Kicker>
            {timeline.length > 5 && (
              <button className="q-tap" onClick={() => setFullTrail(v => !v)} style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: 40, marginBottom: 10, background: "none", border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600, color: "var(--color-text-2)", fontFamily: "var(--font-sans)" }}>
                {fullTrail ? t("g.showless") : t("g.viewall", { count: timeline.length - 4 })}
              </button>
            )}
            <div style={{ position: "relative", paddingLeft: 6 }}>
              {(timeline.length > 5 && !fullTrail ? timeline.slice(timeline.length - 4) : timeline).map((ev, i, arr) => (
                <div key={i} style={{ display: "flex", gap: 12, paddingBottom: i < arr.length - 1 ? 16 : 0, position: "relative" }}>
                  {i < arr.length - 1 && <div style={{ position: "absolute", left: 6, top: 16, bottom: 0, width: 1.5, background: "var(--color-border)" }} />}
                  <div style={{ width: 13, height: 13, borderRadius: "50%", background: ev.color, marginTop: 2, flexShrink: 0, zIndex: 1, border: "2.5px solid var(--color-bg)", boxSizing: "content-box", marginLeft: -1 }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 13, color: "var(--color-text-1)", fontWeight: 500 }}>{ev.text}</div>
                    <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 2 }}>{ev.date}</div>
                    {ev.receipt && <div style={{ marginTop: 8 }}><ReceiptThumb receipt={ev.receipt} size={38} onClick={() => setReceiptView(ev.receipt)} /></div>}
                  </div>
                </div>
              ))}
              {/* future auto-reminders planned by Nita (ghost nodes, calm) */}
              {planned.length > 0 && (
                <div style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 10 }}>
                  <AiKicker>{t("chase.planned.kicker")}</AiKicker>
                  {planned.map(s => (
                    <div key={s.step} style={{ display: "flex", alignItems: "center", gap: 10 }}>
                      <div style={{ width: 26, height: 26, borderRadius: 8, border: "1.4px dashed var(--color-border-strong)", color: "var(--color-text-3)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                        <Icon name="bell" size={13} />
                      </div>
                      <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{t("chase.scheduled." + s.tone, { date: window.fmtDate(s.effectiveAt, "short") })}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        </div>
        <ReceiptViewer open={!!receiptView} receipt={receiptView} onClose={() => setReceiptView(null)} />
      </div>
    );
  }

  function buildTimeline(inv, client, today) {
    const evs = [{ text: t("det.t.created"), date: window.fmtDate(inv.issued), color: "var(--color-text-3)" }];
    if (inv.status !== "draft" && inv.kind !== "quote") {
      evs.push({ text: t("det.t.sent", { email: client.email || client.name }), date: window.fmtDate(inv.issued), color: "var(--color-info)" });
    }
    if (inv.status === "partial") evs.push({ text: t("inv.partial.received", { amount: window.fmtMoney(inv.paid, inv.currency) }), date: window.fmtDate(inv.due), color: "var(--color-info)" });
    if (LIB.effectiveStatus(inv, today) === "overdue") evs.push({ text: t("inv.timeline.overdue"), date: window.fmtDate(inv.due), color: "var(--color-accent)" });
    if (inv.status === "paid") evs.push({ text: t("det.t.paid"), date: window.fmtDate(inv.paidOn || inv.due), color: "var(--color-success)" });
    return evs;
  }

  /* ════════════════════════════════════════════════════════════
     INVOICES LIST
     ════════════════════════════════════════════════════════════ */
  function InvoiceRow({ api, inv, onClick, dup, last }) {
    const { db, setDb, today } = api;
    const client = LIB.getClient(db, inv.clientId) || { name: inv.newClientName || "-" };
    const project = client.projects && inv.projectId ? client.projects.find(p => p.id === inv.projectId) : null;
    const tot = LIB.totals(inv);
    const eff = LIB.effectiveStatus(inv, today);
    const markPaid = () => {
      setDb(d => ({
        ...d, INVOICES: d.INVOICES.map(x => x.id === inv.id ? LIB.recordPayment(x, { amount: LIB.totals(x).balance, at: today, method: "other" }) : x),
        ACTIVITY: [LIB.activityEvent("paid", { client: client.name, id: inv.id }, inv.id), ...d.ACTIVITY],
      }));
      Q.toast(t("toast.markedPaid", { id: inv.id }), "checkCircle", "var(--color-success)");
    };
    const actions = dup ? [] :
      inv.kind === "quote" ? [] :
      inv.kind === "credit" ? (inv.status === "draft" ? [
        { icon: "edit", label: t("g.edit"), tone: "neutral", onPress: () => openManualEdit(api, inv) },
      ] : []) :
      (eff === "sent" || eff === "overdue" || eff === "partial") ? [
        { icon: "bell", label: t("det.remind"), tone: "accent", onPress: () => api.openSend(inv.id, "remind") },
        { icon: "check", label: t("det.markpaid"), tone: "success", onPress: markPaid },
      ] :
      inv.status === "draft" ? [
        { icon: "edit", label: t("g.edit"), tone: "neutral", onPress: () => openManualEdit(api, inv) },
      ] : [];
    return (
      <Q.SwipeRow actions={actions} last={last} onClick={onClick || (() => api.go("invoice", { id: inv.id }))} style={{ background: "var(--color-surface-0)" }}>
        <Avatar name={client.name} size={38} square />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{client.name}</div>
          {/* project lives in the meta line, not a pill (one mono data voice) */}
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{inv.id} · {window.fmtDate(inv.issued, "short")}{project ? ` · ${project.name}` : ""}</div>
        </div>
        <div style={{ textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
          {/* overdue rows tint the amount terracotta so the money — not just the
              pill — signals the debt at a glance; everything else stays gold */}
          <Money amount={LIB.docSign(inv) * tot.total} currency={inv.currency} size={14} cents={false} color={!dup && eff === "overdue" ? "var(--color-accent)" : "var(--color-gold)"} />
          {dup ? <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, fontWeight: 600, color: "var(--color-primary)" }}><Icon name="copy" size={13} />{t("det.duplicate")}</span> : <StatusPill status={eff} solid />}
          {!dup && inv.recurrence && <Badge tone="neutral">{t("rec.badge")}</Badge>}
          {!dup && inv.recurrenceOf && inv.status === "draft" && <Badge tone="primary" ai>{t("rec.auto.badge")}</Badge>}
        </div>
      </Q.SwipeRow>
    );
  }

  const SORTS = [
    { id: "recent", key: "sort.recent" },
    { id: "amount", key: "sort.amount" },
    { id: "client", key: "sort.client" },
  ];

  const VALID_FILTERS = ["all", "draft", "sent", "paid", "overdue", "quotes", "credits"];

  function Invoices({ api }) {
    const { db, today } = api;
    const dup = api.dup;
    /* seed the status filter from the route param so the Home digest CTA
       (goTab('invoices', undefined, {filter:'overdue'})) lands pre-filtered
       instead of dumping the user into the full unfiltered book. Guarded to
       a known filter id; ignored in the duplicate-picker flow. */
    const rawParamFilter = api.route && api.route.params && api.route.params.filter;
    const paramFilter = (!dup && rawParamFilter && VALID_FILTERS.includes(rawParamFilter)) ? rawParamFilter : null;
    const [filter, setFilter] = useState(paramFilter || "all");
    const [query, setQuery] = useState("");
    const [sort, setSort] = useState("recent");
    const [expanded, setExpanded] = useState(false);
    /* if the digest CTA re-fires while the list is already mounted (same tab,
       no remount), the seed above won't re-run — sync the filter to the param
       whenever it changes to a fresh value. */
    useEffect(() => { if (paramFilter) setFilter(paramFilter); }, [paramFilter]);
    useEffect(() => { setExpanded(false); }, [filter, query]);
    const cycleSort = () => setSort(s => SORTS[(SORTS.findIndex(x => x.id === s) + 1) % SORTS.length].id);

    const counts = { all: 0, draft: 0, sent: 0, paid: 0, overdue: 0, quotes: 0, credits: 0 };
    db.INVOICES.forEach(i => {
      if (i.kind === "quote") { counts.quotes++; return; }
      counts.all++;
      if (i.kind === "credit") {
        counts.credits++;
        if (i.status === "draft") counts.draft++;
        return;
      }
      const eff = LIB.effectiveStatus(i, today);
      if (i.status === "draft") counts.draft++;
      else if (eff === "paid") counts.paid++;
      else if (eff === "overdue") counts.overdue++;
      else counts.sent++;
    });

    let list = db.INVOICES.filter(i => {
      if (filter === "quotes") return i.kind === "quote";
      if (filter === "credits") return i.kind === "credit";
      if (i.kind === "quote") return false;
      if (filter === "all") return true;
      if (filter === "draft") return i.status === "draft";
      if (i.kind === "credit") return false;
      const eff = LIB.effectiveStatus(i, today);
      if (filter === "paid") return eff === "paid";
      if (filter === "overdue") return eff === "overdue";
      if (filter === "sent") return eff === "sent" || eff === "partial";
      return true;
    });
    if (query.trim()) {
      const q = query.toLowerCase();
      list = list.filter(i => {
        const c = LIB.getClient(db, i.clientId);
        const name = (c && c.name) || i.newClientName || "";
        const proj = c && c.projects && i.projectId ? (c.projects.find(p => p.id === i.projectId) || {}).name : "";
        return i.id.toLowerCase().includes(q) || name.toLowerCase().includes(q) || (proj && proj.toLowerCase().includes(q));
      });
    }
    list = list.slice().sort((a, b) => {
      if (sort === "amount") return LIB.totals(b).total - LIB.totals(a).total;
      if (sort === "client") {
        const an = (LIB.getClient(db, a.clientId) || {}).name || a.newClientName || ""; const bn = (LIB.getClient(db, b.clientId) || {}).name || b.newClientName || "";
        return an.localeCompare(bn) || new Date(b.issued) - new Date(a.issued);
      }
      return new Date(b.issued) - new Date(a.issued);
    });

    const upcoming = (!dup && filter === "all" && !query.trim()) ? LIB.upcomingRuns(db, today) : [];

    const doDuplicate = (inv) => {
      const d = window.QEditor.draftFromInvoice(inv);
      delete d.editId; delete d.due; d.kind = "invoice"; d.status = "draft"; d.paid = 0;
      d.issued = today; d.terms = Math.max(0, LIB.daysBetween(inv.issued, inv.due));
      /* duplicates stay linkless: strip every link/state field */
      delete d.recurrence; delete d.recurrenceOf; delete d.chase; delete d.quoteId; delete d.depositOf;
      delete d.depositPct; delete d.creditFor; delete d.creditedBy; delete d.payments; delete d.paidOn;
      api.endReuse();
      api.openManual(d);
      Q.toast(t("toast.duplicating", { id: inv.id }), "copy");
    };

    return (
      <div style={{ paddingBottom: 4 }}>
        <AppBar large title={dup ? t("create.dup.title") : t("invs.title")} kicker={t("invs.count", { count: counts.all })}
          right={dup
            ? <Button variant="ghost" size="sm" onClick={() => api.endReuse()}>{t("g.cancel")}</Button>
            : <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <Button size="sm" variant="primary" icon="plus" onClick={() => api.openCreate()}>{t("invs.create")}</Button>
                {/* export affordance: the list's current filter (status chip +
                    search) is mapped into the Export seed shape so the screen
                    opens pre-filtered. The status chips map to the selection
                    engine's dimensions — effective-status for live invoices
                    (sent carries partial, matching the list), kind for the
                    quotes/credits tabs. A search query has no seed dimension,
                    so it falls back to hand-picking the shown ids. The icon
                    carries the narrowed row count (chip); unfiltered → {} (whole
                    book, the Export screen owns the count). */}
                {(() => {
                  const searching = !!query.trim();
                  const FILTER_SEED = {
                    overdue: { status: ["overdue"] },
                    sent: { status: ["sent", "partial"] },
                    paid: { status: ["paid"] },
                    draft: { status: ["draft"] },
                    quotes: { types: ["quote"] },
                    credits: { types: ["credit"] },
                  };
                  const filtered = filter !== "all" || searching;
                  /* a search narrows by free text (no seed dimension) → pin the
                     shown ids; otherwise seed the status/type filter so the user
                     can still adjust it on the Export screen. */
                  const seed = searching ? { ids: list.map(i => i.id) } : (FILTER_SEED[filter] || {});
                  const n = filtered ? list.length : 0;
                  return (
                    <span style={{ position: "relative", display: "inline-flex" }}>
                      <IconButton name="download" label={t("invs.export")} onClick={() => api.openExport(seed)} />
                      {n > 0 && (
                        <span aria-hidden="true" style={{
                          position: "absolute", top: -2, right: -2, minWidth: 16, height: 16,
                          padding: "0 4px", boxSizing: "border-box", borderRadius: 999,
                          background: "var(--color-primary)", color: "var(--color-primary-fg)",
                          fontFamily: "var(--font-mono)", fontSize: 10, fontWeight: 600,
                          display: "inline-flex", alignItems: "center", justifyContent: "center",
                          pointerEvents: "none", border: "1.5px solid var(--color-bg)",
                        }}>{n > 99 ? "99+" : n}</span>
                      )}
                    </span>
                  );
                })()}
              </div>} />

        <div style={{ padding: "6px 16px 0", display: "flex", flexDirection: "column", gap: 12 }}>
          {dup && (
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 14px", background: "var(--color-primary-muted)", border: "1.4px solid var(--color-primary)", borderRadius: 12 }}>
              <Icon name="copy" size={16} color="var(--color-primary)" />
              <span style={{ fontSize: 13, color: "var(--color-text-1)", fontWeight: 500, lineHeight: 1.4 }}>{t("create.dup.sub")}</span>
            </div>
          )}
          {/* Search + sort share a row; the filter chips get the full width
              below, so a chip can never clip against (or hide under) the
              sort pill — the founder flagged that twice. */}
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <label style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 8, height: 42, padding: "0 14px", background: "var(--color-surface-0)", border: "1.4px solid var(--color-border-strong)", borderRadius: 999, cursor: "text", transition: "border-color .15s" }}>
              <Icon name="search" size={17} color="var(--color-text-3)" />
              <input value={query} onChange={e => setQuery(e.target.value)} placeholder={t("invs.search")}
                onFocus={e => e.target.parentElement.style.borderColor = "var(--color-primary)"}
                onBlur={e => e.target.parentElement.style.borderColor = "var(--color-border-strong)"}
                style={{ flex: 1, minWidth: 0, height: "100%", border: "none", background: "transparent", outline: "none", fontSize: 14, color: "var(--color-text-1)", fontFamily: "var(--font-sans)" }} />
              {query && <button onClick={() => setQuery("")} aria-label={t("g.close")} style={{ border: "none", background: "transparent", color: "var(--color-text-3)", cursor: "pointer", display: "flex", padding: 2 }}><Icon name="x" size={16} /></button>}
            </label>
            <button onClick={cycleSort} aria-label={t("sort.label")} className="q-tap" style={{ flexShrink: 0, height: 42, padding: "0 12px", borderRadius: 999, border: "1.4px solid var(--color-border-strong)", background: "var(--color-surface-0)", color: "var(--color-text-2)", fontSize: 12, fontWeight: 600, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 4 }}>
              <Icon name="sliders" size={13} />{t(SORTS.find(s => s.id === sort).key)}
            </button>
          </div>

          {/* 32px visual pills, 40px hit areas: transparent 40px wrappers extend the hit box,
              negative vertical margins keep the row's layout height at 32 (zero visual shift).
              The right-edge fade + trailing padding make the horizontal scroll read as
              intentional instead of a clipped chip. */}
          <div style={{ display: "flex", alignItems: "center", flexWrap: "nowrap", gap: 8, overflowX: "auto", WebkitOverflowScrolling: "touch", margin: "-4px 0", paddingRight: 24, WebkitMaskImage: "linear-gradient(90deg, #000 calc(100% - 22px), transparent)", maskImage: "linear-gradient(90deg, #000 calc(100% - 22px), transparent)" }}>
            {[["all", t("invs.f.all"), counts.all], ["overdue", t("invs.f.overdue"), counts.overdue], ["sent", t("invs.f.sent"), counts.sent], ["paid", t("invs.f.paid"), counts.paid], ["draft", t("invs.f.draft"), counts.draft], ["quotes", t("invs.f.quotes"), counts.quotes], ["credits", t("invs.f.credits"), counts.credits]].map(([id, label, count]) => {
              const active = filter === id;
              const danger = id === "overdue" && count > 0;
              return (
                <button key={id} onClick={() => setFilter(id)} aria-pressed={active} className="q-tap" style={{ flexShrink: 0, height: 40, padding: 0, border: "none", background: "none", cursor: "pointer", display: "inline-flex", alignItems: "center" }}>
                  <span style={{
                    height: 32, padding: "0 12px", borderRadius: 999, whiteSpace: "nowrap", boxSizing: "border-box",
                    border: active ? "1.4px solid var(--color-border-strong)" : "1.4px solid var(--color-border)",
                    background: active ? "var(--color-surface-0)" : "transparent",
                    color: active ? "var(--color-text-1)" : "var(--color-text-3)",
                    fontSize: 12, fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 6,
                    transition: "background-color .15s ease, border-color .15s ease, color .15s ease",
                  }}>
                    {label}
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: danger ? "var(--color-accent)" : "inherit" }}>{count}</span>
                  </span>
                </button>
              );
            })}
          </div>

          {/* upcoming recurring runs */}
          {upcoming.length > 0 && (
            <Q.Collapsible title={t("rec.upcoming")} count={upcoming.length}
              sub={t("rec.upcoming.summary", { count: upcoming.length, date: window.fmtDate(upcoming[0].nextRun, "short") })}>
              {upcoming.map((r, i) => (
                <Row key={r.id} last={i === upcoming.length - 1} onClick={() => api.go("invoice", { id: r.id })}>
                  <Avatar name={r.clientName} size={38} square />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.clientName}</div>
                    <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 2 }}>{t("rec.every." + r.every)} · {window.fmtDate(r.nextRun, "short")}</div>
                  </div>
                  <div style={{ textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
                    <Money amount={r.amount} currency={r.currency} size={14} cents={false} />
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)" }}>{t("due.in", { d: r.inDays })}</span>
                  </div>
                </Row>
              ))}
            </Q.Collapsible>
          )}

          {list.length === 0 ? (
            <EmptyState icon="invoice" title={query ? t("g.nomatch") : t("invs.empty.title")} body={query ? t("g.trysearch") : t("invs.empty.body")}
              action={!query && !dup && <Button variant="ai" size="md" onClick={() => api.openCreate()}>{t("invs.empty.cta")}</Button>} />
          ) : (() => {
            /* cap only the unfiltered, unsearched, non-reuse view: chips, search
               and the reuse picker always show every match */
            const cappable = filter === "all" && !query.trim() && !dup;
            const capped = cappable && !expanded;
            const shown = list.slice(0, capped ? 12 : list.length);
            return (
              <Card pad={0} style={{ overflow: "hidden" }}>
                <div className="q-stagger">
                  {shown.map((inv, i) => <InvoiceRow key={inv.id} api={api} inv={inv} dup={dup} last={i === shown.length - 1} onClick={dup ? () => doDuplicate(inv) : undefined} />)}
                </div>
                {cappable && list.length > 12 && (
                  <button className="q-tap" onClick={() => setExpanded(e => !e)} aria-expanded={expanded} style={{ width: "100%", height: 48, display: "flex", alignItems: "center", justifyContent: "center", gap: 6, background: "none", border: "none", borderTop: "1px solid var(--color-border)", cursor: "pointer", fontSize: 13, fontWeight: 600, color: "var(--color-text-2)", fontFamily: "var(--font-sans)" }}>
                    {expanded ? t("g.showless") : t("g.viewall", { count: list.length })}
                    <span aria-hidden="true" style={{ display: "inline-flex", transform: expanded ? "rotate(180deg)" : "rotate(0deg)", transition: "transform .2s cubic-bezier(.32,.72,0,1)" }}><Icon name="chevronDown" size={15} /></span>
                  </button>
                )}
              </Card>
            );
          })()}
        </div>
      </div>
    );
  }

  window.QScreens = Object.assign(window.QScreens || {}, { Invoices, InvoiceDetail, InvoicePreview });
})();
