/* web.jsx — the DESKTOP web shell for Yoshi.
   Reuses every mobile screen component (home / chat / briefs / accounts /
   studio / the approval-seam flow / transfer / automations / …) unchanged,
   and re-lays them out as three persistent pillars:
       chat (left, resizable)  ·  home (middle)  ·  briefs (right)
   with a top bar (menu · Home · Studio · Accounts · Trade / Transfer / Agents).
   Bottom sheets and the push stack become centered modals / column overlays.
   The nav API matches app.jsx so the child components route identically. */

const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* lets reused mobile screens render a few desktop-only affordances (e.g. a
   clearer "Analyzing" title on Studio) without touching the mobile build. */
window.__YOSHI_WEB = true;

/* No phone status bar on web: the sheets render `window.StatusBar &&` — leaving
   it undefined simply omits it. */

/* ============================================================
   Top bar
   ============================================================ */
const STUDIO_VIEWS = [["investments", "Investments"], ["cash", "Cash"], ["debt", "Debt"], ["automations", "Automations"]];

/* Studio top-nav as a dropdown — Investments / Cash / Debt / Automations. */
const StudioNav = ({ main, studioView, nav }) => {
  const [open, setOpen] = useState(false);
  const on = main === "studio";
  return (
    <span style={{ position: "relative", display: "inline-flex" }}>
      <button className="topnav press" data-on={on ? "1" : "0"} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen((o) => !o)} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
        Studio
        <Icon name="down" size={13} stroke={2} style={{ opacity: 0.7, transform: open ? "rotate(180deg)" : "none", transition: "transform 160ms ease" }} />
      </button>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 40 }} />
          <div style={{ position: "absolute", top: "calc(100% + 7px)", left: 0, zIndex: 41, width: 216, background: "var(--bg-card)", border: "1px solid var(--rule-2)", borderRadius: 12, overflow: "hidden", boxShadow: "0 18px 48px -20px rgba(0,0,0,0.6)" }}>
            {STUDIO_VIEWS.map(([k, l]) => {
              const active = on && studioView === k;
              return (
                <button key={k} className="press" onClick={() => { nav.studio(k); setOpen(false); }} style={{
                  width: "100%", display: "flex", alignItems: "center", gap: 10, padding: "11px 14px",
                  background: active ? "var(--bg-2)" : "transparent", border: "none", borderTop: k === "investments" ? "none" : "1px solid var(--rule)",
                  cursor: "pointer", textAlign: "left", fontFamily: "var(--f-display)", fontSize: 13, fontWeight: active ? 700 : 500, color: active ? "var(--ink)" : "var(--ink-2)"
                }}>
                  {l}
                  {k === "automations" && <span style={{ marginLeft: "auto", fontFamily: "var(--f-mono)", fontSize: 10.5, color: "var(--ink-3)" }}>{(AUTOMATIONS || []).filter((a) => a.status === "active").length}</span>}
                </button>
              );
            })}
          </div>
        </>
      )}
    </span>
  );
};

const Wordmark = ({ palette, onClick, color }) => {
  const id = palette === "bone" ? "wordmarkBlack" : "wordmarkWhite";
  const fb = palette === "bone" ? "assets/logo-wordmark-black.png" : "assets/logo-wordmark-white.png";
  const src = (window.__resources && window.__resources[id]) || fb;
  return (
    <button className="press" onClick={onClick} 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: color || "var(--ink)", WebkitMaskImage: `url("${src}")`, maskImage: `url("${src}")`, WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat", WebkitMaskPosition: "left center", maskPosition: "left center", WebkitMaskSize: "contain", maskSize: "contain", transition: "background-color 160ms ease" }} />
    </button>
  );
};

const PaletteToggle = ({ palette, setPalette }) => {
  const bone = palette === "bone";
  return (
    <button className="press" role="switch" aria-checked={bone} aria-label="Toggle theme" title={bone ? "Light · Bone" : "Dark · Graphite"}
      onClick={() => setPalette(bone ? "graphite" : "bone")}
      style={{ width: 52, height: 28, flex: "none", padding: 0, position: "relative", background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, cursor: "pointer" }}>
      <span style={{ position: "absolute", top: 2, left: bone ? 26 : 2, width: 22, height: 22, borderRadius: 999, background: "var(--accent)", display: "grid", placeItems: "center", transition: "left 200ms var(--ease)" }}>
        <Icon name={bone ? "eye" : "bulb"} size={13} stroke={1.7} color="var(--accent-ink)" />
      </span>
    </button>
  );
};

const TopBar = ({ main, studioView, palette, setPalette, nav }) => {
  const TradeBtns = [
    ["Trade", "trade", () => nav.sheet({ type: "trade" }), "primary"],
    ["Transfer", "swap", () => nav.sheet({ type: "transfer" }), "ghost"],
    ["Agents", "easel", () => nav.sheet({ type: "connect" }), "ghost"],
  ];
  const bellCount = window.useBellCount ? window.useBellCount() : 0;
  return (
    <header className="topbar">
      <button className="topbar-icon press" aria-label="Menu" title="Menu" onClick={() => nav.tab("profile")}>
        <Icon name="menu" size={22} />
      </button>
      <Wordmark palette={palette} onClick={() => nav.tab("home")} color="var(--ink)" />
      <nav style={{ display: "flex", alignItems: "center", gap: 2, marginLeft: 6 }}>
        <button className="topnav press" data-on={main === "home" ? "1" : "0"} onClick={() => nav.tab("home")}>Home</button>
        <StudioNav main={main} studioView={studioView} nav={nav} />
        <button className="topnav press" data-on={main === "accounts" ? "1" : "0"} onClick={() => nav.tab("accounts")}>Accounts</button>
      </nav>
      <div style={{ flex: 1 }} />
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <button className="topbar-icon press" aria-label={bellCount ? `Notifications · ${bellCount} unread` : "Notifications"} title="Notifications" onClick={() => nav.sheet({ type: "briefs" })} style={{ position: "relative" }}>
          <Icon name="bell" size={21} stroke={1.6} />
          {bellCount > 0 && (
            <span style={{ position: "absolute", top: 2, right: 2, minWidth: 16, height: 16, padding: "0 4px", boxSizing: "border-box", borderRadius: 999, background: "#e5301c", color: "#fff", border: "2px solid var(--bg)", display: "grid", placeItems: "center", fontFamily: "var(--f-mono)", fontSize: 9.5, fontWeight: 700, lineHeight: 1 }}>{bellCount > 9 ? "9+" : bellCount}</span>
          )}
        </button>
        {TradeBtns.map(([label, icon, on, kind]) => (
          <Btn key={label} kind={kind} onClick={on} style={{ width: 118, padding: "9px 0", fontSize: 13 }}>
            <Icon name={icon} size={16} color={kind === "primary" ? "var(--accent-ink)" : "var(--ink)"} /> {label}
          </Btn>
        ))}
      </div>
    </header>
  );
};

/* ============================================================
   Chat pillar — persistent, resizable, never closes
   ============================================================ */
const ChatPanel = ({ width, setWidth, palette, nav, children }) => {
  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 next = Math.max(280, Math.min(900, drag.current.w + (e.clientX - drag.current.x)));
    setWidth(next);
  };
  const onUp = (e) => { drag.current = null; e.currentTarget.removeAttribute("data-drag"); };
  return (
    <aside className="chat-col" style={{ width }}>
      <div className="yo-chatwrap">{children}</div>
      <div className="col-resize" onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp} title="Drag to resize" />
    </aside>
  );
};

/* Drag seam on the LEFT edge of the right pillar. Dragging left widens it;
   it's capped so the center column never drops below centerMin. */
const RightResizer = ({ width, setWidth, chatW, winW, centerMin }) => {
  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 maxW = Math.max(300, winW - chatW - centerMin);
    const next = Math.max(300, Math.min(maxW, drag.current.w - (e.clientX - drag.current.x)));
    setWidth(next);
  };
  const onUp = (e) => { drag.current = null; e.currentTarget.removeAttribute("data-drag"); };
  return <div className="col-resize col-resize-left" onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp} title="Drag to resize" />;
};

/* ============================================================
   WebInbox — the notifications bell, opened as a full-page takeover.
   Two-pane email layout: the brief list (BriefsBoard) on the left, the selected
   brief's body (BriefDetail, chromeless) in the reading pane on the right.
   Carries its own back + close in the top bar.
   ============================================================ */
