/* screen-home.jsx — dashboard → window.QScreens.Home + NotificationsSheet */
(function () {
  const Q = window.Q, LIB = window.LIB, Icons = window.Icons;
  const { Icon } = Icons;

  /* Grow-from-left fill for the outstanding split-bar segments (the screen's
     one money-discipline visual — it should draw itself). scaleX works on the
     flex:1 'current' segment where a width animation can't. Injected once;
     globally honors prefers-reduced-motion via the index.html media query. */
  if (typeof document !== "undefined" && !document.getElementById("q-home-anim")) {
    const s = document.createElement("style");
    s.id = "q-home-anim";
    s.textContent = "@keyframes q-grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }";
    document.head.appendChild(s);
  }
  const { AppBar, Card, AnimatedNumber, Kicker, Button, StatusPill, Sparkle, Avatar, EntityLink } = Q;
  const t = window.t;

  /* Activity row icon/color: reuse the audit-trail styling from LIB so every
     event type (ai, sent, paid, overdue, expense, reminder, ...) renders. */
  const actIcon = (type) => {
    const base = String(type || "").split(".")[0];
    return { name: LIB.auditIcon(base), color: LIB.auditColor(base) };
  };

  /* Render a localized template with React nodes substituted for {tokens}.
     `vars` are plain t() params (pre-formatted); tokens covered by `nodes`
     stay as placeholders for node substitution. */
  function tNodes(key, nodes, vars) {
    return t(key, vars).split(/(\{\w+\})/g).map((part, i) => {
      const m = /^\{(\w+)\}$/.exec(part);
      if (!m) return part;
      return m[1] in nodes ? <React.Fragment key={i}>{nodes[m[1]]}</React.Fragment> : part;
    });
  }

  /* Activity/nudge ref router (the :71-75 pattern, shared with the drawer). */
  function openActivityRef(api, ref) {
    if (!ref) return;
    if (String(ref).indexOf("exp") === 0) api.go("expense", { id: ref });
    else api.goTab("invoices", "invoice", { id: ref });
  }

  /* Navigation for a nudge ref ({kind,id,name,token}, shape 1.7). */
  function refGo(api, r) {
    if (r.kind === "client") api.goTab("clients", "client", { id: r.id });
    else if (r.kind === "project") api.goTab("clients", "project", { id: r.id });
    else if (r.kind === "expense") api.go("expense", { id: r.id });
    else if (r.kind === "catalog") api.go("catalog");
    else openActivityRef(api, r.id);
  }

  /* Build the {token} -> EntityLink map from nudge.refs; params not covered
     by a ref interpolate as plain text through t(). */
  function nudgeBody(api, nudge, color, beforeNav) {
    const nodes = {};
    (nudge.refs || []).forEach(r => {
      nodes[r.token] = (
        <EntityLink kind={r.kind} name={r.name} mono={r.kind === "invoice"} icon={null}
          size={r.kind === "invoice" ? 13 : 14} color={color}
          onClick={() => { if (beforeNav) beforeNav(); refGo(api, r); }}
          style={{ verticalAlign: "baseline", padding: "12px 0", margin: "-12px 0" }} />
      );
    });
    const vars = {};
    Object.keys(nudge.params || {}).forEach(k => { if (!(k in nodes)) vars[k] = nudge.params[k]; });
    return tNodes(nudge.key, nodes, vars);
  }

  /* CTA dispatch (work order #3): remind opens the SendSheet, route
     navigates, recurrence promotes the candidate's last invoice to a
     template (the EXACT contract call). */
  function dispatchNudge(api, nudge, beforeNav) {
    const nav = nudge.cta && nudge.cta.nav;
    if (!nav) return;
    if (nav.kind === "recurrence") {
      api.setDb(d => LIB.enableRecurrence(d, nav.lastId, "month"));
      Q.toast(t("rec.suggest.done"));
      return;
    }
    if (beforeNav) beforeNav();
    if (nav.kind === "remind") api.openSend(nav.invoiceId, "remind");
    else if (nav.kind === "route") api.goTab(nav.tab, nav.route, nav.params || {});
  }

  /* home-calm-cap-hides-getpaid-nudge: collection/get-paid nudges outrank
     budget/stock so the calm cap (1 visible with the digest, else 2) can never
     bury "send this draft / chase this quote / bill these expenses" beneath a
     project-budget or low-stock notice. Lower = higher priority; kinds not
     listed keep their emission order after the ranked ones (stable sort). */
  const NUDGE_RANK = { overdue: 0, silentQuote: 1, staleDraft: 2, expenses: 3, vatdue: 4, budget: 5, stock: 6 };
  const nudgeRank = (n) => (n && n.kind in NUDGE_RANK ? NUDGE_RANK[n.kind] : 99);

  /* The calm cap: when the digest renders, Home shows 1 nudge and drops
     kind 'overdue' from the visible set; otherwise 2. Everything else
     stays reachable in the notifications drawer (overflow). */
  function nudgeSplit(db, today) {
    const digest = LIB.dunningDigest ? LIB.dunningDigest(db, today) : null;
    const runs = LIB.upcomingRuns ? LIB.upcomingRuns(db, today) : [];
    const digestOn = !!(digest && (digest.lateClients > 0 || digest.sentThisWeek > 0)) || runs.length > 0;
    const rules = LIB.nudges ? LIB.nudges(db, today) : [];
    /* stable get-paid-first re-rank: collection nudges float above budget/stock
       before the cap slices, so the surviving nudge is the most money-relevant. */
    const ranked = rules.map((n, i) => ({ n, i }))
      .sort((a, b) => (nudgeRank(a.n) - nudgeRank(b.n)) || (a.i - b.i))
      .map(x => x.n);
    const pool = digestOn ? ranked.filter(n => n.kind !== "overdue") : ranked;
    const visible = pool.slice(0, digestOn ? 1 : 2);
    const seen = {};
    visible.forEach(n => { seen[n.id] = true; });
    const overflow = ranked.filter(n => !seen[n.id]);
    return { digest, runs, digestOn, visible, overflow };
  }

  /* One "Nita noticed" card (Card ai pattern, F8 rules). */
  function NudgeCard({ api, nudge }) {
    const color = nudge.kind === "overdue" ? "var(--color-accent)" : "var(--color-text-1)";
    const remind = nudge.cta && nudge.cta.nav && nudge.cta.nav.kind === "remind";
    const dismissKey = nudge.kind === "recurrence" ? "rec.suggest.dismiss" : "nudge.dismiss";
    return (
      <Card ai pad={16}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
          <Q.AiKicker style={{ flex: 1, minWidth: 0 }}>{t("home.ai.kicker")}</Q.AiKicker>
          {nudge.badge === "accent" && (
            <span aria-hidden="true">
              <Q.Badge tone="accent"><Icon name="alert" size={10} /></Q.Badge>
            </span>
          )}
        </div>
        <div style={{ fontSize: 14, fontWeight: 600, color, lineHeight: 1.45 }}>
          {nudgeBody(api, nudge, color)}
        </div>
        <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
          <Button variant="outline" size="md" icon={remind ? "send" : undefined}
            style={{ background: "var(--color-surface-0)", borderColor: "var(--color-primary)", color: "var(--color-primary)" }}
            onClick={() => dispatchNudge(api, nudge)}><Sparkle size={14} />{t(nudge.cta.key)}</Button>
          <Button variant="ghost" size="md" onClick={() => api.dismissNudge(nudge.id)}>{t(dismissKey)}</Button>
        </div>
      </Card>
    );
  }

  function MiniStat({ label, amount, currency, count, sub, tone }) {
    const dot = { accent: "var(--color-accent)", success: "var(--color-success)", info: "var(--color-info)" }[tone];
    return (
      <Card dashed pad={14} style={{ flex: 1 }}>
        <div style={{ minHeight: 28, display: "flex", alignItems: "flex-end", gap: 6 }}>
          {dot && <span aria-hidden="true" style={{ width: 6, height: 6, borderRadius: "50%", background: dot, flexShrink: 0, marginBottom: 2 }} />}
          <Kicker>{label}</Kicker>
        </div>
        <div style={{ marginTop: 8 }}><AnimatedNumber amount={amount} currency={currency} size={19} color="var(--color-gold)" cents={false} animKey={label} /></div>
        <div style={{ fontSize: 12, color: "var(--color-text-3)", marginTop: 3, fontFamily: "var(--font-mono)" }}>{sub}</div>
      </Card>
    );
  }

  /* `wide` (home-quickactions-orphan-tile): a lone tile in the final grid row
     spans the full width and lays its icon/label out horizontally so it reads
     as a deliberate full-width action instead of an orphaned stub. */
  function QuickAction({ icon, label, sub, onClick, wide }) {
    const [pressed, setPressed] = React.useState(false);
    const press = (v) => () => setPressed(v);
    return (
      <button onClick={onClick} className="q-tap"
        onPointerDown={press(true)} onPointerUp={press(false)}
        onPointerLeave={press(false)} onPointerCancel={press(false)}
        style={{
        flex: 1, minWidth: 0, display: "flex",
        flexDirection: wide ? "row" : "column",
        alignItems: "center", gap: wide ? 12 : 8,
        gridColumn: wide ? "1 / -1" : undefined,
        background: "var(--color-surface-0)", border: "1.4px solid var(--color-border-strong)",
        borderRadius: 12, padding: wide ? "12px 14px" : "10px 6px", cursor: "pointer",
        transition: "transform .12s cubic-bezier(.32,.72,0,1), box-shadow .12s ease",
        transform: pressed ? "scale(0.96)" : "scale(1)",
        boxShadow: pressed ? "none" : "0 1px 2px hsl(35 16% 5% / 0.04)",
      }}>
        <div style={{ width: 32, height: 32, borderRadius: 8, background: "var(--color-surface-1)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--color-text-2)", flexShrink: 0 }}>
          <Icon name={icon} size={17} />
        </div>
        <span style={{ fontSize: 12, fontWeight: 600, color: "var(--color-text-1)", textAlign: wide ? "left" : "center", lineHeight: 1.2 }}>{label}</span>
        {sub != null && (
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.06em", lineHeight: 1.2, color: "var(--color-text-3)", maxWidth: "100%", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", textAlign: wide ? "left" : "center", whiteSpace: "normal", overflow: "hidden", marginLeft: wide ? "auto" : 0 }}>{sub}</span>
        )}
      </button>
    );
  }

  /* 7px terracotta unread marker, absolutely placed in the row gutter so
     read/unread rows keep their alignment. */
  const UnreadDot = () => (
    <span aria-hidden="true" style={{
      position: "absolute", left: 5, top: "50%", transform: "translateY(-50%)",
      width: 7, height: 7, borderRadius: "50%", background: "var(--color-accent)",
    }} />
  );

  /* home-calm: the ONE 'what needs me today' focus. When money is late it
     names a single person to chase (LIB.worstPayer) + the overdue total and
     offers the chase CTA inline; otherwise it states calm. This replaces the
     three-up MiniStat density spike as the lead block. All wiring reused:
     api.openSend (remind), api.goTab('invoices',{filter:'overdue'}). */
  function FocusCard({ api, od, worst, overdueInv }) {
    const cur = api.db.COMPANY.currency;
    const late = od.total > 0 && od.count > 0;
    if (!late) {
      return (
        <Card pad={18} style={{ display: "flex", alignItems: "center", gap: 14 }}>
          <div aria-hidden="true" style={{ width: 46, height: 46, borderRadius: 13, background: "color-mix(in oklab, var(--color-success) 14%, transparent)", color: "var(--color-success)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="check" size={22} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <Kicker>{t("home.focus.kicker")}</Kicker>
            <div style={{ fontSize: 16, fontWeight: 600, color: "var(--color-text-1)", marginTop: 4, lineHeight: 1.35 }}>{t("home.focus.allclear")}</div>
          </div>
        </Card>
      );
    }
    const name = worst && worst.client ? worst.client.name : null;
    return (
      <Card pad={18} style={{ position: "relative", overflow: "hidden" }}>
        <Kicker>{t("home.focus.kicker")}</Kicker>
        <div style={{ marginTop: 8, display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>
          <AnimatedNumber amount={od.total} currency={cur} cents={false} size={34} weight={600} color="var(--color-accent)" animKey="home.focus"
            style={{ fontFamily: "var(--font-serif)", lineHeight: 1.05, letterSpacing: "-0.02em" }} />
          <span style={{ fontSize: 13, color: "var(--color-text-2)" }}>{od.count === 1 ? t("home.focus.lateinv.one") : t("home.focus.lateinv", { count: od.count })}</span>
        </div>
        {name && (
          <div style={{ fontSize: 15, fontWeight: 600, color: "var(--color-text-1)", marginTop: 8, lineHeight: 1.4 }}>
            {t("home.focus.chase", { name, days: worst.daysLate })}
          </div>
        )}
        <div style={{ marginTop: 14 }}>
          {overdueInv && (
            <Button variant="secondary" size="md" icon="send" onClick={() => api.openSend(overdueInv.id, "remind")}>{t("home.q.remind")}</Button>
          )}
        </div>
      </Card>
    );
  }

  function Home({ api }) {
    const { db, today } = api;
    const sumAll = LIB.summary(db, today);
    const sum = sumAll.base || sumAll; // tolerate both the flat and { base, fx } summary shapes
    const fx = (Array.isArray(sumAll.fx) ? sumAll.fx : [])
      .filter(f => f && f.currency && isFinite(Number(f.outstanding != null ? f.outstanding : f.amount)));
    const cur = db.COMPANY.currency;
    /* home-fx-line-only-outstanding: the FX summary line only carries the
       OUTSTANDING balance per foreign currency, so a late USD invoice was
       invisible as "overdue" on Home. Compute the overdue slice per non-home
       currency (gross balance, mirroring the hero's overdue predicate) so the
       FX note can flag what is actually LATE abroad, not just outstanding. */
    const fxOverdue = {};
    for (const inv of db.INVOICES) {
      if (inv.kind !== "invoice" || LIB.effectiveStatus(inv, today) !== "overdue") continue;
      const c = inv.currency || cur;
      if (c === cur) continue;
      fxOverdue[c] = LIB.round2((fxOverdue[c] || 0) + LIB.totals(inv).balance);
    }
    /* Greeting personalization (CONTRACT db.COMPANY.owner): onboarding's
       "Your name" writes owner; fall back to the company name so a fresh user
       who skipped the name still gets greeted by something, not a bare
       "Good evening." */
    const firstName = ((db.COMPANY.owner || db.COMPANY.name || "").trim().split(/\s+/)[0]) || "";
    const hour = new Date().getHours();
    const greet = t(hour >= 18 || hour < 5 ? "home.greeting.evening" : hour >= 12 ? "home.greeting.afternoon" : "home.greeting.morning");

    // most overdue unpaid invoice (drives the third quick action)
    const overdueInv = db.INVOICES
      .filter(i => i.kind !== "quote" && LIB.effectiveStatus(i, today) === "overdue")
      .sort((a, b) => new Date(a.due) - new Date(b.due))[0];

    /* home-two-overdue-numbers-disagree (P1-5): the hero "Overdue" stat and
       the Autopilot digest must read the SAME figure to the cent. Both now
       draw from the canonical LIB.overdueSummary (home-currency, credit-netted)
       — dunningDigest.lateTotal is already overdueSummary.total — so a late
       account never sees two different "overdue" totals. */
    const od = LIB.overdueSummary(db, today);

    const pipe = LIB.quotePipeline ? LIB.quotePipeline(db, today) : null;
    /* Average days to get paid (completeness #9): the purest get-paid metric.
       Pulled from the SAME LIB.insights source the Insights screen uses, so the
       two never disagree; null until at least one invoice has been paid (no
       fake "0 days" velocity for a brand-new account). */
    const avgDays = LIB.insights ? LIB.insights(db, today).daysToPaidAvg : null;
    const { digest, runs, digestOn, visible } = nudgeSplit(db, today);
    const nextRun = runs[0];
    const unread = LIB.notifications ? LIB.notifications(db, today).unread : 0;
    const es = LIB.expenseSummary(db);
    const decl = LIB.nextDeclaration ? LIB.nextDeclaration(db, today) : null;
    const showTax = !!(decl && decl.daysLeft <= 30);
    const taxUrgent = !!(decl && decl.daysLeft <= 7);

    // Feed must read newest-first regardless of seed/insertion order.
    const activity = [...db.ACTIVITY].sort((a, b) => new Date(b.at) - new Date(a.at));
    const feed = activity.slice(0, 4); // Home shows 4; full feed lives in the notifications drawer (home.viewall)

    const openRef = (ref) => openActivityRef(api, ref);
    /* home-calm: one person to chase (CONTRACT LIB.worstPayer). */
    const worst = LIB.worstPayer ? LIB.worstPayer(db, today) : null;
    /* home-calm: secondary info (Paid/Pipeline, manual path, quick actions,
       activity feed) opens collapsed so the home leads with the focus +
       the headline number only. Everything stays reachable on tap. */
    const [showMore, setShowMore] = React.useState(false);

    return (
      <div style={{ paddingBottom: 4 }}>
        <AppBar large kicker={window.fmtDate(today, "kicker")} title={firstName ? `${greet}, ${firstName}.` : `${greet}.`}
          right={<Q.BadgedIconButton name="bell" count={unread} label={t("home.notifications")} onClick={api.openNotifications} />} />

        <div className="q-stagger" style={{ padding: "8px 16px 0", display: "flex", flexDirection: "column", gap: 16 }}>
          {/* home-calm: the ONE focus — who to chase + the number that matters */}
          <FocusCard api={api} od={od} worst={worst} overdueInv={overdueInv} />

          {/* Outstanding hero */}
          <Card pad={18} style={{ position: "relative", overflow: "hidden" }}>
            <Kicker>{t("home.outstanding")}</Kicker>
            <div style={{ marginTop: 8, display: "flex", alignItems: "baseline", gap: 10 }}>
              <AnimatedNumber amount={sum.outstanding} currency={cur} cents={false} size={44} weight={600} color="var(--color-gold)" animKey="home.hero"
                style={{ fontFamily: "var(--font-serif)", lineHeight: 1.05, letterSpacing: "-0.02em" }} />
            </div>
            <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 8 }}>{sum.outCount === 1 ? t("home.outstanding.sub.one") : t("home.outstanding.sub", { count: sum.outCount })}</div>
            {fx.length > 0 && (
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-text-3)", marginTop: 4 }}>
                {"+ " + fx.map(f => window.fmtMoney(Number(f.outstanding != null ? f.outstanding : f.amount), f.currency, { cents: false })).join(" · ")}
              </div>
            )}
            {/* home-fx-line-only-outstanding: surface what is LATE abroad so a
               foreign-currency overdue invoice is never silently dropped from
               the money-discipline story (accent = it's late, go chase it). */}
            {Object.keys(fxOverdue).filter(c => fxOverdue[c] > 0).length > 0 && (
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-accent)", marginTop: 3, fontWeight: 600 }}>
                {t("home.legend.overdue") + ": " + Object.keys(fxOverdue).filter(c => fxOverdue[c] > 0).map(c => window.fmtMoney(fxOverdue[c], c, { cents: false })).join(" · ")}
              </div>
            )}
            {/* split bar: overdue vs current — fades in after the hero count
               settles (~120ms), then the segments draw themselves from the left
               so the money figure lands first and its composition follows. */}
            {sum.outstanding > 0 && (
              <div className="q-fade-up" style={{ marginTop: 14, animationDelay: ".12s" }}>
                <div style={{ height: 8, borderRadius: 999, overflow: "hidden", display: "flex", gap: 2, background: "var(--color-surface-2)" }}>
                  {od.total > 0 && <div style={{ width: `${Math.max(6, Math.min(100, (od.total / sum.outstanding) * 100))}%`, background: "var(--color-accent)", transformOrigin: "left", animation: "q-grow-x .42s cubic-bezier(.32,.72,0,1) .22s both" }} />}
                  <div style={{ flex: 1, background: "var(--color-gold)", opacity: 0.85, transformOrigin: "left", animation: "q-grow-x .42s cubic-bezier(.32,.72,0,1) .22s both" }} />
                </div>
              </div>
            )}
            {/* Average days to get paid (completeness #9): the purest get-paid
               velocity, anchored to the outstanding hero. text-1 not gold — a
               day count isn't money; taps through to the full Insights surface.
               Only rendered once a payment exists, so a fresh account never sees
               a hollow "0 days". */}
            {avgDays != null && (
              <button onClick={() => api.go("insights")} className="q-tap" style={{
                marginTop: 14, paddingTop: 12, width: "100%", textAlign: "left", cursor: "pointer",
                background: "none", border: "none", borderTop: "1px solid var(--color-border)",
                display: "flex", alignItems: "baseline", gap: 8,
              }}>
                <span style={{ flex: 1, minWidth: 0, fontFamily: "var(--font-mono)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--color-text-3)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{t("insights.daysToPaid")}</span>
                <span style={{ fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: 17, color: "var(--color-text-1)", letterSpacing: "-0.01em", flexShrink: 0 }}>{t("insights.daysToPaid.value", { d: avgDays })}</span>
                <Icon name="chevronRight" size={15} color="var(--color-text-3)" style={{ flexShrink: 0, alignSelf: "center" }} />
              </button>
            )}
          </Card>

          {/* Autopilot digest relocated into the More disclosure (home-calm:
             it duplicated the FocusCard's chase above the fold). */}

          {/* Nita noticed (rules-driven nudges, calm-capped) */}
          {visible.map(n => <NudgeCard key={n.id} api={api} nudge={n} />)}

          {/* Get-paid-faster CTA — invoice mechanic, collection framing (home.new) */}
          <button onClick={() => api.openComposer()} className="q-tap" style={{
            textAlign: "left", cursor: "pointer", border: "1.4px solid var(--color-primary)",
            borderRadius: "var(--radius-lg)", padding: 18, background: "var(--color-primary-muted)",
            display: "flex", alignItems: "center", gap: 14, transition: "filter .12s",
          }}>
            <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 hsl(155 32% 34% / 0.3)" }}>
              <Sparkle size={20} color="var(--color-primary-fg)" />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-1)", letterSpacing: "-0.01em" }}>{t("home.new")}</div>
              <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 1 }}>{t("home.new.sub")}</div>
              <div style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 4, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)" }}>
                <Icon name="mic" size={11} style={{ flexShrink: 0 }} />
                <span>{t("voice.hold.hint")}</span>
              </div>
            </div>
            <Icon name="arrowRight" size={20} color="var(--color-primary)" />
          </button>

          {/* home-calm: secondary surfaces behind one disclosure. Default
             closed so the home opens quiet; every entry below still navigates. */}
          <button onClick={() => setShowMore(v => !v)} className="q-tap" aria-expanded={showMore} style={{
            display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
            background: "none", border: "none", cursor: "pointer", padding: "10px 0",
            fontFamily: "var(--font-mono)", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em",
            color: "var(--color-text-3)",
          }}>
            <span>{showMore ? t("home.more.hide") : t("home.more.show")}</span>
            <Icon name={showMore ? "chevronUp" : "chevronDown"} size={14} />
          </button>

          {showMore && (<>
          {/* secondary stats (moved out of the lead) */}
          <div style={{ display: "flex", gap: 12 }}>
            <MiniStat label={t("home.paid")} amount={sum.paidMonth} currency={cur} sub={sum.paidCount === 1 ? t("home.paid.sub.one") : t("home.paid.sub", { count: sum.paidCount })} tone="success" />
            {pipe && <MiniStat label={t("home.pipeline")} amount={pipe.total} currency={pipe.currency} sub={t("home.pipeline.sub", { count: pipe.openCount + pipe.acceptedCount })} tone="info" />}
          </div>

          {/* Autopilot digest (F4 + F2 upcoming run) — demoted out of the lead */}
          {digestOn && (
            <Card ai pad={16}>
              <div style={{ marginBottom: 8 }}>
                <Q.AiKicker>{t("chase.autopilot")}</Q.AiKicker>
              </div>
              <div style={{ fontSize: 15, fontWeight: 600, color: "var(--color-text-1)", lineHeight: 1.4 }}>
                {!digest || digest.lateClients === 0 ? t("chase.digest.none")
                  : t("home.digest.chasing", { amount: window.fmtMoney(digest.lateTotal, digest.currency, { cents: false }), count: digest.lateClients })}
              </div>
              {digest && digest.sentThisWeek > 0 && (
                <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 4, lineHeight: 1.4 }}>
                  {digest.sentThisWeek === 1 ? t("chase.digest.sent.one") : t("chase.digest.sent", { n: digest.sentThisWeek })}
                </div>
              )}
              {nextRun && (
                <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 13, color: "var(--color-text-2)", marginTop: 4, lineHeight: 1.4 }}>
                  <Icon name="repeat" size={13} style={{ flexShrink: 0 }} />
                  <span>{t("rec.home.next", { client: nextRun.clientName, date: window.fmtDate(nextRun.nextRun, "short") })}</span>
                </div>
              )}
              <div style={{ marginTop: 12 }}>
                <Button variant="secondary" size="sm" onClick={() => api.goTab("invoices", undefined, { filter: "overdue" })}>{t("chase.digest.cta")}</Button>
              </div>
            </Card>
          )}

          {/* equal manual path */}
          <button onClick={() => api.openManual(null)} className="q-tap" style={{
            textAlign: "left", cursor: "pointer", border: "1.4px solid var(--color-border-strong)",
            borderRadius: "var(--radius-lg)", padding: 12, background: "var(--color-surface-1)",
            display: "flex", alignItems: "center", gap: 14, transition: "filter .12s",
          }}>
            <div style={{ width: 36, height: 36, borderRadius: 12, background: "var(--color-surface-2)", color: "var(--color-text-2)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
              <Icon name="keyboard" size={17} />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 700, color: "var(--color-text-1)", letterSpacing: "-0.01em" }}>{t("create.manual.title")}</div>
              <div style={{ fontSize: 13, color: "var(--color-text-2)", marginTop: 1 }}>{t("create.manual.sub")}</div>
            </div>
            <Icon name="arrowRight" size={20} color="var(--color-text-2)" />
          </button>

          {/* Quick actions (merged with Catalog / Expenses / Taxes / Insights entries) */}
          <div>
            <Kicker style={{ marginBottom: 10, marginTop: 4 }}>{t("home.quick")}</Kicker>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 10 }}>
              <QuickAction icon="clients" label={t("home.q.client")} onClick={() => api.switchTab("clients")} />
              <QuickAction icon="file" label={t("home.q.quote")} onClick={() => api.openManual({ kind: "quote" })} />
              {overdueInv
                ? <QuickAction icon="bell" label={t("home.q.remind")} onClick={() => api.openSend(overdueInv.id, "remind")} />
                : <QuickAction icon="scan" label={t("create.expense.title")} onClick={() => api.openExpenseSheet({ mode: "scan" })} />}
              <QuickAction icon="package" label={t("home.catalog")}
                sub={t("home.catalog.sub", { count: (db.CATALOG || []).length })}
                onClick={() => api.go("catalog")} />
              <QuickAction icon="receipt" label={t("home.expenses")}
                sub={es.toBillCount
                  ? <span style={{ color: "var(--color-accent)", fontWeight: 600 }}>{t("home.expenses.tobill", { count: es.toBillCount })}</span>
                  : t("home.expenses.none")}
                onClick={() => api.go("expenses")} />
              <QuickAction icon="barChart" label={t("home.insights")} sub={t("home.insights.sub")} onClick={() => api.go("insights")} />
              {showTax && (
                /* 7th tile: the 6 base tiles fill two full rows, so Taxes is
                   always alone on row 3 — render it full-width (orphan fix). */
                <QuickAction icon="bank" label={t("home.tax")} wide
                  sub={taxUrgent
                    ? <span style={{ color: "var(--color-accent)", fontWeight: 600 }}>{decl.daysLeft < 0 ? t("tax.due.over", { d: -decl.daysLeft }) : t("home.tax.sub", { d: decl.daysLeft })}</span>
                    : (decl.daysLeft < 0 ? t("tax.due.over", { d: -decl.daysLeft }) : t("home.tax.sub", { d: decl.daysLeft }))}
                  onClick={() => api.go("taxes")} />
              )}
            </div>
          </div>

          {/* Recent activity */}
          <div style={{ marginTop: 6 }}>
            <div style={{ display: "flex", alignItems: "center", marginBottom: 6 }}>
              <Kicker style={{ flex: 1 }}>{t("home.activity")}</Kicker>
              <button onClick={() => api.openNotifications()} className="q-tap" style={{ background: "none", border: "none", color: "var(--color-text-2)", fontSize: 13, fontWeight: 600, cursor: "pointer", padding: "12px 10px", margin: "-12px -10px" }}>{t("home.viewall")}</button>
            </div>
            <Card pad={0}>
              {feed.length === 0 ? (
                <div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", padding: "34px 24px", gap: 10 }}>
                  <div aria-hidden="true" style={{ width: 44, height: 44, borderRadius: 13, background: "var(--color-surface-1)", border: "1.4px dashed var(--color-border-strong)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--color-text-3)" }}>
                    <Icon name="clock" size={20} />
                  </div>
                  <div style={{ fontSize: 13, color: "var(--color-text-3)", lineHeight: 1.5, maxWidth: 240 }}>
                    {t("home.activity.empty")}
                  </div>
                </div>
              ) : feed.map((a, i) => {
                const ic = actIcon(a.type);
                const unreadRow = a.read === false;
                return (
                  <div key={a.id} onClick={() => openRef(a.ref)} className="q-row"
                    role={a.ref ? "button" : undefined} tabIndex={a.ref ? 0 : undefined}
                    onKeyDown={a.ref ? (e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openRef(a.ref); } }) : undefined}
                    style={{ position: "relative", display: "flex", gap: 12, alignItems: "center", padding: "12px 16px", borderBottom: i < feed.length - 1 ? "1px solid var(--color-border)" : "none", cursor: a.ref ? "pointer" : "default", animation: `q-fade-up .4s cubic-bezier(.32,.72,0,1) ${0.04 * i}s both` }}>
                    {unreadRow && <UnreadDot />}
                    <div style={{ width: 30, height: 30, borderRadius: 8, background: `color-mix(in oklab, ${ic.color} 13%, transparent)`, color: ic.color, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                      <Icon name={ic.name} size={15} />
                    </div>
                    <div style={{ flex: 1, fontSize: 13, fontWeight: unreadRow ? 600 : 400, color: "var(--color-text-1)", lineHeight: 1.4 }}>
                      {!a.key && a.type === "ai" && <span style={{ color: "var(--color-primary)", fontWeight: 600 }}>{t("brand.name")} </span>}
                      {a.key ? t(a.key, LIB.fmtEventParams(a.params)) : a.text}
                    </div>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", whiteSpace: "nowrap", flexShrink: 0 }}>{a.at ? window.fmtDate(a.at, "short") : a.ago}</span>
                  </div>
                );
              })}
            </Card>
          </div>
          </>)}
        </div>
      </div>
    );
  }

  function Legend({ color, label }) {
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
        <span style={{ width: 8, height: 8, borderRadius: 2, background: color }} />
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)" }}>{label}</span>
      </div>
    );
  }

  /* ── Notifications drawer (F10, mounted app-level: api + onClose) ──
     Pinned Nita nudges beyond Home's visible set, then the full ACTIVITY
     feed bucketed today / this week / earlier with swipe-to-mark-read. */
  function NotificationsSheet({ api, onClose }) {
    const { db, today } = api;
    const { overflow } = nudgeSplit(db, today);
    const pinned = overflow.slice(0, 6); // the recurrence suggestion stays reachable here
    const notif = LIB.notifications(db, today);
    const openRef = (ref) => openActivityRef(api, ref);

    const onNudge = (n) => dispatchNudge(api, n, onClose);

    const title = (
      <span style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
        <span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t("home.notifications")}</span>
        <button onClick={() => api.markRead("all")} className="q-tap" style={{
          background: "none", border: "none", cursor: "pointer", whiteSpace: "nowrap",
          fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600, color: "var(--color-text-2)",
          padding: "12px 6px", margin: "-12px 0 -12px -6px", letterSpacing: 0,
        }}>{t("notif.markall")}</button>
      </span>
    );

    return (
      <Q.Sheet open onClose={onClose} height="88%" title={title}>
        <div style={{ padding: "2px 16px 28px", display: "flex", flexDirection: "column", gap: 16 }}>
          {pinned.length > 0 && (
            <div>
              <Q.AiKicker style={{ marginBottom: 8 }}>{t("home.ai.kicker")}</Q.AiKicker>
              <Card pad={0} style={{ overflow: "hidden" }}>
                {pinned.map((n, i) => (
                  <div key={n.id} className="q-row" role="button" tabIndex={0}
                    onClick={() => onNudge(n)}
                    onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onNudge(n); } }}
                    style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", borderBottom: i < pinned.length - 1 ? "1px solid var(--color-border)" : "none", cursor: "pointer" }}>
                    <Sparkle size={13} color="var(--color-primary)" style={{ flexShrink: 0 }} />
                    <div style={{ flex: 1, fontSize: 13, color: "var(--color-text-1)", lineHeight: 1.45 }}>
                      {nudgeBody(api, n, "var(--color-text-1)", onClose)}
                    </div>
                    <Icon name="chevronRight" size={16} color="var(--color-text-3)" style={{ flexShrink: 0 }} />
                  </div>
                ))}
              </Card>
            </div>
          )}

          {notif.groups.length === 0 ? (
            <Q.EmptyState icon="bell" title={t("notif.empty")} />
          ) : notif.groups.map(g => (
            <div key={g.key}>
              <Kicker style={{ marginBottom: 8 }}>{t("notif." + g.key)}</Kicker>
              <Card pad={0} style={{ overflow: "hidden" }}>
                {g.items.map((a, i) => {
                  const ic = actIcon(a.type);
                  const unreadRow = a.read === false;
                  return (
                    <Q.SwipeRow key={a.id} last={i === g.items.length - 1}
                      actions={[{ icon: "check", label: t("notif.markread"), tone: "success", onPress: () => api.markRead([a.id]) }]}
                      onClick={() => { api.markRead([a.id]); if (a.ref) { onClose(); openRef(a.ref); } }}
                      style={{ background: "var(--color-surface-0)", padding: "12px 16px" }}>
                      {unreadRow && <UnreadDot />}
                      <div style={{ width: 30, height: 30, borderRadius: 8, background: `color-mix(in oklab, ${ic.color} 13%, transparent)`, color: ic.color, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                        <Icon name={ic.name} size={15} />
                      </div>
                      <div style={{ flex: 1, fontSize: 13, fontWeight: unreadRow ? 600 : 400, color: "var(--color-text-1)", lineHeight: 1.4 }}>
                        {a.key ? t(a.key, LIB.fmtEventParams(a.params)) : a.text}
                      </div>
                      <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-3)", whiteSpace: "nowrap", flexShrink: 0 }}>{a.at ? window.fmtDate(a.at, "short") : a.ago}</span>
                    </Q.SwipeRow>
                  );
                })}
              </Card>
            </div>
          ))}
        </div>
      </Q.Sheet>
    );
  }

  window.QScreens = Object.assign(window.QScreens || {}, { Home, NotificationsSheet });
})();
