/* fs_board.jsx — the FULL-SCREEN version of the onboarding · unhappy paths.
   Same flow + scenario logic as web_board.jsx, but the flow is NOT a floating
   focus card: it fills the whole viewport edge-to-edge. Content stays in a
   centered column for readability, and the entire surface is transform-scaled
   up (~1.3×) so type and feature sizes read at desktop scale. */

/* ---------- full-screen progress rail ------------------------------------- */
const PHASES = ["Verify", "Profile", "Secure", "Connect"];
const FsRail = ({ phase, fill, mins, onBack, canBack }) => (
  <div style={{ flex: "none", background: "var(--bg)" }}>
    <div style={{ maxWidth: 800, margin: "0 auto", padding: "28px 24px 18px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
        <Logo size={22} color="var(--ink)" />
        <span style={{ marginLeft: "auto", fontFamily: "var(--f-mono)", fontSize: 12.5, color: "var(--ink-3)" }}>≈ {mins} min left</span>
      </div>
      <div style={{ display: "flex", gap: 7, marginTop: 17 }}>
        {PHASES.map((_, i) => {
          const f = i < phase ? 1 : i === phase ? fill : 0;
          return (
            <div key={i} style={{ flex: 1, height: 3, background: "var(--rule)", overflow: "hidden" }}>
              <div style={{ height: "100%", transformOrigin: "left", transform: `scaleX(${f})`, background: "var(--accent)", transition: "transform 480ms var(--ease)" }} />
            </div>
          );
        })}
      </div>
      <div style={{ display: "flex", alignItems: "center", marginTop: 12, minHeight: 18 }}>
        {canBack ? (
          <button className="press" onClick={onBack} aria-label="Back" style={{ background: "none", border: "none", padding: 0, margin: "0 10px 0 0", display: "flex", alignItems: "center", gap: 4, color: "var(--ink-2)", cursor: "pointer", fontFamily: "var(--f-display)", fontSize: 11.5, fontWeight: 600 }}>
            <Icon name="back" size={15} color="var(--ink-2)" /> Back
          </button>
        ) : null}
      </div>
    </div>
  </div>
);

/* ---------- web tab bar (once the account is live) ------------------------ */
const TABS = [["home", "Home"], ["trade", "Trade"], ["chat", "Yoshi"], ["studio", "Studio"], ["accounts", "Accounts"]];
const FsTabBar = ({ tab, onTab }) => (
  <div style={{ flex: "none", borderTop: "1px solid var(--rule)", background: "color-mix(in srgb, var(--bg) 92%, transparent)", backdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)" }}>
    <div style={{ maxWidth: 800, margin: "0 auto", padding: "10px 14px 12px", display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 2 }}>
      {TABS.map(([id, label]) => {
        const on = id === tab;
        if (id === "chat") {
          return (
            <button key={id} className="tab press" onClick={() => onTab(id)} style={{ overflow: "visible", display: "flex", flexDirection: "column", alignItems: "center", gap: 4, background: "none", border: "none", position: "relative", cursor: "pointer" }}>
              <span style={{ position: "absolute", top: -24, left: "50%", transform: "translateX(-50%)", width: 54, height: 54, borderRadius: 999, background: "var(--bg-card)", border: on ? "1.5px solid var(--accent)" : "1px solid var(--rule)", boxShadow: "0 8px 22px -8px rgba(0,0,0,0.32)", display: "grid", placeItems: "center" }}>
                <Logo size={24} color="var(--ink)" />
              </span>
              <span style={{ marginTop: 32, fontFamily: "var(--f-display)", fontSize: 9, letterSpacing: "0.05em", fontWeight: on ? 700 : 500, textTransform: "uppercase", color: on ? "var(--ink)" : "var(--ink-3)" }}>{label}</span>
            </button>
          );
        }
        return (
          <button key={id} className="tab press" onClick={() => onTab(id)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, background: "none", border: "none", position: "relative", cursor: "pointer", padding: "4px 0 0" }}>
            {on && <span style={{ position: "absolute", top: -9, left: "50%", transform: "translateX(-50%)", width: 18, height: 2, background: "var(--accent)" }} />}
            <Icon name={id} size={22} stroke={on ? 1.7 : 1.5} color={on ? "var(--ink)" : "var(--ink-3)"} />
            <span style={{ fontFamily: "var(--f-display)", fontSize: 9, letterSpacing: "0.05em", fontWeight: on ? 700 : 500, textTransform: "uppercase", color: on ? "var(--ink)" : "var(--ink-3)" }}>{label}</span>
          </button>
        );
      })}
    </div>
  </div>
);

const Toast = ({ msg }) => (
  <div style={{ position: "absolute", left: "50%", transform: "translateX(-50%)", bottom: 18, zIndex: 350, width: "min(560px, calc(100% - 36px))", background: "var(--terminal-fill)", color: "var(--terminal-ink)", padding: "13px 16px", display: "flex", alignItems: "center", gap: 9, animation: "count-up 240ms ease both", boxShadow: "0 8px 30px -10px rgba(0,0,0,0.5)" }}>
    <span style={{ width: 6, height: 6, borderRadius: 999, background: "var(--accent)", flex: "none" }} />
    <span style={{ fontFamily: "var(--f-display)", fontSize: 13.5, fontWeight: 500 }}>{msg}</span>
  </div>
);

/* =============================================================
   The flow, as a state machine. Identical to the other boards.
   ============================================================= */
const ORDER = ["welcome", "verifyIntro", "plaid", "provision", "success", "income", "checks", "security", "funding", "link", "agent", "disclosures", "offToWork"];
const idxOf = (k) => ORDER.indexOf(k);
const STEP_META = {
  verifyIntro: { rail: true, phase: 0, fill: 0.5 },
  success:     { rail: true, phase: 0, fill: 1 },
  income:      { rail: true, phase: 1, fill: 0.5 },
  checks:      { rail: true, phase: 1, fill: 1 },
  security:    { rail: true, phase: 2, fill: 1 },
  funding:     { rail: true, phase: 3, fill: 0.34 },
  link:        { rail: true, phase: 3, fill: 0.67 },
  agent:       { rail: true, phase: 3, fill: 1 },
  disclosures: { rail: true, phase: 3, fill: 1 },
};
const REVIEW_PATHS = ["identity", "screening", "watchlist", "mismatch", "records"];
const TERMINAL = { happy: "offToWork", declined: "declined" };
REVIEW_PATHS.forEach((p) => { TERMINAL[p] = "reviewHold"; });

const PROVISION_REVIEW = { watchlist: "watchlist", mismatch: "mismatch", records: "records" };
const REVIEW_REASON = { identity: "identity", screening: "screening", watchlist: "watchlist", mismatch: "mismatch", records: "records" };

const BUILD = {
  id: "onb-fullscreen-unhappy-2026.06.15",
  exported: "2026-06-15",
  source: "Onboarding · web · full screen · fs_board.jsx",
  channel: "design-prototype",
};

const STEP_LABELS = {
  welcome: "Welcome", verifyIntro: "Verify intro", plaid: "Identity (Plaid)", provision: "Provisioning",
  success: "Identity confirmed", income: "Income & employment", checks: "Screening questions",
  security: "Security", funding: "Funding source", link: "Deposit", agent: "Connect assistant",
  disclosures: "Disclosures",
  offToWork: "Account live", reviewHold: "Manual review hold", reviewApproved: "Review approved · resume",
  declined: "Declined · hard stop",
};
const DEEP_STEPS = Object.keys(STEP_LABELS);
/* Ordered screen list for the Tweaks "Jump to" selector: the linear flow
   first, then the branch/terminal states. */
const SCREEN_OPTIONS = [
  ...ORDER.map((k) => ({ value: k, label: STEP_LABELS[k] || k })),
  { value: "reviewHold", label: STEP_LABELS.reviewHold },
  { value: "reviewApproved", label: STEP_LABELS.reviewApproved },
  { value: "declined", label: STEP_LABELS.declined },
];

const MOCKS = [
  { k: "Identity (Plaid)", v: "Liveness + document capture are simulated; timing compressed to ~3s. No Plaid Link is opened." },
  { k: "Phone / SMS", v: "No code is sent; phone verification is simulated." },
  { k: "Manual review", v: "The hold and the ready-email are demo-compressed (email lands ~3s). Real reviews can take up to one business day." },
  { k: "Assistant providers", v: "ChatGPT · Claude · OpenClaw · Hermes are illustrative, not the v1 external-connections catalog. Align before treating as a spec." },
  { k: "Assistant connect", v: "OAuth redirect and CLI keys are simulated; no real authorization or live key is issued." },
  { k: "Balances & funding", v: "Deposit and balance figures are sample data." },
];
const NOT_MODELED = ["blocked", "retry", "empty", "error"];

const freshIdv = () => ({ phone: "", dob: "", first: "Rivka", last: "Lipson", street: "1452 Valencia St", city: "San Francisco", state: "CA", zip: "94110" });

/* =============================================================
   FsDevice — the onboarding flow, full-bleed. Content held to a
   centered column; the whole surface is scaled up by the page CSS.
   ============================================================= */
const FsDevice = React.forwardRef(({ path, palette, accent, initialStep, onStep }, ref) => {
  const [step, setStep] = useState(initialStep || "welcome");
  const [cleared, setCleared] = useState(false);
  const [holdReason, setHoldReason] = useState(null);
  const [app, setApp] = useState(false);
  const [tab, setTab] = useState("home");
  const [toast, setToast] = useState(null);
  const [overlay, setOverlay] = useState(null);

  const [idv, setIdv] = useState(freshIdv);
  const [income, setIncome] = useState({ status: "Employed", company: "Northwind Labs", title2: "Product Designer", source: "Salary", incomeBand: "" });
  const [checks, setChecks] = useState(() => path === "screening"
    ? { none: false, flags: { pep: true }, pepOrg: "", pepName: "" }
    : { none: false, flags: {} });
  const [funding, setFunding] = useState(null);
  const [deposit, setDeposit] = useState(0);
  const [externals, setExternals] = useState([]);
  const [agents, setAgents] = useState([]);

  const reset = () => {
    setApp(false); setTab("home"); setToast(null); setCleared(false); setHoldReason(null); setOverlay(null);
    setIdv(freshIdv());
    setIncome({ status: "Employed", company: "Northwind Labs", title2: "Product Designer", source: "Salary", incomeBand: "" });
    setChecks(path === "screening" ? { none: false, flags: { pep: true }, pepOrg: "", pepName: "" } : { none: false, flags: {} });
    setFunding(null); setDeposit(0); setExternals([]); setAgents([]);
  };

  React.useImperativeHandle(ref, () => ({
    restart: () => { reset(); setStep("welcome"); },
    jump: () => { reset(); setStep(TERMINAL[path] || "welcome"); },
    goTo: (s) => { reset(); setStep(DEEP_STEPS.includes(s) ? s : "welcome"); },
  }), [path]);

  useEffect(() => { if (onStep) onStep(app ? ("app:" + tab) : step); }, [step, app, tab]);

  const flash = (m) => { setToast(m); setTimeout(() => setToast((x) => (x === m ? null : x)), 2200); };
  const goto = (k) => setStep(k);

  const back = () => {
    let i = idxOf(step) - 1;
    while (i >= 0 && (ORDER[i] === "plaid" || ORDER[i] === "provision" || ORDER[i] === "success")) i--;
    if (i >= 0) setStep(ORDER[i]);
  };

  const nav = useMemo(() => ({
    tab: (x) => { if (x === "home") setTab("home"); else flash("That lives in the full app"); },
    push: () => flash("Opens in the full app"),
    pop: () => {},
    sheet: (s) => setOverlay(s),
    closeSheet: () => setOverlay(null),
  }), []);

  const screens = {
    welcome:     <Welcome onNext={() => goto("verifyIntro")} />,
    verifyIntro: <VerifyIntro onNext={() => goto("plaid")} />,
    provision:   <Provisioning
                   review={cleared ? null : (PROVISION_REVIEW[path] || null)}
                   onReview={(r) => { setHoldReason(r); goto("reviewHold"); }}
                   onDone={() => goto(path === "declined" ? "declined" : "success")} />,
    success:     <Success onNext={() => goto("income")} />,
    income:      <Income data={income} setData={setIncome} phone={idv.phone} onNext={() => goto("checks")} />,
    checks:      <Checks data={checks} setData={setChecks} onNext={() => {
                   const flagged = !checks.none && Object.values(checks.flags || {}).some(Boolean);
                   if (!cleared && (path === "screening" || flagged)) { setHoldReason("screening"); goto("reviewHold"); }
                   else goto("security");
                 }} />,
    security:    <Security onNext={() => goto("funding")} />,
    funding:     <Funding funding={funding} setFunding={setFunding} externals={externals} setExternals={setExternals} onNext={() => goto("link")} onSkipToAgent={() => goto("agent")} />,
    link:        <FundDeposit funding={funding} setFunding={setFunding} externals={externals} setExternals={setExternals} deposit={deposit} setDeposit={setDeposit} onNext={() => goto("agent")} onSkip={() => goto("agent")} />,
    agent:       <Agent agents={agents} setAgents={setAgents} onNext={() => goto("disclosures")} onSkip={() => { setAgents([]); goto("disclosures"); }} />,
    disclosures: <Disclosures onNext={() => goto("offToWork")} />,
    offToWork:   <OffToWork agents={agents} accent={accent} onEnter={() => { setApp(true); setTab("home"); }} />,
    reviewHold:  <ReviewHold reason={holdReason || REVIEW_REASON[path] || "kyc"} email={idv.email} onReturn={() => goto("reviewApproved")} />,
    reviewApproved: <ReviewApproved onNext={() => { setCleared(true); goto("income"); }} />,
    declined:    <Declined />,
  };

  const meta = STEP_META[step] || {};
  const showRail = !app && meta.rail;
  const mins = Math.max(1, Math.ceil((idxOf("agent") - idxOf(step) + 1) / 3));
  const darkScreen = !app && step === "offToWork";

  return (
    <div className="fs-flow" data-palette={palette} data-testid="onboarding-surface" data-scenario={path} data-state={app ? ("app:" + tab) : step} data-platform="web" style={{ "--accent": accent, background: darkScreen ? "#141310" : "var(--bg)" }}>
      <PlatformCtx.Provider value="web">
      <ThemeCtx.Provider value={palette}>
        {!app ? (
          <>
            {showRail && <FsRail phase={meta.phase} fill={meta.fill} mins={mins} onBack={back} canBack />}
            <div className="fs-col">
              <div key={step} className="tab-swap viewport" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden", position: "relative" }}>
                {screens[step]}
                {step === "plaid" && (
                  <PlaidIDV data={idv} setData={setIdv} fail={path === "identity" && !cleared}
                    onDone={() => goto("provision")} onFail={() => { setHoldReason("identity"); goto("reviewHold"); }} onCancel={() => goto("verifyIntro")} />
                )}
                {toast && <Toast msg={toast} />}
              </div>
            </div>
          </>
        ) : (
          <>
            <div className="fs-app" style={{ position: "relative", flex: 1, minHeight: 0 }}>
              <WebHomeDayOne balance={deposit} externals={externals} nav={nav} flash={flash}
                onAddMoney={() => setOverlay({ type: "addmoney" })}
                onConnect={() => setOverlay({ type: "plaid" })} />
              {overlay?.type === "plaid" && (
                <PlaidLink mode="readonly" onClose={() => setOverlay(null)}
                  onLinked={(accts) => { setExternals((p) => [...p, ...accts]); setOverlay(null); flash("Connected · " + accts[0].inst); }} />
              )}
              {overlay?.type === "addmoney" && (
                <AddMoneySheet onConnect={() => setOverlay({ type: "plaid" })} onClose={() => setOverlay(null)} />
              )}
              {overlay?.type === "activity" && (
                <ActivitySheet item={overlay.item} onClose={() => setOverlay(null)} />
              )}
              {toast && <Toast msg={toast} />}
            </div>
          </>
        )}
      </ThemeCtx.Provider>
      </PlatformCtx.Provider>
    </div>
  );
});

/* =============================================================
   Board chrome — scenarios + spec/notes panel (shared design).
   ============================================================= */
const PATHS = [
  { id: "happy",     cap: "Happy path",          desc: "Verified, funded — account goes live",            tone: "var(--accent-pos)" },
  { id: "identity",  cap: "Identity unverified",  desc: "IDV retried, still no match → manual review",     tone: "var(--signal-warn)" },
  { id: "screening", cap: "Screening flag",       desc: "PEP / insider / FINRA answer → manual review",    tone: "var(--signal-warn)" },
  { id: "watchlist", cap: "Watchlist hit",        desc: "Name matches a watchlist → manual review",        tone: "var(--signal-warn)" },
  { id: "mismatch",  cap: "Info mismatch",        desc: "Details don't match records → manual review",     tone: "var(--signal-warn)" },
  { id: "records",   cap: "Records unverified",   desc: "Address / SSN can't be verified → manual review", tone: "var(--signal-warn)" },
  { id: "declined",  cap: "Declined",             desc: "Application can't be approved → hard stop",       tone: "var(--signal-neg)" },
];
const pathInfo = (id) => PATHS.find((p) => p.id === id) || PATHS[0];

if (typeof window !== "undefined") {
  window.__YOSHI_SPEC = {
    build: BUILD,
    scenarios: PATHS.map((p) => ({ id: p.id, label: p.cap, desc: p.desc, terminal: TERMINAL[p.id] || "offToWork" })),
    steps: STEP_LABELS,
    deepLink: { params: ["scenario", "step"], example: "?scenario=watchlist&step=reviewHold" },
    mocks: MOCKS,
    notModeled: NOT_MODELED,
  };
}

const SpecRow = ({ k, v }) => (
  <div style={{ display: "grid", gridTemplateColumns: "122px 1fr", gap: 12, padding: "10px 0", borderTop: "1px dashed var(--rule-2)" }}>
    <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-3)", lineHeight: 1.4 }}>{k}</div>
    <div style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--ink)", textWrap: "pretty" }}>{v}</div>
  </div>
);
const NotesHead = ({ children }) => (
  <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-3)", margin: "26px 0 6px" }}>{children}</div>
);

