/* home.jsx, the dashboard. Reads top→bottom:
   header · net worth + live sparkline · allocation · quick actions ·
   what's pending (top proposal) · recent activity · automations. */

/* net-worth across horizons. Today reuses the current-day series (so the
   agent-action markers line up); the rest are generated to hit a plausible
   period return ending at today's balance. */
const NW_RANGES = ["Today", "1W", "1M", "6M", "YTD", "1Y", "5Y", "ALL"];
const NW_META = {
  "Today": { pct: DAY_PCT, label: "today", len: 34, vol: 0.0016, markers: true },
  "1W": { pct: 1.1, label: "past week", len: 28, vol: 0.0045, seed: 218 },
  "1M": { pct: 3.6, label: "past month", len: 32, vol: 0.007, seed: 222 },
  "6M": { pct: 14.6, label: "past 6 months", len: 48, vol: 0.014, seed: 226 },
  "YTD": { pct: 9.8, label: "year to date", len: 44, vol: 0.013, seed: 232 },
  "1Y": { pct: 22.4, label: "past year", len: 52, vol: 0.017, seed: 230 },
  "5Y": { pct: 96.2, label: "past 5 years", len: 64, vol: 0.03, seed: 244 },
  "ALL": { pct: 142.0, label: "all time", len: 72, vol: 0.034, seed: 240 }
};
const buildNW = (range) => {
  const m = NW_META[range];
  if (range === "Today") return { data: NW_SERIES, abs: DAY_ABS, pct: DAY_PCT, label: m.label, markers: NW_MARKERS };
  const start = NET_WORTH / (1 + m.pct / 100);
  return { data: series(m.seed, m.len, start, NET_WORTH, m.vol), abs: NET_WORTH - start, pct: m.pct, label: m.label, markers: [] };
};

const ACTIVITY_FILTERS = [
["all", "All"], ["automations", "Automations"], ["spending", "Spending"],
["investments", "Investments"], ["transfers", "Transfers"], ["income", "Income"]];

/* forward-looking schedule — shown above recent activity in one endless feed */
const COMING_UP = [
{ icon: "bolt", cat: "automations", autoId: "a1", title: "Weekly investing · VTI", sub: "Automation", when: "Fri · Jun 5", amount: 500, tone: "out" },
{ icon: "card", cat: "spending", title: "Netflix", sub: "Subscription · Yoshi card ••4417", when: "Sun · Jun 8", amount: 24.99, tone: "out" },
{ icon: "bank", cat: "income", title: "Pay day", sub: "Direct deposit to Yoshi brokerage ••3456", when: "Mon · Jun 15", amount: 8200, tone: "in" }];


/* clearer feed section header — sentence case, legible, replaces the tiny
   tracked eyebrow so category titles read at a glance. */
const SecTitle = ({ children, color = "var(--ink)" }) =>
<div style={{ fontFamily: "var(--f-display)", fontSize: window.__YOSHI_WEB ? 17 : IOS_TYPE.headline, fontWeight: 500, letterSpacing: "normal", color, display: "inline-flex", alignItems: "center" }}>{children}</div>;


const Home = ({ nav, proposals, completed, onApprove, onSeeStream, onBell }) => {
  const [marker, setMarker] = useState(null);
  const [scrubVal, setScrubVal] = useState(null);
  const [page, setPage] = useState(0);
  const [range, setRange] = useState("Today");

  const nw = useMemo(() => buildNW(range), [range]);

  const display = scrubVal != null ? scrubVal : NET_WORTH;

  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
      {/* header strip — same geometry as NavBar so the bell sits identically across tabs */}
      <div style={{ flex: "none", padding: "4px 16px 4px", display: "flex", alignItems: "center", gap: 10, minHeight: 40 }}>
        <YouButton nav={nav} />
        <span style={{ marginLeft: "auto" }}><window.BellButton nav={nav} onClick={onBell} /></span>
      </div>

      <div className="scroll">
        {/* BAND 1 · net worth + sparkline */}
        <section style={{ padding: window.__YOSHI_WEB ? "22px 18px 4px" : "0 18px 4px" }}>
          <Eyebrow style={window.__YOSHI_WEB ? { fontSize: typeSize(14), letterSpacing: "normal" } : undefined}>Yoshi balance</Eyebrow>
          <div style={{ marginTop: 5, lineHeight: 1 }}>
            <Money value={display} size={38} weight={500} />
          </div>
          <div style={{ marginTop: 7, display: "flex", alignItems: "baseline", gap: 7 }}>
            <Delta abs={nw.abs} pct={nw.pct} size={13} />
            <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)" }}>{nw.label.charAt(0).toUpperCase() + nw.label.slice(1)}</span>
          </div>

          {window.__YOSHI_WEB && window.WebNetWorthChart ? (
            <window.WebNetWorthChart range={range} ranges={NW_RANGES} nw={nw} nav={nav}
              setRange={(r) => { setRange(r); setMarker(null); setScrubVal(null); }}
              onScrub={(v) => setScrubVal(v)} />
          ) : (
          <>
          <div style={{ marginTop: -2 }}>
            {(() => {
              const chartColor = nw.data[nw.data.length - 1] >= nw.data[0] ? "var(--accent-pos)" : "var(--signal-neg)";
              return <Chart data={nw.data} height={70} accent={chartColor} fillColor={chartColor} markers={nw.markers} activeMarker={marker} onPickMarker={setMarker}
                scrub onScrub={(idx) => setScrubVal(idx == null ? null : nw.data[idx])} padY={4} range={range} />;
            })()}
          </div>

          {/* horizon toggle, understated text buttons, no heavy chrome */}
          <div style={{ display: "flex", gap: 3, marginTop: 2 }}>
            {NW_RANGES.map((r) => {
              const on = r === range;
              return (
                <button key={r} className="press" onClick={() => {setRange(r);setMarker(null);setScrubVal(null);}} style={{
                  flex: 1, padding: "3px 0", background: on ? "var(--bg-2)" : "transparent", border: "none", borderRadius: 7,
                  fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.caption1, fontWeight: 500, letterSpacing: "normal",
                  color: on ? "var(--ink)" : "var(--ink-3)", cursor: "pointer", fontVariantNumeric: "proportional-nums"
                }}>{r === "Today" ? "today" : r}</button>);

            })}
          </div>
          </>
          )}

          {/* marker tooltip, tap to open the automation in Stream */}
          {marker != null && nw.markers[marker] &&
          <div style={{ marginTop: 10 }}>
              <button className="press" onClick={() => nw.markers[marker].autoId && nav.automation(nw.markers[marker].autoId)} style={{
              width: "100%", textAlign: "left", padding: "9px 11px", background: "var(--bg-2)", border: "none", borderRadius: 10,
              display: "flex", alignItems: "center", gap: 8, cursor: "pointer"
            }}>
                <span style={{ fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.footnote, color: "var(--accent)" }}>{nw.markers[marker].time}</span>
                <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, fontWeight: 500 }}>{nw.markers[marker].name}</span>
                <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 5, fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)" }}>
                  {nw.markers[marker].detail}
                  <Icon name="back" size={14} color="var(--ink-3)" style={{ transform: "scaleX(-1)" }} />
                </span>
              </button>
            </div>
          }
        </section>

        {/* allocation row */}
        <Allocation nav={nav} />

        {/* quick actions — web has these in the top bar */}
        {!window.__YOSHI_WEB &&
        <section style={{ padding: "8px 18px 6px", display: "flex", justifyContent: "center", gap: 10 }}>
          <QuickAction icon="trade" label="Trade" onClick={() => nav.tab("trade")} />
          <QuickAction icon="swap" label="Transfer" onClick={() => nav.sheet({ type: "transfer" })} />
        </section>}

        {/* BAND 2 · pending, a scroll-snap carousel of proposals */}
        <section style={{ padding: "12px 0 6px" }}>
          <div style={{ display: "flex", alignItems: "baseline", marginBottom: 8, padding: "0 18px" }}>
            <SecTitle color="var(--accent)"><LiveDot /> &nbsp;Needs you</SecTitle>
          </div>
          {proposals.length === 0 ?
          <div style={{ padding: "4px 18px 0", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, fontWeight: 500, color: "var(--ink-2)", lineHeight: 1.45 }}>
            You're all caught up
          </div> :
          proposals.length === 1 ?
          <div style={{ padding: "4px 18px 0" }}><PendingCard p={proposals[0]} onReview={() => onApprove(proposals[0].id)} /></div> :

          <>
              <PendingWheel key={proposals.length} proposals={proposals} onReview={onApprove} onActive={setPage} />
              <div style={{ display: "flex", justifyContent: "center", gap: 6, marginTop: 6 }}>
                {proposals.map((_, i) =>
              <span key={i} style={{ width: i === Math.min(page, proposals.length - 1) ? 16 : 6, height: 6, borderRadius: 999, background: i === Math.min(page, proposals.length - 1) ? "var(--accent)" : "var(--rule-2)", transition: "width 200ms var(--ease), background 200ms" }} />
              )}
              </div>
            </>
          }
        </section>

        {/* BAND 3 · Stream — on web the stream lives in its own right pillar
              (StreamPanel, mounted by web.jsx); inline here on mobile only. */}
        {!window.__YOSHI_WEB && <StreamPanel nav={nav} completed={completed} onSeeStream={onSeeStream} />}
        <div style={{ height: 14 }} />
      </div>
    </div>);

};

const QuickAction = ({ icon, logo, label, onClick }) =>
<button className="press" onClick={onClick} style={{
  background: "var(--bg-card)", border: "1px solid var(--rule)", borderRadius: 10, padding: "9px 6px 8px",
  width: 92, flex: "none",
  display: "flex", flexDirection: "column", alignItems: "center", gap: 5, cursor: "pointer"
}}>
    {logo ? <Logo size={19} /> : <Icon name={icon} size={19} stroke={1.5} color="var(--ink)" />}
    <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, fontWeight: 500, letterSpacing: "normal" }}>{label}</span>
  </button>;