const WebInbox = ({ onClose, nav, proposals, onApprove, onExecute, onDecline, initialBriefId, initialBrief, threadApi }) => {
  const refreshedAt = (() => { try { return parseInt(localStorage.getItem("yoshi_brief_refreshed_at") || "0", 10) || Date.now(); } catch (e) { return Date.now(); } })();
  const asOf = new Date(refreshedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
  const dailyBrief = window.MORNING_BRIEF ? { id: "daily-brief", kind: "insight", icon: "bulb", title: "Today's read", body: window.MORNING_BRIEF.body, ask: window.MORNING_BRIEF.ask, when: "Last updated " + asOf } : null;
  const [sel, setSelRaw] = useState(() => initialBrief || (initialBriefId === "daily-brief" ? dailyBrief : null));
  // chatting from a Needs-you brief forks the proposal thread INTO the pane
  const [threadTid, setThreadTid] = useState(null);
  const setSel = (x) => { setThreadTid(null); setSelRaw(x); };
  const itemFromProposal = (p) => ({ id: p.id, kind: "approve", icon: "bolt", title: p.title, body: p.why.split(". ")[0] + ".", value: p.net, tone: p.net >= 0 ? "in" : "out", sec: "needsyou", when: p.agent, agent: p.agent, legs: p.legs, net: p.net, proposal: p });

  // mark-as-read advances to the next brief in the catch-up order, like the phone hub
  const feedList = window.BRIEF_FEED ? window.BRIEF_FEED() : [];
  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); };
  // approving / declining a Needs-you proposal happens inline in the reading
  // pane (no modal); clear the pane afterwards so it returns to the empty state.
  const execProposal = (id) => { onExecute && onExecute(id); setSel(null); };
  const declineProposal = (id) => { onDecline && onDecline(id); setSel(null); };

  return (
    <div style={{ flex: 1, minHeight: 0, background: "var(--bg)", display: "flex", flexDirection: "column", animation: "fade-swap 220ms ease both" }} data-screen-label="Notifications">
      {/* top bar — back + title on the left, close on the right */}
      <header className="topbar" style={{ paddingLeft: 8 }}>
        <button className="topbar-icon press" aria-label="Back" title="Back" onClick={onClose}>
          <Icon name="back" size={22} />
        </button>
        <span style={{ fontFamily: "var(--f-display)", fontSize: 21, fontWeight: 700, letterSpacing: "-0.025em", marginLeft: 4 }}>Briefs</span>
        <div style={{ flex: 1 }} />
      </header>

      <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
        {/* LEFT — the list view */}
        <aside style={{ flex: "none", width: 396, minWidth: 320, borderRight: "1px solid var(--rule)", display: "flex", flexDirection: "column", minHeight: 0, background: "var(--bg)" }}>
          <div className="scroll" style={{ paddingBottom: 24 }}>
            <window.BriefsBoard nav={nav} onOpen={setSel} onClose={onClose} proposals={proposals} onApprove={onApprove} selectedId={sel && sel.id} />
          </div>
        </aside>

        {/* RIGHT — the selected brief's body: a raised reading card on a grey field */}
        <section style={{ flex: 1, minWidth: 0, display: "flex", justifyContent: "center", minHeight: 0, background: "var(--bg-2)" }}>
          {sel && threadTid && threadApi ? (
            <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)" }}>
              <window.ProposalThread key={threadTid} tid={threadTid} thread={threadApi.threads[threadTid]}
                findProp={threadApi.findProp} isExpired={threadApi.isExpired}
                onReview={(id) => { const p = threadApi.findProp(id); if (p) { setSel(itemFromProposal(p)); } }}
                onModify={threadApi.onModify} onPersist={threadApi.onPersist}
                onClose={() => setThreadTid(null)} />
            </div>
          ) : sel ? (
            <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)" }}>
              <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>
              <window.BriefDetail key={sel.id} b={sel} embedded onBack={() => setSel(null)} onClose={onClose} nav={nav} onAdvance={advance} hasNext={hasNext} onExecute={execProposal} onDecline={declineProposal}
                onAskProposal={threadApi && sel.proposal ? (text) => setThreadTid(threadApi.seedThread(sel.id, text)) : undefined} />
            </div>
          ) : (
            <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" }}>Select a brief</div>
                <div style={{ fontFamily: "var(--f-display)", fontSize: 13, color: "var(--ink-3)", marginTop: 7, lineHeight: 1.55 }}>Pick anything from the list to read it here.</div>
              </div>
            </div>
          )}
        </section>
      </div>
    </div>
  );
};

/* ============================================================
   AutomationThread — a forked Yoshi chat tied to one automation. Lives in the
   automations inbox's LEFT pane, the way proposal threads take the chat pillar.
   ============================================================ */
const AUTO_THREAD_CHIPS = ["Pause it", "Change the amount", "Raise the threshold", "Explain the risk"];

// Yoshi's conversational reply when the user hasn't named a concrete change yet.
const autoThreadReply = (a, t) => {
  const s = t.toLowerCase();
  if (/attached/.test(s)) return `Got it. I've read it and tied it to "${a.name}" as context. Ask me anything about it.`;
  if (/risk|safe|why|explain|exposure/.test(s)) return `"${a.name}" only does this: ${a.summary} It runs on the schedule you approved and stays inside your limits. I flag anything outside the guardrails before it runs.`;
  if (/pause|stop|turn it off|disable|halt/.test(s)) return `I can pause it. I'll put the change up for you to approve; nothing runs until you turn it back on.`;
  if (/amount|raise|lower|threshold|limit|change|schedule|less|more/.test(s)) return `Sure. Tell me the new value for "${a.name}", an amount or a threshold, and I'll write up the revised rule for you to approve.`;
  return `On "${a.name}": ${a.summary} ${a.next}. I can change the amount, the threshold, the schedule, or pause it. What would you like?`;
};

// Detect a concrete change in the user's message and draft the revised rule.
const proposeAutoChange = (a, text) => {
  const s = text.toLowerCase();
  if (/\b(pause|stop|turn it off|disable|halt)\b/.test(s)) {
    return { kind: "Pause", from: a.status === "active" ? "Active" : a.status === "watching" ? "Watching" : "Paused", to: "Paused", newSummary: a.summary, newStatus: "paused" };
  }
  const m = text.replace(/,/g, "").match(/\$?\s?(\d+(?:\.\d+)?)\s?(k)?/i);
  const bareNumber = /^\s*\$?\s?\d[\d.]*\s?k?\s*$/i.test(text.trim());
  const hasContext = /\$|amount|threshold|limit|raise|lower|change|buy|invest|move|sweep|above|to\b|by\b/.test(s);
  if (m && (hasContext || bareNumber)) {
    let val = parseFloat(m[1]); if (/k/i.test(m[2] || "")) val *= 1000;
    const newAmt = "$" + val.toLocaleString("en-US");
    const old = a.summary.match(/\$[\d,]+(?:\.\d+)?/);
    const oldAmt = old ? old[0] : "current";
    const newSummary = old ? a.summary.replace(old[0], newAmt) : a.summary;
    return { kind: "Amount", from: oldAmt, to: newAmt, newSummary, newStatus: a.status };
  }
  return null;
};

