// SRI App — root. The full arc, one continuous surface:
//   J1 Onboard (7a–7h) → J2 Reveal (the canvas wakes composed) → J3 Habit (brief,
//   waiting-on-you, universal search) → J4 Expansion (share approval, composed report),
//   plus J0 Pitch mode (the five beats, performed with the same typed intents).
// Geometry is the committed v2 decision: spine · context · THREAD · feature surface.
// Laws enforced here: state renders once (blocks freeze when superseded) · every action
// leaves a receipt · receipts are restorable (time travel) · long work is one job card ·
// prose is judgment, never data.

const SV = new Proxy({}, { get: (_, k) => (window.SRIData || {})[k] });
const SV_MOTION = { crisp: 160, standard: 260, glide: 400 };
const SV_DEFAULTS = /*EDITMODE-BEGIN*/{
  "motion": "standard",
  "featurePanel": "pinned",
  "day": "day 0"
}/*EDITMODE-END*/;

function svNow() { return new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); }
let SV_SEQ = 1;
const svid = () => "sv" + SV_SEQ++;

// ═══ Desk — the working surface (J2 onward) ═══════════════════════════════
function Desk({ t, setTweak, onRestartOnboarding }) {
  const day30 = t.day === "day 30";
  const [role, setRole] = React.useState("priya");
  const [feature, setFeature] = React.useState("home");
  const [filter, setFilter] = React.useState("all");
  const [selection, setSelection] = React.useState([]);
  // The transcript is a VIEW — the agent's context lives in the record (FS + memory).
  // Prior sessions wait as archive chunks; pulling up past the top loads them back in.
  const [archives, setArchives] = React.useState(() =>
    (SV.threadHistory[day30 ? "day30" : "day0"] || []).map((c) => ({
      id: svid(), label: c.label,
      parts: c.parts.map((p) => Object.assign({ id: svid(), hist: true }, p)),
    })));
  const [parts, setParts] = React.useState([]);
  const [pull, setPull] = React.useState(0);
  const [clearing, setClearing] = React.useState(false);
  const [scrolledUp, setScrolledUp] = React.useState(false);
  const [job, setJob] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [composerVal, setComposerVal] = React.useState("");
  const [panel, setPanel] = React.useState({ view: "table" });
  const [panelOpen, setPanelOpen] = React.useState(false);
  const [panelCollapsed, setPanelCollapsed] = React.useState(false);
  const [past, setPast] = React.useState(null);            // {view, filter, time, receiptId}
  const [shortlistExists, setShortlistExists] = React.useState(day30);
  const [shortlistShared, setShortlistShared] = React.useState(day30);
  const [reportBuilt, setReportBuilt] = React.useState(0);
  const [lens, setLens] = React.useState("all");
  const [revealBuilt, setRevealBuilt] = React.useState(0);
  const [revealActed, setRevealActed] = React.useState(null);
  const [pitchBeat, setPitchBeat] = React.useState(null);
  const [panelW, setPanelW] = React.useState(() => {
    try { const v = parseInt(localStorage.getItem("sriPanelW"), 10); return v >= 340 ? v : null; } catch (e) { return null; }
  });
  const [panelDrag, setPanelDrag] = React.useState(false);
  const [panelFocus, setPanelFocus] = React.useState(false);
  const [ctxCollapsed, setCtxCollapsed] = React.useState(false);
  const [leftView, setLeftView] = React.useState(null); // null = Home thread; else {view, filter}
  const [splitOpen, setSplitOpen] = React.useState(false);
  const SPLIT_PAGES = [
    { v: "home", label: "Home", icon: "home" },
    { v: "table", label: "All businesses", icon: "building" },
    { v: "preintake", label: "Sourcing", icon: "clipboard" },
    { v: "shared", label: "Lists", icon: "share" },
    { v: "opps", label: "Opportunity workspace", icon: "briefcase" },
    { v: "report", label: "Reports", icon: "chart" },
  ];
  const onPanelResizeDown = (e) => {
    e.preventDefault();
    const el = e.currentTarget;
    el.setPointerCapture(e.pointerId);
    setPanelDrag(true);
    const move = (ev) => {
      const w = Math.min(Math.max(Math.round(window.innerWidth - ev.clientX), 340), Math.round(window.innerWidth * 0.62));
      setPanelW(w);
    };
    const up = () => {
      el.removeEventListener("pointermove", move);
      el.removeEventListener("pointerup", up);
      setPanelDrag(false);
      setPanelW((w) => { try { if (w) localStorage.setItem("sriPanelW", String(w)); } catch (e2) {} return w; });
    };
    el.addEventListener("pointermove", move);
    el.addEventListener("pointerup", up);
  };
  const resetPanelW = () => { setPanelW(null); try { localStorage.removeItem("sriPanelW"); } catch (e) {} };
  const [spineWide, setSpineWide] = React.useState(() => {
    try { const v = localStorage.getItem("sriSpineWide"); return v == null ? true : v === "1"; } catch (e) { return true; }
  });
  const toggleSpine = () => setSpineWide((w) => {
    const n = !w; try { localStorage.setItem("sriSpineWide", n ? "1" : "0"); } catch (e) {} return n;
  });
  const timers = React.useRef([]);
  const threadRef = React.useRef(null);
  const wakeDone = React.useRef(false);
  const historyRef = React.useRef([]);                     // model turns for continuity
  const wokeRef = React.useRef(false);

  // ── workstreams — the container of work (threads never come back) ──
  // One composer, bound to the focused matter. Parked matters keep working; their
  // updates go to THEIR card and their own transcript — never interleaved here.
  // Law A: every running agent has a card. Law B: you watch behavior through receipts.
  const MAIN_WS = "main";
  const [focusId, setFocusId] = React.useState(MAIN_WS);
  const focusRef = React.useRef(MAIN_WS);
  const storedStreams = React.useRef({});                  // parked matters' frames + transcripts
  const [wsMeta, setWsMeta] = React.useState(() => [
    { id: MAIN_WS, label: day30 ? "Re-engage Q3 caterers" : "Today’s reveal", state: "focused", tick: null },
  ]);
  const setWs = (id, up) => setWsMeta((ms) => ms.map((w) => (w.id === id ? Object.assign({}, w, up) : w)));

  const later = (ms, fn) => { const id = setTimeout(fn, ms); timers.current.push(id); };
  React.useEffect(() => () => timers.current.forEach(clearTimeout), []);
  // scroll law: while today is still composing, the fresh anchor stays pinned at the top
  // (history hides above it); after that, follow the bottom only if the user is already there —
  // reading the record is never interrupted.
  React.useEffect(() => {
    const el = threadRef.current;
    if (!el) return;
    if (!wakeDone.current) {
      const anchor = el.querySelector('[data-fresh="1"]');
      if (anchor) { el.scrollTop = Math.max(0, anchor.offsetTop - 10); return; }
    }
    const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
    if (dist < 360) el.scrollTop = el.scrollHeight;
  }, [parts.length, job && job.state, busy, revealBuilt]);
  const onThreadScroll = (e) => {
    const el = e.currentTarget;
    const up = el.scrollHeight - el.scrollTop - el.clientHeight > 320;
    setScrolledUp((prev) => (prev === up ? prev : up));
  };

  // ── pull-to-load — earlier sessions return with intent, not by accident ──
  const PULL_MAX = 130;
  const pullRef = React.useRef(0);
  const pullDecay = React.useRef(null);
  const prependAnchor = React.useRef(null);
  const loadEarlier = () => {
    pullRef.current = 0; setPull(0);
    clearTimeout(pullDecay.current);
    if (!archives.length) return;
    const chunk = archives[archives.length - 1];
    const el = threadRef.current;
    prependAnchor.current = el ? el.scrollHeight - el.scrollTop : null;
    setArchives(archives.slice(0, -1));
    // tag the chunk so it renders as one group that eases in from above
    setParts((ps) => chunk.parts.map((p) => Object.assign({}, p, { chunk: chunk.id })).concat(ps));
  };
  React.useLayoutEffect(() => {
    if (prependAnchor.current == null) return;
    const el = threadRef.current;
    if (el) {
      // keep the reader anchored, then gently reveal the seam — the loaded chunk
      // eases in above while the viewport drifts up just enough to show it arrived
      const target = Math.max(0, el.scrollHeight - prependAnchor.current);
      el.scrollTop = target;
      if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
        requestAnimationFrame(() => el.scrollTo({ top: Math.max(0, target - 150), behavior: "smooth" }));
      }
    }
    prependAnchor.current = null;
  }, [parts]);
  const onThreadWheel = (e) => {
    const el = threadRef.current;
    if (!el || el.scrollTop > 2 || !archives.length || e.deltaY >= 0) return;
    pullRef.current = Math.min(PULL_MAX, pullRef.current + -e.deltaY * 0.55);
    setPull(pullRef.current);
    clearTimeout(pullDecay.current);
    if (pullRef.current >= PULL_MAX) { loadEarlier(); return; }
    pullDecay.current = setTimeout(() => { pullRef.current = 0; setPull(0); }, 550);
  };
  React.useEffect(() => () => clearTimeout(pullDecay.current), []);

  // ── fresh session — clears the VIEW, never the memory ──
  // Two-phase: the transcript settles out (230ms), then the new session composes in.
  const startFreshSession = () => {
    const moments = parts.filter((p) => p.kind !== "fresh").length;
    if (!moments || clearing) return;
    setClearing(true);
    later(240, () => {
      setClearing(false);
      const frozenParts = parts.map((p) => (p.kind === "block" && !p.frozen
        ? Object.assign({}, p, { frozen: true, time: svNow() }) : p));
      setArchives((as) => as.concat([{ id: svid(), label: "Earlier today · until " + svNow(), parts: frozenParts }]));
      setParts([]);
      wakeDone.current = false;
      push({ kind: "fresh", label: "Fresh session · " + svNow(), sub: "the record continues above — pull up to load" });
      receipt("you", "Started a fresh session", moments + " moments set aside",
        { moved: "nothing forgotten — the agent works from the record, not the transcript" });
      later(450, () => pushBlock("brief"));
      later(1200, () => push({
        kind: "prose",
        text: "Clean slate, same memory. Today's brief is above — ask, or work the rows.",
        suggestions: day30 ? ["See the nine", "Propose the sweep"] : ["Build a re-engagement shortlist", "Who went quiet since 2024?"],
        onSuggestDeterministic: true,
      }));
      later(2000, () => { wakeDone.current = true; });
    });
  };

  const showPanel = (t.featurePanel === "pinned" ? feature !== "home" || panelOpen : panelOpen) && !panelCollapsed;
  const panelAvailable = (t.featurePanel === "pinned" ? feature !== "home" || panelOpen : panelOpen) && panelCollapsed;
  const approvalPending = parts.some((p) => p.kind === "approval" && !p.resolved);

  // ── part helpers ──
  const push = (p) => setParts((ps) => ps.concat([Object.assign({ id: svid() }, p)]));
  const patch = (id, up) => setParts((ps) => ps.map((p) => (p.id === id ? Object.assign({}, p, up) : p)));
  const receipt = (actor, verb, scope, extra) =>
    push(Object.assign({ kind: "receipt", actor, verb, scope, time: svNow() }, extra || {}));
  const pushBlock = (btype, extra) => {
    setParts((ps) => {
      const tidied = ps.map((p) => (p.kind === "block" && !p.frozen && p.btype !== "reveal"
        ? Object.assign({}, p, { frozen: true, time: svNow() }) : p));
      return tidied.concat([Object.assign({ id: svid(), kind: "block", btype, frozen: false }, extra || {})]);
    });
  };
  const reopenBlock = (part) => pushBlock(part.btype, { label: part.label, filterKey: part.filterKey });

  const filteredRows = (f) => {
    if (f === "catering") return SV.businesses.filter((r) => r.category === "catering");
    if (f === "dormant") return SV.businesses.filter((r) => r.dormantSince);
    if (f === "interest") return SV.businesses.filter((r) => r.bucket === "interest");
    return SV.businesses;
  };

  // panel changes leave restorable snapshots on their receipts (law 3: time travel)
  const snapshotOf = (view, f) => ({ view, filter: f || filter });
  const openPanel = (view, opts) => {
    setPast(null);
    setPanel({ view, filter: opts && opts.filter });
    setPanelOpen(true);
    setPanelCollapsed(false);
    if (view === "table" || view === "shortlist" || view === "entity") setFeature("businesses");
    if (view === "report") { setFeature("reports"); startReport(); }
    if (view === "opps") setFeature("opportunities");
    if (view === "shared") setFeature("shared");
  };
  const openEntity = (id) => {
    if (id !== "meridian" && id !== "skyline") return;
    openPanel("entity"); // law 2a: a human open is a read — no receipt; agent opens earn theirs via tools
  };
  const restoreReceipt = (r) => {
    if (!r.snapshot) return;
    setPast(Object.assign({}, r.snapshot, { time: r.time, receiptId: r.id }));
    setPanelOpen(true);
    setPanelCollapsed(false);
  };

  // ── the desk wakes composed — never an empty chat box ──
  // Day 0 (J2): the reveal assembles from the first build.
  // Day 30 (J3): the brief is home — the habit loop, where mornings happen.
  React.useEffect(() => {
    if (wokeRef.current) return;
    wokeRef.current = true;
    if (day30) {
      push({ kind: "fresh", label: "Today · Friday, Jul 10", sub: "your record continues above — pull up to load" });
      receipt("agent", "Nightly sweep finished — today carries 3 items", "3 items",
        { moved: "2 waiting on you: a share decision and 9 sweep drafts · delivered to email and Slack at 7:02" });
      later(500, () => pushBlock("brief"));
      later(1300, () => push({
        kind: "prose",
        text: "Morning. Two of today's three want a decision before 10 — the rest keeps. Ask, or work the rows.",
        suggestions: ["See the nine", "Propose the sweep", "Separately: prep the Skyline AV renewal"], onSuggestDeterministic: true,
      }));
      later(2200, () => { wakeDone.current = true; });
      return;
    }
    push({ kind: "fresh", label: "Today — your first session", sub: "setup is recorded above — pull up to load" });
    receipt("agent", "First build finished — 2,941 facts staged", "nothing committed",
      { moved: "your reveal is below · the set-aside 37 wait with their reasons" });
    later(400, () => pushBlock("reveal"));
    later(1100, () => setRevealBuilt(1));
    later(2100, () => setRevealBuilt(2));
    later(3000, () => setRevealBuilt(3));
    later(3600, () => push({
      kind: "prose",
      text: "This is what your own files already knew. Every claim above carries its receipt — origin and last touch, from your records, resolved. Act on it in one gesture, or ask me anything.",
      suggestions: ["Build a re-engagement shortlist", "Who went quiet since 2024?"], onSuggestDeterministic: true,
    }));
    later(4200, () => { wakeDone.current = true; });
  }, []);

  // ── report assembly (rung 2 — watches itself build) ──
  const startReport = () => {
    if (reportBuilt > 0) return;
    let b = 0;
    const step = () => {
      b += 1; setReportBuilt(b);
      if (b < 4) later(650, step);
      else later(200, () => receipt("agent", "Assembled the June report", "4 sections", { moved: "export ready · leadership lens saved" }));
    };
    later(500, step);
  };

  // ── the share flow (J4) — governance as UI, deterministic ──
  const scriptShare = (edited) => {
    later(500, () => {
      setJob({ title: "Share Q3 caterers to Legacy Sports", state: "needs-you", total: 12, doneCount: 0,
        pauseNote: "crossing an org boundary — waiting on your approval" });
      push({ kind: "approval", resolved: null, req: {
        title: "a share is about to cross orgs",
        fromLevel: "org", fromLabel: "LA28 · org",
        toLevel: "shared", toLabel: "Legacy Sports workspace",
        body: edited
          ? "Share the Q3 caterers shortlist — 12 businesses, LA28 fields only, contact emails removed. No re-export."
          : "Share the Q3 caterers shortlist — 12 businesses, LA28 fields only. Partner sees names, stages, contact emails, and notes marked shareable. No re-export.",
        evidence: ["the 12 rows", "field-level grant preview", "workspace rules"],
      }});
      setBusy(false);
    });
  };
  const proceedShare = (approvalId) => {
    patch(approvalId, { resolved: "Approved at " + svNow() + " — recorded" });
    receipt("you", "Approved share to Legacy Sports", "12 businesses");
    setJob((j) => Object.assign({}, j, { state: "queued", queueNote: "position 1" }));
    later(900, () => {
      setJob((j) => Object.assign({}, j, { state: "running", detail: "Building LA28-only field views · 0 of 12" }));
      let n = 0;
      const tick = () => {
        n += 1 + Math.floor(Math.random() * 2);
        if (n >= 12) {
          setJob((j) => Object.assign({}, j, { state: "done", doneCount: 12, doneNote: "Frozen into the receipt below." }));
          later(700, () => {
            setJob(null); setShortlistShared(true);
            receipt("agent", "Shared Q3 caterers to Legacy Sports", "12 businesses",
              { moved: "Legacy Sports sees LA28 fields only · no re-export · every open lands on the record",
                snapshot: snapshotOf("shared") });
            later(400, () => push({ kind: "prose", text: "Done. Every open on their side is evented — you'll see it here." }));
          });
        } else {
          setJob((j) => Object.assign({}, j, { doneCount: n, detail: "Building LA28-only field views · " + n + " of 12" }));
          later(420, tick);
        }
      };
      later(420, tick);
    });
  };
  const denyShare = (approvalId, withEdit) => {
    if (withEdit) {
      patch(approvalId, { resolved: "Denied with changes at " + svNow() + " — reproposing without contact emails" });
      receipt("you", "Denied with changes — repropose without contact emails", "12 businesses", { moved: "recorded as a denial · nothing crossed" });
      setJob(null); setBusy(true); scriptShare(true);
    } else {
      patch(approvalId, { resolved: "Denied at " + svNow() + " — recorded, nothing crossed" });
      receipt("you", "Denied share to Legacy Sports", "12 businesses", { moved: "nothing crossed · the record keeps the ask" });
      setJob(null);
    }
  };

  // ── applying an agent answer (model or local) to the surface ──
  const applyAnswer = (ans) => {
    if (!ans) { setBusy(false); return; }
    if (ans.startShare) { scriptShare(false); return; }
    let delay = 500;
    if (ans.blocks) ans.blocks.forEach((b) => {
      later(delay, () => {
        if (b.type === "glance") push({ kind: "glance", metrics: b.metrics, caption: b.caption });
        else if (b.type === "table") { setFilter(b.filter || "all"); pushBlock("table", { label: b.filter || "all", filterKey: b.filter || "all" }); }
        else pushBlock(b.type);
      });
      delay += 350;
    });
    if (ans.createsShortlist) later(delay, () => setShortlistExists(true));
    // receipts arrive from tool execution (earned, never model-authored)
    const rcpts = ans.receipts || (ans.receipt ? [ans.receipt] : []);
    rcpts.forEach((rc, i) => { later(delay, () => {
      const snap = i === 0 && ans.panel ? snapshotOf(ans.panel, ans.blocks && ans.blocks[0] && ans.blocks[0].filter) : null;
      receipt("agent", rc.verb, rc.scope, Object.assign({}, rc.moved ? { moved: rc.moved } : {}, snap ? { snapshot: snap } : {}));
    }); delay += 200; });
    if (ans.panel && t.featurePanel === "pinned") later(delay, () => openPanel(ans.panel, { filter: ans.blocks && ans.blocks[0] && ans.blocks[0].filter }));
    later(delay + 400, () => {
      if (ans.prose) push({ kind: "prose", text: ans.prose, suggestions: ans.suggestions, onSuggestDeterministic: true });
      setBusy(false);
    });
  };

  const frameDesc = () => {
    const label = SPINE_ITEMS.find((s) => s.id === feature).label;
    const who = role === "sam" ? "Viewing as Sam Cole, VP Partnerships (role re-projects ranking/emphasis). " : "";
    const ws = wsMeta.find((w) => w.id === focusId);
    const matter = ws && wsMeta.length > 1 ? "Focused matter: " + ws.label + ". " : "";
    return who + matter + label + (selection.length ? " · " + selection.length + " selected" : "") + (showPanel ? " · surface: " + panel.view : "");
  };

  // ── workstream mechanics: push to any matter, switch focus, spawn alongside ──
  const pushToStream = (sid, part) => {
    const p = Object.assign({ id: svid() }, part);
    if (sid === focusRef.current) setParts((ps) => ps.concat([p]));
    else { const s = storedStreams.current[sid]; if (s) s.parts = s.parts.concat([p]); }
  };
  const switchStream = (id) => {
    if (id === focusRef.current || busy) return;
    // park the current matter with its whole frame — transcript, surface, filter
    storedStreams.current[focusRef.current] = {
      parts, archives, panel, panelOpen, filter, selection,
    };
    setWs(focusRef.current, { state: "parked" });
    const s = storedStreams.current[id] || { parts: [], archives: [], panel: { view: "table" }, panelOpen: false, filter: "all", selection: [] };
    setParts(s.parts); setArchives(s.archives); setPanel(s.panel); setPanelOpen(s.panelOpen);
    setFilter(s.filter); setSelection(s.selection || []); setPast(null);
    focusRef.current = id; setFocusId(id);
    setWs(id, { state: "focused", needsYou: false });
    wakeDone.current = true;
  };
  // The router's "this is its own piece of work" moment — helpful, never presumptuous:
    // it opens alongside, you stay where you are, and the card is the control surface.
  const spawnSkyline = (utterText) => {
    const id = "skyline-renewal";
    if (wsMeta.some((w) => w.id === id)) {
      push({ kind: "prose", text: "That matter is already open alongside — its card is above." });
      setBusy(false);
      return;
    }
    storedStreams.current[id] = {
      parts: [
        { id: svid(), kind: "fresh", label: "Skyline AV renewal prep", sub: "opened alongside · " + svNow() },
        { id: svid(), kind: "utter", text: utterText },
      ],
      archives: [], panel: { view: "entity" }, panelOpen: false, filter: "all", selection: [],
    };
    setWsMeta((ms) => ms.concat([{ id, label: "Skyline AV renewal prep", state: "working", tick: "reading 14 invoices…" }]));
    later(400, () => {
      push({ kind: "prose", text: "That’s its own piece of work — I’ve started it alongside. It works while you stay here; the card flags you when it needs a decision." });
      receipt("agent", "Opened workstream Skyline AV renewal prep", "job-scoped · least-privilege · budget 500 actions",
        { moved: "runs in parallel · every step lands as a receipt on its card" });
      setBusy(false);
    });
    later(3200, () => {
      pushToStream(id, { kind: "receipt", actor: "agent", verb: "Pulled Skyline spend summary", scope: "14 invoices · $920k across 2 events", time: svNow() });
      setWs(id, { tick: "spend summary done · drafting…" });
    });
    later(4600, () => pushToStream(id, { kind: "glance", metrics: [
      { n: "$920k", label: "total spend" }, { n: "3", label: "contracts" }, { n: "Oct 15", label: "terms expire" }],
      caption: "From your AP data and contracts folder — the expiry is your leverage." }));
    later(6600, () => pushToStream(id, { kind: "receipt", actor: "agent", verb: "Drafted renewal talking points", scope: "1 draft · held for review", time: svNow(),
      moved: "nothing sends without you" }));
    later(7400, () => {
      pushToStream(id, { kind: "prose", text: "Draft’s ready. The Oct 15 expiry and their FIFA delivery record are your leverage — step in when you want to review.",
        suggestions: ["Send the draft to Tiffanie", "Close this matter"], onSuggestDeterministic: true });
      if (focusRef.current !== id) setWs(id, { state: "ready", needsYou: true, tick: "draft ready for you" });
      else setWs(id, { tick: "draft ready" });
    });
  };

  // ── lifecycle: matters END. Closing settles — summary receipt + artifacts kept,
  // card leaves the strip, the receipt is the reopen handle. Nothing is deleted. ──
  const closeStream = (id) => {
    const w = wsMeta.find((x) => x.id === id);
    if (!w || id === MAIN_WS) return;
    if (focusRef.current === id) {
      // settle from inside: freeze this matter's transcript, then land back on the desk
      storedStreams.current[id] = { parts, archives, panel, panelOpen, filter, selection, closed: true };
      const s = storedStreams.current[MAIN_WS] || { parts: [], archives: [], panel: { view: "table" }, panelOpen: false, filter: "all", selection: [] };
      setParts(s.parts); setArchives(s.archives); setPanel(s.panel); setPanelOpen(s.panelOpen);
      setFilter(s.filter); setSelection(s.selection || []); setPast(null);
      focusRef.current = MAIN_WS; setFocusId(MAIN_WS);
    } else if (storedStreams.current[id]) storedStreams.current[id].closed = true;
    setWsMeta((ms) => ms.filter((x) => x.id !== id).map((x) => (x.id === MAIN_WS ? Object.assign({}, x, { state: "focused" }) : x)));
    const nReceipts = (storedStreams.current[id] ? storedStreams.current[id].parts : []).filter((p) => p.kind === "receipt").length;
    receipt("agent", "Closed matter " + w.label, nReceipts + " receipts · 1 artifact kept",
      { moved: "settled into the record — the draft stays in your files · reopen any time", reopenId: id, wsLabel: w.label });
  };
  const reopenStream = (id, label) => {
    if (wsMeta.some((x) => x.id === id)) { switchStream(id); return; }
    if (storedStreams.current[id]) {
      storedStreams.current[id].closed = false;
      storedStreams.current[id].parts = storedStreams.current[id].parts.concat([
        { id: svid(), kind: "receipt", actor: "you", verb: "Reopened this matter", scope: "transcript and artifacts intact", time: svNow() }]);
    }
    setWsMeta((ms) => ms.concat([{ id, label: label || "Reopened matter", state: "parked", tick: "reopened · " + svNow() }]));
    receipt("you", "Reopened matter " + (label || id), "picked up where it settled");
  };

  // matter-local intents — the focused matter answers its own lifecycle asks
  const matterIntent = (text) => {
    if (focusRef.current === MAIN_WS) return false;
    const q = text.toLowerCase();
    const wsId = focusRef.current;
    const w = wsMeta.find((x) => x.id === wsId);
    if (q.includes("send") && q.includes("draft")) {
      later(500, () => {
        receipt("you", "Approved — draft sent to Tiffanie", "1 message · renewal talking points",
          { moved: "delivery lands on the record when she opens it" });
        setWs(wsId, { tick: "draft delivered" });
        later(700, () => push({
          kind: "prose",
          text: "Sent. That was everything this matter wanted — close it? The draft and every receipt stay in the record.",
          suggestions: ["Close this matter", "Keep it open"], onSuggestDeterministic: true,
        }));
        setBusy(false);
      });
      return true;
    }
    if (q.includes("close") && (q.includes("matter") || q.includes("it"))) {
      later(400, () => { closeStream(wsId); setBusy(false); });
      return true;
    }
    if (q.includes("keep it open")) {
      later(300, () => {
        push({ kind: "prose", text: "Kept. It'll sit parked — if it stays quiet for a week, I'll propose closing it with a summary." });
        setWs(wsId, { tick: "parked · quiet" });
        setBusy(false);
      });
      return true;
    }
    return false;
  };

  const runIntent = async (text, opts) => {
    // the router notices new matters: "separately…" (or a clearly distinct ask)
    // opens a workstream alongside instead of derailing the focused one
    const spawnAsk = /^(separately|meanwhile|in parallel)[\s,:—-]/i.test(text) || /skyline.*(renewal|prep)/i.test(text);
    setBusy(true);
    push({ kind: "utter", text });
    setComposerVal("");
    if (spawnAsk) { spawnSkyline(text); return; }
    if (matterIntent(text)) return;
    const ans = await window.SRIAgent.ask(text, {
      deterministic: opts && opts.deterministic,
      frame: frameDesc(),
      history: historyRef.current.slice(-6),
    });
    historyRef.current.push({ role: "user", content: text });
    if (ans && ans.prose) historyRef.current.push({ role: "assistant", content: JSON.stringify(ans) });
    applyAnswer(ans);
  };
  const runChip = (text) => runIntent(text, { deterministic: true });

  // ── reveal one-gesture actions (J2 → act), role-projected ──
  const onRevealAct = (act) => {
    if (act === "shortlist") { setRevealActed("shortlist"); runChip("Build a re-engagement shortlist"); return; }
    if (act === "nudge") {
      if (role === "sam") {
        receipt("you", "Offered your warm intro to Marco Silva", "path strength 84",
          { moved: "procurement's shortlist row now carries your path · nothing sends without you" });
        later(400, () => push({ kind: "prose", text: "Offered. Priya's row now shows the path through you — the intro draft waits in your queue." }));
        return;
      }
      receipt("agent", "Drafted re-engagement nudges for the 9 quiet vendors", "9 drafts",
        { moved: "held for your review · nothing sends without you" });
      later(400, () => push({ kind: "prose", text: "Nine drafts, one per vendor, each citing the history you'd be re-opening. Review them when you're ready — they wait." }));
      return;
    }
    if (role === "sam") { openEntity("meridian"); return; }
    openPanel("table", { filter: "dormant" }); // law 2a: navigation — no receipt
    setFilter("dormant");
  };

  // ── universal search escalation ──
  const onEscalate = (q) => {
    if (q === "__openShortlist") { openPanel("shortlist"); return; }
    if (q.startsWith("__waiting:")) {
      if (q.includes("approval")) { setBusy(true); push({ kind: "utter", text: "Review the pending share" }); scriptShare(false); }
      else push({ kind: "prose", text: "The re-engagement sweep finished overnight — 9 drafts are ready.", suggestions: ["Build a re-engagement shortlist"], onSuggestDeterministic: true });
      return;
    }
    runIntent(q);
  };

  const switchFeature = (f, opts) => {
    setFeature(f);
    setPast(null);
    setPanelCollapsed(false);
    const flt = opts && opts.filter;
    if (f === "home") { setPanelOpen(false); setPanelFocus(false); setLeftView(null); }
    else if (f === "businesses") {
      if (flt === "interest") { setFilter("interest"); setPanel({ view: "preintake" }); }
      else if (flt) { setFilter(flt); setPanel({ view: "table", filter: flt }); }
      else { setPanel({ view: shortlistExists ? "shortlist" : "table" }); }
      setPanelOpen(true);
    }
    else if (f === "reports") { setPanel({ view: "report" }); setPanelOpen(true); startReport(); }
    else if (f === "opportunities") { setPanel({ view: "opps" }); setPanelOpen(true); }
    else if (f === "shared") { setPanel({ view: "shared" }); setPanelOpen(true); }
  };

  // ── J0 pitch — the five beats, performed ──
  const pitchBeats = [
    () => { openPanel("table"); setFilter("all");
      receipt("you", "Opened Businesses — everything already in your systems", "imported this morning", { snapshot: snapshotOf("table", "all") }); },
    () => { setBusy(true); push({ kind: "utter", text: "Who's gone quiet that we already paid?" });
      later(700, () => push({ kind: "glance", metrics: [
        { n: "9", label: "quiet since 2024" }, { n: "19 mo", label: "median silence" }, { n: "$1.8M", label: "dormant spend" }],
        caption: "Pure AP data — vendors you already paid, nobody re-engaged." }));
      later(1500, () => { setShortlistExists(true); pushBlock("shortlist");
        receipt("agent", "Created shortlist Q3 caterers", "12 businesses", { moved: "tomorrow's brief will carry it", snapshot: snapshotOf("shortlist") });
        openPanel("shortlist"); setBusy(false); }); },
    () => { setBusy(true); later(500, () => { pushBlock("entity");
      receipt("agent", "Pulled Meridian Catering Co. together", "5 facts · 1 locked", { snapshot: snapshotOf("entity") });
      openPanel("entity");
      later(400, () => { push({ kind: "prose", text: "Not a guess — your own records, resolved. The one locked fact is FIFA's to share, not ours to leak." }); setBusy(false); }); }); },
    () => { setBusy(true); push({ kind: "utter", text: "Share this with Legacy Sports" }); scriptShare(false); },
    () => { push({ kind: "day", label: "The morning after" });
      later(400, () => receipt("agent", "Delivered this morning's brief to email and Slack", "3 items",
        { moved: "each item deep-links back to this canvas" }));
      later(1000, () => push({ kind: "prose",
        text: "Marco Silva opened your nudge twice overnight. It was already working before you logged in.", }));
      later(1800, () => push({ kind: "prose", text: SV.reveal.wall })); },
  ];
  const startPitch = () => { setPitchBeat(0); pitchBeats[0](); };
  const nextBeat = () => { const n = pitchBeat + 1; if (n < pitchBeats.length) { setPitchBeat(n); pitchBeats[n](); } };

  // ── the feature surface (right panel) ──
  const renderSubject = (view, f, isPast, asOfTime) => {
    const chrome = (props, children) => (
      <StageChrome {...props} past={isPast ? asOfTime : null} onReturnNow={() => setPast(null)}>{children}</StageChrome>);
    if (view === "entity") return chrome(
      { title: "Meridian Catering Co.", asOf: asOfTime || svNow(), provenance: "entity stim_biz_7QF2K · grant g_fifa_ref", label: "Entity — Meridian",
        actions: !isPast && <StageBtn onClick={() => openPanel("table")}>Businesses</StageBtn> },
      <EntityProfile e={SV.meridian} />);
    if (view === "shortlist") return chrome(
      { title: "Q3 caterers", asOf: asOfTime || svNow(), provenance: "list q3-caterers.v2", label: "Shortlist",
        actions: !isPast && <React.Fragment><StageBtn>Export</StageBtn>
          {!shortlistShared && <StageBtn primary onClick={() => runChip("Share this with Legacy Sports")}>Share as list</StageBtn>}</React.Fragment> },
      <Shortlist rows={SV.businesses.filter((r) => r.category === "catering")} shared={!isPast && shortlistShared} onOpenEntity={openEntity} />);
    if (view === "report") return chrome(
      { title: "Reports", asOf: asOfTime || svNow(), provenance: "report jun-2026 · catalog v2 · data by reference", label: "Composed report",
        actions: !isPast && <React.Fragment><StageBtn>Print</StageBtn><StageBtn>Export</StageBtn></React.Fragment> },
      <ReportView report={SV.report} built={reportBuilt} personalize={lens}
        onPersonalize={(l) => { setLens(l); receipt("you", l === "sales" ? "Report lens set to sales impact only" : "Report lens set to everything", ""); }} />);
    if (view === "opps") return chrome(
      { title: "Opportunities", asOf: asOfTime || svNow(), provenance: "view rfx.v6", label: "Opportunity workspace" },
      <OpportunityWorkspace onOpenEntity={openEntity} />);
    if (view === "shared") return chrome(
      { title: "Lists", asOf: asOfTime || svNow(), provenance: "workspace W · obligations: no-re-export", label: "Lists" },
      <Lists onOpenEntity={openEntity} />);
    if (view === "preintake") return chrome(
      { title: "Sourcing", asOf: asOfTime || svNow(), provenance: "view preintake.v1", label: "Pre-intake" },
      <PreIntake onOpenEntity={openEntity} />);
    return chrome(
      { title: "Businesses", asOf: asOfTime || svNow(), provenance: "view businesses.v14 · grant g_fifa_ref", label: "Businesses table" },
      <AllBusinesses onOpenEntity={openEntity} />);
  };
  const renderPanel = () => past
    ? renderSubject(past.view, past.filter, true, past.time)
    : renderSubject(panel.view, panel.filter, false, null);

  // ── thread part rendering ──
  function renderPart(p) {
    if (p.kind === "fresh") return (
      <div key={p.id} className="svFresh" data-fresh="1">
        <span className="t">{p.label}</span>
        {p.sub && <span className="s">{p.sub}</span>}
      </div>
    );
    if (p.kind === "utter") return <Utterance key={p.id} text={p.text} />;
    if (p.kind === "prose") return <AgentProse key={p.id} text={p.text} suggestions={p.suggestions}
      onSuggest={p.onSuggestDeterministic ? runChip : runIntent} />;
    if (p.kind === "glance") return <Glanceable key={p.id} metrics={p.metrics} caption={p.caption} />;
    if (p.kind === "day") return <DaySep key={p.id} label={p.label} />;
    if (p.kind === "receipt") return <ReceiptRow key={p.id} r={p} onRestore={(r) => (r.reopenId ? reopenStream(r.reopenId, r.wsLabel) : restoreReceipt(r))}
      isPast={past && past.receiptId === p.id} />;
    if (p.kind === "approval") return (
      <ApprovalCard key={p.id} req={p.req} resolved={p.resolved}
        onApprove={() => proceedShare(p.id)} onDeny={() => denyShare(p.id, false)} onEdit={() => denyShare(p.id, true)} />);
    if (p.kind === "block") {
      const common = { frozen: p.frozen, time: p.time, onReopen: () => reopenBlock(p) };
      if (p.btype === "reveal") return <RevealBlock key={p.id} role={role} built={revealBuilt} onAct={onRevealAct} acted={revealActed} {...common} />;
      if (p.btype === "brief") return <BriefBlock key={p.id} role={role} onAsk={runChip} {...common} />;
      if (p.btype === "table") return <TableBlock key={p.id} rows={filteredRows(p.filterKey || "catering")} label={p.label || "catering"}
        onOpenFull={() => openPanel("table")} onOpenEntity={openEntity} {...common} />;
      if (p.btype === "shortlist") return <ShortlistBlock key={p.id} rows={SV.businesses.filter((r) => r.category === "catering")}
        shared={shortlistShared} onOpenFull={() => openPanel("shortlist")} onOpenEntity={openEntity}
        onShare={() => runChip("Share this with Legacy Sports")} {...common} />;
      if (p.btype === "entity") return <EntityCardBlock key={p.id} e={SV.meridian} onOpenFull={() => openPanel("entity")} {...common} />;
      if (p.btype === "journey") return <JourneyBlock key={p.id} e={SV.meridian} onOpenFull={() => openPanel("entity")} {...common} />;
    }
    return null;
  }

  // group consecutive parts from the same loaded chunk so the whole chunk eases in as one
  const renderThreadParts = () => {
    const out = [];
    let run = null;
    parts.forEach((p) => {
      if (p.chunk) {
        if (!run || run.key !== p.chunk) { run = { key: p.chunk, items: [] }; out.push(run); }
        run.items.push(p);
      } else { run = null; out.push(p); }
    });
    return out.map((x) => (x.items
      ? <div className="svChunk" key={"chunk" + x.key}>{x.items.map(renderPart)}</div>
      : renderPart(x)));
  };

  const approvalPendingNow = approvalPending;
  const needsYou = { home: approvalPendingNow ? 1 : 0, businesses: 0, opportunities: 0, shared: approvalPendingNow ? 1 : 0, reports: 0 };
  const navCounts = {
    all: SV.businesses.length,
    intake: SV.businesses.filter((r) => r.bucket === "interest").length,
    lists: shortlistExists ? 1 : 0,
  };
  const panelLabelMap = { table: "Businesses table", preintake: "Pre-intake", shortlist: "shortlist Q3 caterers", entity: "Meridian profile", report: "June report", opps: "Opportunities", shared: "Lists" };

  return (
    <div className="cvApp" data-theme="light" style={{ "--cvDur": SV_MOTION[t.motion] + "ms", ...(panelW ? { "--panelW": panelW + "px" } : {}) }}>
      <TopBar role={role} onOpenEntity={openEntity} onEscalate={onEscalate} />
      <div className={"v2Main" + (showPanel ? " isPanelOpen" : "") + (panelFocus && showPanel ? " isPanelFocus" : "") + (leftView && showPanel ? " isSplit" : "") + (ctxCollapsed ? " isCtxCollapsed" : "") + (spineWide ? " isSpineWide" : "") + (panelDrag ? " isPanelDrag" : "")}>
        <Spine feature={feature} filter={filter} navCounts={navCounts} onSwitch={switchFeature} needsYou={needsYou} role={role} wide={spineWide} onToggleWide={toggleSpine} />
        <div className="cvCtxGhost" aria-hidden="true"></div>
        {false && (
        <ContextPanel feature={feature === "home" ? "home" : feature} filter={filter}
          onFilter={(f) => { // law 2a: navigation, not mutation — no block, no receipt; the frame carries it
            if (f === filter && feature === "businesses" && panel.view === "table") return; // idempotent no-op
            setFilter(f); setFeature("businesses");
            if (t.featurePanel === "pinned") { setPast(null); setPanel({ view: "table", filter: f }); } }}
          selection={selection} onClearSel={() => setSelection([])}
          rows={SV.businesses} waiting={SV.search.waiting}
          onWaiting={(w) => onEscalate("__waiting:" + w.t)}
          shortlistExists={shortlistExists} onOpenShortlist={() => openPanel("shortlist")} day="day30" onCollapse={() => setCtxCollapsed(true)} />
        )}
        {false && ctxCollapsed && !panelFocus && (
          <button type="button" className="v2CtxExpand" onClick={() => setCtxCollapsed(false)}
            aria-label="Show panel" title="Show panel"><Icon name="chevR" size={13} /></button>
        )}
        <div className="v2Center" data-screen-label="Agent workspace — thread">
          {showPanel && !panelFocus && (
            <div className="v2SplitCtrls">
              <div className="v2SplitPick">
                <button type="button" className="v2SplitBtn" onClick={() => setSplitOpen((o) => !o)}
                  aria-expanded={splitOpen} title="Choose what to show beside this page">
                  <Icon name="exchange" size={13} />
                  <span>{leftView ? SPLIT_PAGES.find((s) => s.v === leftView.view) ? SPLIT_PAGES.find((s) => s.v === leftView.view).label : "Page" : "Home"}</span>
                  <Icon name="chevD" size={11} />
                </button>
                {splitOpen && (
                  <div className="v2SplitMenu" onMouseLeave={() => setSplitOpen(false)}>
                    <div className="v2SplitMenuH">Show on the left</div>
                    {SPLIT_PAGES.map((s) => (
                      <button key={s.v} type="button"
                        className={"v2SplitMenuItem" + (((leftView && leftView.view === s.v) || (!leftView && s.v === "home")) ? " isOn" : "")}
                        onClick={() => { setLeftView(s.v === "home" ? null : { view: s.v, filter: s.v === "table" ? "all" : null }); setSplitOpen(false); }}>
                        <Icon name={s.icon} size={14} /><span>{s.label}</span>
                      </button>
                    ))}
                  </div>
                )}
              </div>
              <button type="button" className="v2HomeCollapse" onClick={() => setPanelFocus(true)}
                aria-label="Collapse left pane" title="Collapse — focus the page on the right">
                <Icon name="chevL" size={13} />
              </button>
            </div>
          )}
          {leftView && showPanel ? (
            <div className="v2SplitLeft" data-screen-label={"Left pane — " + leftView.view}>
              {renderSubject(leftView.view, leftView.filter, false, null)}
            </div>
          ) : (
          <React.Fragment>
          {wsMeta.length > 1 && (
            <div className="wsStrip" data-screen-label="Working on — workstreams">
              <span className="wsStripLabel">Working on</span>
              {wsMeta.map((w) => (
                <button key={w.id} type="button"
                  className={"wsCard" + (w.id === focusId ? " isFocused" : "") + (w.needsYou ? " isNeedsYou" : "")}
                  onClick={() => switchStream(w.id)} disabled={busy && w.id !== focusId}
                  title={w.id === focusId ? "Focused — the composer speaks to this matter" : "Step in — full context and surface restore"}>
                  <span className={"wsDot" + (w.state === "working" ? " isWorking" : "") + (w.needsYou ? " isReady" : "")}></span>
                  <span className="wsMain">
                    <span className="wsName">{w.label}</span>
                    {w.tick && w.id !== focusId && <span className="wsTick">{w.tick}</span>}
                  </span>
                  {w.needsYou && <span className="wsBadge">needs you</span>}
                </button>
              ))}
            </div>
          )}
          <div className={"v2Thread" + (clearing ? " isClearing" : "")} ref={threadRef} onScroll={onThreadScroll} onWheel={onThreadWheel}>
            {archives.length > 0 ? (
              <button type="button" className="svPullEarlier" onClick={loadEarlier}
                style={{ transform: "translateY(" + Math.round(pull * 0.22) + "px)" }}
                title="Load the previous session into the transcript">
                <span className="bar"><i style={{ width: Math.round((pull / PULL_MAX) * 100) + "%" }}></i></span>
                <span>{pull > 0 ? "Keep pulling — " : ""}Earlier · {archives[archives.length - 1].label}</span>
              </button>
            ) : (
              <div className="svRecordCap">Beginning of your record — every receipt kept</div>
            )}
            {renderThreadParts()}
            {job && <JobCard job={job} onCancel={() => setJob(null)} />}
            {busy && <div className="cvThinking"><span>Working<span className="cvDots"><i>.</i><i>.</i><i>.</i></span></span></div>}
          </div>
          {scrolledUp && (
            <button type="button" className="svJumpNow"
              onClick={() => { const el = threadRef.current; if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); }}>
              Back to now <Icon name="chevD" size={12} />
            </button>
          )}
          <div className="v2Dock">
            <Composer feature={feature} selection={selection} subject={showPanel ? panelLabelMap[past ? past.view : panel.view] : null}
              acting={busy && selection.length > 0} busy={busy} value={composerVal} setValue={setComposerVal}
              onSubmit={runIntent}
              onFresh={parts.length > 5 ? startFreshSession : null}
              freshDisabled={busy || !!job || approvalPendingNow}
              onFocus={() => {}}
              onBlur={() => {}} />
          </div>
          </React.Fragment>
          )}
        </div>
        {panelAvailable && (
          <button type="button" className="v2PanelExpand" onClick={() => setPanelCollapsed(false)}
            aria-label="Expand feature surface" title="Expand feature surface">
            <Icon name="chevL" size={13} />
          </button>
        )}
        <div className="v2Panel" data-screen-label="Feature surface">
          <div className="v2PanelResize" role="separator" aria-orientation="vertical" aria-label="Resize panel — double-click to reset"
            onPointerDown={onPanelResizeDown} onDoubleClick={resetPanelW} title="Drag to resize · double-click to reset"></div>
          <button type="button" className="v2PanelFocus" onClick={() => setPanelFocus((v) => !v)}
            aria-pressed={panelFocus} aria-label={panelFocus ? "Show Home" : "Focus this page"}
            title={panelFocus ? "Bring Home back" : "Focus — fill the surface with this page"}>
            <Icon name={panelFocus ? "minimize" : "maximize"} size={13} />
          </button>
          <button type="button" className="v2PanelCollapse" onClick={() => { setPanelCollapsed(true); setPast(null); setPanelFocus(false); }}
            aria-label="Collapse feature surface" title="Collapse — the thread keeps working">
            <Icon name="chevR" size={13} />
          </button>
          <div className="v2PanelInner">
            {renderPanel()}
          </div>
        </div>
      </div>

      {pitchBeat != null && <PitchBar beat={pitchBeat} onNext={nextBeat} onEnd={() => setPitchBeat(null)} />}

      <TweaksPanel>
        <TweakSection label="Motion" />
        <TweakRadio label="Timing" value={t.motion} options={["crisp", "standard", "glide"]}
          onChange={(v) => setTweak("motion", v)} />
        <TweakSection label="Feature surface" />
        <TweakRadio label="Right panel" value={t.featurePanel} options={["pinned", "on-demand"]}
          onChange={(v) => setTweak("featurePanel", v)} />
        <TweakSection label="Journey" />
        <TweakRadio label="Role" value={role} options={[
          { label: "Priya · Procurement", value: "priya" },
          { label: "Sam · Partnerships", value: "sam" },
        ]} onChange={setRole} />
        <TweakRadio label="Day state" value={t.day} options={["day 0", "day 30"]}
          onChange={(v) => setTweak("day", v)} />
        <TweakButton label="Run the pitch (J0)" onClick={startPitch} />
        <TweakButton label="Restart onboarding (J1)" secondary onClick={onRestartOnboarding} />
      </TweaksPanel>
    </div>
  );
}