const Allocation = ({ nav }) => {
  const segs = [
  ["Cash", CASH_TOTAL, "var(--alloc-cash)", false, "cash"],
  ["Stocks", INVEST_TOTAL, "var(--alloc-invest)", false, "investments"],
  ["Crypto", CRYPTO_TOTAL, "var(--alloc-crypto)", false, "investments"]];

  const total = segs.reduce((s, x) => s + x[1], 0);
  return (
    <section style={{ padding: window.__YOSHI_WEB ? "4px 18px 9px" : "11px 18px 12px" }}>
      <div style={{ display: "flex", height: window.__YOSHI_WEB ? 26 : 6, gap: 2, marginBottom: window.__YOSHI_WEB ? 9 : 10, borderRadius: window.__YOSHI_WEB ? 7 : 0, overflow: "hidden" }}>
        {segs.map(([l, v, c]) => <div key={l} style={{ background: c, width: `${v / total * 100}%`, display: window.__YOSHI_WEB ? "flex" : "block", alignItems: "center", justifyContent: "center" }}>{window.__YOSHI_WEB && <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(10.5), fontWeight: 500, color: "var(--ld-black)", fontVariantNumeric: "proportional-nums", letterSpacing: "normal", whiteSpace: "nowrap" }}>{usd(v)}</span>}</div>)}
      </div>
      <div style={{ display: "flex", gap: 2 }}>
        {segs.map(([l, v, c, debt, sv]) =>
        <button key={l} className="press" onClick={() => nav.studio(sv)}
        style={{ background: "none", border: "none", cursor: "pointer", width: `${v / total * 100}%`, display: "flex", flexDirection: "column", alignItems: "center", padding: 0 }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 5 }}>
              <span style={{ width: 7, height: 7, background: c, flex: "none" }} />
              <span style={{ fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.footnote, color: "var(--ink-2)" }}>{(v / total * 100).toFixed(2)}%</span>
              <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, fontWeight: 500, color: "var(--ink-2)" }}>{l}</span>
            </div>
            {!window.__YOSHI_WEB && <span style={{ fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)", marginTop: 2 }}>{usd(v, 2)}</span>}
          </button>
        )}
      </div>
    </section>);

};

const PendingCard = ({ p, onReview, fill }) => {
  const web = window.__YOSHI_WEB;
  const amountLabel = p.amountLabel || (p.kind === "trade" ? "Trade amount" : "Transfer amount");
  const amountValue = p.amount || usd(Math.abs(p.net || 0), 2);
  return (
<div style={{ border: "1px solid var(--accent)", background: "var(--bg-card)", padding: web ? "15px 17px 16px" : "14px 15px 15px", borderRadius: 12, boxShadow: "0 0 0 4px color-mix(in srgb, var(--accent) 13%, transparent)", width: "100%", height: fill ? "100%" : undefined, aspectRatio: web ? "1 / 1" : "1.08 / 1", boxSizing: "border-box", display: "flex", flexDirection: "column" }}>
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <span style={{ width: 6, height: 6, borderRadius: 999, flex: "none", background: /^yoshi/i.test(p.agent) ? "var(--accent)" : "transparent", border: /^yoshi/i.test(p.agent) ? "none" : "1.5px solid var(--ink-3)" }} />
      <span style={{ fontFamily: "var(--f-display)", fontSize: web ? 10.5 : IOS_TYPE.footnote, fontWeight: 500, letterSpacing: "normal", color: "var(--ink-3)" }}>{p.agent}</span>
    </div>
    <div style={{ fontFamily: "var(--f-display)", fontSize: web ? 19 : IOS_TYPE.body, fontWeight: 500, letterSpacing: "normal", marginTop: web ? 9 : 7, lineHeight: 1.2 }}>{p.title}</div>
    <div className="proposal-card-body" style={{ fontFamily: "var(--f-display)", fontSize: web ? 14 : IOS_TYPE.subheadline, color: "var(--ink-2)", marginTop: web ? 7 : 5, lineHeight: web ? 1.5 : 1.35, display: "-webkit-box", WebkitBoxOrient: "vertical", WebkitLineClamp: 3, overflow: "hidden" }}>{p.why}</div>
    <div style={{ display: "grid", gap: 5, marginTop: "auto", paddingTop: web ? 11 : 8, borderTop: "1px dashed var(--rule)" }}>
      <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: web ? 10 : IOS_TYPE.footnote, fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)" }}>{amountLabel}</span>
        <span style={{ fontFamily: "var(--f-mono)", fontSize: web ? 15 : IOS_TYPE.subheadline, color: "var(--ink)", fontVariantNumeric: "proportional-nums" }}>{amountValue}</span>
      </div>
      {p.cashImpact && <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: web ? 10 : IOS_TYPE.footnote, fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)" }}>{p.cashImpactLabel || "Cash impact"}</span>
        <span style={{ fontFamily: "var(--f-mono)", fontSize: web ? 13 : IOS_TYPE.footnote, color: p.cashImpact.startsWith("+") ? "var(--accent-pos)" : p.cashImpact.startsWith("-") ? "var(--signal-neg)" : "var(--ink-2)", fontVariantNumeric: "proportional-nums" }}>{p.cashImpact}</span>
      </div>}
    </div>
    <Btn full onClick={onReview} style={{ marginTop: web ? 12 : 10 }}>Review <Icon name="arrow" size={15} color="var(--accent-ink)" /></Btn>
  </div>);
};


/* ---- pending wheel · a flat 2D carousel of proposals ----------------------
   Cards slide horizontally; drag to scroll, neighbours peek in slightly scaled
   and dimmed at the edges, the active card sits centred. Snaps on release. */
const WHEEL_W = 264;
/* A native horizontal scroll-snap carousel, scroll/swipe left or right to move
   between proposals; each card snaps to centre. Neighbours peek, scaled + dimmed
   by their distance from centre. Cards stretch to equal height so the CTA always fits. */
const PendingWheel = ({ proposals, onReview, onActive }) => {
  const N = proposals.length;
  const web = window.__YOSHI_WEB;
  const CARD_W = web ? 318 : WHEEL_W; // wider, squarer tiles on web
  const ref = useRef(null);
  const [sx, setSx] = useState(0);
  const STRIDE = CARD_W + 14;

  const onScroll = () => {
    const el = ref.current;if (!el) return;
    setSx(el.scrollLeft);
    onActive && onActive(Math.max(0, Math.min(N - 1, Math.round(el.scrollLeft / STRIDE))));
  };

  return (
    <div ref={ref} className="hcar" onScroll={onScroll}
    style={{ display: "flex", gap: 14, overflowX: "auto", scrollSnapType: "x mandatory", alignItems: "stretch", padding: web ? "8px 18px 8px 56px" : `10px calc(50% - ${CARD_W / 2}px) 8px`, WebkitOverflowScrolling: "touch" }}>
      {proposals.map((p, i) => {
        const off = i - sx / STRIDE;
        const ao = Math.min(1, Math.abs(off));
        return (
          <div key={p.id} style={{
            flex: "none", width: CARD_W, scrollSnapAlign: web ? "start" : "center",
            transform: `scale(${1 - ao * 0.07})`, opacity: 1 - ao * (web ? 0.62 : 0.42),
            transition: "transform 160ms linear, opacity 160ms linear"
          }}>
            <PendingCard p={p} onReview={() => onReview(p.id)} fill />
          </div>);

      })}
    </div>);

};

const NothingPending = ({ last }) =>
<div style={{ border: "1px solid var(--rule)", background: "var(--bg-2)", padding: "14px" }}>
    <Eyebrow>All clear · last move</Eyebrow>
    <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.body, fontWeight: 500, marginTop: 5 }}>{last.title}</div>
    <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, color: "var(--ink-2)", marginTop: 3 }}>{last.detail} · {last.when}</div>
  </div>;


/* ---- coming-up row (forward-looking schedule, no icon) ------------------- */
/* ---- leading category icon · helps scan the feed by type -----------------
   Agent/automation rows carry the Yoshi mark; everything else gets a schematic
   category glyph in a quiet hairline tile. */
const RowIcon = ({ icon, auto }) => {
  const yoshi = auto || icon === "bolt";
  const p = useTheme();
  return (
    <span style={{ width: 24, height: 24, flex: "none", display: "grid", placeItems: "center", color: "var(--ink-2)", opacity: yoshi && p === "graphite" ? 0.72 : 1 }}>
      {yoshi ? <Logo size={15} /> : <Icon name={icon || "receipt"} size={17} stroke={1.5} color="var(--ink-2)" />}
    </span>);

};