/* the automation pushed up as context — a flat bar attached under the header */
const AutoContextTile = ({ a }) => (
  <div>
    <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
      <span style={{ width: 6, height: 6, borderRadius: 999, flex: "none", background: a.status === "paused" ? "transparent" : "var(--accent)", border: a.status === "paused" ? "1.5px solid var(--ink-3)" : "none" }} />
      <Eyebrow color="var(--accent)">Automation · pinned</Eyebrow>
      <span style={{ marginLeft: "auto", fontFamily: "var(--f-display)", fontSize: 9.5, fontWeight: 600, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--ink-3)" }}>{a.status === "active" ? "Active" : a.status === "watching" ? "Watching" : "Paused"}</span>
    </div>
    <div style={{ fontFamily: "var(--f-display)", fontSize: 14.5, fontWeight: 600, marginTop: 6, letterSpacing: "-0.012em" }}>{a.name}</div>
    <div style={{ fontFamily: "var(--f-display)", fontSize: 12, color: "var(--ink-2)", marginTop: 4, lineHeight: 1.5 }}>{a.summary}</div>
    <div style={{ fontFamily: "var(--f-mono)", fontSize: 10.5, color: "var(--ink-3)", marginTop: 6 }}>{a.next}</div>
  </div>
);

/* a drafted change inside the thread — live (accent, reviewable) or applied (greyed) */
const AutoChangeTile = ({ change, done, onReview }) => (
  <div style={{ alignSelf: "flex-start", maxWidth: "82%", border: `1px solid ${done ? "var(--rule)" : "var(--accent)"}`, background: "var(--bg-card)", borderRadius: 12, opacity: done ? 0.6 : 1, transition: "opacity 280ms ease, border-color 280ms ease" }}>
    <div style={{ padding: "11px 13px 12px" }}>
      <Eyebrow color={done ? "var(--ink-3)" : "var(--accent)"}>{done ? "Applied" : "Proposed change"}</Eyebrow>
      <div style={{ fontFamily: "var(--f-display)", fontSize: 13.5, fontWeight: 600, marginTop: 5 }}>{change.kind === "Pause" ? "Pause this automation" : "Change the " + change.kind.toLowerCase()}</div>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 8, fontFamily: "var(--f-mono)", fontSize: 11.5 }}>
        <span style={{ color: "var(--ink-3)" }}>{change.from}</span>
        <Icon name="arrow" size={13} color="var(--ink-3)" />
        <span style={{ color: done ? "var(--ink-3)" : "var(--ink)", fontWeight: 600 }}>{change.to}</span>
      </div>
      {done ?
      <div style={{ fontFamily: "var(--f-display)", fontSize: 11, color: "var(--ink-3)", marginTop: 9 }}>Applied to your automation.</div> :
      <Btn full onClick={onReview} style={{ marginTop: 11, padding: "10px" }}>Review change <Icon name="arrow" size={16} color="var(--accent-ink)" /></Btn>}
    </div>
  </div>
);

const AutomationThread = ({ a, msgs, typing, onSend, onBack, onReview }) => {
  const [draft, setDraft] = useState("");
  const scrollRef = useRef(null);
  const liveTileRef = useRef(null);
  const [tileVisible, setTileVisible] = useState(true);
  // newest drafted change that hasn't been applied yet
  const liveIdx = (() => { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i].kind === "proposal" && !msgs[i].done) return i; return -1; })();
  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, typing]);
  // once the live change scrolls off, surface a Review shortcut in the header
  useEffect(() => {
    const el = liveTileRef.current, root = scrollRef.current;
    if (!el || !root) { setTileVisible(true); return; }
    const io = new IntersectionObserver((e) => setTileVisible(e[0].isIntersecting), { root, threshold: 0.01 });
    io.observe(el); return () => io.disconnect();
  }, [liveIdx, msgs.length]);
  const send = (t) => { const body = (t != null ? t : draft).trim(); if (!body) return; setDraft(""); onSend(body); };
  return (
    <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", position: "relative" }}>
      <div style={{ flex: "none", padding: "12px 16px 12px", borderBottom: "1px solid var(--rule)", display: "flex", alignItems: "center", gap: 10 }}>
        <button className="press" onClick={onBack} aria-label="Back to the automation" style={{ background: "none", border: "none", padding: 6, margin: "0 -6px", display: "grid", placeItems: "center", color: "var(--ink)", cursor: "pointer", flex: "none" }}>
          <Icon name="back" size={20} stroke={1.7} />
        </button>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 14.5, fontWeight: 600, letterSpacing: "-0.015em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.name}</div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 10.5, color: "var(--ink-3)", letterSpacing: "0.05em", textTransform: "uppercase", marginTop: 1 }}>Yoshi · automation thread</div>
        </div>
        {liveIdx >= 0 && !tileVisible && (
          <button className="press" onClick={() => onReview(liveIdx)} aria-label="Review the proposed change" title="Review the proposed change"
            style={{ flex: "none", display: "inline-flex", alignItems: "center", gap: 5, height: 30, padding: "0 12px", borderRadius: 999, background: "var(--accent)", color: "var(--accent-ink)", border: "none", fontFamily: "var(--f-display)", fontSize: 12.5, fontWeight: 600, letterSpacing: "0.01em", cursor: "pointer", animation: "count-up 200ms ease both" }}>
            Review <Icon name="arrow" size={14} color="var(--accent-ink)" />
          </button>
        )}
      </div>

      <div className="scroll" ref={scrollRef} style={{ padding: "14px 16px 8px", display: "flex", flexDirection: "column", gap: 9 }}>
        <div style={{ position: "sticky", top: 0, zIndex: 5, background: "var(--bg)", margin: "-14px -16px 0", padding: "12px 16px", borderBottom: "1px solid var(--rule)" }}>
          <AutoContextTile a={a} />
        </div>
        {msgs.map((m, i) => m.kind === "proposal"
          ? <div key={i} ref={i === liveIdx ? liveTileRef : undefined} style={{ display: "flex" }}><AutoChangeTile change={m.change} done={m.done} onReview={() => onReview(i)} /></div>
          : <BriefBubble key={i} m={m} />)}
        {typing && <BriefTyping />}
      </div>

      <div style={{ flex: "none", borderTop: "1px solid var(--rule)" }}>
        <div style={{ display: "flex", gap: 7, overflowX: "auto", padding: "10px 16px 6px" }}>
          {AUTO_THREAD_CHIPS.map((q) => (
            <button key={q} className="press" onClick={() => send(q)} style={{ flex: "none", padding: "7px 12px", borderRadius: 999, background: "color-mix(in srgb, var(--ink) 8%, var(--bg-2))", border: "none", fontFamily: "var(--f-display)", fontSize: 12, fontWeight: 500, color: "var(--ink)", whiteSpace: "nowrap", cursor: "pointer" }}>{q}</button>
          ))}
        </div>
        <div style={{ padding: "4px 14px 16px" }}>
          <YoshiComposer value={draft} onChange={setDraft} onSend={() => send()} placeholder="Message Yoshi about this…" onAttach={(doc) => onSend(`Attached ${doc.name}`)} />
        </div>
      </div>
    </div>
  );
};

/* ============================================================
   StudioAutomationsInbox — the Studio "Automations" view as a full-page
   two-pane takeover: the automations list on the left, and on the right either
   the selected automation's detail or, once you chat, a seamless thread with
   the automation pinned up top. A drafted change pops a focused review screen.
   ============================================================ */