// ═══ Root — the phase machine ═══════════════════════════════════════════
function App() {
  const [t, setTweak] = useTweaks(SV_DEFAULTS);
  const [phase, setPhase] = React.useState(() => {
    try { return localStorage.getItem("sriAppPhase") || "onboarding"; } catch (e) { return "onboarding"; }
  });
  const [deskKey, setDeskKey] = React.useState(1);

  const enterDesk = (res) => {
    try {
      localStorage.setItem("sriAppPhase", "desk");
      if (res) localStorage.setItem("sriAppOnboard", JSON.stringify(res));
    } catch (e) {}
    setDeskKey((k) => k + 1);
    setPhase("desk");
  };
  const restartOnboarding = () => {
    try { localStorage.removeItem("sriAppPhase"); localStorage.removeItem("sriAppOnboard"); } catch (e) {}
    setPhase("onboarding");
  };

  if (phase === "onboarding") return <Onboarding onDone={enterDesk} />;
  return <Desk key={deskKey + ":" + t.day} t={t} setTweak={setTweak} onRestartOnboarding={restartOnboarding} />;
}

(function mountWhenReady() {
  if (window.__sriMounted) return;
  if (window.SRIData && window.StimulusDesignSystem_bb8fa3) {
    window.__sriMounted = true;
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
  } else {
    setTimeout(mountWhenReady, 30);
  }
})();

