/* Tour.jsx: the nine-chapter Console tour. Namespace: ch- (never hp-).
   Complete at nine chapters; no further scenes are planned. Deliberate
   decision not to split this into multiple files: the extra script tag plus
   another injectAllCss() registration would cost more than one file this
   size saves. */

// Reduced-motion guard shared by every chapter scene. One-shot matchMedia
// read on mount is enough: the preference doesn't change mid-session, and a
// single check keeps every scene's effect logic simple and consistent.
function usePrefersReducedMotion() {
  const [reduced] = React.useState(
    () => !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches)
  );
  return reduced;
}

const CHAPTERS = [
  { id: "talk",      n: "01", title: "Talk to it. Hand it work." },
  { id: "channels",  n: "02", title: "It works where your team works." },
  { id: "inbox",     n: "03", title: "It asks before anything that matters." },
  { id: "projects",  n: "04", title: "Give it outcomes, not prompts." },
  { id: "workforce", n: "05", title: "One assistant, a whole workforce." },
  { id: "memory",    n: "06", title: "It learns your business." },
  { id: "nebula",    n: "07", title: "See your whole company." },
  { id: "process",   n: "08", title: "How we get you live." },
  { id: "cost",      n: "09", title: "What it costs." },
];
function Chapter({ id, children }) {
  const meta = CHAPTERS.find(c => c.id === id) || { n: "", title: "" };
  return (
    <section className={"ch-chapter ch-" + id} id={id}>
      <div className="shell">
        <p className="chapter-mark">{meta.n}</p>
        <h2 className="h2">{meta.title}</h2>
        {children}
      </div>
    </section>
  );
}

// ── shared scene-head chrome ─────────────────────────────────────────────
// Every dark-panel scene opens with the same id row: a mark plus a label on
// the left. `live` covers the right side: pass `true` for the standard
// dot-plus-"live" indicator, or a node for a scene that shows something else
// there instead (chapter 03's Inbox card shows a queue count, not a live
// dot). Omit it for a scene with nothing on the right.
function ChSceneHead({ label, live }) {
  return (
    <div className="ch-scene-head">
      <span className="ch-scene-id"><span className="ch-scene-mark"><ZMark onDark /></span> {label}</span>
      {live === true ? <span className="ch-scene-live"><span className="ch-scene-dot" /> live</span> : live}
    </div>
  );
}

// ── Chapter 01 scene: a live brief, typed in and handed back ────────────────
// Adapts the old homepage chat demo's typing/working/reply mechanics into a
// single, looping beat rather than a multi-scenario cycle. Visual language
// mirrors the hero Console card's .hp-con-msg-* rules
// (translucent bubbles, magenta hairline on the user turn) but is fully
// self-contained under the ch- namespace.
const CH_TALK_ASK = "Pull together where the Meridian pilot stands for tomorrow's call.";
const CH_TALK_CHIPS = ["reading the thread", "CRM", "last week's notes"];
const CH_TALK_REPLY = "Done. Brief's ready for the 10:00 with Dan: pilot's mid-rollout, two licences unassigned, and the renewal conversation is due. Three open risks flagged.";

function ChDocGlyph() {
  return (
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path d="M6.5 2.75h7.4L18.5 7.8V20.3a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V3.75a1 1 0 0 1 1-1Z" fill="rgba(255,255,255,.9)" />
      <path d="M13.9 2.75V7a1 1 0 0 0 1 1h3.6" fill="rgba(255,255,255,.55)" />
    </svg>
  );
}

