/* bdg living brand system — App shell + section components
   ====================================================================
   IA: persistent left rail (desktop) / hamburger overlay (≤1100px),
   scroll-driven progress bar, ⌘K search, click-to-copy, and 20 chapters
   (00 cover + 19 numbered sections grouped Positioning / Visual Identity
   / Application). All chapters are built out — no scaffolds in use.
   ==================================================================== */

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

/* --------------------------- IA / SECTIONS ------------------------------- */

const SECTIONS = [
  { id: "cover",        num: "00", label: "Home",                 kind: "Manifesto",  group: null },

  // Positioning
  { id: "foundation",   num: "01", label: "Brand Foundation",     kind: "Strategy",   group: "Positioning" },
  { id: "market",       num: "02", label: "Market & Competitors", kind: "Market",     group: "Positioning" },
  { id: "voice",        num: "03", label: "Messaging",            kind: "Language",   group: "Positioning" },
  { id: "edge",         num: "04", label: "Competitive Edge",     kind: "Position",   group: "Positioning" },

  // Visual Identity
  { id: "identity",     num: "05", label: "Logo",                 kind: "Marque",     group: "Visual Identity" },
  { id: "color",        num: "06", label: "Color",                kind: "Tokens",     group: "Visual Identity" },
  { id: "typography",   num: "07", label: "Typography",           kind: "Tokens",     group: "Visual Identity" },
  { id: "icons",        num: "08", label: "Iconography",          kind: "Marks",      group: "Visual Identity" },
  { id: "imagery",      num: "09", label: "Imagery",              kind: "Direction",  group: "Visual Identity" },
  { id: "motion",       num: "10", label: "Motion",               kind: "Principles", group: "Visual Identity" },
  { id: "dataviz",      num: "11", label: "Data Viz",             kind: "Kit",        group: "Visual Identity" },
  { id: "components",   num: "12", label: "UI Components",        kind: "Kit",        group: "Visual Identity" },
  { id: "layout",       num: "13", label: "Layout & Grid",        kind: "Tokens",     group: "Visual Identity" },

  // Application
  { id: "templates",    num: "14", label: "Templates",            kind: "Patterns",   group: "Application" },
  { id: "social",       num: "15", label: "Social Media",         kind: "Channel",    group: "Application" },
  { id: "print",        num: "16", label: "Print",                kind: "Channel",    group: "Application" },
  { id: "digital",      num: "17", label: "Digital & Web",        kind: "Channel",    group: "Application" },
  { id: "audiences",    num: "18", label: "Voice in Action",      kind: "Audience",   group: "Application" },
  { id: "decks",        num: "19", label: "Presentations",        kind: "Slides",     group: "Application" },
  { id: "lab",          num: "20", label: "Motion Lab",            kind: "Workbench",  group: "Application" },
];

const GROUPS = ["Positioning", "Visual Identity", "Application"];

/* --------------------------- UTIL HOOKS ---------------------------------- */

/* --------------------------- DOWNLOAD HELPERS ---------------------------- */
/* Single source of truth for file-fetch and SVG→PNG rasterization. The PNG
   path renders the existing SVG through a canvas at the requested size — no
   external libs, no build step. Paper-background is baked in so the file
   ships with a usable opaque background out of the box. */

function triggerDownload(filename, href) {
  const a = document.createElement("a");
  a.href = href;
  a.download = filename;
  a.rel = "noopener";
  document.body.appendChild(a);
  a.click();
  a.remove();
}

function downloadBlob(filename, blob) {
  const url = URL.createObjectURL(blob);
  triggerDownload(filename, url);
  setTimeout(() => URL.revokeObjectURL(url), 1500);
}

async function fetchAndDownload(filename, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Fetch ${url}: ${res.status}`);
  downloadBlob(filename, await res.blob());
}

let _logoSvgCache = null;
async function getLogoSvgText() {
  if (_logoSvgCache) return _logoSvgCache;
  const res = await fetch("../assets/logo.svg");
  _logoSvgCache = await res.text();
  return _logoSvgCache;
}

async function downloadLogoPng(size, filename, opts = {}) {
  const { background = "#FAFAFA", padding = 0.08 } = opts;
  const svgText = await getLogoSvgText();
  const svgBlob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(svgBlob);
  try {
    const img = new Image();
    img.crossOrigin = "anonymous";
    await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = url; });
    const canvas = document.createElement("canvas");
    canvas.width = size; canvas.height = size;
    const ctx = canvas.getContext("2d");
    if (background) { ctx.fillStyle = background; ctx.fillRect(0, 0, size, size); }
    /* The logo viewBox is 50×48. Fit inside (size · 1-2·padding) preserving aspect. */
    const inner = size * (1 - 2 * padding);
    const ar = 50 / 48;
    const w = ar >= 1 ? inner : inner * ar;
    const h = ar >= 1 ? inner / ar : inner;
    ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
    const blob = await new Promise(res => canvas.toBlob(res, "image/png", 1));
    if (blob) downloadBlob(filename, blob);
  } finally {
    URL.revokeObjectURL(url);
  }
}

function buildTokensJson() {
  const colors = [
    { name: "blue",   hex: "#3236FF", role: "Primary data · intent" },
    { name: "green",  hex: "#00D190", role: "Positive · alignment" },
    { name: "yellow", hex: "#FFE241", role: "Dark-mode signal · alert" },
    { name: "orange", hex: "#F55910", role: "Light-mode signal · highlight" },
    { name: "red",    hex: "#EF353E", role: "Negative · contradiction" },
    { name: "purple", hex: "#D7B3FF", role: "Hypothetical · soft accent" },
  ];
  return {
    $schema: "https://design-tokens.github.io/community-group/format/",
    bdg: {
      color: Object.fromEntries(colors.map(c => [c.name, { $type: "color", $value: c.hex, $description: c.role }])),
      typography: {
        family: {
          headline: { $value: "'Instrument Sans Condensed', sans-serif" },
          body:     { $value: "'Instrument Sans', system-ui, sans-serif" },
          mono:     { $value: "'DM Mono', ui-monospace, Menlo, monospace" },
          serif:    { $value: "'Instrument Serif', serif" },
        },
        bodyLadder: { L: { $value: "34px" }, M: { $value: "26px" }, S: { $value: "22px" }, XS: { $value: "18px" } },
      },
      border: { hair: { $value: "0.8px" }, radius: { $value: "0" } },
    },
  };
}

function buildTailwindPreset() {
  const date = new Date().toISOString().slice(0, 10);
  return `/** bdg — Tailwind v3 preset · generated ${date}
 * Drop into your tailwind.config.js as a preset:
 *   module.exports = { presets: [require('./tailwind-bdg.js')] }
 */
module.exports = {
  theme: {
    extend: {
      colors: {
        bdg: {
          blue:   '#3236FF',
          green:  '#00D190',
          yellow: '#FFE241',
          orange: '#F55910',
          red:    '#EF353E',
          purple: '#D7B3FF',
          ink:    '#09090B',
          paper:  '#FAFAFA',
        },
      },
      fontFamily: {
        sans:    ['Instrument Sans', 'system-ui', 'sans-serif'],
        display: ['Instrument Sans Condensed', 'sans-serif'],
        mono:    ['DM Mono', 'ui-monospace', 'Menlo', 'monospace'],
        serif:   ['Instrument Serif', 'serif'],
      },
      borderRadius: { DEFAULT: '0', chip: '4px', pill: '999px' },
      borderWidth:  { hair: '0.8px' },
      letterSpacing: { mono: '0.16em', headline: '-0.005em' },
    },
  },
};
`;
}

/* --------------------------- CLIPBOARD ----------------------------------- */
function useClipboard() {
  const [last, setLast] = useState(null);
  const timer = useRef();
  const copy = useCallback((value, label) => {
    try {
      navigator.clipboard.writeText(value);
    } catch (e) { /* swallow */ }
    setLast({ value, label: label || value, t: Date.now() });
    clearTimeout(timer.current);
    timer.current = setTimeout(() => setLast(null), 1800);
  }, []);
  return { last, copy };
}

function useRoute(defaultId) {
  const getId = () => {
    const h = (window.location.hash || "").replace(/^#\/?/, "");
    return h || defaultId;
  };
  const [route, setRoute] = useState(getId);
  useEffect(() => {
    const onHash = () => {
      setRoute(getId());
      window.scrollTo({ top: 0, behavior: "auto" });
    };
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);
  return route;
}

function useReadingProgress() {
  const [p, setP] = useState(0);
  useEffect(() => {
    const onScroll = () => {
      const h = document.documentElement;
      const max = h.scrollHeight - h.clientHeight;
      setP(max > 0 ? (h.scrollTop / max) * 100 : 0);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return p;
}

function useTheme() {
  const [t, setT] = useState(() => {
    if (typeof window === "undefined") return "light";
    return localStorage.getItem("bdg-theme") || "light";
  });
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", t);
    localStorage.setItem("bdg-theme", t);
  }, [t]);
  return [t, setT];
}

/* --------------------------- COVER FIELD --------------------------------- */
/* Subtle generative density field — reveals structure, never decorative.
   Two layers: a hairline grid with phase-shifted dots, plus a flowing line.
   Respects prefers-reduced-motion. */

function CoverField() {
  const ref = useRef(null);

  useEffect(() => {
    const cvs = ref.current;
    if (!cvs) return;
    const ctx = cvs.getContext("2d");
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let raf, t0 = performance.now();

    const fit = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const w = cvs.parentElement.clientWidth;
      const h = cvs.parentElement.clientHeight;
      cvs.width = w * dpr; cvs.height = h * dpr;
      cvs.style.width = w + "px"; cvs.style.height = h + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    fit();
    window.addEventListener("resize", fit);

    const draw = (t) => {
      const w = cvs.clientWidth, h = cvs.clientHeight;
      ctx.clearRect(0, 0, w, h);
      const ink = getComputedStyle(document.documentElement).getPropertyValue("--ink").trim();
      const signal = getComputedStyle(document.documentElement).getPropertyValue("--signal").trim();
      const step = 22;
      const cols = Math.ceil(w / step) + 1;
      const rows = Math.ceil(h / step) + 1;
      const time = reduce ? 0 : (t - t0) / 6000;

      ctx.fillStyle = ink;
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          const x = c * step;
          const y = r * step;
          const d = Math.sin((c * 0.31 + r * 0.27) + time) * 0.5 + 0.5;
          const size = d * 1.6 + 0.4;
          ctx.globalAlpha = 0.05 + d * 0.18;
          ctx.fillRect(x - size / 2, y - size / 2, size, size);
        }
      }
      // density line — represents the "system" reading
      ctx.globalAlpha = 0.4;
      ctx.strokeStyle = signal;
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (let x = 0; x < w; x += 4) {
        const phase = x / 180 + time * 1.5;
        const y = h * 0.62 + Math.sin(phase) * 12 + Math.sin(phase * 2.3) * 6;
        if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
      }
      ctx.stroke();
      ctx.globalAlpha = 1;
      if (!reduce) raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", fit); };
  }, []);

  return <canvas ref={ref} className="cover-field" aria-hidden="true" />;
}

/* --------------------------- HOME ---------------------------------------- */

const HOME_PATH = [
  { id: "foundation", num: "01", title: "Brand Foundation",  why: "Where the brand stands. Positioning, archetype, promise." },
  { id: "voice",      num: "03", title: "Messaging",         why: "How the brand sounds. Tone, phrases, sentence style." },
  { id: "color",      num: "06", title: "Color",             why: "The palette and the ratio. 70 paper · 20 ink · 10 color." },
  { id: "typography", num: "07", title: "Typography",        why: "Two faces, a body ladder, one highlight pattern." },
  { id: "templates",  num: "14", title: "Templates",         why: "Six ready-to-use frames for recurring artifacts." },
];

function CoverChapter() {
  const today = useMemo(() => {
    const d = new Date();
    return d.toISOString().slice(0, 10).replace(/-/g, ".");
  }, []);
  const total = SECTIONS.length - 1; // exclude cover

  return (
    <section id="cover" className="home" data-screen-label="00 Home">
      <HomeHero today={today} total={total} />
      <HomeOverview total={total} today={today} />
      <HomeInequalities />
      <HomePath />
      <HomeIndex />
      <HomeMeta today={today} />
    </section>
  );
}

/* --- Home / Hero -------------------------------------------------------- */
function HomeHero({ today, total }) {
  return (
    <header className="home-hero">
      <CoverField />
      <div className="home-hero-top">
        <div>
          <span className="k">Field guide</span>
          <strong className="v">bdg Brand System</strong>
        </div>
        <div className="mid">
          <span className="k">Edition</span>
          <strong className="v">I · 2026</strong>
        </div>
        <div className="right">
          <span className="k">Last updated</span>
          <strong className="v">{today}</strong>
        </div>
      </div>

      <h1 className="home-thesis">
        <span className="line line-a">Data</span>
        <span className="line line-b">in all</span>
        <span className="line line-c"><span className="hl-block">honesty</span>.</span>
      </h1>

      <div className="home-hero-bottom">
        <div>
          <span className="k">What this is</span>
          <strong className="v">A living brand &amp; design system —<br className="br"/>{total} chapters · open &amp; versioned.</strong>
        </div>
        <a href="#foundation" className="home-cta">
          <span className="cta-k">Begin reading</span>
          <span className="cta-v">Chapter 01 — Brand Foundation</span>
          <span className="cta-arrow" aria-hidden="true">→</span>
        </a>
      </div>
    </header>
  );
}

/* --- Home / Overview tiles ------------------------------------------------ */
function HomeOverview({ total, today }) {
  const tiles = [
    { num: total,                    label: "Chapters",      sub: "Strategy · identity · application" },
    { num: "77",                     label: "Color tokens",  sub: "Seven hues at eleven stops each" },
    { num: today.slice(2).replace(/\./g, " · "), label: "Last updated", sub: "Continuously, like the product it serves", mono: true },
  ];
  return (
    <div className="home-overview">
      <SubHead ix="00.A">What's in this book</SubHead>
      <h2 className="section-title">The system is the brand. <br/>This guide is how it operates.</h2>
      <p className="body" style={{maxWidth:"58ch", marginBottom: 28}}>
        bdg's voice, palette, typography, components, charts, decks, and applications live here as one source. Every chapter ships with real artifacts — copy-paste tokens, real components, live slide examples — so the system can be read AND used in the same place.
      </p>
      <div className="home-tiles">
        {tiles.map((t, i) => (
          <div key={i} className="home-tile">
            <div className={"num" + (t.mono ? " mono" : "")}>{t.num}</div>
            <div className="lbl">{t.label}</div>
            <div className="sub">{t.sub}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* --- Home / Three inequalities ------------------------------------------- */
function HomeInequalities() {
  const ineqs = [
    {
      ix: "01",
      a: "Prediction",
      b: "Understanding",
      gloss: "A forecast is a guess with a coefficient. Understanding is the structure that makes the next guess unnecessary.",
      go: { id: "edge", label: "Chapter 04 — Competitive Edge" },
    },
    {
      ix: "02",
      a: "Optimization",
      b: "Alignment",
      gloss: "Speed without agreement is rework, faster. Alignment is the prerequisite for the optimization to matter.",
      go: { id: "foundation", label: "Chapter 01 — Brand Foundation" },
    },
    {
      ix: "03",
      a: "Speed",
      b: "Clarity",
      gloss: "A faster answer to the wrong question is just a more confident wrong answer. Slow the decision down — then move.",
      go: { id: "voice", label: "Chapter 03 — Messaging" },
    },
  ];
  return (
    <div className="home-ineq">
      <SubHead ix="00.B">Three inequalities</SubHead>
      <h2 className="section-title">The arguments the brand <br/>is built to make.</h2>
      <div className="ineq-stack">
        {ineqs.map(i => (
          <a key={i.ix} className="ineq-row" href={"#" + i.go.id}>
            <span className="ineq-ix">{i.ix}</span>
            <div className="ineq-pair">
              <span className="ineq-a">{i.a}</span>
              <span className="ineq-glyph">≠</span>
              <span className="ineq-b">{i.b}</span>
            </div>
            <p className="ineq-gloss">{i.gloss}</p>
            <span className="ineq-go" aria-label={"Open " + i.go.label}>
              <span className="go-k">↘ Go to</span>
              <span className="go-v">{i.go.label}</span>
            </span>
          </a>
        ))}
      </div>
    </div>
  );
}

/* --- Home / Reading path -------------------------------------------------- */
function HomePath() {
  return (
    <div className="home-path">
      <SubHead ix="00.C">Start here — a five-chapter path</SubHead>
      <h2 className="section-title">If it's your first read, take these in order.</h2>
      <p className="body" style={{maxWidth:"58ch", marginBottom: 24}}>
        These five chapters cover what bdg stands for, how it sounds, what it looks like, and how to build with it. They take about thirty minutes end-to-end.
      </p>
      <ol className="path-list">
        {HOME_PATH.map((p, i) => (
          <li key={p.id}>
            <a className="path-step" href={"#" + p.id}>
              <span className="step-n">{String(i + 1).padStart(2, "0")}</span>
              <span className="step-num">{p.num}</span>
              <span className="step-title">{p.title}</span>
              <span className="step-why">{p.why}</span>
              <span className="step-arrow" aria-hidden="true">→</span>
            </a>
          </li>
        ))}
      </ol>
    </div>
  );
}

/* --- Home / Full chapter index ------------------------------------------- */
function HomeIndex() {
  return (
    <div className="home-index">
      <SubHead ix="00.D">Full index</SubHead>
      <h2 className="section-title">All {SECTIONS.length - 1} chapters.</h2>
      <div className="idx-cols">
        {GROUPS.map(g => {
          const items = SECTIONS.filter(s => s.group === g);
          return (
            <div key={g} className="idx-col">
              <div className="idx-col-hed">{g} <span className="ct">{String(items.length).padStart(2, "0")}</span></div>
              <ul>
                {items.map(s => (
                  <li key={s.id}>
                    <a href={"#" + s.id} className="idx-row">
                      <span className="n">{s.num}</span>
                      <span className="l">{s.label}</span>
                      <span className="k">{s.kind}</span>
                    </a>
                  </li>
                ))}
              </ul>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* --- Home / Meta / changelog footer -------------------------------------- */
function HomeMeta({ today }) {
  const entries = [
    { date: today,        chap: "19 · Presentations", note: "Migrated to live HTML decks; archetypes pulled from the master template." },
    { date: "2026.05.18", chap: "07 · Typography",    note: "Highlight-block emphasis pattern formalized; one highlight per title." },
    { date: "2026.05.15", chap: "02 · Market",        note: "Competitor archetype tables expanded — three archetypes, nine firms." },
    { date: "2026.05.10", chap: "10 · Motion",        note: "Approved motions expanded from two to ten patterns." },
  ];
  return (
    <div className="home-foot">
      <SubHead ix="00.E">Recent changes</SubHead>
      <h2 className="section-title">A versioned system, not a stamp.</h2>
      <div className="changelog">
        {entries.map((e, i) => (
          <div key={i} className="change-row">
            <span className="cr-date">{e.date}</span>
            <span className="cr-chap">{e.chap}</span>
            <span className="cr-note">{e.note}</span>
          </div>
        ))}
      </div>
      <div className="home-credits">
        <div>
          <span className="k">Maintained by</span>
          <strong className="v">bdg / Brand &amp; Studio</strong>
        </div>
        <div>
          <span className="k">License</span>
          <strong className="v">Internal · v1.0</strong>
        </div>
        <div>
          <span className="k">Source</span>
          <strong className="v">site/ · open in editor</strong>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- CHAPTER FRAME ------------------------------- */

function ChapterHead({ num, title, kicker, meta }) {
  return (
    <header className="chapter-head">
      <div>
        <div className="crumbs">
          <span>bdg</span><span className="sep">/</span>
          <span>Brand System</span><span className="sep">/</span>
          <span>{kicker}</span>
        </div>
        <div className="chap-num">CHAPTER {num} —</div>
        <h2 className="chap-title">{title}</h2>
      </div>
      <div className="chap-meta">
        {meta.map((m, i) => (
          <div key={i}>
            <span>{m[0]}</span> <strong>{m[1]}</strong>
          </div>
        ))}
      </div>
    </header>
  );
}

function SubHead({ ix, children }) {
  return (
    <div className="sub">
      <span className="dot" />
      <span className="ix">{ix}</span>
      <span>{children}</span>
    </div>
  );
}

/* --------------------------- 01 FOUNDATION ------------------------------- */

function FoundationChapter() {
  return (
    <section id="foundation" className="chapter" data-screen-label="01 Brand Foundation">
      <ChapterHead
        num="01"
        kicker="Strategy"
        title={<>Brand <br/>Foundation.</>}
        meta={[["Sections", "04"], ["Last edited", "May 2026"], ["Owner", "Strategy"]]}
      />

      <div className="opener">
        <div>
          <SubHead ix="01.A">Positioning</SubHead>
          <p className="lede">
            bdg sits at the <span className="neq">tension</span> between computational
            precision and human judgment — the place where data becomes a
            decision, not a dashboard.
          </p>
          <p className="body" style={{marginTop: 18}}>
            Most consultancies optimize the answer. bdg optimizes the question —
            re-architecting the decision itself: who owns it, what evidence is
            sufficient, on what cadence it gets re-opened.
          </p>
        </div>

        <div className="right">
          <div className="position-card">
            <div className="kicker"><span className="ix">01.A.i</span> Strategic statement (internal)</div>
            <h3>Decision intelligence partner.</h3>
            <p>We re-architect how organizations decide — owners, inputs, cadence, evidence threshold — so the next decision is structurally better, not just faster.</p>
          </div>
          <div className="position-card invert">
            <div className="kicker"><span className="ix">01.A.ii</span> External statement (market)</div>
            <h3>Honest data. Better decisions. Real impact.</h3>
            <p>For boards and operators who have read a thousand decks — and need the question right before the answer.</p>
          </div>
        </div>
      </div>

      {/* 01.B — Category */}
      <div style={{marginTop: 96}}>
        <SubHead ix="01.B">Category — what we are, what we are not</SubHead>
        <h2 className="section-title">A category of one: <br/><span className="hl hl-y">decision intelligence,</span> not analytics theater.</h2>

        <div className="contrast is">
          <div>
            <div className="hed"><span className="tag">IS</span> What bdg is</div>
            <ul>
              <li>A re-architecture of how decisions get made</li>
              <li>Owner · inputs · cadence · evidence threshold</li>
              <li>Editorial rigor applied to operational reality</li>
              <li>The structure that survives the next quarter</li>
              <li>A partner for the question, not the answer</li>
            </ul>
          </div>
          <div>
            <div className="hed"><span className="tag" style={{background:"var(--c-red)"}}>NOT</span> What bdg is not</div>
            <ul style={{filter:"none"}}>
              <li className="not">A dashboard vendor</li>
              <li className="not">A forecasting service</li>
              <li className="not">A change-management framework</li>
              <li className="not">A "data culture" workshop</li>
              <li className="not">A faster way to be wrong</li>
            </ul>
          </div>
        </div>
        <style>{`.contrast.is li.not::before { content: "×"; color: var(--c-red); }`}</style>
      </div>

      {/* 01.C — Archetype */}
      <div style={{marginTop: 96}}>
        <SubHead ix="01.C">Archetype — Sage primary, Hero secondary</SubHead>
        <h2 className="section-title">Two voices, one posture: <br/>thoughtful, then decisive.</h2>

        <div className="duality">
          <div className="pole">
            <div className="lead">Primary — <em>Sage</em></div>
            <h4>Insight.<br/>Understanding.<br/>Truth.</h4>
            <p className="body" style={{maxWidth:"32ch"}}>
              The Sage names the structure. Reads the system. Earns trust by
              being right when it is uncomfortable to be right.
            </p>
            <div className="rows">
              <div className="row"><span>Owns</span><span className="pipe">·</span><span>The question, the framework, the diagnosis.</span></div>
              <div className="row"><span>Sounds like</span><span className="pipe">·</span><span>Calm. Specific. Numbered. Quietly certain.</span></div>
              <div className="row"><span>Default mode</span><span className="pipe">·</span><span>Diagnose first. Recommend second.</span></div>
            </div>
          </div>
          <div className="pole" style={{background:"var(--paper-2)"}}>
            <div className="lead">Secondary — <em>Hero</em></div>
            <h4>Impact.<br/>Execution.<br/>Responsibility.</h4>
            <p className="body" style={{maxWidth:"32ch"}}>
              The Hero ships the structure. Carries the call. Earns credit by
              being accountable when the call is hard.
            </p>
            <div className="rows">
              <div className="row"><span>Owns</span><span className="pipe">·</span><span>The decision, the deployment, the outcome.</span></div>
              <div className="row"><span>Sounds like</span><span className="pipe">·</span><span>Direct. Present-tense. Specific to one move.</span></div>
              <div className="row"><span>Default mode</span><span className="pipe">·</span><span>Design the play. Run the play.</span></div>
            </div>
          </div>
        </div>

        <div style={{marginTop: 36, display:"grid", gridTemplateColumns:"repeat(3, 1fr)", gap: 28, borderTop:"0.8px solid var(--line)", paddingTop: 28}}>
          <div className="stat"><div className="num">01</div><div className="lbl">Sage / Hero · the duality, not a balance</div></div>
          <div className="stat"><div className="num">≠</div><div className="lbl">Insight is not the same as impact</div></div>
          <div className="stat"><div className="num">02</div><div className="lbl">One archetype leads each artifact — never both at once</div></div>
        </div>
      </div>

      {/* 01.D — Mission & Promise */}
      <div style={{marginTop: 96}}>
        <SubHead ix="01.D">Mission &amp; promise</SubHead>

        <div className="promise-band">
          <div className="a">
            <div className="kicker">Mission</div>
            <h4>Make the next decision structurally better — not just faster, not just louder.</h4>
            <p>We work with boards and operators to re-architect the decision itself: who owns it, what counts as evidence, on what cadence it is re-opened. The output is not a dashboard. The output is a clearer next move.</p>
          </div>
          <div className="b">
            <div className="kicker">Promise</div>
            <h4 style={{fontSize:"clamp(26px, 2.6vw, 38px)"}}>Honest data. <br/>Better decisions. <br/>Real impact.</h4>
            <p>Three claims, in that order. Honesty before optimization. Decisions before metrics. Impact measured outside this room.</p>
          </div>
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 02 VOICE ------------------------------------ */

function VoiceChapter({ onCopy }) {
  return (
    <section id="voice" className="chapter" data-screen-label="02 Voice">
      <ChapterHead num="02" kicker="Language" title={<>Voice &amp; <br/>Messaging.</>} meta={[["Pillars","03"], ["Tone attrs.","05"], ["Linter","Live"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="02.A">Posture</SubHead>
          <p className="lede">
            Calm authority. Short thesis statements. Structured contrasts.
            Cause <span className="neq">→</span> tension <span className="neq">→</span> implication.
          </p>
          <p className="body" style={{marginTop: 18}}>
            The voice is editorial, not corporate. It does not hype, it does not
            soften. It names the tension and then names the move.
          </p>
        </div>
        <div className="right">
          <div className="pullquote">
            "Reporting tells you what <span className="accent">happened</span>. Decisions need what's <span className="accent">next</span>."
          </div>
          <p className="body">— bdg house style, sample sentence. Two clauses, one tension, no buzzwords.</p>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="02.B">Three messaging pillars</SubHead>
        <div className="found-grid" style={{gridTemplateColumns:"repeat(3, 1fr)", marginTop: 16}}>
          {[
            {ix:"01", t:"Better decisions.", b:"The decision is the unit of value. Forecasts, dashboards, and models are scaffolding. We optimize the decision."},
            {ix:"02", t:"Honest data.", b:"Numbers without padding. Caveats in-line, not in the appendix. We refuse to flatter the reader."},
            {ix:"03", t:"Real impact.", b:"Measured outside the room — margin, cycle time, agreement. If it didn't move, it didn't happen."},
          ].map((p, i) => (
            <div key={i} className="position-card" style={{minHeight: 200}}>
              <div className="kicker"><span className="ix">{p.ix}</span> Pillar</div>
              <h3>{p.t}</h3>
              <p>{p.b}</p>
            </div>
          ))}
        </div>
      </div>

      <MessageLinter />

      <ToneAttributes />
      <ToneSpectrum />
      <ValueProps />
      <PhraseLibrary onCopy={onCopy} />
      <WordLedger />
      <SentenceStyle />
    </section>
  );
}

/* 02.D — Tone attributes */
function ToneAttributes() {
  const attrs = [
    { ix: "01", name: "Clear",      is: ["Two clauses, max.", "One claim per sentence.", "Numbers, not adverbs."], isnt: ["Simple to the point of vague.", "Stripped of context.", "A summary of a summary."] },
    { ix: "02", name: "Confident",  is: ["Present tense.", "Active voice.", "Owned conclusions."], isnt: ["Loud. Brash. Salesy.", "Claims without basis.", "Exclamation marks."] },
    { ix: "03", name: "Human",      is: ["Names the room.", "Acknowledges the cost.", "Specific, not generic."], isnt: ["Folksy. Cute.", "Self-deprecating.", "Performing relatability."] },
    { ix: "04", name: "Intellectual", is: ["Cites the structure.", "Distinguishes signal from noise.", "Earns a long sentence."], isnt: ["Showy. Citation-heavy.", "Jargon as armor.", "Smart at the reader."] },
    { ix: "05", name: "Provocative — measured", is: ["Names what others avoid.", "Stakes a position.", "Risks being wrong on the record."], isnt: ["Contrarian for sport.", "Inflammatory.", "Confident about uncertainty."] },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.D">Tone attributes</SubHead>
      <h2 className="section-title">Five settings, always running together.</h2>
      <p className="body" style={{marginBottom: 28, maxWidth: "60ch"}}>The brand is never just one of these. A good sentence dials all five at once; the failure modes underneath name what each attribute is <em>not</em>.</p>
      <div className="tone-grid">
        {attrs.map(a => (
          <div key={a.ix} className="tone-card">
            <div className="ix">02.D.{a.ix}</div>
            <h4>{a.name}</h4>
            <div style={{display:"flex", flexDirection:"column", gap: 8}}>
              {a.is.map((t, i) => <div className="row is" key={"is"+i}><span>{t}</span></div>)}
            </div>
            <div style={{display:"flex", flexDirection:"column", gap: 8, marginTop: "auto", borderTop:"0.8px solid var(--line)", paddingTop: 12}}>
              {a.isnt.map((t, i) => <div className="row isnt" key={"no"+i}>{t}</div>)}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 02.E — Tone spectrum by context */
function ToneSpectrum() {
  // pos: [warmth, density, edge]  three knobs, 0–2
  const ctx = [
    { ix: "01", lbl: "Website",          pos: [1, 2, 1], sample: "Decision intelligence for boards and operators. The work begins before the dashboard.", sig: "Marketing · top of funnel" },
    { ix: "02", lbl: "Case study",       pos: [1, 2, 2], sample: "Q1 forecast variance was 14.2%. The decision that produced it was made in 11 minutes, on a Slack thread, with one chart.", sig: "Long-form · evidence" },
    { ix: "03", lbl: "Social",           pos: [2, 1, 2], sample: "Reporting tells you what happened. Decisions need what's next.", sig: "Short · hook · provoke" },
    { ix: "04", lbl: "Sales",            pos: [1, 2, 1], sample: "Most boards re-open the forecast once a quarter. Markets move weekly. We re-architect the cadence.", sig: "Direct · room · cost" },
    { ix: "05", lbl: "Thought leadership", pos: [0, 2, 2], sample: "Optimization without alignment is rework, faster. We argue the order of operations matters more than the operations.", sig: "Editorial · stake claim" },
  ];
  const knobs = ["Warmth", "Density", "Edge"];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.E">Tone spectrum by context</SubHead>
      <h2 className="section-title">Same voice, different rooms.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "60ch"}}>Three knobs change per surface — <strong>warmth</strong>, <strong>density</strong>, <strong>edge</strong>. The claims do not change. Only the texture does.</p>
      <div className="spectrum">
        <div className="spec-axis">
          {ctx.map(c => (
            <div key={c.ix}>
              <span className="ix">02.E.{c.ix}</span>
              <span className="lbl">{c.lbl}</span>
              {knobs.map((k, ki) => (
                <div key={k} style={{display:"grid", gridTemplateColumns:"56px 1fr", gap: 8, alignItems:"center"}}>
                  <span style={{fontSize: 9, letterSpacing: "0.16em"}}>{k}</span>
                  <div className="pos" style={{margin: 0}}>
                    <span className={c.pos[ki] >= 0 ? "on" : ""}/>
                    <span className={c.pos[ki] >= 1 ? "on" : ""}/>
                    <span className={c.pos[ki] >= 2 ? "on" : ""}/>
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
        <div className="spec-samples">
          {ctx.map(c => (
            <div key={c.ix}>
              <div>{c.sample}</div>
              <span className="sig">{c.sig}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* 02.F — USP / ESP */
function ValueProps() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.F">USP &amp; ESP — two halves of the claim</SubHead>
      <h2 className="section-title">A rational position. <br/>An emotional one. Both. Always.</h2>
      <div className="dual-prop">
        <div>
          <div className="kind">USP — Unique selling proposition (rational)</div>
          <h4>The only firm that re-architects the <span className="hl hl-y">decision</span>, not the dashboard.</h4>
          <p className="rationale">Where peers optimize the model, optimize the report, or optimize the change program, bdg restructures the decision itself — its owner, its inputs, its cadence, its evidence threshold. The shape of the work is different in kind, not in degree.</p>
          <div className="built">
            <div><span className="lbl">Use when</span><span className="val">Selling, briefing, proposals.</span></div>
            <div><span className="lbl">Avoid when</span><span className="val">Social, op-eds, panels.</span></div>
          </div>
        </div>
        <div>
          <div className="kind">ESP — Emotional selling proposition (felt)</div>
          <h4>Finally, a room where the <span className="hl hl-b">question</span> gets asked before the answer.</h4>
          <p className="rationale">The feeling bdg sells is relief — the relief of being heard before being optimized, of having the structure named before the recommendation lands. The reader is exhausted by decks; bdg's voice is a partner sitting on their side of the table.</p>
          <div className="built">
            <div><span className="lbl">Use when</span><span className="val">Founders, social, panels.</span></div>
            <div><span className="lbl">Avoid when</span><span className="val">Procurement, RFPs, M&amp;A.</span></div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* 02.G — Key phrase library */
function PhraseLibrary({ onCopy }) {
  const phrases = [
    { ix: "001", txt: "Most consultancies optimize the answer. bdg optimizes the question.", ctx: "Positioning" },
    { ix: "002", txt: "Reporting tells you what happened. Decisions need what's next.", ctx: "Social" },
    { ix: "003", txt: "We re-architect how the decision gets made — owner, inputs, cadence, evidence threshold.", ctx: "Sales" },
    { ix: "004", txt: "A faster answer to the wrong question is a more confident wrong answer.", ctx: "Editorial" },
    { ix: "005", txt: "The output is not a dashboard. The output is a clearer next move.", ctx: "Sales" },
    { ix: "006", txt: "Diagnose · Design · Deploy — three phases, one cadence.", ctx: "Method" },
    { ix: "007", txt: "Honesty before optimization. Decisions before metrics. Impact measured outside this room.", ctx: "Promise" },
    { ix: "008", txt: "Optimization without alignment is rework, faster.", ctx: "Editorial" },
    { ix: "009", txt: "We do not flatter the reader. We name the tension and then name the move.", ctx: "Voice rule" },
    { ix: "010", txt: "The decision is the unit of value. Forecasts, dashboards, and models are scaffolding.", ctx: "Positioning" },
    { ix: "011", txt: "Slow the decision down — then move.", ctx: "Social" },
    { ix: "012", txt: "Boards review forecasts once a quarter. Markets move weekly.", ctx: "Sales" },
  ];
  const contexts = ["All", "Positioning", "Sales", "Social", "Editorial", "Method", "Promise", "Voice rule"];
  const [q, setQ] = useState("");
  const [ctx, setCtx] = useState("All");
  const copy = onCopy || ((v) => navigator.clipboard?.writeText(v));
  const filtered = phrases.filter(p =>
    (ctx === "All" || p.ctx === ctx) &&
    (q.trim() === "" || p.txt.toLowerCase().includes(q.toLowerCase()))
  );
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.G">Key phrase library</SubHead>
      <h2 className="section-title">Twelve sentences. <br/>Used verbatim, used by the room.</h2>
      <div className="phrase-tools">
        <input className="phrase-search" placeholder="Search phrases —" value={q} onChange={e => setQ(e.target.value)} />
        {contexts.map(c => (
          <button key={c} className="phrase-chip" aria-pressed={ctx === c} onClick={() => setCtx(c)}>{c}</button>
        ))}
      </div>
      <div className="phrase-list">
        {filtered.length === 0 && <div className="body" style={{padding:"22px 0"}}>No phrases match — try a different word.</div>}
        {filtered.map(p => (
          <div key={p.ix} className="phrase-row" onClick={() => copy(p.txt, "phrase " + p.ix)} title="Click to copy">
            <span className="ix">{p.ix}</span>
            <span className="txt">{p.txt}</span>
            <span className="ctx">{p.ctx}</span>
            <span className="copy-icn">⧉</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 02.H — Word ledger */
function WordLedger() {
  const leanInto = [
    "decision", "diagnose", "structure", "cadence", "evidence", "tension",
    "owner", "inputs", "re-architect", "implication", "next", "specific",
    "honest", "tabular", "alignment", "system", "claim", "trade-off",
    "threshold", "deploy", "operator", "board", "the room"
  ];
  const avoid = [
    "innovative", "cutting-edge", "disruptive", "game-changing", "leading provider",
    "best-in-class", "synergies", "unlock", "empower", "leverage",
    "seamless", "robust", "world-class", "mission-critical", "next-gen",
    "AI-powered", "ecosystem", "paradigm", "revolutionary", "holistic"
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.H">Word ledger</SubHead>
      <h2 className="section-title">Words that reinforce. <br/>Words that weaken.</h2>
      <div className="ledger">
        <div className="lean">
          <div className="col-hed"><span className="tag">LEAN IN</span> Words that do work</div>
          <h3 className="col-title">Specific. <br/>Structural. <br/>Owned.</h3>
          <div className="word-list">
            {leanInto.map(w => <span key={w} className="word">{w}</span>)}
          </div>
        </div>
        <div className="avoid">
          <div className="col-hed"><span className="tag">AVOID</span> Decoration that weakens</div>
          <h3 className="col-title">Vague. <br/>Inflated. <br/>Borrowed.</h3>
          <div className="word-list">
            {avoid.map(w => <span key={w} className="word">{w}</span>)}
          </div>
        </div>
      </div>
      <p className="body" style={{marginTop: 16, maxWidth:"60ch"}}>The test: remove the word. If the claim is unchanged, the word was decoration. If the sentence breaks, the word was structural — keep it.</p>
    </div>
  );
}

/* 02.I — Sentence style */
function SentenceStyle() {
  const examples = [
    {
      ix: "01",
      cause: "Most boards review the forecast once a quarter.",
      tension: "Markets move weekly.",
      implication: "Our cadence is the bug, not the forecast.",
    },
    {
      ix: "02",
      cause: "The model achieved 94% accuracy.",
      tension: "The decision it informed got re-opened the next morning.",
      implication: "Accuracy without ownership is theatre.",
    },
    {
      ix: "03",
      cause: "We added three dashboards.",
      tension: "Two teams still met to reconcile the numbers.",
      implication: "Tools do not replace the meeting. The meeting is the decision.",
    },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.I">Sentence style — cause → tension → implication</SubHead>
      <h2 className="section-title">Every claim is a three-beat sentence.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "60ch"}}>The default house pattern: state the cause, surface the tension, then land the implication. Three beats; usually three sentences; never more than four. Read out loud — if it reads flat, a beat is missing.</p>
      <div className="sent-stack">
        {examples.map(e => (
          <div key={e.ix} className="sent">
            <div className="label"><span className="ix">02.I.{e.ix}</span> Example</div>
            <p className="quote">{e.cause} <span style={{color:"var(--ink-2)"}}>{e.tension}</span> <span style={{color:"var(--signal)"}}>{e.implication}</span></p>
            <div className="parts">
              <div>
                <div className="ptag">01 · Cause</div>
                <div className="ptxt">{e.cause}</div>
              </div>
              <div>
                <div className="ptag">02 · Tension</div>
                <div className="ptxt">{e.tension}</div>
              </div>
              <div>
                <div className="ptag">03 · Implication</div>
                <div className="ptxt">{e.implication}</div>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function MessageLinter() {
  const [val, setVal] = useState("Our innovative, cutting-edge platform empowers leaders to unlock synergies with best-in-class decision-making.");
  const weakWords = ["innovative","cutting-edge","disruptive","game-changing","leading provider","best-in-class","empower","unlock","synergies","robust","seamlessly","leverage","next-generation","world-class","mission-critical","holistic","ecosystem","paradigm","revolutionary"];
  const tokens = val.split(/(\s+|[.,;:!?—])/);
  const flagged = tokens.map((tok, i) => {
    const w = tok.toLowerCase().replace(/[^a-z\-]/g, "");
    const hit = weakWords.find(x => x === w || x.replace(/\s/g,"-") === w);
    if (hit) return <mark key={i} title={`Weakens: "${hit}"`} style={{background:"var(--c-red)", color:"#fff", padding:"0 2px"}}>{tok}</mark>;
    return <span key={i}>{tok}</span>;
  });
  const hits = tokens.filter(t => weakWords.includes(t.toLowerCase().replace(/[^a-z\-]/g, ""))).length;
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="02.C">Messaging linter</SubHead>
      <h2 className="section-title">Paste a sentence. We will flag the words that weaken it.</h2>

      <div style={{border:"0.8px solid var(--line)", padding: 0, display:"grid", gridTemplateColumns:"1fr 280px"}}>
        <textarea
          value={val}
          onChange={e => setVal(e.target.value)}
          style={{
            border:0, outline:0, resize:"vertical",
            padding:"22px 24px", minHeight: 120,
            fontFamily:"var(--f-body)", fontSize: 18, lineHeight: 1.45,
            background:"var(--paper)", color:"var(--ink)"
          }}
        />
        <div style={{borderLeft:"0.8px solid var(--line)", padding:"22px 22px 24px", display:"flex", flexDirection:"column", gap: 10, background:"var(--paper-2)"}}>
          <div className="kicker" style={{fontFamily:"var(--f-mono)", fontSize: 11, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>Verdict</div>
          <div style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 48, lineHeight: 0.95, color: hits ? "var(--c-red)" : "var(--c-green)"}}>{hits}</div>
          <div style={{fontFamily:"var(--f-mono)", fontSize: 11, letterSpacing:"0.14em", textTransform:"uppercase", color:"var(--ink-2)"}}>weak {hits === 1 ? "word" : "words"} flagged</div>
        </div>
      </div>
      <div style={{borderTop:0, borderLeft:"0.8px solid var(--line)", borderRight:"0.8px solid var(--line)", borderBottom:"0.8px solid var(--line)", padding:"18px 24px", fontFamily:"var(--f-body)", fontSize: 17, lineHeight: 1.55}}>
        {flagged}
      </div>
      <p className="body" style={{marginTop: 14, maxWidth:"60ch"}}>The list is a starting point, not a court. The rule is: if removing the word does not change the claim, it was decoration.</p>
    </div>
  );
}

/* --------------------------- 03 IDENTITY --------------------------------- */

function IdentityChapter({ onCopy }) {
  return (
    <section id="identity" className="chapter" data-screen-label="03 Identity">
      <ChapterHead num="03" kicker="Marque" title={<>Logo &amp; <br/>Identity.</>} meta={[["Mark","Square frame"], ["Wordmark","bdg"], ["Clear space","1× cap-height"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="03.A">The mark</SubHead>
          <p className="lede">A square <span className="neq">frames</span> a lowercase lockup — a container holding a claim. The mark is the box around the question.</p>
          <p className="body" style={{marginTop: 18}}>The mark never tilts, glows, or animates on entry. It is a stamp, not a sticker. It inherits the foreground colour of its surface, and only ever appears in one of the system's tokenised colours.</p>
        </div>
        <div className="right">
          <div className="id-frame">
            <div className="mark-img" style={{width:"45%"}}/>
            <div className="id-tag"><span className="dot" style={{background:"var(--ink)"}}/> Primary · ink on paper</div>
          </div>
        </div>
      </div>

      <ClearSpace />
      <ColorContexts />
      <MisuseGallery />
      <Downloads onCopy={onCopy}/>
    </section>
  );
}

/* 03.B — Clear space & minimum sizes */
function ClearSpace() {
  const sizes = [
    { lbl: "Favicon",     meta: "Browser tab · 16×16 minimum", w: 16,  dim: "16 × 15px" },
    { lbl: "App icon",    meta: "iOS / Android",               w: 28,  dim: "28 × 27px" },
    { lbl: "Inline mark", meta: "Body type · email signature", w: 36,  dim: "36 × 35px" },
    { lbl: "Standard",    meta: "Navigation · header",         w: 50,  dim: "50 × 48px" },
    { lbl: "Display",     meta: "Cover · stage · poster",      w: 96,  dim: "96 × 92px" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="03.B">Clear space &amp; minimum sizes</SubHead>
      <h2 className="section-title">One cap-height of nothing — every side.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth:"60ch"}}>The unit of clear space is <strong>1×</strong> the cap-height of the lowercase <em>b</em>. Multiply it. Never reduce it. No type, no rule, no other mark may enter the buffer.</p>

      <div className="clearspace">
        <div className="cs-canvas">
          <div className="cs-box">
            <div className="cs-mark"/>
            <span className="cs-cap top">1× clear</span>
            <span className="cs-cap right">1× clear</span>
            <span className="cs-cap bottom">1× clear</span>
            <span className="cs-cap left">1× clear</span>
          </div>
        </div>

        <div className="size-stack">
          {sizes.map(s => (
            <div key={s.lbl} className="size-row">
              <div className="swatch-mark" style={{width: s.w}}/>
              <div className="desc">
                <span className="lbl">{s.lbl}</span>
                <span className="meta">{s.meta}</span>
              </div>
              <div className="dim">{s.dim}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* 03.C — Color contexts */
function ColorContexts() {
  const ctx = [
    { bg: "var(--paper)", fg: "var(--ink)",    lbl: "Paper", tone: "Primary surface" },
    { bg: "var(--ink)",   fg: "#FFFFFF",       lbl: "Ink",   tone: "Inverted surface" },
    { bg: "#3236FF",      fg: "#FFFFFF",       lbl: "Blue",  tone: "Brand surface" },
    { bg: "#F55910",      fg: "#FFFFFF",       lbl: "Orange",tone: "Signal surface" },
    { bg: "#FFE241",      fg: "#0A0A0C",       lbl: "Yellow",tone: "Alert surface" },
    { bg: "#D7B3FF",      fg: "#0A0A0C",       lbl: "Purple",tone: "Hypothetical" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="03.C">On light, on dark, on color</SubHead>
      <h2 className="section-title">The mark inherits the room — never re-drawn for it.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "60ch"}}>One file. The fill is the foreground colour of the surface. No alternate marks, no second wordmarks, no decorative variants.</p>
      <div className="ctx-grid">
        {ctx.map(c => (
          <div key={c.lbl} className="id-frame" style={{background: c.bg}}>
            <div className="mark-img" style={{background: c.fg, WebkitMask:"url(../assets/logo.svg) center/contain no-repeat", mask:"url(../assets/logo.svg) center/contain no-repeat"}}/>
            <div className="id-tag" style={{color: c.fg, opacity: 0.7}}>
              <span className="dot" style={{background: c.fg}}/> {c.lbl} · {c.tone}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 03.D — Misuse */
function MisuseGallery() {
  const misuses = [
    { cls: "stretch",  cap: "01 · don't stretch" },
    { cls: "rotate",   cap: "02 · don't tilt" },
    { cls: "recolor",  cap: "03 · don't recolor off-palette" },
    { cls: "gradient", cap: "04 · no gradients" },
    { cls: "shadow",   cap: "05 · no shadows · no glow" },
    { cls: "outline",  cap: "06 · don't outline the wordmark" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="03.D">Misuse</SubHead>
      <h2 className="section-title">Six failures, in a single column.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "60ch"}}>The rule is simple. If a treatment would weaken any other word in the system, it weakens the mark too. The marque does not become a moodboard.</p>
      <div className="misuse-grid">
        {misuses.map(m => (
          <div key={m.cls} className={"misuse " + m.cls}>
            <div className="mark-img"/>
            <div className="cap">{m.cap}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 03.E — Downloads (real, working) */
function Downloads({ onCopy }) {
  const [busy, setBusy] = useState(null);
  const [done, setDone] = useState(null);
  const [err, setErr] = useState(null);

  const run = useCallback(async (key, fn) => {
    setBusy(key); setErr(null);
    try {
      await fn();
      setDone(key);
      setTimeout(() => setDone(d => (d === key ? null : d)), 1800);
    } catch (e) {
      console.warn("[bdg] download failed:", key, e);
      setErr(key);
      setTimeout(() => setErr(e2 => (e2 === key ? null : e2)), 2400);
    } finally {
      setBusy(b => (b === key ? null : b));
    }
  }, []);

  const files = [
    { key: "svg",      ttl: "Square mark",            file: "bdg-mark.svg",        type: "SVG · vector",          size: "3.1 KB",
      run: () => fetchAndDownload("bdg-mark.svg", "../assets/logo.svg") },
    { key: "png1024",  ttl: "Square mark · large",    file: "bdg-mark-1024.png",   type: "PNG · 1024 × 1024",     size: "~30 KB",
      run: () => downloadLogoPng(1024, "bdg-mark-1024.png") },
    { key: "png180",   ttl: "App icon (iOS)",         file: "bdg-mark-180.png",    type: "PNG · 180 × 180",       size: "~4 KB",
      run: () => downloadLogoPng(180, "bdg-mark-180.png") },
    { key: "png32",    ttl: "Favicon",                file: "bdg-mark-32.png",     type: "PNG · 32 × 32",         size: "~0.6 KB",
      run: () => downloadLogoPng(32, "bdg-mark-32.png", { padding: 0.04 }) },
    { key: "css",      ttl: "Tokens · CSS",           file: "bdg-tokens.css",      type: "CSS · custom properties", size: "6.5 KB",
      run: () => fetchAndDownload("bdg-tokens.css", "../colors_and_type.css") },
    { key: "json",     ttl: "Tokens · JSON",          file: "bdg-tokens.json",     type: "JSON · DTCG format",    size: "~2 KB",
      run: () => downloadBlob("bdg-tokens.json", new Blob([JSON.stringify(buildTokensJson(), null, 2)], { type: "application/json" })) },
    { key: "tailwind", ttl: "Tokens · Tailwind preset", file: "tailwind-bdg.js",   type: "JS · Tailwind v3",      size: "~1 KB",
      run: () => downloadBlob("tailwind-bdg.js", new Blob([buildTailwindPreset()], { type: "application/javascript" })) },
  ];

  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="03.E">Downloads</SubHead>
      <h2 className="section-title">One source. <br/>Versioned. Dated.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "60ch"}}>Approved assets only. Click a card to download. Filename and type print on the face; the small <span className="kbd" style={{fontSize:10}}>⎘</span> copies the filename to clipboard without triggering a download.</p>
      <div className="dl-grid">
        {files.map(f => {
          const state = err === f.key ? "err" : done === f.key ? "done" : busy === f.key ? "busy" : "idle";
          const glyph = { idle: "⤓", busy: "·", done: "✓", err: "!" }[state];
          return (
            <div key={f.key} className="dl-card" data-state={state}>
              <button
                type="button"
                className="dl-main"
                onClick={() => run(f.key, f.run)}
                aria-label={`Download ${f.file}`}
                title={`Download ${f.file}`}
              >
                <div className="file">{f.file}</div>
                <div className="ttl">{f.ttl}</div>
                <div className="meta">
                  <span>{f.type}</span>
                  <span>{f.size}</span>
                  <span className="arrow" aria-hidden="true">{glyph}</span>
                </div>
              </button>
              <button
                type="button"
                className="dl-copy"
                onClick={() => onCopy && onCopy(f.file, f.file + " — filename copied")}
                aria-label={`Copy filename ${f.file}`}
                title="Copy filename"
              >⎘</button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* --------------------------- 04 COLOR ------------------------------------ */

const BRAND_COLORS = [
  { name: "Blue",   hex: "#3236FF", v: "--color-blue-600",   role: "Primary data · intent" },
  { name: "Green",  hex: "#00D190", v: "--color-green-600",  role: "Positive · alignment" },
  { name: "Yellow", hex: "#FFE241", v: "--color-yellow-500", role: "Dark-mode signal · alert" },
  { name: "Orange", hex: "#F55910", v: "--color-orange-600", role: "Light-mode signal · highlight" },
  { name: "Red",    hex: "#EF353E", v: "--color-red-600",    role: "Negative · contradiction" },
  { name: "Purple", hex: "#D7B3FF", v: "--color-purple-300", role: "Hypothetical · soft accent" },
];

function ColorChapter({ onCopy }) {
  return (
    <section id="color" className="chapter" data-screen-label="04 Color">
      <ChapterHead num="04" kicker="Tokens" title={<>Color.</>} meta={[["Brand hues","06"], ["Neutrals","11 stops"], ["Use","Analytical only"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="04.A">Application rule</SubHead>
          <p className="lede">Default to monochrome. Color is a <span className="neq">data point</span>, not a decoration. One signal accent per surface, maximum.</p>
          <p className="body" style={{marginTop: 18}}>Light mode reserves orange as the load-bearing accent. Dark mode hands the role to yellow. The ratio across a long surface stays close to 70 paper · 20 ink · 10 color.</p>
        </div>
        <div className="right">
          <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap: 8}}>
            <div style={{height: 120, background:"var(--paper)", border:"0.8px solid var(--line)"}}/>
            <div style={{height: 120, background:"var(--ink)"}}/>
            <div style={{height: 120, background:"var(--signal)"}}/>
            <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-2)"}}>~70 · Paper</div>
            <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-2)"}}>~20 · Ink</div>
            <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-2)"}}>~10 · Color</div>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="04.B">Brand hues — click to copy</SubHead>
        <div style={{display:"grid", gridTemplateColumns:"repeat(6, 1fr)", gap: 12, marginTop: 16}}>
          {BRAND_COLORS.map(c => (
            <button key={c.name} className="swatch" onClick={() => onCopy(c.hex, c.name + " · " + c.hex)} title={`Copy ${c.hex}`}>
              <div className="chip" style={{background: c.hex}}/>
              <div className="meta">
                <div className="name">{c.name}</div>
                <div className="hex">{c.hex}</div>
                <div className="hex" style={{color:"var(--ink-3)"}}>{c.v}</div>
                <div className="hex" style={{color:"var(--ink-3)"}}>{c.role}</div>
              </div>
            </button>
          ))}
        </div>
      </div>

      <PrimitiveRamps onCopy={onCopy} />
      <SchemesView />
      <PairingMatrix />
      <ColorInApplication />
      <ColorWheel />
    </section>
  );
}

/* 04.C — Primitive ladder (Neutrals + 6 brand ramps) */

const RAMPS = {
  Neutral: { role: "Ink / Paper · canonical surface ladder", base: "#09090B", stops: [
    "#FAFAFA","#F4F4F5","#E4E4E7","#D5D5D7","#A2A2A9","#727279","#53535A","#404045","#27272A","#161618","#09090B"
  ]},
  Blue:    { role: "Primary data · intent", base: "#3236FF", stops: [
    "#EDEEFF","#D9DAFF","#B3B6FF","#8388FF","#5A5FFF","#4044FF","#3236FF","#2729D4","#1F21A8","#181980","#0D0E5E"
  ]},
  Green:   { role: "Positive · alignment", base: "#00D190", stops: [
    "#DDFBF1","#B6F4DF","#7AEAC4","#3FDFA9","#14D69B","#00D190","#00B57E","#008F66","#006D4F","#00533D","#003729"
  ]},
  Yellow:  { role: "Dark-mode signal · alert", base: "#FFE241", stops: [
    "#FFFBEA","#FFF6CC","#FFEE99","#FFE76B","#FFE241","#F5CC1A","#D9AC0E","#A3810A","#74590A","#4D3B0A","#2A2008"
  ]},
  Orange:  { role: "Light-mode signal · highlight", base: "#F55910", stops: [
    "#FFEFE5","#FFDCC2","#FFB585","#FF8D4A","#FF7224","#F76215","#F55910","#C9430A","#9C3408","#6E2406","#481603"
  ]},
  Red:     { role: "Negative · contradiction", base: "#EF353E", stops: [
    "#FFEAEC","#FFD0D3","#FFA1A7","#FF7079","#FA4D55","#F23E47","#EF353E","#C82530","#991A24","#6E1219","#470A0F"
  ]},
  Purple:  { role: "Hypothetical · soft accent", base: "#D7B3FF", stops: [
    "#F9F2FF","#F1E5FF","#E4CCFF","#D7B3FF","#B383FF","#9054FF","#7235EE","#5826BC","#401A8C","#2D1263","#1A0A3D"
  ]},
};
const STOP_NAMES = ["50","100","200","300","400","500","600","700","800","900","950"];

function _lum(hex) {
  const h = hex.replace("#","");
  const f = (c) => {
    const v = parseInt(c, 16) / 255;
    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
  };
  const r = f(h.slice(0,2)), g = f(h.slice(2,4)), b = f(h.slice(4,6));
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrast(bg, fg) {
  const a = _lum(bg), b = _lum(fg);
  const [hi, lo] = a > b ? [a, b] : [b, a];
  return (hi + 0.05) / (lo + 0.05);
}
function verdictFor(ratio) {
  if (ratio >= 7)   return { v: "pass", lbl: "AAA" };
  if (ratio >= 4.5) return { v: "pass", lbl: "AA"  };
  if (ratio >= 3)   return { v: "aa",   lbl: "AA·LG" };
  return { v: "fail", lbl: "fail" };
}

function PrimitiveRamps({ onCopy }) {
  const names = Object.keys(RAMPS);
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="04.C">Primitive ladder — full 50 → 950</SubHead>
      <h2 className="section-title">One ladder, seven runs. Every step is a token.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>
        Eleven stops per hue. The base sits at its natural perceptual stop; the rest are tuned around it for analytical use — chart fills, text, surfaces, borders. Click any cell to copy.
      </p>
      <div className="ramp-block">
        {names.map(n => (
          <div key={n} className="ramp-row">
            <div className="ramp-hed">
              <span className="name">{n}</span>
              <span className="role">{RAMPS[n].role}</span>
            </div>
            <div className="ramp-cells">
              {RAMPS[n].stops.map((hex, i) => {
                const fg = contrast(hex, "#ffffff") > contrast(hex, "#0A0A0C") ? "#ffffff" : "#0A0A0C";
                const isBase = hex.toLowerCase() === RAMPS[n].base.toLowerCase();
                return (
                  <button
                    key={i}
                    className="ramp-cell"
                    style={{ background: hex, color: fg, outline: isBase ? `1.4px solid ${fg}` : "none", outlineOffset: "-5px" }}
                    onClick={() => onCopy(hex, `${n}-${STOP_NAMES[i]} · ${hex}`)}
                    title={`Copy ${hex}`}
                  >
                    <span className="stop">{STOP_NAMES[i]}{isBase ? " · base" : ""}</span>
                    <span className="hex">{hex}</span>
                  </button>
                );
              })}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 04.D — Light & Dark schemes */
function SchemesView() {
  const light = {
    name: "Light · Paper",
    tag: "Default · 70 / 20 / 10",
    bg: "#FAFAFA", fg: "#09090B", line: "rgba(9,9,11,0.10)", accent: "#F55910",
    rows: [
      ["Background",   "--bg1",     "Paper",          "#FAFAFA"],
      ["Foreground",   "--fg1",     "Ink",            "#09090B"],
      ["Secondary",    "--fg2",     "Muted ink",      "#53535A"],
      ["Tertiary",     "--fg3",     "Subdued",        "#727279"],
      ["Border",       "--stroke",  "Hairline",       "#D5D5D7"],
      ["Surface alt",  "--bg2",     "Bone",           "#F4F4F5"],
      ["Signal",       "--accent",  "Load-bearing",   "#F55910"],
    ],
  };
  const dark = {
    name: "Dark · Ink",
    tag: "Inverse · same ratios",
    bg: "#0A0A0C", fg: "#F4F4F5", line: "rgba(255,255,255,0.14)", accent: "#FFE241",
    rows: [
      ["Background",   "--bg1",     "Ink",            "#0A0A0C"],
      ["Foreground",   "--fg1",     "Paper",          "#F4F4F5"],
      ["Secondary",    "--fg2",     "Muted",          "#A2A2A9"],
      ["Tertiary",     "--fg3",     "Subdued",        "#727279"],
      ["Border",       "--stroke",  "Hairline",       "rgba(255,255,255,0.14)"],
      ["Surface alt",  "--bg2",     "Raised",         "#161618"],
      ["Signal",       "--accent",  "Load-bearing",   "#FFE241"],
    ],
  };
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="04.D">Schemes — light &amp; dark</SubHead>
      <h2 className="section-title">Two modes, one ratio. <br/>The accent moves, the system holds.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>
        The signal colour is the only token that crosses modes. In light, it's orange. In dark, it's yellow. Everything else is the same role, re-anchored.
      </p>
      <div className="scheme-pair">
        {[light, dark].map(s => (
          <div key={s.name} className="scheme"
               style={{
                 background: s.bg, color: s.fg,
                 "--sc-line": s.line,
               }}>
            <div className="sc-hed">
              <span className="sc-name">{s.name}</span>
              <span className="sc-tag">{s.tag}</span>
            </div>
            <div className="sc-rows">
              {s.rows.map((r, i) => (
                <div key={i} className="sc-row">
                  <span className="chip" style={{background: r[3]}}/>
                  <span className="lbl">{r[0]} <small>{r[1]} · {r[2]}</small></span>
                  <span className="hex">{r[3]}</span>
                </div>
              ))}
            </div>
            <div className="sc-demo">
              <h5>The next decision.</h5>
              <p>Two clauses. Numbers without padding. The signal carries the move.</p>
              <span className="pill" style={{background: s.accent, color: s.bg}}>Open · 02</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 04.E — Pairing matrix */
function PairingMatrix() {
  const pairs = [
    { bg: "#FAFAFA", fg: "#09090B", lbl: "Ink on Paper",      hint: "Default body" },
    { bg: "#FAFAFA", fg: "#F55910", lbl: "Orange on Paper",   hint: "Light-mode signal" },
    { bg: "#FAFAFA", fg: "#3236FF", lbl: "Blue on Paper",     hint: "Hyperlink · data" },
    { bg: "#0A0A0C", fg: "#FAFAFA", lbl: "Paper on Ink",      hint: "Default inverse" },
    { bg: "#0A0A0C", fg: "#FFE241", lbl: "Yellow on Ink",     hint: "Dark-mode signal" },
    { bg: "#0A0A0C", fg: "#00D190", lbl: "Green on Ink",      hint: "Alignment marker" },
    { bg: "#3236FF", fg: "#FFFFFF", lbl: "White on Blue",     hint: "Filled button" },
    { bg: "#F55910", fg: "#FFFFFF", lbl: "White on Orange",   hint: "Signal pill" },
    { bg: "#FFE241", fg: "#09090B", lbl: "Ink on Yellow",     hint: "Alert" },
    { bg: "#EF353E", fg: "#FFFFFF", lbl: "White on Red",      hint: "Failure state" },
    { bg: "#D7B3FF", fg: "#09090B", lbl: "Ink on Purple",     hint: "Hypothetical row" },
    { bg: "#F4F4F5", fg: "#53535A", lbl: "Muted on Bone",     hint: "Secondary copy" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="04.E">Pairings &amp; contrast</SubHead>
      <h2 className="section-title">A pairing is a contract. <br/>The matrix marks what's enforceable.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>
        Every approved pairing carries a measured WCAG ratio. AA is the floor for body and chrome; AAA is reserved for legibility-critical content. Anything failing is documented here so it stays out of use.
      </p>
      <div className="pair-mtx">
        {pairs.map(p => {
          const r = contrast(p.bg, p.fg);
          const v = verdictFor(r);
          return (
            <div key={p.lbl} className="pair" style={{background: p.bg, color: p.fg}}>
              <div className="meta">
                <span className="ratio">{r.toFixed(2)}</span>
                <span className={"verdict " + v.v}>{v.lbl}</span>
              </div>
              <div className="demo">Ag<br/>01</div>
              <div className="meta" style={{marginTop:"auto"}}>
                <span>{p.lbl}</span>
                <span style={{marginLeft:"auto", opacity: 0.7}}>{p.hint}</span>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* 04.F — Color in application */
function ColorInApplication() {
  const examples = [
    {
      ix: "01", kind: "Poster · LED banner",
      file: "assets/ex/banner-led-3.png",
      ttl: "Klare Daten — banner",
      desc: "Black surface, full-bleed type, the six brand hues used analytically as directional arrows. No single accent dominates — color is the data.",
      palette: ["#0A0A0C", "#FFE241", "#3236FF", "#00D190", "#F55910", "#EF353E", "#D7B3FF"],
      shape: "tall",
    },
    {
      ix: "02", kind: "Poster · campaign",
      file: "assets/ex/bdg-image2.png",
      ttl: "Honest data → Real impact",
      desc: "Three claims stacked on black. Highlights rotate by claim — purple, blue, yellow — one per line, no mixing within a line.",
      palette: ["#0A0A0C", "#D7B3FF", "#3236FF", "#FFE241", "#00D190"],
      shape: "wide",
    },
    {
      ix: "03", kind: "Social · post",
      file: "assets/ex/bdg-image1.png",
      ttl: "It's crazy until it isn't",
      desc: "Yellow surface, ink type, green tape overlay. One bright surface, one ink figure, one secondary accent — never three colors at peak intensity.",
      palette: ["#FFE241", "#00D190", "#0A0A0C"],
      shape: "square",
    },
    {
      ix: "04", kind: "Social · post",
      file: "assets/ex/bdg-image1-1.png",
      ttl: "Red + orange · provocation",
      desc: "Saturated red surface with orange ampersand. Blue tape carries the secondary line. Used for sharp, contrarian editorial only.",
      palette: ["#EF353E", "#F55910", "#3236FF", "#FFFFFF"],
      shape: "square",
    },
    {
      ix: "05", kind: "Social · post",
      file: "assets/ex/bdg-image1-2.png",
      ttl: "Purple + blue · hypothetical",
      desc: "Purple ampersand on blue surface; yellow tape. Reserved for softer claims — questions, hypotheticals, future-tense.",
      palette: ["#3236FF", "#D7B3FF", "#FFE241", "#0A0A0C"],
      shape: "square",
    },
    {
      ix: "06", kind: "Notion · cover",
      file: "assets/ex/notion-cover-1.png",
      ttl: "Messaging · cover",
      desc: "Black surface, white text, two highlight tags — purple arrow + green exclamations — anchoring the rhythm of the sentence.",
      palette: ["#0A0A0C", "#D7B3FF", "#00D190", "#FFFFFF"],
      shape: "wide",
    },
    {
      ix: "07", kind: "Notion · cover",
      file: "assets/ex/notion-cover-2.png",
      ttl: "Messaging · cover",
      desc: "Three short clauses; one orange arrow, one yellow exclamation. Two color beats per cover, no more.",
      palette: ["#0A0A0C", "#F55910", "#FFE241", "#FFFFFF"],
      shape: "wide",
    },
    {
      ix: "08", kind: "Print · LED banner",
      file: "assets/ex/banner-led-1.png",
      ttl: "Banner — directional arrows",
      desc: "Full-bleed type with a dense grid of colored arrows. Color carries the visual rhythm; black is the surface, not the message.",
      palette: ["#0A0A0C", "#FFE241", "#3236FF", "#00D190", "#F55910", "#EF353E", "#D7B3FF"],
      shape: "tall",
    },
    {
      ix: "09", kind: "Event · backdrop",
      file: "assets/ex/board-event.png",
      ttl: "Board event backdrop",
      desc: "Saturated blue surface with yellow highlight on the verb. Single brand color carries the surface; one highlight carries the claim.",
      palette: ["#3236FF", "#FFE241", "#FFFFFF"],
      shape: "square",
    },
    {
      ix: "10", kind: "Retail · booth",
      file: "assets/ex/retail-booth-1.png",
      ttl: "Retail booth",
      desc: "Physical artifact, ink + paper. The brand mark and the inequality glyph carry the booth at distance.",
      palette: ["#0A0A0C", "#FAFAFA"],
      shape: "wide",
    },
    {
      ix: "11", kind: "Decks · stat slide",
      file: "assets/ex/bdg-images-composite.png",
      ttl: "Stat & chart slides",
      desc: "Each slide uses one surface color. Numbers are flat on the surface; the highlight block reserves itself for verbs.",
      palette: ["#FFE241", "#F55910", "#D7B3FF", "#0A0A0C", "#FFFFFF"],
      shape: "wide",
    },
    {
      ix: "12", kind: "Landing page · hero",
      file: "assets/ex/banner-led-2.png",
      ttl: "Web hero — Klare Wirkung",
      desc: "Long-form vertical hero. Color carries motion (arrows) and emphasis (the green exclamation). One headline, one beat per surface.",
      palette: ["#0A0A0C", "#00D190", "#D7B3FF", "#F55910"],
      shape: "tall",
    },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="04.F">Color in application — posts, decks, print, web</SubHead>
      <h2 className="section-title">Twelve artifacts. <br/>Two or three colors each, balanced.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The brand is never <em>one</em> color. Each artifact carries two or three of the six hues, balanced so no single color dominates a campaign. Surface · highlight · accent — and the surface rotates from artifact to artifact.</p>
      <div className="app-grid">
        {examples.map(e => (
          <div key={e.ix} className="app-card">
            <div className={"stage " + (e.shape || "")}>
              <img src={e.file} alt={e.ttl} loading="lazy"/>
            </div>
            <div className="meta">
              <span className="ix">04.F.{e.ix} · {e.kind}</span>
              <span className="ttl">{e.ttl}</span>
              <span className="desc">{e.desc}</span>
              <div className="palette">{e.palette.map((c, i) => <span key={i} style={{background: c}}/>)}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 04.G — Color wheel (frequency pie) */
function ColorWheel() {
  // Frequency target across the brand's body of work
  const data = [
    { name: "Paper",  color: "#FAFAFA", outline: "#D5D5D7", pct: 36, role: "Default surface" },
    { name: "Ink",    color: "#0A0A0C", pct: 28, role: "Default surface" },
    { name: "Yellow", color: "#FFE241", pct: 12, role: "Primary highlight" },
    { name: "Blue",   color: "#3236FF", pct: 10, role: "Data · intent" },
    { name: "Orange", color: "#F55910", pct: 6,  role: "Light-mode signal" },
    { name: "Green",  color: "#00D190", pct: 4,  role: "Alignment marker" },
    { name: "Purple", color: "#D7B3FF", pct: 3,  role: "Hypothetical · soft" },
    { name: "Red",    color: "#EF353E", pct: 1,  role: "Negative · failure" },
  ];
  const cx = 50, cy = 50, r = 44;
  let acc = 0;
  const slices = data.map(d => {
    const start = acc;
    const end = acc + (d.pct / 100) * Math.PI * 2;
    acc = end;
    const x1 = cx + r * Math.cos(start - Math.PI / 2);
    const y1 = cy + r * Math.sin(start - Math.PI / 2);
    const x2 = cx + r * Math.cos(end - Math.PI / 2);
    const y2 = cy + r * Math.sin(end - Math.PI / 2);
    const large = (end - start) > Math.PI ? 1 : 0;
    return { ...d, path: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z` };
  });
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="04.G">Color wheel — frequency of use</SubHead>
      <h2 className="section-title">How much of which. <br/>Across a year of artifacts.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The target distribution across a year of bdg work. Paper and ink hold the room. Yellow is the most-used highlight. The other five hues rotate by context — never absent, never dominant.</p>
      <div className="wheel-block">
        <svg viewBox="0 0 100 100" role="img" aria-label="Brand color frequency">
          {slices.map(s => (
            <path key={s.name} d={s.path} fill={s.color} stroke={s.outline || "#0A0A0C"} strokeWidth="0.3"/>
          ))}
          <circle cx={cx} cy={cy} r={20} fill="var(--paper)" stroke="var(--line)" strokeWidth="0.4"/>
          <text x={cx} y={cy - 2} textAnchor="middle" fontFamily="DM Mono" fontSize="6" letterSpacing="0.6" fill="var(--ink-2)">DISTRIBUTION</text>
          <text x={cx} y={cy + 6} textAnchor="middle" fontFamily="Instrument Sans Cond, Helvetica" fontWeight="650" fontSize="8" fill="var(--ink)">8 hues</text>
        </svg>
        <div className="wheel-legend">
          {data.map(d => (
            <div key={d.name} className="row">
              <span className="chip" style={{background: d.color, border: d.outline ? "0.8px solid " + d.outline : "0.8px solid transparent"}}/>
              <span className="name">{d.name}</span>
              <span className="pct">{d.pct}%</span>
              <span className="role">{d.role}</span>
            </div>
          ))}
        </div>
      </div>
      <p className="body" style={{marginTop: 14, maxWidth: "60ch"}}>The ratio is a target, not a quota. A single artifact may sit on a single surface; a campaign across artifacts must add up to roughly this distribution. Two highlights or two surface colors competing inside one piece is a sign the artifact is doing too much.</p>
    </div>
  );
}

