/* simple/app.jsx — the redesigned shell.
   Three tabs: HOME (see) · YOSHI (act) · ACTIVITY (verify).
   Proposals still flow through the ConfirmSheet primitive. Move and Trade
   open the ORIGINAL TransferFlow / TradeSheet, wired in through a push
   stack + the overlay set they expect, so they work exactly as before.
   The bell (top-right of Home) opens the original BriefsHub. */

const StatusBar = () => (
  <div className="statusbar">
    <span>9:41</span>
    <div className="sb-right">
      <svg width="18" height="12" viewBox="0 0 18 12"><g fill="currentColor">
        <rect x="0" y="8" width="3" height="4" /><rect x="5" y="5" width="3" height="7" /><rect x="10" y="2" width="3" height="10" /><rect x="15" y="0" width="3" height="12" />
      </g></svg>
      <svg width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.4"><path d="M2 4 C5 1.5 11 1.5 14 4" /><path d="M4 6.5 C6 5 10 5 12 6.5" /><circle cx="8" cy="9.5" r="0.9" fill="currentColor" stroke="none" /></svg>
      <svg width="26" height="13" viewBox="0 0 26 13"><rect x="0.5" y="0.5" width="22" height="12" rx="3" fill="none" stroke="currentColor" strokeOpacity="0.5" /><rect x="2" y="2" width="18" height="9" rx="1.5" fill="currentColor" /><rect x="23.5" y="4" width="2" height="5" rx="1" fill="currentColor" fillOpacity="0.5" /></svg>
    </div>
  </div>
);
window.StatusBar = StatusBar;

/* a floating confirmation pill — dark elevated surface, rounded, lifted off
   the bottom edge, with a check glyph. Quiet acknowledgment, not an alert. */
const MOBILE_TOAST_BOTTOM = 112;
const Toast = ({ msg }) => (
  <div style={{ position: "absolute", left: 16, right: 16, bottom: MOBILE_TOAST_BOTTOM, zIndex: 1800, pointerEvents: "none", background: "var(--bg-card)", color: "var(--ink)", border: "1px solid var(--rule-2)", borderRadius: 12, padding: "12px 14px", 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 }}>{msg}</span>
  </div>
);

/* Home · Yoshi · Activity are tabs (switch views); Trade · Move are actions
   (open the Trade / Transfer sheets in place). They sit either side of the
   elevated Yoshi mark so the two money doors are always one tap away. */
const SIMPLE_TABS = [
  { id: "home", label: "Home", icon: "home" },
  { id: "trade", label: "Trade", icon: "trade", sheet: { type: "trade" } },
  { id: "yoshi", label: "Yoshi", icon: null },
  { id: "move", label: "Transfer", icon: "swap", sheet: { type: "transfer" } },
  { id: "activity", label: "Activity", icon: "inbox" },
];

const SimpleTabBar = ({ tab, onTab, badge, nav, overlay }) => (
  <div className="tabbar" style={{ gridTemplateColumns: "repeat(5, 1fr)" }}>
    {SIMPLE_TABS.map(({ id, label, icon, sheet }) => {
      // a flow (Trade / Move) is "active" while its sheet is open; the tabs are
      // active only when no flow is up — so the bar always shows where you are
      const on = sheet ? (!!overlay && overlay.type === sheet.type) : (!overlay && id === tab);
      if (id === "yoshi") return (
        <button key={id} className="tab press" onClick={() => onTab(id)} style={{ overflow: "visible" }}>
          <span style={{
            position: "absolute", top: -26, left: "50%", transform: "translateX(-50%)",
            width: 58, height: 58, borderRadius: 999, background: "var(--bg-card)",
            border: on ? "1.5px solid var(--accent)" : "1px solid var(--rule)",
            boxShadow: "0 8px 22px -8px rgba(0,0,0,0.32), 0 2px 6px -2px rgba(0,0,0,0.18)",
            display: "grid", placeItems: "center",
          }}>
            <Logo size={26} />
          </span>
          <span className="tlabel" style={{ marginTop: 34, color: on ? "var(--ink)" : "var(--ink-3)", fontWeight: on ? 700 : 500 }}>{label}</span>
        </button>
      );
      return (
        <button key={id} className="tab press" onClick={() => sheet ? (nav && nav.sheet(sheet)) : onTab(id)}>
          {on && <span style={{ position: "absolute", top: -9, left: "50%", transform: "translateX(-50%)", width: 18, height: 2, background: "var(--accent)" }} />}
          <div style={{ position: "relative", height: 23, display: "flex", alignItems: "center" }}>
            <Icon name={icon} size={23} stroke={on ? 1.7 : 1.5} color={on ? "var(--ink)" : "var(--ink-3)"} />
          </div>
          <span className="tlabel" style={{ color: on ? "var(--ink)" : "var(--ink-3)", fontWeight: on ? 700 : 500 }}>{label}</span>
        </button>
      );
    })}
  </div>
);