const NotesPanel = ({ open, onClose, onOpenState, path }) => {
  if (!open) return null;
  const term = (id) => TERMINAL[id] || "offToWork";
  return (
    <>
      <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 400, background: "rgba(0,0,0,0.42)" }} />
      <aside data-testid="mock-notes" style={{ position: "fixed", top: 0, right: 0, bottom: 0, zIndex: 401, width: "min(440px, 94vw)", background: "var(--bg)", borderLeft: "1px solid var(--rule-2)", boxShadow: "-24px 0 60px -30px rgba(0,0,0,0.6)", display: "flex", flexDirection: "column", fontFamily: "var(--f-display)", color: "var(--ink)" }}>
        <header style={{ flex: "none", padding: "18px 24px", borderBottom: "1px solid var(--rule-2)", display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ width: 7, height: 7, borderRadius: 999, background: "var(--signal-warn)", flex: "none" }} />
          <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase" }}>Inspector notes</span>
          <button onClick={onClose} data-testid="notes-close" aria-label="Close" style={{ marginLeft: "auto", background: "none", border: "none", cursor: "pointer", fontSize: 22, lineHeight: 1, color: "var(--ink-3)" }}>×</button>
        </header>
        <div style={{ flex: 1, overflow: "auto", padding: "6px 24px 36px" }}>
          <NotesHead>Build</NotesHead>
          <SpecRow k="Build id" v={BUILD.id} />
          <SpecRow k="Exported" v={BUILD.exported} />
          <SpecRow k="Source" v={BUILD.source} />
          <SpecRow k="Spec object" v="window.__YOSHI_SPEC" />
          <SpecRow k="Deep link" v="?scenario=…&step=… · e.g. ?scenario=watchlist&step=reviewHold" />

          <NotesHead>Scenario index</NotesHead>
          <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
            {PATHS.map((p) => (
              <div key={p.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 11px", border: "1px solid var(--rule-2)", background: "var(--bg-card)", position: "relative" }}>
                {p.id === path && <span style={{ position: "absolute", top: 0, left: 0, right: 0, height: 2, background: "var(--signal-warn)" }} />}
                <span style={{ width: 8, height: 8, borderRadius: 999, background: p.tone, flex: "none" }} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ fontSize: 12.5, fontWeight: 700 }}>{p.cap}</div>
                  <div style={{ fontSize: 9.5, letterSpacing: "0.05em", textTransform: "uppercase", color: "var(--ink-3)", marginTop: 2 }}>ends · {STEP_LABELS[term(p.id)]}</div>
                </div>
                <button data-testid={"open-" + p.id} onClick={() => onOpenState(p.id, term(p.id))} style={{ flex: "none", fontSize: 11, fontWeight: 600, border: "1px solid var(--rule-2)", background: "var(--bg-card)", color: "var(--ink)", borderRadius: 8, padding: "6px 10px", cursor: "pointer", whiteSpace: "nowrap" }}>Open →</button>
              </div>
            ))}
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 10, lineHeight: 1.5, textWrap: "pretty" }}>
            Review scenarios resume via the in-screen mail tap: hold → <button onClick={() => onOpenState(path, "reviewApproved")} style={{ background: "none", border: "none", padding: 0, color: "var(--ink)", textDecoration: "underline", cursor: "pointer", font: "inherit" }}>review approved</button> → income. Not modeled in this build: {NOT_MODELED.join(" · ")}.
          </div>

          <NotesHead>Simulated behavior</NotesHead>
          {MOCKS.map((m) => <SpecRow key={m.k} k={m.k} v={m.v} />)}
        </div>
      </aside>
    </>
  );
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": "graphite",
  "accent": "#deb24e",
  "path": "happy"
}/*EDITMODE-END*/;