function ChTalkScene() {
  const { useState, useEffect } = React;
  const reducedMotion = usePrefersReducedMotion();
  // Default state is the resting/final beat: what shows before the loop's
  // first tick, and exactly what the reduced-motion SEO bake captures.
  const [phase, setPhase] = useState("done"); // user | working | done
  const [userText, setUserText] = useState(CH_TALK_ASK);
  const [chipCount, setChipCount] = useState(CH_TALK_CHIPS.length);

  useEffect(() => {
    if (reducedMotion) return; // stays on the static final state, no timers

    let cancelled = false;
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

    (async function loop() {
      while (!cancelled) {
        // dwell on whatever's on screen (the initial or previous "done" beat)
        // before clearing back to the first turn: no flash on mount.
        await sleep(5200); if (cancelled) return;
        setPhase("user"); setUserText(""); setChipCount(0);
        await sleep(500); if (cancelled) return;
        for (let i = 0; i < CH_TALK_ASK.length; i++) {
          if (cancelled) return;
          setUserText(CH_TALK_ASK.slice(0, i + 1));
          await sleep(16);
        }
        await sleep(450); if (cancelled) return;
        setPhase("working");
        for (let i = 0; i < CH_TALK_CHIPS.length; i++) {
          await sleep(560); if (cancelled) return;
          setChipCount(i + 1);
        }
        await sleep(750); if (cancelled) return;
        setPhase("done");
      }
    })();
    return () => { cancelled = true; };
  }, [reducedMotion]);

  const showWorking = phase === "working";
  const showDone = phase === "done";

  return (
    <div className="ch-scene" role="group" aria-label="A brief handed to Zarco, and its reply">
      <ChSceneHead label="Zarco" live />
      <div className="ch-scene-body">
        <div className="ch-msg ch-msg-user">
          <p>{userText}</p>
          {userText === CH_TALK_ASK && <span className="ch-time">09:47</span>}
        </div>
        {showWorking && (
          <div className="ch-msg ch-msg-bot">
            <div className="ch-working"><span className="ch-dots"><i /><i /><i /></span>working</div>
            <div className="ch-chips">
              {CH_TALK_CHIPS.slice(0, chipCount).map((c) => (<span key={c} className="ch-chip-base ch-chip-animate">{c}</span>))}
            </div>
          </div>
        )}
        {showDone && (
          <div className="ch-msg ch-msg-bot">
            <p>{CH_TALK_REPLY}</p>
            <div className="ch-artifact">
              <span className="ch-artifact-icon"><ChDocGlyph /></span>
              <span className="ch-artifact-name">Meridian-pilot-brief.pdf</span>
            </div>
            <div className="ch-msg-foot">
              <span className="ch-risk">3 open risks</span>
              <span className="ch-time">09:49</span>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function ChTalk() {
  return (
    <Chapter id="talk">
      <p className="ch-lead">Zarco works like an executive assistant who has been with you for years. It knows your customers, your commitments and your calendar, so you brief it the way you would brief a person.</p>
      <ChTalkScene />
    </Chapter>
  );
}
// ── Chapter 02 scene: two chat surfaces, one company bot ────────────────────
// A light chapter (paper ground, same as Tools/Manifesto) holding two quiet
// dark chat cards, so it doesn't repeat chapter 01's single full dark scene
// back to back. Cards reuse the ch-msg/ch-msg-user/ch-msg-bot bubble language
// from ChTalkScene rather than inventing new ones.
const CH_CHANNELS = [
  {
    id: "slack", app: "slack", label: "Slack", room: "#sales",
    ask: "@Zarco can you log that call with Fenwick?",
    reply: "Logged to the CRM. Next step set for Friday.",
    done: true,
  },
  {
    id: "teams", app: "teams", label: "Teams", room: "Ops chat",
    ask: "@Zarco what is due from us this week?",
    reply: "Three things: the Harborview quote, the VAT return prep, the pilot review deck.",
    done: false,
  },
];

function ChTickGlyph() {
  return (
    <svg className="ch-tick-glyph" viewBox="0 0 16 16" aria-hidden="true">
      <path d="M3 8.5l3 3 7-7" fill="none" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function ChChannelsScene() {
  return (
    <div className="ch-split ch-rev" role="group" aria-label="Zarco answering in Slack and in Microsoft Teams">
      {CH_CHANNELS.map((c, i) => (
        <div className="ch-channel" key={c.id}>
          <div className="ch-channel-head">
            <span className="ch-channel-app"><FloatGlyph kind={c.app} /> {c.label}</span>
            <span className="ch-channel-room">{c.room}</span>
          </div>
          <div className="ch-channel-body">
            <div className="ch-msg ch-msg-user ch-cascade" style={{ "--d": i }}>
              <p>{c.ask}</p>
            </div>
            <div className="ch-msg ch-msg-bot ch-cascade" style={{ "--d": i + 1 }}>
              <p>{c.reply}</p>
              {c.done && <span className="ch-done"><ChTickGlyph /> done</span>}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

function ChChannels() {
  return (
    <Chapter id="channels">
      <p className="ch-lead">No new tab to live in. Zarco answers where your company already talks, and each person can have their own assistant through one company bot.</p>
      <ChChannelsScene />
    </Chapter>
  );
}
// ── Chapter 03 scene: the Inbox, and a moment it pushes back ────────────────
// The trust centrepiece of the tour: shown, not asserted. Two beats on the
// established dark panel idiom, reusing .ch-scene/.ch-msg rather than
// inventing new chrome: a ranked approval queue, and a compact exchange
// where Zarco declines to act blind. Both beats are static: there is no
// "waiting" state to perform, the whole point is that they're sitting there
// already resolved, one tap away from a human's decision.
const CH_INBOX_TOP = "Draft to Harborview: renewal terms";
const CH_INBOX_QUEUE = [
  { label: "Invoice chase batch", count: 3 },
  { label: "CRM updates from this morning's calls", count: 2 },
];
const CH_INBOX_ASK = "Just send the price increase to everyone today.";
const CH_INBOX_REPLY = "Two of these accounts are mid-renewal. Sending today risks both. Want me to hold those two and send the rest?";

function ChSwitchGlyph() {
  return (
    <svg viewBox="0 0 20 12" aria-hidden="true">
      <rect x="1" y="1" width="18" height="10" rx="5" fill="none" stroke="currentColor" strokeWidth="1.3" opacity=".55" />
      <circle cx="6.2" cy="6" r="3.4" fill="currentColor" opacity=".85" />
    </svg>
  );
}

function ChInboxScene() {
  return (
    <div className="ch-inbox-grid ch-rev">
      <div className="ch-scene ch-scene--flush ch-cascade" style={{ "--d": 0 }} role="group" aria-label="Inbox: three items waiting for approval">
        {/* Waiting count = the top item plus every row in the queue below it,
            so the head can never drift out of sync with the list it labels. */}
        <ChSceneHead label="Inbox" live={<span className="ch-queue-count">{1 + CH_INBOX_QUEUE.length} waiting</span>} />
        <div className="ch-scene-body ch-queue-body">
          <div className="ch-queue-top">
            <p className="ch-queue-top-label">{CH_INBOX_TOP}</p>
            <div className="ch-queue-actions">
              <span className="ch-pill ch-pill-approve">Approve</span>
              <span className="ch-pill ch-pill-quiet">Edit first</span>
              <span className="ch-pill ch-pill-quiet">Snooze</span>
            </div>
          </div>
          <ul className="ch-queue-rest">
            {CH_INBOX_QUEUE.map((q) => (
              <li className="ch-queue-row" key={q.label}>
                <span>{q.label}</span>
                <span className="ch-queue-row-count">{q.count}</span>
              </li>
            ))}
          </ul>
        </div>
        <div className="ch-queue-foot"><ChSwitchGlyph /> One switch pauses everything.</div>
      </div>

      <div className="ch-scene ch-scene--flush ch-cascade" style={{ "--d": 1 }} role="group" aria-label="A moment where Zarco pushes back before sending">
        <ChSceneHead label="Zarco" live />
        <div className="ch-scene-body">
          <div className="ch-msg ch-msg-user">
            <p>{CH_INBOX_ASK}</p>
          </div>
          <div className="ch-msg ch-msg-bot">
            <p>{CH_INBOX_REPLY}</p>
          </div>
        </div>
      </div>
    </div>
  );
}

function ChInbox() {
  return (
    <Chapter id="inbox">
      <p className="ch-lead">Every draft, every send, every risky action queues in your Inbox and waits for a human. You approve it, edit it first, or snooze it for later. And if you ever want everything to stop, one switch pauses the lot.</p>
      <ChInboxScene />
    </Chapter>
  );
}
// ── Chapter 04 scene: a project that recomposes around what it's for ───────
// One tabbed project card on the dark panel idiom. Four example projects
// share ONE data structure (CH_PROJECTS): the tab strip, the outcome, the
// journey, the routine, the signal and the ask-the-project answer all read
// from it, so the strip and the panel can never drift apart. Each project
// also carries an `order`: the five tile keys in display order, first =
// the emphasised tile for that kind of work. Swapping which key leads the
// order (not just growing a tile) is what makes the recompose read as a
// genuine rearrangement rather than one box getting bigger.
const CH_PROJ_TILE_LABEL = {
  outcome: "Outcome",
  journey: "Stages",
  routine: "Routine",
  signal: "Signal",
  ask: "Ask the project",
};

// outcome.status is a closed enum: "ok" | "warn". Anything else falls back to a neutral dot (see .ch-proj-health-dot in tourCss) instead of rendering invisibly.
const CH_PROJECTS = [
  {
    id: "renewals",
    tab: "Q3 renewals push",
    order: ["outcome", "journey", "routine", "signal", "ask"],
    outcome: { headline: "Renew 18 accounts by 30 Sept", health: "on track", detail: "11 of 18", status: "ok" },
    journey: { stages: [
      { label: "contacted", count: 18 },
      { label: "quoted", count: 14 },
      { label: "signed", count: 11 },
    ] },
    routine: { when: "Mon 08:00", what: "chase silent accounts", owner: "Priya · Customer Success" },
    signal: { text: "Harborview quiet for 9 days", dest: "to your Inbox" },
    ask: {
      q: "What is at risk this week?",
      a: "Harborview and Denholm Partners have both gone quiet past the seven-day threshold.",
      source: "from this project's routines and the CRM",
    },
  },
  {
    id: "collections",
    tab: "Collections catch-up",
    order: ["journey", "outcome", "signal", "routine", "ask"],
    outcome: { headline: "Collect £42,600 of aged debt by 31 Aug", health: "behind", detail: "£18,200 of £42,600", status: "warn" },
    journey: { stages: [
      { label: "0-30 days", count: 9 },
      { label: "31-60 days", count: 7 },
      { label: "61+ days", count: 5 },
    ] },
    routine: { when: "Fri 09:00", what: "call accounts over 60 days", owner: "Sam · Finance" },
    signal: { text: "Turnkey Logistics 61 days overdue, no reply", dest: "to your Inbox" },
    ask: {
      q: "Which accounts need escalating?",
      a: "Turnkey Logistics and Bellweather Ltd: both over 60 days with no reply to two reminders.",
      source: "from the aged-debt ledger and the reminder log",
    },
  },
  {
    id: "launch",
    tab: "Product launch",
    order: ["signal", "journey", "outcome", "routine", "ask"],
    outcome: { headline: "Ship the Q3 release by 15 Sept", health: "at risk", detail: "2 of 3 milestones done", status: "warn" },
    journey: { stages: [
      { label: "design", count: 12 },
      { label: "build", count: 9, note: "2 blocked" },
      { label: "launch", count: 4 },
    ] },
    routine: { when: "Wed 10:00", what: "unblock the build queue", owner: "Josh · Engineering" },
    signal: { text: "Payments milestone blocked 4 days on legal sign-off", dest: "to your Inbox" },
    ask: {
      q: "What's blocking launch?",
      a: "The payments milestone is waiting on legal sign-off, four days overdue.",
      source: "from the milestone board and the legal thread",
    },
  },
  {
    id: "onboarding",
    tab: "New-starter onboarding",
    order: ["routine", "journey", "outcome", "signal", "ask"],
    outcome: { headline: "Get Aisha fully ramped by 1 Sept", health: "on track", detail: "6 of 9 steps done", status: "ok" },
    journey: { stages: [
      { label: "access", count: 3 },
      { label: "training", count: 2 },
      { label: "shadowing", count: 1 },
    ] },
    routine: { when: "Tue 09:00", what: "buddy check-in with Aisha", owner: "Marcus · People Ops" },
    signal: { text: "IT access request unactioned for 3 days", dest: "to your Inbox" },
    ask: {
      q: "What's left before Aisha goes live?",
      a: "Two shadowing sessions and sign-off from her manager.",
      source: "from the onboarding checklist and calendar",
    },
  },
];

function ChArrowGlyph() {
  return (
    <svg className="ch-proj-arrow" viewBox="0 0 12 12" aria-hidden="true">
      <path d="M3.5 2.2l4 3.8-4 3.8" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function ChFlagGlyph() {
  return (
    <svg viewBox="0 0 14 14" aria-hidden="true">
      <path d="M3 1.5v11" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" fill="none" />
      <path d="M3 2.2c1.6-.9 3-.9 4.6 0s3 .9 4.4 0v5c-1.4.9-2.8.9-4.4 0s-3-.9-4.6 0z" fill="currentColor" opacity=".85" />
    </svg>
  );
}

function ChProjTileBody({ kind, project }) {
  if (kind === "outcome") {
    const o = project.outcome;
    return (
      <React.Fragment>
        <p className="ch-proj-outcome-headline">{o.headline}</p>
        <div className="ch-proj-health">
          <span className={"ch-proj-health-dot ch-proj-health-dot--" + o.status} />
          <span>{o.health}</span>
          <span className="ch-proj-health-sep">·</span>
          <span>{o.detail}</span>
        </div>
      </React.Fragment>
    );
  }
  if (kind === "journey") {
    return (
      <div className="ch-proj-journey">
        {project.journey.stages.map((s, i) => (
          <React.Fragment key={s.label}>
            {i > 0 && <ChArrowGlyph />}
            <div className="ch-proj-stage">
              <span className="ch-proj-stage-count">{s.count}</span>
              <span className="ch-proj-stage-label">{s.label}</span>
              {s.note && <span className="ch-proj-stage-note">{s.note}</span>}
            </div>
          </React.Fragment>
        ))}
      </div>
    );
  }
  if (kind === "routine") {
    const r = project.routine;
    return (
      <React.Fragment>
        <p className="ch-proj-routine-when">{r.when}</p>
        <p className="ch-proj-routine-what">{r.what}</p>
        <p className="ch-proj-routine-owner">{r.owner}</p>
      </React.Fragment>
    );
  }
  if (kind === "signal") {
    const s = project.signal;
    return (
      <React.Fragment>
        <p className="ch-proj-signal-text">{s.text}</p>
        <span className="ch-proj-signal-dest"><ChFlagGlyph /> {s.dest}</span>
      </React.Fragment>
    );
  }
  if (kind === "ask") {
    const a = project.ask;
    return (
      <React.Fragment>
        <p className="ch-proj-ask-q">{a.q}</p>
        <p className="ch-proj-ask-a">{a.a}</p>
        <p className="ch-proj-ask-source">{a.source}</p>
      </React.Fragment>
    );
  }
  return null; // unknown kind: fail visibly empty rather than mislabeled as "ask"
}

function ChProjTile({ kind, hero, project }) {
  return (
    <div className={"ch-proj-tile" + (hero ? " ch-proj-tile--hero" : "")}>
      <p className="ch-proj-tile-label">{CH_PROJ_TILE_LABEL[kind]}</p>
      <ChProjTileBody kind={kind} project={project} />
    </div>
  );
}

// Tabs are real controls (WAI-ARIA "manual activation" tabs pattern): a
// single shared tabpanel id (only the active project's panel ever exists in
// the DOM, so aria-controls on every tab points at the same id rather than
// four ids that don't all exist at once); roving tabindex on the tab
// buttons; Left/Right/Home/End move focus, Enter/Space activate natively
// because these are real <button> elements. Auto-cycle only runs when
// motion is allowed, and any click, keyboard move or focus into the tab
// strip cancels it for good: the demo never fights a reader for control.
// Below this width the grid collapses to a single column (see the 640px
// media query in tourCss) and the four projects' stacked heights diverge by
// hundreds of pixels, too much to reserve for without an oversized card at
// rest. Auto-cycle simply doesn't run here; height only ever changes from a
// deliberate tap, never from the page moving under a reader on its own.
const CH_PROJ_SMALL_QUERY = "(max-width: 640px)";

function ChProjectsScene() {
  const { useState, useEffect, useRef } = React;
  const reducedMotion = usePrefersReducedMotion();
  const [smallViewport, setSmallViewport] = useState(
    () => !!(window.matchMedia && window.matchMedia(CH_PROJ_SMALL_QUERY).matches)
  );
  const [activeId, setActiveId] = useState(CH_PROJECTS[0].id);
  const [focusId, setFocusId] = useState(CH_PROJECTS[0].id);
  const [tabFade, setTabFade] = useState(false); // right-edge "more tabs" cue
  const timerRef = useRef(null);
  const tabRefs = useRef({});
  const tabsStripRef = useRef(null);
  const activeIdRef = useRef(activeId); // lets the interval read the latest id without re-arming itself

  useEffect(() => { activeIdRef.current = activeId; }, [activeId]);

  useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia(CH_PROJ_SMALL_QUERY);
    const onChange = (e) => setSmallViewport(e.matches);
    mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange);
    return () => {
      mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange);
    };
  }, []);

  function stopAutoCycle() {
    if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
  }

  useEffect(() => {
    if (reducedMotion || smallViewport) return; // no auto-cycle under reduced motion, or once stacked tiles make height unreservable
    timerRef.current = setInterval(() => {
      // Derive the next id first, then two plain sets. setFocusId used to
      // fire from inside setActiveId's functional updater, which React may
      // invoke more than once per commit; a set call has no business living
      // inside another state setter's updater.
      const i = CH_PROJECTS.findIndex((p) => p.id === activeIdRef.current);
      const next = CH_PROJECTS[(i + 1) % CH_PROJECTS.length];
      setActiveId(next.id);
      setFocusId(next.id);
    }, 5600);
    return stopAutoCycle;
  }, [reducedMotion, smallViewport]);

  useEffect(() => {
    const el = tabsStripRef.current;
    if (!el) return;
    function updateFade() {
      // 1px slack absorbs the sub-pixel scrollLeft rounding some browsers report at the scroll end
      setTabFade(el.scrollWidth - el.clientWidth - el.scrollLeft > 1);
    }
    updateFade();
    el.addEventListener("scroll", updateFade, { passive: true });
    window.addEventListener("resize", updateFade);
    return () => {
      el.removeEventListener("scroll", updateFade);
      window.removeEventListener("resize", updateFade);
    };
  }, []);

  function selectTab(id) {
    stopAutoCycle(); // first interaction stops the cycle permanently
    setActiveId(id);
    setFocusId(id);
  }

  function onTabKeyDown(e, id) {
    const ids = CH_PROJECTS.map((p) => p.id);
    const i = ids.indexOf(id);
    let nextId = null;
    if (e.key === "ArrowRight") nextId = ids[(i + 1) % ids.length];
    else if (e.key === "ArrowLeft") nextId = ids[(i - 1 + ids.length) % ids.length];
    else if (e.key === "Home") nextId = ids[0];
    else if (e.key === "End") nextId = ids[ids.length - 1];
    if (!nextId) return; // Enter/Space activate natively via onClick
    e.preventDefault();
    stopAutoCycle();
    setFocusId(nextId);
    const el = tabRefs.current[nextId];
    if (el) el.focus();
  }

  const active = CH_PROJECTS.find((p) => p.id === activeId) || CH_PROJECTS[0];

  return (
    <div className="ch-scene ch-scene--flush ch-proj-card ch-rev" role="group" aria-label="A project card, and how it recomposes for different kinds of work">
      <ChSceneHead label="Project" live />
      <div className="ch-scene-body ch-proj-body">
        <div
          className={"ch-proj-tabs" + (tabFade ? " ch-proj-tabs--fade-right" : "")}
          ref={tabsStripRef}
          role="tablist"
          aria-label="Example projects"
          onFocus={stopAutoCycle}
        >
          {CH_PROJECTS.map((p) => (
            <button
              key={p.id}
              ref={(el) => { tabRefs.current[p.id] = el; }}
              id={"ch-proj-tab-" + p.id}
              role="tab"
              type="button"
              className={"ch-proj-tab" + (p.id === activeId ? " is-active" : "")}
              aria-selected={p.id === activeId}
              aria-controls="ch-proj-panel"
              tabIndex={p.id === focusId ? 0 : -1}
              onClick={() => selectTab(p.id)}
              onKeyDown={(e) => onTabKeyDown(e, p.id)}
            >
              {p.tab}
            </button>
          ))}
        </div>
        <div className="ch-proj-grid" id="ch-proj-panel" role="tabpanel" aria-labelledby={"ch-proj-tab-" + active.id} tabIndex={0} key={active.id}>
          {active.order.map((kind, i) => (
            <ChProjTile key={kind} kind={kind} hero={i === 0} project={active} />
          ))}
        </div>
      </div>
    </div>
  );
}

function ChProjects() {
  return (
    <Chapter id="projects">
      <p className="ch-lead">Set the outcome and Zarco builds the project around it. The project keeps itself moving and tells you when it needs you.</p>
      <ChProjectsScene />
    </Chapter>
  );
}
// ── Chapter 05 scene: one front door, many hands, one thread back ──────────
// A calm orchestration read on the dark panel idiom, sized like chapter 01's
// card (default .ch-scene, not --flush). Reads top to bottom as a single
// thread: a root line, a fan of four specialists each ticking off in their
// own ch-cascade beat, then a report line the fan resolves back into. The
// same connector rule runs the whole way down (root -> agents list's own
// left border -> report), so the branches read as taps off one thread
// rather than four separate wires. Static and decorative throughout: no
// buttons, no loop, nothing to click, and the completed state IS the state:
// reduced motion and the SEO bake see exactly what a scrolled-in reader
// sees once the cascade has settled. No magenta: this scene's job is calm,
// plural work quietly finishing, not a call to act.
const CH_WORK_ROOT = "EA: prepare the quarterly review";
const CH_WORK_AGENTS = [
  { id: "research", role: "Research", line: "pulled the quarter's account activity", sources: 4 },
  { id: "drafting", role: "Drafting", line: "wrote the executive summary", sources: 2 },
  { id: "crm",      role: "CRM",      line: "updated deal stages and next steps", sources: 3 },
  { id: "finance",  role: "Finance",  line: "reconciled invoiced against collected", sources: 3 },
];
const CH_WORK_REPORT = "Quarterly review pack ready";

function ChWorkforceScene() {
  const totalSources = CH_WORK_AGENTS.reduce((sum, a) => sum + a.sources, 0);
  return (
    <div className="ch-scene ch-rev" role="group" aria-label="Zarco's assistant handing quarterly-review work to four specialists, then reporting back as one">
      <ChSceneHead label="Zarco" live />
      <div className="ch-scene-body ch-work-body">
        <div className="ch-work-node ch-cascade" style={{ "--d": 0 }}>
          <span className="ch-work-dot" />
          <p className="ch-work-root-line">{CH_WORK_ROOT}</p>
        </div>
        <div className="ch-work-connector" aria-hidden="true" />
        <ul className="ch-work-agents">
          {CH_WORK_AGENTS.map((a, i) => (
            <li className="ch-work-agent ch-cascade" style={{ "--d": i + 1 }} key={a.id}>
              <span className="ch-work-agent-role">{a.role}:</span>
              <span className="ch-work-agent-line">{a.line}</span>
              <ChTickGlyph />
            </li>
          ))}
        </ul>
        <div className="ch-work-connector" aria-hidden="true" />
        <div className="ch-work-node ch-work-node--report ch-cascade" style={{ "--d": CH_WORK_AGENTS.length + 1 }}>
          <span className="ch-work-dot ch-work-dot--ok" />
          <div className="ch-work-report">
            <p className="ch-work-report-title">{CH_WORK_REPORT}</p>
            <span className="ch-work-report-sources">{totalSources} {totalSources === 1 ? "source" : "sources"}</span>
          </div>
        </div>
      </div>
    </div>
  );
}

function ChWorkforce() {
  return (
    <Chapter id="workforce">
      <p className="ch-lead">Behind your assistant sits a team of specialists: research, drafting, CRM, finance. It splits the work, keeps them honest, and reports back to you as one thread.</p>
      <ChWorkforceScene />
    </Chapter>
  );
}
// ── Chapter 06 scene: an answer with its source, then a quiet save ─────────
// Two beats on the dark panel idiom, reusing the ch-msg bubble language from
// chapter 01 rather than inventing new chrome. Beat one is the trust move:
// a provenance Q&A where the answer's two source chips are the hero, sitting
// right under the reply rather than tucked away. Beat two is a small dashed
// "Remembered" card easing in after: the system quietly writing memory,
// distinct from a chat bubble because it isn't one. Fenwick continuity with
// chapter 02's Slack beat is deliberate. Note: these source chips share
// .ch-chip-base with chapter 01's working chips, adding only their mono
// font as a delta (.ch-mem-source-chip); they deliberately skip the
// .ch-chip-animate modifier (see its opacity-trap comment in tourCss) since
// these chips must already be visible in the completed/baked state.
const CH_MEM_Q = "What did we agree with Fenwick on payment terms?";
const CH_MEM_A = "45 days, agreed 12 June.";
const CH_MEM_SOURCES = ["email · J. Fenwick", "signed quote"];
const CH_MEM_REMEMBERED = "Fenwick prefers calls after 2pm";

function ChBookmarkGlyph() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true">
      <path d="M4 2.5h8a1 1 0 0 1 1 1v9.3l-5-3-5 3V3.5a1 1 0 0 1 1-1Z" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
    </svg>
  );
}

function ChMemoryScene() {
  return (
    <div className="ch-scene ch-rev" role="group" aria-label="Zarco answering with its source, then quietly remembering a preference">
      <ChSceneHead label="Zarco" live />
      <div className="ch-scene-body">
        <div className="ch-msg ch-msg-user">
          <p>{CH_MEM_Q}</p>
        </div>
        <div className="ch-msg ch-msg-bot">
          <p>{CH_MEM_A}</p>
          <div className="ch-mem-sources">
            {CH_MEM_SOURCES.map((s) => (<span className="ch-chip-base ch-mem-source-chip" key={s}>{s}</span>))}
          </div>
        </div>
        <div className="ch-mem-remembered ch-cascade" style={{ "--d": 1 }}>
          <span className="ch-mem-remembered-icon"><ChBookmarkGlyph /></span>
          <div>
            <p className="ch-mem-remembered-kicker">Remembered</p>
            <p className="ch-mem-remembered-text">{CH_MEM_REMEMBERED}</p>
          </div>
        </div>
      </div>
    </div>
  );
}

function ChMemory() {
  return (
    <Chapter id="memory">
      <p className="ch-lead">Zarco keeps a knowledge base of how your company actually runs: your customers, your terms, your way of doing things. Every answer shows its source, and it gets sharper the longer it works with you.</p>
      <ChMemoryScene />
    </Chapter>
  );
}
// ── Chapter 07 scene: the Nebula, a living map of the whole company ────────
// The tour's cinematic peak: the only full-bleed dark interlude besides the
// hero photo and the Mission contact panel, so it earns the treatment rather
// than repeating the ChScene card idiom chapters 01-06 share. It does NOT go
// through the shared Chapter() wrapper (that wrapper assumes a light .shell
// column start to finish); instead it builds its own section so the ground
// can run edge to edge while the chapter-mark/h2/lead still sit in a .shell
// for column alignment with every other chapter.
//
// Ground is a pure --panel gradient field, not night.jpg: Mission already
// shows that photograph further down the same page, and a literal milky-way
// photo would compete with the CSS-drawn constellations rather than let them
// read as the hero. A gradient keeps the "nebula" idea (soft colour cloud,
// not a star photo) and gives full control over how the clusters sit on it.
//
// Three clusters (Customers / Projects / Knowledge) each render as a loose
// field of small glowing points, deterministically scattered (nebulaScatter)
// so the layout is stable across reloads and across the SEO bake rather than
// truly random. A few points per cluster carry quiet mono labels reusing
// names already seeded elsewhere in the tour (Harborview, Fenwick, the Q3
// renewals push, payment terms) so this reads as one company's data, not
// generic decoration. Harborview is the one magenta point: the same account
// chapters 03 and 04 already flag as going quiet, so the single loud accent
// in this scene ties back to a thread a reader has already seen.
//
// Motion is pure CSS: each cluster's field drifts on its own long, offset
// duration (parallax without JS), and reduced motion simply removes the
// animation, leaving the same composed arrangement the bake captures.
// The two moduli (101, 97) are coprime primes, so `a` and `b` each cycle
// through their full range before repeating and the pair as a whole doesn't
// fall into a short repeating pattern until both wrap round together: safe
// to raise n up to ~101 without the scatter visibly tiling.
function nebulaScatter(seed, n) {
  const pts = [];
  for (let i = 0; i < n; i++) {
    const a = (i * 53 + seed * 17) % 101;
    const b = (i * 31 + seed * 41) % 97;
    pts.push({
      x: 4 + (a / 101) * 92,
      y: 6 + (b / 97) * 88,
      s: 2 + ((i * 7 + seed) % 4),
      o: 0.16 + (((i * 11 + seed * 5) % 10) / 10) * 0.5,
    });
  }
  return pts;
}

// Each point's x/y is a percentage of its cluster's field box (.ch-nebula-field),
// not the viewport, so points stay put relative to their own cluster at every
// breakpoint. Magenta is rationed sitewide, not just within this scene: at
// most ONE point across every cluster here may carry hero:true.
const CH_NEBULA_CLUSTERS = [
  {
    id: "customers", label: "Customers", seed: 3,
    points: [
      { x: 20, y: 28, label: "Harborview", hero: true },
      { x: 60, y: 66, label: "Fenwick" },
    ],
  },
  {
    id: "projects", label: "Projects", seed: 7,
    points: [
      { x: 24, y: 62, label: "Q3 renewals push" },
      { x: 62, y: 24, label: "Product launch" },
    ],
  },
  {
    id: "knowledge", label: "Knowledge", seed: 11,
    points: [
      { x: 22, y: 30, label: "payment terms" },
      { x: 58, y: 64, label: "renewal terms" },
    ],
  },
];

function ChNebulaCluster({ cluster }) {
  const scatter = nebulaScatter(cluster.seed, 16);
  return (
    <div className={"ch-nebula-cluster ch-nebula-cluster--" + cluster.id}>
      <p className="ch-nebula-cluster-label">{cluster.label}</p>
      <div className="ch-nebula-field">
        {scatter.map((p, i) => (
          <span
            key={i}
            className="ch-nebula-dot"
            style={{ "--x": p.x + "%", "--y": p.y + "%", "--s": p.s + "px", "--o": p.o }}
          />
        ))}
        {cluster.points.map((p) => (
          <span
            key={p.label}
            className={"ch-nebula-point" + (p.hero ? " ch-nebula-point--hero" : "")}
            style={{ "--x": p.x + "%", "--y": p.y + "%" }}
          >
            <span className="ch-nebula-point-dot" />
            <span className="ch-nebula-point-label">{p.label}</span>
          </span>
        ))}
      </div>
    </div>
  );
}

// Purely illustrative: the paragraph above already states what the Nebula
// maps, so the field itself is aria-hidden rather than making a screen
// reader step through dozens of decorative dots and repeated place names.
function ChNebulaScene() {
  return (
    <div className="ch-nebula-scene" aria-hidden="true">
      {CH_NEBULA_CLUSTERS.map((c) => (<ChNebulaCluster key={c.id} cluster={c} />))}
    </div>
  );
}

function ChNebula() {
  const meta = CHAPTERS.find(c => c.id === "nebula") || { n: "", title: "" };
  return (
    <section className="ch-nebula ch-rev" id="nebula">
      <div className="ch-nebula-ground" aria-hidden="true" />
      <div className="shell ch-nebula-head">
        <p className="chapter-mark">{meta.n}</p>
        <h2 className="h2">{meta.title}</h2>
        <p className="ch-lead">The Nebula maps your customers, projects and knowledge as one living picture. The more Zarco works, the more of your business appears.</p>
      </div>
      <ChNebulaScene />
    </section>
  );
}
// ── Chapter 08 scene: the adoption process as a quiet numbered ledger ──────
// A light section on paper with no dark card, breaking the nebula-dark →
// manifesto → dark rhythm the tour has run so far: four steps as editorial
// rows, reusing the retired homepage offerings-row idiom (hairline dividers,
// Clash Display row titles) rather than a card grid, so this reads as the
// documented shape of a real process rather than a fourth icon grid on the
// page. Numbers are a quiet ink-40 sequence marker, same register as the
// sitewide .chapter-mark; this chapter spends none of the page's magenta
// budget, saving it for chapter 09 next door. Marked up as a real <ol>/<li>
// (this is an ordered process, not just four visually similar rows) with
// role="list" belt-and-braces on the <ol>: Safari/VoiceOver drops the
// implicit list semantics once list-style is set to none, so the role is
// there to survive that rather than rely on it never being needed.
const CH_PROCESS_STEPS = [
  { n: "01", title: "Map your workflows.", body: "We sit with your team and find the work worth handing over." },
  { n: "02", title: "Set up your instance.", body: "Your tools connected, your agents configured, and your AI policy written with you: who approves what, which systems agents may touch, what always needs a human. The Console enforces it from day one." },
  { n: "03", title: "Pilot with your team.", body: "A few weeks of real work with the people who will use it daily." },
  { n: "04", title: "Expand what works.", body: "More workflows, more roles, at the pace the results earn." },
];

function ChProcessSteps() {
  return (
    <ol className="ch-process-list ch-rev" role="list">
      {CH_PROCESS_STEPS.map((s, i) => (
        <li className="ch-process-row ch-hairline-row ch-cascade" style={{ "--d": i }} key={s.n}>
          <span className="ch-process-n">{s.n}</span>
          <div className="ch-process-copy">
            <h3 className="ch-process-title">{s.title}</h3>
            <p className="ch-process-body">{s.body}</p>
          </div>
        </li>
      ))}
    </ol>
  );
}

function ChProcess() {
  return (
    <Chapter id="process">
      <p className="ch-lead">The models are already good enough. Adoption is the bottleneck. So we do not hand you a login and wish you luck: we set your instance up around your actual business.</p>
      <ChProcessSteps />
    </Chapter>
  );
}

// ── Chapter 09 scene: what it costs, framed as an anchor rather than a figure ──
// Deliberately carries no currency: the case rests on the comparison, not a
// number this page would have to keep accurate. A real <table> rather than
// two parallel lists: the pairing (recruiting time against live in weeks,
// and so on) is the entire rhetorical point, and two <ul>s read end to end
// by a screen reader would announce all four "hire" rows before any "Zarco"
// row, losing the correspondence entirely. One paired data array drives
// both columns so the pairing is structural, not just adjacent markup.
// CSS below turns the table into a two-column grid to keep chapter 08's
// editorial-row look; role/scope attributes on the JSX side keep it reading
// as a table even though display is overridden away from
// table/table-row/table-cell (Safari/VoiceOver can drop the implicit table
// roles once that happens, same class of gotcha as chapter 08's list). The
// Zarco column takes the view's one magenta accent as a hairline edge
// rather than a badge, which is also why the closing CTA below stays a
// quiet sitewide text link (.btn-text) instead of a second loud magenta
// button.
const CH_COST_PAIRS = [
  { hire: "months to recruit", zarco: "live in weeks" },
  { hire: "salary, holiday, management", zarco: "one monthly invoice with a spend cap" },
  { hire: "one pair of hands", zarco: "works every role it is given" },
  { hire: "gone in two years", zarco: "cancel any month" },
];

function ChCostScene() {
  return (
    <table className="ch-cost-table ch-rev" role="table" aria-label="Zarco compared with the hire it replaces">
      <thead role="rowgroup">
        <tr role="row">
          <th scope="col" role="columnheader" className="ch-cost-th">A new hire</th>
          <th scope="col" role="columnheader" className="ch-cost-th ch-cost-th--zarco">Zarco</th>
        </tr>
      </thead>
      <tbody role="rowgroup">
        {CH_COST_PAIRS.map((p, i) => (
          <tr role="row" key={p.hire}>
            <td role="cell" className="ch-cost-cell ch-hairline-row ch-cascade" style={{ "--d": i }}>{p.hire}</td>
            <td role="cell" className="ch-cost-cell ch-cost-cell--zarco ch-hairline-row ch-cascade" style={{ "--d": i }}>{p.zarco}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function ChCost() {
  return (
    <Chapter id="cost">
      <p className="ch-lead">A fraction of the hire it replaces.</p>
      <ChCostScene />
      <p className="ch-cost-close ch-rev">Exact numbers depend on your setup. Tell us what you are replacing and we will give you a straight answer. <a className="btn-text" href="index.html#contact">Discuss a project</a></p>
    </Chapter>
  );
}
const tourCss = `
.ch-chapter { padding-top: clamp(72px, 10vw, 140px); padding-bottom: clamp(72px, 10vw, 140px); }

/* ── shared entrance cascade ──
   Any element carrying ch-cascade rises in with a --d-indexed stagger once
   a revealed ancestor gets .in from the sitewide IntersectionObserver (the
   reveal contract, both the JS toggle and the generic .hp-rev/.ch-rev CSS,
   lives in Glyphs.jsx; .ch-rev is this file's alias of it, and this file's
   own CSS only adds embellishments on top). Scenes opt in by adding the
   class instead of writing their own transition block; the rise
   distance/duration/step are named once here (14px, .55s, .12s, chapter
   02's original values). */
.ch-cascade {
  --ch-cascade-rise: 14px; --ch-cascade-dur: .55s; --ch-cascade-step: .12s;
  opacity: 0; transform: translateY(var(--ch-cascade-rise));
  transition: opacity var(--ch-cascade-dur) var(--ease,ease), transform var(--ch-cascade-dur) var(--ease,ease);
  transition-delay: calc(var(--d, 0) * var(--ch-cascade-step));
}
.in .ch-cascade { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .ch-cascade { opacity: 1; transform: none; }
}

/* ── shared hairline-row divider ──
   The border-top divider repeated down a vertical run of rows, used by
   chapter 08's process steps and chapter 09's comparison rows so the rule
   is written once instead of copied per chapter. Only the divider itself is
   shared: the closing border-bottom on the final row is left to each
   consumer, because "last row" means something different in a flat list
   (:last-child on the row) versus a table body (:last-child on the <tr>,
   not on the <td> the class sits on). */
.ch-hairline-row { border-top: 1px solid var(--ink-10); }

/* ── chapter 01: talk-to-it scene ── */
.ch-lead { margin: 22px 0 0; max-width: 58ch; color: var(--ink-60); font-size: 18px; line-height: 1.55; }
.ch-scene {
  margin-top: clamp(36px, 5vh, 52px); max-width: 620px;
  background: var(--panel); border-radius: 22px; overflow: hidden;
  border: 1px solid var(--panel-line);
  box-shadow: 0 40px 90px -36px rgba(14,14,14,.5);
}
/* Any scene that needs its own margin-top/max-width (nested in a layout
   grid, or simply wider than the default chat-card size) strips the base
   values here instead of a one-off ancestor-scoped override, then sets its
   own via a second class. */
.ch-scene--flush { margin-top: 0; max-width: none; }
.ch-scene-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--panel-line); }
.ch-scene-id { display: flex; align-items: center; gap: 9px; color: var(--panel-ink); font-size: 14px; font-weight: 500; }
.ch-scene-mark { width: 20px; height: 20px; border-radius: 6px; background: var(--paper); display: inline-flex; align-items: center; justify-content: center; padding: 3px; }
.ch-scene-mark svg { width: 100%; height: 100%; }
.ch-scene-live { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--panel-ink-60); }
.ch-scene-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--panel-ok); flex: none; }
.ch-scene-body { padding: 20px 20px 24px; display: flex; flex-direction: column; gap: 14px; min-height: 168px; }

.ch-msg { display: flex; flex-direction: column; gap: 6px; }
.ch-msg-user { align-items: flex-end; }
.ch-msg-bot { align-items: flex-start; }
.ch-msg-user p { margin: 0; max-width: 88%; padding: 10px 14px; font-size: 14px; line-height: 1.5; background: rgba(255,255,255,.1); border: 1px solid var(--magenta); color: var(--panel-ink); border-radius: 14px 14px 4px 14px; min-height: 1.5em; }
.ch-msg-bot p { margin: 0; max-width: 92%; padding: 11px 15px; font-size: 14px; line-height: 1.55; background: rgba(255,255,255,.07); border: 1px solid var(--panel-line); color: var(--panel-ink); border-radius: 4px 14px 14px 14px; }
.ch-time { font-family: var(--font-mono); font-size: 10.5px; color: var(--panel-ink-40); letter-spacing: .02em; }

.ch-working { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--panel-ink-40); }
.ch-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--panel-ink-40); display: inline-block; margin-right: 3px; animation: ch-bnc 1s infinite; }
.ch-dots i:nth-child(2) { animation-delay: .15s; }
.ch-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes ch-bnc { 0%,80%,100% { transform: translateY(0); opacity: .4; } 40% { transform: translateY(-4px); opacity: 1; } }
.ch-chips { display: flex; flex-wrap: wrap; gap: 7px; }
/* Shared static pill recipe: background/border/radius/padding plus the
   layout and colour every chip on this scene agrees on. Chapter 06's source
   chips build on this too (see .ch-mem-source-chip), adding only their mono
   font as a delta rather than repeating the whole shape. */
.ch-chip-base {
  display: inline-flex; align-items: center; background: rgba(255,255,255,.06); border: 1px solid var(--panel-line);
  color: var(--panel-ink-60); font-size: 12px; border-radius: 999px; padding: 5px 12px;
}
/* ⚠️ Opacity trap: this modifier mounts the chip at opacity:0 and only
   reveals it once the keyframe fires. That's correct for chapter 01's
   transient working-chips, which are only ever mounted mid-animation and
   never rendered under reduced motion, but never put it on a chip that must
   already be visible in the resting or baked state, or it stays invisible. */
.ch-chip-animate { opacity: 0; animation: ch-chip-in .3s var(--ease,ease) forwards; }
@keyframes ch-chip-in { to { opacity: 1; } }

.ch-artifact { display: inline-flex; align-items: center; gap: 9px; background: rgba(255,255,255,.06); border: 1px solid var(--panel-line); border-radius: 12px; padding: 8px 13px; margin-top: 2px; align-self: flex-start; }
.ch-artifact-icon { width: 18px; height: 18px; flex: none; display: inline-flex; }
.ch-artifact-icon svg { width: 100%; height: 100%; }
.ch-artifact-name { font-family: var(--font-mono); font-size: 12px; color: var(--panel-ink-60); letter-spacing: .01em; }
.ch-msg-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 2px; align-self: stretch; }
.ch-risk { font-size: 12.5px; color: var(--panel-ink-40); }

@media (max-width: 640px) {
  .ch-scene { max-width: none; border-radius: 18px; }
  .ch-scene-body { padding: 16px 16px 20px; }
}

/* ── chapter 02: channels split-panel scene ── */
.ch-split {
  margin-top: clamp(36px, 5vh, 52px); max-width: 880px;
  display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
}
.ch-channel {
  background: var(--panel); border-radius: 20px; overflow: hidden;
  border: 1px solid var(--panel-line); display: flex; flex-direction: column;
  box-shadow: 0 32px 70px -34px rgba(14,14,14,.45);
}
.ch-channel-head { display: flex; align-items: center; justify-content: space-between; padding: 13px 16px; border-bottom: 1px solid var(--panel-line); }
.ch-channel-app { display: flex; align-items: center; gap: 7px; color: var(--panel-ink-60); font-size: 12.5px; font-weight: 500; }
.ch-channel-app svg { width: 16px; height: 16px; border-radius: 4px; flex: none; }
.ch-channel-room { font-family: var(--font-mono); font-size: 12px; color: var(--panel-ink); letter-spacing: .01em; }
.ch-channel-body { padding: 16px 16px 18px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.ch-done { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--panel-ink-60); margin-top: 2px; }
.ch-done svg { width: 12px; height: 12px; flex: none; }
.ch-tick-glyph { stroke: var(--panel-ok); }

/* settle-in cascade for the two channel bubbles: shared ch-cascade utility
   above, opted into via className on each .ch-msg (see ChChannelsScene) */
@media (max-width: 700px) {
  .ch-split { grid-template-columns: 1fr; }
}

/* ── chapter 03: inbox approvals scene ──
   Two beats on the dark panel idiom: an approval queue and a compact
   pushback exchange. Both reuse .ch-scene/.ch-scene-head/.ch-msg rather than
   inventing new chrome; each carries ch-scene--flush (defined above) since
   they sit inside a two-up grid that already supplies the outer spacing. */
.ch-inbox-grid {
  margin-top: clamp(36px, 5vh, 52px); max-width: 960px;
  display: grid; grid-template-columns: 1.3fr 1fr; gap: 20px; align-items: start;
}

.ch-queue-count { font-size: 12px; color: var(--panel-ink-60); }
.ch-queue-body { gap: 18px; }
.ch-queue-top { display: flex; flex-direction: column; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--panel-line); }
.ch-queue-top-label { margin: 0; font-size: 15px; line-height: 1.4; color: var(--panel-ink); font-weight: 500; }
.ch-queue-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* Not folded onto .ch-chip-base: padding (7px/14px vs the base's 5px/12px)
   and the transparent-by-default border (so the magenta approve pill stays
   borderless) both genuinely differ, so sharing the base would either
   distort this pill's size or paint a hairline onto the approve state. */
.ch-pill { display: inline-flex; align-items: center; justify-content: center; font-size: 12.5px; font-weight: 500; border-radius: 999px; padding: 7px 14px; border: 1px solid transparent; white-space: nowrap; }
.ch-pill-approve { background: var(--magenta); color: #fff; }
.ch-pill-quiet { background: rgba(255,255,255,.06); border-color: var(--panel-line); color: var(--panel-ink-60); }
.ch-queue-rest { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.ch-queue-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--panel-ink-40); }
/* Not folded onto .ch-chip-base: padding (2px/8px, a much shorter pill) and
   no border at all, so the base's border would be a visible addition here. */
.ch-queue-row-count { font-family: var(--font-mono); font-size: 11px; color: var(--panel-ink-40); background: rgba(255,255,255,.06); border-radius: 999px; padding: 2px 8px; flex: none; }
.ch-queue-foot { display: flex; align-items: center; gap: 8px; padding: 13px 20px; border-top: 1px solid var(--panel-line); color: var(--panel-ink-60); font-size: 12px; }
.ch-queue-foot svg { width: 14px; height: 14px; flex: none; }

@media (max-width: 760px) {
  .ch-inbox-grid { grid-template-columns: 1fr; max-width: 620px; }
}
@media (max-width: 420px) {
  .ch-queue-actions { gap: 6px; }
  .ch-pill { padding: 7px 11px; font-size: 12px; }
}

/* ── chapter 04: projects anatomy scene ──
   One tabbed card on the dark panel idiom (ch-scene--flush sets its own
   width). A tab strip picks the example project; the panel below is a
   5-tile anatomy grid built by mapping over that project's 'order' array,
   so the DOM order itself changes per project, not just a size class: the
   grid genuinely recomposes rather than one tile growing in place. The
   panel remounts on tab change (key={project.id}) so ch-proj-grid-in can
   replay the fade/rise on fresh content; the sitewide reduced-motion kill
   switch (site.css) already strips all animation, so no separate guard is
   needed here for that transition. */
.ch-scene.ch-proj-card { margin-top: clamp(36px, 5vh, 52px); max-width: 900px; }
/* Reserves height for the tallest of the four projects at the current
   breakpoint so the auto-cycle in ChProjectsScene never shifts the page
   under a reader (measured live via puppeteer against all four variants:
   401px tall at >=1025px card width, up to 498px right above the 640px
   single-column breakpoint). Below 640px the grid stacks to one column and
   the spread grows past what's worth reserving for at rest, so the
   component stops auto-cycling there instead (smallViewport check above)
   and this floor is just a harmless no-op under the naturally taller stack. */
.ch-proj-body { gap: 18px; min-height: 520px; }
@media (min-width: 861px) { .ch-proj-body { min-height: 455px; } }
@media (min-width: 1025px) { .ch-proj-body { min-height: 420px; } }

.ch-proj-tabs { display: flex; gap: 6px; overflow-x: auto; min-width: 0; padding-bottom: 2px; scrollbar-width: none; }
.ch-proj-tabs::-webkit-scrollbar { display: none; }
/* Right-edge continuation cue: ChProjectsScene toggles this class from a
   scroll listener once the strip actually overflows and isn't scrolled all
   the way right (never shown at desktop widths where all four tabs fit).
   Mask fades into the panel's own dark background rather than an overlay
   box, same technique as .hp-marquee in App.jsx. */
.ch-proj-tabs--fade-right {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 36px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 36px), transparent);
}
.ch-proj-tab {
  flex: none; white-space: nowrap; font: inherit; font-size: 13px; font-weight: 500;
  color: var(--panel-ink-60); background: transparent; border: 1px solid transparent;
  border-radius: 999px; padding: 8px 14px; cursor: pointer;
  transition: color .2s var(--ease,ease), background .2s var(--ease,ease), border-color .2s var(--ease,ease);
}
.ch-proj-tab:hover { color: var(--panel-ink); }
.ch-proj-tab.is-active { color: #fff; background: var(--magenta); border-color: var(--magenta); }

.ch-proj-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
  animation: ch-proj-grid-in .42s var(--ease,ease) both;
}
@keyframes ch-proj-grid-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.ch-proj-tile {
  background: rgba(255,255,255,.05); border: 1px solid var(--panel-line); border-radius: 14px;
  padding: 14px 15px; display: flex; flex-direction: column; gap: 8px; min-width: 0;
}
.ch-proj-tile--hero { grid-column: span 2; }
.ch-proj-tile-label { margin: 0; font-size: 11.5px; color: var(--panel-ink-40); letter-spacing: .01em; }

.ch-proj-outcome-headline { margin: 0; font-size: 15px; line-height: 1.4; color: var(--panel-ink); font-weight: 500; }
.ch-proj-tile--hero .ch-proj-outcome-headline { font-size: 17px; }
.ch-proj-health { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--panel-ink-60); flex-wrap: wrap; }
.ch-proj-health-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--panel-ink-40); } /* fallback for a status value outside the "ok" | "warn" enum, so a typo degrades to a neutral dot instead of vanishing */
.ch-proj-health-dot--ok { background: var(--panel-ok); }
.ch-proj-health-dot--warn { background: var(--panel-warn); }
.ch-proj-health-sep { color: var(--panel-ink-40); }

.ch-proj-journey { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ch-proj-stage { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.ch-proj-stage-count { font-family: var(--font-mono); font-size: 15px; color: var(--panel-ink); }
.ch-proj-tile--hero .ch-proj-stage-count { font-size: 18px; }
.ch-proj-stage-label { font-size: 11.5px; color: var(--panel-ink-60); }
.ch-proj-stage-note { font-size: 10.5px; color: var(--panel-warn); }
.ch-proj-arrow { width: 12px; height: 12px; color: var(--panel-ink-40); flex: none; }

.ch-proj-routine-when { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--panel-ink-60); }
.ch-proj-routine-what { margin: 0; font-size: 13.5px; color: var(--panel-ink); }
.ch-proj-tile--hero .ch-proj-routine-what { font-size: 15px; }
.ch-proj-routine-owner { margin: 0; font-size: 12px; color: var(--panel-ink-40); }

.ch-proj-signal-text { margin: 0; font-size: 13.5px; color: var(--panel-ink); line-height: 1.4; }
.ch-proj-tile--hero .ch-proj-signal-text { font-size: 15px; }
.ch-proj-signal-dest { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--panel-ink-40); }
.ch-proj-signal-dest svg { width: 11px; height: 11px; flex: none; color: var(--panel-ink-40); }

.ch-proj-ask-q { margin: 0; font-size: 13px; color: var(--panel-ink-60); }
.ch-proj-ask-a { margin: 0; font-size: 13.5px; color: var(--panel-ink); line-height: 1.4; }
.ch-proj-ask-source { margin: 0; font-size: 11px; color: var(--panel-ink-40); }

@media (max-width: 640px) {
  .ch-scene.ch-proj-card { max-width: none; border-radius: 18px; }
  .ch-proj-grid { grid-template-columns: 1fr; gap: 10px; }
  .ch-proj-tile--hero { grid-column: auto; }
  /* auto-cycle is stopped at this width (see ChProjectsScene), so nothing
     needs its height reserved here; the stacked column is already taller
     than the desktop floor above on its own. */
  .ch-proj-body { min-height: 0; }
}

/* ── chapter 05: workforce orchestration scene ──
   One thread top to bottom: a root line, a connector, a fan of specialist
   rows hung off the agents list's own left border, another connector, then
   the report line. ch-work-body zeroes the base .ch-scene-body gap so the
   connector divs (fixed height, standing in for spacing) are the only thing
   setting the rhythm between beats. */
.ch-work-body { gap: 0; }
.ch-work-node { display: flex; align-items: center; gap: 12px; }
.ch-work-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--panel-ink-40); flex: none; }
.ch-work-dot--ok { background: var(--panel-ok); }
.ch-work-root-line { margin: 0; font-size: 14.5px; font-weight: 500; color: var(--panel-ink); }
.ch-work-connector { width: 1px; height: 16px; margin-left: 3.5px; background: var(--panel-line); }
.ch-work-agents {
  list-style: none; margin: 0 0 0 4px; padding: 2px 0 2px 22px;
  border-left: 1px solid var(--panel-line);
  display: flex; flex-direction: column; gap: 13px;
}
.ch-work-agent { position: relative; display: grid; grid-template-columns: auto 1fr auto; align-items: baseline; column-gap: 8px; }
/* Centred on the row's own box (not a hand-measured pixel offset) so the
   connector still lands mid-row once the line column wraps to two lines at
   narrow widths, rather than pointing at the first line only. */
.ch-work-agent::before { content: ""; position: absolute; left: -22px; top: 50%; transform: translateY(-50%); width: 16px; height: 1px; background: var(--panel-line); }
.ch-work-agent-role { font-size: 13px; font-weight: 600; color: var(--panel-ink); }
.ch-work-agent-line { font-size: 13px; color: var(--panel-ink-60); min-width: 0; }
.ch-work-agent .ch-tick-glyph { width: 13px; height: 13px; align-self: center; }
.ch-work-node--report { align-items: flex-start; }
.ch-work-node--report .ch-work-dot { margin-top: 5px; }
.ch-work-report { display: flex; flex-direction: column; gap: 3px; }
.ch-work-report-title { margin: 0; font-size: 15px; font-weight: 500; color: var(--panel-ink); }
.ch-work-report-sources { font-family: var(--font-mono); font-size: 11.5px; color: var(--panel-ink-40); letter-spacing: .01em; }

/* ── chapter 06: memory + knowledge base scene ──
   A provenance Q&A (the two source chips are the hero, sitting right under
   the reply) then a quiet dashed "Remembered" card easing in after. The
   source chips build on .ch-chip-base and add only their mono font as a
   delta: see the comment above ChMemoryScene for why they skip the
   animated modifier. */
.ch-mem-sources { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
.ch-mem-source-chip { font-family: var(--font-mono); font-size: 11px; letter-spacing: .01em; }
/* Dashed border is this tour's semantic marker for a system-authored aside,
   Zarco quietly writing something down on its own initiative, and it must
   never be reused for a draft or a pending action still waiting on a human
   (those stay solid: .ch-msg-bot, .ch-pill). Later chapters adding their
   own asides should follow this contract rather than invent a new visual
   language for "the system did this quietly". */
.ch-mem-remembered {
  display: flex; align-items: flex-start; gap: 12px; margin-top: 4px; padding: 14px 16px;
  border-radius: 14px; border: 1px dashed var(--panel-line); background: rgba(255,255,255,.035);
}
.ch-mem-remembered-icon { width: 16px; height: 16px; flex: none; margin-top: 2px; color: var(--panel-ink-40); }
.ch-mem-remembered-icon svg { width: 100%; height: 100%; }
.ch-mem-remembered-kicker { margin: 0 0 2px; font-size: 11px; color: var(--panel-ink-40); letter-spacing: .02em; }
.ch-mem-remembered-text { margin: 0; font-size: 13.5px; color: var(--panel-ink); line-height: 1.4; }

/* ── chapter 07: the Nebula, full-bleed dark interlude ──
   Breaks from the ch-scene card idiom every other chapter uses: the whole
   section is the dark ground (not a card floating on paper), so it needs
   its own padding/overflow handling rather than the shared .ch-chapter
   rule. overflow-x:clip guards against the drifting cluster layers ever
   registering as phantom horizontal scroll; longhand top/bottom padding
   on both the section and the .shell head avoids the shorthand-on-.shell
   gutter gotcha (a padding: shorthand here would zero the side gutter). */
.ch-nebula {
  position: relative; overflow-x: clip;
  padding-top: clamp(80px, 11vw, 150px); padding-bottom: clamp(64px, 9vw, 120px);
  background: var(--panel);
}
.ch-nebula-ground {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(58% 66% at 16% 18%, oklch(0.28 0.06 330 / .55), transparent 65%),
    radial-gradient(52% 62% at 84% 28%, oklch(0.25 0.05 255 / .48), transparent 68%),
    radial-gradient(66% 74% at 50% 104%, oklch(0.24 0.045 205 / .42), transparent 72%);
}
.ch-nebula-ground::after {
  content: ""; position: absolute; inset: 0; opacity: .05;
  /* Glyphs.jsx must load before this script tag sets window.GRAIN_SVG_URL; if it doesn't, this silently renders no texture rather than erroring. */
  background-image: url("${window.GRAIN_SVG_URL}");
}
.ch-nebula-head { position: relative; z-index: 2; }
.ch-nebula .chapter-mark { color: var(--panel-ink-40); }
.ch-nebula .h2 { color: var(--panel-ink); }
.ch-nebula .ch-lead { color: var(--panel-ink-60); }

.ch-nebula-scene {
  position: relative; z-index: 1; margin-top: clamp(48px, 7vh, 84px);
  display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(20px, 4vw, 56px);
  padding-left: clamp(20px, 6vw, 64px); padding-right: clamp(20px, 6vw, 64px);
}
.ch-nebula-cluster { position: relative; min-height: clamp(240px, 30vw, 360px); }
.ch-nebula-cluster-label { margin: 0 0 10px; font-size: 14.5px; font-weight: 500; color: var(--panel-ink-60); }
.ch-nebula-field { position: absolute; inset: 34px 0 0; }
.ch-nebula-cluster--customers .ch-nebula-field { animation: ch-nebula-drift-a 46s ease-in-out infinite; }
.ch-nebula-cluster--projects .ch-nebula-field { animation: ch-nebula-drift-b 58s ease-in-out infinite; }
.ch-nebula-cluster--knowledge .ch-nebula-field { animation: ch-nebula-drift-c 68s ease-in-out infinite; }
@keyframes ch-nebula-drift-a { 0%,100% { transform: translate(0,0); } 50% { transform: translate(14px,-10px); } }
@keyframes ch-nebula-drift-b { 0%,100% { transform: translate(0,0); } 50% { transform: translate(-12px,12px); } }
@keyframes ch-nebula-drift-c { 0%,100% { transform: translate(0,0); } 50% { transform: translate(10px,13px); } }

.ch-nebula-dot {
  position: absolute; left: var(--x); top: var(--y); width: var(--s); height: var(--s);
  border-radius: 50%; background: #fff; opacity: var(--o);
  box-shadow: 0 0 calc(var(--s) * 1.8) rgba(255,255,255,.4);
}
.ch-nebula-point {
  position: absolute; left: var(--x); top: var(--y); transform: translate(-3px, -3px);
  display: flex; align-items: center; gap: 8px;
}
.ch-nebula-point-dot {
  width: 5px; height: 5px; flex: none; border-radius: 50%; background: #fff; opacity: .85;
  box-shadow: 0 0 9px rgba(255,255,255,.55);
}
.ch-nebula-point-label {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .01em; white-space: nowrap;
  color: var(--panel-ink-60); text-shadow: 0 1px 6px rgba(0,0,0,.55);
}
/* The one loud accent in this scene: Harborview, the same account chapters
   03 and 04 already flag as quiet, called out here in magenta rather than
   starlight white, with a slow pulse standing in for "still live". */
.ch-nebula-point--hero .ch-nebula-point-dot {
  width: 8px; height: 8px; background: var(--magenta); opacity: 1;
  box-shadow: 0 0 14px rgba(255,0,102,.6);
  animation: ch-nebula-pulse 2.6s ease-in-out infinite;
}
.ch-nebula-point--hero .ch-nebula-point-label { color: var(--panel-ink); }
@keyframes ch-nebula-pulse {
  0%,100% { box-shadow: 0 0 14px rgba(255,0,102,.6); }
  50% { box-shadow: 0 0 22px rgba(255,0,102,.9); }
}

@media (max-width: 760px) {
  .ch-nebula-scene { grid-template-columns: 1fr; gap: 40px; }
  .ch-nebula-cluster { min-height: clamp(180px, 52vw, 260px); }
}
@media (prefers-reduced-motion: reduce) {
  .ch-nebula-field { animation: none; }
  .ch-nebula-point--hero .ch-nebula-point-dot { animation: none; }
}

/* ── chapter 08: adoption process, a quiet numbered ledger ──
   Light section, no dark card: four rows on paper, reusing the retired
   homepage offerings-row idiom (hairline top border, Clash Display row
   titles) rather than a card grid. Numbers sit in the same quiet register as
   the sitewide .chapter-mark: font-sans, not mono (JetBrains Mono stays data
   only, inside Console mockups, never section grammar per DESIGN.md). */
.ch-process-list { margin-top: clamp(40px, 6vh, 60px); max-width: 760px; list-style: none; padding: 0; }
.ch-process-row {
  display: grid; grid-template-columns: clamp(44px, 6vw, 64px) 1fr; gap: clamp(18px, 3vw, 32px);
  align-items: baseline; padding: clamp(26px, 3.6vh, 36px) 0;
}
.ch-process-row:last-child { border-bottom: 1px solid var(--ink-10); }
.ch-process-n { font-family: var(--font-sans); font-weight: 500; font-size: 15px; color: var(--ink-40); }
.ch-process-title { font-family: var(--font-display); font-weight: 400; font-size: clamp(24px, 2.6vw, 34px); letter-spacing: -.02em; line-height: 1.15; margin: 0 0 10px; color: var(--ink); }
.ch-process-body { margin: 0; max-width: 56ch; color: var(--ink-60); font-size: 16px; line-height: 1.55; }
@media (max-width: 640px) {
  .ch-process-row { grid-template-columns: 1fr; gap: 8px; }
}

/* ── chapter 09: what it costs, an anchor comparison, not a figure ──
   No currency anywhere: the case rests on the comparison rather than a
   number this page would have to keep current. Same editorial-row register
   as chapter 08. The table is set to display:grid so <thead>/<tbody>/<tr>
   collapse to display:contents and every <th>/<td> lands as a direct grid
   item; with two grid columns that auto-places header cells then each
   row's pair of cells in DOM order, giving the desktop two-column look
   without abandoning real table markup. display:contents also means the
   entrance cascade (ch-cascade, opacity/transform) has to sit on the <td>
   cells themselves, not the <tr>: a <tr> with display:contents has no box
   of its own to animate. The Zarco column carries the view's one magenta
   accent as a hairline edge, which is also why the closing line below
   reaches for the sitewide quiet .btn-text link rather than a second loud
   magenta button. */
.ch-cost-table { display: grid; grid-template-columns: 1fr 1fr; column-gap: clamp(32px, 5vw, 64px); width: 100%; max-width: 820px; margin-top: clamp(40px, 6vh, 60px); }
.ch-cost-table thead, .ch-cost-table tbody, .ch-cost-table tr { display: contents; }
.ch-cost-th { font-family: var(--font-display); font-weight: 400; font-size: clamp(20px, 2.2vw, 26px); letter-spacing: -.015em; text-align: left; margin: 0; padding: 0 0 16px; color: var(--ink-40); }
.ch-cost-th--zarco { color: var(--ink); padding-left: clamp(20px, 3vw, 28px); border-left: 2px solid var(--magenta); }
.ch-cost-cell { padding: 14px 0; font-size: 16px; line-height: 1.5; color: var(--ink-60); }
.ch-cost-table tbody tr:last-child .ch-cost-cell { border-bottom: 1px solid var(--ink-10); }
.ch-cost-cell--zarco { color: var(--ink); padding-left: clamp(20px, 3vw, 28px); border-left: 2px solid var(--magenta); }
.ch-cost-close { margin-top: clamp(36px, 5vh, 52px); max-width: 54ch; font-size: 18px; line-height: 1.55; color: var(--ink-60); }
@media (max-width: 640px) {
  .ch-cost-table { grid-template-columns: 1fr; }
  .ch-cost-th--zarco { padding-left: 0; padding-top: clamp(20px, 4vh, 28px); border-left: none; }
  .ch-cost-cell--zarco { padding-left: 0; padding-top: 10px; border-left: none; border-top: 2px solid var(--magenta); }
}
`;
window.ChTalk = ChTalk;
window.ChChannels = ChChannels;
window.ChInbox = ChInbox;
window.ChProjects = ChProjects;
window.ChWorkforce = ChWorkforce;
window.ChMemory = ChMemory;
window.ChNebula = ChNebula;
window.ChProcess = ChProcess;
window.ChCost = ChCost;
window.tourCss = tourCss;
