/* sheets.jsx — Create chooser, Send/Reminder, Record Payment, Client pay view,
 * Client quote view, Deposit request
 * → window.QScreens.{CreateSheet, SendSheet, PaymentSheet, ClientPayView,
 *                    ClientQuoteView, DepositSheet}
 */
(function () {
  const { useState, useEffect, useRef } = React;
  const Q = window.Q, LIB = window.LIB, Icons = window.Icons;
  const { Icon } = Icons;
  const { Sheet, Button, Field, Input, Textarea, Kicker, Money, AnimatedNumber, PaidBurst, Avatar, StatusPill, Segmented, AiKicker } = Q;
  const t = window.t;

  /* Local-day YYYY-MM-DD of a clock stamp — mirrors lib.jsx isoDay() (not
     exported on window.LIB). The demo clock is a local midnight that
     serializes to the previous UTC evening, so a raw .slice(0, 10) on the
     ISO string lands one day early. */
  function isoDay(d) {
    const x = new Date(d);
    const p = n => String(n).padStart(2, "0");
    return x.getFullYear() + "-" + p(x.getMonth() + 1) + "-" + p(x.getDate());
  }

  /* ── Create chooser (the "+" hub) ────────────────────────────
     The AI path is the hero; the manual path sits right beside it as an
     equal, one-tap option (also the obvious fallback if AI is wrong/down). */
  function CreateSheet({ api, onClose }) {
    const go = (fn) => { onClose(); fn(); };
    return (
      <Sheet open onClose={onClose} height="auto" title={t("create.title")}>
        <div style={{ padding: "2px 16px 26px", display: "flex", flexDirection: "column", gap: 10 }}>
          {/* AI hero */}
          <button onClick={() => go(() => api.openComposer())} className="q-tap" style={{ textAlign: "left", cursor: "pointer", border: "1.4px solid var(--color-primary)", borderRadius: 16, padding: 16, background: "var(--color-primary-muted)", display: "flex", alignItems: "center", gap: 14, transition: "transform .12s cubic-bezier(.32,.72,0,1), box-shadow .12s cubic-bezier(.32,.72,0,1)" }}>
            <div style={{ width: 46, height: 46, borderRadius: 12, background: "var(--color-primary)", color: "var(--color-primary-fg)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, boxShadow: "0 4px 12px color-mix(in oklab, var(--color-primary) 30%, transparent)" }}>
              <Q.Sparkle size={20} color="var(--color-primary-fg)" />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
                <span style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-1)" }}>{t("create.ai.title")}</span>
                <Q.Badge tone="primary" ai>{t("create.ai.badge")}</Q.Badge>
              </div>
              <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 2 }}>{t("create.ai.sub")}</div>
            </div>
          </button>
          {/* manual */}
          <CreateRow icon="keyboard" title={t("create.manual.title")} sub={t("create.manual.sub")} onClick={() => go(() => api.openManual(null))} />
          {/* quote */}
          <CreateRow icon="file" title={t("create.quote.title")} sub={t("create.quote.sub")} onClick={() => go(() => api.openManual({ kind: "quote" }))} />
          {/* duplicate */}
          <CreateRow icon="copy" title={t("create.dup.title")} sub={t("create.dup.sub")} onClick={() => go(() => api.beginReuse())} />
          {/* scan a receipt → expense (OCR) */}
          <CreateRow icon="scan" title={t("create.expense.title")} sub={t("create.expense.sub")} onClick={() => go(() => api.openExpenseSheet({ mode: "scan" }))} />
        </div>
      </Sheet>
    );
  }
  function CreateRow({ icon, title, sub, onClick }) {
    return (
      <button onClick={onClick} className="q-tap" style={{ textAlign: "left", cursor: "pointer", border: "1.4px solid var(--color-border-strong)", borderRadius: 12, padding: "14px 16px", background: "var(--color-surface-0)", display: "flex", alignItems: "center", gap: 12, transition: "transform .12s cubic-bezier(.32,.72,0,1)" }}>
        <div style={{ width: 40, height: 40, borderRadius: 12, background: "var(--color-surface-1)", color: "var(--color-text-2)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
          <Icon name={icon} size={18} />
        </div>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--color-text-1)" }}>{title}</div>
          <div style={{ fontSize: 13, color: "var(--color-text-3)", marginTop: 1 }}>{sub}</div>
        </div>
        <Icon name="chevronRight" size={18} color="var(--color-text-3)" />
      </button>
    );
  }

  /* ── Send / Reminder ─────────────────────────────────────────── */
  function SendSheet({ api, invoiceId, mode, onClose }) {
    const { db, setDb, today } = api;
    const inv = db.INVOICES.find(i => i.id === invoiceId);
    const client = inv && (LIB.getClient(db, inv.clientId) || { name: inv.newClientName || "", email: "" });
    const co = db.COMPANY;
    const tot = inv ? LIB.totals(inv) : { total: 0 };
    const isRemind = mode === "remind";

    /* Active pay method the client will use: qrMethod wins, then the first
       enabled method. Maps to a calm "Payment via …" line so the rail that
       collects the money is visible at the moment of sending. */
    const pm = (co && co.payment) || {};
    const payKey = (pm.qrMethod && pm[pm.qrMethod] && pm[pm.qrMethod].enabled) ? pm.qrMethod
      : (pm.nita && pm.nita.enabled) ? "nita"
      : (pm.bank && pm.bank.enabled) ? "bank"
      : (pm.card && pm.card.enabled) ? "card"
      : null;
    const payMethod = payKey ? t("pay.method." + payKey) : null;

    const firstName = (client && client.contact ? client.contact : (client ? client.name : "")).split(" ")[0];
    /* Reminder tone: Nita drafts gentle / firm / final from how late it is. */
    const odDays = inv ? Math.max(0, -LIB.daysBetween(today, inv.due)) : 0;
    const defTone = odDays <= 7 ? "gentle" : odDays <= 21 ? "firm" : "final";
    const remindParams = {
      name: firstName || (client ? client.name : ""), number: invoiceId,
      amount: window.fmtMoney(tot.balance != null ? tot.balance : tot.total, inv ? inv.currency : "EUR"),
      due: inv ? window.fmtDate(inv.due) : "", days: odDays, company: co.name,
    };
    /* Doc-aware defaults: a devis, an avoir and a deposit invoice each go
       out with their own subject/message; plain invoices keep the existing
       default chain; reminders are untouched. */
    const docKind = inv
      ? (inv.kind === "quote" ? "quote" : inv.kind === "credit" ? "credit" : inv.depositOf ? "deposit" : null)
      : null;
    const docParams = {
      name: firstName || (client ? client.name : ""), number: invoiceId,
      amount: window.fmtMoney(tot.total, inv ? inv.currency : "EUR"),
      valid: inv ? window.fmtDate(inv.due) : "",
      invoice: (inv && inv.creditFor) || "", company: co.name,
    };
    const [tone, setTone] = useState(defTone);
    const [to, setTo] = useState(client ? client.email : "");
    const [subject, setSubject] = useState(
      isRemind ? t("send.subject.remind." + defTone, remindParams)
        : docKind ? t("send.subject." + docKind, docParams)
        : t("send.subject.default", { number: invoiceId, company: co.name })
    );
    const [message, setMessage] = useState(
      isRemind ? t("send.msg.remind." + defTone, remindParams)
        : docKind ? t("send.msg." + docKind, docParams)
        : t("send.msg.default")
          .replace("{name}", firstName || (client ? client.name : ""))
          .replace("{number}", invoiceId)
          .replace("{amount}", window.fmtMoney(tot.total, inv ? inv.currency : "EUR"))
          .replace("{due}", inv ? window.fmtDate(inv.due) : "")
          .replace("{company}", co.name)
    );
    const applyTone = (tn) => {
      setTone(tn);
      setSubject(t("send.subject.remind." + tn, remindParams));
      setMessage(t("send.msg.remind." + tn, remindParams));
    };
    const [sent, setSent] = useState(false);
    const [sending, setSending] = useState(false);

    /* THE send write body lives in LIB.sendWrite (the dunning effect shares
       it): draft->sent (credits -> issued), chase stamp, single-shot stock
       move, plain ACTIVITY prepend. The 950ms theater stays here. */
    const sendingRef = useRef(false);
    const doSend = () => {
      if (sendingRef.current) return;
      sendingRef.current = true;
      setSending(true);
      setTimeout(() => {
        setSending(false); setSent(true);
        setDb(d => LIB.sendWrite(d, invoiceId, { mode: isRemind ? "remind" : "send", tone, to, at: today }));
      }, 950);
    };

    /* Real pay-link copy (the deterministic URL + actual clipboard write). */
    const copyLink = () => {
      try { navigator.clipboard && navigator.clipboard.writeText(LIB.payLink(inv)); } catch (e) {}
      Q.toast(t("prev.linkcopied"), "link");
    };

    if (!inv) return null;

    return (
      <Sheet open onClose={onClose} height="auto" title={!sent ? (isRemind ? t("send.remind.title") : t("send.title")) : undefined} hideClose={sent}>
        {sent ? (
          <div className="q-fade" style={{ padding: "24px 24px 30px", display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center" }}>
            <div className="q-pop" style={{ width: 64, height: 64, borderRadius: "50%", background: "var(--color-success)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 18, boxShadow: "0 6px 20px color-mix(in oklab, var(--color-success) 30%, transparent)" }}>
              <Icon name="check" size={32} color="#fff" />
            </div>
            <div className="q-fade-up" style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 22, color: "var(--color-text-1)", animationDelay: ".06s" }}>{isRemind ? t("send.remind.success") : t("send.success.title")}</div>
            {/* No backend dispatches the mail (the CTA opens the user's mail
               app via mailto:), so "Delivered to …" overclaims. State the
               honest outcome: the message is drafted and ready to send
               (send-success-claims-delivered). */}
            <div className="q-fade-up" style={{ fontSize: 14, color: "var(--color-text-2)", marginTop: 6, animationDelay: ".1s" }}>{t("send.ready", { email: to })}</div>
            {payMethod && (
              <div className="q-fade-up" style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10, fontSize: 13, color: "var(--color-text-2)", animationDelay: ".14s" }}>
                <Icon name="card" size={14} color="var(--color-text-3)" />
                {t("send.payvia", { method: payMethod })}
              </div>
            )}
            <div className="q-fade-up" style={{ width: "100%", marginTop: 24, display: "flex", flexDirection: "column", gap: 10, animationDelay: ".18s" }}>
              {inv.kind === "invoice" && <Button variant="primary" size="lg" full icon="card" onClick={() => { onClose(); api.openPayment(invoiceId); }}>{t("det.record")}</Button>}
              <Button variant="secondary" size="lg" full icon="link" onClick={copyLink}>{t("prev.copylink")}</Button>
              {inv.kind === "invoice" && api.openPayView && <Button variant="secondary" size="lg" full icon="eye" onClick={() => { onClose(); api.openPayView(invoiceId); }}>{t("payview.open")}</Button>}
              {inv.kind === "quote" && api.openQuoteView && <Button variant="secondary" size="lg" full icon="eye" onClick={() => { onClose(); api.openQuoteView(invoiceId); }}>{t("payview.open")}</Button>}
              <Button variant="ghost" size="lg" full onClick={() => { api.goTab("invoices"); onClose(); }}>{t("nav.invoices")}</Button>
            </div>
          </div>
        ) : (
          <div style={{ padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 14 }}>
            {/* Nita-drafted reminder: pick the tone, the draft follows */}
            {isRemind && (
              <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                  <AiKicker>{t("send.remind.drafted")}</AiKicker>
                  {odDays > 0 && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-accent)" }}>{t("due.overdue", { d: odDays })}</span>}
                </div>
                <Segmented value={tone} onChange={applyTone} options={[
                  { value: "gentle", label: t("send.remind.tone.gentle") },
                  { value: "firm", label: t("send.remind.tone.firm") },
                  { value: "final", label: t("send.remind.tone.final") },
                ]} />
                {tone === "gentle" && <div style={{ fontSize: 13, color: "var(--color-text-3)" }}>{t("send.remind.note")}</div>}
              </div>
            )}
            {/* attachment */}
            <div className={sending ? "q-fly-up" : undefined} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 14px", background: "var(--color-surface-1)", borderRadius: 12, border: "1px solid var(--color-border)" }}>
              <div style={{ width: 34, height: 42, borderRadius: 6, background: "#fff", border: "1px solid var(--color-border-strong)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                <Icon name="file" size={16} color="var(--color-text-3)" />
              </div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 600 }}>{invoiceId}.pdf</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-gold)" }}>{window.fmtMoney(tot.total, inv.currency)}</div>
              </div>
              <Icon name="check" size={16} color="var(--color-success)" />
            </div>

            {payMethod && (
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "10px 14px", background: "var(--color-surface-1)", borderRadius: 12, border: "1px solid var(--color-border)" }}>
                <span style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--color-text-2)" }}>
                  <Icon name="card" size={15} color="var(--color-text-3)" />
                  {t("send.payvia", { method: payMethod })}
                </span>
                <button onClick={copyLink} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 4, minHeight: 32, padding: "4px 12px", borderRadius: 8, border: "1px solid var(--color-border-strong)", background: "var(--color-surface-0)", color: "var(--color-text-2)", fontSize: 12, fontWeight: 600, cursor: "pointer", flexShrink: 0 }}>
                  <Icon name="link" size={13} />
                  {t("send.sharelink")}
                </button>
              </div>
            )}

            <Field label={t("send.to")}>
              {client.email
                ? <Input value={to} onChange={e => setTo(e.target.value)} type="email" />
                : <div>
                    <Input value={to} onChange={e => setTo(e.target.value)} type="email" placeholder={t("send.to.placeholder")} />
                    <div style={{ fontSize: 12, color: "var(--color-accent)", marginTop: 6, display: "flex", alignItems: "center", gap: 5 }}><Icon name="alert" size={13} />{t("send.noemail", { name: client.name })}</div>
                  </div>}
            </Field>
            <Field label={t("send.subject")}><Input value={subject} onChange={e => setSubject(e.target.value)} /></Field>
            <Field label={t("send.message")}><Textarea value={message} onChange={e => setMessage(e.target.value)} rows={6} /></Field>

            {/* Real mailto anchor (not a button): opens the user's mail app
               with the drafted subject/body prefilled, then runs the same
               mark-as-sent write. Styled byte-for-byte like the primary
               lg Q.Button so it reads as the same CTA. Recipient is
               URL-encoded (@ → %40, valid in mailto URIs); no email on
               file → bare mailto:?subject=… so the user types the
               recipient in their mail app. */}
            <a
              href={"mailto:" + encodeURIComponent(to.trim()) + "?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(message)}
              className="q-btn q-btn-raised"
              onClick={(e) => { if (sending || sent) { e.preventDefault(); return; } doSend(); }}
              style={{
                position: "relative", overflow: "hidden",
                display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
                height: 48, padding: "0 20px", fontSize: 15, width: "100%", boxSizing: "border-box",
                fontFamily: "var(--font-sans)", fontWeight: 600, borderRadius: "var(--radius-md)",
                cursor: sending ? "not-allowed" : "pointer", whiteSpace: "nowrap",
                opacity: sending ? 0.72 : 1, pointerEvents: sending ? "none" : "auto",
                transition: "filter .12s, background .12s, transform .2s cubic-bezier(.32,.72,0,1), box-shadow .2s cubic-bezier(.32,.72,0,1)",
                letterSpacing: "-0.01em", textDecoration: "none",
                backgroundColor: "var(--color-text-1)", color: "var(--color-bg)", border: "none",
              }}>
              {/* in-flight progress sheen — the 950ms theater reads as the
                  message being dispatched, not the button merely dimming */}
              {sending && (
                <span aria-hidden="true" style={{
                  position: "absolute", inset: 0, pointerEvents: "none",
                  background: "linear-gradient(90deg, transparent 20%, color-mix(in oklab, var(--color-bg) 28%, transparent) 50%, transparent 80%)",
                  backgroundSize: "200% 100%", animation: "q-shimmer 1.1s linear infinite",
                }} />
              )}
              <Icon name="send" size={18} style={sending ? { transform: "translateX(2px)", transition: "transform .9s cubic-bezier(.32,.72,0,1)" } : { transition: "transform .2s cubic-bezier(.32,.72,0,1)" }} />
              {sending ? t("send.sending") : t("send.cta")}
            </a>
          </div>
        )}
      </Sheet>
    );
  }

  /* ── Record payment ──────────────────────────────────────────── */
  function PaymentSheet({ api, invoiceId, onClose }) {
    const { db, setDb, today } = api;
    const inv = db.INVOICES.find(i => i.id === invoiceId);
    const tot = inv ? LIB.totals(inv) : { balance: 0, total: 0 };
    const [amount, setAmount] = useState(inv ? (tot.balance || 0).toFixed(2) : "0");
    const [method, setMethod] = useState("transfer");
    const [date, setDate] = useState(isoDay(today));
    const [reference, setReference] = useState("");
    /* After recording, hold the outcome so the sheet visibly confirms the
       invoice moved forward (paid in full, or partial with a new balance)
       before the user closes it. The db is updated either way. */
    const [done, setDone] = useState(null);
    if (!inv) return null;
    /* Quotes are not payable; an avoir is settled by offset, never recorded. */
    if (inv.kind === "credit" || inv.kind === "quote") return null;

    const save = () => {
      const amt = Number(amount) || 0;
      const tt = LIB.totals(inv);
      const newPaid = Math.min(tt.total, LIB.round2((inv.paid || 0) + amt));
      const full = tt.total - newPaid <= 0.005;
      const clientName = (LIB.getClient(db, inv.clientId) || { name: inv.newClientName || "" }).name;
      const fmtAmt = window.fmtMoney(amt, inv.currency);
      const at = date ? new Date(date).toISOString() : today;
      setDb(d => {
        const fresh = d.INVOICES.find(x => x.id === invoiceId);
        if (!fresh) return d;
        const next = LIB.recordPayment(fresh, { amount: amt, at, method, reference: reference.trim() });
        return {
          ...d,
          INVOICES: d.INVOICES.map(x => (x.id === invoiceId ? next : x)),
          ACTIVITY: [
            full ? LIB.activityEvent("paid", { client: clientName, id: invoiceId }, invoiceId)
              : LIB.activityEvent("recorded", { amount: fmtAmt, id: invoiceId }, invoiceId),
            ...d.ACTIVITY,
          ],
        };
      });
      /* No toast here: the success view below confirms in place (paid in full,
         or partial with the new balance). A 4s toast would slide up over that
         very balance the user needs to read (payment-success-toast-occludes-balance). */
      setDone({ full, paid: newPaid, total: tt.total, balance: LIB.round2(tt.total - newPaid) });
    };

    return (
      <Sheet open onClose={onClose} height="auto" title={!done ? t("det.partial.title") : undefined} hideClose={!!done}>
        {done ? (
          <div className="q-fade" style={{ position: "relative", padding: "24px 24px 30px", display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center" }}>
            {done.full && <PaidBurst />}
            <div className="q-pop" style={{ width: 64, height: 64, borderRadius: "50%", background: done.full ? "var(--color-success)" : "var(--color-info)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 18, boxShadow: done.full ? "0 6px 20px color-mix(in oklab, var(--color-success) 30%, transparent)" : "0 6px 20px color-mix(in oklab, var(--color-info) 30%, transparent)" }}>
              <Icon name="check" size={32} color="#fff" />
            </div>
            <div className={done.full ? "q-pop" : undefined} style={done.full ? { animationDelay: ".14s" } : undefined}>
              <Q.StatusPill status={done.full ? "paid" : "partial"} solid />
            </div>
            {done.full ? (
              <div className="q-pop" style={{ animationDelay: ".22s", marginTop: 12 }}>
                <AnimatedNumber amount={done.total} currency={inv.currency} size={26} animKey={inv.id + ":paidfull"}
                  style={{ fontFamily: "var(--font-serif)", fontWeight: 600, letterSpacing: "-0.02em", color: "var(--color-success)" }} />
              </div>
            ) : (
              <div style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 18, color: "var(--color-text-1)", marginTop: 12 }}>{t("det.partial", { paid: window.fmtMoney(done.paid, inv.currency), total: window.fmtMoney(done.total, inv.currency) })}</div>
            )}
            {!done.full && (
              <div style={{ width: "100%", marginTop: 18, display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 14px", background: "var(--color-surface-1)", borderRadius: 12 }}>
                <span style={{ fontSize: 13, color: "var(--color-text-2)" }}>{t("det.balance")}</span>
                <Money amount={done.balance} currency={inv.currency} size={16} />
              </div>
            )}
            <Button variant="primary" size="lg" full icon="check" onClick={onClose} style={{ marginTop: 24 }}>{t("g.done")}</Button>
          </div>
        ) : (
          <div style={{ padding: "4px 20px 26px", display: "flex", flexDirection: "column", gap: 16 }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 14px", background: "var(--color-surface-1)", borderRadius: 12 }}>
              <span style={{ fontSize: 13, color: "var(--color-text-2)" }}>{t("det.balance")}</span>
              <Money amount={tot.balance} currency={inv.currency} size={16} />
            </div>
            <Field label={t("det.partial.amount")}>
              <div style={{ position: "relative" }}>
                <span style={{ position: "absolute", left: 14, top: "50%", transform: "translateY(-50%)", fontFamily: "var(--font-mono)", fontSize: 15, color: "var(--color-text-3)" }}>{(window.CURRENCIES[inv.currency] || {}).symbol}</span>
                <Input value={amount} onChange={e => setAmount(e.target.value.replace(/[^\d.]/g, ""))} type="text" inputMode="decimal" mono style={{ paddingLeft: 34, fontSize: 17, fontWeight: 600 }} />
              </div>
            </Field>
            {/* Quick-amounts sit directly under the field they fill — the
               Full/Half presets were stranded at the bottom of the form, far
               from the amount input (payment-quick-amounts-far-from-field).
               Kept OUTSIDE the amount Field's <label> so a preset tap can't be
               re-routed to the input. The negative marginTop pulls them snug
               against the field. */}
            <div style={{ display: "flex", gap: 8, marginTop: -8 }}>
              {[0.5, 1].map(f => (
                <button key={f} onClick={() => setAmount(LIB.round2(tot.balance * f).toFixed(2))} style={{ flex: 1, height: 36, borderRadius: 8, border: "1.4px solid var(--color-border-strong)", background: "var(--color-surface-0)", color: "var(--color-text-2)", fontSize: 13, fontWeight: 600, cursor: "pointer", fontFamily: "var(--font-mono)" }}>
                  {f === 1 ? t("pay.full") : t("pay.half")}
                </button>
              ))}
            </div>
            {/* F3 ledger capture: how, when, and under which reference */}
            <Field label={t("ledger.method")}>
              <Segmented value={method} onChange={setMethod} options={["transfer", "card", "cash", "nita", "other"].map(k => ({ value: k, label: t("ledger.method." + k) }))} />
            </Field>
            <Field label={t("ledger.date")}>
              <Input value={date} onChange={e => setDate(e.target.value)} type="date" mono />
              {/* The native date control paints YYYY-MM-DD; echo the chosen day
                 in the app's locale format so it matches every other date in
                 the product (paydate-native-input-locale-format). */}
              {date && (
                <div style={{ fontSize: 12, color: "var(--color-text-3)", marginTop: 6, fontFamily: "var(--font-mono)" }}>{window.fmtDate(date)}</div>
              )}
            </Field>
            <Field label={t("pay.ref")}>
              <Input value={reference} onChange={e => setReference(e.target.value)} type="text" mono placeholder={inv.id} />
            </Field>
            <Button variant="primary" size="lg" full icon="check" disabled={!(Number(amount) > 0)} onClick={save}>{t("det.partial.save")}</Button>
          </div>
        )}
      </Sheet>
    );
  }

  /* ── Client pay view ─────────────────────────────────────────
     What the client sees when they open the pay link: who is asking,
     the balance due, every enabled pay method, and the scannable QR.
     Mounted from app.jsx (guarded) exactly like the other sheets.
     `preview` (V2-6): true when the OWNER opens this as "Preview client
     view" — a preview must never fabricate client activity, so the
     viewed stamp below is skipped. Genuine client opens are simulated
     engine-side (lib.jsx effect 40 "viewed"), which stays untouched. */
  function ClientPayView({ api, invoiceId, onClose, preview }) {
    const { db, setDb, today } = api;
    const inv = db.INVOICES.find(i => i.id === invoiceId);
    const co = db.COMPANY;
    const [payPending, setPayPending] = useState(false); // tapped "Pay with Nita" → inline not-live-yet notice
    const [viewerOpen, setViewerOpen] = useState(false);
    const [disputeOpen, setDisputeOpen] = useState(false);
    const [disputeText, setDisputeText] = useState("");
    const [disputeSent, setDisputeSent] = useState(false);

    /* Opening the link IS the view: stamp the one-and-only 'viewed' audit
       event the moment the portal mounts (once-ref so strict-mode double
       mounts and re-renders never write twice). Owner previews bail out:
       only a real client open may write "viewed". */
    const viewedOnce = useRef(false);
    useEffect(() => {
      if (preview) return;
      if (viewedOnce.current) return;
      viewedOnce.current = true;
      setDb(d => {
        const cur = d.INVOICES.find(x => x.id === invoiceId);
        if (!cur || cur.kind !== "invoice" || cur.status === "draft" || cur.status === "paid") return d;
        if ((cur.audit || []).some(e => e.type === "viewed")) return d;
        const clientName = (LIB.getClient(d, cur.clientId) || { name: cur.newClientName || "" }).name;
        return {
          ...d,
          INVOICES: d.INVOICES.map(x => x.id === invoiceId ? LIB.pushAudit(x, LIB.auditEvent("viewed")) : x),
          ACTIVITY: [LIB.activityEvent("viewed", { client: clientName, id: invoiceId }, invoiceId), ...d.ACTIVITY],
        };
      });
    }, []);

    if (!inv) return null;
    const tot = LIB.totals(inv);
    const eff = LIB.effectiveStatus(inv, today);
    const isPaid = eff === "paid";
    const pm = (co && co.payment) || {};
    const qr = isPaid ? null : LIB.payQR(co, inv, tot.balance);
    const link = LIB.payLink(inv);
    const copy = (val) => { try { navigator.clipboard && navigator.clipboard.writeText(val); } catch (e) {} Q.toast(t("pay.copied"), "copy"); };
    const copyLink = () => { try { navigator.clipboard && navigator.clipboard.writeText(link); } catch (e) {} Q.toast(t("prev.linkcopied"), "link"); };
    /* The client's other open invoices from this company (clientId-keyed;
       ad-hoc named clients have nothing to group by). */
    const others = inv.clientId
      ? db.INVOICES.filter(x => x.id !== invoiceId && x.clientId === inv.clientId && x.kind === "invoice" && x.status !== "draft" && x.status !== "paid")
      : [];

    /* Pending MVP: online payment is not live in this preview. Tapping
       "Pay with Nita" reveals an honest inline notice instead of the old
       fake instant-success — nothing is written, nothing flips to paid. */
    const payNow = () => setPayPending(true);

    /* Inline dispute: one write (audit + feed), then a quiet success line.
       No toast - this half of the screen is the client's fiction. */
    const sendDispute = () => {
      const msg = disputeText.trim().slice(0, 140);
      if (!msg) return;
      setDb(d => {
        const cur = d.INVOICES.find(x => x.id === invoiceId);
        if (!cur) return d;
        const clientName = (LIB.getClient(d, cur.clientId) || { name: cur.newClientName || "" }).name;
        return {
          ...d,
          INVOICES: d.INVOICES.map(x => x.id === invoiceId ? LIB.pushAudit(x, LIB.auditEvent("dispute", { message: msg })) : x),
          ACTIVITY: [LIB.activityEvent("dispute", { client: clientName, id: invoiceId }, invoiceId), ...d.ACTIVITY],
        };
      });
      setDisputeSent(true);
    };

    return (
      <Sheet open onClose={onClose} height="auto" title={t("payview.title")}>
        <div style={{ padding: "0 20px 26px", display: "flex", flexDirection: "column", gap: 12 }}>
          {/* answers "What is this?": the user previews the client's pay page */}
          <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--color-text-3)" }}>
            <Icon name="eye" size={14} style={{ flexShrink: 0 }} />
            {t("payview.preview")}
          </div>

          {/* the page the client lands on */}
          <Q.Card pad={0} style={{ overflow: "hidden" }}>
            <div style={{ padding: "16px 16px 14px", display: "flex", alignItems: "center", gap: 12, borderBottom: "1px solid var(--color-border)" }}>
              <Avatar name={co.name} size={38} square />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{co.name}</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", marginTop: 1 }}>{inv.id}</div>
              </div>
              <StatusPill status={eff} solid />
            </div>

            <div style={{ padding: "14px 16px 16px" }}>
              <Kicker>{isPaid ? t("inv.totalPaid") : t("det.balance")}</Kicker>
              <div style={{ marginTop: 4 }}>
                <AnimatedNumber amount={isPaid ? tot.total : tot.balance} currency={inv.currency} size={28} animKey={inv.id + ":pay"}
                  style={{ fontFamily: "var(--font-serif)", letterSpacing: "-0.02em", lineHeight: 1.05, fontVariantNumeric: "normal" }} />
              </div>
              {isPaid ? (
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 8, fontSize: 13, color: "var(--color-success)", fontWeight: 600 }}>
                  <Icon name="checkCircle" size={15} style={{ flexShrink: 0 }} />
                  {t("payview.paid")}
                </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>
                  )}
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", marginTop: 8 }}>{t("inv.due")} · {window.fmtDate(inv.due)}</div>
                </>
              )}
            </div>

            {/* what the money is for */}
            {(inv.items || []).length > 0 && (
              <div style={{ borderTop: "1px solid var(--color-border)", padding: "12px 16px 16px" }}>
                <Kicker style={{ marginBottom: 8 }}>{t("payview.items")}</Kicker>
                <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
                  {inv.items.map(it => (
                    <div key={it.id} style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 13, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.description}</div>
                        <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 1 }}>{it.qty} × {t("unit." + (it.unit || "unit"))}</div>
                      </div>
                      <Money amount={LIB.lineNet(it)} currency={inv.currency} size={13} />
                    </div>
                  ))}
                </div>
                {/* the arithmetic from items to the ask, reconciled in place */}
                <div style={{ marginTop: 12, paddingTop: 12, borderTop: "1px solid var(--color-border)", display: "flex", flexDirection: "column", gap: 6 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10 }}>
                    <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{t("inv.subtotal")}</span>
                    <Money amount={tot.subtotal} currency={inv.currency} size={13} color="var(--color-text-1)" />
                  </div>
                  {tot.vatBreakdown.filter(v => v.rate > 0).map(v => (
                    <div key={v.rate} style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10 }}>
                      <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{`${t("inv.vat")} ${v.rate}%`}</span>
                      <Money amount={v.tax} currency={inv.currency} size={13} color="var(--color-text-1)" />
                    </div>
                  ))}
                  {!isPaid && tot.paid > 0 && (
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10 }}>
                      <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{t("payview.alreadypaid")}</span>
                      <Money amount={-tot.paid} currency={inv.currency} size={13} color="var(--color-text-1)" />
                    </div>
                  )}
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10 }}>
                    <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{t("inv.total")}</span>
                    <Money amount={tot.total} currency={inv.currency} size={14} weight={600} />
                  </div>
                </div>
              </div>
            )}

            {!isPaid && (
              <div style={{ borderTop: "1px solid var(--color-border)", padding: "14px 16px 18px" }}>
                <Kicker style={{ marginBottom: 12 }}>{t("pay.title")}</Kicker>
                {pm.nita && pm.nita.enabled && (
                  /* Once the "not live yet" notice is showing, the CTA can't be
                     fulfilled — keep it visible for context but disable it and
                     demote from primary so it no longer reads as a live action
                     (payview-pay-cta-stays-live-after-pending). */
                  <Button variant={payPending ? "secondary" : "primary"} size="lg" full disabled={payPending} onClick={payNow} style={{ marginBottom: payPending ? 10 : 14 }}>
                    {t("payview.paybtn")}
                  </Button>
                )}
                {payPending && (
                  <div className="q-fade" style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "11px 13px", marginBottom: 14, borderRadius: "var(--radius-md)", background: "color-mix(in oklab, var(--color-info) 9%, var(--color-surface-1))", border: "1px solid color-mix(in oklab, var(--color-info) 28%, transparent)" }}>
                    <Icon name="clock" size={15} color="var(--color-info)" style={{ marginTop: 1 }} />
                    <div style={{ fontSize: 13, color: "var(--color-text-2)", lineHeight: 1.45 }}>{t("payview.pending")}</div>
                  </div>
                )}
                <div style={{ display: "flex", gap: 12 }}>
                  <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 12 }}>
                    {pm.bank && pm.bank.enabled && co.iban && (
                      <PayRow icon="bank" title={t("pay.bank")} value={co.iban} onCopy={() => copy(co.iban.replace(/\s/g, ""))} />
                    )}
                    {pm.card && pm.card.enabled && pm.card.link && (
                      <PayRow icon="card" title={t("pay.card")} value={pm.card.link} onCopy={() => copy(pm.card.link)} />
                    )}
                    {pm.nita && pm.nita.enabled && (
                      <PayRow icon="wallet" title={t("pay.nita.handle")} value={pm.nita.handle || pm.nita.phone} sage onCopy={() => copy(pm.nita.handle || pm.nita.phone)} />
                    )}
                    <div style={{ fontSize: 12, color: "var(--color-text-3)" }}>
                      {t("pay.ref")} · <span style={{ fontFamily: "var(--font-mono)" }}>{inv.id}</span>
                    </div>
                  </div>
                  {qr && (
                    <div style={{ flexShrink: 0, textAlign: "center" }}>
                      <div className="q-qr-flip q-sheen" style={{ padding: 8, background: "#fff", borderRadius: 10, border: "1.4px solid var(--color-border-strong)" }}>
                        <Q.QR value={qr.value} size={92} color="hsl(35 20% 12%)" />
                      </div>
                      <div style={{ fontFamily: "var(--font-mono)", fontSize: 9, 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>
              </div>
            )}
          </Q.Card>

          {/* the document itself, one tap from the pay page */}
          {window.A4 && (
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8, padding: "6px 0 2px" }}>
              <window.A4.Scaled api={api} inv={inv} width={300} balance={tot.balance} onClick={() => setViewerOpen(true)} />
              <button onClick={() => setViewerOpen(true)} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 6, background: "none", border: "none", padding: "6px 10px", cursor: "pointer", color: "var(--color-text-2)", fontSize: 13, fontWeight: 600 }}>
                <Icon name="eye" size={14} color="var(--color-text-3)" />
                {t("payview.viewdoc")}
              </button>
            </div>
          )}

          {/* everything else still open with this company */}
          {others.length > 0 && (
            <div>
              <Kicker style={{ marginBottom: 8 }}>{t("payview.others", { company: co.name })}</Kicker>
              <Q.Card pad={0}>
                {others.map((x, i) => {
                  const xt = LIB.totals(x);
                  return (
                    <Q.Row key={x.id} last={i === others.length - 1} onClick={() => api.openPayView(x.id)}>
                      <span style={{ flex: 1, fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{x.id}</span>
                      <Money amount={xt.balance} currency={x.currency} size={13} />
                      <StatusPill status={LIB.effectiveStatus(x, today)} solid />
                    </Q.Row>
                  );
                })}
              </Q.Card>
            </div>
          )}

          {/* the link the client got: visible, real, copyable */}
          <button onClick={copyLink} className="q-tap" style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 14px", borderRadius: 12, border: "1px solid var(--color-border-strong)", background: "var(--color-surface-0)", cursor: "pointer", textAlign: "left", width: "100%" }}>
            <Icon name="link" size={15} color="var(--color-text-3)" style={{ flexShrink: 0 }} />
            <span style={{ flex: 1, minWidth: 0, fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{link}</span>
            <span style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 12, fontWeight: 600, color: "var(--color-text-2)", flexShrink: 0 }}>
              <Icon name="copy" size={13} />
              {t("prev.copylink")}
            </span>
          </button>

          {/* question or dispute, inline (no sheet-over-sheet) */}
          {disputeSent ? (
            <div className="q-fade" style={{ display: "flex", alignItems: "center", gap: 8, padding: "12px 14px", borderRadius: 12, background: "var(--color-surface-1)", fontSize: 13, color: "var(--color-text-2)" }}>
              <Icon name="checkCircle" size={15} color="var(--color-success)" style={{ flexShrink: 0 }} />
              {t("payview.dispute.sent", { company: co.name })}
            </div>
          ) : disputeOpen ? (
            <div className="q-fade" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              <Textarea value={disputeText} onChange={e => setDisputeText(e.target.value)} rows={3} maxLength={140} placeholder={t("payview.dispute.ph", { company: co.name })} />
              <Button variant="secondary" size="lg" full icon="send" disabled={!disputeText.trim()} onClick={sendDispute}>{t("payview.dispute.send", { company: co.name })}</Button>
            </div>
          ) : (
            <Button variant="ghost" size="lg" full icon="alert" onClick={() => setDisputeOpen(true)}>{t("payview.dispute")}</Button>
          )}

          {/* referral footer: every pay page sells the next account */}
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, padding: "10px 0 2px", textAlign: "center" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
              <Q.NitaMark size={16} />
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--color-text-2)" }}>{t("paper.madewith")}</span>
            </div>
            <span style={{ fontSize: 12, color: "var(--color-text-3)" }}>{t("payview.cta")}</span>
          </div>
        </div>

        {window.A4 && <window.A4.Viewer api={api} inv={inv} open={viewerOpen} onClose={() => setViewerOpen(false)} balance={tot.balance} />}
      </Sheet>
    );
  }
  function PayRow({ icon, title, value, onCopy, sage }) {
    /* Direct copy feedback: the trailing copy glyph flips to a check for
       ~1.1s on tap so "tap to copy IBAN" confirms in place, not only via
       the toast. */
    const [copied, setCopied] = useState(false);
    const copiedRef = useRef(0);
    const handle = () => {
      onCopy && onCopy();
      setCopied(true);
      clearTimeout(copiedRef.current);
      copiedRef.current = setTimeout(() => setCopied(false), 1100);
    };
    useEffect(() => () => clearTimeout(copiedRef.current), []);
    return (
      <button onClick={handle} 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}<span key={copied ? "ck" : "cp"} className="q-pop" style={{ display: "inline-flex" }}><Icon name={copied ? "check" : "copy"} size={12} color={copied ? "var(--color-success)" : "var(--color-text-3)"} /></span></div>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-2)", wordBreak: "break-all", marginTop: 2 }}>{value}</div>
        </div>
      </button>
    );
  }

  /* ── Client quote view ───────────────────────────────────────
     What the client sees at the quote link: the amount, the lines, the
     validity date, and one gold tap to approve. Owner-side preview sheet,
     mounted from app.jsx keyed by quoteId. */
  function ClientQuoteView({ api, invoiceId, onClose }) {
    const { db, setDb, today } = api;
    const inv = db.INVOICES.find(i => i.id === invoiceId);
    const co = db.COMPANY;
    const [state, setState] = useState("idle"); // idle | accepting | done
    if (!inv || inv.kind !== "quote") return null;
    const tot = LIB.totals(inv);
    const eff = LIB.effectiveStatus(inv, today);

    /* 900ms theater, then the one true engine write. */
    const accept = () => {
      setState("accepting");
      setTimeout(() => {
        setDb(d => LIB.acceptQuote(d, invoiceId, today));
        setState("done");
      }, 900);
    };
    const decline = () => {
      setDb(d => LIB.declineQuote(d, invoiceId, today));
      Q.toast(t("toast.declined"), "x");
      onClose();
    };

    return (
      <Sheet open onClose={onClose} height="auto" title={t("quoteview.title")}>
        <div style={{ padding: "0 20px 26px", display: "flex", flexDirection: "column", gap: 12 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--color-text-3)" }}>
            <Icon name="eye" size={14} style={{ flexShrink: 0 }} />
            {t("quoteview.preview")}
          </div>

          {state === "done" && (
            <div className="q-fade" style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 10, padding: "8px 0 2px" }}>
              <PaidBurst size={110} />
              <div className="q-pop" style={{ width: 56, height: 56, borderRadius: "50%", background: "var(--color-success)", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 6px 20px color-mix(in oklab, var(--color-success) 30%, transparent)" }}>
                <Icon name="check" size={28} color="#fff" />
              </div>
              <div style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-1)" }}>{t("quoteview.accepted")}</div>
            </div>
          )}

          {/* the page the client lands on */}
          <Q.Card pad={0} style={{ overflow: "hidden" }}>
            <div style={{ padding: "16px 16px 14px", display: "flex", alignItems: "center", gap: 12, borderBottom: "1px solid var(--color-border)" }}>
              <Avatar name={co.name} size={38} square />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{co.name}</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", marginTop: 1 }}>{inv.id}</div>
              </div>
              <StatusPill status={eff} solid />
            </div>

            <div style={{ padding: "14px 16px 16px" }}>
              <Kicker>{t("quoteview.amount")}</Kicker>
              <div style={{ marginTop: 4 }}>
                <AnimatedNumber amount={tot.total} currency={inv.currency} size={28} animKey={inv.id + ":quote"}
                  style={{ fontFamily: "var(--font-serif)", letterSpacing: "-0.02em", lineHeight: 1.05, fontVariantNumeric: "normal" }} />
              </div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", marginTop: 8 }}>{t("quote.validuntil", { date: window.fmtDate(inv.due) })}</div>
            </div>

            {(inv.items || []).length > 0 && (
              <div style={{ borderTop: "1px solid var(--color-border)", padding: "12px 16px 16px" }}>
                <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
                  {inv.items.map(it => (
                    <div key={it.id} style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
                      <span style={{ flex: 1, minWidth: 0, fontSize: 13, color: "var(--color-text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.description}</span>
                      <Money amount={LIB.lineNet(it)} currency={inv.currency} cents={false} size={13} />
                    </div>
                  ))}
                </div>
              </div>
            )}

            {state !== "done" && (
              <div style={{ borderTop: "1px solid var(--color-border)", padding: "14px 16px 18px" }}>
                {eff === "quote" ? (
                  <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                    <Button variant="primary" size="lg" full icon="check" disabled={state === "accepting"} onClick={accept}>
                      {state === "accepting" ? t("send.sending") : t("quoteview.accept")}
                    </Button>
                    <Button variant="ghost" size="lg" full onClick={decline}>{t("quoteview.decline")}</Button>
                  </div>
                ) : eff === "expired" ? (
                  <div style={{ fontSize: 13, color: "var(--color-text-2)" }}>{t("quoteview.expired", { company: co.name })}</div>
                ) : (
                  <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "var(--color-text-2)" }}>
                    <Icon name={eff === "accepted" ? "checkCircle" : "x"} size={15} color={eff === "accepted" ? "var(--color-success)" : "var(--color-error)"} style={{ flexShrink: 0 }} />
                    {eff === "accepted" ? t("quoteview.accepted") : t("quoteview.declined")}
                  </div>
                )}
              </div>
            )}
          </Q.Card>
        </div>
      </Sheet>
    );
  }

  /* ── Deposit request ─────────────────────────────────────────
     Pick a percentage of the quote TTC; Nita drafts the deposit invoice
     and lands on its preview. Mounted from app.jsx keyed by quoteId. */
  function DepositSheet({ api, quoteId, onClose }) {
    const quote = api.db.INVOICES.find(i => i.id === quoteId);
    const [pick, setPick] = useState(30); // 30 | 50 | 'custom'
    const [customPct, setCustomPct] = useState("30");
    if (!quote || quote.kind !== "quote") return null;
    const total = LIB.totals(quote).total;
    const pct = pick === "custom"
      ? Math.max(1, Math.min(100, Math.round(Number(customPct) || 0)))
      : pick;
    const valid = pick !== "custom" || Number(customPct) > 0;
    const amount = LIB.round2(total * pct / 100);

    const create = () => {
      const r = LIB.createDeposit(api.db, quoteId, pct, api.today);
      api.setDb(() => r.db);
      onClose();
      api.goTab("invoices", "invoice-preview", { id: r.invoiceId, fresh: true });
      Q.toast(t("toast.depositCreated", { id: r.invoiceId }), "coins");
    };

    const chip = (selected) => ({
      flex: 1, height: 36, borderRadius: 8, cursor: "pointer",
      border: selected ? "1.4px solid var(--color-text-1)" : "1.4px solid var(--color-border-strong)",
      background: selected ? "var(--color-surface-1)" : "var(--color-surface-0)",
      color: selected ? "var(--color-text-1)" : "var(--color-text-2)",
      fontSize: 13, fontWeight: 600, fontFamily: "var(--font-mono)",
    });

    return (
      <Sheet open onClose={onClose} height="auto" title={t("dep.title")}>
        <div style={{ padding: "4px 20px 26px", display: "flex", flexDirection: "column", gap: 14 }}>
          <div style={{ fontSize: 13, color: "var(--color-text-2)" }}>{t("dep.sub", { id: quoteId })}</div>

          <div style={{ display: "flex", gap: 8 }}>
            {[30, 50].map(p => (
              <button key={p} onClick={() => setPick(p)} style={chip(pick === p)}>{p}%</button>
            ))}
            <button onClick={() => setPick("custom")} style={chip(pick === "custom")}>{t("dep.custom")}</button>
          </div>
          {pick === "custom" && (
            <div style={{ position: "relative" }}>
              <Input value={customPct} onChange={e => setCustomPct(e.target.value.replace(/[^\d]/g, ""))} type="text" inputMode="numeric" mono aria-label={t("dep.custom")} style={{ paddingRight: 34, fontSize: 17, fontWeight: 600 }} />
              <span style={{ position: "absolute", right: 14, top: "50%", transform: "translateY(-50%)", fontFamily: "var(--font-mono)", fontSize: 15, color: "var(--color-text-3)" }}>%</span>
            </div>
          )}

          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 14px", background: "var(--color-surface-1)", borderRadius: 12 }}>
            <div>
              <div style={{ fontSize: 13, color: "var(--color-text-2)" }}>{t("dep.amount")}</div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", marginTop: 2 }}>{t("dep.of.total", { pct, total: window.fmtMoney(total, quote.currency) })}</div>
            </div>
            <Money amount={amount} currency={quote.currency} size={17} />
          </div>

          <Button variant="primary" size="lg" full icon="coins" disabled={!valid} onClick={create}>{t("dep.create")}</Button>
        </div>
      </Sheet>
    );
  }

  /* ── Download picker (the Invoices "download" action) ─────────
     Replaces the old "dump everything to one CSV". Two labelled choices —
     Scope (All / Filtered) × Format (CSV / Accountant pack) — with an honest
     row-count signal so the user knows exactly what leaves the app. */
  function DownloadSheet({ api, ids, from, to, onClose }) {
    const { db, today } = api;
    /* The opener (api.openDownload) distinguishes two states by contract:
         ids === null  → NO filter is active on the Invoices screen → All only.
         ids is array  → a filter IS active; the array is exactly the shown ids,
                         and ids === [] means the filter matched NOTHING.
       The old code treated [] the same as "no filter" and silently dumped the
       whole book — export-ignores-empty-filter. We now keep 'filtered' as a
       real (empty) scope so the user can't accidentally export everything. */
    const hasFilter = Array.isArray(ids);
    const filteredIds = hasFilter ? ids : null;
    const [scope, setScope] = useState(hasFilter ? "filtered" : "all");
    const [format, setFormat] = useState("csv");

    /* Pack period defaults to the current calendar year when the opener gave
       none — mirrors screen-taxes' span handling. */
    const year = isoDay(today).slice(0, 4);
    const period = { from: from || (year + "-01-01"), to: to || (year + "-12-31") };

    /* All = what the Invoices header counts: real invoices/credits, NOT quotes
       (devis aren't invoices). Ties the "All (N)" hint to the header's count so
       Export "All" and the list header agree — export-all-count-mismatch.
       (csvInvoices "All" path mirrors this exclusion in lib.jsx.) */
    const allCount = (db.INVOICES || []).filter(x => x.kind !== "quote").length;
    const scopeIds = scope === "filtered" ? (filteredIds || []) : null;
    /* Honest signal: how many invoice rows the CSV will contain. */
    const count = scope === "filtered" ? (filteredIds ? filteredIds.length : 0) : allCount;
    /* Filtered scope that matched nothing → there is nothing to export. */
    const emptyScope = scope === "filtered" && count === 0;
    const isPack = format === "pack";

    const run = () => {
      if (isPack) {
        const files = LIB.accountantPack(db, { from: period.from, to: period.to, today });
        files.forEach((f, i) => setTimeout(() => Q.downloadFile(f.name, f.content, f.mime), i * 350));
        setTimeout(() => Q.toast(t("toast.packExported", { count: files.length }), "download"), files.length * 350);
      } else {
        Q.downloadFile("invoices-" + isoDay(today) + ".csv", LIB.csvInvoices(db, { today, ids: scopeIds || undefined }), "text/csv;charset=utf-8");
        Q.toast(t("dl.done"), "download");
      }
      onClose();
    };

    return (
      <Sheet open onClose={onClose} height="auto" title={t("dl.title")}>
        <div style={{ padding: "2px 16px 26px", display: "flex", flexDirection: "column", gap: 18 }}>
          {/* Scope — hidden for the accountant pack: the pack is a fixed
              period export (full year), so a per-scope choice would be a dead
              control that the run() path ignores anyway (pack-scope-ignored). */}
          {!isPack && (
            <div>
              <Kicker style={{ marginBottom: 8 }}>{t("dl.scope")}</Kicker>
              <Segmented value={scope} onChange={setScope} options={[
                { value: "all", label: t("dl.scope.all") },
                ...(hasFilter ? [{ value: "filtered", label: t("dl.scope.filtered") }] : []),
              ]} />
              <div style={{ fontSize: 12, color: emptyScope ? "var(--color-accent)" : "var(--color-text-3)", marginTop: 8 }}>{t("dl.scope.hint", { count })}</div>
            </div>
          )}
          {/* Format */}
          <div>
            <Kicker style={{ marginBottom: 8 }}>{t("dl.format")}</Kicker>
            <Segmented value={format} onChange={setFormat} options={[
              { value: "csv", label: t("dl.format.csv") },
              { value: "pack", label: t("dl.format.pack") },
            ]} />
            {/* The pack covers a fixed period (not the invoice list), so signal
                the period the export spans instead of a misleading row count
                (download-pack-count-misleading). */}
            {isPack && (
              <div style={{ fontSize: 12, color: "var(--color-text-3)", marginTop: 8, fontFamily: "var(--font-mono)" }}>{window.fmtDate(period.from)} – {window.fmtDate(period.to)}</div>
            )}
          </div>
          <Button variant="primary" size="lg" full icon="download" disabled={!isPack && emptyScope} onClick={run}>{t("dl.cta")}</Button>
        </div>
      </Sheet>
    );
  }

  window.QScreens = Object.assign(window.QScreens || {}, { SendSheet, PaymentSheet, CreateSheet, ClientPayView, ClientQuoteView, DepositSheet, DownloadSheet });
})();
