/* compare.jsx — side-by-side onboarding paths.
   Two independent, fully-tappable phones. Each runs the real onboarding flow
   but can be pointed at a different outcome: the happy path, or one of three
   unhappy branches that route the user into a different end state.

   Chrome (StatusBar / ProgressRail / TabBar / Toast) is lifted from the
   single-device shell so the phones look identical to the live flow. */

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

/* ---------- progress rail ------------------------------------------------- */
const PHASES = ["Verify", "Profile", "Secure", "Connect"];
const ProgressRail = ({ phase, fill, mins, onBack, canBack }) => (
  <div style={{ flex: "none", padding: "4px 18px 12px", background: "var(--bg)" }}>
    <div style={{ display: "flex", gap: 6 }}>
      {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: 9, minHeight: 18 }}>
      {canBack ? (
        <button className="press" onClick={onBack} aria-label="Back" style={{ background: "none", border: "none", padding: 0, margin: "0 8px 0 0", display: "flex", color: "var(--ink-2)", cursor: "pointer" }}>
          <Icon name="back" size={17} color="var(--ink-2)" />
        </button>
      ) : null}
      <span style={{ fontFamily: "var(--f-display)", fontSize: 10.5, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-2)" }}>{PHASES[phase]}</span>
      <span style={{ marginLeft: "auto", fontFamily: "var(--f-mono)", fontSize: 11, color: "var(--ink-3)" }}>≈ {mins} min</span>
    </div>
  </div>
);

/* ---------- tab bar (happy path only, once in the live app) --------------- */
const TABS = [["home", "Home"], ["trade", "Trade"], ["chat", "Yoshi"], ["studio", "Studio"], ["accounts", "Accounts"]];
const TabBar = ({ tab, onTab }) => (
  <div className="tabbar">
    {TABS.map(([id, label]) => {
      const on = id === tab;
      if (id === "chat") {
        return (
          <button key={id} className="tab press" onClick={() => onTab(id)} style={{ overflow: "visible" }}>
            <span style={{ position: "absolute", top: -26, left: "50%", transform: "translateX(-50%)", width: 58, height: 58, borderRadius: 999, background: "var(--bg-card)", border: on ? "1.5px solid var(--accent)" : "1px solid var(--rule)", boxShadow: "0 8px 22px -8px rgba(0,0,0,0.32), 0 2px 6px -2px rgba(0,0,0,0.18)", display: "grid", placeItems: "center" }}>
              <Logo size={26} />
            </span>
            <span className="tlabel" style={{ marginTop: 34, color: on ? "var(--ink)" : "var(--ink-3)", fontWeight: on ? 700 : 500 }}>{label}</span>
          </button>
        );
      }
      return (
        <button key={id} className="tab press" onClick={() => onTab(id)}>
          {on && <span style={{ position: "absolute", top: -9, left: "50%", transform: "translateX(-50%)", width: 18, height: 2, background: "var(--accent)" }} />}
          <div style={{ position: "relative", height: 23, display: "flex", alignItems: "center" }}>
            <Icon name={id} size={23} stroke={on ? 1.7 : 1.5} color={on ? "var(--ink)" : "var(--ink-3)"} />
          </div>
          <span className="tlabel" style={{ color: on ? "var(--ink)" : "var(--ink-3)", fontWeight: on ? 700 : 500 }}>{label}</span>
        </button>
      );
    })}
  </div>
);

const Toast = ({ msg }) => (
  <div style={{ position: "absolute", left: 16, right: 16, bottom: 0, zIndex: 350, background: "var(--terminal-fill)", color: "var(--terminal-ink)", padding: "12px 15px", 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, fontWeight: 500 }}>{msg}</span>
  </div>
);

/* =============================================================
   The flow, as a state machine. Order is the happy path; the
   unhappy branches diverge at named points.
   ============================================================= */
const ORDER = ["welcome", "verifyIntro", "plaid", "provision", "success", "income", "checks", "security", "funding", "link", "agent", "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 },
};
/* where each path comes to rest when you "jump to outcome" */
const REVIEW_PATHS = ["identity", "screening", "watchlist", "mismatch", "records"];
const TERMINAL = { happy: "offToWork", declined: "declined" };
REVIEW_PATHS.forEach((p) => { TERMINAL[p] = "reviewHold"; });

/* which paths flag during the automated KYC checks at provisioning, and the
   review reason each path lands the hold on */
const PROVISION_REVIEW = { watchlist: "watchlist", mismatch: "mismatch", records: "records" };
const REVIEW_REASON = { identity: "identity", screening: "screening", watchlist: "watchlist", mismatch: "mismatch", records: "records" };