const SimpleApp = () => {
  const [onboarded, setOnboarded] = useState(() => localStorage.getItem("yoshi_onboarded") === "1");
  const [palette, setPaletteState] = useState(() => localStorage.getItem("yoshi_palette") || "graphite");
  const [tab, setTab] = useState("home");
  const [stack, setStack] = useState([]);           // push stack: holding / account / card
  const [overlay, setOverlay] = useState(() => {
    const view = new URLSearchParams(window.location.search).get("view");
    const previewBrief = {
      "bnk-checking-buffer": "p-checking-buffer",
      "bnk-card-spend-spike": "p-card-spend-spike",
      "bnk-card-limit-buffer": "p-card-limit-buffer",
      "bnk-weekly-card-pay": "p-weekly-card-pay",
      "dividend-reinvest": "p-dividend-reinvest",
      "price-move": "b-price-move",
      "price-move-up": "b-price-move-up",
      "portfolio-update-daily": "b-portfolio-update-daily",
      "bnk-account-review-v2": "b-weekly-financial-review-causal",
      "bnk-payday-automations": "b-payday-with-automations",
      "bnk-payday-no-automations": "b-payday-without-automations",
      "earnings-pre": "b-earnings-pre",
      "earnings-post": "b-earnings-post",
      "earnings-post-example": "b-earnings-post-example",
      "earnings-pre-example": "b-earnings-pre-example",
    }[view];
    return previewBrief ? { type: "briefs", brief: previewBrief } : null;
  });     // full-screen sheets
  const [confirming, setConfirming] = useState(null); // action in the ConfirmSheet
  // where the current proposal-review flow (ConfirmSheet + its thread) was
  // launched from, so closing it returns there rather than dumping to Home.
  // "briefs" → reopen the briefs list; null → Home / wherever it sat.
  const [flowFrom, setFlowFrom] = useState(null);
  const [proposals, setProposals] = useState(PROPOSALS);
  // ---- proposal threads (revived from the original shell) — asking Yoshi about
  // a proposal opens a forked thread; a revision expires the old proposal (it
  // greys/strikes out) and drops the new one in, reviewable right there.
  const [threads, setThreads] = useState({});
  const [expired, setExpired] = useState([]);
  const [extraProposals, setExtraProposals] = useState([]); // revised proposals, kept findable for the thread
  const MOD_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 Coming up queue
  const [transactionStatus, setTransactionStatus] = useState(null);
  const INFLIGHT_SEQ = useRef(0);
  const [actFilter, setActFilter] = useState("Yoshi"); // Activity scope filter (lifted so a new txn can focus it on Yoshi)
  const [toastQueue, setToastQueue] = useState([]);
  const toast = toastQueue[0] || null;
  const [chatInject, setChatInject] = useState(null);
  const [hasCard, setHasCard] = useState(() => localStorage.getItem("yoshi_card") === "1");

  useEffect(() => { document.getElementById("root").setAttribute("data-palette", palette); }, [palette]);
  useEffect(() => { window.__liveProposals = proposals; window.setBellCount && window.setBellCount(window.totalBriefCount ? window.totalBriefCount(proposals.length) : proposals.length); }, [proposals]);
  const setPalette = (p) => { setPaletteState(p); localStorage.setItem("yoshi_palette", p); };
  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(() => ({
    tab: (t) => { setStack([]); setOverlay(null); setConfirming(null); setFlowFrom(null); setActFilter("Yoshi"); setTab(t); },
    sheet: (o) => setOverlay(o),
    closeSheet: () => { setFlowFrom(null); setOverlay(null); },
    push: (v) => setStack((s) => [...s, v]),
    pop: () => setStack((s) => s.slice(0, -1)),
    clearStack: () => setStack([]),
    // selecting a security: on phone, a held security opens its detail; a
    // market-only security opens the trade flow (unchanged mobile behavior)
    security: (id) => (typeof securityHolding === "function" && securityHolding(id)) ? setStack((s) => [...s, { type: "holding", id }]) : setOverlay({ type: "trade", id }),
    ask: (note, reply) => { setChatInject({ note, reply }); setStack([]); setOverlay(null); setConfirming(null); setFlowFrom(null); setTab("yoshi"); },
    transfer: (preset = {}) => { setStack([]); setConfirming(null); setOverlay({ type: "transfer", intent: preset.intent, from: preset.from, rail: preset.rail, retry: preset.retry }); },
    automation: (id) => { setStack([]); setOverlay({ type: "automation", id }); },
    // Studio was retired in the redesign; its deep-links resolve to the
    // automations list (the one place a Studio "automate" call still maps to).
    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([]); setTab("home"); },
    // open another Yoshi account from the menu: auto-name it (Cash / Brokerage /
    // Crypto, incrementing to "Cash 2", "Brokerage 2", … when the name is taken),
    // add it, and jump to the Accounts view with the new account highlighted.
    openAccount: (family) => {
      const base = family === "cash" ? "Cash" : family === "crypto" ? "Crypto" : "Brokerage";
      const seed = ["Cash", "Reserve", "Brokerage", "High Risk", "Crypto"];
      const id = "extra-" + family + "-" + Date.now();
      setExtraAccounts((prev) => {
        const taken = new Set([...seed, ...prev.map((a) => a.name)]);
        let name = base, n = 2;
        while (taken.has(name)) { name = base + " " + n; n += 1; }
        return [...prev, { id, family, name }];
      });
      setStack([]); setConfirming(null);
      setOverlay({ type: "accounts", focus: id });
      flash(base + " account opened");
    },
    signOut: () => { localStorage.removeItem("yoshi_onboarded"); setOnboarded(false); setTab("home"); setStack([]); setOverlay(null); setConfirming(null); setProposals(PROPOSALS); setActivityExtra([]); setExtraAccounts([]); setInflight(COMING_UP_SEED); setActFilter("Yoshi"); },
  }), []);

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

  // ---- proposal-thread helpers (ported from the original shell) --------------
  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);
  // mint a revised (smaller) proposal from an existing one
  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 ? { 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))),
      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}".`;
    const revisedTitle = resized ? "Updated · " + scaleStr(p.title, f, base) :
      safer ? "Safer version · " + p.title :
      later ? p.title + " · Friday" :
      "Updated · " + p.title;
    return {
      ...p,
      title: revisedTitle,
      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))),
      amount: p.amount ? scaleStr(p.amount, f, base) : p.amount,
      cashImpact: p.cashImpact ? scaleStr(p.cashImpact, f, base) : p.cashImpact,
    };
  };
  // create (or continue) a proposal's thread, optionally seeding a question
  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 } }));
  // Yoshi revises a proposal inside its thread: expire the old, mint + surface the new
  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;
  };

  /* ---- In motion — every money instruction currently in flight ------------ */
  const inflightRef = useRef(inflight);
  useEffect(() => { inflightRef.current = inflight; }, [inflight]);
  const addInflight = (item) => setInflight((xs) => [{ id: "f" + Date.now(), ...item }, ...xs]);

  /* settlement: the item leaves In motion and drops into Recent with its
     details — the toast is the in-app stand-in for the push notification */
  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);
  };

  /* add an item to the In motion queue — one pulse on arrival, plus a demo
     settlement timer that later drops it into Recent. No navigation. */
  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);
  };
  /* manual placement (Move/Trade): enqueue, then land in Activity to watch it */
  const placeOrder = (item) => {
    enqueueInflight(item);
    setOverlay(null); setStack([]); setConfirming(null);
    setActFilter("Yoshi");
    setTransactionStatus(transactionStatusForTracker(item));
  };
  /* every flow instance (including embedded ones) reaches the tracker
     through this bridge — same pattern as window.setBellCount */
  useEffect(() => { window.yoshiTrack = { placed: placeOrder }; return () => { delete window.yoshiTrack; }; }, []);
  /* how much of an inbound transfer is already committed to queued one-time
     automations — summed from the ONE In-motion list so the trade/move ceiling
     can read it (data.jsx arrivingFree). */
  useEffect(() => { window.yoshiArrivingCommitted = (tid) => inflightRef.current.filter((x) => x.fundedBy === tid).reduce((s, x) => s + (x.funds || 0), 0); return () => { delete window.yoshiArrivingCommitted; }; }, []);
  const cancelInflight = (it) => {
    setInflight((xs) => xs.filter((x) => x.id !== it.id));
    // the canceled instruction drops into the feed as a record, not a hole
    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);
    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));
  };

  /* Every approved brief now produces the same submitted receipt as manual
     transactions. A series expands into real individual Activity outcomes. */
  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);
  };
  const submitApprovedBrief = (a) => {
    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((p) => p.id !== a.id) : proposals;
    if (a.id) setProposals(remaining);
    setOverlay(null); setStack([]); setConfirming(null); setFlowFrom(null); setActFilter("Yoshi");
    setTransactionStatus({ ...transactionStatusForBrief(a), origin: "brief", destination: needsYouApprovalDestination(a) });
  };
  const onDeclined = (a, reason) => {
    if (a.id) setProposals((ps) => ps.filter((p) => p.id !== a.id));
    setConfirming(null);
    flash("Declined · " + reason);
  };
  const proposalActivity = (p) => ({ 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 });
  /* approving/declining INSIDE a brief (board detail) — same settle work as
     the ConfirmSheet path, minus its advance-to-next-review behavior */
  const execFromBrief = (id) => {
    const p = findProp(id); if (!p) return;
    const a = { ...p, activity: p.activity || proposalActivity(p) };
    submitApprovedBrief(a);
  };
  const declineFromBrief = (id) => {
    const p = findProp(id);
    setProposals((ps) => ps.filter((x) => x.id !== id));
    flash("Declined" + (p ? " · " + p.title : ""));
  };
  const review = (p) => p && setConfirming({
    ...p,
    activity: p.activity || proposalActivity(p),
  });
  const completeTransactionStatus = () => {
    setTransactionStatus(null);
    setActFilter("Yoshi");
    if (transactionStatus.destination === "automations") {
      setOverlay({ type: "automations" });
      return;
    }
    setOverlay(null);
    setTab("activity");
  };
  // asking Yoshi about a proposal opens its forked thread (the proposal pins at
  // the top; a revision expires it and drops in the new one, reviewable there)
  const askAboutProposal = (a, text) => {
    setConfirming(null);
    const tid = seedThread(a.id, text && text.trim() ? text.trim() : undefined);
    setOverlay({ type: "thread", tid });
  };

  if (!onboarded) {
    return (
      <ThemeCtx.Provider value={palette}>
        <StatusBar />
        <Onboarding onDone={() => { localStorage.setItem("yoshi_onboarded", "1"); setOnboarded(true); }} />
      </ThemeCtx.Provider>
    );
  }

  const screen = {
    home: <HomeTab nav={nav} proposals={proposals} onReview={(p) => { setFlowFrom(null); review(p); }} inflight={inflight} hideFastPaths />,
    yoshi: <YoshiTab nav={nav} proposals={proposals} onReview={(p) => { setFlowFrom(null); review(p); }} inject={chatInject} onInjected={() => setChatInject(null)} flash={flash} />,
    activity: <ActivityTab nav={nav} extra={activityExtra} coming={inflight} filter={actFilter} onFilter={setActFilter} />,
  }[tab];

  // Trade / Move are flows that keep the bottom nav visible — they render
  // inside the viewport (above the screen, beside the persistent nav), so they
  // behave like Home / Activity. Every other overlay is a full takeover.
  const flowOverlay = overlay && (overlay.type === "trade" || overlay.type === "transfer");
  const hideTabBar = !!transactionStatus || (!!overlay && !flowOverlay) || !!confirming || stack.length > 0 || tab === "yoshi";

  return (
    <ThemeCtx.Provider value={palette}>
      {/* Trade / Move render their own status bar; hide the app's so there's
          only one while a flow is open */}
      {!flowOverlay && <StatusBar />}
      <div className="viewport">
        <div key={tab} className="tab-swap" style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column" }}>
          {screen}
        </div>

        {/* push stack — original holding / account / card detail screens */}
        {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>
        ))}

        {/* Trade / Move flows — rendered INSIDE the viewport so they sit above
            the screen but leave the bottom nav (a flex sibling below) visible,
            the same way Home / Activity keep it. */}
        {overlay?.type === "transfer" && (
          <div style={{ position: "absolute", inset: 0, zIndex: 410, background: "var(--bg)", display: "flex", flexDirection: "column" }}>
            <TransferFlow preset={overlay.from} intent={overlay.intent} initialRail={overlay.rail} rootTitle="Transfer" onClose={nav.closeSheet} nav={nav} flash={flash} onPlaced={placeOrder} />
          </div>
        )}
        {overlay?.type === "trade" && (
          <div style={{ position: "absolute", inset: 0, zIndex: 410, background: "var(--bg)", display: "flex", flexDirection: "column" }}>
            <TradeSheet id={overlay.id} side={overlay.side} hideMenu onClose={nav.closeSheet} nav={nav} onPlaced={placeOrder} />
          </div>
        )}

      </div>

      {!hideTabBar && <SimpleTabBar tab={tab} onTab={nav.tab} badge={proposals.length} nav={nav} overlay={overlay} />}

      {/* modal layers — lifted above the push stack. Account / holding / card
          detail screens are pushed at z-index 400+, so any sheet opened from
          inside one (a transaction detail, a holding detail) must sit above
          that. This host is absolute to the phone screen, not fixed to the
          browser viewport, so full-screen sheets stay clipped by the iPhone
          frame. pointer-events pass through when no sheet is open so the base
          UI stays interactive. */}
      <div data-mobile-shell-layer="modal-host" style={{ position: "absolute", inset: 0, zIndex: 500, pointerEvents: (transactionStatus || confirming || (overlay && !flowOverlay)) ? "auto" : "none" }}>
      {/* redesigned layers */}
      {overlay?.type === "profile" && <ProfileSheet palette={palette} setPalette={setPalette} nav={nav} onClose={nav.closeSheet} flash={flash} initialSection={overlay.section} />}
      {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} />}
      {overlay?.type === "receipt" && <ReceiptSheet tx={overlay.tx} nav={nav} onClose={nav.closeSheet} />}
      {overlay?.type === "automations" && <AutomationsListSheet nav={nav} onClose={nav.closeSheet} />}
      {overlay?.type === "inflight" && <InflightSheet item={inflight.find((x) => x.id === overlay.id)} nav={nav} onClose={nav.closeSheet} onCancel={cancelInflight} onSkip={skipInflight} />}

      {/* Trade / Move render inside the viewport (above), so the nav persists */}
      {overlay?.type === "txn" && <TxnDetailSheet tx={overlay.tx} onClose={nav.closeSheet} nav={nav} onCancel={overlay.item && overlay.cancelable ? () => (overlay.item.kind === "recurring" ? skipInflight(overlay.item) : cancelInflight(overlay.item)) : undefined} />}
      {overlay?.type === "link" && <LinkSheet onClose={nav.closeSheet} />}
      {overlay?.type === "connect" && <ConnectAgentsSheet onClose={nav.closeSheet} nav={nav} />}
      {overlay?.type === "support" && <SupportFlow onClose={nav.closeSheet} nav={nav} />}
      {overlay?.type === "documents" && <DocumentsHub onClose={nav.closeSheet} nav={nav} flash={flash} initialAcct={overlay.acct} />}
      {overlay?.type === "info" && <InfoSheet title={overlay.title} body={overlay.body} onClose={nav.closeSheet} shellLayer />}
      {overlay?.type === "briefs" && <BriefsHub onClose={nav.closeSheet} nav={nav} proposals={proposals} onApprove={(id) => { setFlowFrom("briefs"); review(findProp(id)); }} onExecute={execFromBrief} onDecline={declineFromBrief} onAskProposal={(id, text) => { setFlowFrom("briefs"); const tid = seedThread(id, text); setOverlay({ type: "thread", tid }); }} onReviseProposal={reviseBriefProposal} openNeedsInDetail initialBriefId={overlay.brief} initialBrief={overlay.briefData} />}
      {/* the revived proposal thread — pinned proposal, chat, revise → the old
          greys/strikes out and the new one drops in, reviewable right here */}
      {overlay?.type === "thread" && threads[overlay.tid] && <window.ProposalThread key={overlay.tid} tid={overlay.tid} thread={threads[overlay.tid]}
        findProp={findProp} isExpired={isExpired}
        onReview={(id) => { setOverlay(null); review(findProp(id)); }}
        onModify={reviseInThread} onPersist={persistThread}
        onClose={() => setOverlay(null)} />}
      {overlay?.type === "automation" && <AutomationSheet automation={AUTOMATIONS.find((a) => a.id === overlay.id)} onClose={nav.closeSheet} onBack={overlay.back === "automations" ? () => nav.sheet({ type: "profile", section: "automations" }) : undefined} nav={nav} flash={flash} />}

      {/* the confirm primitive — rendered last so it stays above Briefs and
          Close returns to the list behind it. */}
      {confirming && (
        <ConfirmSheet key={confirming.id} action={confirming} onClose={() => { setConfirming(null); if (flowFrom === "briefs") { setFlowFrom(null); setOverlay({ type: "briefs" }); } }}
          onApproved={submitApprovedBrief} onDeclined={onDeclined} onAsk={askAboutProposal}
          onSource={(id) => { setConfirming(null); nav.automation(id); }} />
      )}
      {transactionStatus && (
        <div style={{ position: "absolute", inset: 0, zIndex: 620, display: "flex", flexDirection: "column", background: "var(--bg)" }}>
          <StatusBar />
          <TransactionSubmissionScreen status={transactionStatus} onDone={completeTransactionStatus} />
        </div>
      )}
      </div>
      {toast && <Toast msg={toast} />}
    </ThemeCtx.Provider>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<SimpleApp />);
