/* ============================================================
   AI BOOK BUILDER  —  standalone build for static hosting
   Staged long-form writing studio: idea -> title/concept ->
   outline -> chapter plan -> section-by-section drafting.
   Generation is routed through ./api.php (your Anthropic key).
   No build step: this file is compiled in the browser by Babel.
   ============================================================ */

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

const STYLES = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&display=swap');

* { box-sizing: border-box; }
:root{
  --paper:#f3ecdc;
  --paper-2:#ece2cd;
  --card:#fcf9f1;
  --ink:#231c14;
  --ink-soft:#5b4f3d;
  --ink-faint:#8a7c64;
  --line:#d9ccae;
  --line-soft:#e6dcc4;
  --accent:#8c3522;
  --accent-soft:#b25438;
  --gold:#a9802f;
  --ok:#4f6b3a;
}
.abb-root{
  font-family:'Newsreader', Georgia, serif;
  color:var(--ink);
  background:
    radial-gradient(120% 90% at 100% 0%, rgba(169,128,47,0.07), transparent 55%),
    radial-gradient(120% 90% at 0% 100%, rgba(140,53,34,0.06), transparent 55%),
    var(--paper);
  min-height:100vh;
  -webkit-font-smoothing:antialiased;
}
.abb-root::before{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:0.5;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
}
.abb-wrap{position:relative;z-index:1;}

.serif-display{font-family:'Fraunces','Newsreader',serif;}
.label{font-family:'Fraunces',serif;text-transform:uppercase;letter-spacing:0.18em;font-size:11px;font-weight:600;color:var(--ink-faint);}

.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;
  padding:14px 26px;border-bottom:1px solid var(--line);
  background:rgba(243,236,220,0.85);backdrop-filter:blur(8px);}
.brand{display:flex;align-items:center;gap:12px;}
.brand-mark{width:34px;height:34px;border:1.5px solid var(--ink);border-radius:50%;
  display:grid;place-items:center;font-family:'Fraunces',serif;font-weight:600;font-size:17px;
  background:var(--card);}
.brand-name{font-family:'Fraunces',serif;font-weight:600;font-size:18px;letter-spacing:0.01em;}
.brand-sub{font-size:12px;color:var(--ink-faint);letter-spacing:0.04em;margin-top:-2px;}

.steps{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
.step-dot{font-family:'Fraunces',serif;font-size:11.5px;letter-spacing:0.04em;color:var(--ink-faint);
  padding:4px 10px;border-radius:999px;border:1px solid transparent;white-space:nowrap;cursor:default;}
.step-dot.active{color:var(--accent);border-color:var(--line);background:var(--card);}
.step-dot.done{color:var(--ink-soft);cursor:pointer;}

.stage{max-width:760px;margin:0 auto;padding:46px 26px 90px;}
.stage.wide{max-width:1180px;}
.fade-up{animation:fadeUp .5s cubic-bezier(.2,.7,.2,1) both;}
@keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.stagger > *{animation:fadeUp .55s cubic-bezier(.2,.7,.2,1) both;}
.stagger > *:nth-child(1){animation-delay:.04s}
.stagger > *:nth-child(2){animation-delay:.12s}
.stagger > *:nth-child(3){animation-delay:.2s}
.stagger > *:nth-child(4){animation-delay:.28s}
.stagger > *:nth-child(5){animation-delay:.36s}

.eyebrow{font-family:'Fraunces',serif;text-transform:uppercase;letter-spacing:0.32em;font-size:12px;color:var(--accent);font-weight:600;}
h1.title{font-family:'Fraunces',serif;font-weight:500;font-size:clamp(34px,6vw,58px);line-height:1.02;margin:14px 0 0;letter-spacing:-0.01em;}
.lede{font-size:19px;line-height:1.5;color:var(--ink-soft);max-width:54ch;margin-top:18px;}
h2.sec{font-family:'Fraunces',serif;font-weight:500;font-size:clamp(26px,4vw,38px);margin:0;letter-spacing:-0.01em;}

.card{background:var(--card);border:1px solid var(--line);border-radius:4px;padding:22px 24px;
  box-shadow:0 1px 0 rgba(35,28,20,0.03), 0 18px 40px -34px rgba(35,28,20,0.5);}
.choice{cursor:pointer;text-align:left;transition:transform .18s ease, box-shadow .25s ease, border-color .2s;}
.choice:hover{transform:translateY(-3px);box-shadow:0 1px 0 rgba(35,28,20,0.03),0 26px 50px -32px rgba(35,28,20,0.55);border-color:var(--gold);}
.choice.sel{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent),0 18px 40px -34px rgba(140,53,34,0.5);}

.btn{font-family:'Fraunces',serif;font-weight:600;letter-spacing:0.02em;cursor:pointer;border-radius:3px;
  border:1px solid var(--ink);background:var(--ink);color:var(--paper);padding:12px 22px;font-size:15px;
  transition:transform .14s ease,opacity .2s,background .2s;}
.btn:hover{transform:translateY(-1px);}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none;}
.btn.accent{background:var(--accent);border-color:var(--accent);}
.btn.ghost{background:transparent;color:var(--ink);}
.btn.ghost:hover{background:var(--paper-2);}
.btn.sm{padding:7px 13px;font-size:13px;}
.btn.tiny{padding:5px 10px;font-size:12px;border-radius:3px;}

.ilabel{display:block;margin-bottom:7px;}
.input,.area,.select{width:100%;font-family:'Newsreader',serif;font-size:16px;color:var(--ink);
  background:var(--card);border:1px solid var(--line);border-radius:3px;padding:11px 13px;outline:none;
  transition:border-color .2s, box-shadow .2s;}
.input:focus,.area:focus,.select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(140,53,34,0.1);}
.area{resize:vertical;line-height:1.55;}
.hint{font-size:13px;color:var(--ink-faint);margin-top:6px;}

.field{margin-bottom:18px;}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
@media(max-width:640px){.grid2{grid-template-columns:1fr;}}

.pill{display:inline-flex;align-items:center;gap:7px;font-family:'Fraunces',serif;font-size:12px;letter-spacing:0.04em;
  padding:6px 12px;border-radius:999px;border:1px solid var(--line);background:var(--card);color:var(--ink-soft);cursor:pointer;}
.pill.on{background:var(--ink);color:var(--paper);border-color:var(--ink);}

.divider{height:1px;background:var(--line);margin:30px 0;}
.row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
.spread{justify-content:space-between;}