/* =============================================================
   Build + spec metadata — so humans and coding agents inspecting
   this prototype know which export they're looking at, which
   behaviour is simulated, and how to reach every state. Mirrored
   onto window.__YOSHI_SPEC below for programmatic crawling.
   ============================================================= */
const BUILD = {
  id: "onb-unhappy-2026.06.13",
  exported: "2026-06-13",
  source: "Unhappy Paths · compare.jsx",
  channel: "design-prototype",
};

/* Human labels for every reachable state — also the set of valid ?step= values. */
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",
  offToWork: "Account live", reviewHold: "Manual review hold", reviewApproved: "Review approved · resume",
  declined: "Declined · hard stop",
};
const DEEP_STEPS = Object.keys(STEP_LABELS);

/* Simulated mechanics — labelled so inspectors don't treat demo timing or
   placeholder catalogs as v1 product / API requirements. */
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." },
];
/* States the inspection brief asked about that this build does NOT model. */
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" });

/* =============================================================
   Device — one phone running the flow on a chosen path.
   Fully self-contained: tap the real UI to walk it. The board
   can also restart it or jump it straight to its end state.
   ============================================================= */
const Device = React.forwardRef(({ path, palette, accent, initialStep, onStep }, ref) => {
  const [step, setStep] = useState(initialStep || "welcome");
  const [cleared, setCleared] = useState(false); // manual review resolved → don't re-route
  const [holdReason, setHoldReason] = useState(null); // which trigger sent us to review
  const [app, setApp] = useState(false);
  const [tab, setTab] = useState("home");
  const [toast, setToast] = useState(null);

  // flow data — seeded per path so the branch reads honestly
  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);
    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]);

  // surface the live state to the board (URL sync + state chip)
  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: () => {},
  }), []);

  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} deposit={deposit} setDeposit={setDeposit} onNext={() => goto("agent")} onSkip={() => goto("agent")} />,
    agent:       <Agent agents={agents} setAgents={setAgents} onNext={() => goto("offToWork")} onSkip={() => { setAgents([]); goto("offToWork"); }} />,
    offToWork:   <OffToWork agents={agents} accent={accent} onEnter={() => { setApp(true); setTab("home"); }} />,
    reviewHold:  <ReviewHold reason={holdReason || REVIEW_REASON[path] || "kyc"} phone={idv.phone} 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";
  const sbColor = (!app && (step === "offToWork" || step === "plaid")) ? "#f3f1ea" : "var(--ink)";

  return (
    <div className="bezel">
      <div className="island" />
      <div className="screen" data-palette={palette} data-testid="device-screen" data-scenario={path} data-state={app ? ("app:" + tab) : step} style={{ "--accent": accent, background: darkScreen ? "#141310" : undefined }}>
        <ThemeCtx.Provider value={palette}>
          <StatusBar color={sbColor} />
          {!app ? (
            <div className="viewport" style={{ position: "relative" }}>
              {showRail && <ProgressRail phase={meta.phase} fill={meta.fill} mins={mins} onBack={back} canBack />}
              <div key={step} className="tab-swap" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
                {screens[step]}
              </div>
              {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 className="viewport">
                <div key={tab} className="tab-swap" style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column" }}>
                  <HomeDayOne balance={deposit} goal={null} externals={externals} nav={nav} flash={flash}
                    onAddMoney={() => flash("Add money opens in the full app")}
                    onConnect={() => flash("Opens in the full app")}
                    onPickGoal={() => flash("Pick a focus in the full app")} />
                </div>
                {toast && <Toast msg={toast} />}
              </div>
              <TabBar tab={tab} onTab={nav.tab} />
            </>
          )}
        </ThemeCtx.Provider>
      </div>
    </div>
  );
});

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

/* Mirror the spec onto the window so coding agents can crawl scenarios,
   states and mocked behaviour without scraping the DOM. */
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 CtrlBtn = ({ children, onClick, primary, testid }) => (
  <button onClick={onClick} data-testid={testid} className="cv-btn" style={primary ? { background: "var(--cv-ink)", color: "var(--cv-bg)", borderColor: "var(--cv-ink)" } : null}>{children}</button>
);

/* =============================================================
   Inspector notes — build metadata, the scenario index (with deep-link
   shortcuts), and a labelled list of simulated behaviour. Lives outside
   the scaled board so its text stays crisp.
   ============================================================= */