const ComingRow = ({ c, last, nav, onDetail }) => {
  const isAuto = !!c.autoId;
  const open = () => {
    // web Stream pillar: everything opens in-pillar (full detail)
    if (onDetail) { onDetail(isAuto ? { kind: "auto", id: c.autoId } : { kind: "txn", tx: c }); return; }
    isAuto ? nav.sheet({ type: "automation", id: c.autoId }) : nav.sheet({ type: "txn", tx: c });
  };
  return (
    <button className="press" onClick={open} style={{ width: "100%", textAlign: "left", background: "none", border: "none", display: "grid", gridTemplateColumns: "auto 1fr auto", gap: 11, alignItems: "center", padding: "12px 0", cursor: "pointer", borderBottom: last ? "none" : "1px solid var(--rule)" }}>
      <RowIcon icon={c.icon} auto={isAuto} />
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.body, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{c.title}</div>
        <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, color: "var(--ink-3)", marginTop: 3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{c.sub}</div>
      </div>
      <div style={{ textAlign: "right", whiteSpace: "nowrap" }}>
        <Money value={c.tone === "out" ? -c.amount : c.amount} size={IOS_TYPE.callout} sign={c.tone === "in"} color={c.tone === "in" ? "var(--accent-pos)" : "var(--ink)"} dim="var(--ink-3)" />
        <div style={{ fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)", marginTop: 3 }}>{c.when}</div>
      </div>
    </button>);

};

/* ---- recent-activity transaction row (flat ledger, no pill) --------------- */
const txnAutomationId = (t) => t.autoId || null;
const AutomationTxnButton = ({ tx, onClick }) =>
  <button className="press" onClick={onClick} aria-label={`View automation for ${tx.title}`} style={{ flex: "none", display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 999, border: "1px solid var(--accent)", background: "color-mix(in srgb, var(--accent) 14%, var(--bg-2))", color: "var(--accent)", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.caption2, fontWeight: 500, lineHeight: 1.35, whiteSpace: "nowrap", cursor: "pointer", boxShadow: "0 0 0 2px color-mix(in srgb, var(--accent) 10%, transparent)" }}>
    automation <Icon name="arrow" size={10} stroke={1.7} color="var(--accent)" />
  </button>;
const activityTimestamp = (t) => t.time ? `${t.when} · ${t.time}` : t.when;

const TxnRow = ({ t, last, nav, onDetail }) =>
<button className="press" onClick={() => onDetail ? onDetail({ kind: "txn", tx: t }) : nav.sheet({ type: "txn", tx: t })} style={{ width: "100%", textAlign: "left", background: "none", border: "none", display: "grid", gridTemplateColumns: "auto 1fr auto", gap: 11, alignItems: "center", padding: "12px 0", cursor: "pointer", borderBottom: last ? "none" : "1px solid var(--rule)" }}>
    <RowIcon icon={t.icon} />
    <div style={{ minWidth: 0 }}>
      <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.body, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{t.title}</div>
      <div style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, color: "var(--ink-3)", marginTop: 3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{t.detail}</div>
    </div>
    <div style={{ textAlign: "right", whiteSpace: "nowrap" }}>
      {t.net !== 0 ?
    <Money value={t.net} size={IOS_TYPE.callout} sign color={t.net > 0 ? "var(--accent-pos)" : "var(--ink)"} dim="var(--ink-3)" /> :
    <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)" }}>moved</span>}
      <div style={{ fontFamily: "var(--f-mono)", fontSize: IOS_TYPE.footnote, color: "var(--ink-3)", marginTop: 3 }}>{activityTimestamp(t)}</div>
    </div>
  </button>;

/* ---- transaction / scheduled-item detail · a receipt-style bottom sheet --- */
const TXN_CATEGORY = { receipt: "Spending", down: "Income", swap: "Transfer", trade: "Investment", bolt: "Automation" };
const CATEGORY_OPTIONS = ["Spending", "Groceries", "Dining", "Bills", "Travel", "Shopping", "Income", "Transfer", "Investment", "Health", "Entertainment", "Subscriptions"];
const isExternalLinkedTransaction = (tx) => tx.accountScope === "external";

const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

/* Past activity for this merchant / transaction type — a short statement of
   prior occurrences. Recurring charges repeat at the same amount; variable
   merchants vary a little (deterministic per merchant, so it stays stable). */
const txHistory = (tx, amount) => {
  const recurring = /subscription|autopay|membership|recurring/i.test(`${tx.detail || ""} ${tx.sub || ""}`);
  const baseM = (() => { const i = MONTHS.findIndex((m) => new RegExp(m, "i").test(tx.when || "")); return i >= 0 ? i : 4; })();
  const dayM = (tx.when || "").match(/\b(\d{1,2})\b/);
  const day = dayM ? dayM[1] : "8";
  const seed = [...(tx.title || "x")].reduce((a, c) => (a * 31 + c.charCodeAt(0)) >>> 0, 7);
  const mag = Math.abs(amount) || 0;
  const rows = [];
  for (let i = 1; i <= 3; i++) {
    const mi = ((baseM - i) % 12 + 12) % 12;
    const jitter = recurring ? 0 : (((seed >> (i * 4)) % 31) - 15) / 100; // ±15%
    rows.push({ when: `${MONTHS[mi]} ${day}`, amt: Math.round(mag * (1 + jitter) * 100) / 100 });
  }
  return { recurring, rows, mag };
};

const TxnHistory = ({ tx, amount }) => {
  const { recurring, rows, mag } = txHistory(tx, amount);
  if (!rows.length) return null;
  const neg = amount < 0;
  return (
    <div style={{ marginTop: 22 }}>
      <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 11 }}>
        <Eyebrow>Past activity</Eyebrow>
        <span style={{ marginLeft: "auto", fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)" }}>{recurring ? "Recurring · monthly" : "Recent"}</span>
      </div>
      <div style={{ border: "1px solid var(--rule)" }}>
        {rows.map((r, i) =>
        <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "11px 14px", borderBottom: i === rows.length - 1 ? "none" : "1px dashed var(--rule)" }}>
          <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13), color: "var(--ink-2)" }}>{r.when}</span>
          {mag > 0 ?
          <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(13), color: "var(--ink)", fontVariantNumeric: "proportional-nums" }}>{neg ? "−" : "+"}{usd(r.amt)}</span> :
          <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(11), color: "var(--ink-3)", letterSpacing: "normal", textTransform: "uppercase" }}>Ran</span>}
        </div>
        )}
      </div>
    </div>);

};

const TransactionCategoryPicker = ({ category, onSelect }) => {
  const options = CATEGORY_OPTIONS.includes(category) ? CATEGORY_OPTIONS : [category, ...CATEGORY_OPTIONS];
  return (
    <div data-category-picker-style="ledger-list" style={{ borderBottom: "1px dashed var(--rule)", background: "transparent" }}>
      <div role="listbox" aria-label="Transaction category">
        {options.map((option, i) => {
          const on = option === category;
          return (
            <button key={option} role="option" aria-selected={on} className="press" onClick={() => onSelect(option)} style={{
              width: "100%", minHeight: 38, display: "flex", alignItems: "center", justifyContent: "flex-end",
              padding: "9px 14px", background: "transparent", color: on ? "var(--accent)" : "var(--ink-2)",
              border: "none", borderBottom: i === options.length - 1 ? "none" : "1px dashed var(--rule)", cursor: "pointer",
              fontFamily: "var(--f-display)", fontSize: typeSize(13), fontWeight: 500, textAlign: "left"
            }}>
              <span data-category-choice-group="check-near-label" style={{ display: "inline-flex", alignItems: "center", justifyContent: "flex-end", gap: 7, minWidth: 0, maxWidth: "100%" }}>
                <span data-category-check-placement="near-label" style={{ width: 14, minWidth: 14, display: "grid", placeItems: "center", visibility: on ? "visible" : "hidden" }}><Icon name="check" size={13} stroke={1.8} color="var(--accent)" /></span>
                <span data-category-label-align="right" style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "right" }}>{option}</span>
              </span>
            </button>
          );
        })}
      </div>
    </div>
  );
};

const EditableCategoryRow = ({ category, open, onToggle, onSelect }) => (
  <>
    <button className="press" onClick={onToggle} aria-expanded={open} aria-label={`Change category from ${category}`} style={{
      width: "100%", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12,
      padding: "12px 14px", border: "none", borderBottom: open ? "1px solid var(--accent)" : "1px dashed var(--rule)", background: "transparent",
      color: "var(--ink)", cursor: "pointer", textAlign: "left"
    }}>
      <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(11), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)", flex: "none" }}>Category</span>
      <span style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 7, minWidth: 0 }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13.5), fontWeight: 500, color: open ? "var(--accent)" : "var(--ink)", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{category}</span>
        <Icon name={open ? "up" : "down"} size={15} stroke={1.7} color={open ? "var(--accent)" : "var(--ink-3)"} />
      </span>
    </button>
    {open && <TransactionCategoryPicker category={category} onSelect={onSelect} />}
  </>
);

const cleanTransactionDetail = (value) => String(value || "").replace(/\s+/g, " ").trim();
const splitTransactionDetail = (value) => cleanTransactionDetail(value).split(/\s+·\s+/).map(cleanTransactionDetail).filter(Boolean);
const transactionAccountLike = (value) => /(account|brokerage|cash|reserve|checking|savings|card|ira|yoshi|schwab|chase|coinbase|••|··)/i.test(cleanTransactionDetail(value));
const transactionMethodLike = (value) => /(ach|wire|zelle|order|transfer|deposit|autopay|auto-invest|instant|market|reinvest|round-ups|subscription|bill pay|direct deposit|apy|payroll|notional|received|card)/i.test(cleanTransactionDetail(value));
const splitTransactionMethodAccount = (value) => {
  const parts = splitTransactionDetail(value);
  if (parts.length < 2) return { method: cleanTransactionDetail(value), account: "" };
  const first = parts[0];
  const rest = parts.slice(1).join(" · ");
  if (transactionAccountLike(first) && !transactionAccountLike(rest) && transactionMethodLike(rest)) return { method: rest, account: first };
  return { method: first, account: rest };
};
const inferredTransactionAccount = (tx, category) => {
  const scope = tx.accountScope || tx.scope;
  if (scope === "card" || /card/i.test(tx.detail || "")) return cleanTransactionDetail(tx.detail || tx.acct || "Yoshi Card");
  if (scope === "yoshi") {
    if (category === "Trade" || category === "Investment") return "Yoshi Brokerage";
    if (category === "Reserve") return "Yoshi Reserve";
    return "Yoshi Cash";
  }
  if (scope === "external" && tx.acct) return cleanTransactionDetail(tx.acct);
  return "";
};
const transactionRouteFromText = (value) => {
  const text = cleanTransactionDetail(value);
  if (!text) return "";
  if (text.includes("→")) return text;
  const normalized = text.replace(/\s+to\s+/i, " → ");
  return normalized.includes("→") ? normalized : "";
};
const transactionRouteAccountName = (value) => {
  const name = cleanTransactionDetail(value);
  if (/^yoshi$/i.test(name) || /^cash$/i.test(name)) return "Yoshi Cash";
  return name;
};
const transactionRouteFromTitle = (title, account, amount) => {
  const source = transactionRouteAccountName(account);
  if (!source) return "";
  const moveMatch = cleanTransactionDetail(title).match(/^Move\s+\$[\d,.]+\s+to\s+(.+)$/i);
  const transferMatch = cleanTransactionDetail(title).match(/^Transfer to\s+(.+)$/i);
  const target = cleanTransactionDetail((moveMatch && moveMatch[1]) || (transferMatch && transferMatch[1]));
  if (!target) return "";
  return amount >= 0 ? `${target} → ${source}` : `${source} → ${target}`;
};
const transactionTransferAccountRoute = (tx, account, category) => {
  if (!category.startsWith("Transfer")) return account;
  const amount = tx.net != null ? tx.net : tx.tone === "out" ? -tx.amount : tx.amount;
  const explicitRoute = transactionRouteFromText(tx.accountRoute || tx.route || tx.detail || tx.acct || account);
  if (explicitRoute) return explicitRoute;
  const titleRoute = transactionRouteFromTitle(tx.title, account, amount);
  if (titleRoute) return titleRoute;
  if (category.startsWith("Transfer") && account && !account.includes("→") && tx.title) {
    const title = cleanTransactionDetail(tx.title).replace(/^Canceled ·\s*/, "");
    return amount >= 0 ? `${title} → ${account}` : `${account} → ${title}`;
  }
  return account;
};
const transactionDetailRows = (tx, category) => {
  let method = cleanTransactionDetail(tx.method || tx.rail || tx.type);
  let account = cleanTransactionDetail(tx.account || tx.accountName || "");
  const acctValue = cleanTransactionDetail(tx.acct);
  if (acctValue) {
    const split = splitTransactionMethodAccount(acctValue);
    if (split.account) {
      if (!method) method = split.method;
      account = account || split.account;
    } else if (!account) {
      account = acctValue;
    }
  }
  const detailValue = cleanTransactionDetail(tx.detail || tx.sub);
  if (detailValue) {
    const split = splitTransactionMethodAccount(detailValue);
    if (!method) method = split.method;
    if (!account) account = split.account || (transactionAccountLike(detailValue) && !transactionMethodLike(detailValue) ? detailValue : "");
  }
  account = account || inferredTransactionAccount(tx, category);
  account = transactionTransferAccountRoute(tx, account, category);
  if (method && account && method.toLowerCase() === account.toLowerCase()) account = "";
  return { method, account };
};