const VALID_PATHS = PATHS.map((p) => p.id);

const CtrlBtn = ({ children, onClick, primary, testid }) => (
  <button onClick={onClick} data-testid={testid} className="wc-btn" style={primary ? { background: "var(--ink)", color: "var(--bg)", borderColor: "var(--ink)" } : null}>{children}</button>
);

const FsBoard = () => {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  const boot = useMemo(() => {
    const q = new URLSearchParams(window.location.search);
    const sc = q.get("scenario");
    const stp = q.get("step");
    return {
      path: VALID_PATHS.includes(sc) ? sc : null,
      step: DEEP_STEPS.includes(stp) ? stp : null,
    };
  }, []);

  const [path, setPath] = useState(boot.path || t.path);
  const [bootStep, setBootStep] = useState(boot.step || null);
  const [bootNonce, setBootNonce] = useState(0);
  const [curState, setCurState] = useState(boot.step || "welcome");
  const [notesOpen, setNotesOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const ref = useRef(null);

  useEffect(() => { if (t.path !== path) { setPath(t.path); setBootStep(null); } }, [t.path]);
  useEffect(() => { document.documentElement.setAttribute("data-palette", t.palette); }, [t.palette]);

  useEffect(() => {
    const q = new URLSearchParams(window.location.search);
    q.set("scenario", path);
    if (curState && curState !== "welcome") q.set("step", curState); else q.delete("step");
    window.history.replaceState(null, "", window.location.pathname + "?" + q.toString());
  }, [path, curState]);

  useEffect(() => { document.documentElement.setAttribute("data-build-id", BUILD.id); }, []);

  const pick = (v) => { setPath(v); setBootStep(null); setTweak("path", v); };
  const openState = (sc, stp) => { setTweak("path", sc); setPath(sc); setBootStep(stp); setBootNonce((n) => n + 1); setNotesOpen(false); };
  const copyLink = () => { try { navigator.clipboard && navigator.clipboard.writeText(window.location.href); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1600); };
  const info = pathInfo(path);

  return (
    <>
      <div className="fs-root" data-palette={t.palette} style={{ "--accent": t.accent }}>
        {/* slim utility bar — the prototype controls live here so the flow stays full-bleed */}
        <div className="fs-topbar">
          <Logo size={18} color="var(--ink)" />
          <span className="fs-title">Onboarding · unhappy paths</span>
          <span className="fs-divider" />
          <div className="wc-select fs-scenario">
            <select value={path} data-testid="scenario-select" onChange={(e) => pick(e.target.value)}>
              {PATHS.map((p) => <option key={p.id} value={p.id}>{p.cap}</option>)}
            </select>
            <span className="wc-caret"><Icon name="down" size={14} color="var(--ink-3)" /></span>
          </div>
          <CtrlBtn testid="restart" onClick={() => ref.current && ref.current.restart()}>Restart</CtrlBtn>
          <CtrlBtn primary testid="jump-to-outcome" onClick={() => ref.current && ref.current.jump()}>Jump to outcome →</CtrlBtn>
          <span className="fs-spacer" />
          <span data-testid="state-chip" className="fs-state">state · {curState}</span>
          <CtrlBtn testid="copy-link" onClick={copyLink}>{copied ? "Copied ✓" : "Copy link"}</CtrlBtn>
          <CtrlBtn testid="open-notes" onClick={() => setNotesOpen(true)}>Inspector notes</CtrlBtn>
        </div>

        {/* the full-bleed, scaled-up flow surface */}
        <FsDevice ref={ref} key={path + ":" + bootNonce} path={path} palette={t.palette} accent={t.accent} initialStep={bootStep} onStep={setCurState} />
      </div>

      <NotesPanel open={notesOpen} onClose={() => setNotesOpen(false)} onOpenState={openState} path={path} />

      <TweaksPanel>
        <TweakSection label="Path" />
        <TweakSelect label="Path" value={path} options={PATHS.map((p) => ({ value: p.id, label: p.cap }))} onChange={pick} />
        <TweakSection label="Screen" />
        <TweakSelect label="Jump to" value={DEEP_STEPS.includes(curState) ? curState : "welcome"} options={SCREEN_OPTIONS} onChange={(s) => openState(path, s)} />
        <TweakSection label="Theme" />
        <TweakRadio label="Palette" value={t.palette} options={["bone", "graphite"]} onChange={(v) => setTweak({ palette: v, accent: v === "graphite" ? "#deb24e" : "#4a6b3f" })} />
        <TweakColor label="Accent" value={t.accent} options={t.palette === "graphite" ? ["#deb24e", "#8fa490", "#b8865a", "#7a98b8"] : ["#4a6b3f", "#a85a3e", "#3b6ea5", "#7a5c8a"]} onChange={(v) => setTweak("accent", v)} />
      </TweaksPanel>
    </>
  );
};

ReactDOM.createRoot(document.getElementById("board")).render(<FsBoard />);