const SpecRow = ({ k, v }) => (
  <div style={{ display: "grid", gridTemplateColumns: "122px 1fr", gap: 12, padding: "10px 0", borderTop: "1px dashed var(--cv-rule)" }}>
    <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--cv-ink-3)", lineHeight: 1.4 }}>{k}</div>
    <div style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--cv-ink)", textWrap: "pretty" }}>{v}</div>
  </div>
);
const NotesHead = ({ children }) => (
  <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--cv-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(20,19,15,0.28)" }} />
      <aside data-testid="mock-notes" style={{ position: "fixed", top: 0, right: 0, bottom: 0, zIndex: 401, width: "min(440px, 94vw)", background: "#f6f7f8", borderLeft: "1px solid var(--cv-rule)", boxShadow: "-24px 0 60px -30px rgba(0,0,0,0.4)", display: "flex", flexDirection: "column", fontFamily: '"Trebuchet MS", "Lucida Grande", Tahoma, sans-serif', color: "var(--cv-ink)" }}>
        <header style={{ flex: "none", padding: "18px 24px", borderBottom: "1px solid var(--cv-rule)", display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ width: 7, height: 7, borderRadius: 999, background: "var(--cv-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(--cv-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(--cv-rule)", background: "#fff", position: "relative" }}>
                {p.id === path && <span style={{ position: "absolute", top: 0, left: 0, right: 0, height: 2, background: "var(--cv-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(--cv-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(--cv-rule)", background: "#fff", borderRadius: 8, padding: "6px 10px", cursor: "pointer", whiteSpace: "nowrap" }}>Open →</button>
              </div>
            ))}
          </div>
          <div style={{ fontSize: 12, color: "var(--cv-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(--cv-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": "screening"
}/*EDITMODE-END*/;

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

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

  // deep link: ?scenario=watchlist&step=reviewHold
  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); // forces a fresh Device mount for deep-link jumps
  const [curState, setCurState] = useState(boot.step || "welcome");
  const [notesOpen, setNotesOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const ref = useRef(null);
  const scalerRef = useRef(null);

  useEffect(() => { if (t.path !== path) { setPath(t.path); setBootStep(null); } }, [t.path]);

  // keep the URL in sync so any state is deep-linkable and shareable
  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);

  useEffect(() => {
    const fit = () => {
      const el = scalerRef.current; if (!el) return;
      const W = 412 + 96;    // one phone + side padding
      const H = 1040;        // header + controls + bezel
      const s = Math.min(window.innerWidth / W, window.innerHeight / H, 1);
      el.style.transform = `scale(${s})`;
    };
    fit();
    window.addEventListener("resize", fit);
    const id = setTimeout(() => document.body.classList.add("anim-ready"), 800);
    return () => { window.removeEventListener("resize", fit); clearTimeout(id); };
  }, []);

  return (
    <>
      <div className="cv-stage" data-build-id={BUILD.id}>
        <div className="cv-scaler" ref={scalerRef}>
          {/* board header */}
          <div className="cv-head">
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <Logo size={20} color="var(--cv-ink)" />
              <span style={{ fontSize: 17, fontWeight: 700, letterSpacing: "-0.02em", color: "var(--cv-ink)" }}>Onboarding · unhappy paths</span>
            </div>
          </div>

          {/* controls — above the prototype */}
          <div style={{ width: 412, margin: "0 auto 18px", display: "flex", flexDirection: "column", gap: 12 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <div className="cv-select" style={{ flex: 1 }}>
                <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="cv-select-caret"><Icon name="down" size={14} color="var(--cv-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>
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
              <span style={{ width: 9, height: 9, borderRadius: 999, background: info.tone, flex: "none" }} />
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--cv-ink)" }}>{info.cap}</div>
                <div style={{ fontSize: 12.5, color: "var(--cv-ink-3)", marginTop: 2 }}>{info.desc}</div>
              </div>
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, paddingTop: 2 }}>
              <span data-testid="build-chip" style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.07em", textTransform: "uppercase", color: "var(--cv-ink-3)", border: "1px solid var(--cv-rule)", borderRadius: 6, padding: "4px 8px", background: "#fff" }}>{BUILD.id}</span>
              <span data-testid="state-chip" style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--cv-ink-3)" }}>state · {curState}</span>
              <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
                <CtrlBtn testid="copy-link" onClick={copyLink}>{copied ? "Copied ✓" : "Copy link"}</CtrlBtn>
                <CtrlBtn testid="open-notes" onClick={() => setNotesOpen(true)}>Inspector notes</CtrlBtn>
              </div>
            </div>
          </div>

          {/* the prototype */}
          <div style={{ display: "flex", justifyContent: "center" }}>
            <Device ref={ref} key={path + ":" + bootNonce} path={path} palette={t.palette} accent={t.accent} initialStep={bootStep} onStep={setCurState} />
          </div>
        </div>
      </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="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(<Board />);