const TxnDetailSheet = ({ tx, onClose, nav, onAutomation, backLabel, onCancel }) => {
  const pending = !!tx.pending; // in motion — settles into the ledger later
  const upcoming = tx.net == null;
  // Pending items show when the instruction was submitted, not a speculative
  // settlement time. Completed transactions use their ledger occurrence.
  const actionDate = tx.occurredOn || tx.when;
  const amount = tx.net != null ? tx.net : tx.tone === "out" ? -tx.amount : tx.amount;
  const instantAvailable = Math.max(tx.instantAvailable || 0, 0);
  const pendingAmount = Math.max(tx.pendingAmount || 0, 0);
  const fundingAvailable = pending && instantAvailable > 0;
  const fundingSplit = pending && instantAvailable > 0 && pendingAmount > 0;
  const defaultCategory = directionalCategory(tx.category || TXN_CATEGORY[tx.icon] || (tx.cat ? tx.cat.charAt(0).toUpperCase() + tx.cat.slice(1) : "Activity"), amount);
  const [category, setCategory] = useState(defaultCategory);
  const [categoryPickerOpen, setCategoryPickerOpen] = useState(false);
  const categoryEditable = !upcoming && isExternalLinkedTransaction(tx);
  const automationId = txnAutomationId(tx);
  const { method, account } = transactionDetailRows(tx, category);
  const ask = () => {onClose();nav.ask(`Tell me about this ${upcoming ? "scheduled item" : "transaction"}: ${tx.title}.`, `Here's what I have on "${tx.title}" — ${upcoming ? "it's scheduled" : "it settled"} ${transactionStamp(tx)}, ${amount >= 0 ? "a credit" : "a debit"} of ${usd(Math.abs(amount))} via ${method || category}${account ? ` in ${account}` : ""}. Want me to find related activity, set an alert, or categorize it differently?`);};
  const openAutomation = () => {
    if (!automationId) return;
    if (onAutomation) { onAutomation(automationId); return; }
    onClose();
    nav && nav.automation && nav.automation(automationId);
  };
  const changeCategory = (next) => {
    setCategory(next);
    setCategoryPickerOpen(false);
  };
  return (
    <>
      {!window.__YOSHI_WEB && <div className="yo-focus-backdrop" onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 300 }} />}
      <div style={window.__YOSHI_WEB ? { position: "absolute", inset: 0, zIndex: 301, background: "var(--bg)", display: "flex", flexDirection: "column" } : { position: "absolute", left: 0, right: 0, bottom: 0, zIndex: 301, background: "var(--bg)", borderTop: "1px solid var(--accent)", maxHeight: "88%", display: "flex", flexDirection: "column" }} data-screen-label="Transaction detail">
        {!window.__YOSHI_WEB && <div style={{ display: "flex", justifyContent: "center", paddingTop: 8, flex: "none" }}><span style={{ width: 36, height: 4, background: "var(--rule-2)", borderRadius: 999 }} /></div>}
        <div className="scroll" style={{ padding: window.__YOSHI_WEB ? "18px 22px 26px" : "12px 20px 26px" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, flex: 1, minWidth: 0 }}>
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(17), fontWeight: 500, letterSpacing: "normal", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{tx.title}</span>
              {automationId && <AutomationTxnButton tx={tx} onClick={openAutomation} />}
            </div>
            {/* close: the app's standard circle X on web; plain glyph on mobile.
                No back breadcrumb — the X returns to the list it replaced. */}
            <button className="press" onClick={onClose} aria-label="Close" style={window.__YOSHI_WEB
              ? { flex: "none", background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 999, width: 30, height: 30, display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer", padding: 0 }
              : { background: "none", border: "none", display: "flex", color: "var(--ink-3)" }}><Icon name="close" size={window.__YOSHI_WEB ? 17 : 19} /></button>
          </div>
          <div style={{ marginTop: 14 }}>
            <Money value={amount} size={38} weight={500} sign={amount > 0} color={amount > 0 ? "var(--accent-pos)" : "var(--ink)"} dim="var(--ink-3)" />
          </div>
          <div style={{ marginTop: 7, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
            <Eyebrow style={{ color: pending || upcoming ? "var(--accent)" : "var(--ink-3)" }}>{pending ? (fundingAvailable ? "Deposit pending" : "Pending") : `${upcoming ? "Scheduled" : "Settled"} · ${transactionStamp(tx)}`}</Eyebrow>
            {fundingAvailable && !fundingSplit && (
              <div style={{ display: "flex", alignItems: "center", gap: 5, fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, color: "var(--accent-pos)", whiteSpace: "nowrap" }}>
                <Icon name="bolt" size={11.5} stroke={1.6} />Available instantly
              </div>
            )}
          </div>

          {fundingSplit && (
            <div style={{ marginTop: 10, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18 }}>
              <div>
                <div style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(13), fontWeight: 500, color: "var(--ink)", fontVariantNumeric: "proportional-nums" }}>{usd(instantAvailable)}</div>
                <div style={{ marginTop: 2, display: "flex", alignItems: "center", gap: 4, fontFamily: "var(--f-display)", fontSize: typeSize(9.5), color: "var(--accent-pos)" }}><Icon name="bolt" size={10.5} stroke={1.6} />Available instantly</div>
              </div>
              <div style={{ textAlign: "right" }}>
                <div style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(13), fontWeight: 500, color: "var(--ink)", fontVariantNumeric: "proportional-nums" }}>{usd(pendingAmount)}</div>
                <div style={{ marginTop: 2, fontFamily: "var(--f-display)", fontSize: typeSize(9.5), color: "var(--ink-3)" }}>Pending until settled</div>
              </div>
            </div>
          )}

          <div style={{ marginTop: fundingSplit ? 14 : 18, border: "1px solid var(--rule)" }}>
            <ReviewRow label="Status" value={pending ? "Pending" : upcoming ? "Scheduled" : "Settled"} accent={pending || upcoming ? "var(--accent)" : "var(--accent-pos)"} />
            {/* a one-time automation is explicit about what kicks it off */}
            {(pending || upcoming) && tx.trigger && <ReviewRow label="Trigger" value={tx.trigger} accent="var(--accent)" />}
            <ReviewRow label="Date" value={actionDate} />
            {!upcoming && <ReviewRow label="Time" value={tx.time} />}
            {categoryEditable ?
            <EditableCategoryRow category={category} open={categoryPickerOpen} onToggle={() => setCategoryPickerOpen((open) => !open)} onSelect={changeCategory} /> :
            <ReviewRow label="Category" value={category} />}
            {method && <ReviewRow label="Method" value={method} />}
            {account && <ReviewRow label="Account" value={account} />}
            <ReviewRow label="Amount" value={`${amount >= 0 ? "+" : "−"}${usd(Math.abs(amount))}`} last />
          </div>

          {/* one-time / in-flight items have no past activity — only settled
              transactions show a history */}
          {!pending && <TxnHistory tx={tx} amount={amount} />}

          {/* pending items note whether they can still be pulled back */}
          {(pending || upcoming) && (tx.cancelNote || tx.noCancelNote) && (
            <div style={{ marginTop: 12, display: "flex", gap: 8, alignItems: "flex-start", padding: "10px 12px", background: "var(--bg-2)" }}>
              <Icon name="info" size={14} color="var(--ink-3)" style={{ marginTop: 1, flex: "none" }} />
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)", lineHeight: 1.5 }}>{onCancel ? tx.cancelNote : tx.noCancelNote}</span>
            </div>
          )}
          {onCancel ? (
            <div style={{ marginTop: 14, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
              <Btn kind={tx.cancelLabel ? "ghost" : "danger"} onClick={onCancel}>{tx.cancelLabel || (tx.category === "Transfer" ? "Cancel transfer" : "Cancel order")}</Btn>
              {/* automation runs route to the rule — its detail carries the
                  thread, so Manage is the door to talking about it */}
              {automationId
                ? <Btn onClick={openAutomation}>Manage</Btn>
                : <Btn kind="ghost" onClick={ask}>Ask Yoshi</Btn>}
            </div>
          ) : (
            <div style={{ marginTop: 14 }}><Btn kind="ghost" full onClick={ask}>Ask Yoshi about this</Btn></div>
          )}
        </div>
      </div>
    </>);

};