const StudioAutomationsInbox = ({ onClose, nav, flash, initialThread }) => {
  const list = AUTOMATIONS || [];
  const [sel, setSel] = useState(list[0] || null);
  // Chatting about an automation pushes it up into a pinned context tile and
  // opens a seamless thread IN THE READING PANE (mirrors a Needs-you proposal).
  // The list stays on the left for navigation; an approved change pops a review.
  const [threads, setThreads] = useState({});
  const [threadOpen, setThreadOpen] = useState(false);
  const [autoTyping, setAutoTyping] = useState(false);
  const [applied, setApplied] = useState({});   // approved rule changes, by automation id
  const [review, setReview] = useState(null);    // { idx } — change under review
  const [listW, setListW] = useState(396);
  const listDrag = useRef(null);
  const view = (a) => (a ? { ...a, ...(applied[a.id] || {}) } : a);
  const openAuto = (a) => { setSel(a); setThreadOpen(false); setReview(null); };
  const askAutomation = (a, text) => {
    const av = view(a);
    setThreadOpen(true);
    setThreads((ts) => ({ ...ts, [a.id]: [...(ts[a.id] || []), { from: "user", t: text }] }));
    setAutoTyping(true);
    setTimeout(() => {
      setAutoTyping(false);
      const change = proposeAutoChange(av, text);
      setThreads((ts) => {
        const arr = [...(ts[a.id] || [])];
        if (change) {
          arr.push({ from: "agent", t: change.kind === "Pause"
            ? `I've drafted the pause for "${av.name}". Review it whenever you're ready.`
            : `Here's the revised rule for "${av.name}", ready for you to review.` });
          arr.push({ kind: "proposal", change });
        } else {
          arr.push({ from: "agent", t: autoThreadReply(av, text) });
        }
        return { ...ts, [a.id]: arr };
      });
    }, 1100);
  };
  const applyChange = () => {
    if (!sel || !review) return;
    const arr = threads[sel.id] || [];
    const change = arr[review.idx] && arr[review.idx].change;
    if (!change) { setReview(null); return; }
    setApplied((p) => ({ ...p, [sel.id]: { ...(p[sel.id] || {}), summary: change.newSummary, status: change.newStatus } }));
    setThreads((ts) => {
      const next = (ts[sel.id] || []).map((m, i) => (i === review.idx ? { ...m, done: true } : m));
      next.push({ from: "agent", t: change.kind === "Pause"
        ? `Done. "${sel.name}" is paused; tell me when to turn it back on.`
        : `Done. "${sel.name}" now reads ${change.to}. It takes effect on the next run.` });
      return { ...ts, [sel.id]: next };
    });
    flash((change.kind === "Pause" ? "Paused · " : "Updated · ") + sel.name);
    setReview(null);
  };
  // arriving from elsewhere (e.g. an automation opened in the Stream pillar):
  // land with that automation selected, opening the thread if a message seeded it
  useEffect(() => {
    if (!initialThread) return;
    const a = list.find((x) => x.id === initialThread.id);
    if (!a) return;
    setSel(a);
    if (initialThread.text) askAutomation(a, initialThread.text);
    else { setThreadOpen(false); setReview(null); }
  }, [initialThread && initialThread.key]);
  const build = () => nav.ask(
    "Build a new automation",
    "Sure. Tell me the rule in plain words, something like \"move anything over $20k in Checking to Savings\" or \"buy $200 of VTI every payday.\" I'll set it up within your limits and show you the first run before it executes."
  );
  return (
    <div style={{ flex: 1, minHeight: 0, background: "var(--bg)", display: "flex", flexDirection: "column", animation: "fade-swap 220ms ease both" }} data-screen-label="Automations">
      <header className="topbar" style={{ paddingLeft: 8 }}>
        <button className="topbar-icon press" aria-label="Back" title="Back" onClick={onClose}><Icon name="back" size={22} /></button>
        <span style={{ fontFamily: "var(--f-display)", fontSize: 16, fontWeight: 700, letterSpacing: "-0.01em", marginLeft: 4 }}>Automations</span>
        <div style={{ flex: 1 }} />
        <button className="topbar-icon press" aria-label="Close" title="Close" onClick={onClose}><Icon name="close" size={20} /></button>
      </header>

      <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
        {/* LEFT — the automations list, or the open automation's thread (resizable) */}
        <aside style={{ flex: "none", width: listW, minWidth: 320, maxWidth: 640, borderRight: "1px solid var(--rule)", display: "flex", flexDirection: "column", minHeight: 0, background: "var(--bg)", position: "relative" }}>
          <div className="col-resize" title="Drag to resize"
            onPointerDown={(e) => { listDrag.current = { x: e.clientX, w: listW }; e.currentTarget.setAttribute("data-drag", "1"); try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {} }}
            onPointerMove={(e) => { if (!listDrag.current) return; setListW(Math.max(320, Math.min(640, listDrag.current.w + (e.clientX - listDrag.current.x)))); }}
            onPointerUp={(e) => { listDrag.current = null; e.currentTarget.removeAttribute("data-drag"); }}
            onPointerCancel={(e) => { listDrag.current = null; e.currentTarget.removeAttribute("data-drag"); }} />
          <div className="scroll" style={{ paddingBottom: 24 }}>
            <div style={{ padding: "16px 18px 14px", borderBottom: "1px solid var(--rule)" }}>
              <div style={{ fontFamily: "var(--f-display)", fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5 }}>Rules you approved that Yoshi continues to run in the background, within your limits.</div>
            </div>
            {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 }} />}
                <window.AutoRow a={view(a)} last={i === list.length - 1} onOpen={() => openAuto(a)} />
              </div>
            ))}
          </div>
        </aside>

        {/* RIGHT — the selected automation's detail, presented exactly like the
           briefs reading pane: a raised square card centered on a lighter field. */}
        <section style={{ flex: 1, minWidth: 0, display: "flex", justifyContent: "center", minHeight: 0, background: "var(--bg-2)" }}>
          {sel ? (
            <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)" }}>
              {threadOpen ? (
                <AutomationThread a={view(sel)} msgs={threads[sel.id] || []} typing={autoTyping}
                  onSend={(t) => askAutomation(sel, t)} onBack={() => setThreadOpen(false)} onReview={(idx) => setReview({ idx })} />
              ) : (
                <window.AutomationSheet key={sel.id} embedded automation={view(sel)} onClose={() => setSel(null)} nav={nav} flash={flash} onThread={(t) => askAutomation(sel, t)} />
              )}
            </div>
          ) : (
            <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)" }}>
                  <Icon name="bolt" size={20} stroke={1.5} />
                </div>
                <div style={{ fontFamily: "var(--f-display)", fontSize: 16, fontWeight: 600, marginTop: 15, letterSpacing: "-0.01em" }}>Select an automation</div>
                <div style={{ fontFamily: "var(--f-display)", fontSize: 13, color: "var(--ink-3)", marginTop: 7, lineHeight: 1.55 }}>Pick a rule to see its performance, run history, and controls.</div>
              </div>
            </div>
          )}
        </section>
      </div>

      {/* change review — a focused approval screen for the drafted change */}
      {review && sel && (() => {
        const change = (threads[sel.id] || [])[review.idx] && (threads[sel.id] || [])[review.idx].change;
        if (!change) return null;
        const av = view(sel);
        const lbl = { fontFamily: "var(--f-display)", fontSize: 11, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-3)", flex: "none" };
        const valS = { fontFamily: "var(--f-mono)", fontSize: 13, textAlign: "right", fontVariantNumeric: "tabular-nums" };
        return (
          <div style={{ position: "absolute", inset: 0, zIndex: 60, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }} data-screen-label="Change review">
            <div onClick={() => setReview(null)} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)", animation: "scrim-in 200ms ease both" }} />
            <div role="dialog" aria-modal="true" style={{ position: "relative", width: "100%", maxWidth: 420, background: "var(--bg-card)", border: "1px solid var(--rule-2)", borderRadius: 16, overflow: "hidden", boxShadow: "0 30px 80px -28px rgba(0,0,0,0.6)", animation: "modal-in 240ms cubic-bezier(0.16,1,0.30,1) both" }}>
              <div style={{ padding: "20px 22px 0" }}>
                <Eyebrow color="var(--accent)">Review change</Eyebrow>
                <div style={{ fontFamily: "var(--f-display)", fontSize: 19, fontWeight: 600, letterSpacing: "-0.02em", marginTop: 8 }}>{change.kind === "Pause" ? "Pause this automation" : "Change the " + change.kind.toLowerCase()}</div>
                <div style={{ fontFamily: "var(--f-display)", fontSize: 12.5, color: "var(--ink-3)", marginTop: 4 }}>{av.name}</div>
              </div>
              <div style={{ margin: "16px 22px", border: "1px solid var(--rule)", borderRadius: 12, overflow: "hidden" }}>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 12, padding: "12px 14px", borderBottom: "1px dashed var(--rule)" }}>
                  <span style={lbl}>Current</span><span style={{ ...valS, color: "var(--ink-3)" }}>{change.from}</span>
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 12, padding: "12px 14px" }}>
                  <span style={lbl}>New</span><span style={{ ...valS, color: "var(--ink)", fontWeight: 600 }}>{change.to}</span>
                </div>
              </div>
              <div style={{ padding: "0 22px", fontFamily: "var(--f-display)", fontSize: 12, color: "var(--ink-2)", lineHeight: 1.5 }}>{change.kind === "Pause" ? "Nothing runs until you turn it back on." : change.newSummary}</div>
              <div style={{ padding: "0 22px" }}><Seam style={{ margin: "16px 0 12px" }} /></div>
              <div style={{ padding: "0 22px 20px", display: "flex", gap: 9 }}>
                <Btn kind="ghost" full onClick={() => setReview(null)}>Cancel</Btn>
                <Btn full onClick={applyChange}>{change.kind === "Pause" ? "Pause automation" : "Apply change"}</Btn>
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
};

/* ============================================================
   Toast
   ============================================================ */
const Toast = ({ msg }) => (
  <div style={{ position: "fixed", left: "50%", bottom: 28, transform: "translateX(-50%)", zIndex: 1400, background: "var(--terminal-fill)", color: "var(--terminal-ink)", padding: "13px 18px", borderRadius: 12, display: "flex", alignItems: "center", gap: 10, animation: "count-up 240ms ease both", boxShadow: "0 14px 40px -12px rgba(0,0,0,0.5)" }}>
    <span style={{ width: 6, height: 6, borderRadius: 999, background: "var(--accent)", flex: "none" }} />
    <span style={{ fontFamily: "var(--f-display)", fontSize: 13.5, fontWeight: 500 }}>{msg}</span>
  </div>
);