/* --------------------------- 05 TYPOGRAPHY ------------------------------- */

function TypographyChapter({ onCopy }) {
  const scale = [
    {n:"H1", px: 56, lh: 1.05, weight: 650, fam:"Cond"},
    {n:"H2", px: 48, lh: 1.05, weight: 650, fam:"Cond"},
    {n:"H3", px: 40, lh: 1.10, weight: 650, fam:"Cond"},
    {n:"H4", px: 32, lh: 1.15, weight: 650, fam:"Cond"},
    {n:"H5", px: 24, lh: 1.20, weight: 650, fam:"Cond"},
    {n:"H6", px: 20, lh: 1.25, weight: 650, fam:"Cond"},
  ];
  return (
    <section id="typography" className="chapter" data-screen-label="05 Typography">
      <ChapterHead num="05" kicker="Tokens" title={<>Typography.</>} meta={[["Headline","Instr. Sans Cond"], ["Body","Instrument Sans"], ["Mono","DM Mono"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="05.A">Pairing</SubHead>
          <p className="lede"><span className="neq">Condensed</span> for thesis. Regular for narrative. Mono for chrome.</p>
          <p className="body" style={{marginTop: 18}}>Tight condensed type at large scale is the visual identity. Body copy snaps to a ladder — there are no inline overrides.</p>
        </div>
        <div className="right">
          <div style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 96, lineHeight: 0.92, letterSpacing:"-0.025em"}}>Ag 01.</div>
          <div style={{fontFamily:"var(--f-body)", fontSize: 22, lineHeight: 1.4}}>The quick brown fox jumps over the lazy dog. — Instrument Sans, body</div>
          <div style={{fontFamily:"var(--f-mono)", fontSize: 12, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-2)"}}>DM Mono · chrome · labels</div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="05.B">Heading scale — desktop</SubHead>
        <div style={{borderTop:"0.8px solid var(--line)"}}>
          {scale.map(s => (
            <button key={s.n} onClick={() => onCopy(`font-size: ${s.px}px; line-height: ${s.lh}; font-weight: ${s.weight};`, s.n + " · " + s.px + "/" + s.lh)} style={{display:"grid", gridTemplateColumns:"80px 120px 1fr 120px", alignItems:"baseline", padding:"22px 0", borderBottom:"0.8px solid var(--line)", width:"100%", textAlign:"left", cursor:"pointer", background:"transparent"}}>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 12, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-2)"}}>{s.n}</span>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 12, color:"var(--ink-3)"}}>{s.px}/{Math.round(s.px*s.lh)}</span>
              <span style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: s.px, lineHeight: s.lh, letterSpacing:"-0.01em"}}>The next decision.</span>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", color:"var(--ink-3)", textAlign:"right"}}>Copy CSS</span>
            </button>
          ))}
        </div>
      </div>

      <BodyLadder onCopy={onCopy}/>
      <WeightTable />
      <TaglineStyle />
      <VerticalRhythm />
      <LineLength />
    </section>
  );
}