.spin{display:inline-block;width:15px;height:15px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px;}
@keyframes sp{to{transform:rotate(360deg)}}
.dots::after{content:"";animation:dt 1.4s steps(4,end) infinite;}
@keyframes dt{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}}

.err{background:#fbeee9;border:1px solid var(--accent-soft);color:var(--accent);padding:11px 14px;border-radius:3px;font-size:14px;}

.ws{display:grid;grid-template-columns:300px 1fr;gap:30px;align-items:start;}
@media(max-width:880px){.ws{grid-template-columns:1fr;}}
.rail{position:sticky;top:78px;}
.chap-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:3px;cursor:pointer;
  border:1px solid transparent;transition:background .15s,border-color .15s;}
.chap-item:hover{background:var(--paper-2);}
.chap-item.sel{background:var(--card);border-color:var(--line);}
.statusdot{width:9px;height:9px;border-radius:50%;flex:none;border:1.5px solid var(--ink-faint);}
.statusdot.part{background:var(--gold);border-color:var(--gold);}
.statusdot.done{background:var(--ok);border-color:var(--ok);}
.chap-no{font-family:'Fraunces',serif;font-size:12px;color:var(--ink-faint);width:22px;flex:none;}
.chap-t{font-size:14.5px;line-height:1.25;overflow:hidden;text-overflow:ellipsis;}

.prog{height:7px;background:var(--paper-2);border-radius:999px;overflow:hidden;border:1px solid var(--line-soft);}
.prog > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--gold));border-radius:999px;transition:width .5s ease;}

.section-block{position:relative;}
.manuscript .area{font-size:17.5px;line-height:1.72;border-color:transparent;background:transparent;padding:6px 2px;}
.manuscript .area:focus{background:var(--card);border-color:var(--line);padding:11px 13px;}
.section-tools{opacity:0;transition:opacity .2s;display:flex;gap:6px;}
.section-block:hover .section-tools{opacity:1;}

.optcard{padding:16px 18px;}
.optcard p{margin:0;font-size:16px;line-height:1.55;}
.small{font-size:13px;color:var(--ink-faint);}
.kbd{font-family:'Fraunces',serif;font-size:12px;color:var(--ink-faint);}
a.link{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--line);}
`;

/* ----------------------------- API ----------------------------- */
const API_URL = "./api.php";          // PHP proxy on your Hostinger domain
const MODEL = "claude-sonnet-4-20250514";

async function callClaude({ system, user, maxTokens = 1200, maxRetries = 1 }) {
  let lastErr;
  for (let i = 0; i <= maxRetries; i++) {
    try {
      const res = await fetch(API_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          model: MODEL,
          max_tokens: maxTokens,
          system,
          messages: [{ role: "user", content: user }],
        }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) {
        const msg = data && data.error && data.error.message ? data.error.message : "Request failed (" + res.status + ")";
        throw new Error(msg);
      }
      const text = (data.content || [])
        .filter((b) => b.type === "text")
        .map((b) => b.text)
        .join("\n")
        .trim();
      if (!text) throw new Error("Empty response");
      return text;
    } catch (e) {
      lastErr = e;
    }
  }
  throw lastErr || new Error("Generation failed");
}

function stripFences(t) {
  return t.replace(/```(?:json)?/gi, "").trim();
}
function safeParse(t) {
  const cleaned = stripFences(t);
  try { return JSON.parse(cleaned); } catch (_) {}
  const m = cleaned.match(/[\[{][\s\S]*[\]}]/);
  if (m) { try { return JSON.parse(m[0]); } catch (_) {} }
  return null;
}
function wordCount(s) {
  if (!s) return 0;
  const m = s.trim().match(/\S+/g);
  return m ? m.length : 0;
}
function chapterWords(ch) {
  return (ch.sections || []).reduce((a, s) => a + wordCount(s.text), 0);
}
function fmt(n) { return n.toLocaleString(); }
function uid() { return Math.random().toString(36).slice(2, 9); }

/* ----------------------------- storage (localStorage) ----------------------------- */
const KEY = "abb:project:v2";
function loadProject() {
  try {
    const v = localStorage.getItem(KEY);
    if (v) return JSON.parse(v);
  } catch (_) {}
  return null;
}
function saveProject(p) {
  try { localStorage.setItem(KEY, JSON.stringify(p)); } catch (_) {}
}

function blankProject() {
  return {
    id: uid(),
    stage: "welcome",
    mode: "guided",
    setup: {
      type: "fiction",
      titleChoice: "generate",
      titleInput: "",
      conceptChoice: "generate",
      conceptInput: "",
      chapterCount: 12,
      targetWords: 70000,
      genre: "",
      audience: "",
      tone: "",
      themes: "",
      style: "",
      outlineMode: "auto",
    },
    titleOptions: [],
    title: "",
    conceptOptions: [],
    concept: "",
    outline: "",
    chapters: [],
    activeChapter: 0,
    createdAt: Date.now(),
  };
}

function bookContext(p) {
  const s = p.setup;
  const lines = [
    `Project type: ${s.type === "fiction" ? "Fiction" : "Nonfiction"}`,
    s.genre && `Genre/category: ${s.genre}`,
    s.audience && `Intended audience: ${s.audience}`,
    s.tone && `Tone: ${s.tone}`,
    s.themes && `Themes / subject focus: ${s.themes}`,
    s.style && `Writing style: ${s.style}`,
    p.title && `Working title: "${p.title}"`,
    p.concept && `Concept / premise: ${p.concept}`,
    `Planned length: about ${fmt(s.targetWords)} words across ${s.chapterCount} ${
      s.type === "fiction" ? "chapters" : "chapters/sections"
    }.`,
  ].filter(Boolean);
  return lines.join("\n");
}

const STEPS = [
  ["mode", "Mode"],
  ["setup", "Setup"],
  ["titleconcept", "Title & Concept"],
  ["outline", "Outline"],
  ["chapterplan", "Chapters"],
  ["workspace", "Draft"],
];