/* ---- automation status pill + row ----------------------------------------- */
const StatusPill = ({ status }) => {
  const map = {
    active: ["●", "Active", "var(--accent)"],
    watching: ["○", "Watching", "var(--ink-3)"],
    paused: ["○", "Paused", "var(--ink-3)"]
  };
  const [dot, label, color] = map[status] || map.active;
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 3, padding: "1px 5px", borderRadius: 999, border: "1px solid var(--rule-2)", fontFamily: "var(--f-display)", fontSize: typeSize(7.5), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color, flex: "none" }}>
      <span style={{ fontSize: typeSize(5.5), lineHeight: 1 }}>{dot}</span>{label}
    </span>);

};

const AutomationRow = ({ a, onOpen }) =>
<button className="press" onClick={onOpen} style={{
  width: "100%", textAlign: "left", background: "var(--bg-card)", border: "1px solid var(--rule)", borderRadius: 10,
  display: "grid", gridTemplateColumns: "1fr auto auto", gap: 9, alignItems: "center", padding: "10px 11px 11px"
}}>
    <div style={{ minWidth: 0 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
        <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13), fontWeight: 500, letterSpacing: "normal", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.name}</span>
        <StatusPill status={a.status} />
      </div>
    </div>
    <div style={{ textAlign: "right" }}>
      <div style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(13), color: a.measure.startsWith("+") ? "var(--accent-pos)" : "var(--ink-2)", fontVariantNumeric: "proportional-nums" }}>{a.measure}</div>
    </div>
    <Icon name="back" size={15} color="var(--ink-3)" style={{ transform: "scaleX(-1)" }} />
  </button>;


/* ---- automation detail sheet ---------------------------------------------- */
/* ============================================================
   Automation chat — the in-sheet thread used on MOBILE (web routes to the
   Automations inbox instead). Pinned context attaches under the header, chat
   runs below, and a concrete change drafts a proposal you review + approve.
   ============================================================ */
const AUTO_CHIPS_M = ["Pause it", "Change the amount", "Raise the threshold", "Explain the risk"];
const formatAutomationAmount = (value) => "$" + value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });

const autoReply = (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?`;
};

const autoDraftChange = (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" };
  }
  // pull every amount from the note and prefer the one that isn't the current
  // amount, so "instead of $500, make it $250" resolves to $250 — not the first
  // number it sees
  const base = parseFloat(((a.summary.match(/\$([\d,]+(?:\.\d+)?)/) || [])[1] || "0").replace(/,/g, ""));
  const nums = (text.replace(/,/g, "").match(/\$?\s?\d+(?:\.\d+)?\s?k?/gi) || [])
    .map((tok) => { let n = parseFloat(tok.replace(/[^\d.]/g, "")); if (/k/i.test(tok)) n *= 1000; return n; })
    .filter((n) => n > 0);
  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 (nums.length && (hasContext || bareNumber)) {
    const notBase = nums.filter((n) => Math.abs(n - base) > 0.01);
    const val = notBase.length ? notBase[0] : nums[0];
    const newAmt = formatAutomationAmount(val);
    const old = a.summary.match(/\$[\d,]+(?:\.\d+)?/);
    const newSummary = old ? a.summary.replace(old[0], newAmt) : a.summary;
    return { kind: "Amount", from: old ? old[0] : "current", to: newAmt, newSummary, newStatus: a.status };
  }
  return null;
};

const AutoContextTile = ({ a, onOpen }) => (
  <button className="press" onClick={onOpen} aria-label={`View automation ${a.name}`} style={{ alignSelf: "flex-start", maxWidth: "88%", border: "1px solid var(--accent)", background: "var(--bg-card)", borderRadius: 12, padding: 0, color: "var(--ink)", textAlign: "left", font: "inherit", cursor: "pointer" }}>
    <div style={{ padding: "11px 13px 12px" }}>
      <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: typeSize(9.5), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)" }}>{a.status === "active" ? "Active" : a.status === "watching" ? "Watching" : "Paused"}</span>
      </div>
      <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(14.5), fontWeight: 500, marginTop: 6, letterSpacing: "normal" }}>{a.name}</div>
      <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12), color: "var(--ink-2)", marginTop: 4, lineHeight: 1.5 }}>{a.summary}</div>
      <div style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(10.5), color: "var(--ink-3)", marginTop: 6 }}>{a.next}</div>
    </div>
  </button>
);

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: typeSize(13.5), fontWeight: 500, 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: typeSize(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: 500 }}>{change.to}</span>
      </div>
      {done ?
      <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)", marginTop: 9 }}>Applied to your automation.</div> :
      <Btn full size="md" onClick={onReview} style={{ marginTop: 11 }}>Review <Icon name="arrow" size={16} color="var(--accent-ink)" /></Btn>}
    </div>
  </div>
);

const ABubble = ({ m }) => {
  const u = m.from === "user";
  return (
    <div className="yo-enter" style={{ display: "flex", justifyContent: u ? "flex-end" : "flex-start" }}>
      <div style={{ maxWidth: "82%", padding: "10px 13px", borderRadius: u ? "14px 14px 4px 14px" : "14px 14px 14px 4px", background: u ? "var(--accent)" : "var(--bg-card)", color: u ? "var(--accent-ink)" : "var(--ink)", border: u ? "none" : "1px solid var(--rule)", fontFamily: "var(--f-display)", fontSize: typeSize(14), lineHeight: 1.45 }}>{m.t}</div>
    </div>);
};
const ATyping = () =>
  <div style={{ display: "flex", justifyContent: "flex-start" }}>
    <div style={{ padding: "12px 14px", background: "var(--bg-card)", border: "1px solid var(--rule)", borderRadius: "14px 14px 14px 4px", display: "flex", gap: 4 }}>
      {[0, 1, 2].map((i) => <span key={i} className="yo-pulse" style={{ width: 6, height: 6, borderRadius: 999, background: "var(--ink-3)", animationDelay: `${i * 200}ms` }} />)}
    </div>
  </div>;

const AutomationThread = ({ a, msgs, typing, onSend, onBack, onReview }) => {
  const [draft, setDraft] = useState("");
  const scrollRef = useRef(null);
  const focusRef = useRef(null);
  const [focusVisible, setFocusVisible] = useState(true);
  const lastPropIdx = (() => { for (let i = msgs.length - 1; i >= 0; i--) if (msgs[i].kind === "proposal") return i; return -1; })();
  const isProp = lastPropIdx >= 0;
  const focusProp = isProp ? msgs[lastPropIdx] : null;
  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, typing]);
  useEffect(() => {
    const el = focusRef.current, root = scrollRef.current;
    if (!el || !root) { setFocusVisible(true); return; }
    const io = new IntersectionObserver((e) => setFocusVisible(e[0].isIntersecting), { root, threshold: 0.01 });
    io.observe(el); return () => io.disconnect();
  }, [lastPropIdx, msgs.length]);
  const scrollToFocus = () => {
    const root = scrollRef.current, el = focusRef.current; if (!root) return;
    if (!isProp || !el) { root.scrollTo({ top: 0, behavior: "smooth" }); return; }
    const top = root.scrollTop + el.getBoundingClientRect().top - root.getBoundingClientRect().top - 14;
    root.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
  };
  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: "4px 16px 10px", 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={22} stroke={1.7} />
        </button>
        {focusVisible ? (
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(15), fontWeight: 500, letterSpacing: "normal", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.name}</div>
            <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10.5), color: "var(--ink-3)", letterSpacing: "normal", textTransform: "uppercase", marginTop: 1 }}>Yoshi · automation thread</div>
          </div>
        ) : (
          <button className="press" onClick={scrollToFocus} aria-label="Show details" title="Show details"
            style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 9, height: 40, padding: "0 11px", borderRadius: 10, background: "var(--bg-card)", border: "1px solid var(--accent)", cursor: "pointer", textAlign: "left", animation: "count-up 200ms ease both" }}>
            <span style={{ width: 6, height: 6, borderRadius: 999, flex: "none", background: "var(--accent)" }} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(8.5), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--accent)" }}>{isProp ? (focusProp.done ? "Applied" : "Proposed change") : "Automation · pinned"}</div>
              <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13), fontWeight: 500, letterSpacing: "normal", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", marginTop: 1 }}>{isProp ? (focusProp.change.kind === "Pause" ? "Pause this automation" : "Change the " + focusProp.change.kind.toLowerCase()) : a.name}</div>
            </div>
            <span style={{ flex: "none", display: "grid", placeItems: "center", transform: isProp ? "rotate(90deg)" : "rotate(-90deg)" }}><Icon name="arrow" size={15} color="var(--ink-3)" /></span>
          </button>
        )}
      </div>

      <div className="scroll" ref={scrollRef} style={{ padding: "14px 16px 8px", display: "flex", flexDirection: "column", gap: 9 }}>
        <div ref={!isProp ? focusRef : null} style={{ display: "flex" }}>
          <AutoContextTile a={a} onOpen={onBack} />
        </div>
        {msgs.map((m, i) => m.kind === "proposal"
          ? <div key={i} ref={i === lastPropIdx ? focusRef : undefined} style={{ display: "flex" }}><AutoChangeTile change={m.change} done={m.done} onReview={() => onReview(i)} /></div>
          : <ABubble key={i} m={m} />)}
        {typing && <ATyping />}
      </div>

      <div style={{ flex: "none", borderTop: "1px solid var(--rule)" }}>
        <div style={{ display: "flex", gap: 7, overflowX: "auto", padding: "10px 16px 6px" }}>
          {AUTO_CHIPS_M.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: typeSize(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="Talk to Yoshi…" onAttach={(doc) => onSend(`Attached ${doc.name}`)} />
        </div>
      </div>
    </div>
  );
};

const AutoReviewModal = ({ change, name, onCancel, onApply }) => {
  const lbl = { fontFamily: "var(--f-display)", fontSize: typeSize(11), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)", flex: "none" };
  const val = { fontFamily: "var(--f-mono)", fontSize: typeSize(13), textAlign: "right", fontVariantNumeric: "proportional-nums" };
  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 60, display: "flex", alignItems: "center", justifyContent: "center", padding: 22 }} data-screen-label="Change review">
      <div onClick={onCancel} 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: 360, 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" }}>
          <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--accent)" }}>Review change</span>
          <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(18), fontWeight: 500, letterSpacing: "normal", marginTop: 8 }}>{change.kind === "Pause" ? "Pause this automation" : "Change the " + change.kind.toLowerCase()}</div>
          <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink-3)", marginTop: 4 }}>{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={{ ...val, 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={{ ...val, color: "var(--ink)", fontWeight: 500 }}>{change.to}</span>
          </div>
        </div>
        <div style={{ padding: "0 22px", fontFamily: "var(--f-display)", fontSize: typeSize(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: "16px 22px 20px", display: "flex", gap: 9 }}>
          <Btn kind="ghost" full onClick={onCancel}>Cancel</Btn>
          <Btn full onClick={onApply}>Confirm</Btn>
        </div>
      </div>
    </div>);
};

/* pause/cancel are consequence moments, not toasts — say exactly what stops,
   what survives (history), and what happens to a pending ask before acting. */
const ConsequenceModal = ({ kind, a, onKeep, onConfirm }) => {
  const pendingAsk = a.upNext && a.upNext.some((u) => u.briefId);
  const lines = kind === "pause"
    ? ["Nothing runs until you turn it back on.", "Past runs are unaffected and stay in Activity."]
    : ["This deletes the rule — it won't run again.", "Past runs and history stay in Activity."];
  if (pendingAsk) lines.push("The pending ask in Needs You is withdrawn.");
  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 60, display: "flex", alignItems: "center", justifyContent: "center", padding: 22 }} data-screen-label="Confirm change">
      <div onClick={onKeep} 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: 360, 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" }}>
          <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: kind === "cancel" ? "var(--signal-neg)" : "var(--accent)" }}>{kind === "cancel" ? "Cancel rule" : "Pause rule"}</span>
          <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(18), fontWeight: 500, letterSpacing: "normal", marginTop: 8 }}>{kind === "cancel" ? "Cancel" : "Pause"} “{a.name}”?</div>
        </div>
        <div style={{ margin: "14px 22px 0", border: "1px solid var(--rule)", borderRadius: 12, overflow: "hidden" }}>
          {lines.map((t, i) => (
            <div key={i} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "10px 13px", borderTop: i ? "1px dashed var(--rule)" : "none" }}>
              <span style={{ marginTop: 2, flex: "none" }}><Icon name={i === lines.length - 1 && pendingAsk ? "clock" : "check"} size={13} color="var(--ink-3)" stroke={1.7} /></span>
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink-2)", lineHeight: 1.5 }}>{t}</span>
            </div>
          ))}
        </div>
        <div style={{ padding: "16px 22px 20px", display: "flex", gap: 9 }}>
          <Btn kind="ghost" full onClick={onKeep}>Keep rule</Btn>
          <Btn kind={kind === "cancel" ? "danger" : "primary"} full onClick={onConfirm}>{kind === "cancel" ? "Cancel rule" : "Pause rule"}</Btn>
        </div>
      </div>
    </div>);
};

const AutomationSheet = ({ automation, onClose, onBack, nav, flash, embedded, onThread }) => {
  const automationRuleUpdate = (a, change) => {
    const replaceAmount = (value) => typeof value === "string" ? value.split(change.from).join(change.to) : value;
    const updateEvent = {
      d: "Today",
      k: "updated",
      t: "Rule updated",
      s: change.kind === "Pause"
        ? "Paused with Passkey confirmation. Previous runs stay below."
        : `Amount changed from ${change.from} to ${change.to}. Previous runs stay below.`,
      amt: "",
    };
    if (change.kind === "Pause") {
      return {
        summary: change.newSummary,
        status: change.newStatus,
        next: "Paused · nothing runs until you resume it",
        upNext: [{ k: "watching", t: "Paused after your approval", s: "Nothing runs until you turn it back on. Prior moves remain in the history below." }],
        runs: [updateEvent, ...(a.runs || [])],
      };
    }
    return {
      summary: change.newSummary,
      status: change.newStatus,
      next: a.next ? replaceAmount(a.next) : a.next,
      rule: (a.rule || []).map((row) => row.map(replaceAmount)),
      upNext: (a.upNext || []).map((u) => ({ ...u, t: replaceAmount(u.t), s: replaceAmount(u.s) })),
      runs: [updateEvent, ...(a.runs || [])],
    };
  };
  const [msg, setMsg] = useState("");
  // mobile chats in-sheet; web (onThread) routes to the Automations inbox.
  const [msgs, setMsgs] = useState([]);
  const [typing, setTyping] = useState(false);
  const [applied, setApplied] = useState(null);  // approved rule overrides for this automation
  const [review, setReview] = useState(null);     // index of the change msg under review
  const [ending, setEnding] = useState(null);     // "pause" | "cancel" → consequence confirm
  const [txn, setTxn] = useState(null);           // an executed run, opened as its Activity receipt
  const [threadOpen, setThreadOpen] = useState(false);
  const a = applied ? { ...automation, ...applied } : automation;
  const pushChat = (text) => {
    setThreadOpen(true);
    setMsgs((m) => [...m, { from: "user", t: text }]);
    setTyping(true);
    setTimeout(() => {
      setTyping(false);
      const change = autoDraftChange(a, text);
      setMsgs((m) => {
        const arr = [...m];
        if (change) {
          arr.push({ from: "agent", t: change.kind === "Pause" ? `I've drafted the pause for "${a.name}". Review it when you're ready.` : `Here's the revised rule for "${a.name}", ready for you to review.` });
          arr.push({ kind: "proposal", change });
        } else { arr.push({ from: "agent", t: autoReply(a, text) }); }
        return arr;
      });
    }, 1000);
  };
  const send = (text) => {
    const t = (typeof text === "string" ? text : msg).trim(); if (!t) return;
    setMsg("");
    if (onThread) { onThread(t); return; }
    pushChat(t);
  };
  const attachDoc = (doc) => {
    const t = `Attached ${doc.name}`;
    if (onThread) { onThread(t); return; }
    pushChat(t);
  };
  const applyChange = () => {
    if (review == null) return;
    const change = msgs[review] && msgs[review].change;
    if (!change) { setReview(null); return; }
    const update = automationRuleUpdate(a, change);
    setApplied((p) => ({ ...(p || {}), ...update }));
    setMsgs((m) => {
      const next = m.map((x, i) => (i === review ? { ...x, done: true } : x));
      next.push({ from: "agent", t: change.kind === "Pause" ? `Done. "${a.name}" is paused; tell me when to turn it back on.` : `Done. "${a.name}" now reads ${change.to}. It takes effect on the next run.` });
      return next;
    });
    flash((change.kind === "Pause" ? "Paused · " : "Updated · ") + a.name);
    setReview(null);
    setThreadOpen(false);
  };
  return (
    <div className="push-enter" style={{ position: "absolute", inset: 0, zIndex: 320, background: "var(--bg)", display: "flex", flexDirection: "column" }} data-screen-label="Automation">
      {!embedded && window.StatusBar && <window.StatusBar />}
      {!embedded && !threadOpen &&
      <NavBar title={a.name} onBack={onBack || onClose}
        right={<button className="press" onClick={onClose} style={{ background: "none", border: "none", color: "var(--ink-3)", display: "flex" }}><Icon name="close" size={20} /></button>} />}
      {threadOpen ? (
        <AutomationThread a={a} msgs={msgs} typing={typing} onSend={send} onBack={() => setThreadOpen(false)} onReview={(i) => setReview(i)} />
      ) : (
      <React.Fragment>
      <div className="scroll" style={{ padding: "10px 20px 0" }}>
        {/* "Needs you" vocabulary — agent dot + eyebrow, then title + read.
            The MODE pill states the rule's authority at a glance. */}
        <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
          <span style={{ width: 6, height: 6, borderRadius: 999, flex: "none", background: a.status === "active" ? "var(--accent)" : a.status === "attention" ? "var(--signal-neg)" : "transparent", border: a.status === "active" || a.status === "attention" ? "none" : "1.5px solid var(--ink-3)" }} />
          <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: a.status === "attention" ? "var(--signal-neg)" : "var(--ink-3)" }}>Yoshi agent · {a.status === "active" ? "Active" : a.status === "watching" ? "Watching" : a.status === "attention" ? "Needs attention" : "Paused"}</span>
          {a.mode && (
            <span style={{ marginLeft: "auto", flex: "none", display: "inline-flex", alignItems: "center", gap: 5, padding: "3px 9px", borderRadius: 999, border: "1px solid " + (a.mode === "auto" ? "var(--accent)" : "var(--rule-2)"), background: a.mode === "auto" ? "color-mix(in srgb, var(--accent) 10%, transparent)" : "var(--bg-2)" }}>
              <Icon name={a.mode === "auto" ? "bolt" : a.mode === "asks" ? "shield" : "bell"} size={10.5} color={a.mode === "auto" ? "var(--accent)" : "var(--ink-2)"} stroke={1.8} />
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(9.5), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: a.mode === "auto" ? "var(--accent)" : "var(--ink-2)" }}>{a.mode === "auto" ? "Automatic" : a.mode === "asks" ? "Asks each run" : "Alerts only"}</span>
            </span>
          )}
        </div>
        <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(22), fontWeight: 500, letterSpacing: "normal", marginTop: 10, lineHeight: 1.2 }}>{a.name}</div>
        <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(14), color: "var(--ink-2)", marginTop: 8, lineHeight: 1.55 }}>{a.summary}</div>
        <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink-3)", marginTop: 8 }}>{a.next}</div>
        <div style={{ height: 1, background: "var(--rule)", margin: "16px 0" }} />

        {/* needs attention — a broken data dependency goes loud: what Yoshi
            can't see, what's on hold, and the fix inline. */}
        {a.attention && (
          <div style={{ marginBottom: 20, border: "1px solid color-mix(in srgb, var(--signal-neg) 45%, transparent)", background: "color-mix(in srgb, var(--signal-neg) 7%, transparent)", borderRadius: 12, padding: "13px 14px" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
              <Icon name="alert" size={15} color="var(--signal-neg)" stroke={1.7} />
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(13), fontWeight: 500 }}>{a.attention.t}</span>
            </div>
            <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12), color: "var(--ink-2)", marginTop: 6, lineHeight: 1.5 }}>{a.attention.s}</div>
            <Btn size="sm" onClick={() => flash("Reconnect flow — out of prototype scope")} style={{ marginTop: 10 }}>{a.attention.cta}</Btn>
          </div>
        )}

        {/* the rule you approved — the setup brief's contract, pinned. Same
            slots (WHEN / IF / DO / LIMITS / MODE), same LegRow rendering. */}
        {a.rule && (
          <div style={{ marginBottom: 20 }}>
            <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)", marginBottom: 8 }}>The rule you approved</div>
            <div style={{ border: "1px solid var(--rule-2)", borderRadius: 12, overflow: "hidden" }}>
              {a.rule.map((leg, i) => <LegRow key={i} leg={leg} last={i === a.rule.length - 1} />)}
            </div>
          </div>
        )}

        {/* up next — armed & pending states: a brief waiting on you, funds
            arriving. What the rule is about to do, before it does it. Pending
            rows only show while their brief is still live — once approved or
            declined anywhere, the ask row drops instead of dangling. */}
        {(() => {
          const live = (typeof window !== "undefined" && window.__liveProposals) || (typeof PROPOSALS !== "undefined" ? PROPOSALS : []);
          const upNext = (a.upNext || []).filter((u) => !u.briefId || live.some((p) => p.id === u.briefId));
          return upNext.length > 0 && (
          <div style={{ marginBottom: 20 }}>
            <div style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)", marginBottom: 8 }}>Up next</div>
            <div style={{ border: "1px solid var(--rule)", borderRadius: 12, overflow: "hidden" }}>
              {upNext.map((u, i) => {
                const row = (
                  <div style={{ display: "flex", gap: 9, alignItems: "flex-start", padding: "11px 13px", borderTop: i ? "1px solid var(--rule)" : "none", width: "100%", boxSizing: "border-box", textAlign: "left" }}>
                    <span style={{ marginTop: 3.5, flex: "none" }}><Icon name={u.k === "pending" ? "clock" : "eye"} size={13.5} color={u.k === "pending" ? "var(--accent)" : "var(--ink-3)"} stroke={1.7} /></span>
                    <span style={{ minWidth: 0, flex: 1 }}>
                      <span style={{ display: "block", fontFamily: "var(--f-display)", fontSize: typeSize(12.5), fontWeight: 500, color: "var(--ink)", lineHeight: 1.4 }}>{u.t}</span>
                      {u.s && <span style={{ display: "block", fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)", marginTop: 3, lineHeight: 1.45 }}>{u.s}</span>}
                    </span>
                    {u.briefId && <Icon name="back" size={13} color="var(--ink-3)" style={{ transform: "scaleX(-1)", marginTop: 3, flex: "none" }} />}
                  </div>
                );
                return u.briefId
                  ? <button key={i} className="press" onClick={() => nav.sheet({ type: "briefs", brief: u.briefId })} style={{ display: "block", width: "100%", background: "none", border: "none", padding: 0, cursor: "pointer" }}>{row}</button>
                  : <div key={i}>{row}</div>;
              })}
            </div>
          </div>
          );
        })()}
        {/* run history \u2014 every firing, not just the wins. One outcome verb per
            run (Ran / Asked\u00b7approved / Asked\u00b7declined / Skipped / Failed), and
            skips carry the guardrail or condition that stopped them: the
            evidence that the rule's limits are real. */}
        {a.runs && a.runs.length > 0 ? (
        <div style={{ marginBottom: 20 }}>
          <div style={{ display: "flex", alignItems: "baseline", marginBottom: 8 }}>
            <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)" }}>Run history</span>
            {a.record && <span style={{ marginLeft: "auto", fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)" }}>{a.record}</span>}
          </div>
          <div style={{ border: "1px solid var(--rule)", borderRadius: 12, overflow: "hidden" }}>
            {a.runs.map((r, i) => {
              const oc = {
                ran:      { label: "Ran",              color: "var(--accent-pos)", dot: "var(--accent-pos)" },
                updated:  { label: "Rule updated",     color: "var(--accent)",     dot: "var(--accent)" },
                approved: { label: "Asked \u00b7 approved", color: "var(--accent-pos)", dot: "var(--accent-pos)" },
                declined: { label: "Asked \u00b7 declined", color: "var(--ink-3)",      dot: "transparent", ring: true },
                expired:  { label: "Asked \u00b7 expired",  color: "var(--ink-3)",      dot: "transparent", ring: true },
                skipped:  { label: "Skipped",          color: "var(--ink-3)",      dot: "transparent", ring: true },
                failed:   { label: "Failed",           color: "var(--signal-neg)", dot: "var(--signal-neg)" },
              }[r.k] || { label: "Ran", color: "var(--ink-2)", dot: "var(--ink-3)" };
              /* an EXECUTED run is a transaction \u2014 tapping opens the same
                 receipt Activity opens, built from the rule's runTx template */
              const executed = a.runTx && (r.k === "ran" || r.k === "approved") && r.amt;
              const openRun = () => {
                if (!executed) return;
                const n = parseFloat((r.amt || "").replace(/[^0-9.]/g, "")) || 0;
                setTxn({ id: "run-" + a.id + "-" + i, icon: a.runTx.icon, title: r.t, detail: a.runTx.detail, when: r.d, net: a.runTx.sign === 0 ? 0 : a.runTx.sign * n, category: a.runTx.category, type: a.runTx.type, rail: a.runTx.rail, accountScope: a.runTx.accountScope, autoId: a.id });
              };
              return (
                <div key={i} className={executed ? "press" : undefined} onClick={openRun} role={executed ? "button" : undefined}
                  style={{ display: "grid", gridTemplateColumns: executed ? "56px 1fr auto auto" : "56px 1fr auto", gap: 10, alignItems: "start", padding: "11px 13px", borderTop: i ? "1px solid var(--rule)" : "none", cursor: executed ? "pointer" : "default" }}>
                  <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(11), color: "var(--ink-3)", whiteSpace: "nowrap", marginTop: 1 }}>{r.d}</span>
                  <span style={{ minWidth: 0 }}>
                    <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
                      <span style={{ width: 6, height: 6, borderRadius: 999, flex: "none", background: oc.dot, border: oc.ring ? "1.5px solid var(--ink-3)" : "none", boxSizing: "border-box" }} />
                      <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(9.5), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: oc.color, whiteSpace: "nowrap" }}>{oc.label}</span>
                    </span>
                    <span style={{ display: "block", fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink)", marginTop: 3, lineHeight: 1.4 }}>{r.t}</span>
                    {r.s && <span style={{ display: "block", fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)", marginTop: 2.5, lineHeight: 1.45 }}>{r.s}</span>}
                  </span>
                  <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(12.5), fontWeight: 500, whiteSpace: "nowrap", marginTop: 1, color: (r.amt || "").trim().startsWith("\u2212") ? "var(--signal-neg)" : (r.amt || "").trim().startsWith("+") ? "var(--accent-pos)" : "var(--ink-2)" }}>{r.amt}</span>
                  {executed && <Icon name="back" size={13} color="var(--ink-3)" style={{ transform: "scaleX(-1)", marginTop: 2 }} />}
                </div>
              );
            })}
          </div>
        </div>
        ) : a.ledger && a.ledger.length > 0 &&
        <div style={{ marginBottom: 20 }}>
          <div style={{ display: "flex", alignItems: "baseline", marginBottom: 8 }}>
            <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(10), fontWeight: 500, letterSpacing: "normal", textTransform: "uppercase", color: "var(--ink-3)" }}>Run history</span>
            {a.record && <span style={{ marginLeft: "auto", fontFamily: "var(--f-display)", fontSize: typeSize(11), color: "var(--ink-3)" }}>{a.record}</span>}
          </div>
          <div style={{ border: "1px solid var(--rule)", borderRadius: 12, overflow: "hidden" }}>
            {a.ledger.map((row, i) =>
            <div key={i} style={{ display: "grid", gridTemplateColumns: "56px 1fr auto", gap: 10, alignItems: "center", padding: "11px 13px", borderTop: i ? "1px solid var(--rule)" : "none" }}>
              <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(11), color: "var(--ink-3)", whiteSpace: "nowrap" }}>{row[0]}</span>
              <span style={{ fontFamily: "var(--f-display)", fontSize: typeSize(12.5), color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row[1]}</span>
              <span style={{ fontFamily: "var(--f-mono)", fontSize: typeSize(12.5), fontWeight: 500, whiteSpace: "nowrap", color: row[2].trim().startsWith("\u2212") ? "var(--signal-neg)" : row[2].trim().startsWith("+") ? "var(--accent-pos)" : "var(--ink-2)" }}>{row[2]}</span>
            </div>
            )}
          </div>
        </div>}
      </div>

      <div style={{ flex: "none", borderTop: "1px solid var(--rule)" }}>
        <div style={{ padding: "12px 20px 20px", display: "flex", gap: 8 }}>
          <Btn kind="ghost" full onClick={() => setEnding("cancel")}>Cancel</Btn>
          {/* resuming has no consequences to confirm — it just turns back on */}
          {a.status === "paused"
            ? <Btn full onClick={() => { onClose(); flash("Resumed · " + a.name); }}>Resume</Btn>
            : <Btn full onClick={() => setEnding("pause")}>Pause</Btn>}
        </div>
        {/* free-form Yoshi chat — same bar used in briefs */}
        <div style={{ margin: "0 20px 0" }}>
          <YoshiComposer value={msg} onChange={setMsg} onSend={send} placeholder="Talk to Yoshi…" onAttach={attachDoc} />
        </div>
        <div style={{ height: 20 }} />
      </div>
      </React.Fragment>
      )}
      {review != null && msgs[review] && msgs[review].change && <AutoReviewModal change={msgs[review].change} name={a.name} onCancel={() => setReview(null)} onApply={applyChange} />}
      {ending && <ConsequenceModal kind={ending} a={a} onKeep={() => setEnding(null)}
        onConfirm={() => { const k = ending; setEnding(null); onClose(); flash((k === "cancel" ? "Cancelled · " : "Paused · ") + a.name); }} />}
      {/* an executed run, opened as the same transaction receipt Activity uses;
          closing returns to the rule */}
      {txn && <div style={{ position: "absolute", inset: 0, zIndex: 400 }}><TxnDetailSheet tx={txn} onClose={() => setTxn(null)} nav={nav} /></div>}
    </div>);

};

