/* app.jsx — shell: campaign/session sidebar, step rail, prep/run modes,
   theme application, and the Tweaks panel. Depends on all other window globals. */

const { useState, useEffect, useMemo, useCallback, useRef } = React;

const TWEAK_DEFAULTS = {
  "dark": false,
  "accent": "oxblood",
  "font": "storybook",
  "density": "cozy",
  "layout": "checklist",
  "runLayout": "focus",
  "flourish": true,
  "apiKey": "",
  "model": "claude-haiku-4-5-20251001",
  "openaiApiKey": "",
};

const ACCENT_CHIPS = [
  ["#9a352e"], ["#4a7349"], ["#b07d22"], ["#7e54c8"], ["#2c7c8c"]
];

const ACCENT_NAMES = { "#9a352e": "oxblood", "#4a7349": "forest", "#b07d22": "gold", "#7e54c8": "violet", "#2c7c8c": "teal" };
const NAME_TO_HEX = { oxblood: "#9a352e", forest: "#4a7349", gold: "#b07d22", violet: "#7e54c8", teal: "#2c7c8c" };

const PREP_NOTES_TAB = {
  id: "prepNotes",
  short: "Prep notes",
  icon: "feather",
  synthetic: true,
  name: "prep notes",
  tagline: "Freeform space for ideas, research, and anything that doesn't fit the eight steps. Only visible during prep."
};

function stepHasContent(items) {
  return Array.isArray(items) && items.some((it) =>
    (it.title && it.title.trim()) || (it.body && it.body.trim()) || (Array.isArray(it.aspects) && it.aspects.length)
  );
}

// ── Campaign / session sidebar ───────────────────────────────────────────────
function Sidebar({ camp, session, onSelectSession, onAddSession, onRenameCampaign, onRenameBlurb, onRenameTone, onEditCampaign, onRenameSession, onDeleteSession, onReorderSessions, open, onClose, onCollapse, onCampaigns }) {
  const dragIndex = React.useRef(null);
  const [dragOver, setDragOver] = React.useState(null);

  const onDrop = (toIdx) => {
    const from = dragIndex.current;
    if (from == null || from === toIdx) { setDragOver(null); dragIndex.current = null; return; }
    const next = camp.sessions.slice();
    const [moved] = next.splice(from, 1);
    next.splice(toIdx, 0, moved);
    const renumbered = next.map((s, i) => ({ ...s, number: i + 1 }));
    onReorderSessions(renumbered);
    setDragOver(null); dragIndex.current = null;
  };
  return (
    <>
      <div className={`scrim-side${open ? " show" : ""}`} onClick={onClose} />
      <aside className={`side${open ? " open" : ""}`}>
        <div className="side-brand">
          <window.Icon name="flame" size={22} className="brand-flame" />
          <div className="side-brand-tt">
            <span className="brand-name">Emberwright</span>
            <span className="brand-sub">session prep</span>
          </div>
          <button type="button" className="side-collapse-btn" onClick={onCollapse} title="Collapse sidebar" aria-label="Collapse sidebar">
            <window.Icon name="back" size={16} />
          </button>
        </div>

        <window.Flourish w={180} />

        <div className="side-camp">
          <div className="side-camp-hd">
            <span className="side-label">Campaign</span>
            <window.IconButton name="pen" title="Edit campaign details" onClick={onEditCampaign} />
          </div>
          <window.EditableArea value={camp.name} minRows={1} className="camp-name" onChange={onRenameCampaign} />
          <window.EditableArea value={camp.blurb} minRows={1} className="camp-blurb" placeholder="A one-line description of the campaign…" onChange={onRenameBlurb} />
          <span className="side-label" style={{ marginTop: 4 }}>Tone &amp; themes</span>
          <window.EditableArea value={camp.tone} minRows={1} className="camp-blurb" placeholder="Mood, themes, and the feel you want every scene to carry…" onChange={onRenameTone} />
        </div>

        <div className="side-sessions">
          <div className="side-sessions-hd">
            <span className="side-label">Sessions</span>
            <window.IconButton name="plus" title="New session" onClick={onAddSession} />
          </div>
          <ul>
            {camp.sessions.map((s, i) =>
              <li key={s.id}
                className={`${s.id === session.id ? "active" : ""}${dragOver === i ? " drag-over" : ""}`}
                onDragOver={(e) => { e.preventDefault(); if (dragOver !== i) setDragOver(i); }}
                onDrop={() => onDrop(i)}>
                <button type="button" className="session-pick"
                  draggable
                  onDragStart={(e) => { dragIndex.current = i; e.dataTransfer.effectAllowed = "move"; }}
                  onDragEnd={() => { dragIndex.current = null; setDragOver(null); }}
                  onClick={() => onSelectSession(s.id)}>
                  <span className="session-no">#{s.number}</span>
                  <span className="session-nm">{s.name}</span>
                </button>
                {camp.sessions.length > 1 &&
                  <window.ConfirmDelete onConfirm={() => onDeleteSession(s.id)} label="Delete session" />
                }
              </li>
            )}
          </ul>
        </div>

        <div className="side-foot">
          <button type="button" className="side-camps-btn" onClick={() => { onClose(); onCampaigns(); }}>
            <window.Icon name="book" size={15} /> All campaigns
          </button>
        </div>
      </aside>
    </>
  );
}