/* ============================================================
   Modal host — centers a fixed-size "stage" the reused overlays render into.
   The overlays position themselves with absolute/inset-0/bottom anchoring,
   so the stage is their positioning context (just like the phone screen was).
   ============================================================ */
const Modal = ({ onClose, width = 500, height, bare, children, z = 1200 }) => (
  <div className="modal-layer" style={{ zIndex: z }}>
    <div className="modal-scrim" onClick={onClose} />
    <div className={"modal-stage" + (bare ? " bare" : "")}
      style={{ width: "min(" + width + "px, 96vw)", height: height || "min(880px, 92vh)" }}>
      <div style={{ flex: "none", height: 15 }} />
      <div style={{ position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
        {children}
      </div>
    </div>
  </div>
);

/* Trade / Transfer / Agents render as full-screen takeovers (not centered
   modals): a full-bleed surface with the flow in a centered, hairline-framed
   column. The reused sheets keep their own header + close. */
const WebFlowTakeover = ({ onClose, children }) => (
  <div style={{ position: "fixed", inset: 0, zIndex: 1300, background: "var(--bg)", display: "flex", justifyContent: "center", animation: "fade-swap 200ms ease both" }}>
    <div style={{ position: "relative", width: "100%", maxWidth: 560, display: "flex", flexDirection: "column", minHeight: 0, borderLeft: "1px solid var(--rule)", borderRight: "1px solid var(--rule)", overflow: "hidden" }}>
      {children}
    </div>
  </div>
);

/* Trade / Transfer / Agents as a PAGE beside the persistent chat: the flow
   takes the main area (a centered, hairline-framed column), chat stays left.
   The reused sheet is absolute inset:0 — the relative column hosts it. */
const WebFlowPane = ({ children, wide, onClose }) => (
  <div style={{ flex: 1, minWidth: 360, position: "relative", display: "flex", minHeight: 0, background: "var(--bg)" }}>
    {onClose &&
    <button className="press" onClick={onClose} aria-label="Close" title="Close" style={{ position: "absolute", top: 14, right: 18, zIndex: 40, background: "none", border: "none", display: "flex", color: "var(--ink-3)", cursor: "pointer", padding: 4 }}>
      <Icon name="close" size={20} />
    </button>}
    {wide ? (
      /* wide flows (trade / transfer) span the full area between the chat seam
         and the right edge; their internal layout handles the columns */
      <div style={{ position: "relative", flex: 1, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
        {children}
      </div>
    ) : (
      <div style={{ position: "absolute", inset: 0, display: "flex", justifyContent: "center", overflow: "hidden" }}>
        <div style={{ position: "relative", width: "100%", maxWidth: 560, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
          {children}
        </div>
      </div>
    )}
  </div>
);

/* one quick-action row in the Accounts right rail */
const AcctAction = ({ icon, label, sub, onClick }) => (
  <button className="press" onClick={onClick} style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, padding: "13px 14px", background: "var(--bg-card)", border: "1px solid var(--rule-2)", borderRadius: 12, cursor: "pointer" }}>
    <span style={{ width: 34, height: 34, flex: "none", border: "1px solid var(--rule-2)", borderRadius: 10, display: "grid", placeItems: "center", color: "var(--ink)" }}><Icon name={icon} size={18} stroke={1.6} /></span>
    <div style={{ flex: 1, minWidth: 0 }}>
      <div style={{ fontFamily: "var(--f-display)", fontSize: 13.5, fontWeight: 600 }}>{label}</div>
      {sub && <div style={{ fontFamily: "var(--f-display)", fontSize: 11, color: "var(--ink-3)", marginTop: 2 }}>{sub}</div>}
    </div>
    <Icon name="back" size={14} color="var(--ink-3)" style={{ transform: "scaleX(-1)" }} />
  </button>
);

/* the Accounts right pillar — quick actions that open the page flows */
const AccountsActPanel = ({ nav }) => (
  <aside className="studio-act-col">
    <div className="scroll" style={{ paddingBottom: 20 }}>
      <div style={{ padding: "18px 16px 0" }}><Eyebrow>Quick actions</Eyebrow></div>
      <div style={{ display: "flex", flexDirection: "column", gap: 9, padding: "12px 16px 0" }}>
        <AcctAction icon="swap" label="Transfer" sub="Move money between accounts" onClick={() => nav.sheet({ type: "transfer" })} />
        <AcctAction icon="trade" label="Trade" sub="Buy or sell holdings" onClick={() => nav.sheet({ type: "trade" })} />
        <AcctAction icon="accounts" label="Link account" sub="Connect a bank or brokerage, read-only" onClick={() => nav.sheet({ type: "link" })} />
      </div>
      <section style={{ padding: "20px 16px 0", marginTop: 14, borderTop: "1px solid var(--rule)" }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: 15, fontWeight: 600, letterSpacing: "-0.015em" }}>Bring your agents</span>
        <p style={{ fontFamily: "var(--f-display)", fontSize: 12.5, color: "var(--ink-2)", margin: "5px 0 12px", lineHeight: 1.5 }}>Connect Yoshi to Claude, ChatGPT, or your own agent over MCP, an SDK, or an API key.</p>
        <AcctAction icon="easel" label="Connect an agent" sub="MCP · SDK · API key" onClick={() => nav.sheet({ type: "connect" })} />
      </section>
    </div>
  </aside>
);

/* canned backtest for a drafted rule — deterministic from the title */
const btStatsFor = (s) => { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return { runs: 4 + h % 6, pl: 320 + h % 900, dd: 4 + h % 9 }; };
const DraftBacktest = ({ title, onAction }) => {
  const bt = btStatsFor(title);
  const [acted, setActed] = useState(null);
  const act = (kind) => {
    if (acted) return; setActed(kind);
    onAction(kind === "paper"
      ? "It's running in your paper account now. I'll report each simulated trigger in your stream, and after two weeks you can turn it on for real in one tap."
      : "Drafted. The finished rule, with its caps, is waiting for your approval before anything runs.");
  };
  const stat = (k, v, c) => (
    <div style={{ flex: 1, padding: "8px 10px", borderRight: "1px dashed var(--rule)" }}>
      <div style={{ fontFamily: "var(--f-display)", fontSize: 8.5, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-3)" }}>{k}</div>
      <div style={{ fontFamily: "var(--f-mono)", fontSize: 13, fontWeight: 600, marginTop: 3, fontVariantNumeric: "tabular-nums", color: c || "var(--ink)" }}>{v}</div>
    </div>
  );
  return (
    <div style={{ border: "1px solid var(--rule-2)", background: "var(--bg-card)", borderRadius: 12, overflow: "hidden" }}>
      <div style={{ padding: "9px 12px 7px" }}><Eyebrow>Backtest · last 12 months</Eyebrow></div>
      <div style={{ display: "flex", borderTop: "1px solid var(--rule)" }}>
        {stat("Triggers", bt.runs)}
        {stat("Net P/L", "+" + usd(bt.pl, 0), "var(--accent-pos)")}
        <div style={{ flex: 1, padding: "8px 10px" }}>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 8.5, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-3)" }}>Worst drawdown</div>
          <div style={{ fontFamily: "var(--f-mono)", fontSize: 13, fontWeight: 600, marginTop: 3, fontVariantNumeric: "tabular-nums", color: "var(--signal-neg)" }}>−{bt.dd}%</div>
        </div>
      </div>
      <div style={{ display: "flex", gap: 8, padding: "10px 12px 12px", borderTop: "1px solid var(--rule)" }}>
        <button className="press" onClick={() => act("paper")} style={{ flex: 1, padding: "8px 0", background: acted === "paper" ? "var(--bg-2)" : "none", border: "1px solid var(--rule-2)", borderRadius: 9, fontFamily: "var(--f-display)", fontSize: 12, fontWeight: 600, color: "var(--ink)", cursor: "pointer" }}>{acted === "paper" ? "On paper · 2 wks" : "Run it on paper"}</button>
        <button className="press" onClick={() => act("arm")} style={{ flex: 1, padding: "8px 0", background: "var(--accent)", border: "none", borderRadius: 9, fontFamily: "var(--f-display)", fontSize: 12, fontWeight: 600, color: "var(--accent-ink)", cursor: "pointer" }}>{acted === "arm" ? "Awaiting approval" : "Turn it on"}</button>
      </div>
    </div>
  );
};

/* a fresh thread forked into the chat pillar — used by "Draft an automation"
   and other idea-starters: pinned context up top, the conversation below. */
const InlineThread = ({ title, seedQ, seedA, onClose }) => {
  const [msgs, setMsgs] = useState([{ from: "user", t: seedQ }]);
  const [typing, setTyping] = useState(true);
  const [draft, setDraft] = useState("");
  const scrollRef = useRef(null);
  useEffect(() => { const t = setTimeout(() => { setTyping(false); setMsgs((m) => [...m, { from: "agent", t: seedA }, { kind: "bt" }]); }, 950); return () => clearTimeout(t); }, []);
  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, typing]);
  const send = (t) => {
    const body = (t != null ? t : draft).trim(); if (!body) return;
    setDraft("");
    setMsgs((m) => [...m, { from: "user", t: body }]);
    setTyping(true);
    setTimeout(() => { setTyping(false); setMsgs((m) => [...m, { from: "agent", t: `Noted — I'll fold that into the draft. I'll show you the full rule with caps before anything turns on.` }]); }, 1000);
  };
  return (
    <div className="push-enter" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", position: "relative" }}>
      <div style={{ flex: "none", padding: "8px 16px 10px", borderBottom: "1px solid var(--rule)", display: "flex", alignItems: "center", gap: 10 }}>
        <button className="press" onClick={onClose} aria-label="Back to chat" style={{ background: "none", border: "none", padding: 6, margin: "0 -6px", display: "grid", placeItems: "center", color: "var(--ink)", cursor: "pointer", flex: "none" }}>
          <Icon name="back" size={20} stroke={1.7} />
        </button>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 14.5, fontWeight: 600, letterSpacing: "-0.015em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{title}</div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 10.5, color: "var(--ink-3)", letterSpacing: "0.05em", textTransform: "uppercase", marginTop: 1 }}>Yoshi · new thread</div>
        </div>
      </div>
      <div className="scroll" ref={scrollRef} style={{ padding: "14px 16px 8px", display: "flex", flexDirection: "column", gap: 9 }}>
        <div style={{ border: "1px solid var(--accent)", background: "var(--bg-card)", borderRadius: 12, padding: "11px 13px" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
            <span style={{ fontFamily: "var(--f-mono)", fontSize: 11, color: "var(--accent)" }}>◆</span>
            <Eyebrow>Draft · pinned</Eyebrow>
          </div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 13.5, fontWeight: 600, marginTop: 6 }}>{title}</div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: 11.5, color: "var(--ink-3)", marginTop: 3, lineHeight: 1.45 }}>Nothing runs until you approve the finished rule.</div>
        </div>
        {msgs.map((m, i) => m.kind === "bt"
          ? <DraftBacktest key={i} title={title} onAction={(t) => setMsgs((mm) => [...mm, { from: "agent", t }])} />
          : <BriefBubble key={i} m={m} />)}
        {typing && <BriefTyping />}
      </div>
      <div style={{ flex: "none", borderTop: "1px solid var(--rule)", padding: "10px 14px 16px" }}>
        <YoshiComposer value={draft} onChange={setDraft} onSend={() => send()} placeholder="Message Yoshi about this…" onAttach={(doc) => send(`Attached ${doc.name}`)} />
      </div>
    </div>
  );
};