/* ================================ APP ================================ */
function App() {
  const [p, setP] = useState(null);
  const [booting, setBooting] = useState(true);
  const saveTimer = useRef(null);

  useEffect(() => {
    const saved = loadProject();
    setP(saved || blankProject());
    setBooting(false);
  }, []);

  useEffect(() => {
    if (!p) return;
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => saveProject(p), 500);
  }, [p]);

  const update = useCallback((patch) => {
    setP((prev) => ({ ...prev, ...(typeof patch === "function" ? patch(prev) : patch) }));
  }, []);
  const setStage = useCallback((stage) => update({ stage }), [update]);

  if (booting || !p) {
    return (
      <div className="abb-root">
        <style>{STYLES}</style>
        <div className="stage" style={{ paddingTop: 120, textAlign: "center", color: "var(--ink-faint)" }}>
          <span className="spin" /> &nbsp;Opening your studio<span className="dots" />
        </div>
      </div>
    );
  }

  const stepIndex = STEPS.findIndex((s) => s[0] === p.stage);

  return (
    <div className="abb-root">
      <style>{STYLES}</style>
      <div className="abb-wrap">
        <TopBar p={p} stepIndex={stepIndex} onStep={setStage} onReset={() => {
          if (window.confirm("Start a brand-new book? Your current project will be cleared.")) {
            const np = blankProject();
            setP(np);
            saveProject(np);
          }
        }} />

        {p.stage === "welcome" && <Welcome onStart={() => setStage("mode")} p={p} />}
        {p.stage === "mode" && <ModeSelect p={p} update={update} onNext={() => setStage("setup")} />}
        {p.stage === "setup" && <Setup p={p} update={update} onNext={() => setStage("titleconcept")} />}
        {p.stage === "titleconcept" && <TitleConcept p={p} update={update} onNext={() => setStage("outline")} onBack={() => setStage("setup")} />}
        {p.stage === "outline" && <OutlineStage p={p} update={update} onNext={() => setStage("chapterplan")} onBack={() => setStage("titleconcept")} />}
        {p.stage === "chapterplan" && <ChapterPlan p={p} update={update} onNext={() => setStage("workspace")} onBack={() => setStage("outline")} />}
        {p.stage === "workspace" && <Workspace p={p} update={update} onBack={() => setStage("chapterplan")} />}
      </div>
    </div>
  );
}

/* ------------------------------ TopBar ------------------------------ */
function TopBar({ p, stepIndex, onStep, onReset }) {
  const total = p.setup.targetWords || 0;
  const written = (p.chapters || []).reduce((a, c) => a + chapterWords(c), 0);
  return (
    <div className="topbar">
      <div className="brand">
        <div className="brand-mark">B</div>
        <div>
          <div className="brand-name">AI Book Builder</div>
          <div className="brand-sub">idea &middot; outline &middot; manuscript</div>
        </div>
      </div>
      {p.stage !== "welcome" && (
        <div className="steps">
          {STEPS.map((s, i) => (
            <button
              key={s[0]}
              className={"step-dot " + (i === stepIndex ? "active" : i < stepIndex ? "done" : "")}
              onClick={() => i <= stepIndex && onStep(s[0])}
              title={i <= stepIndex ? "Go to " + s[1] : "Locked"}
            >
              {s[1]}
            </button>
          ))}
        </div>
      )}
      <div className="row" style={{ gap: 16 }}>
        {p.stage === "workspace" && (
          <div style={{ textAlign: "right" }}>
            <div className="label" style={{ marginBottom: 2 }}>Manuscript</div>
            <div style={{ fontFamily: "'Fraunces',serif", fontSize: 14 }}>
              {fmt(written)} / {fmt(total)}
            </div>
          </div>
        )}
        <button className="btn ghost sm" onClick={onReset}>New</button>
      </div>
    </div>
  );
}

