/* simple/app.jsx — the redesigned shell, DESKTOP edition.
   Same state machine as the mobile simple app, re-laid out as three
   persistent pillars: Yoshi chat (left) · Home (center) · Activity (right),
   under a slim top bar (menu · wordmark · bell). A selection takes over the
   area right of chat; Trade / Transfer render their WIDE desktop two-pane
   layouts (market context + ticket, form + more ways), the rest center.
   No phone chrome: window.StatusBar stays undefined so reused screens skip
   their status-bar row, and the pillar wraps hide per-screen headers. */

/* identify as the web build — flips reused components (Trade / Transfer /
   detail screens) to their desktop layouts and the type scale to web sizes,
   exactly like the main web app. Set before first render. */
window.__YOSHI_WEB = true;

/* a floating confirmation pill — dark elevated surface, rounded, with a check
   glyph. Quiet acknowledgment, not an alert. */
const Toast = ({ msg }) => (
  <div style={{ position: "fixed", left: "50%", transform: "translateX(-50%)", bottom: 26, zIndex: 1600, background: "var(--bg-card)", color: "var(--ink)", border: "1px solid var(--rule-2)", borderRadius: 12, padding: "11px 15px", display: "flex", alignItems: "center", gap: 10, animation: "count-up 240ms ease both", boxShadow: "0 14px 40px -12px rgba(0,0,0,0.55)" }}>
    <span style={{ width: 20, height: 20, flex: "none", borderRadius: 999, background: "color-mix(in srgb, var(--accent) 16%, transparent)", display: "grid", placeItems: "center" }}>
      <Icon name="check" size={12} color="var(--accent)" stroke={2.4} />
    </span>
    <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13), fontWeight: 500, whiteSpace: "nowrap" }}>{msg}</span>
  </div>
);

/* the reused phone sheets are position:absolute inset:0 with their own header +
   close. Instead of a modal, a selection (Move / Trade / Accounts / …) takes
   over the whole area to the right of chat — the center AND the Activity pillar,
   like the main web app's flow panes — with the sheet content centered. Close /
   back returns to Home. willChange:transform makes the inner frame a containing
   block so the sheets' fixed-position sub-sheets (kebab / info / options) anchor
   inside the pane instead of escaping to the browser viewport. */