/* 05.D — Body ladder */
function BodyLadder({ onCopy }) {
  const rungs = [
    { rung: "L",  size: 34, lh: 1.30, demo: "Primary supporting copy. Sits below a thesis statement; carries the framing argument. Reads at distance.", spec: "34 / 44" },
    { rung: "M",  size: 26, lh: 1.35, demo: "Standard reading copy. The default body. Long paragraphs, narrative, framing arguments — most of the words live here.", spec: "26 / 35" },
    { rung: "S",  size: 22, lh: 1.40, demo: "List items, descriptions, meta. Dense rows, table cells, content adjacent to numerical data.", spec: "22 / 31" },
    { rung: "XS", size: 18, lh: 1.45, demo: "Captions, footnotes, chrome. Sits beneath the eye, never the centerpiece.", spec: "18 / 26" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="05.C">Body ladder — four rungs, no overrides</SubHead>
      <h2 className="section-title">Every paragraph snaps to a rung.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>The ladder is non-negotiable. There are no in-line font-size overrides; if a paragraph needs a different size, it belongs on a different rung. Click any row to copy the spec.</p>
      <div className="body-ladder">
        {rungs.map(r => (
          <div key={r.rung} className="br" onClick={() => onCopy(`font-size: ${r.size}px; line-height: ${r.lh};`, r.rung + " · " + r.spec)} style={{cursor:"pointer"}}>
            <span className="rung">{r.rung}</span>
            <span className="spec">{r.spec}</span>
            <span className="demo" style={{fontSize: r.size, lineHeight: r.lh}}>{r.demo}</span>
            <span className="copy">Copy ⧉</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 05.D.ii — weights */
function WeightTable() {
  const wts = [
    { w: 400, name: "Regular" },
    { w: 500, name: "Medium" },
    { w: 600, name: "Semi" },
    { w: 650, name: "Bold" },
    { w: 700, name: "Extra" },
  ];
  return (
    <div style={{marginTop: 24}}>
      <div className="weight-grid">
        {wts.map(w => (
          <div className="wt" key={w.w}>
            <span className="num" style={{fontWeight: w.w}}>Ag 01</span>
            <span className="lbl">{w.name} · {w.w}</span>
          </div>
        ))}
      </div>
      <p className="body" style={{marginTop: 14, maxWidth:"60ch"}}>
        Headlines use <strong>650</strong> exclusively. Body sits at <strong>400</strong>; emphasis is <strong>600</strong>. Italic is reserved for the editorial serif pull-quote — the brand's other emphasis is the solid orange box, not slant.
      </p>
    </div>
  );
}

/* 05.E — Tagline style */
function TaglineStyle() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="05.D">Tagline style</SubHead>
      <h2 className="section-title">The tagline is a unit. <br/>Three lines, one highlight.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>The brand never accents text by re-colouring the characters. Emphasis is always a <strong>solid block</strong>, tight to cap-height, with contrasting text inside it. One word, one block — rotate the colour to fit the room.</p>
      <div className="tagline-block">
        <h3 className="lockup">Honest data.<br/>Better decisions.<br/><span className="em">Real impact.</span></h3>
        <div className="anatomy">
          <div><span className="k">Family</span><span className="v">Instr. Sans Cond</span></div>
          <div><span className="k">Weight</span><span className="v">650</span></div>
          <div><span className="k">Case</span><span className="v">Sentence + period</span></div>
          <div><span className="k">Tracking</span><span className="v">−2.5%</span></div>
          <div><span className="k">Leading</span><span className="v">92%</span></div>
          <div><span className="k">Last line</span><span className="v">Highlight block</span></div>
          <div><span className="k">Rule</span><span className="v">Always three lines</span></div>
          <div><span className="k">Min size</span><span className="v">28pt / 36px</span></div>
        </div>
      </div>

      <div style={{marginTop: 28, display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(220px, 1fr))", gap: 12}}>
        {[
          { c: "",  bg: "var(--paper-2)", lbl: "Yellow · default light surface" },
          { c: "b", bg: "var(--paper-2)", lbl: "Blue · used against muted surface" },
          { c: "p", bg: "var(--paper-2)", lbl: "Purple · hypothetical, soft tone" },
          { c: "g", bg: "#0A0A0C",         lbl: "Green · dark surface · alignment", dark: true },
          { c: "o", bg: "#0A0A0C",         lbl: "Orange · dark surface · signal",   dark: true },
        ].map((v, i) => (
          <div key={i} style={{border:"0.8px solid var(--line)", padding: 20, background: v.bg, color: v.dark ? "#F4F4F5" : "var(--ink)", display:"flex", flexDirection:"column", gap: 14, minHeight: 200}}>
            <div style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 20, lineHeight: 0.96, letterSpacing:"-0.015em"}}>
              Honest data.<br/>Better decisions.<br/><span className={"em " + v.c}>Real impact.</span>
            </div>
            <div style={{marginTop:"auto", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.16em", textTransform:"uppercase", opacity: 0.65}}>{v.lbl}</div>
          </div>
        ))}
      </div>

      <p className="body" style={{marginTop: 14, maxWidth:"60ch"}}>
        The colour of the highlight is not fixed. Light surfaces lean yellow or blue; dark surfaces lean green or orange; purple is reserved for hypothetical and softer applications. <strong>One highlight per title</strong> — never two blocks on the same headline. Mix colours across the page, not within a single line.
      </p>
    </div>
  );
}

/* 05.F — Vertical rhythm */
function VerticalRhythm() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="05.E">Vertical rhythm</SubHead>
      <h2 className="section-title">An 8-pixel grid. <br/>Every baseline lands on it.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>
        Line-heights snap to multiples of <strong>8px</strong>. Headlines, body, captions — everything rides the same rhythm. The eye doesn't see it; the page just feels right.
      </p>
      <div className="rhythm">
        <div>
          <div className="tag ok">On rhythm · 8-unit baseline</div>
          <div className="demo">
            <h6>The decision is the unit.</h6>
            <p>Two clauses per sentence. Three sentences per paragraph. Each line lands on the 8-unit baseline; the gap between blocks is a multiple of 8.</p>
            <p>The result is a page that reads as one continuous column of evidence, not a collection of cards.</p>
          </div>
        </div>
        <div>
          <div className="tag no">Off rhythm · ad-hoc leading</div>
          <div className="demo">
            <h6>The decision is the unit.</h6>
            <p>Two clauses per sentence. Three sentences per paragraph. Line-heights drift; gaps are whatever felt right. The page reads as a collection of unrelated cards.</p>
            <p>You can feel the difference long before you can name it.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

/* 05.G — Line length */
function LineLength() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="05.F">Line length</SubHead>
      <h2 className="section-title">45 to 75 characters. <br/>The reading column has limits.</h2>
      <div className="linelen">
        <div className="ll short">
          <span className="tag">× Too short · &lt;45 ch</span>
          <span className="demo">The reader is tossed back to the start before settling.</span>
          <span className="ch">28 ch</span>
        </div>
        <div className="ll right">
          <span className="tag">✓ Right · 45–75 ch</span>
          <span className="demo">The default reading column for body copy. Long enough to settle, short enough to never lose the next line.</span>
          <span className="ch">66 ch</span>
        </div>
        <div className="ll long">
          <span className="tag">× Too long · &gt;75 ch</span>
          <span className="demo">When a column gets too wide the eye loses its way at the start of the next line and the reader rereads or skips ahead — both kill comprehension and slow the page down.</span>
          <span className="ch">112 ch</span>
        </div>
      </div>
      <p className="body" style={{marginTop: 14, maxWidth:"60ch"}}>Set <code style={{fontFamily:"var(--f-mono)", fontSize: 13}}>max-width: 60ch</code> as the default. Use <code style={{fontFamily:"var(--f-mono)", fontSize: 13}}>text-wrap: pretty</code> on paragraphs and <code style={{fontFamily:"var(--f-mono)", fontSize: 13}}>balance</code> on headlines.</p>
    </div>
  );
}

/* --------------------------- 06 ICONOGRAPHY ------------------------------ */

function IconographyChapter() {
  return (
    <section id="icons" className="chapter" data-screen-label="06 Iconography">
      <ChapterHead num="06" kicker="Marks" title={<>Iconography.</>} meta={[["Glyphs", "Unicode only"], ["Icon set", "None"], ["Marks", "≠ · → ● —"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="06.A">Position</SubHead>
          <p className="lede">The system has <span className="neq">no</span> icon font. Structure is communicated through hairlines, dots, arrows, and the inequality glyph.</p>
          <p className="body" style={{marginTop: 18}}>Where most brands hire an illustrator, bdg hires a typographer. The vocabulary is small on purpose: every mark earns its place, and every mark is a Unicode glyph or a CSS primitive. Nothing is drawn from scratch.</p>
        </div>
        <div className="right">
          <div className="glyph-grid">
            {[
              { m: "≠", lbl: "U+2260", role: "Inequality" },
              { m: "→", lbl: "U+2192", role: "Arrow" },
              { m: "↑", lbl: "U+2191", role: "Up" },
              { m: "↓", lbl: "U+2193", role: "Down" },
              { m: "·", lbl: "U+00B7", role: "Middle dot" },
              { m: "—", lbl: "U+2014", role: "Em dash" },
            ].map(g => (
              <div key={g.m} className="gly">
                <div className="mark">{g.m}</div>
                <div className="lbl"><span>{g.lbl}</span><span className="role">{g.role}</span></div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <NeqAsSystem />
      <Structural />
      <FiguresAndMatrix />
    </section>
  );
}

/* 06.B — ≠ as a system mark */
function NeqAsSystem() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="06.B">≠ as a system mark</SubHead>
      <h2 className="section-title">One symbol carries the brand's logic.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The inequality glyph is the recurring brand mark. It frames every contrast in the system — what something is, and what it is mistaken for. Use it as a separator, a header rule, a layout anchor; never as decoration.</p>
      <div className="neq-block">
        <div className="left">
          <span className="big-neq">≠</span>
          <div style={{display:"flex", flexDirection:"column", gap: 6, marginTop: "auto"}}>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 11, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>Inequality · U+2260</span>
            <span style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 16, letterSpacing:"-0.005em"}}>The brand's logic, in one glyph.</span>
          </div>
        </div>
        <div className="right">
          {[
            ["Prediction",  "Understanding"],
            ["Optimization","Alignment"],
            ["Speed",       "Clarity"],
            ["Reporting",   "Decision"],
            ["Accuracy",    "Ownership"],
            ["Dashboard",   "Decision"],
          ].map(([a, b], i) => (
            <div key={i} className="nex">
              <span className="a">{a}</span>
              <span className="g">≠</span>
              <span className="b">{b}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* 06.C — Hairlines & dots */
function Structural() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="06.C">Hairlines, dots, arrows</SubHead>
      <h2 className="section-title">Structure without illustration.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Three primitives do the work an icon set would do elsewhere — divide, mark, point.</p>
      <div className="struct-grid">
        <div className="struct">
          <div className="ttl">01 · Hairline</div>
          <div className="stage" style={{flexDirection:"column", gap: 14, alignItems:"stretch", padding:"0 4px"}}>
            <hr className="hair" style={{borderTop:"0.8px solid var(--ink)"}}/>
            <hr className="hair" style={{borderTop:"0.8px solid var(--signal)"}}/>
            <hr className="hair" style={{borderTop:"0.8px dashed var(--ink-2)"}}/>
          </div>
          <div className="desc">0.8px solid. The default page rule, the cell border, the chapter divider. Dashed only for guides; never decoration.</div>
        </div>
        <div className="struct">
          <div className="ttl">02 · Dot</div>
          <div className="stage" style={{gap: 18}}>
            <span style={{width: 8, height: 8, background:"var(--signal)", borderRadius:"50%", display:"inline-block"}}/>
            <span style={{width: 10, height: 10, background:"var(--ink)", display:"inline-block"}}/>
            <span style={{width: 12, height: 12, border:"1px solid var(--ink)", display:"inline-block"}}/>
          </div>
          <div className="desc">Solid signal dot · solid ink square · outlined square. Used as eyebrow marker, list bullet, and active-state indicator.</div>
        </div>
        <div className="struct">
          <div className="ttl">03 · Arrow</div>
          <div className="stage" style={{gap: 18, fontFamily:"var(--f-headline)", fontWeight: 650, fontSize: 40, color:"var(--ink)"}}>
            <span>→</span>
            <span style={{color:"var(--signal)"}}>↑</span>
            <span style={{color:"var(--ink-2)"}}>↓</span>
          </div>
          <div className="desc">Unicode glyphs only. → for sequence; ↑↓ for change; ▲▼ for trend. One weight, one purpose, no rotation.</div>
        </div>
      </div>
    </div>
  );
}

/* 06.D + 06.E — figure pictogram and dot matrix */
function FiguresAndMatrix() {
  // 17 dots on, rest off — visualises ~17%
  const matrix = Array.from({length: 100}, (_, i) => i < 17);
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="06.D / 06.E">Pictogram &amp; dot matrix</SubHead>
      <h2 className="section-title">Two diagrams. <br/>Both communicate scale, neither decorates.</h2>
      <div className="figure-grid">
        <div className="figure-cell">
          <div style={{display:"flex", justifyContent:"space-between", alignItems:"baseline", gap: 8}}>
            <h3 className="block-title">Figure · X of Y</h3>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>06.D</span>
          </div>
          <div className="fig-stage" aria-hidden="true">
            <span className="fig signal"/><span className="fig signal"/><span className="fig signal"/>
            <span className="fig dim"/><span className="fig dim"/><span className="fig dim"/><span className="fig dim"/><span className="fig dim"/><span className="fig dim"/><span className="fig dim"/>
          </div>
          <div className="body">3 in 10 — a single inline-SVG figure repeated; foreground inherits <code style={{fontFamily:"var(--f-mono)"}}>currentColor</code>, background uses the muted ladder. Use for ratios that need a human read.</div>
        </div>
        <div className="figure-cell">
          <div style={{display:"flex", justifyContent:"space-between", alignItems:"baseline", gap: 8}}>
            <h3 className="block-title">Dot matrix · 100 = 100%</h3>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>06.E</span>
          </div>
          <div className="dot-matrix" role="img" aria-label="17 of 100">
            {matrix.map((on, i) => <span key={i} className={"d" + (on ? " on" : "")}/>)}
          </div>
          <div className="body">17 of 100 — every dot is a unit of scale. Filled = signal; dimmed = remainder. Reads cleanly at any size, including print.</div>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- 07 IMAGERY ---------------------------------- */

function FieldCanvas({ kind, accent }) {
  const ref = useRef(null);
  useEffect(() => {
    const cvs = ref.current;
    if (!cvs) return;
    const ctx = cvs.getContext("2d");
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let raf, t0 = performance.now();

    const fit = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const w = cvs.parentElement.clientWidth;
      const h = cvs.parentElement.clientHeight;
      cvs.width = w * dpr; cvs.height = h * dpr;
      cvs.style.width = w + "px"; cvs.style.height = h + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(cvs.parentElement);

    // Seeded random
    let seed = 1234;
    const rnd = () => { seed = (seed * 16807) % 2147483647; return seed / 2147483647; };

    let nodes = [];
    if (kind === "network") {
      for (let i = 0; i < 22; i++) {
        nodes.push({ x: rnd(), y: rnd(), vx: (rnd() - 0.5) * 0.0006, vy: (rnd() - 0.5) * 0.0006, s: rnd() * 2 + 1, hot: i < 3 });
      }
    } else if (kind === "particle") {
      for (let i = 0; i < 80; i++) {
        nodes.push({ x: rnd(), y: rnd(), vx: (rnd() - 0.5) * 0.0014, vy: (rnd() - 0.5) * 0.0014, life: rnd() });
      }
    }

    const sig = accent || "#F55910";

    const draw = (t) => {
      const w = cvs.clientWidth, h = cvs.clientHeight;
      ctx.clearRect(0, 0, w, h);
      const time = reduce ? 0 : (t - t0) / 1000;

      if (kind === "network") {
        // edges
        for (let i = 0; i < nodes.length; i++) {
          for (let j = i + 1; j < nodes.length; j++) {
            const a = nodes[i], b = nodes[j];
            const dx = (a.x - b.x) * w, dy = (a.y - b.y) * h;
            const d = Math.hypot(dx, dy);
            if (d < 160) {
              ctx.globalAlpha = (1 - d / 160) * 0.35;
              ctx.strokeStyle = (a.hot || b.hot) ? sig : "rgba(255,255,255,0.7)";
              ctx.lineWidth = (a.hot || b.hot) ? 0.8 : 0.6;
              ctx.beginPath();
              ctx.moveTo(a.x * w, a.y * h);
              ctx.lineTo(b.x * w, b.y * h);
              ctx.stroke();
            }
          }
        }
        ctx.globalAlpha = 1;
        // nodes
        nodes.forEach(n => {
          n.x += n.vx; n.y += n.vy;
          if (n.x < 0 || n.x > 1) n.vx *= -1;
          if (n.y < 0 || n.y > 1) n.vy *= -1;
          ctx.fillStyle = n.hot ? sig : "rgba(255,255,255,0.85)";
          ctx.beginPath();
          ctx.arc(n.x * w, n.y * h, n.s + (n.hot ? 1.5 : 0), 0, Math.PI * 2);
          ctx.fill();
        });
      }
      else if (kind === "density") {
        const cols = 26, rows = 12;
        const cw = w / cols, ch = h / rows;
        for (let r = 0; r < rows; r++) {
          for (let c = 0; c < cols; c++) {
            const phase = (c * 0.34 + r * 0.21) + time * 0.7;
            const d = (Math.sin(phase) + Math.sin(phase * 1.7)) * 0.25 + 0.5;
            ctx.fillStyle = "rgba(255,255,255," + d * 0.85 + ")";
            const inset = 1.5;
            ctx.fillRect(c * cw + inset, r * ch + inset, cw - inset * 2, ch - inset * 2);
            if ((c + r) % 11 === 0) {
              ctx.fillStyle = sig;
              ctx.fillRect(c * cw + inset, r * ch + inset, cw - inset * 2, ch - inset * 2);
            }
          }
        }
      }
      else if (kind === "particle") {
        nodes.forEach(n => {
          n.x += n.vx; n.y += n.vy;
          n.life += 0.004;
          if (n.x < 0) n.x = 1; if (n.x > 1) n.x = 0;
          if (n.y < 0) n.y = 1; if (n.y > 1) n.y = 0;
          if (n.life > 1) { n.life = 0; n.x = rnd(); n.y = rnd(); }
          ctx.globalAlpha = Math.sin(n.life * Math.PI) * 0.75;
          ctx.fillStyle = n.life > 0.7 ? sig : "rgba(255,255,255,0.85)";
          ctx.beginPath();
          ctx.arc(n.x * w, n.y * h, 1.5, 0, Math.PI * 2);
          ctx.fill();
        });
        ctx.globalAlpha = 1;
      }
      else if (kind === "flow") {
        ctx.lineWidth = 0.6;
        for (let i = 0; i < 14; i++) {
          ctx.strokeStyle = i % 5 === 0 ? sig : "rgba(255,255,255," + (0.25 + i * 0.04) + ")";
          ctx.beginPath();
          for (let x = 0; x <= w; x += 4) {
            const p = x / 140 + time * 0.4 + i * 0.4;
            const y = h * (0.1 + i / 14 * 0.8) + Math.sin(p) * 10 + Math.sin(p * 2.3) * 5;
            if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
          }
          ctx.stroke();
        }
      }
      if (!reduce) raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, [kind, accent]);
  return <canvas ref={ref} aria-hidden="true" />;
}

function ImageryChapter() {
  return (
    <section id="imagery" className="chapter" data-screen-label="07 Imagery">
      <ChapterHead num="07" kicker="Direction" title={<>Imagery.</>} meta={[["Stock","Forbidden"], ["AI collage","Forbidden"], ["Generative","Default"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="07.A">Direction</SubHead>
          <p className="lede">No stock. No collage. <span className="neq">Generative</span> data art and editorial photography only — the image is itself a system reading.</p>
          <p className="body" style={{marginTop: 18}}>An image earns its place by revealing structure that words can't. If the visual is illustrative — a metaphor, a stock business hero, a generic AI rendering — it's removed. Every image is either a system diagram, a real artefact, or a particle field that visualises the underlying data.</p>
        </div>
        <div className="right">
          <div className="imagery-grid">
            {[
              { kind: "network", ix: "01", ttl: "Network", desc: "Nodes and connections — decision graphs, dependency maps, agreement clusters." },
              { kind: "density", ix: "02", ttl: "Density grid", desc: "Cells of varying weight — distributions, coverage, signal across categories." },
              { kind: "particle",ix: "03", ttl: "Particle field", desc: "Discrete events resolving into pattern — flow of decisions over time." },
              { kind: "flow",    ix: "04", ttl: "Flow lines", desc: "Tracing structure through a system — cadence, throughput, drift." },
            ].map(c => (
              <div key={c.kind} className="img-card">
                <div className="canvas"><FieldCanvas kind={c.kind} /></div>
                <div className="meta">
                  <span className="ix">07.A.{c.ix}</span>
                  <h4>{c.ttl}</h4>
                  <p>{c.desc}</p>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <ReferenceArtists />
      <DoDontGallery />
      <LiveGenerativeField />
      <PhotoLibrary />
    </section>
  );
}

/* 07.E — Photo library (filterable + searchable + editable + downloadable) */
function PhotoLibrary() {
  const store = usePhotoStore();
  const allPhotos = store.photos;
  const [q, setQ] = useState("");
  const [industry, setIndustry] = useState("All");
  const [active, setActive] = useState(null);
  const [editMode, setEditMode] = useState(() => localStorage.getItem(STORE_KEYS.editMode) === "1");
  const [editing, setEditing] = useState(null);     // photo being edited (existing or draft)
  const [exportOpen, setExportOpen] = useState(false);

  useEffect(() => {
    localStorage.setItem(STORE_KEYS.editMode, editMode ? "1" : "0");
  }, [editMode]);

  const industries = useMemo(() => {
    const counts = { All: allPhotos.length };
    for (const i of PHOTO_INDUSTRIES) counts[i] = allPhotos.filter(p => p.industry === i).length;
    return counts;
  }, [allPhotos]);

  const filtered = useMemo(() => {
    const v = q.trim().toLowerCase();
    return allPhotos.filter(p => {
      if (industry !== "All" && p.industry !== industry) return false;
      if (!v) return true;
      const hay = (p.name + " " + p.industry + " " + (p.description || "") + " " + p.tags.join(" ")).toLowerCase();
      return hay.includes(v);
    });
  }, [q, industry, allPhotos]);

  /* Esc closes the lightbox; arrow keys step through the filtered set. */
  useEffect(() => {
    if (active == null) return;
    const onKey = (e) => {
      if (e.key === "Escape") setActive(null);
      if (e.key === "ArrowRight") setActive(a => Math.min((a ?? 0) + 1, filtered.length - 1));
      if (e.key === "ArrowLeft")  setActive(a => Math.max((a ?? 0) - 1, 0));
    };
    document.body.style.overflow = "hidden";
    window.addEventListener("keydown", onKey);
    return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); };
  }, [active, filtered.length]);

  const downloadOne = async (p) => {
    await fetchAndDownload(photoSlug(p.name), photoFullUrl(p));
  };

  const activePhoto = active != null ? filtered[active] : null;

  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="07.E">Reference library</SubHead>
      <h2 className="section-title">{PHOTOS.length} editorial references. <br/>Searchable. Tagged. Downloadable.</h2>
      <p className="body" style={{marginBottom: 24, maxWidth: "62ch"}}>Use these for case studies, audience pages, and deck specimens — where a real artefact reads truer than a generative field. Never decorative; always evidence. Click a thumbnail to preview. Click <span className="kbd" style={{fontSize:10}}>⤓</span> to download the original.</p>

      <div className="lib-controls">
        <div className="lib-search">
          <span className="lib-search-glyph" aria-hidden="true">⌕</span>
          <input
            type="search"
            placeholder="Search by name, tag, industry, or description —"
            value={q}
            onChange={e => setQ(e.target.value)}
            aria-label="Search photo library"
          />
          {q && <button className="lib-clear" type="button" onClick={() => setQ("")} aria-label="Clear search">×</button>}
        </div>
        <div className="lib-actions">
          <div className="lib-meta">{filtered.length} of {allPhotos.length} shown</div>
          <button
            type="button"
            className="lib-mode"
            data-active={editMode}
            onClick={() => setEditMode(m => !m)}
            aria-pressed={editMode}
            title="Toggle edit mode"
          >{editMode ? "✓ Editing" : "✎ Edit"}</button>
          {editMode && (
            <>
              <UploadButton onUpload={async (drafts) => {
                for (const d of drafts) await store.addDraft(d.draft, d.blob);
              }} />
              <button
                type="button"
                className="lib-export-btn"
                onClick={() => setExportOpen(true)}
                title="Export updated manifest"
              >⇪ Export</button>
            </>
          )}
        </div>
      </div>

      <div className="lib-chips" role="tablist" aria-label="Filter by industry">
        {["All", ...PHOTO_INDUSTRIES].map(k => (
          <button
            key={k}
            type="button"
            role="tab"
            aria-selected={industry === k}
            className="lib-chip"
            data-active={industry === k}
            onClick={() => setIndustry(k)}
          >
            {k}
            <span className="lib-chip-count">{industries[k] ?? 0}</span>
          </button>
        ))}
      </div>

      {filtered.length === 0 ? (
        <div className="lib-empty">
          No matches for <em>{q}</em> in <em>{industry}</em>. Try a broader term, or reset the industry filter.
        </div>
      ) : (
        <div className="lib-grid">
          {filtered.map((p, i) => (
            <figure key={p.file + (p.draft ? "-d" : "")} className="lib-card" data-draft={p.draft ? "true" : undefined}>
              <button
                type="button"
                className="lib-thumb"
                onClick={() => setActive(i)}
                aria-label={`Open ${p.name} preview`}
              >
                <img
                  src={photoThumbUrl(p)}
                  alt={p.name}
                  loading="lazy"
                  decoding="async"
                />
                {p.draft && <span className="lib-badge">Draft</span>}
                {!p.draft && store.overrides[p.file] && <span className="lib-badge edit">Edited</span>}
              </button>
              <figcaption className="lib-cap">
                <div className="lib-name">{p.name}</div>
                <div className="lib-meta-row">
                  <span className="lib-ind">{p.industry}</span>
                  <div className="lib-card-actions">
                    {editMode && (
                      <button
                        type="button"
                        className="lib-pencil"
                        onClick={() => setEditing(p)}
                        aria-label={`Edit ${p.name}`}
                        title="Edit metadata"
                      >✎</button>
                    )}
                    <button
                      type="button"
                      className="lib-dl"
                      onClick={() => downloadOne(p)}
                      aria-label={`Download ${p.name}`}
                      title={`Download as ${photoSlug(p.name)}`}
                    >⤓</button>
                  </div>
                </div>
                <div className="lib-tags">
                  {p.tags.slice(0, 3).map(t => (<span key={t} className="lib-tag">{t}</span>))}
                </div>
              </figcaption>
            </figure>
          ))}
        </div>
      )}

      {editing && (
        <PhotoEditor
          photo={editing}
          store={store}
          onClose={() => setEditing(null)}
        />
      )}
      {exportOpen && (
        <ExportManifest
          photos={allPhotos}
          overrides={store.overrides}
          drafts={store.drafts}
          onClose={() => setExportOpen(false)}
        />
      )}

      {activePhoto && (
        <div className="lib-light" onClick={() => setActive(null)} role="dialog" aria-modal="true" aria-label={activePhoto.name}>
          <button className="lib-light-close" onClick={() => setActive(null)} aria-label="Close preview">×</button>
          <button
            className="lib-light-nav prev"
            onClick={(e) => { e.stopPropagation(); setActive(a => Math.max(a - 1, 0)); }}
            disabled={active === 0}
            aria-label="Previous"
          >←</button>
          <button
            className="lib-light-nav next"
            onClick={(e) => { e.stopPropagation(); setActive(a => Math.min(a + 1, filtered.length - 1)); }}
            disabled={active === filtered.length - 1}
            aria-label="Next"
          >→</button>
          <figure className="lib-light-stage" onClick={e => e.stopPropagation()}>
            <img src={photoFullUrl(activePhoto)} alt={activePhoto.name} />
            <figcaption className="lib-light-cap">
              <div className="lib-light-meta">
                <span className="lib-light-name">{activePhoto.name}</span>
                <span className="lib-ind">{activePhoto.industry}</span>
              </div>
              {activePhoto.description && (
                <div className="lib-light-desc">{activePhoto.description}</div>
              )}
              <div className="lib-light-tags">
                {activePhoto.tags.map(t => (<span key={t} className="lib-tag">{t}</span>))}
              </div>
              <div className="lib-light-actions">
                <span className="lib-light-pos">{active + 1} / {filtered.length}</span>
                {editMode && (
                  <button
                    type="button"
                    className="lib-light-edit"
                    onClick={() => { setEditing(activePhoto); setActive(null); }}
                    title="Edit metadata"
                  >✎ Edit</button>
                )}
                <button
                  type="button"
                  className="lib-light-dl"
                  onClick={() => downloadOne(activePhoto)}
                >⤓ Download original</button>
              </div>
            </figcaption>
          </figure>
        </div>
      )}
    </div>
  );
}

/* 07.B — References
   Wikipedia data is fetched in-browser at render time via the public REST
   summary API. That keeps the cards live (no manual image refresh needed)
   and avoids embedding any artwork copyright into source. The article
   thumbnail Wikipedia returns is either a portrait of the artist or a
   representative work; both serve as a credible visual anchor. */

const REF_ARTISTS = [
  { ix:"01", name:"Ryoji Ikeda",          slug:"Ryoji_Ikeda",          famous:"test pattern · 2008",       medium:"Audio-visual data art",            work:"Data art — granular, monochrome, computational. The reference for density visualisation." },
  { ix:"02", name:"Refik Anadol",         slug:"Refik_Anadol",         famous:"Machine Hallucinations · 2019", medium:"Machine-learning data sculpture", work:"Machine-learning latent space made physical. Reference for generative fields at architectural scale." },
  { ix:"03", name:"Mark Lombardi",        slug:"Mark_Lombardi",        famous:"BCCI-ICIC & FAB · 1996",    medium:"Conceptual network diagrams",      work:"Network diagrams as journalism. The reference for relationship maps and decision graphs." },
  { ix:"04", name:"Cy Twombly",           slug:"Cy_Twombly",           famous:"Untitled (Bacchus) · 2005", medium:"Gestural mark-making painting",    work:"Marks as evidence — handwriting on a wall. Reference for annotation gestures over computational structure." },
  { ix:"05", name:"Jean-Michel Basquiat", slug:"Jean-Michel_Basquiat", famous:"Untitled (Skull) · 1981",   medium:"Neo-expressionist painting",       work:"Layered notation — text, mark, system, body. Reference for expressive interrupt moments." },
  { ix:"06", name:"Keith Haring",         slug:"Keith_Haring",         famous:"Radiant Baby · 1990",       medium:"Pop-graffiti pictograms",          work:"Pictogram as a system. Reference for repeated marks that read as data at distance." },
];

function useArtistSummary(slug) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let cancelled = false;
    /* Wikipedia REST: https://en.wikipedia.org/api/rest_v1/page/summary/<slug>
       Returns JSON with thumbnail.source (CDN URL), content_urls.desktop.page,
       extract, etc. CORS-enabled, no auth required. */
    fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(slug)}`)
      .then(r => r.ok ? r.json() : null)
      .then(j => { if (!cancelled && j) setData(j); })
      .catch(() => { /* network blocked / offline — silently fall back to text-only card */ });
    return () => { cancelled = true; };
  }, [slug]);
  return data;
}

function ReferenceArtists() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="07.B">Reference artists</SubHead>
      <h2 className="section-title">Six points of reference. <br/>Not a mood board — a coordinate system.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The brand sits where computational rigor meets editorial expression. These artists define the corners; new imagery is plotted somewhere inside. Cards link to Wikipedia; the page-image is the article thumbnail, fetched live.</p>
      <div className="ref-grid">
        {REF_ARTISTS.map(r => (
          <ReferenceArtistCard key={r.name} artist={r} />
        ))}
      </div>
    </div>
  );
}

function ReferenceArtistCard({ artist: r }) {
  const data = useArtistSummary(r.slug);
  const wiki = data?.content_urls?.desktop?.page || `https://en.wikipedia.org/wiki/${r.slug}`;
  const thumb = data?.thumbnail?.source;
  return (
    <a
      key={r.name}
      className="ref"
      href={wiki}
      target="_blank"
      rel="noopener noreferrer"
      title={`Wikipedia: ${r.name}`}
    >
      <span className="ix">07.B.{r.ix}</span>
      <div className="ref-stage">
        {thumb ? (
          <img src={thumb} alt={`${r.name} — Wikipedia thumbnail`} loading="lazy" decoding="async" />
        ) : (
          <div className="ref-stage-empty" aria-hidden="true">
            <span className="ref-stage-label">{r.medium}</span>
          </div>
        )}
      </div>
      <h5>{r.name}</h5>
      <div className="ref-famous">{r.famous}</div>
      <p className="work">{r.work}</p>
      <div className="arrow">
        <span>Wikipedia</span><span>↗</span>
      </div>
    </a>
  );
}

/* 07.C — Do / Don't */
function DoDontGallery() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="07.C">Do, don't</SubHead>
      <h2 className="section-title">Two stages. <br/>The first earns the page; the second is removed.</h2>
      <div className="dodont">
        <div className="panel do">
          <div className="stage"><FieldCanvas kind="network" /></div>
          <div className="meta">
            <span className="tag">DO · System diagram</span>
            <h4>Reveal the structure the words just named.</h4>
            <p>A network, a density, a flow. Visible logic. The image is the second reading of the argument.</p>
          </div>
        </div>
        <div className="panel dont">
          <div className="stage"/>
          <div className="meta">
            <span className="tag">DON'T · Decorative gradient</span>
            <h4>No AI collage. No coloured fog. No "feeling."</h4>
            <p>If the image is mood, it goes. If you cannot point to what it visualises, it doesn't visualise anything.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

/* 07.D — Live generative field */
function LiveGenerativeField() {
  const today = new Date().toISOString().slice(0, 10);
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="07.D">Live generative field</SubHead>
      <h2 className="section-title">An image, computed in place.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>This is what an approved hero image looks like at full width. It runs in the page, respects reduced-motion, and never repeats. The field is the brand at scale.</p>
      <div className="live-field">
        <FieldCanvas kind="network" />
        <div className="caption">
          <span>System reading · 22 nodes · <strong>3 active</strong></span>
          <span>Rendered · {today} · respects ↩ reduced-motion</span>
        </div>
      </div>
    </div>
  );
}

/* 07.E — Reference photo library ----------------------------------------
   Hand-classified set of editorial reference photography. The brand
   forbids stock-as-decoration; this library exists for case studies, deck
   case-illustrations, and audience pages — places where a real artefact
   is the right answer. Each entry carries an industry primary tag plus
   2–4 keyword tags for filter + global-search use. */

const PHOTO_INDUSTRIES = ["Energy", "Manufacturing", "Logistics", "Architecture", "Retail", "Technology", "Healthcare", "Food", "Workspace", "Media", "Abstract"];

const PHOTOS = [
  { file: "a-c-oKdPxoc23g0-unsplash.jpg",                    name: "Cobalt grid · signal",        industry: "Abstract",     tags: ["data","blue","pattern","closeup"] },
  { file: "a-c-qxKnvb2HDHY-unsplash.jpg",                    name: "Red ribbon · motion",         industry: "Abstract",     tags: ["red","motion","liquid","macro"] },
  { file: "a-chosen-soul-qnHhZFU6rMQ-unsplash.jpg",          name: "Aluminium fluting",           industry: "Architecture", tags: ["monochrome","metal","facade","pattern"] },
  { file: "alex-shuper-GQ0eYCsAkYI-unsplash.jpg",            name: "Network cable",               industry: "Technology",   tags: ["telecom","cables","blue","infrastructure"] },
  { file: "alexander-mils-HfLtQ1gX7GE-unsplash.jpg",         name: "Turbine blade · close",       industry: "Energy",       tags: ["renewable","wind","closeup"] },
  { file: "alexander-mils-JzZWlDBMTs8-unsplash.jpg",         name: "Silicon board",               industry: "Technology",   tags: ["hardware","chip","orange","semiconductors"] },
  { file: "alexander-mils-Y9hZP7wTiLo-unsplash.jpg",         name: "Wind farm · open field",      industry: "Energy",       tags: ["renewable","wind","landscape","golden-hour"] },
  { file: "alexander-mils-xISlt5k2sfs-unsplash.jpg",         name: "Container yard",              industry: "Logistics",    tags: ["shipping","containers","orange","supply-chain"] },
  { file: "alexander-mils-zyu9gwx2kkw-unsplash.jpg",         name: "Samsung storefront · night",  industry: "Retail",       tags: ["brand","electronics","night","storefront"] },
  { file: "american-public-power-association-eIBTh5DXW9w-unsplash.jpg", name: "Wind farm · silhouette",     industry: "Energy",       tags: ["renewable","wind","silhouette","dawn"] },
  { file: "andrej-lisakov-B8CeMG3wxfs-unsplash.jpg",         name: "Turbines · alpine ridge",     industry: "Energy",       tags: ["renewable","wind","mountains","landscape"] },
  { file: "aron-yigin-lNpAmLA_bvQ-unsplash.jpg",             name: "Roofline · ochre",            industry: "Architecture", tags: ["pattern","orange","rooftop","aerial"] },
  { file: "aron-yigin-sNY6B9NsPP8-unsplash.jpg",             name: "Container stack",             industry: "Logistics",    tags: ["containers","orange","stack","port"] },
  { file: "aron-yigin-wGR7OXAMeQE-unsplash.jpg",             name: "Pyramids · sand",             industry: "Architecture", tags: ["vintage","sand","geometric","monument"] },
  { file: "benny-hassum-tcdssHZ_xeM-unsplash.jpg",           name: "Samsung facade · day",        industry: "Retail",       tags: ["brand","electronics","day","storefront"] },
  { file: "benoit-deschasaux-jVolCtV0zYI-unsplash.jpg",      name: "Turbine over crop",           industry: "Energy",       tags: ["renewable","wind","agriculture","landscape"] },
  { file: "cemrecan-yurtman-thdb7o0nLyc-unsplash.jpg",       name: "Skyline · night",             industry: "Architecture", tags: ["urban","night","skyline","city"] },
  { file: "christian-wiediger-I8-T4lMCA6k-unsplash.jpg",     name: "Retail interior · warm",      industry: "Retail",       tags: ["interior","warm","modern","commerce"] },
  { file: "clark-street-mercantile-P3pI6xzovu0-unsplash.jpg",name: "Fashion floor",               industry: "Retail",       tags: ["fashion","interior","apparel"] },
  { file: "clayton-cardinalli-hkJNx0EDbjE-unsplash.jpg",     name: "Port crane · sunset",         industry: "Logistics",    tags: ["port","sunset","infrastructure","supply-chain"] },
  { file: "cphotos-LWwlbpsaIvI-unsplash (1).jpg",            name: "Arch ceiling · steel",        industry: "Architecture", tags: ["interior","geometric","modern","steel"] },
  { file: "cphotos-LWwlbpsaIvI-unsplash.jpg",                name: "Arch ceiling · twin",         industry: "Architecture", tags: ["interior","geometric","modern","steel"] },
  { file: "cphotos-QWK3xmUThYU-unsplash.jpg",                name: "City spires · river",         industry: "Architecture", tags: ["urban","skyline","london","river"] },
  { file: "craig-lovelidge-atsTDZfD2qw-unsplash.jpg",        name: "Mall concourse · crowd",      industry: "Retail",       tags: ["crowd","commerce","interior","mall"] },
  { file: "default-cameraman-EtNO0GXNiIw-unsplash.jpg",      name: "Surveillance lens",           industry: "Technology",   tags: ["security","camera","closeup","monitoring"] },
  { file: "didier-weemaels-ZKVBM2_Dp84-unsplash.jpg",        name: "Warehouse brick",             industry: "Manufacturing",tags: ["vintage","warehouse","brick","facade"] },
  { file: "evgeniy-alyoshin-FXw3zkbqd0w-unsplash.jpg",       name: "Pylon row · dusk",            industry: "Energy",       tags: ["grid","pylon","sunset","transmission"] },
  { file: "freestocks-_3Q3tsJ01nc-unsplash.jpg",             name: "Night portrait · neon",       industry: "Media",        tags: ["people","portrait","night","editorial"] },
  { file: "galina-nelyubova-DRuBA4LCa6w-unsplash.jpg",       name: "Lab apparatus",               industry: "Manufacturing",tags: ["science","lab","equipment","research"] },
  { file: "george-dagerotip-wBFVNdepe1k-unsplash.jpg",       name: "Turbine · blade",             industry: "Energy",       tags: ["renewable","wind","closeup"] },
  { file: "georgi-kalaydzhiev-AiXz7ciJwdE-unsplash.jpg",     name: "Telecom tower",               industry: "Technology",   tags: ["telecom","infrastructure","silhouette","broadcast"] },
  { file: "getty-images--ZachX5bZzc-unsplash.jpg",           name: "Walking figure · minimal",    industry: "Media",        tags: ["people","minimal","portrait","editorial"] },
  { file: "getty-images-6-_2W4bOjbE-unsplash.jpg",           name: "Workstation · screen",        industry: "Workspace",    tags: ["office","computer","desk","work"] },
  { file: "getty-images-UZ-1tePpaJg-unsplash.jpg",           name: "Pipe stack · orange",         industry: "Energy",       tags: ["industrial","pipes","infrastructure","plant"] },
  { file: "getty-images-g_mIxxxP0pM-unsplash.jpg",           name: "Workshop · prototype",        industry: "Manufacturing",tags: ["science","workshop","equipment","prototype"] },
  { file: "getty-images-ubXbjt1k_Ds-unsplash.jpg",           name: "Industrial blue facade",      industry: "Manufacturing",tags: ["facade","blue","industrial","plant"] },
  { file: "getty-images-yyWGGOhJ31A-unsplash.jpg",           name: "Clinic interior · warm",      industry: "Healthcare",   tags: ["interior","warm","clinic","wellness"] },
  { file: "hanson-lu-sq5P00L7lXc-unsplash.jpg",              name: "Aisle row · grocery",         industry: "Retail",       tags: ["grocery","aisle","interior","supermarket"] },
  { file: "hartono-creative-studio-HruFrBJF9ko-unsplash.jpg",name: "Avocado · half",              industry: "Food",         tags: ["fresh","green","produce","macro"] },
  { file: "hyang-imant-70PNshkP0bg-unsplash.jpg",            name: "Greenhouse seedling",         industry: "Food",         tags: ["agriculture","plant","science","green"] },
  { file: "igor-omilaev-AeMzmxBwS70-unsplash.jpg",           name: "Shears · pair",               industry: "Workspace",    tags: ["tools","craft","orange","object"] },
  { file: "igor-omilaev-zqnfqFYaIhE-unsplash.jpg",           name: "Machinery · close",           industry: "Manufacturing",tags: ["machinery","equipment","closeup","industrial"] },
  { file: "insung-yoon-G0jRHvCyPAs-unsplash.jpg",            name: "Street signs · alley",        industry: "Retail",       tags: ["urban","signage","commerce","night"] },
  { file: "jason-mavrommatis-kufsOr1-F-s-unsplash.jpg",      name: "Curtain wall · vertical",     industry: "Architecture", tags: ["facade","modern","commercial","glass"] },
  { file: "jimmy-chang-ACt8ycSzpdE-unsplash.jpg",            name: "Blue facade · curl",          industry: "Architecture", tags: ["facade","blue","modern","abstract"] },
  { file: "kamran-abdullayev-35IWhpOtbrg-unsplash.jpg",      name: "Cinema projector",            industry: "Media",        tags: ["film","projector","vintage","craft"] },
  { file: "kay-D_vWjCGXTjE-unsplash.jpg",                    name: "Turbine · vane",              industry: "Energy",       tags: ["renewable","wind","closeup"] },
  { file: "kenny-leys-wfPCw0s6Jg4-unsplash.jpg",             name: "Wellness studio",             industry: "Healthcare",   tags: ["wellness","fitness","interior","studio"] },
  { file: "kier-in-sight-archives-3Nwt6w-KU3E-unsplash.jpg", name: "Store front · evening",       industry: "Retail",       tags: ["commerce","storefront","evening","urban"] },
  { file: "laura-adai-7Jdaf6S9UJY-unsplash.jpg",             name: "Office block · grid",         industry: "Architecture", tags: ["commercial","modern","grid","facade"] },
  { file: "markus-spiske-70Rir5vB96U-unsplash.jpg",          name: "Code matrix",                 industry: "Technology",   tags: ["code","data","abstract","monochrome"] },
  { file: "markus-spiske-XrIfY_4cK1w-unsplash.jpg",          name: "Plan · technical",            industry: "Architecture", tags: ["blueprint","construction","technical","drawing"] },
  { file: "mohamed-nohassi-FaZb9hOgL5A-unsplash.jpg",        name: "Vintage console · gold",      industry: "Media",        tags: ["vintage","gold","equipment","audio"] },
  { file: "mohamed-nohassi-QexLLbx75zc-unsplash.jpg",        name: "Metal facade · slats",        industry: "Architecture", tags: ["facade","blue","metallic","slats"] },
  { file: "nasa-Q1p7bh3SHj8-unsplash.jpg",                   name: "Port lights · night",         industry: "Logistics",    tags: ["port","night","infrastructure","aerial"] },
  { file: "noah-windler-gQI8BOaL69o-unsplash.jpg",           name: "Cable bundle",                industry: "Manufacturing",tags: ["cables","industrial","closeup","wiring"] },
  { file: "nrd-D6Tu_L3chLE-unsplash.jpg",                    name: "Market produce",              industry: "Food",         tags: ["grocery","produce","fresh","market"] },
  { file: "pauline-figuet-iQiDp1EoSIY-unsplash.jpg",         name: "Gucci facade",                industry: "Retail",       tags: ["luxury","brand","fashion","storefront"] },
  { file: "pierre-chatel-innocenti-Lk-nu_hX6ms-unsplash.jpg",name: "Sculpture · contrast",        industry: "Media",        tags: ["art","sculpture","abstract","red","blue"] },
  { file: "planet-volumes-Ej02tLPQ65k-unsplash.jpg",         name: "Solar farm · row",            industry: "Energy",       tags: ["solar","renewable","panels","aerial"] },
  { file: "planet-volumes-FBBcdlwIDK0-unsplash.jpg",         name: "Solar cell · grid",           industry: "Energy",       tags: ["solar","renewable","abstract","macro"] },
  { file: "planet-volumes-qkhMy14Oh1M-unsplash.jpg",         name: "Solar field · aerial",        industry: "Energy",       tags: ["solar","aerial","renewable","landscape"] },
  { file: "radek-spanninger-LHZ5lxlhl6o-unsplash.jpg",       name: "Spire · vertical",            industry: "Architecture", tags: ["commercial","modern","tower","glass"] },
  { file: "rashid-mamedov-Ta_Og8EFsFo-unsplash.jpg",         name: "Nike facade",                 industry: "Retail",       tags: ["sports","brand","storefront","apparel"] },
  { file: "route4design-Wkmy_s2WMdk-unsplash.jpg",           name: "Spotlight · ambient",         industry: "Manufacturing",tags: ["lighting","industrial","equipment","studio"] },
  { file: "shubham-dhage-M7PWgSlws5Y-unsplash.jpg",          name: "Dark machinery",              industry: "Manufacturing",tags: ["industrial","equipment","monochrome","closeup"] },
  { file: "simon-kadula--gkndM1GvSA-unsplash.jpg",           name: "Hands · keys",                industry: "Workspace",    tags: ["people","laptop","business","work"] },
  { file: "sonika-agarwal-DaQR-GafxYw-unsplash.jpg",         name: "Brief at screen",             industry: "Workspace",    tags: ["people","business","monitor","meeting"] },
  { file: "spencer-demera-Lf4OlBMuP48-unsplash.jpg",         name: "Pylon · tall",                industry: "Energy",       tags: ["grid","pylon","infrastructure","transmission"] },
  { file: "tim-stief-dH6IjhWHNQQ-unsplash.jpg",              name: "Turbine · profile",           industry: "Energy",       tags: ["renewable","wind","closeup"] },
  { file: "toa-heftiba-bYP89K7HX6c-unsplash.jpg",            name: "Curved facade · white",       industry: "Architecture", tags: ["facade","modern","white","curve"] },
  { file: "tom-arran-DzCY1gDGomw-unsplash.jpg",              name: "Turbine · solo",              industry: "Energy",       tags: ["renewable","wind","landscape"] },
  { file: "umit-yildirim-Ass0DusYDk4-unsplash.jpg",          name: "Worker · concrete",           industry: "Manufacturing",tags: ["people","construction","labor","cement"] },
  { file: "victor-2PJMDIgK9EA-unsplash.jpg",                 name: "Modular tower · ochre",       industry: "Architecture", tags: ["commercial","orange","modular","tower"] },
  { file: "willian-justen-de-vasconcellos-RhUOQlbwKAM-unsplash.jpg", name: "LV facade",         industry: "Retail",       tags: ["luxury","brand","fashion","storefront"] },
  { file: "xianjuan-hu-uPYpcsbICI4-unsplash.jpg",            name: "Drafting · plan",             industry: "Architecture", tags: ["blueprint","construction","technical","drawing"] },
  { file: "yousef-hussain-NUyQFZqK4rw-unsplash.jpg",         name: "Tower · monolith",            industry: "Architecture", tags: ["commercial","modern","facade","monolith"] },
];

function photoSlug(name) {
  return ("bdg-imagery-" + name.toLowerCase().replace(/[·]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")) + ".jpg";
}

/* For drafts (uploaded in-browser), URLs come from the draft itself
   (data URL on the thumb, ObjectURL on the original). For shipped photos,
   resolve against the served filesystem. */
function photoThumbUrl(p) {
  if (p.draft) return p.thumbDataUrl;
  return "../assets/photos/thumbs/" + encodeURIComponent(p.file);
}
function photoFullUrl(p) {
  if (p.draft) return p.fullObjectUrl || p.thumbDataUrl;
  return "../assets/photos-src/" + encodeURIComponent(p.file);
}

/* --- Editor store: localStorage overrides + IDB-backed draft blobs ------- */
const STORE_KEYS = {
  overrides: "bdg.photos.overrides.v1",
  drafts:    "bdg.photos.drafts.v1",
  editMode:  "bdg.photos.editMode",
};
const STORE_EVENT = "bdg-photos-changed";

function readJson(key, fallback) {
  try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
  catch { return fallback; }
}
function writeJson(key, val) {
  try { localStorage.setItem(key, JSON.stringify(val)); }
  catch (e) { console.warn("[bdg] localStorage write failed:", key, e); }
  window.dispatchEvent(new Event(STORE_EVENT));
}

/* Tiny IDB wrapper for storing draft originals as blobs (no size limit hassle). */
const IDB = {
  _db: null,
  async _open() {
    if (this._db) return this._db;
    this._db = await new Promise((res, rej) => {
      const r = indexedDB.open("bdg-photos", 1);
      r.onupgradeneeded = () => r.result.createObjectStore("blobs", { keyPath: "id" });
      r.onsuccess = () => res(r.result);
      r.onerror = () => rej(r.error);
    });
    return this._db;
  },
  async put(id, blob) {
    const db = await this._open();
    return new Promise((res, rej) => {
      const tx = db.transaction("blobs", "readwrite");
      tx.objectStore("blobs").put({ id, blob });
      tx.oncomplete = () => res();
      tx.onerror = () => rej(tx.error);
    });
  },
  async get(id) {
    const db = await this._open();
    return new Promise((res, rej) => {
      const tx = db.transaction("blobs", "readonly");
      const req = tx.objectStore("blobs").get(id);
      req.onsuccess = () => res(req.result?.blob);
      req.onerror = () => rej(req.error);
    });
  },
  async del(id) {
    const db = await this._open();
    return new Promise((res, rej) => {
      const tx = db.transaction("blobs", "readwrite");
      tx.objectStore("blobs").delete(id);
      tx.oncomplete = () => res();
      tx.onerror = () => rej(tx.error);
    });
  },
};

/* Apply overrides + add drafts → unified photo list used by the library AND
   the global search corpus. Drafts and shipped photos share a schema; the
   `draft` flag tells render code which URL source to use. */
function mergePhotos(overrides, drafts) {
  const overridden = PHOTOS.map(p => {
    const o = overrides[p.file];
    return o ? { ...p, ...o } : p;
  });
  const draftPhotos = drafts.map(d => ({
    draft: true,
    id: d.id,
    file: d.file || (d.id + ".jpg"),
    name: d.name,
    industry: d.industry,
    description: d.description || "",
    tags: d.tags || [],
    thumbDataUrl: d.thumbDataUrl,
  }));
  return [...draftPhotos, ...overridden];
}

function usePhotoStore() {
  const [overrides, setOverrides] = useState(() => readJson(STORE_KEYS.overrides, {}));
  const [drafts, setDrafts] = useState(() => readJson(STORE_KEYS.drafts, []));
  const objectUrls = useRef(new Map());

  /* Hydrate object URLs for draft originals from IDB on mount. Same-tab and
     cross-tab changes both refresh state via storage / custom events. */
  useEffect(() => {
    let cancelled = false;
    (async () => {
      for (const d of drafts) {
        if (objectUrls.current.has(d.id)) continue;
        try {
          const blob = await IDB.get(d.id);
          if (cancelled) return;
          if (blob) objectUrls.current.set(d.id, URL.createObjectURL(blob));
        } catch (e) { /* draft blob missing — display as thumb-only */ }
      }
      if (!cancelled) setDrafts(ds => [...ds]); // poke a render
    })();
    return () => { cancelled = true; };
  }, [drafts.length]);

  useEffect(() => {
    const refresh = () => {
      setOverrides(readJson(STORE_KEYS.overrides, {}));
      setDrafts(readJson(STORE_KEYS.drafts, []));
    };
    window.addEventListener("storage", refresh);
    window.addEventListener(STORE_EVENT, refresh);
    return () => {
      window.removeEventListener("storage", refresh);
      window.removeEventListener(STORE_EVENT, refresh);
    };
  }, []);

  const setOverride = useCallback((file, patch) => {
    const next = { ...readJson(STORE_KEYS.overrides, {}) };
    const cur = next[file] || {};
    const merged = { ...cur, ...patch };
    /* Drop keys that match the source PHOTO (so reset effectively clears them). */
    const src = PHOTOS.find(p => p.file === file);
    if (src) for (const k of Object.keys(merged)) {
      if (JSON.stringify(merged[k]) === JSON.stringify(src[k])) delete merged[k];
    }
    if (Object.keys(merged).length === 0) delete next[file];
    else next[file] = merged;
    writeJson(STORE_KEYS.overrides, next);
  }, []);

  const resetOverride = useCallback((file) => {
    const next = { ...readJson(STORE_KEYS.overrides, {}) };
    delete next[file];
    writeJson(STORE_KEYS.overrides, next);
  }, []);

  const addDraft = useCallback(async (draft, blob) => {
    if (blob) await IDB.put(draft.id, blob);
    const next = [...readJson(STORE_KEYS.drafts, []), draft];
    writeJson(STORE_KEYS.drafts, next);
  }, []);

  const updateDraft = useCallback((id, patch) => {
    const next = readJson(STORE_KEYS.drafts, []).map(d => d.id === id ? { ...d, ...patch } : d);
    writeJson(STORE_KEYS.drafts, next);
  }, []);

  const removeDraft = useCallback(async (id) => {
    try { await IDB.del(id); } catch {}
    const u = objectUrls.current.get(id);
    if (u) { URL.revokeObjectURL(u); objectUrls.current.delete(id); }
    const next = readJson(STORE_KEYS.drafts, []).filter(d => d.id !== id);
    writeJson(STORE_KEYS.drafts, next);
  }, []);

  /* Patch drafts with their hydrated object URLs at consumption time. */
  const draftsHydrated = useMemo(() => drafts.map(d => ({
    ...d,
    fullObjectUrl: objectUrls.current.get(d.id),
  })), [drafts]);

  const photos = useMemo(() => mergePhotos(overrides, draftsHydrated), [overrides, draftsHydrated]);

  return { photos, overrides, drafts: draftsHydrated, setOverride, resetOverride, addDraft, updateDraft, removeDraft };
}

/* ============== PHOTO EDITOR MODAL ===================================== */

function PhotoEditor({ photo, store, onClose }) {
  const [name, setName] = useState(photo.name || "");
  const [industry, setIndustry] = useState(photo.industry || PHOTO_INDUSTRIES[0]);
  const [tagInput, setTagInput] = useState((photo.tags || []).join(", "));
  const [description, setDescription] = useState(photo.description || "");
  const [deleteConfirm, setDeleteConfirm] = useState(false);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.body.style.overflow = "hidden";
    window.addEventListener("keydown", onKey);
    return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); };
  }, [onClose]);

  const tags = tagInput.split(",").map(t => t.trim().toLowerCase()).filter(Boolean);

  const save = () => {
    const patch = { name: name.trim(), industry, tags, description: description.trim() };
    if (photo.draft) store.updateDraft(photo.id, patch);
    else store.setOverride(photo.file, patch);
    onClose();
  };

  const reset = () => {
    if (photo.draft) return;
    store.resetOverride(photo.file);
    onClose();
  };

  const remove = async () => {
    if (!photo.draft) return;
    await store.removeDraft(photo.id);
    onClose();
  };

  return (
    <div className="ed-shade" onClick={onClose} role="dialog" aria-modal="true" aria-label={`Edit ${photo.name}`}>
      <div className="ed-panel" onClick={e => e.stopPropagation()}>
        <header className="ed-head">
          <div className="ed-title">
            <span className="ed-eyebrow">Edit photo</span>
            <h3>{photo.name}</h3>
          </div>
          <button className="ed-close" onClick={onClose} aria-label="Close">×</button>
        </header>

        <div className="ed-body">
          <div className="ed-preview">
            <img src={photoThumbUrl(photo)} alt={photo.name} />
            <div className="ed-file">{photo.draft ? `${photo.id}.jpg (draft)` : photo.file}</div>
          </div>

          <div className="ed-fields">
            <label className="ed-field">
              <span className="ed-label">Name</span>
              <input
                type="text"
                value={name}
                onChange={e => setName(e.target.value)}
                placeholder="e.g. Wind farm · open field"
                autoFocus
              />
            </label>

            <label className="ed-field">
              <span className="ed-label">Industry</span>
              <select value={industry} onChange={e => setIndustry(e.target.value)}>
                {PHOTO_INDUSTRIES.map(k => (<option key={k} value={k}>{k}</option>))}
              </select>
            </label>

            <label className="ed-field">
              <span className="ed-label">Tags <span className="ed-hint">(comma-separated)</span></span>
              <input
                type="text"
                value={tagInput}
                onChange={e => setTagInput(e.target.value)}
                placeholder="renewable, wind, landscape"
              />
              <div className="ed-tag-preview">
                {tags.length === 0 ? <em>no tags</em> : tags.map(t => (<span key={t} className="lib-tag">{t}</span>))}
              </div>
            </label>

            <label className="ed-field">
              <span className="ed-label">Description <span className="ed-hint">(shown in lightbox)</span></span>
              <textarea
                rows={3}
                value={description}
                onChange={e => setDescription(e.target.value)}
                placeholder="One sentence on what this image is and when to use it."
              />
            </label>
          </div>
        </div>

        <footer className="ed-foot">
          {photo.draft ? (
            <button
              type="button"
              className="ed-del"
              data-confirm={deleteConfirm}
              onClick={() => deleteConfirm ? remove() : setDeleteConfirm(true)}
            >{deleteConfirm ? "Confirm delete" : "× Delete draft"}</button>
          ) : (
            <button
              type="button"
              className="ed-reset"
              onClick={reset}
              disabled={!store.overrides[photo.file]}
              title="Discard edits and restore the original manifest values"
            >↺ Reset to original</button>
          )}
          <span className="ed-foot-spacer"/>
          <button type="button" className="ed-cancel" onClick={onClose}>Cancel</button>
          <button type="button" className="ed-save" onClick={save}>Save</button>
        </footer>
      </div>
    </div>
  );
}

/* ============== UPLOAD BUTTON ========================================= */

function UploadButton({ onUpload }) {
  const inputRef = useRef(null);
  const [busy, setBusy] = useState(false);

  const handleFiles = async (files) => {
    setBusy(true);
    try {
      const drafts = [];
      for (const file of files) {
        if (!file.type.startsWith("image/")) continue;
        const id = "draft-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
        const thumbDataUrl = await fileToThumb(file, 400);
        const baseName = file.name.replace(/\.[a-z0-9]+$/i, "").replace(/[-_]+/g, " ").trim();
        drafts.push({
          draft: {
            id,
            file: file.name,
            name: baseName.slice(0, 60),
            industry: "Abstract",
            tags: [],
            description: "",
            thumbDataUrl,
            createdAt: new Date().toISOString(),
          },
          blob: file,
        });
      }
      if (drafts.length) await onUpload(drafts);
    } catch (e) {
      console.warn("[bdg] upload failed:", e);
      alert("Upload failed: " + e.message);
    } finally {
      setBusy(false);
      if (inputRef.current) inputRef.current.value = "";
    }
  };

  return (
    <>
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        multiple
        style={{display: "none"}}
        onChange={(e) => handleFiles(Array.from(e.target.files || []))}
      />
      <button
        type="button"
        className="lib-upload-btn"
        onClick={() => inputRef.current?.click()}
        disabled={busy}
        title="Upload one or more images"
      >{busy ? "· Uploading…" : "⬆ Upload"}</button>
    </>
  );
}

async function fileToThumb(file, size = 400) {
  const url = URL.createObjectURL(file);
  try {
    const img = await new Promise((res, rej) => {
      const im = new Image();
      im.onload = () => res(im);
      im.onerror = rej;
      im.src = url;
    });
    const ar = img.width / img.height;
    const w = ar >= 1 ? size : Math.round(size * ar);
    const h = ar >= 1 ? Math.round(size / ar) : size;
    const canvas = document.createElement("canvas");
    canvas.width = w; canvas.height = h;
    const ctx = canvas.getContext("2d");
    ctx.imageSmoothingQuality = "high";
    ctx.drawImage(img, 0, 0, w, h);
    return canvas.toDataURL("image/jpeg", 0.78);
  } finally {
    URL.revokeObjectURL(url);
  }
}

/* ============== EXPORT MANIFEST MODAL ================================= */

function ExportManifest({ photos, overrides, drafts, onClose }) {
  const [copied, setCopied] = useState(false);

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

  const code = useMemo(() => emitManifest(photos), [photos]);
  const hasOverrides = Object.keys(overrides).length > 0;
  const hasDrafts = drafts.length > 0;

  const copy = async () => {
    try { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1800); }
    catch { alert("Couldn't copy. Select the text and copy manually."); }
  };

  return (
    <div className="ed-shade" onClick={onClose} role="dialog" aria-modal="true" aria-label="Export manifest">
      <div className="ed-panel ed-export" onClick={e => e.stopPropagation()}>
        <header className="ed-head">
          <div className="ed-title">
            <span className="ed-eyebrow">Export updated manifest</span>
            <h3>{Object.keys(overrides).length} edit(s) · {drafts.length} draft(s)</h3>
          </div>
          <button className="ed-close" onClick={onClose} aria-label="Close">×</button>
        </header>

        <div className="ed-export-body">
          <p className="ed-export-help">
            Copy this block and paste it over the <code>const PHOTOS = […]</code> array in <code>site/app.jsx</code>.
            {hasDrafts && <> Drafts get a <code>file:</code> path you'll need to wire up — see notes below.</>}
          </p>
          {hasDrafts && (
            <div className="ed-export-drafts">
              <h4>Drafts — manual step required:</h4>
              <ol>
                {drafts.map(d => (
                  <li key={d.id}>
                    <code>{d.file}</code> → save as <code>bdg photos/{photoSlug(d.name).replace(/^bdg-imagery-/, '')}</code>
                    <button
                      type="button"
                      className="ed-mini-dl"
                      onClick={async () => {
                        try {
                          const blob = await IDB.get(d.id);
                          if (blob) downloadBlob(photoSlug(d.name).replace(/^bdg-imagery-/, ''), blob);
                        } catch {}
                      }}
                    >⤓ original</button>
                  </li>
                ))}
              </ol>
              <p className="ed-export-help">
                After moving the originals, regenerate thumbnails with: <code>sips -Z 400 -s formatOptions 78 *.jpg --out bdg\ Design\ System/assets/photos/thumbs/</code>
              </p>
            </div>
          )}
          <textarea
            className="ed-export-code"
            readOnly
            value={code}
            onClick={e => e.target.select()}
            rows={14}
          />
        </div>

        <footer className="ed-foot">
          {!hasOverrides && !hasDrafts && <span className="ed-export-empty">No edits or drafts yet — the manifest matches the source.</span>}
          <span className="ed-foot-spacer"/>
          <button type="button" className="ed-cancel" onClick={onClose}>Close</button>
          <button type="button" className="ed-save" onClick={copy} data-copied={copied}>{copied ? "✓ Copied" : "⎘ Copy manifest"}</button>
        </footer>
      </div>
    </div>
  );
}

function emitManifest(photos) {
  const lines = photos.map(p => {
    const file = p.draft ? photoSlug(p.name).replace(/^bdg-imagery-/, '') : p.file;
    const tagStr = JSON.stringify(p.tags || []);
    const desc = p.description ? `, description: ${JSON.stringify(p.description)}` : "";
    return `  { file: ${JSON.stringify(file)}, name: ${JSON.stringify(p.name)}, industry: ${JSON.stringify(p.industry)}, tags: ${tagStr}${desc} },`;
  });
  return "const PHOTOS = [\n" + lines.join("\n") + "\n];";
}

/* --------------------------- 08 MOTION ----------------------------------- */

function MotionChapter({ onCopy }) {
  return (
    <section id="motion" className="chapter" data-screen-label="08 Motion">
      <ChapterHead num="08" kicker="Principles" title={<>Motion.</>} meta={[["Curve","bdg-curve"], ["Durations","04 tokens"], ["Reduced motion","Hard fallback"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="08.A">Principle</SubHead>
          <p className="lede">Motion <span className="neq">reveals</span> structure — never decorates. Easing is decisive, durations are short, every motion respects reduced-motion completely.</p>
          <p className="body" style={{marginTop: 18}}>If a motion does not show the system reading itself, it is removed. We do not bounce, we do not parallax, we do not animate on entry for the sake of animating.</p>
        </div>
        <div className="right">
          <EasingCurve onCopy={onCopy} />
        </div>
      </div>

      <DurationTokens />
      <MotionDemos />
      <ReducedMotion />
    </section>
  );
}

/* 08.A — Easing curve */
function EasingCurve({ onCopy }) {
  const [run, setRun] = useState(false);
  const easing = "cubic-bezier(0.22, 1, 0.36, 1)";
  const trigger = () => { setRun(false); setTimeout(() => setRun(true), 30); };
  return (
    <div className="curve-block">
      <div className="pane">
        <svg viewBox="0 0 100 100" aria-hidden="true">
          <line x1="0"  y1="100" x2="100" y2="100" stroke="var(--line)" strokeWidth="0.6"/>
          <line x1="0"  y1="0"   x2="0"   y2="100" stroke="var(--line)" strokeWidth="0.6"/>
          <line x1="0"  y1="0"   x2="100" y2="0"   stroke="var(--line)" strokeDasharray="2,2" strokeWidth="0.4"/>
          <line x1="100" y1="0"  x2="100" y2="100" stroke="var(--line)" strokeDasharray="2,2" strokeWidth="0.4"/>
          {/* control point arrows */}
          <line x1="0" y1="100" x2="22" y2="0"   stroke="var(--signal)" strokeWidth="0.4" strokeDasharray="2,2"/>
          <line x1="100" y1="0" x2="64" y2="0"   stroke="var(--signal)" strokeWidth="0.4" strokeDasharray="2,2"/>
          {/* the curve */}
          <path d="M 0 100 C 22 0, 64 0, 100 0" fill="none" stroke="var(--ink)" strokeWidth="1.4"/>
          {/* control point dots */}
          <circle cx="22" cy="0" r="1.6" fill="var(--signal)"/>
          <circle cx="64" cy="0" r="1.6" fill="var(--signal)"/>
          <circle cx="0"  cy="100" r="1.6" fill="var(--ink)"/>
          <circle cx="100" cy="0" r="1.6" fill="var(--ink)"/>
        </svg>
      </div>
      <div className="pane">
        <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing: "0.18em", textTransform: "uppercase", color:"var(--ink-2)"}}>08.A · bdg-curve</div>
        <h3 className="block-title">Decisive — settles cleanly.</h3>
        <p className="body">Front-loaded ease, then a long graceful landing. The curve carries the conclusion all the way down without ever bouncing back.</p>
        <code onClick={() => onCopy && onCopy(easing, "bdg-curve · " + easing)} title="Copy">{easing}</code>
        <div className={"demo-track" + (run ? " run" : "")}>
          <div className="ball"/>
        </div>
        <div className="kvs">
          <div><span className="k">x1, y1</span><span className="v">0.22, 1.00</span></div>
          <div><span className="k">x2, y2</span><span className="v">0.36, 1.00</span></div>
          <div><span className="k">Equivalent</span><span className="v">ease-out · long</span></div>
          <div><span className="k">Type</span><span className="v">Single-stage</span></div>
        </div>
        <button onClick={trigger} style={{padding:"10px 14px", border:"0.8px solid var(--ink)", color:"var(--ink)", background:"transparent", fontFamily:"var(--f-mono)", fontSize: 11, letterSpacing:"0.16em", textTransform:"uppercase", cursor:"pointer", alignSelf:"flex-start", marginTop: 8}}>↻ Replay</button>
      </div>
    </div>
  );
}

/* 08.B — Duration tokens */
function DurationTokens() {
  const [playing, setPlaying] = useState(null);
  const tokens = [
    { name: "Tick",   ms: 120,  use: "Hover · focus" },
    { name: "Beat",   ms: 220,  use: "Toggle · reveal" },
    { name: "Phrase", ms: 420,  use: "Card · sheet entry" },
    { name: "Page",   ms: 720,  use: "Section transition" },
  ];
  const play = (name) => { setPlaying(null); setTimeout(() => setPlaying(name), 30); };
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="08.B">Duration tokens</SubHead>
      <h2 className="section-title">Four durations, all under a second.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>Anything over 720ms feels like the system is thinking about itself. Anything under 120ms is invisible. Click a card to preview.</p>
      <div className="dur-grid">
        {tokens.map(t => (
          <button key={t.name} className={"dur" + (playing === t.name ? " play" : "")} style={{"--ms": t.ms + "ms", cursor:"pointer", textAlign:"left", background:"transparent"}} onClick={() => play(t.name)}>
            <div className="name">{t.name}</div>
            <div className="ms">{t.ms} <small>ms</small></div>
            <div className="use">{t.use}</div>
            <div className="bar"/>
          </button>
        ))}
      </div>
    </div>
  );
}

/* 08.C — 08.L · Approved motions */
function MotionDemos() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="08.C — 08.L">Approved motions</SubHead>
      <h2 className="section-title">Ten patterns are the system. <br/>Everything else is decoration.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Each card runs the real animation in the browser. Press <strong>Replay</strong> to see it again. All ten share the bdg-curve and respect <code style={{fontFamily:"var(--f-mono)", fontSize: 13}}>prefers-reduced-motion</code>.</p>
      <div className="motion-demos">
        <NumberReveal />
        <ChartUnveil />
        <HairlineDraw />
        <StaggerList />
        <MaskWipe />
        <NetworkPulse />
        <ProgressFill />
        <DeltaTick />
        <RowEnter />
        <HighlightSweep />
      </div>
    </div>
  );
}

function NumberReveal() {
  const target = 17;
  const [val, setVal] = useState(target);
  const play = () => {
    setVal(0);
    const start = performance.now();
    const dur = 720;
    const tick = (t) => {
      const e = Math.min((t - start) / dur, 1);
      // cubic-bezier approximated by ease-out cubic
      const k = 1 - Math.pow(1 - e, 3);
      setVal(Math.round(k * target));
      if (e < 1) requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
  };
  return (
    <div className="motion-demo">
      <span className="ix">08.C · Progressive number reveal</span>
      <h4>Numbers count into place.</h4>
      <div className="stage">
        <div className="count">{val}<span className="unit">%</span></div>
      </div>
      <div className="ctl">
        <span className="desc">Duration · 720ms · bdg-curve</span>
        <button onClick={play}>↻ Replay</button>
      </div>
    </div>
  );
}

function ChartUnveil() {
  const [tick, setTick] = useState(0);
  const data = [12, 18, 14, 22, 17, 28, 24, 32, 29, 38, 36, 44];
  const w = 320, h = 140, pad = 12;
  const max = Math.max(...data);
  const stepX = (w - pad * 2) / (data.length - 1);
  const pts = data.map((d, i) => [pad + i * stepX, h - pad - (d / max) * (h - pad * 2)]);
  // length-based reveal: clip path width animates
  const play = () => { setTick(t => t + 1); };
  return (
    <div className="motion-demo">
      <span className="ix">08.D · Scroll / trigger chart unveil</span>
      <h4>The chart draws itself, line by line.</h4>
      <div className="stage">
        <svg key={tick} viewBox={`0 0 ${w} ${h}`} className="unveil-svg" preserveAspectRatio="xMidYMid meet">
          {/* grid */}
          {[0.25, 0.5, 0.75].map(g => (
            <line key={g} x1={pad} x2={w - pad} y1={h - pad - g*(h - pad*2)} y2={h - pad - g*(h - pad*2)} className="grid"/>
          ))}
          <path
            className="signal"
            d={"M " + pts.map(p => p.join(" ")).join(" L ")}
            style={{
              strokeDasharray: 600,
              strokeDashoffset: 600,
              animation: "draw 1100ms cubic-bezier(0.22,1,0.36,1) forwards",
            }}
          />
          <style>{`@keyframes draw { to { stroke-dashoffset: 0; } }`}</style>
          {pts.map((p, i) => (
            <circle key={i} cx={p[0]} cy={p[1]} r={i === pts.length - 1 ? 3 : 1.4} fill={i === pts.length - 1 ? "var(--signal)" : "var(--ink)"} opacity={0} style={{animation: `fade 200ms ${300 + i * 60}ms cubic-bezier(0.22,1,0.36,1) forwards`}}/>
          ))}
          <style>{`@keyframes fade { to { opacity: 1; } }`}</style>
        </svg>
      </div>
      <div className="ctl">
        <span className="desc">Stroke + dot reveal · 1100ms</span>
        <button onClick={play}>↻ Replay</button>
      </div>
    </div>
  );
}

/* 08.E — Hairline draw: rules sketch across the row */
function HairlineDraw() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const rows = [
    { lbl: "Diagnose", val: "Owner · inputs",       d: 0 },
    { lbl: "Design",   val: "Cadence · threshold",  d: 220 },
    { lbl: "Deploy",   val: "Run · re-open",        d: 440 },
  ];
  return (
    <div className="motion-demo">
      <span className="ix">08.E · Hairline draw</span>
      <h4>Lines sketch the structure.</h4>
      <div className="stage" style={{flexDirection:"column", alignItems:"stretch", gap: 0, padding: "20px 24px"}}>
        {rows.map((r, i) => (
          <div key={tick + "-" + i} style={{position:"relative", padding:"14px 0", display:"grid", gridTemplateColumns:"100px 1fr auto", gap: 12, alignItems:"baseline"}}>
            <span style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 14, letterSpacing:"-0.005em"}}>{r.lbl}</span>
            <span style={{fontFamily:"var(--f-body)", fontSize: 13, color:"var(--ink-2)"}}>{r.val}</span>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", color:"var(--ink-3)"}}>0{i+1}</span>
            <span style={{
              position:"absolute", left: 0, right: 0, bottom: 0, height: 0.8,
              background: "var(--ink)",
              transformOrigin: "left center",
              transform: "scaleX(0)",
              animation: `hairline-sketch 640ms cubic-bezier(0.22,1,0.36,1) ${r.d}ms forwards`,
            }}/>
          </div>
        ))}
      </div>
      <style>{`@keyframes hairline-sketch { to { transform: scaleX(1); } }`}</style>
      <div className="ctl"><span className="desc">Hairlines · staggered · 640ms</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.F — Stagger list: rows enter one after another */
function StaggerList() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const items = [
    "Prediction ≠ Understanding",
    "Optimization ≠ Alignment",
    "Speed ≠ Clarity",
    "Reporting ≠ Decision",
  ];
  return (
    <div className="motion-demo">
      <span className="ix">08.F · Stagger reveal</span>
      <h4>Rows arrive in order.</h4>
      <div className="stage" style={{flexDirection:"column", alignItems:"stretch", gap: 4, padding:"22px 26px"}}>
        {items.map((it, i) => (
          <div key={tick + "-" + i} style={{
            fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase",
            fontSize: 18, lineHeight: 1.15, letterSpacing:"-0.005em",
            color: "var(--ink)",
            padding: "8px 0",
            borderBottom: "0.8px solid var(--line)",
            opacity: 0,
            transform: "translateY(8px)",
            animation: `stagger-in 420ms cubic-bezier(0.22,1,0.36,1) ${i * 110}ms forwards`,
          }}>{it}</div>
        ))}
      </div>
      <style>{`@keyframes stagger-in { to { opacity: 1; transform: translateY(0); } }`}</style>
      <div className="ctl"><span className="desc">110ms stagger · bdg-curve</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.G — Mask wipe: ink block slides off a word, leaving it behind */
function MaskWipe() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  return (
    <div className="motion-demo">
      <span className="ix">08.G · Mask wipe</span>
      <h4>Block slides off, the word lands.</h4>
      <div className="stage" style={{justifyContent:"flex-start", padding: "26px 30px"}}>
        <div key={tick} style={{position:"relative", display:"inline-block", fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 56, lineHeight: 0.95, letterSpacing:"-0.02em", color:"var(--ink)"}}>
          <span>Honest&nbsp;data.</span>
          <span style={{
            position:"absolute", inset: 0,
            background:"var(--ink)",
            transformOrigin:"left center",
            transform:"scaleX(1)",
            animation:"mask-off 800ms cubic-bezier(0.22,1,0.36,1) 200ms forwards",
          }}/>
        </div>
      </div>
      <style>{`@keyframes mask-off { to { transform: scaleX(0); transform-origin: right center; } }`}</style>
      <div className="ctl"><span className="desc">800ms wipe · ink → text</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.H — Network pulse: nodes brighten one by one */
function NetworkPulse() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const nodes = [
    {x: 50,  y: 30,  d: 0},
    {x: 120, y: 18,  d: 180},
    {x: 200, y: 36,  d: 360},
    {x: 60,  y: 80,  d: 540},
    {x: 140, y: 92,  d: 720},
    {x: 220, y: 84,  d: 900},
    {x: 170, y: 60,  d: 1080},
  ];
  const edges = [[0,1],[1,2],[1,6],[0,3],[3,4],[4,5],[5,2],[3,6],[4,6],[6,2]];
  return (
    <div className="motion-demo">
      <span className="ix">08.H · Network pulse</span>
      <h4>Signal moves through the graph.</h4>
      <div className="stage">
        <svg key={tick} viewBox="0 0 280 120" style={{width:"100%", maxHeight: 160}}>
          {edges.map(([a, b], i) => (
            <line key={i} x1={nodes[a].x} y1={nodes[a].y} x2={nodes[b].x} y2={nodes[b].y} stroke="var(--ink-2)" strokeWidth="0.6" opacity="0.4"/>
          ))}
          {nodes.map((n, i) => (
            <g key={i}>
              <circle cx={n.x} cy={n.y} r="3" fill="var(--ink)" opacity="0.35"/>
              <circle cx={n.x} cy={n.y} r="3" fill="var(--c-yellow)" style={{
                transformOrigin: `${n.x}px ${n.y}px`,
                transform: "scale(0)",
                animation: `pulse 900ms cubic-bezier(0.22,1,0.36,1) ${n.d}ms forwards`,
              }}/>
            </g>
          ))}
          <style>{`@keyframes pulse { 0% { transform: scale(0); } 40% { transform: scale(2.2); opacity: 1; } 100% { transform: scale(1); opacity: 0.6; } }`}</style>
        </svg>
      </div>
      <div className="ctl"><span className="desc">180ms cascade · 7 nodes</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.I — Progress fill: load bar resolves */
function ProgressFill() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const rows = [
    { lbl: "Honest data",      pct: 0.87, d: 0 },
    { lbl: "Better decisions", pct: 0.71, d: 160 },
    { lbl: "Real impact",      pct: 0.62, d: 320 },
  ];
  return (
    <div className="motion-demo">
      <span className="ix">08.I · Progress fill</span>
      <h4>Bars resolve to their value.</h4>
      <div className="stage" style={{flexDirection:"column", alignItems:"stretch", gap: 14, padding:"22px 26px"}}>
        {rows.map((r, i) => (
          <div key={tick + "-" + i}>
            <div style={{display:"flex", justifyContent:"space-between", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", color:"var(--ink-2)", marginBottom: 6}}>
              <span style={{color:"var(--ink)"}}>{r.lbl}</span>
              <span style={{fontVariantNumeric:"tabular-nums"}}>{Math.round(r.pct * 100)}%</span>
            </div>
            <div style={{position:"relative", height: 10, border:"0.8px solid var(--line)", background:"var(--paper)"}}>
              <div style={{
                position:"absolute", inset: 0,
                background: i === 0 ? "var(--c-blue)" : i === 1 ? "var(--c-green)" : "var(--c-yellow)",
                transformOrigin:"left center",
                transform:"scaleX(0)",
                animation: `progress-${i} 880ms cubic-bezier(0.22,1,0.36,1) ${r.d}ms forwards`,
              }}/>
              <style>{`@keyframes progress-${i} { to { transform: scaleX(${r.pct}); } }`}</style>
            </div>
          </div>
        ))}
      </div>
      <div className="ctl"><span className="desc">Bar fill · 880ms · easeout</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.J — Delta tick: chip shifts up/down then settles */
function DeltaTick() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const cells = [
    { lbl: "Re-open latency", val: "−38%", dir: "down", color: "var(--c-green)" },
    { lbl: "Owner alignment", val: "+14",  dir: "up",   color: "var(--c-blue)" },
    { lbl: "Decisions / Q",   val: "×3",   dir: "up",   color: "var(--c-yellow)" },
  ];
  return (
    <div className="motion-demo">
      <span className="ix">08.J · Delta tick</span>
      <h4>Change chip nudges into place.</h4>
      <div className="stage" style={{gap: 14, justifyContent:"space-around", flexWrap:"wrap"}}>
        {cells.map((c, i) => (
          <div key={tick + "-" + i} style={{textAlign:"center", display:"flex", flexDirection:"column", gap: 8, opacity: 0, animation: `delta-pop 520ms cubic-bezier(0.22,1,0.36,1) ${i * 140}ms forwards`}}>
            <div style={{
              fontFamily:"var(--f-headline)", fontWeight: 650,
              fontSize: 40, lineHeight: 1.0, letterSpacing:"-0.02em",
              padding:"4px 10px",
              background: c.color, color: c.color === "var(--c-yellow)" || c.color === "var(--c-green)" ? "#0A0A0C" : "#FFFFFF",
              fontVariantNumeric:"tabular-nums",
              display:"inline-block",
            }}>{c.val}</div>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", color:"var(--ink-2)"}}>{c.lbl}</span>
          </div>
        ))}
      </div>
      <style>{`@keyframes delta-pop { 0% { opacity: 0; transform: translateY(8px); } 60% { opacity: 1; transform: translateY(-2px); } 100% { opacity: 1; transform: translateY(0); } }`}</style>
      <div className="ctl"><span className="desc">140ms stagger · 520ms ease</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.K — Row enter: a tabular row slides up into the list */
function RowEnter() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  const rows = [
    { k: "Q1 · Forecast",  v: "+18%", t: "Locked",  d: 0 },
    { k: "Q2 · Cadence",   v: "Wkly", t: "Diagnose", d: 140 },
    { k: "Q3 · Threshold", v: "0.62", t: "Owned",   d: 280 },
    { k: "Q4 · Outcome",   v: "+38%", t: "Deploy",  d: 420 },
  ];
  return (
    <div className="motion-demo">
      <span className="ix">08.K · Row enter</span>
      <h4>Tabular rows arrive top-down.</h4>
      <div className="stage" style={{padding: 0, alignItems:"stretch"}}>
        <div style={{width:"100%", borderTop:"0.8px solid var(--line)"}}>
          {rows.map((r, i) => (
            <div key={tick + "-" + i} style={{
              display:"grid", gridTemplateColumns:"1fr 80px 100px",
              padding:"12px 18px",
              borderBottom:"0.8px solid var(--line)",
              alignItems:"baseline",
              fontFamily:"var(--f-body)", fontSize: 13,
              opacity: 0,
              transform: "translateY(10px)",
              animation: `row-up 460ms cubic-bezier(0.22,1,0.36,1) ${r.d}ms forwards`,
            }}>
              <span style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 13, letterSpacing:"-0.005em"}}>{r.k}</span>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 13, fontVariantNumeric:"tabular-nums", textAlign:"right"}}>{r.v}</span>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", color:"var(--ink-2)", textAlign:"right"}}>{r.t}</span>
            </div>
          ))}
        </div>
      </div>
      <style>{`@keyframes row-up { to { opacity: 1; transform: translateY(0); } }`}</style>
      <div className="ctl"><span className="desc">Row slide · 140ms stagger</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.L — Highlight sweep: yellow block scans over a word */
function HighlightSweep() {
  const [tick, setTick] = useState(0);
  const play = () => setTick(t => t + 1);
  return (
    <div className="motion-demo">
      <span className="ix">08.L · Highlight sweep</span>
      <h4>Color sweeps over the verb.</h4>
      <div className="stage" style={{justifyContent:"flex-start", padding:"26px 30px", flexWrap:"wrap", gap: 8}}>
        <div key={tick} style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 44, lineHeight: 0.95, letterSpacing:"-0.02em", display:"inline-flex", flexWrap:"wrap", gap:"0.2em", color:"var(--ink)"}}>
          <span>The decision is the</span>
          <span style={{position:"relative", display:"inline-block", padding:"0.04em 0.18em 0.10em", color:"transparent", animation: "hl-text 0ms 600ms forwards"}}>
            unit.
            <span style={{position:"absolute", inset: 0, background:"var(--c-yellow)", transformOrigin:"left center", transform:"scaleX(0)", animation:"hl-sweep 600ms cubic-bezier(0.22,1,0.36,1) forwards"}}/>
          </span>
        </div>
      </div>
      <style>{`
        @keyframes hl-sweep { to { transform: scaleX(1); } }
        @keyframes hl-text { to { color: #0A0A0C; } }
      `}</style>
      <div className="ctl"><span className="desc">Sweep + text reveal · 600ms</span><button onClick={play}>↻ Replay</button></div>
    </div>
  );
}

/* 08.M — Reduced motion */
function ReducedMotion() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="08.M">Reduced motion</SubHead>
      <h2 className="section-title">The system without motion still reads.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>
        Every animation in this site is a polish layer. The page is functional, navigable, and complete without any motion at all. <code style={{fontFamily:"var(--f-mono)", fontSize: 13}}>prefers-reduced-motion: reduce</code> kills animations and transitions globally.
      </p>
      <div className="reduced">
        <div className="with">
          <span className="tag">Default — motion on</span>
          <h4>Numbers count in. Lines draw. Cards settle.</h4>
          <p>The motion communicates the order of arrival of information: cause, then tension, then implication.</p>
          <code>transition: all 220ms cubic-bezier(0.22,1,0.36,1);</code>
        </div>
        <div className="without">
          <span className="tag">Reduce — motion off</span>
          <h4>The page lands in its final state.</h4>
          <p>No fades, no count-ups, no drawn lines. Everything is present immediately. Order is communicated by layout, not by timing.</p>
          <code>animation-duration: 0.001ms !important;</code>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- 09 COMPONENTS ------------------------------- */

function CompBlock({ ix, name, desc, anatomy, children }) {
  return (
    <div className="comp-block">
      <div className="meta">
        <span className="ix">{ix}</span>
        <h4>{name}</h4>
        <p>{desc}</p>
        {anatomy && (
          <div className="anatomy">
            {anatomy.map((row, i) => (
              <div key={i}>
                <div className="k">{row[0]}</div>
                <div className="v">{row[1]}</div>
              </div>
            ))}
          </div>
        )}
      </div>
      {children}
    </div>
  );
}

function ComponentsChapter() {
  return (
    <section id="components" className="chapter" data-screen-label="09 UI Components">
      <ChapterHead num="09" kicker="Kit" title={<>UI <br/>Components.</>} meta={[["Families","12"], ["States","Default · Hover · Focus · Active · Disabled"], ["Modes","Light + Dark"]]}/>

      <div className="opener" style={{marginBottom: 32}}>
        <div>
          <SubHead ix="09 — overview">A working kit, not a screenshot reel.</SubHead>
          <p className="lede">Every component below is a <span className="neq">real</span>, functional React component reading from the same tokens. Light and dark are shown together — both modes are first-class.</p>
          <p className="body" style={{marginTop: 18}}>Sharp corners. Hairline borders. Hover-inverts on solid actions. No shadows. No glass. No motion that isn't reveal.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            One <span className="accent">vocabulary</span>, every surface. <br/>The widgets are the system, made tangible.
          </div>
        </div>
      </div>

      <CompButtons />
      <CompInputs />
      <CompSelection />
      <CompToggles />
      <CompTags />
      <CompTooltips />
      <CompTabsFilters />
      <CompCardsBadges />
      <CompAlerts />
      <CompCrumbsPager />
      <CompModalDemo />
      <CompTable />
    </section>
  );
}

/* 09.A — Buttons */
function CompButtons() {
  return (
    <CompBlock
      ix="09.A"
      name="Buttons"
      desc="Three variants: solid (primary action), outline (default), ghost (chrome). Sizes track the type ladder."
      anatomy={[["Family","Instr. Sans Cond"], ["Weight","650"], ["Radius","0"], ["Border","0.8px"]]}
    >
      {[{cls:"", lbl:"Light"}, {cls:"dark", lbl:"Dark"}].map(s => (
        <div key={s.lbl} className={"stage " + s.cls} style={{borderTop: s.lbl === "Dark" ? "0.8px solid var(--line)" : 0}}>
          <div className="stage-lbl">{s.lbl} · variants &amp; sizes</div>
          <div className="stage-row">
            <button className="bdg-btn solid">Open chapter</button>
            <button className="bdg-btn">Outline</button>
            <button className="bdg-btn ghost">Ghost</button>
            <button className="bdg-btn signal">Signal · accent</button>
          </div>
          <div className="stage-row">
            <button className="bdg-btn solid sm">Small</button>
            <button className="bdg-btn solid">Default</button>
            <button className="bdg-btn solid lg">Large</button>
            <button className="bdg-btn"><span className="ico">↘</span> With icon</button>
            <button className="bdg-btn" disabled>Disabled</button>
          </div>
        </div>
      ))}
    </CompBlock>
  );
}

/* 09.B — Inputs */
function CompInputs() {
  return (
    <CompBlock
      ix="09.B"
      name="Form inputs"
      desc="Text, search, prefix / suffix, URL, textarea, select. Same border, same focus state, same ladder."
      anatomy={[["Padding","11 / 14"], ["Border","0.8px"], ["Focus","Signal border"], ["Invalid","Red border"]]}
    >
      {[{cls:"", lbl:"Light"}, {cls:"dark", lbl:"Dark"}].map(s => (
        <div key={s.lbl} className={"stage " + s.cls} style={{borderTop: s.lbl === "Dark" ? "0.8px solid var(--line)" : 0}}>
          <div className="stage-lbl">{s.lbl}</div>
          <div className="stage-row" style={{alignItems:"flex-start", gap: 16}}>
            <label className="bdg-field">
              <span className="lbl">Label · default</span>
              <input className="bdg-input" placeholder="Type a phrase…"/>
              <span className="hint">Helper text sits below.</span>
            </label>
            <label className="bdg-field">
              <span className="lbl">Search · with suffix</span>
              <span className="bdg-input-wrap">
                <input placeholder="Search the system —"/>
                <span className="suff btn">⌘K</span>
              </span>
            </label>
          </div>
          <div className="stage-row" style={{alignItems:"flex-start", gap: 16}}>
            <label className="bdg-field">
              <span className="lbl">URL · with prefix</span>
              <span className="bdg-input-wrap">
                <span className="pref">https://</span>
                <input placeholder="bdg.io / decisions"/>
              </span>
            </label>
            <label className="bdg-field">
              <span className="lbl">Select</span>
              <select className="bdg-select" defaultValue="board">
                <option value="board">Board review</option>
                <option value="cfo">CFO briefing</option>
                <option value="op">Operating cadence</option>
              </select>
            </label>
          </div>
          <div className="stage-row" style={{alignItems:"flex-start", gap: 16}}>
            <label className="bdg-field" style={{maxWidth: 480}}>
              <span className="lbl">Textarea</span>
              <textarea className="bdg-textarea" rows={3} placeholder="Paste the sentence you want flagged."/>
            </label>
            <label className="bdg-field">
              <span className="lbl">Invalid state</span>
              <input className="bdg-input invalid" defaultValue="cutting-edge"/>
              <span className="err">Weakening word — see word ledger.</span>
            </label>
          </div>
        </div>
      ))}
    </CompBlock>
  );
}

/* 09.C — Selection */
function CompSelection() {
  const [chk, setChk] = useState({a: true, b: false});
  const [chk2, setChk2] = useState({a: true, b: false});
  const [rad, setRad] = useState("diagnose");
  const [rad2, setRad2] = useState("diagnose");
  const [card, setCard] = useState("design");
  return (
    <CompBlock
      ix="09.C"
      name="Selection — check, radio, card-style"
      desc="Simple checkboxes and radios for dense forms; card-style versions when the option deserves a sentence of explanation."
      anatomy={[["Box","18 × 18px"], ["Border","0.8px"], ["Check fill","Currentcolor"], ["Card active","Signal-soft"]]}
    >
      {[
        { cls:"", lbl:"Light", state:{chk, setChk: setChk, rad, setRad} },
        { cls:"dark", lbl:"Dark", state:{chk: chk2, setChk: setChk2, rad: rad2, setRad: setRad2} }
      ].map((s, idx) => (
        <div key={s.lbl} className={"stage " + s.cls} style={{borderTop: idx === 1 ? "0.8px solid var(--line)" : 0}}>
          <div className="stage-lbl">{s.lbl} · simple</div>
          <div className="stage-row" style={{gap: 22}}>
            <label className={"bdg-check" + (s.state.chk.a ? " on" : "")} onClick={() => s.state.setChk(p => ({...p, a: !p.a}))}>
              <span className="box"/>
              <span>Honest data</span>
            </label>
            <label className={"bdg-check" + (s.state.chk.b ? " on" : "")} onClick={() => s.state.setChk(p => ({...p, b: !p.b}))}>
              <span className="box"/>
              <span>Better decisions</span>
            </label>
            <label className={"bdg-radio" + (s.state.rad === "diagnose" ? " on" : "")} onClick={() => s.state.setRad("diagnose")}>
              <span className="box"/><span>Diagnose</span>
            </label>
            <label className={"bdg-radio" + (s.state.rad === "design" ? " on" : "")} onClick={() => s.state.setRad("design")}>
              <span className="box"/><span>Design</span>
            </label>
            <label className={"bdg-radio" + (s.state.rad === "deploy" ? " on" : "")} onClick={() => s.state.setRad("deploy")}>
              <span className="box"/><span>Deploy</span>
            </label>
          </div>
          <div className="stage-lbl">{s.lbl} · card-style</div>
          <div className="stage-row" style={{alignItems:"stretch", gap: 12}}>
            {[
              {id:"diagnose", t:"Diagnose", d:"Name the structure that's producing the result."},
              {id:"design",   t:"Design",   d:"Re-architect the owner, inputs, cadence, threshold."},
              {id:"deploy",   t:"Deploy",   d:"Ship the change. Run the new cadence. Measure outside the room."},
            ].map(o => (
              <div key={o.id} role="radio" aria-checked={card === o.id} className="bdg-card-opt radio" style={{flex:1, minWidth: 200}} onClick={() => setCard(o.id)}>
                <span className="ctrl-box"/>
                <div><div className="ttl">{o.t}</div><div className="desc">{o.d}</div></div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </CompBlock>
  );
}

/* 09.D — Toggles */
function CompToggles() {
  const [t1, setT1] = useState(true);
  const [t2, setT2] = useState(false);
  const [t3, setT3] = useState(true);
  const [t4, setT4] = useState(false);
  return (
    <CompBlock
      ix="09.D"
      name="Toggle"
      desc="Binary state. Use when the off / on is symmetric — feature flags, dark mode, motion. For destructive actions use a button + confirmation."
      anatomy={[["Track","40 × 22"], ["Knob","16 × 16"], ["Easing","bdg-curve"], ["Active","Signal fill"]]}
    >
      <div className="stage">
        <div className="stage-lbl">Light</div>
        <div className="stage-row">
          <label className="bdg-check" style={{cursor:"pointer"}} onClick={() => setT1(!t1)}>
            <span className={"bdg-toggle" + (t1 ? " on" : "")}/>
            <span>Honest data — on</span>
          </label>
          <label className="bdg-check" style={{cursor:"pointer"}} onClick={() => setT2(!t2)}>
            <span className={"bdg-toggle" + (t2 ? " on" : "")}/>
            <span>Reduced motion</span>
          </label>
        </div>
      </div>
      <div className="stage dark" style={{borderTop: "0.8px solid var(--line)"}}>
        <div className="stage-lbl">Dark</div>
        <div className="stage-row">
          <label className="bdg-check" style={{cursor:"pointer"}} onClick={() => setT3(!t3)}>
            <span className={"bdg-toggle" + (t3 ? " on" : "")}/>
            <span>Yellow signal — on</span>
          </label>
          <label className="bdg-check" style={{cursor:"pointer"}} onClick={() => setT4(!t4)}>
            <span className={"bdg-toggle" + (t4 ? " on" : "")}/>
            <span>Auto-publish</span>
          </label>
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.E — Tags / chips */
function CompTags() {
  const [tags, setTags] = useState(["Decision", "Honest data", "Better"]);
  const [tags2, setTags2] = useState(["Cadence", "Owner"]);
  return (
    <CompBlock
      ix="09.E"
      name="Tags &amp; chips"
      desc="Inline metadata, filter selections, removable selections. Mono type, sharp corners, signal fill for active."
      anatomy={[["Family","DM Mono"], ["Tracking","0.14em"], ["Padding","5 / 10"], ["Close ×","Optional"]]}
    >
      <div className="stage">
        <div className="stage-lbl">Light</div>
        <div className="stage-row dense">
          <span className="bdg-tag">Strategy</span>
          <span className="bdg-tag">Method</span>
          <span className="bdg-tag solid">Active</span>
          {tags.map((t, i) => (
            <span key={i} className="bdg-tag">{t}<span className="x" onClick={() => setTags(tags.filter((_, j) => j !== i))}>×</span></span>
          ))}
        </div>
      </div>
      <div className="stage dark" style={{borderTop: "0.8px solid var(--line)"}}>
        <div className="stage-lbl">Dark</div>
        <div className="stage-row dense">
          <span className="bdg-tag">Cadence</span>
          <span className="bdg-tag solid">Selected</span>
          {tags2.map((t, i) => (
            <span key={i} className="bdg-tag">{t}<span className="x" onClick={() => setTags2(tags2.filter((_, j) => j !== i))}>×</span></span>
          ))}
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.F — Tooltips */
function CompTooltips() {
  return (
    <CompBlock
      ix="09.F"
      name="Tooltips"
      desc="Small reveal on hover or focus. Two variants — simple label and titled with optional link. Tooltips never carry critical information."
      anatomy={[["Background","Ink → Paper"], ["Family","DM Mono / Body"], ["Trigger","Hover · focus"], ["Position","Above"]]}
    >
      <div className="stage">
        <div className="stage-lbl">Light · hover the buttons</div>
        <div className="stage-row">
          <span className="bdg-tt-wrap">
            <button className="bdg-btn">Open chapter</button>
            <span className="bdg-tt">Navigate to chapter</span>
          </span>
          <span className="bdg-tt-wrap">
            <button className="bdg-btn solid">Copy hex</button>
            <span className="bdg-tt titled">
              <span className="tt-h">Click-to-copy</span>
              <span>Copies the hex code AND the CSS variable name to your clipboard. <a href="#color">See chapter 04</a>.</span>
            </span>
          </span>
        </div>
      </div>
      <div className="stage dark" style={{borderTop: "0.8px solid var(--line)"}}>
        <div className="stage-lbl">Dark</div>
        <div className="stage-row">
          <span className="bdg-tt-wrap">
            <button className="bdg-btn">Hover me</button>
            <span className="bdg-tt">Tooltip · dark</span>
          </span>
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.G — Tabs, filters, slider arrows */
function CompTabsFilters() {
  const [tab, setTab] = useState("month");
  const [tab2, setTab2] = useState("month");
  const [filter, setFilter] = useState(new Set(["pos"]));
  const toggleF = (k) => {
    const n = new Set(filter); n.has(k) ? n.delete(k) : n.add(k); setFilter(n);
  };
  const [page, setPage] = useState(2);
  return (
    <CompBlock
      ix="09.G"
      name="Tabs · filters · arrows"
      desc="Segmented switchers, multi-select filter rows, and paired slider arrows. Active state inverts the surface."
      anatomy={[["Tabs","Border + invert"], ["Filters","Count chip"], ["Arrows","36 × 36 cell"], ["Family","Cond + Mono"]]}
    >
      <div className="stage">
        <div className="stage-lbl">Light · tabs (segmented)</div>
        <div className="stage-row">
          <div className="bdg-tabs">
            {["month","quarter","year"].map(k => (
              <button key={k} className="tab" aria-selected={tab === k} onClick={() => setTab(k)}>{k}</button>
            ))}
          </div>
        </div>
        <div className="stage-lbl">Light · filters (multi-select)</div>
        <div className="stage-row">
          <div className="bdg-filters">
            <button className="bdg-filter" aria-pressed={filter.size === 0} onClick={() => setFilter(new Set())}>View all <span className="ct">{filter.size === 0 ? "·" : filter.size}</span></button>
            {[
              {k:"pos", l:"Positioning", n: 4},
              {k:"sal", l:"Sales", n: 12},
              {k:"soc", l:"Social", n: 8},
              {k:"edi", l:"Editorial", n: 6},
            ].map(f => (
              <button key={f.k} className="bdg-filter" aria-pressed={filter.has(f.k)} onClick={() => toggleF(f.k)}>{f.l} <span className="ct">{f.n}</span></button>
            ))}
          </div>
        </div>
        <div className="stage-lbl">Light · slider arrows · pagination</div>
        <div className="stage-row">
          <div className="bdg-slider-arrows">
            <button>←</button>
            <span className="count">{String(page).padStart(2,"0")} / 12</span>
            <button>→</button>
          </div>
          <div className="bdg-pager">
            <button className="arrow">←</button>
            {[1,2,3,4].map(n => (
              <button key={n} aria-current={page === n} onClick={() => setPage(n)}>{n}</button>
            ))}
            <button>…</button>
            <button onClick={() => setPage(12)}>12</button>
            <button className="arrow">→</button>
          </div>
        </div>
      </div>
      <div className="stage dark" style={{borderTop: "0.8px solid var(--line)"}}>
        <div className="stage-lbl">Dark</div>
        <div className="stage-row">
          <div className="bdg-tabs">
            {["month","quarter","year"].map(k => (
              <button key={k} className="tab" aria-selected={tab2 === k} onClick={() => setTab2(k)}>{k}</button>
            ))}
          </div>
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.H — Cards, badges */
function CompCardsBadges() {
  return (
    <CompBlock
      ix="09.H"
      name="Card · badge · divider"
      desc="The base container, the state marker, and the rule. Cards hold one thought; badges name a status; dividers cut without decorating."
      anatomy={[["Card border","0.8px"], ["Padding","18 / 20"], ["Badge","Outline + dot"], ["Divider","0.8px"]]}
    >
      <div className="stage">
        <div className="stage-lbl">Light</div>
        <div className="stage-row" style={{alignItems:"stretch"}}>
          <div className="bdg-card-demo">
            <span className="ix">02 · Voice</span>
            <h5>The room's voice.</h5>
            <p>Five attributes — clear, confident, human, intellectual, provocative-measured.</p>
            <div className="foot"><span>5 attributes</span><span>↗</span></div>
          </div>
          <div className="bdg-card-demo">
            <span className="ix">04 · Color</span>
            <h5>50 → 950 ladder.</h5>
            <p>Seven hues at eleven stops. Every cell click-to-copy.</p>
            <div className="foot"><span>77 tokens</span><span>↗</span></div>
          </div>
        </div>
        <div className="stage-row dense">
          <span className="bdg-badge live"><span className="dot"/> Live</span>
          <span className="bdg-badge beta"><span className="dot"/> Beta</span>
          <span className="bdg-badge warn"><span className="dot"/> Deprecated</span>
        </div>
        <hr style={{border:0, borderTop: "0.8px solid var(--ink)", margin:"8px 0", width: "100%"}}/>
        <span className="stage-lbl" style={{marginTop: -12}}>Divider · 0.8px solid hairline · the only rule in the system</span>
      </div>
    </CompBlock>
  );
}

/* 09.I — Alerts */
function CompAlerts() {
  return (
    <CompBlock
      ix="09.I"
      name="Alerts"
      desc="Inline notices. Two intents — informational (default ink) and warning (red). Always dismissible; never auto-popping."
      anatomy={[["Border","0.8px"], ["Padding","16 / 18"], ["Icon","14 × 14 box"], ["Auto-hide","Never"]]}
    >
      <div className="stage">
        <div className="bdg-alert info">
          <span className="ic"/>
          <div className="body-c">
            <span className="ttl">Color tokens updated.</span>
            <p>Purple ramp was re-tuned for AA contrast on dark surfaces. See chapter 04.C for the new stops.</p>
          </div>
          <button className="x">×</button>
        </div>
        <div className="bdg-alert warn">
          <span className="ic"/>
          <div className="body-c">
            <span className="ttl">Weakening words in copy.</span>
            <p>The phrase "cutting-edge" appears 3 times across the marketing site. See guardrails for the substitution table.</p>
          </div>
          <button className="x">×</button>
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.J — Breadcrumbs + pagination */
function CompCrumbsPager() {
  return (
    <CompBlock
      ix="09.J"
      name="Breadcrumbs"
      desc="Mono trail at the top of long pages. Light, optional, structural. Pairs with the page-foot pager."
      anatomy={[["Family","DM Mono"], ["Tracking","0.16em"], ["Active","Opacity 1.0"], ["Inactive","0.6"]]}
    >
      <div className="stage">
        <div className="bdg-crumbs">
          <a>bdg</a><span className="sep">/</span>
          <a>Brand System</a><span className="sep">/</span>
          <a>Chapter 09</a><span className="sep">/</span>
          <span className="cur">UI Components</span>
        </div>
      </div>
    </CompBlock>
  );
}

/* 09.K — Modal */
function CompModalDemo() {
  const [open, setOpen] = useState(false);
  return (
    <CompBlock
      ix="09.K"
      name="Modal"
      desc="A blocking dialog for a single decision. Headline, body, two actions — primary on the right, ghost on the left. No close-on-overlay-click without a confirm path."
      anatomy={[["Width","520 max"], ["Padding","22 / 26"], ["Overlay","55% ink"], ["Close","X + Esc"]]}
    >
      <div className="stage">
        <div className="stage-row">
          <button className="bdg-btn solid" onClick={() => setOpen(true)}>Open modal</button>
        </div>
        {open && (
          <div className="bdg-modal-overlay" onClick={() => setOpen(false)}>
            <div className="bdg-modal" onClick={e => e.stopPropagation()}>
              <div className="m-hed">
                <h5>Re-open the forecast?</h5>
                <button className="x" onClick={() => setOpen(false)}>×</button>
              </div>
              <div className="m-body">
                <p>Re-opening the forecast resets the evidence threshold and notifies the room. This is a structural change — it is not undoable from the dashboard.</p>
              </div>
              <div className="m-foot">
                <button className="bdg-btn ghost" onClick={() => setOpen(false)}>Cancel</button>
                <button className="bdg-btn solid" onClick={() => setOpen(false)}>Re-open the forecast</button>
              </div>
            </div>
          </div>
        )}
      </div>
    </CompBlock>
  );
}

/* 09.L — Table */
function CompTable() {
  const rows = [
    { k: "Honest data",      v: 0.87, d: +0.04 },
    { k: "Better decisions", v: 0.71, d: +0.02 },
    { k: "Real impact",      v: 0.62, d: -0.03 },
    { k: "Decision cadence", v: 0.94, d: +0.06 },
    { k: "Evidence threshold", v: 0.55, d: 0.00 },
  ];
  return (
    <CompBlock
      ix="09.L"
      name="Table"
      desc="Tabular data. Mono headers, body type in cells, numeric columns tabular-aligned and right-set. Zebra strips at 50% bone."
      anatomy={[["Header","DM Mono · 10px · 0.18em"], ["Row","13px body"], ["Numeric","Tabular · right"], ["Border","0.8px"]]}
    >
      <div className="stage">
        <table className="bdg-table">
          <thead>
            <tr>
              <th style={{width: "40%"}}>Promise</th>
              <th className="num">Coverage</th>
              <th className="num">Δ vs Q1</th>
              <th>Owner</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => (
              <tr key={r.k}>
                <td>{r.k}</td>
                <td className="num">{(r.v * 100).toFixed(1)}%</td>
                <td className={"num delta " + (r.d > 0 ? "up" : r.d < 0 ? "down" : "")}>{r.d > 0 ? "+" : ""}{(r.d * 100).toFixed(1)}%</td>
                <td><span className="bdg-tag">Studio</span></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </CompBlock>
  );
}

/* --------------------------- 10 DATA VIZ --------------------------------- */

function DataVizChapter() {
  return (
    <section id="dataviz" className="chapter" data-screen-label="10 Data viz">
      <ChapterHead num="10" kicker="Kit" title={<>Data <br/>Visualisation.</>} meta={[["Chart types","07"], ["Color","Analytical"], ["Grid","Minimal"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="10.A">Principle</SubHead>
          <p className="lede">Charts must <span className="neq">be honest</span>. Minimal grids. Large numeric anchors. Color is meaning — never mood.</p>
          <p className="body" style={{marginTop: 18}}>The page is the chart's first reader. If the chart only makes sense with a sentence of explanation, it's the wrong chart. If color is decorative, it's removed.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            A chart is a <span className="accent">claim.</span> <br/>It must survive being read at distance.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="10.B">Chart taxonomy</SubHead>
        <h2 className="section-title">Seven shapes. <br/>Each one earns a different question.</h2>
        <div className="viz-grid">
          <ChartLine />
          <ChartBar />
          <ChartArea />
          <ChartScatter />
          <ChartNetwork />
          <ChartSankey />
          <ChartDistribution />
        </div>
      </div>

      <Annotation />
      <AntiPatterns />
    </section>
  );
}

const Q = ["Q1","Q2","Q3","Q4"];

function ChartLine() {
  const data = [44, 47, 52, 51, 56, 61, 64, 71];
  const w = 280, h = 120, pad = 10;
  const max = Math.max(...data), min = Math.min(...data) - 6;
  const stepX = (w - pad * 2) / (data.length - 1);
  const pts = data.map((d, i) => [pad + i * stepX, h - pad - ((d - min) / (max - min)) * (h - pad * 2)]);
  return (
    <div className="viz">
      <span className="ix">10.B.01 · Line</span>
      <h4>Trend over time</h4>
      <span className="anchor">71<span className="unit">%</span></span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} className="grid"/>
        <line x1={pad} x2={w - pad} y1={pad} y2={pad} className="grid"/>
        <line x1={pad} x2={w - pad} y1={(h)/2} y2={(h)/2} className="grid"/>
        <path d={"M " + pts.map(p => p.join(" ")).join(" L ")} fill="none" stroke="var(--ink)" strokeWidth="1.4"/>
        {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r={i === pts.length - 1 ? 3 : 1.4} fill={i === pts.length - 1 ? "var(--signal)" : "var(--ink)"}/>)}
        <text x={pts[pts.length-1][0] - 6} y={pts[pts.length-1][1] - 10} textAnchor="end" className="axis-lbl" fill="var(--signal)">+27 pts</text>
      </svg>
      <div className="meta"><span>FY · 8 periods</span><span>↗ +27 pts</span></div>
    </div>
  );
}

function ChartBar() {
  const data = [28, 42, 31, 56];
  const w = 280, h = 120, pad = 10;
  const max = Math.max(...data);
  const bw = (w - pad * 2) / data.length - 8;
  return (
    <div className="viz">
      <span className="ix">10.B.02 · Bar</span>
      <h4>Discrete categories</h4>
      <span className="anchor">+56%</span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} className="grid"/>
        {data.map((d, i) => {
          const bh = (d / max) * (h - pad * 2);
          const x = pad + 4 + i * ((w - pad * 2) / data.length);
          const isMax = d === max;
          return (
            <g key={i}>
              <rect x={x} y={h - pad - bh} width={bw} height={bh} fill={isMax ? "var(--signal)" : "var(--ink)"}/>
              <text x={x + bw / 2} y={h - 2} textAnchor="middle" className="axis-lbl">{Q[i]}</text>
            </g>
          );
        })}
      </svg>
      <div className="meta"><span>Cycle time, weeks</span><span>Q4 leads</span></div>
    </div>
  );
}

function ChartArea() {
  const a = [16, 22, 19, 26, 24, 32, 30, 38];
  const b = [12, 14, 17, 18, 22, 23, 28, 32];
  const w = 280, h = 120, pad = 10;
  const max = Math.max(...a) + 6;
  const stepX = (w - pad * 2) / (a.length - 1);
  const ap = a.map((d, i) => [pad + i * stepX, h - pad - (d / max) * (h - pad * 2)]);
  const bp = b.map((d, i) => [pad + i * stepX, h - pad - (d / max) * (h - pad * 2)]);
  const pathA = "M " + ap.map(p => p.join(" ")).join(" L ") + ` L ${ap[ap.length-1][0]} ${h - pad} L ${ap[0][0]} ${h - pad} Z`;
  const pathB = "M " + bp.map(p => p.join(" ")).join(" L ") + ` L ${bp[bp.length-1][0]} ${h - pad} L ${bp[0][0]} ${h - pad} Z`;
  return (
    <div className="viz">
      <span className="ix">10.B.03 · Area</span>
      <h4>Stacked accumulation</h4>
      <span className="anchor">2.1<span className="unit">×</span></span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} className="grid"/>
        <path d={pathA} fill="var(--c-blue)" fillOpacity="0.18" stroke="var(--c-blue)" strokeWidth="1"/>
        <path d={pathB} fill="var(--c-green)" fillOpacity="0.22" stroke="var(--c-green)" strokeWidth="1"/>
      </svg>
      <div className="meta"><span>Decisions per cadence</span><span>Diagnose · Design</span></div>
    </div>
  );
}

function ChartScatter() {
  const pts = [
    [16, 22], [20, 28], [24, 26], [32, 30], [40, 44],
    [48, 38], [56, 56], [60, 50], [70, 62], [80, 68], [88, 78],
  ];
  const w = 280, h = 120, pad = 12;
  return (
    <div className="viz">
      <span className="ix">10.B.04 · Scatter</span>
      <h4>Correlation</h4>
      <span className="anchor">r=0.82</span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} className="grid"/>
        <line x1={pad} x2={pad} y1={pad} y2={h - pad} className="grid"/>
        {/* trend */}
        <line x1={pad + 8} y1={h - pad - 12} x2={w - pad - 8} y2={pad + 14} stroke="var(--signal)" strokeWidth="0.8" strokeDasharray="2,2"/>
        {pts.map((p, i) => (
          <circle key={i} cx={pad + (p[0] / 100) * (w - pad * 2)} cy={h - pad - (p[1] / 100) * (h - pad * 2)} r="2.4" fill="var(--ink)"/>
        ))}
      </svg>
      <div className="meta"><span>Cadence vs alignment</span><span>n = 11</span></div>
    </div>
  );
}

function ChartNetwork() {
  // small static network
  const nodes = [
    {x:50, y:30, hot:true,  lbl:"A"},
    {x:130,y:18, hot:false, lbl:"B"},
    {x:200,y:34, hot:true,  lbl:"C"},
    {x:60, y:80, hot:false, lbl:"D"},
    {x:130,y:90, hot:false, lbl:"E"},
    {x:210,y:84, hot:false, lbl:"F"},
    {x:170,y:60, hot:true,  lbl:"G"},
  ];
  const edges = [[0,1],[1,2],[1,6],[0,3],[3,4],[4,5],[5,2],[3,6],[4,6],[6,2]];
  const w = 280, h = 120;
  return (
    <div className="viz">
      <span className="ix">10.B.05 · Network</span>
      <h4>Relationships</h4>
      <span className="anchor">7 nodes</span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        {edges.map(([a, b], i) => (
          <line key={i} x1={nodes[a].x} y1={nodes[a].y} x2={nodes[b].x} y2={nodes[b].y} stroke="var(--ink-2)" strokeWidth="0.6" opacity="0.5"/>
        ))}
        {nodes.map((n, i) => (
          <g key={i}>
            <circle cx={n.x} cy={n.y} r={n.hot ? 5 : 3.5} fill={n.hot ? "var(--signal)" : "var(--ink)"}/>
            <text x={n.x + 8} y={n.y + 3} className="axis-lbl">{n.lbl}</text>
          </g>
        ))}
      </svg>
      <div className="meta"><span>Decision dependency</span><span>3 active</span></div>
    </div>
  );
}

function ChartSankey() {
  // 3 sources, 2 destinations
  const w = 280, h = 120;
  return (
    <div className="viz">
      <span className="ix">10.B.06 · Sankey</span>
      <h4>Flow</h4>
      <span className="anchor">94%</span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        {/* source bars */}
        <rect x="6" y="20" width="6" height="22" fill="var(--c-blue)"/>
        <rect x="6" y="50" width="6" height="30" fill="var(--c-green)"/>
        <rect x="6" y="90" width="6" height="14" fill="var(--c-orange)"/>
        {/* destination bars */}
        <rect x={w-12} y="22" width="6" height="46" fill="var(--ink)"/>
        <rect x={w-12} y="76" width="6" height="20" fill="var(--ink)"/>
        {/* flows */}
        <path d={`M 12 31 C 80 31, ${w-100} 45, ${w-12} 45`} fill="none" stroke="var(--c-blue)" strokeWidth="20" strokeOpacity="0.25"/>
        <path d={`M 12 65 C 80 65, ${w-100} 50, ${w-12} 50`} fill="none" stroke="var(--c-green)" strokeWidth="26" strokeOpacity="0.28"/>
        <path d={`M 12 97 C 80 97, ${w-100} 86, ${w-12} 86`} fill="none" stroke="var(--c-orange)" strokeWidth="12" strokeOpacity="0.28"/>
      </svg>
      <div className="meta"><span>Inputs · decisions</span><span>3 · 2</span></div>
    </div>
  );
}

function ChartDistribution() {
  const bins = [4, 9, 16, 28, 42, 38, 24, 12, 6, 3];
  const w = 280, h = 120, pad = 10;
  const max = Math.max(...bins);
  const bw = (w - pad * 2) / bins.length - 2;
  return (
    <div className="viz">
      <span className="ix">10.B.07 · Distribution</span>
      <h4>Histogram</h4>
      <span className="anchor">σ 1.4</span>
      <svg viewBox={`0 0 ${w} ${h}`}>
        <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} className="grid"/>
        {bins.map((d, i) => {
          const bh = (d / max) * (h - pad * 2);
          const x = pad + i * ((w - pad * 2) / bins.length);
          const isPeak = d === max;
          return (
            <rect key={i} x={x} y={h - pad - bh} width={bw} height={bh} fill={isPeak ? "var(--signal)" : "var(--ink)"} opacity={isPeak ? 1 : 0.78}/>
          );
        })}
      </svg>
      <div className="meta"><span>Decision latency</span><span>median 3d</span></div>
    </div>
  );
}

/* 10.C — Annotation */
function Annotation() {
  const data = [24, 28, 34, 31, 38, 44, 47, 52, 56, 58, 61, 64];
  const w = 720, h = 280, pad = 28;
  const max = 70, min = 18;
  const stepX = (w - pad * 2) / (data.length - 1);
  const pts = data.map((d, i) => [pad + i * stepX, h - pad - ((d - min) / (max - min)) * (h - pad * 2)]);
  // annotations: at index 5 (event) and index 10 (peak)
  const eventIdx = 5;
  const peakIdx = 10;
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="10.C">Annotation patterns</SubHead>
      <h2 className="section-title">Layer the human reading over the computational structure.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The chart is the system reading. The annotations are the meeting talking. They live in mono type, on the same plane, never on top.</p>
      <div className="annot-block">
        <div className="stage">
          <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet">
            {/* grid */}
            {[0.25, 0.5, 0.75].map(g => (
              <line key={g} x1={pad} x2={w - pad} y1={h - pad - g*(h - pad*2)} y2={h - pad - g*(h - pad*2)} stroke="var(--line)" strokeWidth="0.6"/>
            ))}
            <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} stroke="var(--ink)" strokeWidth="0.8"/>
            {/* annotation lines */}
            <line x1={pts[eventIdx][0]} x2={pts[eventIdx][0]} y1={pad} y2={h - pad} stroke="var(--signal)" strokeDasharray="3,3" strokeWidth="0.8"/>
            <text x={pts[eventIdx][0] + 6} y={pad + 14} fontFamily="DM Mono" fontSize="10" letterSpacing="1.4" fill="var(--signal)">EVENT · CADENCE CHANGED</text>
            {/* peak callout */}
            <line x1={pts[peakIdx][0]} y1={pts[peakIdx][1] - 6} x2={pts[peakIdx][0] + 60} y2={pts[peakIdx][1] - 36} stroke="var(--ink)" strokeWidth="0.8"/>
            <circle cx={pts[peakIdx][0] + 60} cy={pts[peakIdx][1] - 36} r="3" fill="var(--ink)"/>
            <text x={pts[peakIdx][0] + 66} y={pts[peakIdx][1] - 30} fontFamily="DM Mono" fontSize="10" letterSpacing="1.4" fill="var(--ink)">PEAK · 61%</text>
            <text x={pts[peakIdx][0] + 66} y={pts[peakIdx][1] - 16} fontFamily="DM Mono" fontSize="9" letterSpacing="1.2" fill="var(--ink-2)">Owner confirmed</text>
            {/* line */}
            <path d={"M " + pts.map(p => p.join(" ")).join(" L ")} fill="none" stroke="var(--ink)" strokeWidth="1.4"/>
            {pts.map((p, i) => (
              <circle key={i} cx={p[0]} cy={p[1]} r={i === eventIdx || i === peakIdx ? 3.5 : 1.6} fill={i === eventIdx ? "var(--signal)" : i === peakIdx ? "var(--ink)" : "var(--ink)"}/>
            ))}
            {/* axes */}
            <text x={pad} y={h - 10} fontFamily="DM Mono" fontSize="9" letterSpacing="1.2" fill="var(--ink-2)">JAN</text>
            <text x={w - pad - 18} y={h - 10} fontFamily="DM Mono" fontSize="9" letterSpacing="1.2" fill="var(--ink-2)">DEC</text>
          </svg>
        </div>
        <div className="notes">
          <div className="note">
            <span className="pin">01</span>
            <div className="nx">Event marker
              <small>Vertical signal-dashed line + caps mono label. Marks a structural change in the underlying process.</small>
            </div>
          </div>
          <div className="note">
            <span className="pin">02</span>
            <div className="nx">Point callout
              <small>Lead line + secondary text. Carries the headline number and a one-line gloss — never a paragraph.</small>
            </div>
          </div>
          <div className="note">
            <span className="pin">03</span>
            <div className="nx">Axis labels
              <small>Mono caps, low contrast. Present, not loud. Drop the gridlines before you raise the label weight.</small>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* 10.D — Anti-patterns */
function AntiPatterns() {
  const data = [22, 28, 34, 31, 38, 44, 47, 52, 56, 58, 61, 64];
  const w = 280, h = 140, pad = 14;
  const max = 70, min = 18;
  const stepX = (w - pad * 2) / (data.length - 1);
  const pts = data.map((d, i) => [pad + i * stepX, h - pad - ((d - min) / (max - min)) * (h - pad * 2)]);
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="10.D">Anti-patterns</SubHead>
      <h2 className="section-title">The chart that earns the page <br/>vs the chart that decorates it.</h2>
      <div className="anti-grid">
        <div className="anti-card good">
          <div className="stage">
            <svg viewBox={`0 0 ${w} ${h}`} style={{width: "100%", maxHeight: 160}}>
              <line x1={pad} x2={w - pad} y1={h - pad} y2={h - pad} stroke="var(--ink-2)" strokeWidth="0.6"/>
              <path d={"M " + pts.map(p => p.join(" ")).join(" L ")} fill="none" stroke="var(--ink)" strokeWidth="1.4"/>
              {pts.map((p, i) => i === pts.length - 1 ? <circle key={i} cx={p[0]} cy={p[1]} r="3" fill="var(--signal)"/> : null)}
              <text x={pad} y={pad - 2} fontFamily="DM Mono" fontSize="9" letterSpacing="1.4" fill="var(--ink-2)">DECISION CADENCE · WEEKLY</text>
            </svg>
          </div>
          <div className="label">
            <span className="tag">DO · Honest chart</span>
            <h4>One line. One claim. Tabular.</h4>
            <p>The chart's job is to make the claim visible at distance. The reader reaches a verdict without leaving the page.</p>
          </div>
        </div>
        <div className="anti-card bad">
          <div className="stage">
            <svg viewBox={`0 0 ${w} ${h}`} style={{width: "100%", maxHeight: 160}}>
              <defs>
                <linearGradient id="badgrad" x1="0" y1="0" x2="1" y2="1">
                  <stop offset="0" stopColor="#3236FF"/>
                  <stop offset="0.5" stopColor="#F55910"/>
                  <stop offset="1" stopColor="#00D190"/>
                </linearGradient>
              </defs>
              <path d={"M " + pts.map(p => p.join(" ")).join(" L ")} fill="none" stroke="url(#badgrad)" strokeWidth="3"/>
              {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r="3.4" fill="#fff" stroke="var(--c-orange)" strokeWidth="1.5"/>)}
              {/* drop shadow flair */}
              <text x={pad} y={pad + 4} fontFamily="DM Mono" fontSize="13" letterSpacing="1.6" fill="#fff" fontWeight="600">📊 GROWTH! 📈</text>
            </svg>
          </div>
          <div className="label">
            <span className="tag">DON'T · Decorative dashboard</span>
            <h4>Gradients. Glow. Emoji. Drop shadow.</h4>
            <p>The chart is now an illustration of itself. The claim is lost behind ornament; the reader stops at "looks important" without reaching "is true."</p>
          </div>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- 11 LAYOUT & GRID ---------------------------- */

function LayoutChapter({ onCopy }) {
  return (
    <section id="layout" className="chapter" data-screen-label="11 Layout & Grid">
      <ChapterHead num="11" kicker="Tokens" title={<>Layout &amp; <br/>Grid.</>} meta={[["Spacing","10 stops"], ["Grid","12-col / 4-col"], ["Gutters","24 · 36 · 60"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="11.A">Position</SubHead>
          <p className="lede">A 12-column desktop grid. Editorial asymmetry. Content hugs the <span className="neq">left</span>, whitespace lives on the right.</p>
          <p className="body" style={{marginTop: 18}}>The layout is a column, not a card system. Cards exist; they serve the column. Pages read top-down, with hairlines as the cadence and numeric anchors as the chapter beats.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            One column of <span className="accent">evidence.</span> <br/>Not a wall of cards.
          </div>
        </div>
      </div>

      <SpacingScale onCopy={onCopy}/>
      <GridSystem />
      <EditorialPatterns />
    </section>
  );
}

/* 11.B — Spacing scale */
function SpacingScale({ onCopy }) {
  const stops = [
    { name: "xs",  px: 4,   use: "Hairline gap · tight inline" },
    { name: "sm",  px: 8,   use: "Baseline rhythm · between inline-flex items" },
    { name: "md",  px: 12,  use: "Padding inside a chip · between meta rows" },
    { name: "lg",  px: 16,  use: "Card padding · between block elements" },
    { name: "xl",  px: 24,  use: "Between subsections" },
    { name: "xxl", px: 32,  use: "Section padding · column gutter" },
    { name: "3xl", px: 48,  use: "Between content groups" },
    { name: "4xl", px: 64,  use: "Vertical rhythm between full subsections" },
    { name: "5xl", px: 96,  use: "Between chapter beats" },
    { name: "6xl", px: 128, use: "Page top / bottom rhythm" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="11.B">Spacing scale</SubHead>
      <h2 className="section-title">A doubling ladder. <br/>Anything else is improvisation.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>Every margin, padding, and gap snaps to one of these stops. Click any row to copy the value. If a space doesn't appear on this ladder, it's wrong.</p>
      <div className="spacing-ladder">
        {stops.map(s => (
          <div key={s.name} className="sp-row" onClick={() => onCopy(s.px + "px", "spacing-" + s.name + " · " + s.px + "px")}>
            <span className="name">{s.name}</span>
            <span className="bar" style={{width: s.px + "px", minWidth: 2}}/>
            <span className="px">{s.px}px · {s.use}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 11.C — Grid system */
function GridSystem() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="11.C">12-column grid</SubHead>
      <h2 className="section-title">Twelve on desktop. <br/>Four on mobile. Same gutters.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The 12-column desktop grid yields useful divisions — halves, thirds, quarters, two-thirds, plus the asymmetric 5/7 the editorial layouts depend on. Mobile collapses to a four-column grid with the same gutter widths.</p>
      <div className="grid-demo">
        <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>Desktop — 12 columns · 24px gutter</div>
        <div className="grid-rule">
          {Array.from({length: 12}, (_, i) => <div key={i} className="col">{String(i+1).padStart(2,"0")}</div>)}
        </div>
        <div style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)"}}>Mobile — 4 columns · 16px gutter</div>
        <div className="grid-rule mobile">
          {Array.from({length: 4}, (_, i) => <div key={i} className="col">{String(i+1).padStart(2,"0")}</div>)}
        </div>
        <div className="grid-meta">
          <div><span className="k">Desktop</span><span className="v">12 col · 24 gutter</span></div>
          <div><span className="k">Tablet</span><span className="v">8 col · 20 gutter</span></div>
          <div><span className="k">Mobile</span><span className="v">4 col · 16 gutter</span></div>
          <div><span className="k">Breakpoints</span><span className="v">1100 · 640</span></div>
          <div><span className="k">Max width</span><span className="v">1440 px</span></div>
          <div><span className="k">Outer gutter</span><span className="v">80 · 36 · 20</span></div>
        </div>
      </div>
    </div>
  );
}

/* 11.D — Editorial patterns */
function EditorialPatterns() {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="11.D">Editorial patterns</SubHead>
      <h2 className="section-title">Four layouts cover most of the system.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>If you find yourself drawing a fifth, ask whether it's actually a new pattern or a small variant of one of these. The brand's character lives in repetition — same shape, different content.</p>

      <div className="pattern-grid">
        <div className="pattern">
          <div className="demo">
            <div className="pat-anchor">
              <div className="big-num">04</div>
              <div className="text-stack">
                <div className="h">Color is a data point.</div>
                <div className="b">Default to monochrome. Use brand colors analytically — a chart fill, a state marker, a single accent. Never as background decoration.</div>
                <div className="b">Light mode reserves orange. Dark mode hands the role to yellow.</div>
              </div>
            </div>
          </div>
          <div className="label">
            <span className="tag">11.D.01</span>
            <h4>Numeric anchor + thesis</h4>
            <p>The default chapter opener. A large number on the left, the thesis on the right. Reads like a magazine column.</p>
          </div>
        </div>

        <div className="pattern">
          <div className="demo">
            <div className="pat-asym">
              <div className="left">
                <div className="h">Prediction ≠ understanding</div>
              </div>
              <div className="right">
                <div className="row"><span>Forecast</span><span className="g">≠</span><span className="b">Forecast survives</span></div>
                <div className="row"><span>Accuracy</span><span className="g">≠</span><span className="b">Ownership</span></div>
                <div className="row"><span>Reporting</span><span className="g">≠</span><span className="b">Decision</span></div>
              </div>
            </div>
          </div>
          <div className="label">
            <span className="tag">11.D.02</span>
            <h4>5/7 asymmetric — thesis + contrasts</h4>
            <p>One thesis on the left in 5 columns, structured contrasts on the right in 7. The brand's signature page shape.</p>
          </div>
        </div>

        <div className="pattern">
          <div className="demo">
            <div className="pat-runin">
              <div className="meta">
                <span>02.C</span>
                <strong>Linter</strong>
                <span>Tool · live</span>
              </div>
              <div className="copy">
                The messaging linter flags weakening words live as you type. Paste a sentence; the system returns a count and highlights the words to remove. The rule is simple — if removing the word doesn't change the claim, it was decoration.
              </div>
            </div>
          </div>
          <div className="label">
            <span className="tag">11.D.03</span>
            <h4>Run-in meta + copy</h4>
            <p>Mono meta column on the left names the section; body copy on the right carries the argument. Used inside long chapters.</p>
          </div>
        </div>

        <div className="pattern">
          <div className="demo">
            <div className="pat-stack">
              <div className="line">Honest data.</div>
              <div className="line">Better decisions.</div>
              <div className="line em">Real impact.</div>
              <div className="foot">11.D.04 · Stacked tagline · three lines, one breath</div>
            </div>
          </div>
          <div className="label">
            <span className="tag">11.D.04</span>
            <h4>Stacked tagline lockup</h4>
            <p>Three short lines, one beat each. Last line in signal. Lives on covers, posters, and the front of pitch decks.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- 12 TEMPLATES -------------------------------- */

function TemplatesChapter() {
  return (
    <section id="templates" className="chapter" data-screen-label="12 Templates">
      <ChapterHead num="12" kicker="Patterns" title={<>Templates.</>} meta={[["Patterns", "06"], ["Editable", "Yes"], ["Source", "This system"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="12.A">Position</SubHead>
          <p className="lede">Six ready-to-use frames for the artifacts the brand <span className="neq">repeats</span> — hero, case study, article, exec bio, event slide, signature.</p>
          <p className="body" style={{marginTop: 18}}>Each template is built from the same tokens used everywhere else in this system. The point is not to give designers a starting line; it's to make the line they walk hard to miss.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            Repetition <span className="accent">is the brand.</span> <br/>Same shape, different content.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="12.B">The six</SubHead>
        <h2 className="section-title">Hero · case study · article · bio · slide · sig.</h2>
        <div className="tpl-grid">
          <TplHero/>
          <TplCase/>
          <TplArticle/>
          <TplBio/>
          <TplSlide/>
          <TplSig/>
        </div>
      </div>
    </section>
  );
}

function TplHero() {
  return (
    <div className="tpl">
      <div className="stage">
        <div className="tpl-hero">
          <div className="top"><span>● Field guide / 2026</span><span>bdg.io / decisions</span></div>
          <h3 className="thesis">Honest data.<br/>Better decisions.<br/><span className="em">Real impact.</span></h3>
          <div className="bottom"><span>Volume I</span><span>Q2 · Board edition</span></div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.01 · Hero / thesis</span>
        <h4>Hero / thesis statement</h4>
        <p>Three lines, one breath, signal on the last. Top and bottom chrome carries the edition and the room.</p>
      </div>
    </div>
  );
}

function TplCase() {
  return (
    <div className="tpl">
      <div className="stage bone">
        <div className="tpl-case">
          <div className="meta">
            <span>● Case · <strong>Q4 — Operator</strong></span>
            <span>Cycle · <strong>11 weeks</strong></span>
            <span>Phase · <strong>Deploy</strong></span>
          </div>
          <h3 className="head">Re-architecting the <span className="hl hl-y">forecast cadence.</span></h3>
          <div className="body">
            <div>
              <div className="stat">−38<span className="u">%</span></div>
              <span>Re-open latency</span>
            </div>
            <div>
              <div className="stat">+14<span className="u">pts</span></div>
              <span>Owner alignment</span>
            </div>
            <div>
              <div className="stat">×3</div>
              <span>Decisions / quarter</span>
            </div>
          </div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.02 · Case study</span>
        <h4>Case study layout</h4>
        <p>Three-up stat row beneath a single-sentence headline. Mono meta on top. Numbers carry the claim; words explain.</p>
      </div>
    </div>
  );
}

function TplArticle() {
  return (
    <div className="tpl">
      <div className="stage">
        <div className="tpl-article">
          <div className="left">
            <strong>Editorial</strong>
            <span>04.27.26</span>
            <span>—</span>
            <span>By the studio</span>
            <span>—</span>
            <span>06 min</span>
          </div>
          <div className="right">
            <h5>Why the question matters more than the answer.</h5>
            <p>Most consultancies optimize the answer. bdg optimizes the question. Most boards review the forecast once a quarter — the market moves weekly.</p>
            <p>A faster answer to the wrong question is just a more confident wrong answer.</p>
          </div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.03 · Article</span>
        <h4>Article framework</h4>
        <p>Mono meta column on the left, body column on the right. Single-headline lede; ladder follows.</p>
      </div>
    </div>
  );
}

function TplBio() {
  return (
    <div className="tpl">
      <div className="stage">
        <div className="tpl-bio">
          <div className="avatar"/>
          <div className="info">
            <span className="role">● Partner / Diagnose</span>
            <span className="name">A. Reader</span>
            <span className="bio">Twenty years in operating roles before crossing to advisory. Owns the diagnostic phase — what counts as evidence, how the room knows when it knows.</span>
          </div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.04 · Executive bio</span>
        <h4>Executive bio</h4>
        <p>Stripe-pattern avatar (no portrait), role in signal, three-sentence bio. Same skeleton for every operator and partner.</p>
      </div>
    </div>
  );
}

function TplSlide() {
  return (
    <div className="tpl">
      <div className="stage ink">
        <div className="tpl-slide">
          <div className="chrome"><span className="mark"/><span>bdg / briefing</span><span style={{marginLeft:"auto"}}>● 04 of 12</span></div>
          <h3 className="head">Reporting tells you what happened. <br/>Decisions need what's <span className="em">next.</span></h3>
          <div className="foot"><span>Q2 BOARD · 04.27.26</span><span>FIELD GUIDE</span></div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.05 · Event slide · 1920×1080</span>
        <h4>Event talk slide</h4>
        <p>Dark stage. Mono chrome top + bottom. Headline drops the implication; the orange highlight carries the verbal beat.</p>
      </div>
    </div>
  );
}

function TplSig() {
  return (
    <div className="tpl">
      <div className="stage bone">
        <div className="tpl-sig">
          <div className="who">A. Reader</div>
          <div className="role">Partner · Diagnose</div>
          <hr/>
          <div className="lines">
            <span className="lk">a.reader@bdg.io</span>
            <span>+1 · 415 · 555 · 0144</span>
            <span>bdg.io / Q2-board</span>
            <span style={{marginTop: 6}}>—</span>
            <span>Honest data. Better decisions. Real impact.</span>
          </div>
          <div className="tag">● bdg / brand-system / signature</div>
        </div>
      </div>
      <div className="label">
        <span className="tag">12.B.06 · Email signature</span>
        <h4>Email signature</h4>
        <p>One name, one role, mono contact rows, the tagline at the bottom in body. Always six lines max; never an image.</p>
      </div>
    </div>
  );
}

/* --------------------------- 13 AUDIENCES -------------------------------- */

const AUDIENCES = [
  {
    id: "cfo",
    role: "CFO",
    room: "Board / Quarterly · 60-minute floor",
    pain: "The forecast is right on average and wrong on every quarter that matters.",
    framing: <>
      The CFO is being asked to underwrite a forecast that re-opens on a quarterly cadence — in a market that moves weekly. The pain is not <strong>accuracy</strong>; it is <strong>latency between knowing and deciding</strong>. We re-architect the cadence, not the model.
    </>,
    phrases: [
      "Most boards re-open the forecast once a quarter. Markets move weekly.",
      "Accuracy without ownership is theatre.",
      "We restructure the decision before we touch the model.",
      "Honesty before optimization. Decisions before metrics.",
    ],
    cta: ["Open the Q-cadence diagnostic", "Read the CFO field guide"],
  },
  {
    id: "controller",
    role: "Controller",
    room: "Operating cadence / Monthly close",
    pain: "Three dashboards. Five sources of truth. Still the meeting.",
    framing: <>
      The controller's pain is reconciliation — tools were supposed to remove the meeting; instead the meeting just changed venue. We address the workflow: one number of record, one owner per row, one cadence per decision.
    </>,
    phrases: [
      "Tools don't replace the meeting. The meeting is the decision.",
      "One number of record. One owner per row.",
      "Reconciliation is rework with a calendar invite.",
      "If two teams reconcile after the report, the report failed.",
    ],
    cta: ["Audit your decision cadence", "Talk to a controller partner"],
  },
  {
    id: "it",
    role: "IT / Data",
    room: "Architecture review · Procurement",
    pain: "Modeling capacity isn't the bottleneck. The decision protocol is.",
    framing: <>
      IT has shipped the platform. The model is performant; the data is clean. And the decision still sits in a Slack thread. We close the gap between <strong>delivered insight</strong> and <strong>made decision</strong> — owners, thresholds, write-back paths.
    </>,
    phrases: [
      "The model is not the bottleneck. The protocol is.",
      "Insight without write-back is a screenshot.",
      "Define the owner before you define the schema.",
      "A decision intelligence layer is workflow, not warehouse.",
    ],
    cta: ["Map the decision protocol", "Architecture briefing"],
  },
  {
    id: "ceo",
    role: "CEO",
    room: "Off-site / Founder briefing",
    pain: "The room agrees on the answer. Nobody agrees on the question.",
    framing: <>
      For the CEO the pain is alignment — every leader can show a chart that confirms their position. The work is not to choose the answer; it is to <strong>name the question</strong> the company is actually deciding, then make the cadence honor it.
    </>,
    phrases: [
      "Most consultancies optimize the answer. bdg optimizes the question.",
      "Alignment is the prerequisite. Speed is the consequence.",
      "Optimization without alignment is rework, faster.",
      "If the room disagrees on the question, they will optimize different things.",
    ],
    cta: ["Founder briefing · 45 min", "Read 'The question first'"],
  },
];

function AudiencesChapter() {
  const [id, setId] = useState("cfo");
  const current = AUDIENCES.find(a => a.id === id);
  return (
    <section id="audiences" className="chapter" data-screen-label="13 Voice in Action">
      <ChapterHead num="13" kicker="Audience" title={<>Voice in <br/>Action.</>} meta={[["Audiences","04"], ["Shape","Same"], ["Texture","Different"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="13.A">Position</SubHead>
          <p className="lede">Same brand, <span className="neq">different</span> rooms. The voice shifts by who is reading; the claims do not.</p>
          <p className="body" style={{marginTop: 18}}>The four pages below show how the same posture lands in front of four different audiences. The pain changes. The framing changes. The phrases are still in the library. The promise still reads "honest data, better decisions, real impact."</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            The <span className="accent">room</span> is the variable. <br/>The voice is the constant.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="13.B">Select an audience</SubHead>
        <div className="audience-tabs">
          {AUDIENCES.map(a => (
            <button key={a.id} aria-selected={id === a.id} onClick={() => setId(a.id)}>{a.role}</button>
          ))}
        </div>

        <div className="aud-card">
          <div className="meta">
            <span className="role-tag">Audience · {String(AUDIENCES.findIndex(a => a.id === id) + 1).padStart(2, "0")} of 04</span>
            <h3 className="role-name">{current.role}.</h3>
            <p className="body" style={{maxWidth:"32ch", color:"var(--ink-2)", margin: 0}}>The room that this voice lands in. What they came to read. What they leave with.</p>
            <div className="room">{current.room}</div>
          </div>
          <div className="stages">
            <div className="stage-row">
              <div className="stage-ix"><span className="num">01</span> Pain — what they feel</div>
              <h4 className="pain">{current.pain}</h4>
            </div>
            <div className="stage-row">
              <div className="stage-ix"><span className="num">02</span> Framing — how bdg says it</div>
              <p className="framing">{current.framing}</p>
            </div>
            <div className="stage-row">
              <div className="stage-ix"><span className="num">03</span> Key phrases — verbatim</div>
              <div className="phrases">
                {current.phrases.map((p, i) => (
                  <div key={i} className="phrase">
                    <span className="ix">{String(i + 1).padStart(2, "0")}</span>
                    <span>{p}</span>
                  </div>
                ))}
              </div>
            </div>
            <div className="stage-row">
              <div className="stage-ix"><span className="num">04</span> CTA — the next move</div>
              <div className="cta-row">
                {current.cta.map((c, i) => (
                  <button key={c} className={"bdg-btn " + (i === 0 ? "solid" : "")}>{c} {i === 0 ? "↘" : ""}</button>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 02 MARKET & COMPETITORS --------------------- */

function MarketChapter() {
  const clusters = [
    {
      ix: "01", tag: "Quadrant · NW",
      name: "Analytics platforms",
      examples: ["Tableau", "Power BI", "Looker", "Sigma", "ThoughtSpot"],
      does: "Sell dashboards, semantic layers, and the promise that visibility produces decisions. They optimise the answer.",
      stops: "Stops at the chart. The dashboard is the deliverable; what the room does with it is someone else's problem.",
    },
    {
      ix: "02", tag: "Quadrant · NE",
      name: "Strategy firms",
      examples: ["McKinsey", "BCG", "Bain", "Boutique strategy"],
      does: "Re-architect strategy and operating models. Heavy on framework, deep on industry context. They optimise the recommendation.",
      stops: "Stops at the recommendation. The cadence that re-opens the decision is left to the client to build — and almost never is.",
    },
    {
      ix: "03", tag: "Quadrant · SW",
      name: "AI / ML vendors",
      examples: ["Forecasting tools", "Decision intelligence platforms", "GenAI copilots"],
      does: "Sell predictive models and the promise that better forecasts produce better decisions. They optimise the prediction.",
      stops: "Stops at the model. Accuracy without ownership is theatre — and ownership doesn't ship in the SDK.",
    },
    {
      ix: "04", tag: "Quadrant · SE",
      name: "Change consultancies",
      examples: ["Org design", "Transformation", "OKR practitioners"],
      does: "Re-shape teams, rituals, and ways-of-working. Heavy on people. They optimise the process around the decision.",
      stops: "Skips the diagnosis. Without a clear read on what evidence the decision needs, the new ritual just decides the wrong thing on cadence.",
    },
  ];
  return (
    <section id="market" className="chapter" data-screen-label="02 Market & Competitors">
      <ChapterHead num="02" kicker="Market" title={<>Market &amp; <br/>Competitors.</>} meta={[["Clusters","04"], ["Quadrant","2×2"], ["bdg sits","Between"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="02.A">Position</SubHead>
          <p className="lede">Four clusters cover the decision-intelligence space. <span className="neq">None</span> of them sit where bdg sits — between the question and the answer.</p>
          <p className="body" style={{marginTop: 18}}>The category is crowded, but the crowd is split into four corners. Analytics optimises the answer. Strategy optimises the recommendation. AI optimises the prediction. Change optimises the process. bdg optimises <strong>the decision</strong> — the unit that all four claim to serve but none actually owns.</p>
        </div>
        <div className="right">
          <div className="posmap">
            <div className="axis-h"/><div className="axis-v"/>
            <span className="ax-lbl top">Re-architects the decision →</span>
            <span className="ax-lbl bottom">← Delivers a tool</span>
            <span className="ax-lbl left">Process · people</span>
            <span className="ax-lbl right">Structure · evidence</span>
            <span className="pin" style={{left:"22%", top:"32%"}}>Analytics</span>
            <span className="pin" style={{left:"66%", top:"24%"}}>Strategy</span>
            <span className="pin" style={{left:"20%", top:"72%"}}>AI · ML</span>
            <span className="pin" style={{left:"64%", top:"74%"}}>Change</span>
            <span className="pin bdg" style={{left:"46%", top:"46%"}}>bdg</span>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="02.B">Four clusters</SubHead>
        <h2 className="section-title">What each one does. <br/>Where each one stops.</h2>
        <div className="market-grid">
          {clusters.map(c => (
            <div key={c.ix} className="cluster">
              <div className="head">
                <span className="ix">02.B.{c.ix}</span>
                <span className="tag">{c.tag}</span>
              </div>
              <h4>{c.name}</h4>
              <div className="examples">{c.examples.map(e => <span key={e}>{e}</span>)}</div>
              <p className="does">{c.does}</p>
              <div className="stops"><strong>Stops at</strong> — {c.stops}</div>
            </div>
          ))}
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="02.C">Brand archetypes — how the nine present themselves</SubHead>
        <h2 className="section-title">Three visual postures. <br/>Nine specific firms.</h2>
        <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>The functional clusters above describe what the market does. The brand archetypes below describe how it looks while doing it. Each archetype is one of bdg's competitive surfaces — the one a buyer reads first.</p>

        <div className="arch-stack">
          <div className="arch">
            <div className="lead">
              <span className="ix">02.C.01</span>
              <h4>Strategy-led intellectual minimalism.</h4>
              <p>Recently rebranded. Restraint, high-contrast typography, custom serif faces. Authoritative without being warm. Digital presence is editorial — when it lands, it lands.</p>
              <div className="vis"><strong>Visual signature</strong> · Deep blue / white · custom typography · sparse data viz · "high contrast"</div>
            </div>
            <div className="members">
              <div className="brand-cell">
                <div className="row"><span className="name">McKinsey</span><span className="geo">Global · MBB</span></div>
                <p className="pos">"Change that matters." Wolff Olins, 2019. High-contrast deep blue against white; <em>Bower</em> serif + <em>Theinhardt</em> sans. Stacked logo. Blue line-pattern as system motif. Elevation of data viz as a brand element.</p>
                <div className="swatch-row"><span style={{background:"#051C2C"}}/><span style={{background:"#2251FF"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Bower serif</span><span>Theinhardt</span><span>Deep blue</span><span>Line pattern</span></div>
                <div className="gap"><strong>Where they stop</strong> — bold but institutional. Thought leadership is hit-or-miss. The brand performs authority more than it earns it on the page.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">BCG</span><span className="geo">Global · MBB</span></div>
                <p className="pos">"Beyond is where we begin." Carbone Smolan, 2018. Connected-character sans-serif logo — the B, C, G merge. Dropped "The." Single deep green. Bold, tech-forward, less staid than its peers.</p>
                <div className="swatch-row"><span style={{background:"#00723D"}}/><span style={{background:"#21D672"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Geo-sans</span><span>Merged letters</span><span>BCG green</span><span>"Beyond" campaign</span></div>
                <div className="gap"><strong>Where they stop</strong> — the website is an interactive mess. The bold colors and modern restraint don't always coexist. Personal thought leadership outshines institutional voice.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">Horváth</span><span className="geo">DACH · Stuttgart</span></div>
                <p className="pos">"Road to Sustainable Value." Performance management + transformation specialist. €300M revenue, ~1,400 staff. Horváth.AI subbrand. Generous whitespace, severely outdated visual identity, German-focused.</p>
                <div className="swatch-row"><span style={{background:"#003E7E"}}/><span style={{background:"#DCDCE0"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Old-school sans</span><span>Whitespace</span><span>Navy</span><span>Horváth.AI</span></div>
                <div className="gap"><strong>Where they stop</strong> — digital performance is similar to bdg's, but the brand is locked in 2012. Operates without a public posture; relies entirely on rankings and relationships.</div>
              </div>
            </div>
          </div>

          <div className="arch">
            <div className="lead">
              <span className="ix">02.C.02</span>
              <h4>Large, trust-first enterprise.</h4>
              <p>The Big Four posture. Dark or restrained palettes, heavy structured layouts, dense information hierarchy. Minimal emotion, maximum institutional authority. Outdated by design — the whole point is that they don't have to chase.</p>
              <div className="vis"><strong>Visual signature</strong> · Dark + restrained · institutional type · cinematic campaigns · dense hierarchy</div>
            </div>
            <div className="members">
              <div className="brand-cell">
                <div className="row"><span className="name">Deloitte</span><span className="geo">Global · Big Four</span></div>
                <p className="pos">Dark, restrained palette. Heavy use of typography and structured layouts. Minimal emotion, maximum authority. Cinematic athletic-inspired campaigns focused on people and diversity; otherwise the system is impersonal and editorial.</p>
                <div className="swatch-row"><span style={{background:"#86BC25"}}/><span style={{background:"#000000"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Black + green dot</span><span>Real Headline</span><span>Dense grid</span><span>Cinematic film</span></div>
                <div className="gap"><strong>Where they stop</strong> — outdated by design. The brand is so big it can't move; campaigns substitute for posture; the system relies on scale, not voice.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">PwC</span><span className="geo">Global · Big Four</span></div>
                <p className="pos">Modular layouts. Warm but corporate gradients. Dense information hierarchy. Strong institutional tone. Expertise-focused digital presence — user-oriented but impersonal; the brand never speaks in the first person.</p>
                <div className="swatch-row"><span style={{background:"#E0301E"}}/><span style={{background:"#FFB600"}}/><span style={{background:"#A32020"}}/></div>
                <div className="marks"><span>Orange + red</span><span>Sectra ITC</span><span>Modular</span><span>Gradients</span></div>
                <div className="gap"><strong>Where they stop</strong> — the gradient is the brand. Warmth replaces position. Reads as a portal, not an argument.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">KPMG</span><span className="geo">Global · Big Four</span></div>
                <p className="pos">The most institutional of the four. Deep blue, square cropping, photography of buildings and boardrooms. Heavy serifs in white papers; "trust" as the operative noun across all communications.</p>
                <div className="swatch-row"><span style={{background:"#00338D"}}/><span style={{background:"#0091DA"}}/><span style={{background:"#1E49E2"}}/></div>
                <div className="marks"><span>KPMG blue</span><span>Univers</span><span>Square crops</span><span>Trust</span></div>
                <div className="gap"><strong>Where they stop</strong> — leans hardest on legacy. Has the lowest digital expressiveness of the three; the brand is the institution.</div>
              </div>
            </div>
          </div>

          <div className="arch">
            <div className="lead">
              <span className="ix">02.C.03</span>
              <h4>Specialist · boutique analytics &amp; BI.</h4>
              <p>The bdg-adjacent layer. Small to mid-size firms in DACH and Europe. Often deeply expert, often visually under-invested. Brand is technical rather than editorial; voice rarely makes it past LinkedIn.</p>
              <div className="vis"><strong>Visual signature</strong> · Generic sans · stock photography · technical pages · low-fidelity decks</div>
            </div>
            <div className="members">
              <div className="brand-cell">
                <div className="row"><span className="name">b.telligent</span><span className="geo">DACH · Munich</span></div>
                <p className="pos">German specialist in BI, data warehousing, advanced analytics. Founded 2004. Highly competent, severely outdated and low-quality visual presence. The brand reads as a freelancers' cooperative more than a category leader.</p>
                <div className="swatch-row"><span style={{background:"#0F2A66"}}/><span style={{background:"#1B7BC9"}}/><span style={{background:"#F4F4F5"}}/></div>
                <div className="marks"><span>Generic sans</span><span>Stock hero</span><span>Tech-blue</span><span>2014 web</span></div>
                <div className="gap"><strong>Where they stop</strong> — expertise without a story. The brand assumes the reader already trusts it; nobody outside the existing client base does.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">BearingPoint</span><span className="geo">Europe · DACH-led</span></div>
                <p className="pos">"Boutique but trying." Transparent and open posture. Content is an experimental mix — case studies, point-of-view, social. The voice has range but lacks a single anchor claim.</p>
                <div className="swatch-row"><span style={{background:"#FF5C00"}}/><span style={{background:"#1C1C1C"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Orange dot</span><span>Open campaigns</span><span>Mixed voice</span><span>Editorial-curious</span></div>
                <div className="gap"><strong>Where they stop</strong> — reaches for editorial but doesn't commit. The mix without a thesis reads as a sampler, not a system.</div>
              </div>
              <div className="brand-cell">
                <div className="row"><span className="name">MAIT</span><span className="geo">DACH · Stuttgart</span></div>
                <p className="pos">Mid-tier specialist. IT consulting + engineering software. Visual identity built around primary blue + photography of products and people. Functional, generic — the brand serves procurement, not the room.</p>
                <div className="swatch-row"><span style={{background:"#003F7E"}}/><span style={{background:"#6EBE2C"}}/><span style={{background:"#FFFFFF"}}/></div>
                <div className="marks"><span>Corp blue</span><span>Product photos</span><span>Generic sans</span><span>Procurement voice</span></div>
                <div className="gap"><strong>Where they stop</strong> — sells to IT, not to the board. The brand never makes the leap from vendor to partner.</div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="02.D">Where bdg sits</SubHead>
        <h2 className="section-title">In the gap none of the four owns.</h2>
        <p className="body" style={{maxWidth:"60ch", marginBottom: 16}}>The market has tools, strategies, models, and rituals — but no one is on the hook for the decision itself. That's the gap bdg occupies. We carry the diagnosis through to a decision that survives the next quarter.</p>
        <div className="pullquote" style={{fontSize:"clamp(28px, 3.6vw, 52px)", maxWidth: "26ch", marginTop: 24}}>
          Not a fifth cluster — <span className="accent">a different layer</span> running across all four.
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 04 COMPETITIVE EDGE ------------------------- */

function EdgeChapter() {
  const redefs = [
    {
      ix: "01",
      stop: "Data is not static.",
      start: "It has velocity, tension, contradiction, inertia.",
      gloss: "Every number in the room is moving against another number in the room. The job is not to freeze the data — it's to read the forces acting on it.",
      forces: ["Velocity", "Tension", "Contradiction", "Inertia"],
    },
    {
      ix: "02",
      stop: "Decisions are not moments.",
      start: "They are processes unfolding over time.",
      gloss: "The board meeting is not the decision; it is one frame in a process that started months earlier and continues for quarters after. We instrument the whole arc — not the moment.",
      forces: ["Frame · before", "Frame · now", "Frame · after"],
    },
    {
      ix: "03",
      stop: "Intelligence is not output.",
      start: "It's the ability to sense change early.",
      gloss: "Reports are output. Dashboards are output. Decision intelligence is the standing capacity to see the shift before it forces a meeting — and to act while the shift is still cheap to act on.",
      forces: ["Sense", "Frame", "Act"],
    },
  ];
  const edges = [
    {
      ix: "01",
      stop: "Strategy firms stop at the recommendation.",
      start: "bdg starts with the cadence.",
      gloss: "We design the operating rhythm that re-opens the decision on a frequency the market actually moves at — and we run the first three cycles with the room.",
    },
    {
      ix: "02",
      stop: "Analytics stops at the chart.",
      start: "bdg starts with the owner.",
      gloss: "Every dashboard we ship carries a name on it. If no one owns the row, the row doesn't get built. Visibility is necessary; ownership is sufficient.",
    },
    {
      ix: "03",
      stop: "AI vendors stop at the model.",
      start: "bdg starts with the threshold.",
      gloss: "Models produce predictions. We define what evidence is enough to act — the threshold the prediction has to cross before the decision moves. Without it, the model is a confidence factory.",
    },
    {
      ix: "04",
      stop: "Change consultancies stop at the ritual.",
      start: "bdg starts with the diagnosis.",
      gloss: "Before we rewrite the meeting, we name the question. The structure of the decision — owner, inputs, cadence, evidence — comes before the new ritual that holds it.",
    },
  ];
  return (
    <section id="edge" className="chapter" data-screen-label="04 Competitive Edge">
      <ChapterHead num="04" kicker="Position" title={<>Competitive <br/>Edge.</>} meta={[["Redefinitions","03"], ["Posture","Sense early"], ["Defensibility","Cadence + ownership"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="04.A">Position</SubHead>
          <p className="lede">The edge is a <span className="neq">redefinition,</span> not a feature. We don't compete on the answer — we redefine the question that produces it.</p>
          <p className="body" style={{marginTop: 18}}>Three claims about the world separate bdg from the four clusters: data is not static, decisions are not moments, intelligence is not output. Each of these is a sentence the market would agree with in principle — and contradict in every project they ship.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            They sell <span className="accent">snapshots.</span> <br/>We sell the ability to <span className="accent">sense change early.</span>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="04.B">Three redefinitions</SubHead>
        <h2 className="section-title">What the market gets wrong. <br/>What bdg gets right.</h2>
        <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>Each row reframes a noun the consulting market treats as fixed. The reframe is the position — and the position is how every project is staffed, instrumented, and judged.</p>
        <div className="edge-stack">
          {redefs.map(r => (
            <div key={r.ix} className="edge-row redef">
              <div className="ix">{r.ix}</div>
              <div className="stop"><span style={{display:"block", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", color:"var(--signal)", marginBottom: 6}}>The market thinks</span>{r.stop}</div>
              <div className="start">
                <span style={{display:"block", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", color:"var(--signal)", marginBottom: 6}}>bdg says</span>
                {r.start}
                <small>{r.gloss}</small>
                <div style={{display:"flex", flexWrap:"wrap", gap: 6, marginTop: 12}}>
                  {r.forces.map(f => (
                    <span key={f} style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", padding:"4px 8px", border:"0.8px solid var(--line)", color:"var(--ink-2)"}}>{f}</span>
                  ))}
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="04.C">Where they stop · where we start</SubHead>
        <h2 className="section-title">The three redefinitions, applied to the four clusters.</h2>
        <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>Each cluster solves a piece of one of the three. None of them carries the full posture. The seam between their deliverable and the next decision is where bdg lives.</p>
        <div className="edge-stack">
          {edges.map(e => (
            <div key={e.ix} className="edge-row">
              <div className="ix">{e.ix}</div>
              <div className="stop">{e.stop}</div>
              <div className="start">
                {e.start}
                <small>{e.gloss}</small>
              </div>
            </div>
          ))}
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="04.D">Defensibility</SubHead>
        <h2 className="section-title">The edge is the <span className="hl hl-y">posture,</span> not the deck.</h2>
        <p className="body" style={{maxWidth:"60ch"}}>Decks get copied. Frameworks get repackaged. What does not get copied is the standing capacity to sense change early — the partner who shows up before the meeting is called, names the shift, and stays on the hook for the call. Sensing is the moat.</p>
      </div>
    </section>
  );
}

/* --------------------------- 15 SOCIAL MEDIA ----------------------------- */

function SocialChapter() {
  return (
    <section id="social" className="chapter" data-screen-label="15 Social Media">
      <ChapterHead num="15" kicker="Channel" title={<>Social <br/>Media.</>} meta={[["Channels","04"], ["Formats","03"], ["Cadence","Editorial · not daily"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="15.A">Position</SubHead>
          <p className="lede">Short. Provocative-measured. The post is the <span className="neq">opening line</span> of an argument, not the argument itself.</p>
          <p className="body" style={{marginTop: 18}}>bdg posts to start conversations, not to fill calendars. One claim per post. No engagement bait, no thread-bombs, no infographic-soup. The post should make sense in the timeline; the depth lives in what it links to.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            One <span className="accent">claim,</span> one image, one link. <br/>If it needs three, it's an article.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="15.B">Format specimens</SubHead>
        <h2 className="section-title">Real posts from the system.</h2>
        <p className="body" style={{marginBottom: 16, maxWidth: "60ch"}}>Three frames, two-to-three colors each. Surface rotates; the highlight reserves itself for the verb. None of these is a mock — each one shipped, each one is a template you can fork.</p>
        <div className="specimen-grid">
          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.01 · Square · yellow</span>
              <h4>It's crazy until it isn't</h4>
              <p>Yellow surface, ink type, green tape overlay. One bright surface + one figure + one secondary accent — never three colors at peak.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/bdg-image1.png" alt="It's crazy until it isn't · yellow" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.02 · Square · red + orange</span>
              <h4>It's crazy until it isn't · alt</h4>
              <p>Saturated red with orange ampersand. Blue tape carries the secondary line. Reserved for sharp, contrarian editorial.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/bdg-image1-1.png" alt="It's crazy until it isn't · red" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.03 · Square · purple + blue</span>
              <h4>It's crazy until it isn't · alt</h4>
              <p>Purple ampersand on blue surface; yellow tape. Reserved for softer claims — questions, hypotheticals, future-tense.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/bdg-image1-2.png" alt="It's crazy until it isn't · purple" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.04 · Square · photo</span>
              <h4>$7.2M · case stat</h4>
              <p>Black-and-white photograph anchored by an orange numeric panel. Photography is monochrome; only the data carries color.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/bdg-image2.png" alt="$7.2M case stat" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.05 · Cover · Notion</span>
              <h4>Messaging · cover</h4>
              <p>Black surface, white type, two highlight tags — purple arrow + green exclamations — marking the rhythm of the sentence.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/notion-cover-1.png" alt="Notion cover" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">15.B.06 · Cover · Notion alt</span>
              <h4>Messaging · cover alt</h4>
              <p>Same skeleton, different beat: orange arrow + yellow exclamation. Two color beats per cover, no more.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/notion-cover-2.png" alt="Notion cover alt" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="15.C">Channel matrix</SubHead>
        <h2 className="section-title">Same posture. Different room.</h2>
        <div className="channel-matrix">
          <div className="cm-row head">
            <div className="cm-cell">Channel</div>
            <div className="cm-cell">Format</div>
            <div className="cm-cell">Tone knob</div>
            <div className="cm-cell">Cadence</div>
            <div className="cm-cell">CTA</div>
          </div>
          {[
            ["LinkedIn",  "1:1 · 4:5", "Direct · numbered",        "2× / week",  "Read the case"],
            ["X",         "1:1 · 16:9", "Provocative-measured",     "3× / week",  "Reply · DM"],
            ["Substack",  "16:9 hero · long",   "Editorial",         "Bi-weekly",  "Subscribe"],
            ["YouTube",   "16:9",       "Calm authority",            "Monthly",    "Watch · Diagnose"],
          ].map(([c, f, t, cd, ct], i) => (
            <div key={c} className="cm-row">
              <div className="cm-cell name">{c}</div>
              <div className="cm-cell">{f}</div>
              <div className="cm-cell">{t}</div>
              <div className="cm-cell dim">{cd}</div>
              <div className="cm-cell fill">{ct}</div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 16 PRINT ------------------------------------ */

function PrintChapter() {
  return (
    <section id="print" className="chapter" data-screen-label="16 Print">
      <ChapterHead num="16" kicker="Channel" title={<>Print.</>} meta={[["Artifacts","04"], ["Stock","Uncoated · 110gsm+"], ["Tagline","Always"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="16.A">Position</SubHead>
          <p className="lede">Paper is a <span className="neq">commitment</span>. Anything printed lives longer than a webpage and must read as deliberate at six paces.</p>
          <p className="body" style={{marginTop: 18}}>Print is uncoated by default — the system reads as paper, not chrome. Type is set to the same ladder. Signal accent appears once on the artifact; never twice. Bleeds are clean; no decorative borders.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            One <span className="accent">accent</span> per artifact. <br/>If you used it on the front, you don't use it on the back.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="16.B">Artifacts</SubHead>
        <h2 className="section-title">Four print formats. <br/>One tagline lockup.</h2>
        <div className="specimen-grid">
          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.01 · LED banner · vertical</span>
              <h4>Event banner · vertical</h4>
              <p>Black surface, full-bleed type, six brand hues used analytically as directional arrows. Color carries the rhythm; black is the surface.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/banner-led-3.png" alt="LED banner" style={{width:"100%", height:"100%", objectFit:"contain", maxHeight: 360}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.02 · LED banner · stacked type</span>
              <h4>Event banner · type stack</h4>
              <p>Three claims stacked. Highlights rotate by claim — purple, green, orange. One highlight per line, no two on the same line.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/banner-led-2.png" alt="LED banner stacked" style={{width:"100%", height:"100%", objectFit:"contain", maxHeight: 360}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.03 · LED banner · horizontal</span>
              <h4>Event banner · horizontal</h4>
              <p>Mass-market readout. Same hierarchy as the vertical, re-flowed for landscape sightlines. Mark + QR carries the chrome.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/banner-led-1.png" alt="LED banner horizontal" style={{width:"100%", height:"100%", objectFit:"contain", maxHeight: 360}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.04 · Retail booth</span>
              <h4>Retail booth</h4>
              <p>Physical artifact — ink + paper, large mark, the inequality glyph. Built to carry the booth at distance.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/retail-booth-1.png" alt="Retail booth" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.05 · Conference event</span>
              <h4>Event backdrop</h4>
              <p>Saturated blue surface; one yellow highlight on the verb. Color carries the surface; the highlight reserves itself for the claim.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/board-event.png" alt="Board event backdrop" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">16.B.06 · Retail · ambient</span>
              <h4>Retail · ambient</h4>
              <p>Quiet, ink-on-paper artifact for retail spaces where the brand sits in the background. Reads correctly without any color.</p>
            </div>
            <div className="preview ink" style={{padding: 0}}>
              <img src="assets/ex/retail-booth.png" alt="Retail booth ambient" style={{width:"100%", height:"100%", objectFit:"cover"}}/>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 17 DIGITAL & WEB ---------------------------- */

function DigitalChapter() {
  return (
    <section id="digital" className="chapter" data-screen-label="17 Digital & Web">
      <ChapterHead num="17" kicker="Channel" title={<>Digital &amp; <br/>Web.</>} meta={[["Surfaces","04"], ["Grid","12 · 4 col"], ["Default","Light mode"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="17.A">Position</SubHead>
          <p className="lede">A bdg website reads as a <span className="neq">column of evidence</span>, not a wall of cards. Pages are essays; products are documented as systems.</p>
          <p className="body" style={{marginTop: 18}}>The home page carries one thesis. Product pages carry one claim each. The CTA is always specific — never "Learn more." If a section doesn't move the argument forward, it's removed.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            Long pages. <span className="accent">Short claims.</span> <br/>Read once, scrolled all the way.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="17.B">Surface specimens</SubHead>
        <h2 className="section-title">Four web surfaces. <br/>Same scaffolding, different ladder.</h2>
        <div className="specimen-grid">
          <div className="spec-card">
            <div className="pad">
              <span className="ix">17.B.01 · Home hero</span>
              <h4>Home / hero</h4>
              <p>Tagline lockup. One CTA. Mono chrome above; volume meta below.</p>
            </div>
            <div className="preview paper">
              <div className="mount-landscape" style={{width:"100%"}}>
                <div className="fp">
                  <div className="chrome"><span><span className="mark-img"/> bdg</span><span>● 2026</span></div>
                  <div className="head md" style={{alignSelf:"center"}}>Honest data.<br/>Better decisions.<br/><span className="em">Real impact.</span></div>
                  <div className="foot"><span>FIELD GUIDE</span><span>OPEN ↘</span></div>
                </div>
              </div>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">17.B.02 · Product / method page</span>
              <h4>Method · diagnose</h4>
              <p>One claim, one chart, one CTA. Eyebrow names the phase.</p>
            </div>
            <div className="preview bone">
              <div className="mount-landscape" style={{width:"100%"}}>
                <div className="fp">
                  <div className="chrome"><span>● DIAGNOSE</span><span>METHOD · 01 / 03</span></div>
                  <div className="head sm" style={{alignSelf:"center"}}>Name the question <br/>before the model runs.</div>
                  <div className="foot"><span>↗ READ THE CASE</span><span>11 MIN</span></div>
                </div>
              </div>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">17.B.03 · Newsletter · email</span>
              <h4>Email · field guide</h4>
              <p>Plain text first. One thesis, two paragraphs, one link out. No HTML hero.</p>
            </div>
            <div className="preview paper">
              <div className="mount-portrait">
                <div className="fp" style={{gap: 6}}>
                  <div className="chrome"><span>FROM · BDG</span><span>04.27</span></div>
                  <div style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 14, lineHeight: 1.05, letterSpacing:"-0.005em", color:"var(--ink)"}}>The question first.</div>
                  <div style={{fontFamily:"var(--f-body)", fontSize: 9, lineHeight: 1.5, color:"var(--ink-2)"}}>
                    Most boards review the forecast once a quarter. The market moves weekly. The pain is not the model — it is the cadence.
                  </div>
                  <div style={{fontFamily:"var(--f-mono)", fontSize: 8, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--signal)", marginTop:"auto"}}>↘ READ ON BDG.IO</div>
                </div>
              </div>
            </div>
          </div>

          <div className="spec-card">
            <div className="pad">
              <span className="ix">17.B.04 · Article reader</span>
              <h4>Editorial · article</h4>
              <p>Mono meta column on the left. Body in a 60-character column. Pull-quotes use the brand's italic-highlight.</p>
            </div>
            <div className="preview paper">
              <div className="mount-landscape" style={{width:"100%"}}>
                <div className="fp">
                  <div className="chrome"><span>EDITORIAL · 04.27.26</span><span>06 MIN</span></div>
                  <div className="head sm" style={{alignSelf:"center"}}>Why the question matters <br/>more than the answer.</div>
                  <div className="foot"><span>By the studio</span><span>● 04 of 12</span></div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

/* --------------------------- 19 PRESENTATIONS ---------------------------- */

/* Embeds one slide from the master deck via the deck-stage hash protocol.
   The bundled deck respects #N (1-indexed) on load. */
function MasterSlide({ n, label }) {
  return (
    <iframe
      src={`decks/master-deck.html#${n}`}
      title={label}
      loading="lazy"
      style={{
        position: "absolute", inset: 0,
        width: "100%", height: "100%", border: 0,
        background: "#fff",
      }}
    />
  );
}

function MasterSlideCard({ ix, title, gloss, n, label, tone }) {
  return (
    <div className="spec-card">
      <div className="pad">
        <span className="ix">{ix} · slide #{n}</span>
        <h4>{title}</h4>
        <p>{gloss}</p>
      </div>
      <div className="preview" style={{padding: 0, background: tone === "dark" ? "#0A0A0C" : "#FAFAFA", aspectRatio: "16 / 9", minHeight: 0, position: "relative"}}>
        <MasterSlide n={n} label={label} />
      </div>
    </div>
  );
}

function DecksChapter() {
  return (
    <section id="decks" className="chapter" data-screen-label="19 Presentations">
      <ChapterHead num="19" kicker="Slides" title={<>Presentations <br/>&amp; Slides.</>} meta={[["Format","Live HTML"], ["Archetypes","08"], ["Stage","1920 × 1080"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="19.A">Position</SubHead>
          <p className="lede">Decks are <span className="neq">live HTML</span> — not PowerPoint files. The presentation is a page on the web, versioned with the brand and updated like any other product surface.</p>
          <p className="body" style={{marginTop: 18}}>One thought per page, one claim per slide. Decks read as essays, not as decoration around a verbal track. The stage is 1920×1080. Default surface is paper; section dividers go dark. Headlines in condensed; body on the four-rung ladder.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            From PowerPoint <br/>to <span className="accent">live HTML.</span> <br/>One source, every surface.
          </div>
        </div>
      </div>

      <DeckMigration />
      <LiveDecks />
      <DeckArchetypes />
      <DataVizSlides />
    </section>
  );
}

/* 19.D — Slide archetypes drawn from the master deck */
function DeckArchetypes() {
  const archetypes = [
    { ix: "19.D.01", n: 1,  label: "Cover — Primary",      title: "Cover slide",         gloss: "Three-line tagline, one highlight on the last. Volume meta and date in the chrome.", tone: "dark" },
    { ix: "19.D.02", n: 7,  label: "Section — Context",    title: "Section divider",     gloss: "Dark slide. Numeric anchor in highlight. One headline. Used between chapters in long decks.", tone: "dark" },
    { ix: "19.D.03", n: 48, label: "T.01 Title + body",    title: "Title + body",        gloss: "Headline up top, body on the rung. The default narrative slide.", tone: "paper" },
    { ix: "19.D.04", n: 50, label: "T.03 Big stat",        title: "Stat tile",           gloss: "One number, one caption, one source. The claim survives being read at distance.", tone: "dark" },
    { ix: "19.D.05", n: 49, label: "T.02 Two-column",      title: "Two-column compare",  gloss: "5/7 asymmetric grid. Thesis left, contrasts right. The brand's signature slide shape.", tone: "paper" },
    { ix: "19.D.06", n: 51, label: "T.04 Quote",           title: "Quote / verbatim",    gloss: "Operator quote with a highlight block. Attribution mono. Used for evidence — never for filler.", tone: "paper" },
    { ix: "19.D.07", n: 52, label: "T.05 Full-bleed image",title: "Full-bleed image",    gloss: "Editorial photography corner-to-corner. Caption in mono, bottom-left. Used sparingly.", tone: "dark" },
    { ix: "19.D.08", n: 53, label: "T.06 Table",           title: "Table",               gloss: "Mono headers, tabular numerics, hairline rows. The reading of a real spreadsheet.", tone: "paper" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="19.D">Slide archetypes — from the master deck</SubHead>
      <h2 className="section-title">Eight building blocks. <br/>Every deck is an assembly.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Each thumbnail below is a live iframe of the actual slide from the master deck. Hover over a card to read the rule; open in a new tab to see it at full stage size.</p>
      <div className="specimen-grid">
        {archetypes.map(a => <MasterSlideCard key={a.ix} {...a} />)}
      </div>
    </div>
  );
}

/* 19.E — Data visualization slides */
function DataVizSlides() {
  const viz = [
    { ix: "19.E.01", n: 32, label: "DV · Hero number",        title: "Hero number",        gloss: "A single number, scaled until it carries the whole slide. The headline below names the room." },
    { ix: "19.E.02", n: 33, label: "DV · Three tiles",        title: "Three stat tiles",   gloss: "Three numbers side-by-side. Same unit, different cuts. Tabular alignment across cells." },
    { ix: "19.E.03", n: 34, label: "DV · Pictogram row",      title: "Pictogram row",      gloss: "X-of-Y framed as a row of figures. Foreground fills the share; background ghosts the remainder." },
    { ix: "19.E.04", n: 35, label: "DV · World map",          title: "World map",          gloss: "Geo distribution as discrete dots. Sized by weight, never colored by emotion." },
    { ix: "19.E.05", n: 36, label: "DV · Pixel portrait",     title: "Pixel portrait",     gloss: "An individual rendered as a low-resolution density grid. Used for verbatim slides." },
    { ix: "19.E.06", n: 37, label: "DV · Growth line",        title: "Growth line",        gloss: "One series, one line, one signal-marked terminal dot. Annotation in mono." },
    { ix: "19.E.07", n: 38, label: "DV · Ranked bars",        title: "Ranked bars",        gloss: "Horizontal bars sorted by value. The top bar carries the highlight; the rest sit in ink." },
    { ix: "19.E.08", n: 39, label: "DV · Dashboard card",     title: "Dashboard card",     gloss: "Number + delta + sparkline + source line. The compressed unit of a recurring metric." },
    { ix: "19.E.09", n: 40, label: "DV · Sentiment wall",     title: "Sentiment wall",     gloss: "Grid of small cells, each carrying a verbatim phrase. Color marks cluster; size never varies." },
    { ix: "19.E.10", n: 41, label: "DV · Unit matrix",        title: "Unit matrix",        gloss: "100 = 100%. Every dot is one unit. Used when the count must feel countable." },
    { ix: "19.E.11", n: 42, label: "DV · Mega grid",          title: "Mega grid",          gloss: "A field of micro-cells reading as a single distribution at distance — Ikeda-mode." },
    { ix: "19.E.12", n: 43, label: "DV · Before / After",     title: "Before / After",     gloss: "Two charts, identical scale, paired horizontally. The change is the slide's argument." },
    { ix: "19.E.13", n: 44, label: "DV · ASCII histogram",    title: "ASCII histogram",    gloss: "Distribution rendered in mono characters — typographic rather than graphical." },
    { ix: "19.E.14", n: 45, label: "DV · Vertical bars",      title: "Vertical bars",      gloss: "Periods across the bottom, bars rising. Peak in the highlight color; rest in ink." },
    { ix: "19.E.15", n: 46, label: "DV · Composite dashboard",title: "Composite dashboard",gloss: "Multiple modules on one slide — sparingly, with hairline separators between cells." },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="19.E">Data visualization slides</SubHead>
      <h2 className="section-title">Fifteen charts. <br/>Each one a slide on its own.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Drawn from the data-visualization section of the master deck. Use these as a kit — not by re-drawing them, but by selecting the shape that fits the claim. Each card embeds the live slide.</p>
      <div className="specimen-grid">
        {viz.map(v => (
          <div key={v.ix} className="spec-card">
            <div className="pad">
              <span className="ix">{v.ix} · slide #{v.n}</span>
              <h4>{v.title}</h4>
              <p>{v.gloss}</p>
            </div>
            <div className="preview" style={{padding: 0, background:"#FAFAFA", aspectRatio:"16 / 9", minHeight: 0, position:"relative"}}>
              <MasterSlide n={v.n} label={v.label} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 19.B — Migration rationale */
function DeckMigration() {
  const why = [
    { ix:"01", t:"Consistency",       d:"One token sheet drives every slide. Change a colour or a heading style once; every deck in the org updates. PowerPoint's master slides drift the moment two people open the file." },
    { ix:"02", t:"AI efficiency",     d:"An LLM can read, edit, and ship HTML in seconds. A .pptx file is a zip of XML behind a binary wrapper — every edit round-trips through human hands. Live HTML closes that loop." },
    { ix:"03", t:"Motion · interaction", d:"Real animation, real charts, real linking. Scroll-driven reveals, hover states, embedded calls to claude.complete — none of which a slide deck can run." },
    { ix:"04", t:"Versioning",        d:"Decks live in git. Every change is a commit. Old versions are recoverable, not lost in someone's Downloads folder labelled v17_final_v3." },
    { ix:"05", t:"Distribution",      d:"A URL beats a 40MB attachment. The deck is the link; the link is dated; the link can be deprecated when the claim changes." },
    { ix:"06", t:"Accessibility",     d:"Real text, real headings, real focus order. Screen readers read it; search engines index it; print-to-PDF still works for the people who insist on paper." },
  ];
  const transition = [
    { from: "Slide master",       to: "Token sheet + components",  who: "Design system" },
    { from: ".pptx file",         to: "HTML route on bdg.io",       who: "Studio" },
    { from: "Email attachment",   to: "Versioned URL",              who: "Comms" },
    { from: "Manual chart edit",  to: "Data binding + replot",      who: "Data" },
    { from: "Speaker notes pane", to: "<script id=speaker-notes>",  who: "Author" },
    { from: "PDF export",         to: "Cmd+P · print stylesheet",   who: "Anyone" },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="19.B">Why we left PowerPoint</SubHead>
      <h2 className="section-title">Decks are a <span style={{background:"var(--c-yellow)", color:"#0A0A0C", padding:"0.04em 0.18em 0.10em", lineHeight: 0.88, display:"inline-block"}}>product surface,</span> not a file format.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Every deck the firm ships now starts as an HTML route in this system. The decision is not about aesthetics — it is about how the brand stays consistent, how AI can help us ship faster, and how decks earn the same engineering rigor as the rest of the product.</p>

      <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(240px, 1fr))", gap: 12, border:"0.8px solid var(--line)"}}>
        {why.map((w, i) => (
          <div key={w.ix} style={{padding:"20px 22px 22px", borderRight: i % 3 !== 2 ? "0.8px solid var(--line)" : "0", borderBottom:"0.8px solid var(--line)", display:"flex", flexDirection:"column", gap: 8}}>
            <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--signal)"}}>19.B.{w.ix}</span>
            <h4 style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 20, lineHeight: 1.05, letterSpacing:"-0.005em", margin: 0}}>{w.t}</h4>
            <p style={{fontFamily:"var(--f-body)", fontSize: 13, lineHeight: 1.5, color:"var(--ink-2)", margin: 0}}>{w.d}</p>
          </div>
        ))}
      </div>

      <div style={{marginTop: 32}}>
        <h3 className="block-title" style={{marginBottom: 12}}>The transition map</h3>
        <div style={{border:"0.8px solid var(--line)"}}>
          <div style={{display:"grid", gridTemplateColumns:"1fr 24px 1fr 160px", background:"var(--paper-2)", borderBottom:"0.8px solid var(--line)", padding:"10px 18px", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--ink-2)", gap: 12}}>
            <span>From — PowerPoint</span>
            <span/>
            <span>To — live HTML</span>
            <span style={{textAlign:"right"}}>Owner</span>
          </div>
          {transition.map((t, i) => (
            <div key={i} style={{display:"grid", gridTemplateColumns:"1fr 24px 1fr 160px", alignItems:"center", padding:"14px 18px", borderBottom: i < transition.length - 1 ? "0.8px solid var(--line)" : 0, gap: 12, fontFamily:"var(--f-body)", fontSize: 14, lineHeight: 1.4}}>
              <span style={{color:"var(--ink-2)", textDecoration:"line-through", textDecorationColor:"var(--c-red)"}}>{t.from}</span>
              <span style={{color:"var(--signal)", textAlign:"center", fontFamily:"var(--f-headline)", fontWeight: 650}}>→</span>
              <span style={{color:"var(--ink)", fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 14, letterSpacing:"-0.005em"}}>{t.to}</span>
              <span style={{textAlign:"right", fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.16em", textTransform:"uppercase", color:"var(--ink-3)"}}>{t.who}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* 19.C — Live deck specimens */
function LiveDecks() {
  const decks = [
    {
      ix: "01",
      title: "BDG Master Deck Template",
      summary: "The canonical slide framework. Eight archetypes, three surface modes, full keyboard nav, print-to-PDF, speaker notes hooked into the rail.",
      kind: "Field guide",
      file: "decks/master-deck.html",
      tags: ["8 archetypes", "1920 × 1080", "Speaker notes", "Keyboard nav"],
    },
    {
      ix: "02",
      title: "Agentic AI — Whitepaper",
      summary: "Long-form editorial rendered as a scrollable web deck. Same tokens, different reading posture. Reads as an essay; prints as a paper.",
      kind: "Editorial",
      file: "decks/whitepaper-agentic-ai.html",
      tags: ["Long-form", "Scroll-driven", "Embedded chart", "Print stylesheet"],
    },
  ];
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="19.C">Live deck specimens</SubHead>
      <h2 className="section-title">Two HTML decks. <br/>Open them, copy them, build on them.</h2>
      <p className="body" style={{marginBottom: 16, maxWidth:"60ch"}}>Both decks are published as routes in this system. Use them as reference, fork them as a starting frame, or open them directly in the room.</p>

      <div style={{display:"grid", gridTemplateColumns:"1fr", gap: 24}}>
        {decks.map(d => (
          <div key={d.ix} style={{border:"0.8px solid var(--line)", display:"grid", gridTemplateColumns:"minmax(0, 1fr) minmax(0, 1.6fr)", gap: 0}}>
            <div style={{padding:"28px 30px 30px", display:"flex", flexDirection:"column", gap: 14, borderRight:"0.8px solid var(--line)", background:"var(--paper-2)"}}>
              <span style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.18em", textTransform:"uppercase", color:"var(--signal)"}}>19.C.{d.ix} · {d.kind}</span>
              <h4 style={{fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 24, lineHeight: 1.05, letterSpacing:"-0.01em", margin: 0}}>{d.title}</h4>
              <p style={{fontFamily:"var(--f-body)", fontSize: 13, lineHeight: 1.5, color:"var(--ink-2)", margin: 0}}>{d.summary}</p>
              <div style={{display:"flex", flexWrap:"wrap", gap: 6, marginTop: 4}}>
                {d.tags.map(t => (
                  <span key={t} style={{fontFamily:"var(--f-mono)", fontSize: 10, letterSpacing:"0.14em", textTransform:"uppercase", padding:"4px 8px", border:"0.8px solid var(--line)", color:"var(--ink-2)"}}>{t}</span>
                ))}
              </div>
              <a href={d.file} target="_blank" rel="noreferrer" style={{marginTop:"auto", padding:"12px 16px 13px", border:"0.8px solid var(--ink)", color:"var(--ink)", fontFamily:"var(--f-headline)", fontWeight: 650, textTransform:"uppercase", fontSize: 12, letterSpacing:"0.01em", textDecoration:"none", display:"inline-flex", justifyContent:"space-between", alignItems:"center"}}>
                Open in new tab <span>↗</span>
              </a>
            </div>
            <div style={{position:"relative", background:"var(--ink)", overflow:"hidden", aspectRatio:"16 / 9"}}>
              <iframe
                src={d.file}
                title={d.title}
                loading="lazy"
                style={{position:"absolute", inset: 0, width:"100%", height:"100%", border: 0, background:"#fff"}}
              />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* 19.D — Slide archetypes (live iframes from master deck) */
function _OldDeckArchetypes_unused() {
  return null;
}

const MSG_CHECKS = [
  { id: "thesis",  txt: "Has a thesis you can quote in one sentence.",  sub: "No multi-clause hedges. The claim survives being read aloud." },
  { id: "tension", txt: "Names a tension — cause → tension → implication.", sub: "Without tension, the page reads as filler." },
  { id: "numbers", txt: "Carries at least one tabular number that anchors the claim.", sub: "Numbers are unrounded. No '~ish' figures." },
  { id: "owner",   txt: "Identifies an owner or a decision in the sentence stack.", sub: "The reader can name who is moving after they finish." },
  { id: "no-weak", txt: "Contains zero weakening words from the avoid ledger.", sub: "innovative · cutting-edge · synergies · empower · unlock · seamless · etc." },
  { id: "no-emoji",txt: "Carries no emoji, no exclamation marks, no sparkles.", sub: "The only ✓ and × are in do/don't tables." },
  { id: "active",  txt: "Voices the claim in present tense, active voice.", sub: "Decisions get made, not 'will be made'." },
  { id: "short",   txt: "No paragraph runs longer than four sentences.", sub: "If it does, split it. If you can't split it, cut it." },
];

const VIS_CHECKS = [
  { id: "rad",     txt: "Sharp corners — no border-radius except .cat-tag and .chip.", sub: "Cards, tiles, buttons, panels all at radius: 0." },
  { id: "hair",    txt: "Hairline borders only — 0.8px solid.", sub: "No thick borders. No double rules." },
  { id: "shadow",  txt: "No shadows. No drop-shadows. No glow.", sub: "The system reads as paper, not chrome." },
  { id: "grad",    txt: "No gradient backgrounds anywhere.", sub: "Brand colors as flat fills only. Color-mix is a generator tool, not a surface." },
  { id: "accent",  txt: "One signal accent per surface, maximum.", sub: "Orange in light; yellow in dark. Never both." },
  { id: "ratio",   txt: "70 paper / 20 ink / 10 color ratio across the page.", sub: "If color dominates, the page is decorative." },
  { id: "type",    txt: "Headlines in Instrument Sans Cond, weight 650, uppercase.", sub: "Body in Instrument Sans, mono in DM Mono." },
  { id: "ladder",  txt: "Body type snaps to one of L / M / S / XS — no overrides.", sub: "If a paragraph needs a different size, change the rung, don't override." },
  { id: "icon",    txt: "Zero raster icons, zero emoji, zero illustrative SVG.", sub: "Unicode glyphs only — → ↑ ↓ · — ≠." },
  { id: "img",     txt: "Imagery is generative, editorial, or absent.", sub: "No stock photography. No AI collage." },
];

function GuardrailsChapter() {
  const [msg, setMsg] = useState(() => Object.fromEntries(MSG_CHECKS.map(c => [c.id, false])));
  const [vis, setVis] = useState(() => Object.fromEntries(VIS_CHECKS.map(c => [c.id, false])));
  const [q, setQ] = useState("");

  const msgDone = Object.values(msg).filter(Boolean).length;
  const visDone = Object.values(vis).filter(Boolean).length;

  const leanInto = [
    "decision","diagnose","structure","cadence","evidence","tension","owner","inputs",
    "re-architect","implication","next","specific","honest","tabular","alignment",
    "system","claim","trade-off","threshold","deploy","operator","board","the room",
    "thesis","question","scaffolding","protocol","write-back","reconciliation",
  ];
  const avoid = [
    "innovative","cutting-edge","disruptive","game-changing","leading provider",
    "best-in-class","synergies","unlock","empower","leverage","seamless","robust",
    "world-class","mission-critical","next-gen","AI-powered","ecosystem","paradigm",
    "revolutionary","holistic","best-of-breed","frictionless","supercharge",
  ];

  const filter = (list) => list.filter(w => q.trim() === "" || w.toLowerCase().includes(q.toLowerCase()));
  const fLean = filter(leanInto);
  const fAvoid = filter(avoid);

  return (
    <section id="guardrails" className="chapter" data-screen-label="14 Guardrails">
      <ChapterHead num="14" kicker="Review" title={<>Guardrails.</>} meta={[["Messaging","08 checks"], ["Visual","10 checks"], ["Words","52 entries"]]}/>

      <div className="opener">
        <div>
          <SubHead ix="14.A">Position</SubHead>
          <p className="lede">The <span className="neq">checklist</span> before anything leaves the building. Two columns: tick the rules you've honored.</p>
          <p className="body" style={{marginTop: 18}}>Save the page; the ticks are yours. If the checklist isn't all green, the artifact isn't ready. There is no judgment column — the rule is the bar.</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            All ticks <span className="accent">green,</span> then ship. <br/>Otherwise, back to the desk.
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="14.B">Review checklists — messaging &amp; visual</SubHead>
        <div className="checklist">
          <div className="col">
            <div className="col-hed">Messaging <span className="ct">{msgDone} / {MSG_CHECKS.length}</span></div>
            <h3 className="col-title">Does the copy carry the brand?</h3>
            <div className="items">
              {MSG_CHECKS.map(c => (
                <div key={c.id} className="item" aria-checked={msg[c.id]} onClick={() => setMsg(p => ({...p, [c.id]: !p[c.id]}))}>
                  <span className="box"/>
                  <div>
                    <span className="txt">{c.txt}</span>
                    <span className="sub">{c.sub}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
          <div className="col">
            <div className="col-hed">Visual <span className="ct">{visDone} / {VIS_CHECKS.length}</span></div>
            <h3 className="col-title">Does the artifact carry the system?</h3>
            <div className="items">
              {VIS_CHECKS.map(c => (
                <div key={c.id} className="item" aria-checked={vis[c.id]} onClick={() => setVis(p => ({...p, [c.id]: !p[c.id]}))}>
                  <span className="box"/>
                  <div>
                    <span className="txt">{c.txt}</span>
                    <span className="sub">{c.sub}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>

      <div style={{marginTop: 64}}>
        <SubHead ix="14.C">Word ledger — searchable</SubHead>
        <h2 className="section-title">Words we lean into. <br/>Words we never use.</h2>
        <div className="ledger-search">
          <input placeholder="Search the ledger —" value={q} onChange={e => setQ(e.target.value)}/>
          <span className="tot">{fLean.length} · {fAvoid.length}</span>
          {q && <button className="clear" onClick={() => setQ("")}>Clear ×</button>}
        </div>
        <div className="ledger2">
          <div className="col lean">
            <div className="col-hed"><span className="tag">LEAN IN</span> {fLean.length} of {leanInto.length}</div>
            <div className="word-list">
              {fLean.length === 0 && <span className="empty">No matches in the lean column.</span>}
              {fLean.map(w => <span key={w} className={"word" + (q && w.toLowerCase().includes(q.toLowerCase()) && q.trim() !== "" ? " hit" : "")}>{w}</span>)}
            </div>
          </div>
          <div className="col avoid">
            <div className="col-hed"><span className="tag">AVOID</span> {fAvoid.length} of {avoid.length}</div>
            <div className="word-list">
              {fAvoid.length === 0 && <span className="empty">No matches in the avoid column.</span>}
              {fAvoid.map(w => <span key={w} className={"word" + (q && w.toLowerCase().includes(q.toLowerCase()) && q.trim() !== "" ? " hit" : "")}>{w}</span>)}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

function ComingSubsections({ items }) {
  return (
    <div style={{marginTop: 64}}>
      <SubHead ix="·">In progress — subsections in this chapter</SubHead>
      <div className="coming">
        {items.map((it, i) => (
          <div key={i} className="card">
            <div className="ix">{it.ix}</div>
            <div className="title">{it.t}</div>
            <div className="desc">{it.d}</div>
            <div className="status"><span className="dot"/> Drafted · review pending</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function ScaffoldChapter({ s, intro, sub }) {
  return (
    <section id={s.id} className="chapter" data-screen-label={`${s.num} ${s.label}`}>
      <ChapterHead num={s.num} kicker={s.kind} title={s.label + "."} meta={[["Status", s.status === "live" ? "Live" : "Draft"], ["Sections", String(sub.length).padStart(2,"0")], ["Owner", "Studio"]]}/>
      <div className="opener">
        <div>
          <SubHead ix={`${s.num}.A`}>Thesis</SubHead>
          <p className="lede">{intro}</p>
        </div>
        <div className="right">
          <div className="pullquote" style={{fontSize:"clamp(28px, 3vw, 44px)"}}>
            <span className="accent">{s.label}.</span> <br/>Documented as a system, not a mood board.
          </div>
        </div>
      </div>
      <ComingSubsections items={sub} />
    </section>
  );
}

/* --------------------------- PAGE FOOT ----------------------------------- */

function PageFoot({ prev, next, current, total }) {
  if (!current) return null;
  return (
    <nav className="page-foot" aria-label="Chapter navigation">
      <div className="page-foot-meta">
        <span className="pf-label">You are reading</span>
        <span className="pf-current">Chapter {current.num} — {current.label}</span>
        <span className="pf-of">{String(parseInt(current.num, 10) + 1).padStart(2,"0")} / {String(total).padStart(2,"0")}</span>
      </div>
      <div className="page-foot-pager">
        {prev ? (
          <a className="pf-link prev" href={`#${prev.id}`}>
            <span className="pf-dir">← Previous</span>
            <span className="pf-num">{prev.num}</span>
            <span className="pf-name">{prev.label}</span>
          </a>
        ) : <span className="pf-link disabled"><span className="pf-dir">— Start</span><span className="pf-name">of the system</span></span>}
        {next ? (
          <a className="pf-link next" href={`#${next.id}`}>
            <span className="pf-dir">Next →</span>
            <span className="pf-num">{next.num}</span>
            <span className="pf-name">{next.label}</span>
          </a>
        ) : <span className="pf-link disabled"><span className="pf-dir">End —</span><span className="pf-name">of the system</span></span>}
      </div>
    </nav>
  );
}

/* --------------------------- LEFT RAIL ----------------------------------- */

function Rail({ active, theme, setTheme, openSearch }) {
  return (
    <aside className="rail">
      <div className="rail-head">
        <a className="rail-mark" href="#cover">
          <span className="mark"/>
          <div className="lockup">
            <strong>bdg</strong>
            Brand System
          </div>
        </a>
        <div className="rail-actions">
          <button className="icon-btn" onClick={openSearch} aria-label="Search (cmd+K)" title="Search · ⌘K">⌕</button>
          <button className="icon-btn" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme" title="Toggle theme">
            {theme === "dark" ? "☼" : "☾"}
          </button>
        </div>
      </div>

      <ul className="toc-list" role="list">
        {/* Cover */}
        <li>
          <a
            className="toc-item"
            href="#cover"
            aria-current={active === "cover" ? "true" : undefined}
          >
            <span className="toc-num">00</span>
            <span className="toc-label">Home</span>
          </a>
        </li>
        {/* Grouped */}
        {GROUPS.map(g => (
          <React.Fragment key={g}>
            <li className="toc-group">{g}</li>
            {SECTIONS.filter(s => s.group === g).map(s => (
              <li key={s.id}>
                <a
                  className="toc-item"
                  href={`#${s.id}`}
                  aria-current={active === s.id ? "true" : undefined}
                >
                  <span className="toc-num">{s.num}</span>
                  <span className="toc-label">{s.label}</span>
                </a>
              </li>
            ))}
          </React.Fragment>
        ))}
      </ul>

      <div className="rail-foot">
        <div className="rail-foot-row">
          <span>Edition</span><span>I · 2026</span>
        </div>
        <div className="rail-foot-row">
          <span>Search</span><span className="kbd">⌘K</span>
        </div>
        <div className="rail-foot-row">
          <span>Theme</span><span>{theme === "dark" ? "Dark · Yellow" : "Light · Orange"}</span>
        </div>
      </div>
    </aside>
  );
}

/* --------------------------- MOBILE BAR + NAV ---------------------------- */

function MobileBar({ theme, setTheme, openSearch, openNav }) {
  return (
    <header className="mobile-bar">
      <a className="rail-mark" href="#cover" aria-label="bdg Brand System home">
        <span className="mark"/>
        <div className="lockup">
          <strong>bdg</strong>
          Brand System
        </div>
      </a>
      <div className="rail-actions">
        <button className="icon-btn" onClick={openSearch} aria-label="Search" title="Search · ⌘K">⌕</button>
        <button className="icon-btn" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme" title="Toggle theme">
          {theme === "dark" ? "☼" : "☾"}
        </button>
        <button className="icon-btn" onClick={openNav} aria-label="Open chapter index" title="Chapters">≡</button>
      </div>
    </header>
  );
}

function MobileNav({ open, onClose, active }) {
  useEffect(() => {
    if (!open) return;
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = prevOverflow; };
  }, [open]);

  return (
    <div
      className="mobile-nav"
      data-open={open ? "true" : "false"}
      role="dialog"
      aria-modal="true"
      aria-label="Chapter index"
      aria-hidden={!open}
    >
      <header className="mobile-nav-head">
        <a className="rail-mark" href="#cover" onClick={onClose}>
          <span className="mark"/>
          <div className="lockup">
            <strong>bdg</strong>
            Chapters
          </div>
        </a>
        <button className="icon-btn" onClick={onClose} aria-label="Close chapter index" title="Close">×</button>
      </header>
      <ul className="toc-list mobile-nav-list" role="list">
        <li>
          <a
            className="toc-item"
            href="#cover"
            aria-current={active === "cover" ? "true" : undefined}
            onClick={onClose}
            tabIndex={open ? 0 : -1}
          >
            <span className="toc-num">00</span>
            <span className="toc-label">Home</span>
          </a>
        </li>
        {GROUPS.map(g => (
          <React.Fragment key={g}>
            <li className="toc-group">{g}</li>
            {SECTIONS.filter(s => s.group === g).map(s => (
              <li key={s.id}>
                <a
                  className="toc-item"
                  href={`#${s.id}`}
                  aria-current={active === s.id ? "true" : undefined}
                  onClick={onClose}
                  tabIndex={open ? 0 : -1}
                >
                  <span className="toc-num">{s.num}</span>
                  <span className="toc-label">{s.label}</span>
                </a>
              </li>
            ))}
          </React.Fragment>
        ))}
      </ul>
    </div>
  );
}

/* --------------------------- SEARCH -------------------------------------- */

/* Search corpus: hand-curated index across the whole system.
   Each row carries `id` (navigation target), `ix` (visual index), `label`,
   `kind` (badge), optional `path` (breadcrumb), `tags` (keywords for fuzzy
   match), and optional `file` (asset path shown in monospace).
   Keep entries terse; the matcher scores label > tags > file > kind. */
const SEARCH_KINDS = ["Section", "Color", "Type", "Component", "Motion", "Template", "Phrase", "Audience", "File", "Tool"];

const SEARCH_CORPUS = [
  /* --- Sections (20) ----------------------------------------------------- */
  ...SECTIONS.map(s => ({
    id: s.id, ix: s.num, label: s.label, kind: "Section",
    path: s.group || "Manifesto",
    tags: [s.kind?.toLowerCase(), s.group?.toLowerCase(), s.label.toLowerCase()].filter(Boolean),
  })),

  /* --- Brand hues (6) ---------------------------------------------------- */
  { id: "color", ix: "06.B", label: "Blue",   kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#3236FF", tags: ["color", "brand", "hue", "primary", "blue", "intent"] },
  { id: "color", ix: "06.B", label: "Green",  kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#00D190", tags: ["color", "brand", "hue", "green", "positive", "alignment"] },
  { id: "color", ix: "06.B", label: "Yellow", kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#FFE241", tags: ["color", "brand", "hue", "yellow", "alert", "signal", "dark"] },
  { id: "color", ix: "06.B", label: "Orange", kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#F55910", tags: ["color", "brand", "hue", "orange", "signal", "accent", "light"] },
  { id: "color", ix: "06.B", label: "Red",    kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#EF353E", tags: ["color", "brand", "hue", "red", "negative", "contradiction"] },
  { id: "color", ix: "06.B", label: "Purple", kind: "Color", path: "Visual Identity › Color › Brand hues",   file: "#D7B3FF", tags: ["color", "brand", "hue", "purple", "hypothetical", "soft"] },

  /* --- Neutral ladders --------------------------------------------------- */
  { id: "color", ix: "06.A", label: "Ink ladder · 11 stops",   kind: "Color", path: "Visual Identity › Color › Neutrals", tags: ["neutral", "ink", "dark", "ladder", "stops", "scale"] },
  { id: "color", ix: "06.A", label: "Paper ladder · 11 stops", kind: "Color", path: "Visual Identity › Color › Neutrals", tags: ["neutral", "paper", "light", "ladder", "stops", "scale"] },
  { id: "color", ix: "06.D", label: "Light + Dark schemes",    kind: "Color", path: "Visual Identity › Color › Schemes",  tags: ["scheme", "light", "dark", "mode", "theme"] },
  { id: "color", ix: "06.E", label: "Pairings & WCAG contrast", kind: "Color", path: "Visual Identity › Color › Pairings", tags: ["contrast", "wcag", "pairing", "accessibility", "ratio"] },

  /* --- Typefaces (4) ----------------------------------------------------- */
  { id: "typography", ix: "07.A", label: "Instrument Sans Condensed", kind: "Type", path: "Visual Identity › Typography › Pairing", file: "fonts/InstrumentSansCondensed.ttf", tags: ["headline", "display", "uppercase", "condensed", "font", "typeface"] },
  { id: "typography", ix: "07.A", label: "Instrument Sans",           kind: "Type", path: "Visual Identity › Typography › Pairing", file: "fonts/InstrumentSans-Regular.woff2", tags: ["body", "sans", "font", "typeface", "variable"] },
  { id: "typography", ix: "07.A", label: "DM Mono",                   kind: "Type", path: "Visual Identity › Typography › Pairing", file: "fonts/DMMono-Regular.woff2", tags: ["mono", "monospace", "label", "chrome", "ticker", "font"] },
  { id: "typography", ix: "07.A", label: "Instrument Serif Italic",   kind: "Type", path: "Visual Identity › Typography › Pairing", file: "fonts/InstrumentSerif-Italic.woff2", tags: ["serif", "italic", "pullquote", "font"] },
  { id: "typography", ix: "07.C", label: "Body ladder · 4 rungs",     kind: "Type", path: "Visual Identity › Typography › Body",    tags: ["ladder", "body", "scale", "rungs", "34", "26", "22", "18"] },
  { id: "typography", ix: "07.B", label: "Heading scale · 6 sizes",   kind: "Type", path: "Visual Identity › Typography › Heading", tags: ["heading", "scale", "display", "h1", "title"] },

  /* --- Components (12 families) ----------------------------------------- */
  { id: "components", ix: "09.A", label: "Buttons",      kind: "Component", path: "Visual Identity › UI Components", tags: ["button", "solid", "outline", "ghost", "cta"] },
  { id: "components", ix: "09.B", label: "Inputs",       kind: "Component", path: "Visual Identity › UI Components", tags: ["input", "form", "text", "email", "password", "field"] },
  { id: "components", ix: "09.C", label: "Selection",    kind: "Component", path: "Visual Identity › UI Components", tags: ["checkbox", "radio", "select", "form"] },
  { id: "components", ix: "09.D", label: "Toggles",      kind: "Component", path: "Visual Identity › UI Components", tags: ["toggle", "switch", "form"] },
  { id: "components", ix: "09.E", label: "Tags & chips", kind: "Component", path: "Visual Identity › UI Components", tags: ["tag", "chip", "pill", "label"] },
  { id: "components", ix: "09.F", label: "Tooltips",     kind: "Component", path: "Visual Identity › UI Components", tags: ["tooltip", "hint", "popover"] },
  { id: "components", ix: "09.G", label: "Tabs & filters", kind: "Component", path: "Visual Identity › UI Components", tags: ["tab", "filter", "segmented"] },
  { id: "components", ix: "09.H", label: "Cards & badges", kind: "Component", path: "Visual Identity › UI Components", tags: ["card", "badge", "surface"] },
  { id: "components", ix: "09.I", label: "Alerts",       kind: "Component", path: "Visual Identity › UI Components", tags: ["alert", "warning", "error", "notice"] },
  { id: "components", ix: "09.J", label: "Breadcrumbs & pager", kind: "Component", path: "Visual Identity › UI Components", tags: ["breadcrumb", "pager", "pagination", "nav"] },
  { id: "components", ix: "09.K", label: "Modals",       kind: "Component", path: "Visual Identity › UI Components", tags: ["modal", "dialog", "overlay", "sheet"] },
  { id: "components", ix: "09.L", label: "Tables",       kind: "Component", path: "Visual Identity › UI Components", tags: ["table", "data", "grid", "row"] },

  /* --- Motion patterns (10) --------------------------------------------- */
  { id: "motion", ix: "08.B", label: "Number reveal",   kind: "Motion", path: "Visual Identity › Motion", tags: ["count", "number", "reveal", "stat"] },
  { id: "motion", ix: "08.B", label: "Chart unveil",    kind: "Motion", path: "Visual Identity › Motion", tags: ["chart", "unveil", "draw", "viz"] },
  { id: "motion", ix: "08.B", label: "Hairline draw",   kind: "Motion", path: "Visual Identity › Motion", tags: ["line", "draw", "stroke", "hairline"] },
  { id: "motion", ix: "08.B", label: "Stagger reveal",  kind: "Motion", path: "Visual Identity › Motion", tags: ["stagger", "list", "reveal", "sequence"] },
  { id: "motion", ix: "08.B", label: "Mask wipe",       kind: "Motion", path: "Visual Identity › Motion", tags: ["mask", "wipe", "reveal"] },
  { id: "motion", ix: "08.B", label: "Network pulse",   kind: "Motion", path: "Visual Identity › Motion", tags: ["network", "pulse", "node", "graph"] },
  { id: "motion", ix: "08.B", label: "Progress fill",   kind: "Motion", path: "Visual Identity › Motion", tags: ["progress", "fill", "bar"] },
  { id: "motion", ix: "08.B", label: "Delta tick",      kind: "Motion", path: "Visual Identity › Motion", tags: ["delta", "tick", "change", "arrow"] },
  { id: "motion", ix: "08.B", label: "Row enter",       kind: "Motion", path: "Visual Identity › Motion", tags: ["row", "enter", "table"] },
  { id: "motion", ix: "08.B", label: "Highlight sweep", kind: "Motion", path: "Visual Identity › Motion", tags: ["highlight", "sweep", "italic"] },
  { id: "motion", ix: "08.A", label: "Easing curve · cubic-bezier(0.22, 1, 0.36, 1)", kind: "Motion", path: "Visual Identity › Motion › Curve", tags: ["easing", "curve", "bdg-curve", "cubic-bezier"] },

  /* --- Templates (6) ---------------------------------------------------- */
  { id: "templates", ix: "14.A", label: "Hero / Thesis",       kind: "Template", path: "Application › Templates", tags: ["template", "hero", "thesis", "landing"] },
  { id: "templates", ix: "14.B", label: "Case study · stat row", kind: "Template", path: "Application › Templates", tags: ["template", "case", "study", "stat", "row"] },
  { id: "templates", ix: "14.C", label: "Article",             kind: "Template", path: "Application › Templates", tags: ["template", "article", "editorial", "long-form"] },
  { id: "templates", ix: "14.D", label: "Executive bio",       kind: "Template", path: "Application › Templates", tags: ["template", "bio", "executive", "person"] },
  { id: "templates", ix: "14.E", label: "Event slide · 1920×1080", kind: "Template", path: "Application › Templates", tags: ["template", "event", "slide", "stage"] },
  { id: "templates", ix: "14.F", label: "Email signature",     kind: "Template", path: "Application › Templates", tags: ["template", "email", "signature", "plain-text"] },

  /* --- Slide archetypes (8) --------------------------------------------- */
  { id: "decks", ix: "19.A", label: "Slide · Cover",        kind: "Template", path: "Application › Presentations", tags: ["slide", "cover", "deck", "archetype"] },
  { id: "decks", ix: "19.A", label: "Slide · Section",      kind: "Template", path: "Application › Presentations", tags: ["slide", "section", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Title + body", kind: "Template", path: "Application › Presentations", tags: ["slide", "title", "body", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Big stat",     kind: "Template", path: "Application › Presentations", tags: ["slide", "stat", "number", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Two column",   kind: "Template", path: "Application › Presentations", tags: ["slide", "two-col", "compare", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Quote",        kind: "Template", path: "Application › Presentations", tags: ["slide", "quote", "pullquote", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Full-bleed image", kind: "Template", path: "Application › Presentations", tags: ["slide", "image", "bleed", "deck"] },
  { id: "decks", ix: "19.A", label: "Slide · Data table",   kind: "Template", path: "Application › Presentations", tags: ["slide", "table", "data", "deck"] },

  /* --- Voice phrases ---------------------------------------------------- */
  { id: "voice",     ix: "03.B", label: '"Honest data. Better decisions. Real impact."', kind: "Phrase", path: "Positioning › Messaging › Tagline", tags: ["tagline", "promise", "honesty"] },
  { id: "voice",     ix: "03.B", label: '"bdg optimizes the question, not the answer."', kind: "Phrase", path: "Positioning › Messaging › Pillars", tags: ["positioning", "differentiation", "question"] },
  { id: "voice",     ix: "03.G", label: '"Reporting tells you what happened. Decisions need what’s next."', kind: "Phrase", path: "Positioning › Messaging › Phrase library", tags: ["phrase", "reporting", "decisions"] },
  { id: "voice",     ix: "03.G", label: '"The decision is the unit of value."', kind: "Phrase", path: "Positioning › Messaging › Phrase library", tags: ["phrase", "decision", "value"] },
  { id: "voice",     ix: "03.C", label: "Messaging linter",                kind: "Tool",   path: "Positioning › Messaging › Linter", tags: ["linter", "tool", "check", "voice"] },
  { id: "voice",     ix: "03.H", label: "Word ledger · lean-into / avoid", kind: "Tool",   path: "Positioning › Messaging › Ledger", tags: ["word", "ledger", "vocabulary", "lean", "avoid"] },

  /* --- Audience personas (4) ------------------------------------------- */
  { id: "audiences", ix: "18.A", label: "CFO",       kind: "Audience", path: "Application › Voice in Action", tags: ["cfo", "finance", "audience", "persona"] },
  { id: "audiences", ix: "18.A", label: "Controller", kind: "Audience", path: "Application › Voice in Action", tags: ["controller", "audience", "persona"] },
  { id: "audiences", ix: "18.A", label: "IT lead",    kind: "Audience", path: "Application › Voice in Action", tags: ["it", "tech", "audience", "persona"] },
  { id: "audiences", ix: "18.A", label: "CEO",       kind: "Audience", path: "Application › Voice in Action", tags: ["ceo", "executive", "audience", "persona"] },

  /* --- Foundation primitives ------------------------------------------- */
  { id: "foundation", ix: "01.C", label: "Sage + Hero · archetype",     kind: "Phrase", path: "Positioning › Brand Foundation › Archetype", tags: ["archetype", "sage", "hero", "strategy"] },
  { id: "foundation", ix: "01.D", label: "Mission & promise",           kind: "Phrase", path: "Positioning › Brand Foundation › Promise",    tags: ["mission", "promise", "purpose"] },
  { id: "foundation", ix: "01.B", label: "Category · what we are",      kind: "Phrase", path: "Positioning › Brand Foundation › Category",   tags: ["category", "definition", "position"] },

  /* --- Files / assets (downloadable items carry a `download` action) ------ */
  { id: "identity",   ix: "05",  label: "bdg logo · SVG",  kind: "File", path: "Visual Identity › Logo › Downloads",         file: "assets/logo.svg",     tags: ["logo", "mark", "svg", "asset", "brand", "download"],
    download: { kind: "fetch", name: "bdg-mark.svg", url: "../assets/logo.svg" } },
  { id: "identity",   ix: "05",  label: "bdg logo · PNG 1024", kind: "File", path: "Visual Identity › Logo › Downloads",      file: "bdg-mark-1024.png",   tags: ["logo", "mark", "png", "asset", "raster", "download"],
    download: { kind: "png", name: "bdg-mark-1024.png", size: 1024 } },
  { id: "identity",   ix: "05",  label: "bdg favicon · 32px", kind: "File", path: "Visual Identity › Logo › Downloads",       file: "bdg-mark-32.png",     tags: ["favicon", "icon", "png", "32", "download"],
    download: { kind: "png", name: "bdg-mark-32.png", size: 32, padding: 0.04 } },
  { id: "identity",   ix: "05",  label: "bdg app icon · 180px", kind: "File", path: "Visual Identity › Logo › Downloads",     file: "bdg-mark-180.png",    tags: ["app", "icon", "ios", "png", "180", "download"],
    download: { kind: "png", name: "bdg-mark-180.png", size: 180 } },
  { id: "decks",      ix: "19",  label: "Master deck",     kind: "File", path: "Application › Presentations",     file: "site/decks/master-deck.html", tags: ["deck", "html", "presentation", "master"] },
  { id: "decks",      ix: "19",  label: "Whitepaper · agentic AI", kind: "File", path: "Application › Presentations", file: "site/decks/whitepaper-agentic-ai.html", tags: ["whitepaper", "agentic", "ai", "html"] },
  { id: "typography", ix: "07",  label: "Tokens · CSS",    kind: "File", path: "Tokens › Downloads", file: "colors_and_type.css", tags: ["tokens", "css", "drop-in", "colors", "type", "download"],
    download: { kind: "fetch", name: "bdg-tokens.css", url: "../colors_and_type.css" } },
  { id: "typography", ix: "07",  label: "Tokens · JSON",   kind: "File", path: "Tokens › Downloads", file: "bdg-tokens.json",     tags: ["tokens", "json", "dtcg", "download"],
    download: { kind: "json" } },
  { id: "typography", ix: "07",  label: "Tokens · Tailwind preset", kind: "File", path: "Tokens › Downloads", file: "tailwind-bdg.js", tags: ["tokens", "tailwind", "preset", "js", "download"],
    download: { kind: "tailwind" } },
  { id: "components", ix: "09",  label: "site.css",             kind: "File", path: "Source", file: "site/site.css", tags: ["css", "styles", "site"] },
  { id: "components", ix: "09",  label: "app.jsx",              kind: "File", path: "Source", file: "site/app.jsx", tags: ["jsx", "react", "app", "components"] },

  /* --- Aliases for common queries --------------------------------------- */
  { id: "edge",       ix: "04",  label: "What makes bdg different",   kind: "Phrase",  path: "Positioning › Competitive Edge", tags: ["edge", "differentiation", "moat", "position"] },
  { id: "market",     ix: "02",  label: "Four competitor clusters",   kind: "Phrase",  path: "Positioning › Market",           tags: ["market", "competitors", "clusters"] },
  { id: "layout",     ix: "11",  label: "12-column grid · gutters",   kind: "Component", path: "Visual Identity › Layout",     tags: ["grid", "12-column", "gutter", "spacing"] },
  { id: "layout",     ix: "11",  label: "Spacing scale · 10 stops",   kind: "Component", path: "Visual Identity › Layout",     tags: ["spacing", "scale", "rhythm"] },
];

/* Build photo rows for the global ⌘K corpus from the live, override-aware
   photo list. Each row carries a `thumb` URL so the Search component can
   render a small preview alongside the label. */
function photosToSearchRows(photos) {
  return photos.map(p => ({
    id: "imagery",
    ix: "07.E",
    label: p.name,
    kind: "File",
    path: `Visual Identity › Imagery › ${p.industry}`,
    file: p.draft ? `(draft) ${p.file || p.name}` : p.file,
    tags: ["photo", p.industry.toLowerCase(), ...(p.tags || [])],
    thumb: photoThumbUrl(p),
    download: p.draft
      ? null  /* drafts aren't on disk yet — user exports first */
      : { kind: "fetch", name: photoSlug(p.name), url: photoFullUrl(p) },
  }));
}

/* Dispatch a download spec from any corpus row that carries one.
   Spec shapes: { kind: "fetch", name, url } | { kind: "png", name, size, padding? }
                | { kind: "json" } | { kind: "tailwind" }                          */
async function runDownloadSpec(spec) {
  switch (spec.kind) {
    case "fetch":    return fetchAndDownload(spec.name, spec.url);
    case "png":      return downloadLogoPng(spec.size, spec.name, { padding: spec.padding });
    case "json":     return downloadBlob("bdg-tokens.json", new Blob([JSON.stringify(buildTokensJson(), null, 2)], { type: "application/json" }));
    case "tailwind": return downloadBlob("tailwind-bdg.js", new Blob([buildTailwindPreset()], { type: "application/javascript" }));
    default: throw new Error("Unknown download kind: " + spec.kind);
  }
}

/* Fuzzy-ish scoring: label match dominates; tags / file / path tier under it. */
function scoreSearch(row, qLower) {
  if (!qLower) return 1;
  const label = row.label.toLowerCase();
  if (label === qLower) return 1000;
  if (label.startsWith(qLower)) return 500;
  let score = 0;
  if (label.includes(qLower)) score += 200;
  if (row.tags?.some(t => t && t.startsWith(qLower))) score += 120;
  if (row.tags?.some(t => t && t.includes(qLower))) score += 80;
  if (row.file?.toLowerCase().includes(qLower)) score += 60;
  if (row.path?.toLowerCase().includes(qLower)) score += 40;
  if (row.kind.toLowerCase().includes(qLower)) score += 30;
  if (row.ix.toLowerCase().includes(qLower)) score += 25;
  return score;
}

function Search({ open, onClose }) {
  const store = usePhotoStore();
  const [q, setQ] = useState("");
  const [active, setActive] = useState(0);
  const [kindFilter, setKindFilter] = useState("All");
  const [dlBusy, setDlBusy] = useState(null);
  const [dlDone, setDlDone] = useState(null);
  const inputRef = useRef(null);
  useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 30); }, [open]);

  /* Live corpus = static rows + photo rows from the store (override-aware). */
  const corpus = useMemo(
    () => [...SEARCH_CORPUS, ...photosToSearchRows(store.photos)],
    [store.photos]
  );

  const results = useMemo(() => {
    const v = q.trim().toLowerCase();
    const scoped = kindFilter === "All" ? corpus : corpus.filter(r => r.kind === kindFilter);
    if (!v) return scoped.slice(0, 10);
    return scoped
      .map(r => ({ r, s: scoreSearch(r, v) }))
      .filter(x => x.s > 0)
      .sort((a, b) => b.s - a.s)
      .slice(0, 14)
      .map(x => x.r);
  }, [q, kindFilter, corpus]);

  /* Kind counts so chips can show totals (Linear/Stripe pattern). */
  const kindCounts = useMemo(() => {
    const counts = { All: corpus.length };
    for (const k of SEARCH_KINDS) counts[k] = corpus.filter(r => r.kind === k).length;
    return counts;
  }, [corpus]);

  useEffect(() => { setActive(0); }, [q, open, kindFilter]);

  const onKey = (e) => {
    if (e.key === "Escape") { e.preventDefault(); onClose(); return; }
    if (e.key === "ArrowDown") { e.preventDefault(); setActive(a => Math.min(a + 1, results.length - 1)); }
    if (e.key === "ArrowUp")   { e.preventDefault(); setActive(a => Math.max(a - 1, 0)); }
    if (e.key === "Enter" && results[active]) {
      e.preventDefault();
      window.location.hash = "#" + results[active].id;
      onClose();
    }
  };

  /* Highlight the matching substring in a label (Linear/Raycast pattern). */
  const highlight = useCallback((text, query) => {
    if (!query) return text;
    const i = text.toLowerCase().indexOf(query.toLowerCase());
    if (i < 0) return text;
    return (
      <>
        {text.slice(0, i)}
        <mark className="sr-hl">{text.slice(i, i + query.length)}</mark>
        {text.slice(i + query.length)}
      </>
    );
  }, []);

  if (!open) return null;
  return (
    <div className="search-shade" onClick={onClose}>
      <div className="search-panel" onClick={e => e.stopPropagation()} role="dialog" aria-label="Search the system">
        <div className="search-input-wrap">
          <span className="search-glyph" aria-hidden="true">⌕</span>
          <input
            ref={inputRef}
            value={q}
            onChange={e => setQ(e.target.value)}
            onKeyDown={onKey}
            placeholder="Search chapters, tokens, components, files —"
            aria-label="Search"
          />
          <span className="search-kbd kbd">esc</span>
        </div>

        <div className="search-chips" role="tablist" aria-label="Filter by type">
          {["All", ...SEARCH_KINDS].map(k => (
            <button
              key={k}
              role="tab"
              aria-selected={kindFilter === k}
              className="search-chip"
              data-active={kindFilter === k}
              onClick={() => setKindFilter(k)}
            >
              {k}
              <span className="search-chip-count">{kindCounts[k] ?? 0}</span>
            </button>
          ))}
        </div>

        <div className="search-meta">
          <span className="search-count">{results.length} {results.length === 1 ? "result" : "results"}</span>
          {q.trim() && <span className="search-query">for <em>{q.trim()}</em></span>}
        </div>

        <div className="search-results">
          {results.length === 0 && (
            <div className="sr-empty">
              No matches for <em>{q}</em>. Try a hex code, a chapter number, a file extension (woff2, svg, html), or a tag (color, motion, template).
            </div>
          )}
          {results.map((r, i) => {
            const rowKey = `${r.id}-${r.ix}-${i}`;
            const dlState = dlBusy === rowKey ? "busy" : dlDone === rowKey ? "done" : "idle";
            return (
              <a
                key={rowKey}
                className="sr-row"
                data-active={i === active}
                data-has-thumb={r.thumb ? "true" : undefined}
                href={"#" + r.id}
                onClick={onClose}
                onMouseEnter={() => setActive(i)}
              >
                <span className="ix">{r.ix}</span>
                {r.thumb && (
                  <span className="sr-thumb" aria-hidden="true">
                    <img src={r.thumb} alt="" loading="lazy" decoding="async" />
                  </span>
                )}
                <span className="sr-main">
                  <span className="lbl">{highlight(r.label, q.trim())}</span>
                  {r.path && <span className="sr-path">{r.path}</span>}
                  {r.file && <span className="sr-file">{r.file}</span>}
                </span>
                <span className="sr-meta">
                  {r.tags?.slice(0, 2).map(t => (
                    <span key={t} className="sr-tag">{t}</span>
                  ))}
                  {r.download && (
                    <button
                      type="button"
                      className="sr-dl"
                      data-state={dlState}
                      onClick={async (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        setDlBusy(rowKey);
                        try {
                          await runDownloadSpec(r.download);
                          setDlDone(rowKey);
                          setTimeout(() => setDlDone(d => d === rowKey ? null : d), 1800);
                        } catch (err) {
                          console.warn("[bdg] search download failed:", err);
                        } finally {
                          setDlBusy(b => b === rowKey ? null : b);
                        }
                      }}
                      aria-label={`Download ${r.download.name || r.file}`}
                      title={`Download ${r.download.name || r.file}`}
                    >
                      <span aria-hidden="true">{dlState === "done" ? "✓" : dlState === "busy" ? "·" : "⤓"}</span>
                      <span>Download</span>
                    </button>
                  )}
                  <span className="kind" data-kind={r.kind}>{r.kind}</span>
                </span>
              </a>
            );
          })}
        </div>

        <footer className="search-foot">
          <span><span className="kbd">↑</span><span className="kbd">↓</span> navigate</span>
          <span><span className="kbd">↵</span> open</span>
          <span><span className="kbd">esc</span> close</span>
          <span className="search-foot-spacer"/>
          <span>{SEARCH_CORPUS.length} indexed</span>
        </footer>
      </div>
    </div>
  );
}

/* --------------------------- LAB LAZY MOUNT ------------------------------ */
/* Section 20 (Motion Lab) lives in `lab.jsx` and depends on p5.js. Both are
   injected the first time the user navigates to #lab; visiting any other
   section never pays for the lab bundle. The lab module self-registers on
   `window.__bdgLab` once Babel-standalone finishes transforming it. */

const LAB_CDN_DEPS = [
  { id: "bdg-cdn-p5", src: "https://unpkg.com/p5@1.10.0/lib/p5.min.js" },
];

function injectScriptOnce(src, id) {
  return new Promise((resolve, reject) => {
    if (id && document.getElementById(id)) return resolve();
    const s = document.createElement("script");
    if (id) s.id = id;
    s.src = src;
    s.async = true;
    s.crossOrigin = "anonymous";
    s.onload = () => resolve();
    s.onerror = () => reject(new Error("Failed to load " + src));
    document.head.appendChild(s);
  });
}

let _labPromise = null;
function loadLabModule() {
  if (window.__bdgLab) return Promise.resolve(window.__bdgLab);
  if (_labPromise) return _labPromise;
  _labPromise = (async () => {
    for (const dep of LAB_CDN_DEPS) {
      await injectScriptOnce(dep.src, dep.id);
    }
    if (document.getElementById("bdg-lab-script")) {
      /* Already injected; just wait for it to register. */
    } else {
      if (!window.Babel || !window.Babel.transform) {
        throw new Error("Babel-standalone is not loaded");
      }
      /* Fetch + transform + inject ourselves so we never call
         Babel.transformScriptTags() — that would re-fetch and re-execute the
         original app.jsx <script type="text/babel"> tag, mounting <App />
         twice. */
      const res = await fetch("lab.jsx?v=" + Date.now());
      if (!res.ok) throw new Error("Failed to fetch lab.jsx: " + res.status);
      const source = await res.text();
      const transformed = window.Babel.transform(source, {
        presets: ["react"],
        filename: "lab.jsx",
      }).code;
      const s = document.createElement("script");
      s.id = "bdg-lab-script";
      s.textContent = transformed;
      document.body.appendChild(s);
    }
    return new Promise((res, rej) => {
      const start = Date.now();
      const tick = () => {
        if (window.__bdgLab) return res(window.__bdgLab);
        if (Date.now() - start > 12000) return rej(new Error("Motion Lab load timed out"));
        setTimeout(tick, 32);
      };
      tick();
    });
  })().catch(err => {
    _labPromise = null;
    throw err;
  });
  return _labPromise;
}

/* Error boundary — isolates lab failures from the rest of the app so a
   thrown setup/draw inside any experiment can't unmount the whole site. */
class LabBoundary extends React.Component {
  constructor(props) { super(props); this.state = { err: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err) { console.error("[bdg-lab] caught:", err); }
  render() {
    if (this.state.err) {
      return (
        <section className="lab-empty">
          <p className="bdg-eyebrow">Motion Lab · runtime error</p>
          <p className="bdg-body">{String(this.state.err.message || this.state.err)}</p>
          <p className="bdg-body-xs bdg-muted" style={{marginTop: 16}}>
            Open the browser console for the stack. <a href="#lab" onClick={() => this.setState({ err: null })}>Reset</a>
          </p>
        </section>
      );
    }
    return this.props.children;
  }
}

function LabSectionMount() {
  const [mod, setMod] = useState(() => window.__bdgLab || null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    if (mod) return;
    let alive = true;
    loadLabModule()
      .then(m => { if (alive) setMod(m); })
      .catch(e => { if (alive) setErr(e); });
    return () => { alive = false; };
  }, [mod]);
  if (err) {
    return (
      <section className="lab-empty">
        <p className="bdg-eyebrow">Motion Lab · load failed</p>
        <p className="bdg-body">{String(err.message || err)}</p>
      </section>
    );
  }
  if (!mod) {
    return (
      <section className="lab-loading" aria-live="polite">
        <p className="bdg-eyebrow">20 · Motion Lab</p>
        <p className="bdg-body bdg-body-m">Loading runtime…</p>
        <div className="lab-loading-bar" aria-hidden="true"><span /></div>
      </section>
    );
  }
  const Inner = mod.LabRoute;
  return <LabBoundary><Inner /></LabBoundary>;
}

/* --------------------------- APP ROOT ------------------------------------ */

function App() {
  const ids = SECTIONS.map(s => s.id);
  const route = useRoute("cover");
  /* Sub-routes like `lab/01-type-particle-field` resolve to the parent
     section ("lab") for chrome/nav purposes; the section component reads
     the full hash to pick its sub-view. */
  const baseRoute = route.split("/")[0];
  const active = SECTIONS.find(s => s.id === baseRoute) ? baseRoute : "cover";
  const activeIndex = SECTIONS.findIndex(s => s.id === active);
  const prev = activeIndex > 0 ? SECTIONS[activeIndex - 1] : null;
  const next = activeIndex < SECTIONS.length - 1 ? SECTIONS[activeIndex + 1] : null;
  const p = useReadingProgress();
  const [theme, setTheme] = useTheme();
  const { last, copy } = useClipboard();
  const [searchOpen, setSearchOpen] = useState(false);
  const [navOpen, setNavOpen] = useState(false);

  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        setSearchOpen(s => !s);
      }
      if (e.key === "Escape") { setSearchOpen(false); setNavOpen(false); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  return (
    <div className="app">
      <Rail active={active} theme={theme} setTheme={setTheme} openSearch={() => setSearchOpen(true)} />
      <MobileBar
        theme={theme}
        setTheme={setTheme}
        openSearch={() => setSearchOpen(true)}
        openNav={() => setNavOpen(true)}
      />

      <main className="stream" key={active}>
        <div className="progress" style={{ "--p": p + "%" }} aria-hidden="true" />

        {(() => {
          const CHAPTERS = {
            cover:      () => <CoverChapter />,
            foundation: () => <FoundationChapter />,
            voice:      () => <VoiceChapter onCopy={copy} />,
            identity:   () => <IdentityChapter onCopy={copy} />,
            color:      () => <ColorChapter onCopy={copy} />,
            typography: () => <TypographyChapter onCopy={copy} />,
            icons:      () => <IconographyChapter />,
            imagery:    () => <ImageryChapter />,
            motion:     () => <MotionChapter onCopy={copy} />,
            components: () => <ComponentsChapter />,
            dataviz:    () => <DataVizChapter />,
            layout:     () => <LayoutChapter onCopy={copy} />,
            templates:  () => <TemplatesChapter />,
            audiences:  () => <AudiencesChapter />,
            market:     () => <MarketChapter />,
            edge:       () => <EdgeChapter />,
            social:     () => <SocialChapter />,
            print:      () => <PrintChapter />,
            digital:    () => <DigitalChapter />,
            decks:      () => <DecksChapter />,
            lab:        () => <LabSectionMount />,
          };
          const Comp = CHAPTERS[active];
          if (Comp) return Comp();
          const s = SECTIONS.find(x => x.id === active);
          if (s) return <ScaffoldChapter s={s} intro={s.label} sub={[]} />;
          return <CoverChapter />;
        })()}

        <PageFoot prev={prev} next={next} current={SECTIONS[activeIndex]} total={SECTIONS.length}/>
      </main>

      <Search open={searchOpen} onClose={() => setSearchOpen(false)} />
      <MobileNav open={navOpen} onClose={() => setNavOpen(false)} active={active} />

      <div className="toast" data-on={Boolean(last)}>
        Copied — {last?.label}
      </div>
    </div>
  );
}

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