/* ------------------------------ Welcome ------------------------------ */
function Welcome({ onStart, p }) {
  const has = p.chapters.length > 0 || p.title || p.concept;
  return (
    <div className="stage stagger fade-up" style={{ paddingTop: 70 }}>
      <div className="eyebrow">A studio for long-form writing</div>
      <h1 className="title serif-display">
        Write the whole book —<br />one deliberate stage at a time.
      </h1>
      <p className="lede">
        Plan and draft fiction or nonfiction up to <strong>120,000 words</strong>. We move from idea to
        title, to a working outline, to chapter summaries, and finally into a drafting desk where the
        manuscript grows section by section — so nothing is dashed off in a single breathless block.
      </p>
      <div className="row" style={{ marginTop: 30, gap: 14 }}>
        <button className="btn accent" onClick={onStart}>{has ? "Continue" : "Begin a book"} →</button>
        {has && <span className="small">A project is in progress — your place is saved in this browser.</span>}
      </div>
      <div className="divider" />
      <div className="grid2" style={{ gap: 22 }}>
        <div className="card">
          <div className="label">Staged, not rushed</div>
          <p style={{ margin: "8px 0 0", lineHeight: 1.55 }}>
            Onboarding, then title &amp; concept, then outline, then chapter summaries, then drafting.
            Each result is yours to edit before the next step.
          </p>
        </div>
        <div className="card">
          <div className="label">Consistent to the end</div>
          <p style={{ margin: "8px 0 0", lineHeight: 1.55 }}>
            Every chapter is drafted with the outline and the summaries of what came before it in view,
            keeping voice, plot, and argument coherent across the manuscript.
          </p>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------ Mode ------------------------------ */
function ModeSelect({ p, update, onNext }) {
  const set = (mode) => update({ mode });
  return (
    <div className="stage fade-up">
      <div className="eyebrow">Step one</div>
      <h2 className="sec serif-display" style={{ marginTop: 10 }}>How would you like to work?</h2>
      <p className="lede" style={{ fontSize: 17 }}>
        You can change nothing about your authorship — both modes let you edit every result.
      </p>
      <div className="grid2" style={{ marginTop: 26 }}>
        <button className={"card choice " + (p.mode === "guided" ? "sel" : "")} onClick={() => set("guided")}>
          <div className="label" style={{ color: "var(--accent)" }}>Guided Mode</div>
          <h3 className="serif-display" style={{ fontSize: 24, margin: "8px 0 6px", fontWeight: 500 }}>Decide as you go</h3>
          <p style={{ margin: 0, lineHeight: 1.55, color: "var(--ink-soft)" }}>
            The studio offers several options at each step — pick titles, concepts, and beats yourself, and
            confirm before moving on. Best when you have strong instincts.
          </p>
        </button>
        <button className={"card choice " + (p.mode === "generate" ? "sel" : "")} onClick={() => set("generate")}>
          <div className="label" style={{ color: "var(--gold)" }}>Generate Mode</div>
          <h3 className="serif-display" style={{ fontSize: 24, margin: "8px 0 6px", fontWeight: 500 }}>Draft me a starting point</h3>
          <p style={{ margin: 0, lineHeight: 1.55, color: "var(--ink-soft)" }}>
            The studio proposes a single strong version of each stage automatically. You refine and revise
            rather than choose. Best for momentum.
          </p>
        </button>
      </div>
      <div className="row" style={{ marginTop: 30 }}>
        <button className="btn accent" onClick={onNext}>Continue →</button>
      </div>
    </div>
  );
}

/* ------------------------------ Setup ------------------------------ */
function Setup({ p, update, onNext }) {
  const s = p.setup;
  const setS = (patch) => update((prev) => ({ setup: { ...prev.setup, ...patch } }));

  const clampWords = (v) => Math.max(1000, Math.min(120000, v || 0));
  const clampCh = (v) => Math.max(1, Math.min(60, v || 0));

  const ready = s.genre.trim().length > 0;

  return (
    <div className="stage fade-up">
      <div className="eyebrow">Step two</div>
      <h2 className="sec serif-display" style={{ marginTop: 10 }}>Tell the studio about your book</h2>
      <p className="hint" style={{ fontSize: 14, marginBottom: 24 }}>Everything here shapes generation — and everything stays editable later.</p>

      <div className="field">
        <span className="label ilabel">Project type</span>
        <div className="row">
          {[["fiction", "Fiction"], ["nonfiction", "Nonfiction"]].map(([v, l]) => (
            <span key={v} className={"pill " + (s.type === v ? "on" : "")} onClick={() => setS({ type: v })}>{l}</span>
          ))}
        </div>
      </div>

      <div className="grid2">
        <div className="field">
          <span className="label ilabel">Title</span>
          <div className="row" style={{ marginBottom: 9 }}>
            <span className={"pill " + (s.titleChoice === "generate" ? "on" : "")} onClick={() => setS({ titleChoice: "generate" })}>Generate one for me</span>
            <span className={"pill " + (s.titleChoice === "enter" ? "on" : "")} onClick={() => setS({ titleChoice: "enter" })}>I'll enter my own</span>
          </div>
          {s.titleChoice === "enter" && (
            <input className="input" placeholder="Your title" value={s.titleInput} onChange={(e) => setS({ titleInput: e.target.value })} />
          )}
        </div>
        <div className="field">
          <span className="label ilabel">Concept / premise</span>
          <div className="row" style={{ marginBottom: 9 }}>
            <span className={"pill " + (s.conceptChoice === "generate" ? "on" : "")} onClick={() => setS({ conceptChoice: "generate" })}>Generate one for me</span>
            <span className={"pill " + (s.conceptChoice === "enter" ? "on" : "")} onClick={() => setS({ conceptChoice: "enter" })}>I'll write my own</span>
          </div>
          {s.conceptChoice === "enter" && (
            <textarea className="area" rows={3} placeholder="A sentence or two about the core idea" value={s.conceptInput} onChange={(e) => setS({ conceptInput: e.target.value })} />
          )}
        </div>
      </div>

      <div className="grid2">
        <div className="field">
          <span className="label ilabel">Number of chapters</span>
          <input className="input" type="number" min={1} max={60} value={s.chapterCount}
            onChange={(e) => setS({ chapterCount: clampCh(parseInt(e.target.value)) })} />
          <div className="hint">≈ {fmt(Math.round((s.targetWords || 0) / Math.max(1, s.chapterCount)))} words each</div>
        </div>
        <div className="field">
          <span className="label ilabel">Total target word count</span>
          <input className="input" type="number" min={1000} max={120000} step={1000} value={s.targetWords}
            onChange={(e) => setS({ targetWords: clampWords(parseInt(e.target.value)) })} />
          <input type="range" min={5000} max={120000} step={1000} value={s.targetWords} style={{ width: "100%", marginTop: 10, accentColor: "var(--accent)" }}
            onChange={(e) => setS({ targetWords: clampWords(parseInt(e.target.value)) })} />
          <div className="hint">Max 120,000 words.</div>
        </div>
      </div>

      <div className="grid2">
        <div className="field">
          <span className="label ilabel">{s.type === "fiction" ? "Genre" : "Category"} *</span>
          <input className="input" placeholder={s.type === "fiction" ? "e.g. literary thriller, cozy fantasy" : "e.g. memoir, business, popular science"} value={s.genre} onChange={(e) => setS({ genre: e.target.value })} />
        </div>
        <div className="field">
          <span className="label ilabel">Audience</span>
          <input className="input" placeholder="e.g. adult readers of upmarket fiction" value={s.audience} onChange={(e) => setS({ audience: e.target.value })} />
        </div>
      </div>

      <div className="grid2">
        <div className="field">
          <span className="label ilabel">Tone</span>
          <input className="input" placeholder="e.g. wry and intimate; urgent; warm" value={s.tone} onChange={(e) => setS({ tone: e.target.value })} />
        </div>
        <div className="field">
          <span className="label ilabel">Writing style</span>
          <input className="input" placeholder="e.g. spare prose; lyrical; journalistic" value={s.style} onChange={(e) => setS({ style: e.target.value })} />
        </div>
      </div>

      <div className="field">
        <span className="label ilabel">Themes / subject focus</span>
        <textarea className="area" rows={2} placeholder="e.g. memory and forgiveness; the cost of ambition" value={s.themes} onChange={(e) => setS({ themes: e.target.value })} />
      </div>

      <div className="field">
        <span className="label ilabel">Outline approach</span>
        <div className="row">
          <span className={"pill " + (s.outlineMode === "auto" ? "on" : "")} onClick={() => setS({ outlineMode: "auto" })}>Automatic generation</span>
          <span className={"pill " + (s.outlineMode === "guided" ? "on" : "")} onClick={() => setS({ outlineMode: "guided" })}>Guided outlining</span>
        </div>
        <div className="hint">Automatic drafts a complete outline; guided builds it with you, then you edit freely.</div>
      </div>

      <div className="row spread" style={{ marginTop: 24 }}>
        <span className="small">{ready ? "Ready when you are." : "A genre or category is needed to continue."}</span>
        <button className="btn accent" disabled={!ready} onClick={onNext}>Build title &amp; concept →</button>
      </div>
    </div>
  );
}

/* ------------------------- Title & Concept ------------------------- */
function TitleConcept({ p, update, onNext, onBack }) {
  const guided = p.mode === "guided";
  const [busyT, setBusyT] = useState(false);
  const [busyC, setBusyC] = useState(false);
  const [err, setErr] = useState("");

  useEffect(() => {
    if (p.setup.titleChoice === "enter" && !p.title && p.setup.titleInput) update({ title: p.setup.titleInput });
    if (p.setup.conceptChoice === "enter" && !p.concept && p.setup.conceptInput) update({ concept: p.setup.conceptInput });
    // eslint-disable-next-line
  }, []);

  async function genTitles() {
    setErr(""); setBusyT(true);
    try {
      const n = guided ? 5 : 1;
      const text = await callClaude({
        system: "You are an acclaimed book editor who titles books. Return ONLY a JSON array of strings, no commentary.",
        user: `Propose ${n} distinctive, original ${n > 1 ? "title options" : "title"} for this book. Avoid clichés and generic phrasing; make them specific and memorable.\n\n${bookContext(p)}\n\nReturn JSON like ["Title One","Title Two"].`,
      });
      const arr = safeParse(text);
      if (Array.isArray(arr) && arr.length) {
        update({ titleOptions: arr.map(String), title: n === 1 ? String(arr[0]) : "" });
      } else {
        update({ titleOptions: [], title: stripFences(text) });
      }
    } catch (e) { setErr("Couldn't generate titles. " + e.message); }
    setBusyT(false);
  }

  async function genConcepts() {
    setErr(""); setBusyC(true);
    try {
      const n = guided ? 3 : 1;
      const text = await callClaude({
        system: "You are a developmental editor. Return ONLY a JSON array of strings. Each string is one self-contained concept of 60-90 words. No markdown.",
        user: `Write ${n} compelling, original ${n > 1 ? "concept options" : "concept"} (the premise / hook) for this book. ${p.setup.type === "fiction" ? "Establish protagonist, central conflict, stakes, and what makes it fresh." : "Establish the core argument or promise, who it serves, and what makes it distinctive."} Encourage originality; avoid formula.\n\n${bookContext(p)}\n\nReturn JSON like ["concept one ...","concept two ..."].`,
      });
      const arr = safeParse(text);
      if (Array.isArray(arr) && arr.length) {
        update({ conceptOptions: arr.map(String), concept: n === 1 ? String(arr[0]) : "" });
      } else {
        update({ conceptOptions: [], concept: stripFences(text) });
      }
    } catch (e) { setErr("Couldn't generate the concept. " + e.message); }
    setBusyC(false);
  }

  const canContinue = (p.title || "").trim() && (p.concept || "").trim();

  return (
    <div className="stage fade-up">
      <div className="eyebrow">Step three</div>
      <h2 className="sec serif-display" style={{ marginTop: 10 }}>Title &amp; concept</h2>
      {err && <div className="err" style={{ marginTop: 16 }}>{err}</div>}

      <div className="card" style={{ marginTop: 22 }}>
        <div className="row spread">
          <div className="label">The title</div>
          <button className="btn ghost sm" onClick={genTitles} disabled={busyT}>
            {busyT ? <><span className="spin" /> Generating</> : (p.titleOptions.length || p.title ? "Regenerate" : "Generate")}
          </button>
        </div>
        {guided && p.titleOptions.length > 1 && (
          <div style={{ marginTop: 14, display: "grid", gap: 10 }}>
            {p.titleOptions.map((t, i) => (
              <button key={i} className={"card choice optcard " + (p.title === t ? "sel" : "")} onClick={() => update({ title: t })}>
                <p className="serif-display" style={{ fontSize: 21 }}>{t}</p>
              </button>
            ))}
          </div>
        )}
        <input className="input" style={{ marginTop: 14, fontFamily: "'Fraunces',serif", fontSize: 20 }}
          placeholder="Your title appears here — fully editable" value={p.title} onChange={(e) => update({ title: e.target.value })} />
        {guided && p.titleOptions.length > 1 && <div className="hint">Pick an option above or write your own.</div>}
      </div>

      <div className="card" style={{ marginTop: 18 }}>
        <div className="row spread">
          <div className="label">The concept</div>
          <button className="btn ghost sm" onClick={genConcepts} disabled={busyC}>
            {busyC ? <><span className="spin" /> Generating</> : (p.conceptOptions.length || p.concept ? "Regenerate" : "Generate")}
          </button>
        </div>
        {guided && p.conceptOptions.length > 1 && (
          <div style={{ marginTop: 14, display: "grid", gap: 10 }}>
            {p.conceptOptions.map((c, i) => (
              <button key={i} className={"card choice optcard " + (p.concept === c ? "sel" : "")} onClick={() => update({ concept: c })}>
                <p>{c}</p>
              </button>
            ))}
          </div>
        )}
        <textarea className="area" rows={5} style={{ marginTop: 14 }}
          placeholder="Your concept appears here — refine it freely" value={p.concept} onChange={(e) => update({ concept: e.target.value })} />
      </div>

      <div className="row spread" style={{ marginTop: 26 }}>
        <button className="btn ghost" onClick={onBack}>← Back</button>
        <button className="btn accent" disabled={!canContinue} onClick={onNext}>Create the outline →</button>
      </div>
      {!canContinue && <div className="hint" style={{ textAlign: "right", marginTop: 8 }}>A title and concept are needed to outline.</div>}
    </div>
  );
}

/* ------------------------------ Outline ------------------------------ */
function OutlineStage({ p, update, onNext, onBack }) {
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  useEffect(() => {
    if (p.setup.outlineMode === "auto" && !p.outline && !busy) gen();
    // eslint-disable-next-line
  }, []);

  async function gen() {
    setErr(""); setBusy(true);
    try {
      const fic = p.setup.type === "fiction";
      const text = await callClaude({
        system: "You are a developmental editor creating a high-level book outline. Write clean, readable Markdown. Be specific and original — no filler.",
        user: `Create a high-level outline for this book. Include:\n- A one-line logline.\n- A short synopsis (3-5 sentences).\n${fic ? "- The dramatic structure as parts/acts with the key turning points, midpoint, and climax.\n- The main character arc(s) in brief." : "- The argument's structure as parts/sections with the throughline.\n- The key takeaways the reader should leave with."}\nDo NOT write per-chapter breakdowns yet — that comes next. Keep it under ~350 words. Use Markdown headings and short bullet points.\n\n${bookContext(p)}`,
      });
      update({ outline: stripFences(text) });
    } catch (e) { setErr("Couldn't generate the outline. " + e.message); }
    setBusy(false);
  }

  return (
    <div className="stage fade-up">
      <div className="eyebrow">Step four</div>
      <div className="row spread" style={{ marginTop: 10 }}>
        <h2 className="sec serif-display">The outline</h2>
        <button className="btn ghost sm" onClick={gen} disabled={busy}>
          {busy ? <><span className="spin" /> Generating</> : (p.outline ? "Regenerate" : "Generate outline")}
        </button>
      </div>
      <p className="hint" style={{ marginTop: 6 }}>
        A map of the whole book before chapters. {p.setup.outlineMode === "guided" ? "Generate, then shape it yourself." : ""} Edit anything.
      </p>
      {err && <div className="err" style={{ marginTop: 14 }}>{err}</div>}

      {busy && !p.outline ? (
        <div className="card" style={{ marginTop: 18, color: "var(--ink-faint)" }}>
          Drafting your outline<span className="dots" />
        </div>
      ) : (
        <textarea className="area" rows={18} style={{ marginTop: 18, lineHeight: 1.6 }}
          placeholder="Your outline will appear here. You can also write it from scratch."
          value={p.outline} onChange={(e) => update({ outline: e.target.value })} />
      )}

      <div className="row spread" style={{ marginTop: 24 }}>
        <button className="btn ghost" onClick={onBack}>← Back</button>
        <button className="btn accent" disabled={!(p.outline || "").trim()} onClick={onNext}>Plan the chapters →</button>
      </div>
    </div>
  );
}

/* --------------------------- Chapter Plan --------------------------- */
function distributeWords(total, chapters) {
  const n = chapters.length || 1;
  const sugg = chapters.map((c) => (c.targetWords && c.targetWords > 0 ? c.targetWords : null));
  const known = sugg.filter((x) => x != null);
  let weights;
  if (known.length === n) {
    const sum = known.reduce((a, b) => a + b, 0) || 1;
    weights = sugg.map((x) => x / sum);
  } else {
    weights = chapters.map(() => 1 / n);
  }
  return chapters.map((c, i) => ({ ...c, targetWords: Math.max(300, Math.round((total * weights[i]) / 100) * 100) }));
}

function ChapterPlan({ p, update, onNext, onBack }) {
  const [busy, setBusy] = useState(false);
  const [progress, setProgress] = useState("");
  const [err, setErr] = useState("");
  const [regen, setRegen] = useState(null);

  async function genBatch(start, end, existing) {
    const fic = p.setup.type === "fiction";
    const priorTitles = existing.slice(0, start).map((c, i) => `${i + 1}. ${c.title}`).join("\n") || "(none yet)";
    const text = await callClaude({
      system: "You are a developmental editor breaking a book into chapters. Return ONLY a JSON array of objects with keys: title, summary, words. No markdown, no commentary.",
      user: `Plan ${fic ? "chapters" : "chapters/sections"} ${start + 1} through ${end} of ${p.setup.chapterCount} for this book, consistent with the outline below.\nFor each: a distinctive "title", a "summary" of 30-55 words describing what happens / is covered (and how it advances the ${fic ? "story and character arc" : "argument"}), and "words" = recommended word count.\nKeep continuity with earlier chapters; do not repeat material. Aim so the full book totals about ${fmt(p.setup.targetWords)} words.\n\n${bookContext(p)}\n\nOUTLINE:\n${p.outline}\n\nEARLIER CHAPTER TITLES:\n${priorTitles}\n\nReturn JSON array of exactly ${end - start} items.`,
    });
    const arr = safeParse(text);
    if (!Array.isArray(arr)) throw new Error("Unexpected format");
    return arr.map((o) => ({
      id: uid(),
      title: String(o.title || "Untitled"),
      summary: String(o.summary || ""),
      targetWords: parseInt(o.words) || 0,
      sections: [],
      done: false,
    }));
  }

  async function genAll() {
    setErr(""); setBusy(true); setProgress("");
    try {
      const count = p.setup.chapterCount;
      const BATCH = 6;
      let chapters = [];
      for (let st = 0; st < count; st += BATCH) {
        const e = Math.min(count, st + BATCH);
        setProgress(`Chapters ${st + 1}–${e} of ${count}`);
        const batch = await genBatch(st, e, chapters);
        chapters = chapters.concat(batch);
      }
      chapters = distributeWords(p.setup.targetWords, chapters);
      update({ chapters, activeChapter: 0 });
    } catch (e) { setErr("Couldn't generate the chapter plan. " + e.message); }
    setBusy(false); setProgress("");
  }

  async function regenOne(idx) {
    setErr(""); setRegen(idx);
    try {
      const fic = p.setup.type === "fiction";
      const ch = p.chapters[idx];
      const surrounding = p.chapters
        .map((c, i) => `${i + 1}. ${c.title}${i === idx ? "  <-- regenerate this one" : ""}`)
        .join("\n");
      const text = await callClaude({
        system: "You are a developmental editor. Return ONLY a JSON object with keys: title, summary, words. No commentary.",
        user: `Rewrite a fresh, original version of chapter ${idx + 1} so it fits between its neighbours without overlap, advancing the ${fic ? "story" : "argument"}. Provide "title", a 30-55 word "summary", and recommended "words" (current target ${ch.targetWords}).\n\n${bookContext(p)}\n\nOUTLINE:\n${p.outline}\n\nALL CHAPTERS:\n${surrounding}\n\nReturn one JSON object.`,
      });
      const o = safeParse(text);
      if (o && (o.title || o.summary)) {
        update((prev) => {
          const chapters = prev.chapters.slice();
          chapters[idx] = { ...chapters[idx], title: String(o.title || chapters[idx].title), summary: String(o.summary || chapters[idx].summary), targetWords: parseInt(o.words) || chapters[idx].targetWords };
          return { chapters };
        });
      }
    } catch (e) { setErr("Couldn't regenerate that chapter. " + e.message); }
    setRegen(null);
  }

  const editChapter = (idx, patch) => update((prev) => {
    const chapters = prev.chapters.slice();
    chapters[idx] = { ...chapters[idx], ...patch };
    return { chapters };
  });

  const sumWords = p.chapters.reduce((a, c) => a + (c.targetWords || 0), 0);
  const has = p.chapters.length > 0;

  return (
    <div className="stage fade-up">
      <div className="eyebrow">Step five</div>
      <div className="row spread" style={{ marginTop: 10 }}>
        <h2 className="sec serif-display">Chapter titles &amp; summaries</h2>
        <button className="btn ghost sm" onClick={genAll} disabled={busy}>
          {busy ? <><span className="spin" /> {progress || "Generating"}</> : (has ? "Regenerate all" : "Generate chapters")}
        </button>
      </div>
      <p className="hint" style={{ marginTop: 6 }}>
        {p.setup.chapterCount} planned. Recommended word counts shown — edit titles, summaries, or counts; regenerate any single one.
        {has && <> &nbsp;Planned total: <strong>{fmt(sumWords)}</strong> words.</>}
      </p>
      {err && <div className="err" style={{ marginTop: 14 }}>{err}</div>}

      {!has && !busy && (
        <div className="card" style={{ marginTop: 18, textAlign: "center", padding: 40, color: "var(--ink-soft)" }}>
          No chapters yet. Generate the plan from your outline, then refine.
        </div>
      )}
      {busy && !has && (
        <div className="card" style={{ marginTop: 18, color: "var(--ink-faint)" }}>Planning chapters — {progress}<span className="dots" /></div>
      )}

      <div style={{ marginTop: 18, display: "grid", gap: 14 }}>
        {p.chapters.map((c, i) => (
          <div className="card" key={c.id}>
            <div className="row spread" style={{ alignItems: "flex-start" }}>
              <div className="chap-no" style={{ marginTop: 12 }}>{i + 1}</div>
              <input className="input" style={{ fontFamily: "'Fraunces',serif", fontSize: 18, flex: 1, margin: "0 12px" }}
                value={c.title} onChange={(e) => editChapter(i, { title: e.target.value })} />
              <button className="btn ghost tiny" disabled={regen === i} onClick={() => regenOne(i)}>
                {regen === i ? <span className="spin" /> : "Regenerate"}
              </button>
            </div>
            <textarea className="area" rows={2} style={{ marginTop: 10 }} value={c.summary} onChange={(e) => editChapter(i, { summary: e.target.value })} />
            <div className="row" style={{ marginTop: 10 }}>
              <span className="label">Target words</span>
              <input className="input" type="number" style={{ width: 120, padding: "6px 10px" }} value={c.targetWords}
                onChange={(e) => editChapter(i, { targetWords: Math.max(0, parseInt(e.target.value) || 0) })} />
            </div>
          </div>
        ))}
      </div>

      <div className="row spread" style={{ marginTop: 26 }}>
        <button className="btn ghost" onClick={onBack}>← Back</button>
        <button className="btn accent" disabled={!has} onClick={onNext}>Enter the drafting desk →</button>
      </div>
    </div>
  );
}

/* ------------------------------ Workspace ------------------------------ */
function Workspace({ p, update, onBack }) {
  const idx = Math.min(p.activeChapter || 0, Math.max(0, p.chapters.length - 1));
  const ch = p.chapters[idx];
  const [busy, setBusy] = useState(false);
  const [regenSec, setRegenSec] = useState(null);
  const [err, setErr] = useState("");

  const setActive = (i) => update({ activeChapter: i });

  const editChapter = (i, patch) => update((prev) => {
    const chapters = prev.chapters.slice();
    chapters[i] = { ...chapters[i], ...(typeof patch === "function" ? patch(chapters[i]) : patch) };
    return { chapters };
  });

  function buildContext() {
    const fic = p.setup.type === "fiction";
    const allChapters = p.chapters.map((c, i) => `${i + 1}. ${c.title} — ${c.summary}`).join("\n");
    const written = chapterWords(ch);
    const existing = (ch.sections || []).map((s) => s.text).join("\n\n");
    const tail = existing.split(/\s+/).slice(-400).join(" ");
    return { fic, allChapters, written, existing, tail };
  }

  async function genSection() {
    setErr(""); setBusy(true);
    try {
      const { fic, allChapters, written, tail } = buildContext();
      const remaining = Math.max(0, (ch.targetWords || 0) - written);
      const first = (ch.sections || []).length === 0;
      const text = await callClaude({
        maxTokens: 1500,
        system: `You are ghostwriting a ${fic ? "novel" : "nonfiction book"} as the author's collaborator. Write polished, publication-quality prose in the established voice. Output ONLY the prose for the next section — no headings, no labels, no meta-commentary, no summaries. Be original; avoid cliché and filler.`,
        user:
          `Write the next section of Chapter ${idx + 1}: "${ch.title}". Aim for roughly 450-650 words of continuous ${fic ? "narrative" : "exposition"}.\n` +
          `${first ? "This is the opening of the chapter — establish it cleanly." : "Continue seamlessly from the prose so far; do not recap."}\n` +
          `Maintain consistent ${fic ? "POV, tense, voice and continuity" : "argument, voice and terminology"}.\n` +
          `If the chapter reaches its natural conclusion (about ${fmt(ch.targetWords)} words total, ~${fmt(remaining)} remain), bring it to a satisfying close and then put the token [[END]] on its own final line.\n\n` +
          `${bookContext(p)}\n\nOUTLINE:\n${p.outline}\n\nALL CHAPTERS (for continuity):\n${allChapters}\n\nTHIS CHAPTER'S SUMMARY:\n${ch.summary}\n\n` +
          (tail ? `THE PROSE SO FAR (end of it):\n...${tail}\n` : ``),
      });
      let body = stripFences(text);
      let done = false;
      if (/\[\[END\]\]/.test(body)) { done = true; body = body.replace(/\[\[END\]\]/g, "").trim(); }
      editChapter(idx, (c) => {
        const sections = (c.sections || []).concat([{ id: uid(), text: body }]);
        const total = sections.reduce((a, s) => a + wordCount(s.text), 0);
        return { sections, done: done || total >= (c.targetWords || 0) * 0.95 };
      });
    } catch (e) { setErr("Couldn't draft that section. " + e.message); }
    setBusy(false);
  }

  async function regenerateSection(secIdx) {
    setErr(""); setRegenSec(secIdx);
    try {
      const { fic, allChapters } = buildContext();
      const before = (ch.sections || []).slice(0, secIdx).map((s) => s.text).join("\n\n");
      const tail = before.split(/\s+/).slice(-400).join(" ");
      const text = await callClaude({
        maxTokens: 1500,
        system: `You are ghostwriting a ${fic ? "novel" : "nonfiction book"}. Output ONLY replacement prose for one section — no headings or meta-commentary. Match the established voice; be original.`,
        user: `Rewrite a fresh version of this section of Chapter ${idx + 1}: "${ch.title}" (~450-650 words). It must flow from the prose before it and set up what follows, without repeating other sections.\n\n${bookContext(p)}\n\nTHIS CHAPTER'S SUMMARY:\n${ch.summary}\n\nALL CHAPTERS:\n${allChapters}\n\n${tail ? "PROSE BEFORE THIS SECTION (end):\n..." + tail : "This is the opening section."}`,
      });
      const body = stripFences(text).replace(/\[\[END\]\]/g, "").trim();
      editChapter(idx, (c) => {
        const sections = c.sections.slice();
        sections[secIdx] = { ...sections[secIdx], text: body };
        return { sections };
      });
    } catch (e) { setErr("Couldn't regenerate that section. " + e.message); }
    setRegenSec(null);
  }

  function editSectionText(secIdx, txt) {
    editChapter(idx, (c) => {
      const sections = c.sections.slice();
      sections[secIdx] = { ...sections[secIdx], text: txt };
      return { sections };
    });
  }
  function deleteSection(secIdx) {
    editChapter(idx, (c) => ({ sections: c.sections.filter((_, i) => i !== secIdx) }));
  }

  function exportManuscript() {
    const lines = [];
    lines.push("# " + (p.title || "Untitled") + "\n");
    if (p.concept) lines.push("> " + p.concept + "\n");
    p.chapters.forEach((c, i) => {
      lines.push(`\n\n## Chapter ${i + 1}. ${c.title}\n`);
      (c.sections || []).forEach((s) => lines.push(s.text + "\n\n"));
    });
    const blob = new Blob([lines.join("\n")], { type: "text/markdown" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = (p.title || "manuscript").replace(/[^a-z0-9]+/gi, "-").toLowerCase() + ".md";
    a.click();
    URL.revokeObjectURL(url);
  }

  const written = chapterWords(ch);
  const totalWritten = p.chapters.reduce((a, c) => a + chapterWords(c), 0);
  const pctChap = ch.targetWords ? Math.min(100, Math.round((written / ch.targetWords) * 100)) : 0;
  const pctBook = p.setup.targetWords ? Math.min(100, Math.round((totalWritten / p.setup.targetWords) * 100)) : 0;

  return (
    <div className="stage wide fade-up">
      <div className="ws">
        <aside className="rail">
          <div className="label">Manuscript</div>
          <div className="serif-display" style={{ fontSize: 22, lineHeight: 1.1, margin: "4px 0 12px" }}>{p.title || "Untitled"}</div>
          <div className="prog"><i style={{ width: pctBook + "%" }} /></div>
          <div className="small" style={{ marginTop: 6 }}>{fmt(totalWritten)} / {fmt(p.setup.targetWords)} words &middot; {pctBook}%</div>

          <div style={{ marginTop: 18, display: "grid", gap: 2, maxHeight: "52vh", overflow: "auto", paddingRight: 4 }}>
            {p.chapters.map((c, i) => {
              const w = chapterWords(c);
              const stt = c.done ? "done" : w > 0 ? "part" : "";
              return (
                <div key={c.id} className={"chap-item " + (i === idx ? "sel" : "")} onClick={() => setActive(i)}>
                  <span className={"statusdot " + stt} />
                  <span className="chap-no">{i + 1}</span>
                  <span className="chap-t">{c.title}</span>
                </div>
              );
            })}
          </div>

          <div className="divider" style={{ margin: "16px 0" }} />
          <button className="btn ghost sm" style={{ width: "100%" }} onClick={exportManuscript}>↓ Export manuscript (.md)</button>
          <button className="btn ghost sm" style={{ width: "100%", marginTop: 8 }} onClick={onBack}>← Chapter plan</button>
        </aside>

        <main>
          <div className="row spread">
            <div className="label">Chapter {idx + 1} of {p.chapters.length}</div>
            <div className="row" style={{ gap: 8 }}>
              <button className="btn ghost tiny" disabled={idx === 0} onClick={() => setActive(idx - 1)}>← Prev</button>
              <button className="btn ghost tiny" disabled={idx === p.chapters.length - 1} onClick={() => setActive(idx + 1)}>Next →</button>
            </div>
          </div>

          <input className="input" style={{ fontFamily: "'Fraunces',serif", fontSize: 30, marginTop: 10, border: "none", background: "transparent", padding: "4px 0" }}
            value={ch.title} onChange={(e) => editChapter(idx, { title: e.target.value })} />

          <details style={{ marginTop: 4 }}>
            <summary className="small" style={{ cursor: "pointer" }}>Chapter summary &amp; target (reference)</summary>
            <textarea className="area" rows={2} style={{ marginTop: 8 }} value={ch.summary} onChange={(e) => editChapter(idx, { summary: e.target.value })} />
            <div className="row" style={{ marginTop: 8 }}>
              <span className="label">Target</span>
              <input className="input" type="number" style={{ width: 110, padding: "5px 9px" }} value={ch.targetWords} onChange={(e) => editChapter(idx, { targetWords: Math.max(0, parseInt(e.target.value) || 0) })} />
            </div>
          </details>

          <div className="row spread" style={{ marginTop: 14, marginBottom: 6 }}>
            <span className="small">{fmt(written)} / {fmt(ch.targetWords)} words &middot; {pctChap}%</span>
            <label className="small row" style={{ gap: 6, cursor: "pointer" }}>
              <input type="checkbox" checked={!!ch.done} onChange={(e) => editChapter(idx, { done: e.target.checked })} style={{ accentColor: "var(--ok)" }} />
              mark complete
            </label>
          </div>
          <div className="prog"><i style={{ width: pctChap + "%" }} /></div>

          {err && <div className="err" style={{ marginTop: 14 }}>{err}</div>}

          <div className="manuscript" style={{ marginTop: 22 }}>
            {(ch.sections || []).length === 0 && !busy && (
              <div className="card" style={{ textAlign: "center", padding: 36, color: "var(--ink-soft)" }}>
                Empty page. Draft the opening section to begin — the studio uses your outline and the summaries of
                earlier chapters to keep it consistent.
              </div>
            )}
            {(ch.sections || []).map((sx, si) => (
              <div className="section-block" key={sx.id} style={{ marginBottom: 18 }}>
                <div className="row spread" style={{ marginBottom: 2 }}>
                  <span className="kbd">§ {si + 1} &middot; {fmt(wordCount(sx.text))} words</span>
                  <div className="section-tools">
                    <button className="btn ghost tiny" disabled={regenSec === si} onClick={() => regenerateSection(si)}>{regenSec === si ? <span className="spin" /> : "Regenerate"}</button>
                    <button className="btn ghost tiny" onClick={() => deleteSection(si)}>Delete</button>
                  </div>
                </div>
                <textarea className="area" value={sx.text} rows={Math.max(4, Math.ceil(sx.text.length / 64))} onChange={(e) => editSectionText(si, e.target.value)} />
              </div>
            ))}
          </div>

          <div className="row" style={{ marginTop: 18, gap: 12 }}>
            <button className="btn accent" onClick={genSection} disabled={busy}>
              {busy ? <><span className="spin" /> Drafting<span className="dots" /></> : ((ch.sections || []).length === 0 ? "Draft opening section" : "Draft next section")}
            </button>
            {written >= (ch.targetWords || 0) && (ch.targetWords || 0) > 0 && (
              <span className="small">Target reached — keep going, or move to the next chapter.</span>
            )}
          </div>
        </main>
      </div>
    </div>
  );
}

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