/* 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, pointerEvents: "none", 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>
);

/* Header actions use black for the active section and neutral for the rest. */
const TOPBAR_ACTION_WIDTH = 118;
const TopAction = ({ icon, label, onClick, filled }) => (
  <Btn size="sm" kind={filled ? "primary" : "ghost"} onClick={onClick} current={filled} style={filled ? { width: TOPBAR_ACTION_WIDTH, border: "1px solid transparent" } : { width: TOPBAR_ACTION_WIDTH, border: "1px solid transparent", 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 — explain both recovery paths. */
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: "normal", textTransform: "uppercase", color: "var(--accent)" }}>More room needed</div>
      <h1 style={{ margin: "12px 0 0", fontFamily: "var(--f-display)", fontSize: 30, lineHeight: 1.08, fontWeight: 500, letterSpacing: "normal" }}>Continue on a larger screen</h1>
      <p style={{ margin: "13px auto 0", maxWidth: 320, fontFamily: "var(--f-display)", fontSize: 15, lineHeight: 1.55, color: "var(--ink-2)" }}>If you&rsquo;re on desktop, widen your browser window. If you&rsquo;re on a phone or small tablet, open the Yoshi mobile app to continue.</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: 500, marginTop: 15, letterSpacing: "normal" }}>{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, readingPane, onApplyAutomationRevision }) => {
  const itemFromProposal = (p) => ({ id: p.id, kind: "approve", icon: "bolt", agentIcon: p.agentIcon, series: p.series, title: p.title, body: p.briefBody || p.why.split(". ")[0] + ".", detail: p.detail, 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;
  const initialFeed = initialProposal ? null : (initialBriefId ? feedList.find((x) => x.id === initialBriefId) : null);
  const [sel, setSel] = useState(() => (initialProposal ? itemFromProposal(initialProposal) : initialFeed) || firstNeedsYou);
  useEffect(() => {
    if (!initialBriefId) return;
    const selectedProposal = (proposals || []).find((proposal) => proposal.id === initialBriefId);
    if (selectedProposal) { setSel(itemFromProposal(selectedProposal)); return; }
    const selectedFeed = feedList.find((brief) => brief.id === initialBriefId);
    if (selectedFeed) setSel(selectedFeed);
  }, [initialBriefId]);
  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); };
  // Declining a needs-you brief advances to the next one. Approvals leave the
  // current selection behind the submitted receipt, which then routes to
  // Activity or Automations based on the approved action.
  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) => { onExecute && onExecute(id); };
  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: 32, fontWeight: 500, letterSpacing: "0" }}>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 or its submitted receipt on a raised
            reading card. The Briefs inbox stays visible through approval. */}
        <section style={{ flex: 1, minWidth: 0, display: "flex", justifyContent: "center", minHeight: 0, background: "var(--bg-2)" }}>
          {readingPane || (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 onApplyAutomationRevision={onApplyAutomationRevision} 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, automations = AUTOMATIONS }) => {
  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: 500, letterSpacing: "normal" }}>Automations</span>
            </div>
            {/* authority line — inline icon so wrapping keeps it on the first line */}
            <div style={{ fontFamily: "var(--f-display)", fontSize: 12, fontWeight: 500, 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 = "bone"; // Lucid Dreaming Phase 1: light only
  const [stack, setStack] = useState([]);           // push stack: holding / account / card
  const [overlay, setOverlay] = useState(() => {
    const view = new URLSearchParams(window.location.search).get("view");
    if (view === "transfer-pending-visibility") return { type: "transfer", intent: "withdraw" };
    if (["delete-yoshi-blocked", "delete-yoshi-ready", "delete-yoshi-requested"].includes(view)) {
      return { type: "profile", section: "delete" };
    }
    const previewBrief = {
      "bnk-checking-buffer": "p-checking-buffer",
      "price-move": "b-price-move",
      "price-move-up": "b-price-move-up",
      "automation-move-review": "b-automation-move-review",
      "earnings-pre": "b-earnings-pre",
      "earnings-post": "b-earnings-post",
      "earnings-post-example": "b-earnings-post-example",
      "portfolio-update-daily": "b-portfolio-update-daily",
      "bnk-account-review-v2": "b-weekly-financial-review-causal",
      "investor-profile": "b-investor-profile-new",
      "investor-profile-new": "b-investor-profile-new",
      "bnk-payday-automations": "b-payday-with-automations",
      "bnk-payday-no-automations": "b-payday-without-automations",
      "bnk-debt-overview-personalized": "b-debt-overview-personalized",
      "bnk-debt-overview-personalized-895c": "b-debt-overview-personalized-895c",
      "bnk-debt-overview-personalized-2d6b": "b-debt-overview-personalized-2d6b"
    }[view];
    return previewBrief ? { type: "briefs", brief: previewBrief } : 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 [reviewedAutomations, setReviewedAutomations] = useState(() => BRIEFS.find((b) => b.id === "b-automation-move-review").automationMoveReview.automations.map(window.AutomationReview.asAutomation));
  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 [transactionStatus, setTransactionStatus] = useState(null);
  const INFLIGHT_SEQ = useRef(0);
  const [actFilter, setActFilter] = useState("all");          // Activity type filter
  const inflightRef = useRef(inflight);
  useEffect(() => { inflightRef.current = inflight; }, [inflight]);
  const [toastQueue, setToastQueue] = useState([]);
  const toast = toastQueue[0] || null;
  const [chatInject, setChatInject] = useState(null);
  const instantFundingUsed = [...inflight, ...activityExtra].reduce((sum, item) => sum + (item.instantFundsUsed || 0), 0);
  const instantFundingGranted = Math.min(
    INSTANT_FUNDING_CAP,
    inflight.filter((item) => item.category === "Transfer" && (item.net || 0) > 0).reduce((sum, item) => sum + (item.instantAvailable || 0), 0),
  );
  const instantFundingAvailable = Math.max(0, instantFundingGranted - instantFundingUsed);
  const unsettledSaleProceeds = [...inflight, ...activityExtra].reduce((sum, item) => sum + (item.unsettledSaleProceeds || 0), 0);
  const unsettledSaleProceedsUsed = [...inflight, ...activityExtra].reduce((sum, item) => sum + (item.unsettledSaleProceedsUsed || 0), 0);
  const settledCashUsed = [...inflight, ...activityExtra].reduce((sum, item) => sum + (item.settledCashUsed || 0), 0);
  const unsettledSaleProceedsAvailable = Math.max(0, unsettledSaleProceeds - unsettledSaleProceedsUsed);
  const settledCashAvailable = Math.max(0, CASH_TOTAL - settledCashUsed);
  const isPendingTransferPreview = new URLSearchParams(window.location.search).get("view") === "transfer-pending-visibility";
  const transferSettledCashAvailable = isPendingTransferPreview ? 13924.16 : settledCashAvailable;
  const transferPendingIncomingAmount = isPendingTransferPreview ? 99995.06 : unsettledSaleProceedsAvailable;
  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 = () => {}; // Legacy settings prop; no alternate theme.
  const flash = (msg) => setToastQueue((queue) => [...queue, msg]);
  useEffect(() => {
    if (!toast) return;
    const timer = setTimeout(() => setToastQueue((queue) => queue.slice(1)), 2600);
    return () => clearTimeout(timer);
  }, [toast]);
  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("all"); },
    // 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) => {
      if (!["cash", "brokerage", "crypto"].includes(family)) return;
      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("all"); 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() + "-" + INFLIGHT_SEQ.current++, when: "Today", ...row, ...currentTransactionOccurrence() }, ...xs]);
    flash((it.category === "Trade" ? "Executed · " : "Cleared · ") + row.title);
  };
  const enqueueInflight = (item) => {
    const id = `f${Date.now()}-${INFLIGHT_SEQ.current++}`;
    setInflight((xs) => [{ ...item, ...currentTransactionOccurrence(), id, fresh: true }, ...xs]);
    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("all");
    setTransactionStatus(transactionStatusForTracker(item));
  };
  useEffect(() => { window.yoshiTrack = { placed: placeOrder }; return () => { delete window.yoshiTrack; }; }, []);
  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: "Today", net: 0, amount: Math.abs(it.net || 0), accountScope: it.scope === "yoshi" ? "yoshi" : "external", by: "You", ...currentTransactionOccurrence() }, ...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 pendingTransfer = (a) => a.kind === "transfer";
  const needsYouApprovalDestination = (action) => action.kind === "automation" ? "automations" : "activity";
  const addRecentRow = (row) => {
    const id = "x" + Date.now() + "-" + INFLIGHT_SEQ.current++;
    setActivityExtra((xs) => [{ ...row, id, when: "Today", fresh: true, ...currentTransactionOccurrence() }, ...xs]);
    setTimeout(() => setActivityExtra((xs) => xs.map((item) => item.id === id ? { ...item, fresh: false } : item)), 1800);
  };
  /* Every approved brief produces the same submitted receipt as a manual
     transaction. A series expands into real individual Activity outcomes. */
  const submitApprovedBrief = (a) => {
    if (a.automationRevision) {
      const revision = a.automationRevision;
      window.AutomationReview.complete(revision.automationId);
      setReviewedAutomations((items) => items.map((item) => item.id === revision.automationId ? window.AutomationReview.asAutomation(revision.revised) : item));
      setOverlay({ type: "automation", id: revision.automationId });
      flash(revision.choice === "stop" ? "Automation paused" : "Automation updated");
      return;
    }
    const expandedRows = transactionRowsForBrief(a);
    if (a.series || a.kind === "trade") {
      expandedRows.map(seriesInflightItem).filter(Boolean).forEach(enqueueInflight);
    }
    else if (a.kind === "automation") enqueueInflight(automationInflightItem(a));
    else if (pendingTransfer(a)) {
      const outgoing = (a.net || 0) < 0; // money leaving Yoshi can still be pulled back
      const rail = a.rail || "Standard ACH";
      const instant = /second|instant/i.test(a.settles || "");
      enqueueInflight({
        icon: "swap", title: a.title, category: "Transfer", kind: "once", state: "pending",
        agent: false, when: instant ? "Usually in seconds" : "Clears " + a.settles, chip: instant ? "Processing · usually in seconds" : "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: [["Submitted", "Just now"], [instant ? "Processing" : "Pending at the bank network", "You are here"], ["Clears", a.settles]], stepAt: 1,
        settleMs: instant ? 6500 : 22000,
        settle: { icon: "swap", title: a.title, detail: rail + " · cleared", category: "Transfer", accountScope: "yoshi", net: a.net || 0 },
      });
    }
    else if (a.activity) addRecentRow(a.activity);
    const remaining = a.id ? proposals.filter((proposal) => proposal.id !== a.id) : proposals;
    if (a.id) setProposals(remaining);
    setStack([]); setActFilter("all");
    setTransactionStatus({ ...transactionStatusForBrief(a), origin: "brief", destination: needsYouApprovalDestination(a) });
  };
  // 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) submitApprovedBrief({ ...p, activity: proposalActivity(p) }); };
  const declineFromBrief = (id) => { const p = findProp(id); setProposals((ps) => ps.filter((x) => x.id !== id)); flash("Declined" + (p ? " · " + p.title : "")); };
  const completeTransactionStatus = () => {
    setTransactionStatus(null); setStack([]); setActFilter("all");
    if (transactionStatus.destination === "automations") {
      setOverlay({ type: "automations" });
      return;
    }
    setOverlay(null);
  };

  // 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 briefTransactionStatus = transactionStatus?.origin === "brief" && overlay?.type === "briefs";
  const centerNode = transactionStatus && !briefTransactionStatus ? (
    <TransactionSubmissionScreen status={transactionStatus} onDone={completeTransactionStatus} />
  ) : 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} inflight={inflight} instantFundingAvailable={instantFundingAvailable} />}
      {overlay.type === "transfer" && <TransferFlow preset={overlay.from} intent={overlay.intent} initialRail={overlay.rail} rootTitle="Transfer" onClose={nav.closeSheet} nav={nav} flash={flash} onPlaced={placeOrder} settledCashAvailable={transferSettledCashAvailable} pendingIncomingAmount={transferPendingIncomingAmount} />}
      {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} settledCashAvailable={settledCashAvailable} instantFundingAvailable={instantFundingAvailable} unsettledSaleProceeds={unsettledSaleProceedsAvailable} />}
      {overlay.type === "link" && <LinkSheet onClose={nav.closeSheet} />}
      {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 activeHeaderSection = stack.length === 0 ? overlay?.type : null;
  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) · icon mark · 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>
          {/* Updated agency icon mark, extracted from the saved brand PDF. */}
          <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" }}>
            <img src="assets/lucid-yoshi-mark.svg" alt="" style={{ display: "block", height: 28, width: 28 }} />
          </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} style={{ width: TOPBAR_ACTION_WIDTH }} selected={activeHeaderSection === "briefs"} onClick={() => { nav.clearStack(); nav.sheet({ type: "briefs" }); }} />
            <TopAction icon="trade" label="Trade" filled={activeHeaderSection === "trade"} onClick={() => nav.trade()} />
            <TopAction icon="swap" label="Transfer" filled={activeHeaderSection === "transfer"} onClick={() => nav.transfer()} />
          </div>
        </div>

        {fullTakeover ? (
          /* Briefs / Automations take over the full width (chat included) */
          overlay.type === "briefs"
            ? <WebBriefs onApplyAutomationRevision={submitApprovedBrief} proposals={proposals} nav={nav} onClose={nav.closeSheet} onExecute={execFromBrief} onDecline={declineFromBrief}
                initialBriefId={overlay.brief}
                onReviseProposal={reviseBriefProposal}
                readingPane={briefTransactionStatus ? (
                  <InboxCard><TransactionSubmissionScreen status={transactionStatus} onDone={completeTransactionStatus} /></InboxCard>
                ) : null} />
            : <WebAutomations automations={[...reviewedAutomations, ...AUTOMATIONS]} 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} instantFundingUsed={instantFundingUsed} />
              </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} instantFundingUsed={instantFundingUsed} />
                </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 automations={[...reviewedAutomations, ...AUTOMATIONS]} palette={palette} setPalette={setPalette} nav={nav} onClose={nav.closeSheet} flash={flash} initialSection={overlay.section} />
          </aside>
        </>
      )}

      {/* Center details replace the Activity pane, so their transaction receipts
          need an overlay above the center, including security History. */}
      {centerNode && 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>
      )}
      {centerNode && 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 />);