window.AutomationSheet = AutomationSheet;

/* ============================================================
   StreamPanel — the activity stream, lifted out of Home so the web shell can
   mount it as its own right pillar. Inline (mobile) it returns just the
   <section>; standalone (web) it wraps itself in a pillar with head + scroll.
   Same data, filters, search, look — unchanged from the original Home band.
   ============================================================ */
const StreamPanel = ({ nav, completed, onSeeStream, standalone, onRepin }) => {
  const [filter, setFilter] = useState("all");
  // web pillar: any stream item opens full-pillar, with everything the sheet has
  const [detail, setDetail] = useState(null); // { kind: "auto", id } | { kind: "txn", tx }
  const onItemDetail = standalone && window.__YOSHI_WEB ? (d) => setDetail(d) : null;
  const [searchOpen, setSearchOpen] = useState(false);
  const [query, setQuery] = useState("");

  // recent activity = freshly-approved agent moves prepended onto the ledger
  const txns = useMemo(() => {
    const fresh = (completed || []).filter((c) => String(c.id).startsWith("ex_")).map((c) => ({
      id: c.id, icon: "bolt", title: c.title, detail: `${c.detail} \u00b7 ${c.by}`, when: c.when, net: c.net
    }));
    return [...fresh, ...TRANSACTIONS];
  }, [completed]);

  // one feed: concrete transactions only. Rows triggered by standing rules are
  // tagged so the Automations filter still surfaces their corresponding money
  // movement without showing automation cards in Recent activity.
  const feed = useMemo(() => {
    const txCat = { receipt: "spending", down: "income", swap: "transfers", trade: "investments", bolt: "automations" };
    return txns.map((t) => ({ kind: "txn", t, id: "f_" + t.id, cat: t.cat || txCat[t.icon] || "spending" }));
  }, [txns]);

  const shownFeed = useMemo(() => {
    const q = query.trim().toLowerCase();
    return feed.filter((f) => {
      if (filter !== "all" && f.cat !== filter) return false;
      if (!q) return true;
      const hay = f.kind === "auto" ? `${f.a.name} ${f.a.measure}` : `${f.t.title} ${f.t.detail}`;
      return hay.toLowerCase().includes(q);
    });
  }, [feed, filter, query]);

  const shownComing = useMemo(() => {
    const q = query.trim().toLowerCase();
    return COMING_UP.filter((c) => {
      if (filter !== "all" && c.cat !== filter) return false;
      if (!q) return true;
      return `${c.title} ${c.sub}`.toLowerCase().includes(q);
    });
  }, [filter, query]);

  const body = (
    <section style={{ padding: "8px 0 0", minHeight: "100%" }}>
      {/* sticky header: Stream title · filter pills · search */}
      <div style={{ position: "sticky", top: 0, zIndex: 6, background: "var(--bg)", paddingBottom: 10 }}>
        {!standalone &&
        <div style={{ padding: "2px 18px 0", display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ fontFamily: "var(--f-display)", fontSize: IOS_TYPE.headline, fontWeight: onSeeStream ? 500 : 500, letterSpacing: "normal", color: onSeeStream ? "var(--ink-2)" : "var(--ink)" }}>{onSeeStream ? "Recent activity" : "Stream"}</span>
          {onSeeStream && <button className="press" onClick={onSeeStream} aria-label="Open Stream" style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 4, background: "none", border: "none", cursor: "pointer", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, fontWeight: 500, color: "var(--ink-3)" }}><Icon name="back" size={14} color="var(--ink-3)" style={{ transform: "scaleX(-1)" }} /></button>}
          {onRepin && <button className="press" onClick={onRepin} aria-label="Pin Stream to the side" title="Pin to the side" style={{ marginLeft: "auto", flex: "none", width: 28, height: 28, borderRadius: 999, background: "none", border: "none", display: "grid", placeItems: "center", color: "var(--ink-2)", cursor: "pointer", padding: 0 }}><Icon name="external" size={14} /></button>}
        </div>}
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: standalone ? 4 : 9 }}>
          <div className="hcar" style={{ display: "flex", gap: 7, overflowX: "auto", padding: "0 0 0 18px", flex: 1, minWidth: 0 }}>
            {ACTIVITY_FILTERS.map(([k, l]) => {
              const on = filter === k;
              return <button key={k} className="press" onClick={() => setFilter(k)} style={{ flex: "none", padding: "6px 12px", borderRadius: 999, background: on ? "color-mix(in srgb, var(--ink) 15%, var(--bg-2))" : "color-mix(in srgb, var(--ink) 8%, var(--bg-2))", color: on ? "var(--ink)" : "var(--ink-2)", border: "none", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.footnote, fontWeight: 500, whiteSpace: "nowrap", cursor: "pointer" }}>{l}</button>;
            })}
          </div>
          <button className="press" onClick={() => {setSearchOpen((o) => {if (o) setQuery("");return !o;});}}
          aria-label="Search activity"
          style={{ flex: "none", marginRight: 18, background: "none", border: "none", padding: 2, display: "flex", color: searchOpen ? "var(--ink)" : "var(--ink-3)", cursor: "pointer" }}>
            <Icon name={searchOpen ? "close" : "search"} size={18} stroke={1.6} />
          </button>
        </div>
        {searchOpen &&
        <div style={{ display: "flex", alignItems: "center", gap: 9, padding: "9px 12px", margin: "10px 18px 0", background: "var(--bg-2)", border: "1px solid var(--rule-2)", borderRadius: 10 }}>
            <Icon name="search" size={15} color="var(--ink-3)" stroke={1.5} />
            <input autoFocus value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search activity"
          style={{ flex: 1, border: "none", background: "transparent", outline: "none", color: "var(--ink)", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.body }} />
            {query && <button className="press" onClick={() => setQuery("")} style={{ background: "none", border: "none", display: "flex", color: "var(--ink-3)", padding: 0 }}><Icon name="close" size={14} /></button>}
          </div>
        }
      </div>

      {/* COMING UP */}
      {shownComing.length > 0 &&
      <div style={{ padding: "16px 18px 4px" }}>
          <SecTitle>Coming up</SecTitle>
          <div style={{ marginTop: 8 }}>
            {shownComing.map((c, i) => <ComingRow key={i} c={c} last={i === shownComing.length - 1} nav={nav} onDetail={onItemDetail} />)}
          </div>
        </div>
      }

      {/* RECENT ACTIVITY */}
      <div style={{ padding: "18px 18px 4px" }}>
        <SecTitle>Recent activity</SecTitle>
        <div style={{ marginTop: 8 }}>
          {shownFeed.length ? shownFeed.map((item, i) =>
          <TxnRow key={item.id} t={item.t} last={i === shownFeed.length - 1} nav={nav} onDetail={onItemDetail} />) :
          <div style={{ padding: "26px 0", textAlign: "center", fontFamily: "var(--f-display)", fontSize: IOS_TYPE.subheadline, color: "var(--ink-3)" }}>{query.trim() ? `No activity matches "${query}".` : "Nothing in this filter yet."}</div>}
        </div>
      </div>
    </section>
  );

  if (!standalone) return body;
  const openAuto = detail && detail.kind === "auto" ? AUTOMATIONS.find((a) => a.id === detail.id) : null;
  const openTxn = detail && detail.kind === "txn" ? detail.tx : null;
  return (
    <aside className="stream-col">
      <div className="pillar-head">
        {detail ?
        <button className="press" onClick={() => setDetail(null)} style={{ display: "inline-flex", alignItems: "center", gap: 8, background: "none", border: "none", padding: 0, cursor: "pointer", color: "var(--ink)" }}>
          <Icon name="back" size={18} stroke={1.7} />
          <span className="pillar-title">Stream</span>
        </button> :
        <span className="pillar-title">Stream</span>}
      </div>
      {openAuto ? (
        <div style={{ flex: 1, minHeight: 0, position: "relative", display: "flex", flexDirection: "column" }} className="push-enter">
          <window.AutomationSheet key={openAuto.id} embedded automation={openAuto} onClose={() => setDetail(null)} nav={nav} flash={() => {}}
            onThread={nav.automationChat ? (t) => nav.automationChat(openAuto.id, t) : undefined} />
        </div>
      ) : openTxn ? (
        <div style={{ flex: 1, minHeight: 0, position: "relative", display: "flex", flexDirection: "column" }} className="push-enter">
          <TxnDetailSheet key={openTxn.id || openTxn.title} tx={openTxn} onClose={() => setDetail(null)} nav={nav} onAutomation={(id) => setDetail({ kind: "auto", id })} />
        </div>
      ) : (
        <div className="scroll" style={{ paddingBottom: 20 }}>{body}</div>
      )}
    </aside>
  );
};
window.StreamPanel = StreamPanel;

window.Home = Home;