const CenterPage = ({ wide, onClose, children }) => (
  <div className="tab-swap" style={{ flex: 1, minWidth: 0, position: "relative", display: "flex", justifyContent: "center", overflow: "hidden", background: "var(--bg)" }}>
    <div style={{ position: "relative", width: "100%", maxWidth: wide ? "none" : 720, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden", willChange: "transform" }}>
      {children}
    </div>
    {/* flows whose reused web layout carries no close of its own (Trade /
        Transfer) get one here, so there's always a way back to Home */}
    {onClose && (
      <button className="press" onClick={onClose} aria-label="Close" title="Close" style={{ position: "absolute", top: 12, right: 16, zIndex: 50, background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, width: 32, height: 32, display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer" }}>
        <Icon name="close" size={18} />
      </button>
    )}
  </div>
);

/* compact accent action for the top bar (Move / Trade), matching the labeled
   Briefs bell so the three read as one cluster */
/* topbar quick action on the Btn atom — filled = solid accent (primary);
   otherwise an accent outline with white (ink) icon + label */
const TOPBAR_ACTION_WIDTH = 118;
const TopAction = ({ icon, label, onClick, filled }) => (
  <Btn size="sm" kind={filled ? "primary" : "ghost"} onClick={onClick} style={filled ? { width: TOPBAR_ACTION_WIDTH, border: "1px solid transparent" } : { width: TOPBAR_ACTION_WIDTH, border: "1px solid var(--accent)", background: "var(--bg-2)", color: "var(--ink)" }}>
    <Icon name={icon} size={16} color={filled ? "var(--accent-ink)" : "var(--ink)"} />{label}
  </Btn>
);

/* column geometry — chat (act) and Activity (verify) are resizable; the center
   (see) is prioritized and never drops below CENTER_MIN. Below the 3-column
   threshold the Activity pillar drops away (two columns); below MOBILE_MIN the
   web app blocks and points to the phone. Mirrors the main web app. */
const CHAT_MIN = 300, CHAT_MAX = 560;
const ACT_MIN = 300, ACT_MAX = 520;
const CENTER_MIN = 460;
const MOBILE_MIN = 768;

/* a draggable seam. side="right" sits on a column's right edge (chat),
   side="left" on the left edge (Activity). clamp() keeps every column within
   its bounds AND preserves CENTER_MIN for the middle. */
const ColResizer = ({ side, width, setWidth, clamp }) => {
  const drag = useRef(null);
  const onDown = (e) => { drag.current = { x: e.clientX, w: width }; e.currentTarget.setAttribute("data-drag", "1"); try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {} };
  const onMove = (e) => { if (!drag.current) return; const dx = e.clientX - drag.current.x; setWidth(clamp(drag.current.w + (side === "left" ? -dx : dx))); };
  const onUp = (e) => { drag.current = null; e.currentTarget.removeAttribute("data-drag"); };
  return <div className={"col-resize" + (side === "left" ? " col-resize-left" : "")} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp} title="Drag to resize" />;
};

/* below MOBILE_MIN the desktop pillars don't fit — point to the phone app. */
const MobileBlock = () => (
  <div style={{ position: "fixed", inset: 0, zIndex: 5000, background: "var(--bg)", color: "var(--ink)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
    <div aria-hidden="true" style={{ position: "absolute", top: 0, left: 0, right: 0, height: 3, background: "linear-gradient(90deg, transparent, var(--accent) 20%, var(--accent) 80%, transparent)", opacity: 0.9 }} />
    <div style={{ width: "100%", maxWidth: 340, transform: "translateY(-4vh)", textAlign: "center" }}>
      <div style={{ display: "flex", justifyContent: "center" }}><Logo size={34} /></div>
      <div style={{ marginTop: 24, fontFamily: "var(--f-mono)", fontSize: 11, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--accent)" }}>Mobile app required</div>
      <h1 style={{ margin: "12px 0 0", fontFamily: "var(--f-display)", fontSize: 30, lineHeight: 1.08, fontWeight: 600, letterSpacing: "-0.03em" }}>Use Yoshi on your phone</h1>
      <p style={{ margin: "13px auto 0", maxWidth: 320, fontFamily: "var(--f-display)", fontSize: 15, lineHeight: 1.55, color: "var(--ink-2)" }}>Yoshi&rsquo;s web app is built for desktop. To manage your money on this device, open the Yoshi mobile app.</p>
    </div>
  </div>
);

/* the empty reading-pane state shared by the Briefs / Automations takeovers */
const InboxEmpty = ({ title, sub }) => (
  <div style={{ flex: 1, display: "grid", placeItems: "center", padding: 40 }}>
    <div style={{ textAlign: "center", maxWidth: 300 }}>
      <div style={{ width: 48, height: 48, margin: "0 auto", borderRadius: 999, border: "1px solid var(--rule-2)", display: "grid", placeItems: "center", color: "var(--ink-3)" }}><Logo size={24} /></div>
      <div style={{ fontFamily: "var(--f-display)", fontSize: 16, fontWeight: 600, marginTop: 15, letterSpacing: "-0.01em" }}>{title}</div>
      <div style={{ fontFamily: "var(--f-display)", fontSize: 13, color: "var(--ink-3)", marginTop: 7, lineHeight: 1.55 }}>{sub}</div>
    </div>
  </div>
);

/* the raised reading card the selected item renders into */
const InboxCard = ({ children }) => (
  <div style={{ position: "relative", width: "100%", maxWidth: 780, display: "flex", flexDirection: "column", minHeight: 0, margin: "18px 22px", background: "var(--bg)", border: "1px solid var(--rule-2)", borderRadius: 14, overflow: "hidden", boxShadow: "0 22px 48px -20px rgba(0,0,0,0.4), 0 4px 14px -8px rgba(0,0,0,0.2)" }}>
    {children}
  </div>
);

/* Briefs on web — the same full-width two-pane takeover as the main web app.
   It REPLACES the chat pillar (the brief's own thread lives in the reading
   card), with the inbox list on the left (BriefsBoard) and the selected brief
   on the right (BriefDetail, embedded). Reuses the exact briefs.jsx components
   so it tracks main as briefs evolve. */
const WebBriefs = ({ proposals, nav, onClose, onExecute, onDecline, onReviseProposal, initialBriefId }) => {
  const itemFromProposal = (p) => ({ id: p.id, kind: "approve", icon: "bolt", agentIcon: p.agentIcon, series: p.series, title: p.title, body: p.why.split(". ")[0] + ".", value: null, tone: p.net >= 0 ? "in" : "out", sec: "needsyou", when: p.when || "Today", agent: p.agent, source: p.source, preview: p.preview, legs: p.legs, net: p.net, amountLabel: p.amountLabel, amount: p.amount, cashImpactLabel: p.cashImpactLabel, cashImpact: p.cashImpact, proposal: p });
  const firstNeedsYou = proposals && proposals.length ? itemFromProposal(proposals[0]) : null;
  const feedList = window.BRIEF_FEED ? window.BRIEF_FEED() : [];
  const initialProposal = initialBriefId ? (proposals || []).find((p) => p.id === initialBriefId) : null;
  // deep-link can also target the pulse's "daily-brief" (Today's read), which
  // BriefsBoard builds from window.MORNING_BRIEF, or any other feed brief
  const dailyBrief = window.MORNING_BRIEF ? { id: "daily-brief", kind: "insight", icon: "bulb", title: "Pulse", body: window.MORNING_BRIEF.body, ask: window.MORNING_BRIEF.ask, stat: window.MORNING_BRIEF.stat, tone: window.MORNING_BRIEF.tone, visual: window.MORNING_BRIEF.visual, detail: window.MORNING_BRIEF.detail, when: "9:41 AM", sec: "insights" } : null;
  const initialFeed = initialProposal ? null : (initialBriefId === "daily-brief" ? dailyBrief : (initialBriefId ? feedList.find((x) => x.id === initialBriefId) : null));
  const [sel, setSel] = useState(() => (initialProposal ? itemFromProposal(initialProposal) : initialFeed) || firstNeedsYou);
  const idx = sel ? feedList.findIndex((x) => x.id === sel.id) : -1;
  const hasNext = idx >= 0 && idx < feedList.length - 1;
  const advance = () => { if (hasNext) setSel(feedList[idx + 1]); else setSel(null); };
  // after approving/declining a needs-you brief, auto-advance to the next one
  // in the list (the brief that followed, else the first remaining) — like the
  // mobile ConfirmSheet flow. When none are left, the pane clears.
  const nextNeedsYou = (id) => {
    const ny = (proposals || []);
    const i = ny.findIndex((p) => p.id === id);
    const remaining = ny.filter((p) => p.id !== id);
    const next = remaining[i] || remaining[0];
    return next ? itemFromProposal(next) : null;
  };
  const pick = (b) => { setSel(b); };
  const exec = (id) => { const nx = nextNeedsYou(id); onExecute && onExecute(id); setSel(nx); };
  const decline = (id) => { const nx = nextNeedsYou(id); onDecline && onDecline(id); setSel(nx); };
  return (
    <div style={{ position: "relative", flex: 1, minHeight: 0, background: "var(--bg)", display: "flex", flexDirection: "column", animation: "fade-swap 220ms ease both" }} data-screen-label="Briefs">
      {/* exit the whole takeover — top-right, the app's standard circle X */}
      <button className="press" onClick={onClose} aria-label="Close" title="Close" style={{ position: "absolute", top: 12, right: 16, zIndex: 50, background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, width: 32, height: 32, display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer" }}><Icon name="close" size={18} /></button>
      <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
        {/* LEFT — the inbox list */}
        <aside style={{ flex: "none", width: 396, minWidth: 320, borderRight: "1px solid var(--rule)", display: "flex", flexDirection: "column", minHeight: 0 }}>
          <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 8, padding: "16px 18px 10px" }}>
            <span style={{ flex: 1, fontFamily: "var(--f-display)", fontSize: 21, fontWeight: 700, letterSpacing: "-0.025em" }}>Briefs</span>
          </div>
          <div className="scroll" style={{ paddingBottom: 24 }}>
            <BriefsBoard nav={nav} onOpen={pick} onClose={onClose} proposals={proposals} onApprove={exec} selectedId={sel && sel.id} />
          </div>
        </aside>
        {/* RIGHT — the selected brief on a raised reading card. Web keeps brief
            context visible and grows the conversation inline beneath it. */}
        <section style={{ flex: 1, minWidth: 0, display: "flex", justifyContent: "center", minHeight: 0, background: "var(--bg-2)" }}>
          {sel ? (
            <InboxCard>
              <div style={{ flex: "none", display: "flex", justifyContent: "flex-end", padding: "10px 14px 0" }}>
                <button className="press" onClick={() => setSel(null)} aria-label="Close brief" title="Close" style={{ width: 28, height: 28, borderRadius: 999, background: "color-mix(in srgb, var(--ink) 7%, var(--bg-2))", border: "none", display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer", padding: 0 }}><Icon name="close" size={15} /></button>
              </div>
              <BriefDetail key={sel.id} b={sel} embedded onBack={() => setSel(null)} onClose={onClose} nav={nav} onAdvance={advance} hasNext={hasNext} onExecute={exec} onDecline={decline}
                onReviseProposal={onReviseProposal && sel.proposal ? (id, text) => onReviseProposal(id, text) : undefined} />
            </InboxCard>
          ) : <InboxEmpty title="Select a brief" sub="Pick anything from the list to read it here." />}
        </section>
      </div>
    </div>
  );
};

/* Automations on web — the same full-width two-pane takeover: the automations
   list on the left, the selected automation's detail on the right (the reused
   AutomationSheet in embedded mode, which carries its own modify/pause thread).
   Mirrors main's Studio → Automations. */
const WebAutomations = ({ nav, onClose, flash, initialId }) => {
  const list = AUTOMATIONS || [];
  const [sel, setSel] = useState(() => list.find((a) => a.id === initialId) || list[0] || null);
  // the aggregate authority line — the one-glance answer to "which of my
  // rules can move money without asking me?" Paused rules count in none of
  // the mode buckets; they get their own tail segment.
  const running = list.filter((a) => a.status !== "paused");
  const autos = running.filter((a) => a.mode === "auto").length;
  const asks = running.filter((a) => a.mode === "asks").length;
  const alerts = running.filter((a) => a.mode === "alert").length;
  const attention = list.filter((a) => a.status === "attention").length;
  const paused = list.filter((a) => a.status === "paused").length;
  const build = () => { onClose(); nav.ask("Build a new automation", "Sure. Tell me the rule in plain words — like “invest $200 every Friday” or “keep my spendable cash near $15k and move the rest to Reserve.” I'll set it up inside your limits and show you the first run before it executes."); };
  return (
    <div style={{ position: "relative", flex: 1, minHeight: 0, background: "var(--bg)", display: "flex", flexDirection: "column", animation: "fade-swap 220ms ease both" }} data-screen-label="Automations">
      {/* exit the whole takeover — top-right, the app's standard circle X */}
      <button className="press" onClick={onClose} aria-label="Close" title="Close" style={{ position: "absolute", top: 12, right: 16, zIndex: 50, background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, width: 32, height: 32, display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer" }}><Icon name="close" size={18} /></button>
      <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
        {/* LEFT — the automations list */}
        <aside style={{ flex: "none", width: 396, minWidth: 320, borderRight: "1px solid var(--rule)", display: "flex", flexDirection: "column", minHeight: 0 }}>
          <div style={{ flex: "none", padding: "16px 18px 12px" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <span style={{ flex: 1, fontFamily: "var(--f-display)", fontSize: 21, fontWeight: 700, letterSpacing: "-0.025em" }}>Automations</span>
            </div>
            {/* authority line — inline icon so wrapping keeps it on the first line */}
            <div style={{ fontFamily: "var(--f-display)", fontSize: 12, fontWeight: 600, color: "var(--ink)", marginTop: 8, lineHeight: 1.55 }}>
              <Icon name="bolt" size={12} color="var(--accent)" stroke={1.8} style={{ display: "inline-block", verticalAlign: "-1.5px", marginRight: 5 }} />
              {autos} run automatically within limits · {asks} ask you first · {alerts} alerts only{attention > 0 && <span style={{ color: "var(--signal-neg)" }}> · {attention} needs attention</span>}{paused > 0 && <span style={{ color: "var(--ink-3)" }}> · {paused} paused</span>}
            </div>
            {/* no value claims and no definitional copy here — the authority line
                IS the description, with the user's own numbers */}
            <Btn full onClick={build} style={{ marginTop: 14 }}>Build with Yoshi <Icon name="plus" size={16} color="var(--accent-ink)" /></Btn>
          </div>
          <div className="scroll" style={{ paddingBottom: 24 }}>
            {list.map((a, i) => (
              <div key={a.id} style={{ position: "relative", background: sel && sel.id === a.id ? "var(--bg-2)" : "transparent" }}>
                {sel && sel.id === a.id && <span style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: 3, background: "var(--accent)", zIndex: 1 }} />}
                <AutoRow a={a} last={i === list.length - 1} onOpen={() => setSel(a)}
                  onNow={(x) => nav.sheet({ type: "briefs", brief: x.now.briefId })} />
              </div>
            ))}
          </div>
        </aside>
        {/* RIGHT — the selected automation on a raised reading card */}
        <section style={{ flex: 1, minWidth: 0, display: "flex", justifyContent: "center", minHeight: 0, background: "var(--bg-2)" }}>
          {sel ? (
            <InboxCard><AutomationSheet key={sel.id} embedded automation={sel} onClose={() => setSel(null)} nav={nav} flash={flash} /></InboxCard>
          ) : <InboxEmpty title="Select an automation" sub="Pick a rule from the list to see how it's running." />}
        </section>
      </div>
    </div>
  );
};

const SimpleWebApp = () => {
  const [palette, setPaletteState] = useState("graphite"); // web is graphite-first
  const [stack, setStack] = useState([]);           // push stack: holding / account / card
  const [overlay, setOverlay] = useState(null);     // modal sheets
  const [txnOverlay, setTxnOverlay] = useState(null); // txn/receipt detail — a modal that layers OVER the current view (e.g. Accounts) rather than replacing it
  const [proposals, setProposals] = useState(PROPOSALS);
  // ---- legacy proposal-thread state, still used by older reference paths ----
  // The active Briefs flow keeps needs-you chat inline inside the brief detail.
  const [threads, setThreads] = useState({});
  const [expired, setExpired] = useState([]);
  const [extraProposals, setExtraProposals] = useState([]);
  const [threadTid, setThreadTid] = useState(null);
  const MOD_SEQ = useRef(0);
  const TRADE_LAUNCH_SEQ = useRef(0);
  const [activityExtra, setActivityExtra] = useState([]);
  const [extraAccounts, setExtraAccounts] = useState([]); // Yoshi accounts opened from the menu ("Open another …")
  const [inflight, setInflight] = useState(COMING_UP_SEED); // the In motion / Coming up queue
  const [actFilter, setActFilter] = useState("Yoshi");        // Activity scope filter
  const inflightRef = useRef(inflight);
  useEffect(() => { inflightRef.current = inflight; }, [inflight]);
  const [toast, setToast] = useState(null);
  const [chatInject, setChatInject] = useState(null);
  const [hasCard, setHasCard] = useState(() => localStorage.getItem("yoshi_card") === "1");

  // resizable columns (persisted). center absorbs the rest, floored at CENTER_MIN.
  const [winW, setWinW] = useState(() => window.innerWidth);
  const [chatW, setChatW] = useState(() => { const v = parseInt(localStorage.getItem("yoshi_web_chatw") || "0", 10); return v >= CHAT_MIN && v <= CHAT_MAX ? v : 420; });
  const [actW, setActW] = useState(() => { const v = parseInt(localStorage.getItem("yoshi_web_actw") || "0", 10); return v >= ACT_MIN && v <= ACT_MAX ? v : 400; });
  useEffect(() => { const onResize = () => setWinW(window.innerWidth); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []);
  const setChatWidth = (w) => { setChatW(w); localStorage.setItem("yoshi_web_chatw", String(Math.round(w))); };
  const setActWidth = (w) => { setActW(w); localStorage.setItem("yoshi_web_actw", String(Math.round(w))); };

  useEffect(() => { document.getElementById("app").setAttribute("data-palette", palette); }, [palette]);
  useEffect(() => { window.__liveProposals = proposals; window.setBellCount && window.setBellCount(window.totalBriefCount ? window.totalBriefCount(proposals.length) : proposals.length); }, [proposals]);
  const setPalette = (p) => setPaletteState(p);
  const flash = (msg) => { setToast(msg); setTimeout(() => setToast((t) => (t === msg ? null : t)), 2600); };
  const getCard = () => { localStorage.setItem("yoshi_card", "1"); setHasCard(true); };

  const nav = useMemo(() => ({
    // pillars are persistent — "switching tabs" just clears the layers
    tab: () => { setStack([]); setOverlay(null); setActFilter("Yoshi"); },
    // a transaction/receipt detail layers over whatever's showing (Home,
    // Accounts, …) as a modal; every other sheet replaces the work area
    sheet: (o) => (o && (o.type === "txn" || o.type === "receipt")) ? setTxnOverlay(o) : setOverlay(o),
    closeSheet: () => setOverlay(null),
    push: (v) => setStack((s) => [...s, v]),
    pop: () => setStack((s) => s.slice(0, -1)),
    clearStack: () => setStack([]),
    // selecting a security ANYWHERE opens the Trade view with it preselected —
    // same as opening Trade and picking it (the detail shows on the left, ticket
    // on the right). Holdings resolve because MARKET includes them (held: true).
    security: (id) => { setStack([]); setOverlay({ type: "trade", id }); },
    // The top-bar action is a root navigation: relaunch Trade even when its
    // current instance is showing the all-holdings phase.
    trade: () => { setStack([]); TRADE_LAUNCH_SEQ.current += 1; setOverlay({ type: "trade", launch: TRADE_LAUNCH_SEQ.current }); },
    ask: (note, reply) => { setChatInject({ note, reply }); setStack([]); setOverlay(null); },
    transfer: (preset = {}) => { setStack([]); setOverlay({ type: "transfer", intent: preset.intent, from: preset.from, rail: preset.rail, retry: preset.retry }); },
    automation: (id) => { setStack([]); setOverlay({ type: "automation", id }); },
    studio: (view) => { setOverlay(view === "automations" ? { type: "automations" } : null); if (view !== "automations") nav.ask("Show me the markets", "Ask me about anything you hold or want to research — I'll pull the chart, the comparison, or the trade right here."); },
    accountsRoot: () => { setOverlay(null); setStack([]); },
    // open another Yoshi account from the menu: auto-name it (Cash / Brokerage /
    // Crypto, incrementing to "Cash 2", "Brokerage 2", … when the name is taken),
    // add it, and jump to the Accounts view with the new account highlighted.
    openAccount: (family) => {
      const base = family === "cash" ? "Cash" : family === "crypto" ? "Crypto" : "Brokerage";
      const seed = ["Cash", "Reserve", "Brokerage", "High Risk", "Crypto"];
      const id = "extra-" + family + "-" + Date.now();
      setExtraAccounts((prev) => {
        const taken = new Set([...seed, ...prev.map((a) => a.name)]);
        let name = base, n = 2;
        while (taken.has(name)) { name = base + " " + n; n += 1; }
        return [...prev, { id, family, name }];
      });
      setStack([]); setTxnOverlay(null);
      setOverlay({ type: "accounts", focus: id });
      flash(base + " account opened");
    },
    signOut: () => { setStack([]); setOverlay(null); setProposals(PROPOSALS); setActivityExtra([]); setExtraAccounts([]); setInflight(COMING_UP_SEED); setActFilter("Yoshi"); flash("Signed out"); },
  }), []);

  const findProp = (id) => proposals.find((p) => p.id === id) || extraProposals.find((p) => p.id === id) || PROPOSALS.find((p) => p.id === id);

  // ---- proposal / brief revision helpers ------------------------------------
  const rootOf = (id) => { let p = findProp(id), guard = 0; while (p && p.modifiedFrom && guard++ < 12) p = findProp(p.modifiedFrom); return p ? p.id : id; };
  const tidFor = (id) => "th_" + rootOf(id);
  const isExpired = (id) => expired.includes(id);
  const parseMoney = (s) => {
    const m = String(s || "").match(/\$([\d,]+(?:\.\d+)?)/);
    return m ? parseFloat(m[1].replace(/,/g, "")) : 0;
  };
  const proposalBaseAmount = (p) => Math.abs(parseMoney(p && p.amount) || p && p.net || 0);
  // a target amount in a revise note ("$250", "$250 instead of $500") drives the
  // resize — pick the figure that isn't the current per-run amount
  const parseTarget = (note, base) => {
    const nums = (String(note || "").match(/\$\s?[\d,]+(?:\.\d+)?/g) || [])
      .map((x) => parseFloat(x.replace(/[^\d.]/g, ""))).filter((n) => n > 0);
    if (!nums.length) return null;
    const notBase = nums.filter((n) => Math.abs(n - base) > 0.01);
    return notBase.length ? notBase[0] : nums[0];
  };
  const fmtUsd = (n) => "$" + n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const formatScaledMoney = (prefix, raw, decimals, f) => {
    const n = parseFloat(raw.replace(/,/g, "")) * f;
    return prefix + "$" + n.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  };
  const shouldScaleMoney = (n, base, text) => {
    if (!base) return true;
    if (Math.abs(n - base) < 0.01) return true;
    if (/current balance|available|current holding/i.test(text)) return false;
    const ratio = n / base;
    return ratio > 1 && ratio <= 25 && Math.abs(ratio - Math.round(ratio)) < 0.001;
  };
  const scaleStr = (value, f, base = 0) => String(value).replace(/([+-]?)\$([\d,]+)(\.\d+)?/g, (match, prefix, whole, cents) => {
    const n = parseFloat((whole + (cents || "")).replace(/,/g, ""));
    return shouldScaleMoney(n, base, String(value)) ? formatScaledMoney(prefix, whole + (cents || ""), cents ? cents.length - 1 : 0, f) : match;
  }).replace(/(about\s+)(\d+(?:\.\d+)?)(\s+shares\b)/gi, (match, lead, raw, tail) => {
    const n = parseFloat(raw) * f;
    return lead + n.toLocaleString(undefined, { minimumFractionDigits: raw.includes(".") ? 2 : 0, maximumFractionDigits: raw.includes(".") ? 2 : 0 }) + tail;
  });
  const scaleCell = (cell, f, base) => {
    if (typeof cell === "string") return scaleStr(cell, f, base);
    if (!cell || typeof cell !== "object") return cell;
    return Object.fromEntries(Object.entries(cell).map(([k, v]) => [k, typeof v === "string" ? scaleStr(v, f, base) : v]));
  };
  const makeRevised = (p) => {
    MOD_SEQ.current += 1;
    const f = 0.5;
    const base = proposalBaseAmount(p);
    return { ...p, id: p.id + "_m" + MOD_SEQ.current, modifiedFrom: p.id,
      net: Math.round(p.net * f * 100) / 100,
      title: scaleStr(p.title, f, base),
      why: scaleStr(p.why, f, base),
      preview: p.preview ? { ...p.preview, t: scaleStr(p.preview.t, f, base), s: scaleStr(p.preview.s, f, base) } : p.preview,
      legs: p.legs.map((l) => l.map((cell) => scaleCell(cell, f, base))),
      series: p.series ? p.series.map((row) => scaleCell(row, f, base)) : p.series,
      amount: p.amount ? scaleStr(p.amount, f, base) : p.amount,
      cashImpact: p.cashImpact ? scaleStr(p.cashImpact, f, base) : p.cashImpact };
  };
  const makeBriefRevision = (p, note) => {
    const s = (note || "").toLowerCase();
    const base = proposalBaseAmount(p);
    const smaller = /smaller|less|half|reduce|lower|trim|too large/.test(s);
    const safer = /safe|safest|only the/.test(s);
    const later = /wait|push|friday|monday|tomorrow|timing/.test(s);
    // a specific amount in the note wins ("$250" → resize $500 → $250); else the
    // qualitative keywords set the factor
    const target = parseTarget(note, base);
    const f = target && base > 0 ? target / base : smaller ? 0.5 : safer ? 0.65 : 1;
    const resized = base > 0 && Math.abs(f - 1) > 0.001;
    const timing = later ? " Timing moved later per your note; nothing runs until you confirm." : "";
    // lead the brief with the actual change so the card is unambiguous
    const change = resized
      ? `Resized from ${fmtUsd(base)} to ${fmtUsd(base * f)} per run, per your chat request.`
      : `I updated this brief from your chat request: "${note}".`;
    return {
      ...p,
      title: resized ? "Updated · " + scaleStr(p.title, f, base) : safer ? "Safer version · " + p.title : later ? p.title + " · Friday" : "Updated · " + p.title,
      net: Math.round((p.net || 0) * f * 100) / 100,
      why: `${change}${safer ? " I kept the lower-risk leg emphasis and removed the aggressive posture." : ""}${timing} Review the revised proposal below, then confirm with passkey or decline it here.`,
      settles: later ? "Friday" : p.settles,
      preview: resized && p.preview ? { ...p.preview, t: scaleStr(p.preview.t, f, base), s: scaleStr(p.preview.s, f, base) } :
        p.preview ? { ...p.preview, t: "Updated proposal", s: `${change}${timing}` } :
        { t: "Updated proposal", s: change },
      legs: p.legs.map((l) => l.map((cell) => scaleCell(cell, f, base))),
      series: p.series ? p.series.map((row) => scaleCell(row, f, base)) : p.series,
      amount: p.amount ? scaleStr(p.amount, f, base) : p.amount,
      cashImpact: p.cashImpact ? scaleStr(p.cashImpact, f, base) : p.cashImpact,
    };
  };
  const seedThread = (pid, userMsg) => {
    const tid = tidFor(pid);
    setThreads((ts) => {
      const existing = ts[tid];
      let msgs = existing ? existing.msgs : [{ kind: "proposal", id: rootOf(pid) }];
      if (userMsg) msgs = [...msgs, { from: "user", t: userMsg, time: "now" }, { from: "agent", t: window.threadReply(userMsg).t, time: "now" }];
      return { ...ts, [tid]: { id: tid, msgs } };
    });
    return tid;
  };
  const persistThread = (tid, msgs) => setThreads((ts) => ({ ...ts, [tid]: { ...(ts[tid] || { id: tid }), msgs } }));
  const reviseInThread = (origId) => {
    const p = findProp(origId);
    const pm = makeRevised(p);
    setExtraProposals((xs) => [...xs, pm]);
    setProposals((ps) => ps.some((x) => x.id === origId) ? ps.map((x) => x.id === origId ? pm : x) : [...ps, pm]);
    setExpired((e) => e.includes(origId) ? e : [...e, origId]);
    flash("Proposal revised");
    return pm.id;
  };
  const reviseBriefProposal = (id, note) => {
    const p = findProp(id);
    if (!p) return null;
    const pm = makeBriefRevision(p, note);
    setProposals((ps) => ps.map((x) => x.id === id ? pm : x));
    flash("Brief updated");
    return pm;
  };
  const proposalActivity = (p) => p.activity || { icon: p.kind === "trade" ? "trade" : "swap", title: p.title, detail: p.legs.map((l) => l[1] && typeof l[1] === "object" ? l[1].ticker : l[1]).filter(Boolean).slice(0, 2).join(" · "), category: p.kind === "trade" ? "Investment" : "Transfer", by: p.agent, net: p.net };

  /* ---- In motion queue ---------------------------------------------------- */
  const settleInflight = (id) => {
    const it = inflightRef.current.find((x) => x.id === id);
    if (!it) return; // canceled before it settled
    const row = it.settle || { icon: it.icon, title: it.title, detail: it.acct, category: it.category, net: it.net, accountScope: it.scope === "yoshi" ? "yoshi" : "external" };
    setInflight((xs) => xs.filter((x) => x.id !== id));
    setActivityExtra((xs) => [{ id: "x" + Date.now(), when: "Just now", ...row }, ...xs]);
    flash((it.category === "Trade" ? "Executed · " : "Cleared · ") + row.title);
  };
  const enqueueInflight = (item) => {
    const id = "f" + Date.now();
    setInflight((xs) => [{ ...item, id, fresh: true }, ...xs]); // prepend → newest at top
    setTimeout(() => setInflight((xs) => xs.map((x) => x.fresh ? { ...x, fresh: false } : x)), 2600);
    if (item.settleMs) setTimeout(() => settleInflight(id), item.settleMs);
  };
  const placeOrder = (item) => {
    enqueueInflight(item);
    setOverlay(null); setStack([]); setActFilter("Yoshi");
    flash((item.category === "Trade" ? "Order placed" : "Transfer started") + " · In motion");
  };
  const recordTrade = (row) => {
    const id = "x" + Date.now();
    setActivityExtra((xs) => [{ id, when: "Just now", fresh: true, ...row }, ...xs]);
    setTimeout(() => setActivityExtra((xs) => xs.map((x) => x.id === id ? { ...x, fresh: false } : x)), 1800);
    setOverlay(null); setStack([]); setActFilter("Yoshi");
    flash("Executed · " + row.title);
  };
  useEffect(() => { window.yoshiTrack = { placed: placeOrder, executed: recordTrade }; return () => { delete window.yoshiTrack; }; }, []);
  /* committed-against-a-transfer sum for the trade/move ceiling — read from the
     ONE In-motion list (fundedBy-tagged one-time automations). */
  useEffect(() => { window.yoshiArrivingCommitted = (tid) => inflightRef.current.filter((x) => x.fundedBy === tid).reduce((s, x) => s + (x.funds || 0), 0); return () => { delete window.yoshiArrivingCommitted; }; }, []);
  const cancelInflight = (it) => {
    setInflight((xs) => xs.filter((x) => x.id !== it.id));
    setActivityExtra((xs) => [{ id: "x" + Date.now(), icon: it.icon, title: "Canceled · " + it.title, detail: it.acct, category: it.category, when: "Just now", net: 0, amount: Math.abs(it.net || 0), accountScope: it.scope === "yoshi" ? "yoshi" : "external", by: "You" }, ...xs]);
    setOverlay(null); setTxnOverlay(null);
    flash("Canceled · " + it.title);
  };
  const skipInflight = (it) => {
    setInflight((xs) => xs.map((x) => x.id === it.id ? { ...x, when: x.nextWhen || x.when, chip: x.nextChip || x.chip } : x));
    setOverlay(null);
    flash("Skipped this run · next " + (it.nextWhen || it.when));
  };

  const instantSettle = (a) => /second|instant/i.test(a.settles || "");
  const queuedTrade = (a) => a.kind === "trade" && !MARKET_OPEN;
  const pendingTransfer = (a) => a.kind === "transfer" && !instantSettle(a);
  /* approving a needs-you brief: anything that doesn't settle on the spot lands
     In motion (a trade queues, a non-instant transfer stays pending); instant
     transfers and standing automations just post to Activity. */
  const onExecuted = (a) => {
    if (queuedTrade(a)) enqueueInflight({
      icon: "trade", title: a.title, category: "Trade", kind: "once", state: "queued",
      agent: false, when: "Fri, May 30", chip: "Queued · opens Fri 9:30 AM",
      net: a.net || 0, scope: "yoshi", acct: "One-time order · Yoshi Brokerage",
      cancelNote: "Cancellation is best efforts.",
      steps: [["Approved", "Just now"], ["Queued for market open", "You are here"], ["Executes", "Fri · 9:30 AM"]], stepAt: 1,
      settleMs: 15000,
      settle: { icon: "trade", title: a.title, detail: "Executed at the open · Your approval", category: "Investment", net: a.net || 0, accountScope: "yoshi" },
    });
    else if (pendingTransfer(a)) {
      const outgoing = (a.net || 0) < 0; // money leaving Yoshi can still be pulled back
      const rail = a.rail || "Standard ACH";
      enqueueInflight({
        icon: "swap", title: a.title, category: "Transfer", kind: "once", state: "pending",
        agent: false, when: "Clears " + a.settles, chip: "Pending · clears " + a.settles,
        net: a.net || 0, scope: "yoshi", acct: rail, cancelable: outgoing,
        cancelNote: outgoing ? "You can cancel while it's still pending. Once it's handed to the bank network it can't be recalled." : undefined,
        noCancelNote: outgoing ? undefined : "This transfer is already at the bank network, so it can't be recalled.",
        steps: [["Approved", "Just now"], ["Pending at the bank network", "You are here"], ["Clears", a.settles]], stepAt: 1,
        settleMs: 22000,
        settle: { icon: "swap", title: a.title, detail: rail + " · cleared", category: "Transfer", accountScope: "yoshi", net: a.net || 0 },
      });
    }
    else if (a.activity) setActivityExtra((xs) => [{ ...a.activity, id: "x" + Date.now(), when: "Just now" }, ...xs]);
    if (a.id) setProposals((ps) => ps.filter((p) => p.id !== a.id));
    setActFilter("Yoshi");
    flash((a.kind === "automation" ? "Scheduled · " : queuedTrade(a) ? "Order placed · " : pendingTransfer(a) ? "Transfer started · " : "Executed · ") + a.title);
  };
  // on web, "Review" opens the Briefs UI with that exact brief selected — its
  // detail + approve/decline live in the reading pane (web's only proposal-
  // review surface; there is no separate ConfirmSheet modal)
  const reviewInBriefs = (p) => p && nav.sheet({ type: "briefs", brief: p.id });
  // briefs approve/decline inline in the reading pane (web two-pane)
  const execFromBrief = (id) => { const p = findProp(id); if (p) onExecuted({ ...p, activity: proposalActivity(p) }); };
  const declineFromBrief = (id) => { const p = findProp(id); setProposals((ps) => ps.filter((x) => x.id !== id)); flash("Declined" + (p ? " · " + p.title : "")); };

  // Esc closes the topmost layer (sheet → push detail)
  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== "Escape") return;
      if (txnOverlay) setTxnOverlay(null);
      else if (threadTid) setThreadTid(null);
      else if (overlay) setOverlay(null);
      else if (stack.length) setStack((s) => s.slice(0, -1));
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [txnOverlay, threadTid, overlay, stack.length]);

  /* the active center page — a selection shifts the center column to this
     inline instead of opening a modal. Precedence: push detail > overlay
     sheet. null → the Home pane shows. */
  const centerNode = stack.length > 0 ? (
    stack.map((v, i) => (
      <div key={i} className="push-enter" style={{ position: "absolute", inset: 0, background: "var(--bg)", display: "flex", flexDirection: "column", zIndex: 400 + i }}>
        {v.type === "holding" && <HoldingDetail id={v.id} nav={nav} />}
        {v.type === "account" && <AccountDetail acct={v.acct} nav={nav} />}
        {v.type === "card" && <CardDetail nav={nav} flash={flash} hasCard={hasCard} onGetCard={getCard} />}
      </div>
    ))
  ) : (overlay && !["profile", "briefs", "automations", "automation", "txn", "receipt", "inflight"].includes(overlay.type)) ? (
    <>
      {overlay.type === "holding" && <HoldingView id={overlay.id} nav={nav} onClose={nav.closeSheet} />}
      {overlay.type === "holdings" && <HoldingsDetailSheet nav={nav} onClose={nav.closeSheet} />}
      {overlay.type === "accounts" && <AccountsSheet nav={nav} onClose={nav.closeSheet} extra={extraAccounts} focus={overlay.focus} />}
      {overlay.type === "transfer" && <TransferFlow preset={overlay.from} intent={overlay.intent} initialRail={overlay.rail} rootTitle="Transfer" onClose={nav.closeSheet} nav={nav} flash={flash} onPlaced={placeOrder} />}
      {overlay.type === "trade" && <TradeSheet key={`trade-${overlay.holdings ? "holdings" : overlay.launch || "root"}`} id={overlay.id} side={overlay.side} holdings={overlay.holdings} hideMenu onClose={nav.closeSheet} nav={nav} onPlaced={placeOrder} />}
      {overlay.type === "link" && <LinkSheet onClose={nav.closeSheet} />}
      {overlay.type === "connect" && <ConnectAgentsSheet onClose={nav.closeSheet} nav={nav} />}
      {overlay.type === "support" && <SupportFlow onClose={nav.closeSheet} nav={nav} />}
      {overlay.type === "documents" && <DocumentsHub onClose={nav.closeSheet} nav={nav} flash={flash} initialAcct={overlay.acct} />}
    </>
  ) : null;

  // too narrow for the desktop pillars → block and point to the phone
  if (winW < MOBILE_MIN) return <ThemeCtx.Provider value={palette}><MobileBlock /></ThemeCtx.Provider>;
  // a flow / detail takes over the whole area right of chat (center + Activity),
  // so Activity isn't its own column then. Otherwise Activity is a third column
  // while it fits; below that it docks BELOW the center (two columns).
  const wideEnough = winW >= chatW + actW + CENTER_MIN;
  const actVisible = !centerNode && wideEnough; // Activity as its own right column
  const docked = !centerNode && !wideEnough;    // Activity flows below the center
  // Briefs / Automations replace the WHOLE work area including chat — the
  // reading pane carries its own thread, so the chat pillar isn't needed (like
  // main's WebInbox / Studio Automations).
  const fullTakeover = stack.length === 0 && !!overlay && (overlay.type === "briefs" || overlay.type === "automations" || overlay.type === "automation");
  // Trade / Transfer / Accounts carry their own desktop two-pane layout, so
  // they fill the takeover area right of chat; other sheets center in a column
  const wideFlow = stack.length === 0 && !!overlay && (overlay.type === "trade" || overlay.type === "transfer" || overlay.type === "accounts");
  // Trade/Transfer's reused web layout has no close of its own; every other
  // hosted sheet (accounts, briefs, details) carries one already
  const paneClose = stack.length === 0 && !!overlay && (overlay.type === "trade" || overlay.type === "transfer") ? nav.closeSheet : undefined;
  // effective width keeps the center at CENTER_MIN even when a window resize
  // (not a drag) leaves the stored chat width too large
  const effChatW = actVisible ? chatW : Math.max(CHAT_MIN, Math.min(chatW, winW - CENTER_MIN));
  const clampChat = (w) => Math.max(CHAT_MIN, Math.min(CHAT_MAX, Math.min(w, winW - CENTER_MIN - (actVisible ? actW : 0))));
  const clampAct = (w) => Math.max(ACT_MIN, Math.min(ACT_MAX, Math.min(w, winW - chatW - CENTER_MIN)));

  return (
    <ThemeCtx.Provider value={palette}>
      <div style={{ position: "fixed", inset: 0, display: "flex", flexDirection: "column", background: "var(--bg)" }}>
        {/* top bar — menu (left) · wordmark · bell (right) */}
        <div className="topbar">
          <button className="topbar-icon press" aria-label="Menu" title="Menu" onClick={() => nav.sheet({ type: "profile" })}>
            <Icon name="menu" size={22} />
          </button>
          {/* just the wordmark (bigger), like main — masked so it follows the
              theme's ink color; no separate glyph */}
          <button className="press" onClick={() => nav.tab()} aria-label="Yoshi home" title="Home" style={{ background: "none", border: "none", padding: "6px 8px", display: "flex", alignItems: "center", cursor: "pointer" }}>
            <span aria-hidden="true" style={{ display: "block", height: 23, width: 95, background: "var(--ink)", WebkitMaskImage: `url("${(window.__resources && window.__resources.wordmarkWhite) || "assets/logo-wordmark-white.png"}")`, maskImage: `url("${(window.__resources && window.__resources.wordmarkWhite) || "assets/logo-wordmark-white.png"}")`, WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat", WebkitMaskPosition: "left center", maskPosition: "left center", WebkitMaskSize: "contain", maskSize: "contain" }} />
          </button>
          <div style={{ flex: 1 }} />
          {/* quick actions live in the top bar on web (Home hides its own pair) */}
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <BellButton nav={nav} label="Briefs" size={16} />
            <TopAction icon="trade" label="Trade" filled onClick={() => nav.trade()} />
            <TopAction icon="swap" label="Transfer" onClick={() => nav.sheet({ type: "transfer" })} />
          </div>
        </div>

        {fullTakeover ? (
          /* Briefs / Automations take over the full width (chat included) */
          overlay.type === "briefs"
            ? <WebBriefs proposals={proposals} nav={nav} onClose={nav.closeSheet} onExecute={execFromBrief} onDecline={declineFromBrief} initialBriefId={overlay.brief}
                onReviseProposal={reviseBriefProposal} />
            : <WebAutomations nav={nav} onClose={nav.closeSheet} flash={flash} initialId={overlay.type === "automation" ? overlay.id : undefined} />
        ) : (
        <div className="pillars">
          {/* Yoshi chat — persistent left pillar (act), resizable; no header,
              the chat starts at the top (its phone header stays hidden by CSS) */}
          <aside className="chat-col" style={{ width: effChatW }}>
            <div className="yo-chatwrap">
              <YoshiTab nav={nav} proposals={proposals} onReview={reviewInBriefs} inject={chatInject} onInjected={() => setChatInject(null)} flash={flash} />
            </div>
            <ColResizer side="right" width={effChatW} setWidth={setChatWidth} clamp={clampChat} />
          </aside>

          {centerNode ? (
            /* a selection takes over the whole area right of chat — the center
               AND the Activity column (like main's flow panes) */
            <CenterPage wide={wideFlow} onClose={paneClose}>{centerNode}</CenterPage>
          ) : docked ? (
            /* two columns: Activity docks below the Home center and the whole
               center column scrolls as one region */
            <main className="main-col docked" style={{ position: "relative" }}>
              <div className="yo-mainwrap" style={{ width: "100%", maxWidth: 860, margin: "0 auto" }}>
                <HomeTab nav={nav} proposals={proposals} onReview={reviewInBriefs} hideFastPaths inflight={inflight} />
              </div>
              <section className="pillar-dock" style={{ width: "100%", maxWidth: 860, margin: "18px auto 0", borderTop: "1px solid var(--rule)" }}>
                <ActivityTab nav={nav} extra={activityExtra} coming={inflight} filter={actFilter} onFilter={setActFilter} />
              </section>
              {txnOverlay?.type === "txn" && <TxnDetailSheet tx={txnOverlay.tx} onClose={() => setTxnOverlay(null)} nav={nav} onCancel={txnOverlay.item && txnOverlay.cancelable ? () => (txnOverlay.item.kind === "recurring" ? skipInflight(txnOverlay.item) : cancelInflight(txnOverlay.item)) : undefined} />}
              {txnOverlay?.type === "receipt" && <ReceiptSheet tx={txnOverlay.tx} nav={nav} onClose={() => setTxnOverlay(null)} />}
            </main>
          ) : (
            /* three columns: Home center + Activity right pillar (resizable) */
            <>
              <main className="main-col">
                <div className="yo-mainwrap" style={{ width: "100%", maxWidth: 820, margin: "0 auto" }}>
                  <HomeTab nav={nav} proposals={proposals} onReview={reviewInBriefs} hideFastPaths inflight={inflight} />
                </div>
              </main>
              <aside className="stream-col" style={{ width: actW }}>
                <ColResizer side="left" width={actW} setWidth={setActWidth} clamp={clampAct} />
                <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", paddingTop: 8, position: "relative" }}>
                  <ActivityTab nav={nav} extra={activityExtra} coming={inflight} filter={actFilter} onFilter={setActFilter} />
                  {txnOverlay?.type === "txn" && <TxnDetailSheet tx={txnOverlay.tx} onClose={() => setTxnOverlay(null)} nav={nav} onCancel={txnOverlay.item && txnOverlay.cancelable ? () => (txnOverlay.item.kind === "recurring" ? skipInflight(txnOverlay.item) : cancelInflight(txnOverlay.item)) : undefined} />}
                  {txnOverlay?.type === "receipt" && <ReceiptSheet tx={txnOverlay.tx} nav={nav} onClose={() => setTxnOverlay(null)} />}
                </div>
              </aside>
            </>
          )}
        </div>
        )}
      </div>

      {/* menu — a left drawer over a grey scrim (like main). ProfileSheet's own
          X (top-left) or a click on the scrim closes it. */}
      {overlay?.type === "profile" && (
        <>
          <div onClick={nav.closeSheet} style={{ position: "fixed", inset: 0, zIndex: 1240, background: "rgba(0,0,0,0.45)", animation: "scrim-in 200ms ease both" }} />
          <aside data-screen-label="Menu" style={{ position: "fixed", top: 0, left: 0, bottom: 0, width: "min(400px, 92vw)", zIndex: 1250, background: "var(--bg)", borderRight: "1px solid var(--rule)", display: "flex", flexDirection: "column", minHeight: 0, boxShadow: "0 24px 60px -20px rgba(0,0,0,0.5)", animation: "drawer-in 240ms cubic-bezier(0.16,1,0.30,1) both" }}>
            <ProfileSheet palette={palette} setPalette={setPalette} nav={nav} onClose={nav.closeSheet} flash={flash} />
          </aside>
        </>
      )}

      {/* a transaction / receipt opened from INSIDE a pushed detail (an external
          account's ledger) — the pane-level sheet isn't mounted while the stack
          fills the center, so render it above the push stack (z-index 400+). */}
      {stack.length > 0 && txnOverlay?.type === "txn" && (
        <div style={{ position: "fixed", inset: 0, zIndex: 460, background: "var(--bg)" }}>
          <TxnDetailSheet tx={txnOverlay.tx} onClose={() => setTxnOverlay(null)} nav={nav} onCancel={txnOverlay.item && txnOverlay.cancelable ? () => (txnOverlay.item.kind === "recurring" ? skipInflight(txnOverlay.item) : cancelInflight(txnOverlay.item)) : undefined} />
        </div>
      )}
      {stack.length > 0 && txnOverlay?.type === "receipt" && (
        <div style={{ position: "fixed", inset: 0, zIndex: 460, background: "var(--bg)" }}>
          <ReceiptSheet tx={txnOverlay.tx} nav={nav} onClose={() => setTxnOverlay(null)} />
        </div>
      )}

      {/* recurring "Coming up" rows open the rule sheet (Skip this run / Manage) */}
      {overlay?.type === "inflight" && <InflightSheet item={inflight.find((x) => x.id === overlay.id)} nav={nav} onClose={nav.closeSheet} onCancel={cancelInflight} onSkip={skipInflight} />}

      {toast && <Toast msg={toast} />}
    </ThemeCtx.Provider>
  );
};

ReactDOM.createRoot(document.getElementById("app")).render(<SimpleWebApp />);