// ── Campaign details modal ───────────────────────────────────────────────────
function CampaignModal({ camp, onChange, onClose }) {
  return (
    <window.Modal title="Campaign details" onClose={onClose} width={540}>
      <div className="camp-form">
        <label className="cf-row">
          <span className="cf-label">Name</span>
          <input className="cf-input" value={camp.name} onChange={(e) => onChange({ name: e.target.value })} />
        </label>
        <label className="cf-row">
          <span className="cf-label">Description</span>
          <textarea className="cf-area" rows={2} value={camp.blurb || ""}
            placeholder="A one-line pitch for the campaign."
            onChange={(e) => onChange({ blurb: e.target.value })} />
        </label>
        <label className="cf-row">
          <span className="cf-label">Tone &amp; themes</span>
          <textarea className="cf-area" rows={3} value={camp.tone || ""}
            placeholder="e.g. gothic and waterlogged; bargains, forgotten names, bittersweet wins. The mood you want every scene to carry."
            onChange={(e) => onChange({ tone: e.target.value })} />
          <span className="cf-hint">Carried into every AI suggestion and rewrite across the campaign.</span>
        </label>
      </div>
    </window.Modal>
  );
}

// ── Step rail (prep navigation) — simple tabs ────────────────────────────────
function StepRail({ tabs, data, activeStep, onPick }) {
  return (
    <nav className="rail">
      {tabs.map((s) => {
        const isActive = s.id === activeStep;
        const filled = !s.synthetic && stepHasContent(data[s.id]);
        return (
          <button type="button" key={s.id} className={`rail-tab${isActive ? " active" : ""}`}
            onClick={() => onPick(s.id)}>
            <window.Icon name={s.icon} size={17} className="rail-icon" />
            <span className="rail-name">{s.short}</span>
            {filled && <span className="rail-dot" title="Has content" />}
          </button>
        );
      })}
    </nav>
  );
}