/* push-stack detail (holding / account / card) wants a full-height flex column
   with a positioned ancestor — give it exactly that inside the stage. */
const DetailFrame = ({ children }) => (
  <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "var(--bg)" }}>
    {children}
  </div>
);

/* a small floating close affordance for detail / profile modals (the reused
   detail screens carry their own Back, but a dismiss for the whole modal helps) */
const ModalClose = ({ onClose }) => (
  <button className="press" onClick={onClose} aria-label="Close" title="Close"
    style={{ position: "absolute", top: 10, right: 10, zIndex: 60, width: 34, height: 34, borderRadius: 999, background: "var(--bg-2)", border: "1px solid var(--rule-2)", display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer" }}>
    <Icon name="close" size={17} />
  </button>
);

/* ============================================================
   App
   ============================================================ */
/* a few extra real proposals so the desktop "Needs you" coverflow has depth.
   Web-only (added to the initial deck here, not in shared data.jsx). */
const WEB_EXTRA_PROPOSALS = [
  { id: "pw1", agent: "Yoshi agent", channel: "in-app", title: "Harvest a tax loss",
    why: "AMD is down enough to sell at a loss and offset gains you've realized this year. You'd trim your tax bill, and you can rebuy a similar fund after 31 days.",
    net: 318.00, settles: "Tomorrow", kind: "trade", cat: "investments",
    legs: [["SELL", "AMD", "9 sh", "\u2248 $1,520"], ["BUY", "QQQ", "3 sh", "\u2248 $1,537"]] },
  { id: "pw2", agent: "Cash agent", channel: "in-app", title: "Top up your emergency fund",
    why: "Your Savings dipped below your six-month target. Moving $1,500 from Checking refills the buffer, and you still keep enough cash on hand for the month.",
    net: 0, settles: "Today", kind: "transfer", cat: "cash",
    legs: [["MOVE", "Checking \u20228841", "To Savings", "$1,500"], ["AFTER", "Savings \u20222207", "Balance", "$65,500"]] },
  { id: "pw3", agent: "External agent", channel: "whatsapp", title: "Trim SOL into cash",
    why: "Solana has run up and is now a large slice of your crypto. Selling a little locks in some of the gain and keeps the sleeve closer to your target weight.",
    net: 1880.00, settles: "in 2 days", kind: "trade", cat: "investments",
    legs: [["SELL", "SOL", "10", "\u2248 $1,884"], ["TO", "Cash \u20228841", "Proceeds", "$1,884"]] },
];
const WEB_PROPOSALS = [...PROPOSALS, ...WEB_EXTRA_PROPOSALS];

const App = () => {
  // Web is graphite-only (light mode removed); don't read the mobile palette pref.
  const [palette, setPaletteState] = useState("graphite");
  const [main, setMain] = useState("home");        // home | studio | accounts
  const [studioView, setStudioView] = useState(null);
  const [stack, setStack] = useState([]);           // push: holding / account / card
  const [overlay, setOverlay] = useState(null);     // centered sheet
  const [profileOpen, setProfileOpen] = useState(false);
  const [menuPage, setMenuPage] = useState(null); // menu drawer's right-hand page: "settings" | "documents"
  const [proposals, setProposals] = useState(WEB_PROPOSALS);
  useEffect(() => { window.setBellCount && window.setBellCount(proposals.length); }, [proposals]);
  const [completed, setCompleted] = useState(COMPLETED);
  const [toast, setToast] = useState(null);
  const [chatInject, setChatInject] = useState(null);
  const [autoSeed, setAutoSeed] = useState(null); // deep-link: automations page + open thread
  const [threads, setThreads] = useState({});
  const [extraProposals, setExtraProposals] = useState([]);
  const [expired, setExpired] = useState([]);
  const MOD_SEQ = useRef(0);
  const [hasCard, setHasCard] = useState(() => localStorage.getItem("yoshi_card") === "1");
  const [chatW, setChatW] = useState(() => {
    const v = parseInt(localStorage.getItem("yoshi_web_chatw") || "0", 10);
    return v >= 280 && v <= 900 ? v : 440;
  });
  const [rightW, setRightW] = useState(() => {
    const v = parseInt(localStorage.getItem("yoshi_web_rightw") || "0", 10);
    return v >= 300 && v <= 560 ? v : 360;
  });

  useEffect(() => { document.getElementById("app").setAttribute("data-palette", palette); }, [palette]);
  const setPalette = (p) => { setPaletteState(p); localStorage.setItem("yoshi_palette", p); };
  const setChatWidth = (w) => { setChatW(w); localStorage.setItem("yoshi_web_chatw", String(Math.round(w))); };
  const setRightWidth = (w) => { setRightW(w); localStorage.setItem("yoshi_web_rightw", String(Math.round(w))); };
  const getCard = () => { localStorage.setItem("yoshi_card", "1"); setHasCard(true); };

  const flash = useCallback((msg) => { setToast(msg); setTimeout(() => setToast((t) => (t === msg ? null : t)), 2600); }, []);

  const nav = useMemo(() => ({
    tab: (t) => {
      setStack([]); setOverlay(null);
      if (t === "profile") { setProfileOpen(true); setMenuPage(null); return; }
      setProfileOpen(false); setMenuPage(null);
      if (t === "chat") return;                 // chat is always-on (left pillar)
      if (t === "trade") { setOverlay({ type: "trade" }); return; }
      setMain(["home", "studio", "accounts"].includes(t) ? t : "home");
    },
    studio: (view) => { setStack([]); setOverlay(null); setProfileOpen(false); setStudioView(view || "investments"); setMain("studio"); },
    push: (v) => setStack((s) => [...s, v]),
    pop: () => setStack((s) => s.slice(0, -1)),
    sheet: (o) => { setProfileOpen(false); setMenuPage(null); setOverlay(o); },
    closeSheet: () => setOverlay(null),
    closeMenu: () => { setProfileOpen(false); setMenuPage(null); },
    // open a page beside the menu drawer (drawer stays open), like the Agents pane
    menuPage: (p) => { setProfileOpen(true); setMenuPage(p); },
    ask: (note, reply) => { setChatInject({ note, reply }); setStack([]); setOverlay(null); setProfileOpen(false); setStudioView((v) => (v === "automations" ? "investments" : v)); },
    automation: (id) => { setStack([]); setOverlay({ type: "automation", id }); },
    // jump to the Studio Automations page with this automation's thread open
    automationChat: (id, text) => { setStack([]); setOverlay(null); setProfileOpen(false); setAutoSeed({ id, text, key: Date.now() }); setStudioView("automations"); setMain("studio"); },
    signOut: () => { setProfileOpen(false); setStack([]); setOverlay(null); setProposals(WEB_PROPOSALS); setCompleted(COMPLETED); setHasCard(false); localStorage.removeItem("yoshi_card"); setMain("home"); flash("Signed out"); },
  }), [flash]);

  const openApproval = (id) => setOverlay({ type: "approve", id });

  // "Build a basket" — the generated basket lands as a real proposal in Needs you
  const proposeBasket = (b, amount) => {
    const id = "pb" + Date.now();
    const top = b.holdings.slice(0, 3).map((h) => ["BUY", h.ticker, h.weight + "%", "≈ " + usd(amount * h.weight / 100, 0)]);
    const restW = b.holdings.slice(3).reduce((s, h) => s + h.weight, 0);
    const legs = restW > 0 ? [...top, ["BUY", "+" + (b.holdings.length - 3) + " more", restW + "%", "≈ " + usd(amount * restW / 100, 0)]] : top;
    setProposals((ps) => [{ id, agent: "Yoshi agent", channel: "in-app", title: "Buy basket · " + b.name,
      why: `Funds your "${b.name}" basket across ${b.holdings.length} positions, weighted as generated. No single name above ${b.cap}%.`,
      net: -amount, settles: "Tomorrow", kind: "trade", cat: "investments", legs }, ...ps]);
    return id;
  };

  // remember the last non-automations Studio view so closing the Automations
  // takeover returns the user to where they were.
  const lastStudioMainView = useRef("investments");
  useEffect(() => { if (["investments", "cash", "debt"].includes(studioView)) lastStudioMainView.current = studioView; }, [studioView]);

  // Esc closes the topmost layer (sheet → detail → profile).
  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== "Escape") return;
      if (overlay) setOverlay(null);
      else if (stack.length) setStack([]);
      else if (profileOpen) setProfileOpen(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [overlay, stack.length, profileOpen]);

  // ---- proposal threads (identical logic to the mobile shell) ----------------
  const findProp = (id) => proposals.find((x) => x.id === id) || extraProposals.find((x) => x.id === id) || PROPOSALS.find((x) => x.id === id);
  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 hasThread = (id) => !!threads[tidFor(id)];

  const scaleStr = (s, f) => String(s).replace(/\d[\d,]*(\.\d+)?/g, (mm) => {
    const n = parseFloat(mm.replace(/,/g, "")) * f;
    if (n >= 1000) return Math.round(n).toLocaleString();
    if (n >= 1) return (Math.round(n * 100) / 100).toLocaleString();
    return String(Math.round(n * 1000) / 1000);
  });
  const makeRevised = (p) => {
    MOD_SEQ.current += 1;
    const f = 0.5;
    return {
      ...p,
      id: p.id + "_m" + MOD_SEQ.current,
      modifiedFrom: p.id,
      net: Math.round(p.net * f * 100) / 100,
      why: "Revised down per your note: about half the original size, same direction. Smaller market impact, and it still moves you toward your target.",
      legs: p.legs.map((l) => l.map((cell, i) => (i >= 2 ? scaleStr(cell, f) : cell))),
    };
  };
  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 openThread = (pid, userMsg) => {
    const tid = seedThread(pid, userMsg);
    setOverlay({ type: "thread", 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 onApproved = (id) => {
    const p = proposals.find((x) => x.id === id);
    const rest = proposals.filter((x) => x.id !== id);
    setProposals(rest);
    setCompleted((cs) => [{ id: "ex_" + id, title: p.title, detail: p.legs.map((l) => l[1]).join(" · "), when: "May 29", net: p.net, by: p.agent }, ...cs]);
    if (rest[0]) setOverlay({ type: "approve", id: rest[0].id }); else setOverlay(null);
    flash("Executed · " + p.title);
  };
  const onDeclined = (id, reason) => {
    const rest = proposals.filter((x) => x.id !== id);
    setProposals(rest);
    if (rest[0]) setOverlay({ type: "approve", id: rest[0].id }); else setOverlay(null);
    flash("Declined · " + reason);
  };
  const onModified = (id, note) => {
    const p = proposals.find((x) => x.id === id);
    setOverlay(null);
    setChatInject({ note, reply: `Got it — on "${p.title}": "${note}". Let me take a look and come back with an updated proposal you can approve.` });
    flash("Sent back to Yoshi");
  };

  // ---- the active main-column screen ----------------------------------------
  const sv = ["investments", "cash", "debt"].includes(studioView) ? studioView : "investments";
  const StudioViewComp = { investments: window.InvestmentsView, cash: window.CashView, debt: window.DebtView }[sv];
  const studioAutomations = main === "studio" && studioView === "automations";

  // Both side pillars (chat on the left, the right pillar) are user-resizable
  // via drag seams. The right pillar is NOT manually dockable — it docks below
  // the center column automatically, and only when the window is too narrow to
  // hold all three at a comfortable center width.
  const [winW, setWinW] = useState(() => window.innerWidth);
  useEffect(() => {
    const onResize = () => setWinW(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  const studioKey = main === "studio" ? "studio:" + sv : main;
  // the center pillar is prioritized: it must keep at least CENTER_MIN px. When
  // chat + right + that minimum no longer fit, the right pillar drops below.
  const CENTER_MIN = 420;
  const tooNarrow = winW < chatW + rightW + CENTER_MIN;
  const pillarDocked = tooNarrow;

  const mainScreen = main === "home"
    ? <window.Home nav={nav} proposals={proposals} completed={completed} onApprove={openApproval} onBell={() => setOverlay({ type: "briefs" })} />
    : main === "studio"
      ? <div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}><div className="scroll">
          <div style={{ padding: "24px 18px 0", marginBottom: -10, fontFamily: "var(--f-display)", fontSize: 21, fontWeight: 700, letterSpacing: "-0.025em" }}>{sv === "cash" ? "Cash" : sv === "debt" ? "Debt" : "Investments"} Studio</div>
          {StudioViewComp && <StudioViewComp nav={nav} proposals={proposals} onReview={openApproval} />}
        </div></div>
      : <window.Accounts nav={nav} hasCard={hasCard} linkUnderTotal={pillarDocked} />;

  // right pillar: Stream on home, the Studio "Act" surface on studio (inv/cash/debt)
  const rightPillar = main === "home"
    ? <window.StreamPanel standalone nav={nav} completed={completed} />
    : main === "studio"
      ? <window.StudioActPanel standalone view={sv} nav={nav} proposals={proposals} onReview={openApproval} />
      : main === "accounts"
        ? <window.AccountsExternalPanel nav={nav} />
        : null;
  // inline form of the same pillar, for when it docks below the center column
  const rightInline = main === "home"
    ? <window.StreamPanel nav={nav} completed={completed} />
    : main === "studio"
      ? <window.StudioActPanel view={sv} nav={nav} proposals={proposals} onReview={openApproval} />
      : main === "accounts"
        ? <window.AccountsExternalPanel nav={nav} inline />
        : null;

  // Trade / Transfer / Agents open as a page beside the persistent chat (not a
  // modal): the flow takes the main area, chat stays on the left.
  const flowType = overlay && ["trade", "transfer", "connect", "basket"].includes(overlay.type) ? overlay.type : null;

  const closeAutomations = () => nav.studio(lastStudioMainView.current || "investments");

  const detail = stack[stack.length - 1];

  return (
    <ThemeCtx.Provider value={palette}>
      <TopBar main={main} studioView={studioView} palette={palette} setPalette={setPalette} nav={nav} />
      {studioAutomations ? (
        <StudioAutomationsInbox onClose={closeAutomations} nav={nav} flash={flash} initialThread={autoSeed} />
      ) : overlay && overlay.type === "briefs" ? (
        <WebInbox onClose={nav.closeSheet} nav={nav} proposals={proposals} onApprove={openApproval}
          onExecute={(id) => {
            const p = proposals.find((x) => x.id === id); if (!p) return;
            setProposals((ps) => ps.filter((x) => x.id !== id));
            setCompleted((cs) => [{ id: "ex_" + id, title: p.title, detail: p.legs.map((l) => l[1]).join(" \u00b7 "), when: "May 29", net: p.net, by: p.agent }, ...cs]);
            flash("Executed \u00b7 " + p.title);
          }}
          onDecline={(id) => { setProposals((ps) => ps.filter((x) => x.id !== id)); flash("Declined"); }}
          initialBriefId={overlay.brief} initialBrief={overlay.briefData}
          threadApi={{ threads, seedThread, findProp, isExpired, onModify: reviseInThread, onPersist: persistThread }} />
      ) : (
        <>
          <div className="pillars">
            {overlay && ["approve", "thread", "draft"].includes(overlay.type) ? (
              <aside className="chat-col" style={{ width: chatW }}>
                {overlay.type === "approve" ? (
                  <window.ApprovalSheet key={overlay.id} proposal={findProp(overlay.id)} onClose={nav.closeSheet}
                    onApproved={onApproved} onDeclined={onDeclined} onModified={onModified}
                    onOpenThread={openThread} hasThread={hasThread(overlay.id)} />
                ) : overlay.type === "draft" ? (
                  <InlineThread key={overlay.title + overlay.q} title={overlay.title} seedQ={overlay.q} seedA={overlay.a} onClose={nav.closeSheet} />
                ) : (
                  <window.ProposalThread key={overlay.tid} tid={overlay.tid} thread={threads[overlay.tid]}
                    findProp={findProp} isExpired={isExpired} onReview={openApproval} onModify={reviseInThread}
                    onPersist={persistThread} onClose={nav.closeSheet} />
                )}
              </aside>
            ) : (
              <ChatPanel width={chatW} setWidth={setChatWidth} palette={palette} nav={nav}>
                <window.Chat nav={nav} proposals={proposals} onApprove={openApproval} inject={chatInject} onInjected={() => setChatInject(null)} />
              </ChatPanel>
            )}

            {flowType ? (
              <WebFlowPane wide={flowType === "trade" || flowType === "transfer" || flowType === "basket"} onClose={flowType === "connect" ? nav.closeSheet : undefined}>
                {flowType === "trade" && <window.TradeSheet id={overlay.id} side={overlay.side} onClose={nav.closeSheet} nav={nav} />}
                {flowType === "transfer" && <window.TransferFlow preset={overlay.from} intent={overlay.intent} initialRail={overlay.rail} onClose={nav.closeSheet} nav={nav} flash={flash} />}
                {flowType === "connect" && <window.ConnectAgentsSheet onClose={nav.closeSheet} nav={nav} />}
                {flowType === "basket" && <window.BasketStudio onClose={nav.closeSheet} nav={nav} onPropose={proposeBasket} onReview={(id) => setOverlay({ type: "approve", id })} />}
              </WebFlowPane>
            ) : (
              <>
                <main className={pillarDocked ? "main-col docked" : "main-col"}>
                  {detail ? (
                    /* push-stack detail (holding / account / card) renders center-screen;
                       its own NavBar back arrow pops back to the underlying view */
                    <div className="tab-swap" style={{ position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
                      <DetailFrame>
                        {detail.type === "holding" && <window.HoldingDetail id={detail.id} nav={nav} />}
                        {detail.type === "account" && <window.AccountDetail acct={detail.acct} nav={nav} />}
                        {detail.type === "card" && <window.CardDetail nav={nav} flash={flash} hasCard={hasCard} onGetCard={getCard} />}
                      </DetailFrame>
                    </div>
                  ) : (
                    <div key={studioKey} className="yo-mainwrap tab-swap">{mainScreen}</div>
                  )}
                  {pillarDocked && rightInline && (
                    /* docked: the panel flows on as a continuation of the center column */
                    <section className="pillar-dock" style={{ flex: "none", display: "flex", flexDirection: "column", background: "var(--bg)" }}>
                      <div className="scroll">{rightInline}</div>
                    </section>
                  )}
                </main>
                {!pillarDocked && rightPillar && (
                  <div className="right-col-wrap" style={{ width: rightW }}>
                    <RightResizer width={rightW} setWidth={setRightWidth} chatW={chatW} winW={winW} centerMin={CENTER_MIN} />
                    {rightPillar}
                  </div>
                )}
              </>
            )}
          </div>
        </>
      )}

      {/* ---- Menu drawer — pulls out from the top-left hamburger, takes over
           the wordmark + nav. Hamburger morphs to an X (in the drawer header). ---- */}
      {profileOpen && (
        <>
          <div onClick={nav.closeMenu} style={{ position: "fixed", inset: 0, zIndex: 1240, background: "rgba(0,0,0,0.4)", animation: "scrim-in 200ms ease both" }} />
          <aside style={{ position: "fixed", top: 0, left: 0, bottom: 0, width: "min(384px, 88vw)", 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.45)", animation: "drawer-in 240ms cubic-bezier(0.16,1,0.30,1) both" }} data-screen-label="Menu">
            <window.Profile palette={palette} setPalette={setPalette} nav={nav} onClose={nav.closeMenu} />
          </aside>
          {menuPage && (
            /* the page beside the drawer — same centered, framed look as Agents */
            <section style={{ position: "fixed", top: 0, left: "min(384px, 88vw)", right: 0, bottom: 0, zIndex: 1250, background: "var(--bg)", display: "flex", minHeight: 0, animation: "fade-swap 200ms ease both" }} data-screen-label="Menu page">
              <WebFlowPane>
                {menuPage === "settings" && <window.SettingsScreen palette={palette} setPalette={setPalette} onClose={() => setMenuPage(null)} />}
                {menuPage === "documents" && <window.DocumentsHub onClose={() => setMenuPage(null)} nav={nav} flash={flash} />}
              </WebFlowPane>
            </section>
          )}
        </>
      )}

      {/* ---- push-stack detail now renders center-screen in the main column ---- */}

      {/* ---- Notifications bell — now rendered in the body below the persistent toolbar (see above) ---- */}

      {/* ---- Trade / Transfer / Agents now render inline as a page beside chat
           (see flowType + WebFlowPane in the pillars above) ---- */}

      {/* ---- other sheets / flows (centered modal) ---- */}
      {overlay && !["briefs", "trade", "transfer", "connect", "approve", "thread", "draft"].includes(overlay.type) && (() => {
        const close = nav.closeSheet;
        const wide = overlay.type === "documents" ? 560 : overlay.type === "txn" ? 460 : 500;
        return (
          <Modal onClose={close} width={wide} z={1200}>
            {overlay.type === "support" && <window.SupportFlow onClose={close} nav={nav} />}
            {overlay.type === "documents" && <window.DocumentsHub onClose={close} nav={nav} flash={flash} initialAcct={overlay.acct} />}
            {overlay.type === "link" && <window.LinkSheet onClose={close} />}
            {overlay.type === "txn" && <window.TxnDetailSheet tx={overlay.tx} onClose={close} nav={nav} />}
            {overlay.type === "automation" && <window.AutomationSheet automation={AUTOMATIONS.find((a) => a.id === overlay.id)} onClose={close} nav={nav} flash={flash} />}
          </Modal>
        );
      })()}

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

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