// ── Step switcher (mobile prep navigation) — dropdown jump sheet ─────────────
function StepSwitcher({ tabs, data, activeStep, onPick }) {
  const [open, setOpen] = React.useState(false);
  const idx = Math.max(0, tabs.findIndex((s) => s.id === activeStep));
  const cur = tabs[idx] || tabs[0];
  const pick = (id) => { onPick(id); setOpen(false); };

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  return (
    <nav className="switcher">
      <div className="switcher-bar">
        <button type="button" className="sw-current" onClick={() => setOpen(true)} aria-haspopup="true" aria-expanded={open}>
          <window.Icon name={cur.icon} size={18} className="sw-cur-icon" />
          <span className="sw-cur-name">{cur.short}</span>
          <window.Icon name="chevron" size={14} className="sw-cur-chev" />
        </button>
      </div>

      {open &&
        <div className="sheet-scrim" onMouseDown={() => setOpen(false)}>
          <div className="sheet" onMouseDown={(e) => e.stopPropagation()} role="menu">
            <div className="sheet-grab" />
            <ul className="sheet-list">
              {tabs.map((s, i) => {
                const filled = !s.synthetic && stepHasContent(data[s.id]);
                return (
                  <li key={s.id}>
                    <button type="button" className={`sheet-item${s.id === activeStep ? " active" : ""}`} onClick={() => pick(s.id)} role="menuitem">
                      <window.Icon name={s.icon} size={18} className="sheet-icon" />
                      <span className="sheet-name">{s.short}</span>
                      {filled && <span className="sheet-check"><window.Icon name="check" size={14} sw={2.6} /></span>}
                    </button>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      }
    </nav>
  );
}

// ── Notes panes ───────────────────────────────────────────────────────────────
function PrepNotesPane({ session, onChange }) {
  return (
    <div className="step-pane">
      <div className="step-head">
        <div className="step-head-l">
          <span className="step-eyebrow">Prep only</span>
          <h2 className="step-title">prep notes</h2>
          <p className="step-tagline">Ideas, research threads, and anything that doesn't fit the eight steps. Only visible during prep — not shown at the table.</p>
        </div>
      </div>
      <div className="notes-pane">
        <window.EditableArea value={session.prepNotes} minRows={10}
          placeholder="Inspiration, lore you're developing, alternate scene ideas, player backstory hooks, things to research…"
          onChange={onChange} className="notes-text" />
      </div>
    </div>
  );
}

function SessionNotesPane({ session, onChange }) {
  return (
    <div className="notes-pane notes-run">
      <window.EditableArea value={session.notes} minRows={4}
        placeholder="Jot what happened, dropped clues, dangling threads, what to open with next time…"
        onChange={onChange} className="run-notes" />
    </div>
  );
}

// ── Top bar ──────────────────────────────────────────────────────────────────
function TopBar({ session, mode, setMode, onRenameSession, onMenu, onTweaks }) {
  return (
    <header className="topbar">
      <button type="button" className="menu-btn" onClick={onMenu} aria-label="Menu">
        <window.Icon name="dots" size={20} />
      </button>
      <div className="topbar-title">
        <span className="topbar-eyebrow">Session {session.number}</span>
        <window.EditableLine value={session.name} big className="session-title" onChange={onRenameSession} />
      </div>
      <div className="topbar-right">
        <div className="mode-toggle">
          <button type="button" className={mode === "prep" ? "on" : ""} onClick={() => setMode("prep")}>
            <window.Icon name="pen" size={15} /><span className="mode-label">Prep</span>
          </button>
          <button type="button" className={mode === "run" ? "on" : ""} onClick={() => setMode("run")}>
            <window.Icon name="play" size={15} /><span className="mode-label">Run</span>
          </button>
        </div>
        <window.AuthControls />
        <window.IconButton name="settings" title="Tweaks" onClick={onTweaks} size={18} />
      </div>
    </header>
  );
}

// ── Campaigns management screen ──────────────────────────────────────────────
function CampaignsScreen({ state, setState, onOpen, onClose }) {
  const [editing, setEditing] = useState(null);
  const [confirmDelete, setConfirmDelete] = useState(null);

  const addCampaign = () => {
    const blank = window.RPG.seedCampaign();
    blank.name = "New Campaign";
    blank.blurb = "";
    blank.sessions = [window.RPG.blankSession(1)];
    blank.id = window.RPG.uid();
    setState((st) => ({ ...st, campaigns: [...st.campaigns, blank], activeCampaign: blank.id, activeSession: blank.sessions[0].id }));
    setEditing(blank.id);
  };

  const deleteCampaign = (id) => {
    const remaining = state.campaigns.filter((c) => c.id !== id);
    if (remaining.length === 0) return;
    setState((st) => ({
      ...st,
      campaigns: remaining,
      activeCampaign: st.activeCampaign === id ? remaining[0].id : st.activeCampaign,
      activeSession: st.activeCampaign === id ? remaining[0].sessions[0].id : st.activeSession,
    }));
    setConfirmDelete(null);
  };

  const patchCamp = (id, changes) =>
    setState((st) => ({ ...st, campaigns: st.campaigns.map((c) => c.id === id ? { ...c, ...changes } : c) }));

  const stepsDone = (camp) => {
    const steps = window.RPG.STEP_DEFS;
    const latest = camp.sessions[camp.sessions.length - 1];
    if (!latest) return 0;
    return steps.filter((s) => (latest.data[s.id] || []).some((it) => it.body || it.title)).length;
  };

  return (
    <div className="camp-screen">
      <div className="camp-screen-hd">
        <div>
          <h1 className="camp-screen-title">Campaigns</h1>
          <p className="camp-screen-sub">Choose a campaign to prep, or start a new one.</p>
        </div>
        <button type="button" className="btn btn-accent" onClick={addCampaign}>
          <window.Icon name="plus" size={16} sw={2} /> New campaign
        </button>
      </div>

      <div className="camp-grid">
        {state.campaigns.map((c) => {
          const isActive = c.id === state.activeCampaign;
          const isEditing = editing === c.id;
          const done = stepsDone(c);
          return (
            <div key={c.id} className={`camp-card${isActive ? " current" : ""}`}>
              <div className="camp-card-hd">
                {isEditing ? (
                  <input className="camp-card-name-input" autoFocus value={c.name}
                    onChange={(e) => patchCamp(c.id, { name: e.target.value })}
                    onBlur={() => setEditing(null)}
                    onKeyDown={(e) => { if (e.key === "Enter") setEditing(null); }} />
                ) : (
                  <h2 className="camp-card-name" onClick={() => setEditing(c.id)}>{c.name || "Unnamed campaign"}</h2>
                )}
                {isActive && <span className="camp-badge">Active</span>}
              </div>
              <p className="camp-card-blurb">{c.blurb || <span className="run-muted">No description yet.</span>}</p>
              {c.tone && <p className="camp-card-tone"><window.Icon name="feather" size={13} /> {c.tone}</p>}
              <div className="camp-card-stats">
                <span><window.Icon name="scroll" size={14} /> {c.sessions.length} session{c.sessions.length !== 1 ? "s" : ""}</span>
                <span><window.Icon name="check" size={14} sw={2.4} /> {done}/8 last prep</span>
              </div>
              <div className="camp-card-actions">
                <button type="button" className="btn btn-accent btn-sm" onClick={() => onOpen(c.id)}>
                  <window.Icon name="play" size={14} /> Open
                </button>
                <button type="button" className="btn btn-sm" onClick={() => setEditing(c.id)}>
                  <window.Icon name="pen" size={14} /> Edit
                </button>
                {state.campaigns.length > 1 && (
                  <button type="button" className="btn btn-sm btn-ghost-danger"
                    onClick={() => setConfirmDelete(c.id)}>
                    <window.Icon name="trash" size={14} /> Delete
                  </button>
                )}
              </div>
            </div>
          );
        })}
      </div>

      {confirmDelete && (
        <window.Modal title="Delete campaign?" onClose={() => setConfirmDelete(null)} width={400}>
          <div style={{ display: "flex", flexDirection: "column", gap: 18, padding: "4px 0" }}>
            <p style={{ fontSize: 15, color: "var(--ink-soft)" }}>This will permanently delete the campaign and all its sessions. This cannot be undone.</p>
            <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
              <button type="button" className="btn btn-sm" onClick={() => setConfirmDelete(null)}>Cancel</button>
              <button type="button" className="btn btn-sm btn-accent" onClick={() => deleteCampaign(confirmDelete)}>Delete</button>
            </div>
          </div>
        </window.Modal>
      )}
    </div>
  );
}

function App() {
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [state, setState] = useState(null); // null until hydrated from IndexedDB
  const [mode, setMode] = useState("prep");
  const [view, setView] = useState("session");
  const [activeStep, setActiveStep] = useState("characters");
  const [sideOpen, setSideOpen] = useState(false);
  const [sideCollapsed, setSideCollapsed] = useState(false);
  const [campModal, setCampModal] = useState(false);
  const boardRefs = useRef({});

  // Hydrate from IndexedDB once; let the sync engine push fresh cloud state in.
  useEffect(() => {
    let cancelled = false;
    window.RPGDB.loadState().then((s) => { if (!cancelled) setState(s); });
    if (window.RPGSync) window.RPGSync.onRemoteState((tree) => { if (!cancelled) setState(tree); });
    return () => { cancelled = true; };
  }, []);

  // Debounced persistence: local mirror (IndexedDB) + cloud push (no-op if signed out).
  useEffect(() => {
    if (!state) return;
    const h = setTimeout(() => {
      window.RPGDB.saveState(state);
      if (window.RPGSync) window.RPGSync.pushState(state);
    }, 400);
    return () => clearTimeout(h);
  }, [state]);

  const camp = state ? (state.campaigns.find((c) => c.id === state.activeCampaign) || state.campaigns[0]) : null;
  const session = camp ? (camp.sessions.find((s) => s.id === state.activeSession) || camp.sessions[0]) : null;
  const steps = window.RPG.STEP_DEFS;

  const themeVars = useMemo(() => {
    const accent = typeof t.accent === "object" ? t.accent[0] : NAME_TO_HEX[t.accent] || t.accent;
    return window.RPGTheme.buildTheme({ ...t, accent });
  }, [t]);

  useEffect(() => {
    document.body.style.background = themeVars["--bg"];
    document.body.style.colorScheme = themeVars.colorScheme;
  }, [themeVars]);

  useEffect(() => {
    const tb = document.querySelector(".topbar");
    if (!tb) return;
    const set = () => {
      const h = tb.offsetHeight + "px";
      document.documentElement.style.setProperty("--topbar-h", h);
      const app = tb.closest(".app");
      if (app) app.style.setProperty("--topbar-h", h);
    };
    set();
    let ro;
    if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(set); ro.observe(tb); }
    window.addEventListener("resize", set);
    return () => { if (ro) ro.disconnect(); window.removeEventListener("resize", set); };
  }, [mode, t.density, t.font, session && session.name]);

  const updateCamp = (changes) => setState((st) => ({
    ...st, campaigns: st.campaigns.map((c) => c.id === camp.id ? { ...c, ...changes } : c)
  }));
  const updateSession = (next) => updateCamp({
    sessions: camp.sessions.map((s) => s.id === next.id ? next : s)
  });
  const setStepItems = (stepId, items) =>
    updateSession({ ...session, data: { ...session.data, [stepId]: items } });

  const addSession = () => {
    const num = Math.max(0, ...camp.sessions.map((s) => s.number)) + 1;
    const ns = window.RPG.blankSession(num);
    updateCamp({ sessions: [...camp.sessions, ns] });
    setState((st) => ({ ...st, activeSession: ns.id }));
    setMode("prep"); setActiveStep("characters"); setSideOpen(false);
  };
  const deleteSession = (id) => {
    const remaining = camp.sessions.filter((s) => s.id !== id);
    const renumbered = remaining.map((s, i) => ({ ...s, number: i + 1 }));
    updateCamp({ sessions: renumbered });
    if (session.id === id) setState((st) => ({ ...st, activeSession: renumbered[0].id }));
  };
  const reorderSessions = (reordered) => updateCamp({ sessions: reordered });
  const selectSession = (id) => { setState((st) => ({ ...st, activeSession: id })); setSideOpen(false); };

  const resetDemo = async () => {
    if (!window.confirm("Reset all data back to the sample campaign? This erases your sessions.")) return;
    await window.RPGDB.clearState();
    if (window.RPGSync) await window.RPGSync.clearCloud();
    const fresh = await window.RPGDB.loadState();
    setState(fresh); setMode("prep"); setActiveStep("characters");
  };

  const openTweaks = () => window.postMessage({ type: '__activate_edit_mode' }, '*');

  const ctx = session
    ? { campaign: camp.name, blurb: camp.blurb, tone: camp.tone, session: session.name, pcs: session.data.characters || [] }
    : {};
  const tabs = [...steps, PREP_NOTES_TAB];
  const isNotes = activeStep === "prepNotes";
  const onNotes = (v) => updateSession({ ...session, prepNotes: v });

  const toggleSidebar = () => {
    const side = document.querySelector(".side");
    const drawer = side && getComputedStyle(side).position === "fixed";
    if (drawer) setSideOpen((o) => !o); else setSideCollapsed((c) => !c);
  };

  useEffect(() => {
    if (t.layout !== "board") return;
    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue;
        const id = Object.keys(boardRefs.current).find((k) => boardRefs.current[k] === entry.target);
        if (id) setActiveStep(id);
        break;
      }
    }, { threshold: 0.3 });
    Object.values(boardRefs.current).forEach((el) => el && io.observe(el));
    return () => io.disconnect();
  }, [t.layout, session && session.id]);

  const pickStep = (id) => {
    setActiveStep(id);
    if (t.layout === "board") {
      const el = boardRefs.current[id];
      if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" });
    }
  };

  const activeDef = steps.find((s) => s.id === activeStep) || steps[0];
  const dev = t.previewSize || "desktop";

  // Loading gate — placed after all hooks so hook order stays stable.
  if (!state) {
    return <div className="app" style={themeVars}><div className="boot" /></div>;
  }

  return (
    <div className={`stage${dev !== "desktop" ? " stage-on" : ""}`} data-dev={dev}>
      <div className={`viewport${dev !== "desktop" ? " vp-frame vp-" + dev : ""}`}>
        <div className={`app${mode === "run" ? " is-run" : ""}${sideCollapsed ? " side-collapsed" : ""}${view === "campaigns" ? " view-campaigns" : ""}`}
          style={themeVars} data-flourish={t.flourish ? "1" : "0"}>

          {view === "campaigns" ? (
            <div className="main">
              <header className="topbar">
                <button type="button" className="menu-btn" style={{ display: "grid" }} onClick={() => setView("session")} aria-label="Back">
                  <window.Icon name="back" size={20} />
                </button>
                <div className="topbar-title">
                  <span className="topbar-eyebrow">Emberwright</span>
                  <span className="session-title" style={{ fontFamily: "var(--font-display)", fontWeight: 600 }}>Campaigns</span>
                </div>
                <div className="topbar-right">
                  <window.AuthControls />
                  <window.IconButton name="settings" title="Tweaks" onClick={openTweaks} size={18} />
                </div>
              </header>
              <CampaignsScreen state={state} setState={setState}
                onOpen={(id) => {
                  setState((st) => ({ ...st, activeCampaign: id, activeSession: state.campaigns.find(c => c.id === id)?.sessions[0]?.id || st.activeSession }));
                  setView("session");
                }}
                onClose={() => setView("session")} />
            </div>
          ) : (
            <>
              {view !== "campaigns" &&
                <Sidebar
                  camp={camp} session={session} open={sideOpen} onClose={() => setSideOpen(false)}
                  onSelectSession={selectSession} onAddSession={addSession} onDeleteSession={deleteSession}
                  onRenameCampaign={(v) => updateCamp({ name: v })}
                  onRenameBlurb={(v) => updateCamp({ blurb: v })}
                  onRenameTone={(v) => updateCamp({ tone: v })}
                  onEditCampaign={() => setCampModal(true)}
                  onCollapse={() => setSideCollapsed(true)}
                  onCampaigns={() => { setSideOpen(false); setView("campaigns"); }}
                  onReorderSessions={reorderSessions}
                  onRenameSession={(v) => updateSession({ ...session, name: v })} />
              }
              <div className="main">
                <TopBar
                  session={session} mode={mode} setMode={setMode}
                  onRenameSession={(v) => updateSession({ ...session, name: v })}
                  onMenu={toggleSidebar}
                  onTweaks={openTweaks} />

                {mode === "prep" ?
                  <div className={`prep prep-${t.layout}`}>
                    <StepRail tabs={tabs} data={session.data} activeStep={activeStep} onPick={pickStep} />
                    <StepSwitcher tabs={tabs} data={session.data} activeStep={activeStep} onPick={pickStep} />
                    <div className="prep-content">
                      {t.layout === "checklist" ?
                        isNotes ?
                          <PrepNotesPane session={session} onChange={onNotes} /> :
                          <window.StepEditor
                            key={activeDef.id} step={activeDef}
                            items={session.data[activeDef.id] || []}
                            setItems={(items) => setStepItems(activeDef.id, items)}
                            ctx={ctx} sessions={camp.sessions} sessionId={session.id} />
                        :
                        <>
                          {steps.map((s) =>
                            <div key={s.id} className="board-section" ref={(el) => boardRefs.current[s.id] = el}>
                              <window.StepEditor
                                step={s} items={session.data[s.id] || []}
                                setItems={(items) => setStepItems(s.id, items)} ctx={ctx}
                                sessions={camp.sessions} sessionId={session.id} />
                            </div>
                          )}
                          <div className="board-section" ref={(el) => boardRefs.current.prepNotes = el}>
                            <PrepNotesPane session={session} onChange={onNotes} />
                          </div>
                        </>
                      }
                    </div>
                  </div> :
                  <window.RunMode session={session} setSession={updateSession} ctx={ctx} layout={t.runLayout} />
                }
              </div>
              {campModal && <CampaignModal camp={camp} onChange={updateCamp} onClose={() => setCampModal(false)} />}
            </>
          )}
        </div>
      </div>

      <window.TweaksPanel title="Settings">
        <window.TweakSection label="Theme" />
        <window.TweakToggle label="Candlelight (dark)" value={t.dark} onChange={(v) => setTweak("dark", v)} />
        <window.TweakColor label="Accent" value={NAME_TO_HEX[t.accent] ? [NAME_TO_HEX[t.accent]] : t.accent}
          options={ACCENT_CHIPS}
          onChange={(v) => setTweak("accent", ACCENT_NAMES[v[0]] || v[0])} />
        <window.TweakSection label="Type" />
        <window.TweakSelect label="Lettering" value={t.font}
          options={[{ value: "storybook", label: "Storybook — Cinzel" }, { value: "tome", label: "Tome — IM Fell" }, { value: "quill", label: "Quill — Cormorant" }]}
          onChange={(v) => setTweak("font", v)} />
        <window.TweakSection label="Layout" />
        <window.TweakRadio label="Density" value={t.density} options={["compact", "cozy", "roomy"]}
          onChange={(v) => setTweak("density", v)} />
        <window.TweakRadio label="Run view" value={t.runLayout} options={[{ value: "focus", label: "Focus" }, { value: "board", label: "Board" }]}
          onChange={(v) => setTweak("runLayout", v)} />
        <window.TweakSection label="AI" />
        <window.TweakPassword label="Claude API key" value={t.apiKey} placeholder="sk-ant-…"
          onChange={(v) => setTweak("apiKey", v)} />
        <window.TweakSelect label="Model" value={t.model}
          options={[
            { value: "claude-haiku-4-5-20251001", label: "Haiku — fast" },
            { value: "claude-sonnet-4-6", label: "Sonnet — balanced" },
            { value: "claude-opus-4-8", label: "Opus — most capable" },
          ]}
          onChange={(v) => setTweak("model", v)} />
        <window.TweakSection label="Image AI" />
        <window.TweakPassword label="OpenAI API key" value={t.openaiApiKey} placeholder="sk-…"
          onChange={(v) => setTweak("openaiApiKey", v)} />
      </window.TweaksPanel>
    </div>
  );
}

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