// Page components — Swiss direction

const { useState, useEffect, useRef } = React;
const useBP = () => React.useContext(window.BreakpointContext);

function Shell({ page, setPage, children }) {
  const bp = useBP();
  const mob = bp === 'mobile';
  const nav = [
    ['cover',  '01', 'Index'],
    ['table',  '02', 'The 81'],
    ['read',   '03', 'Manuscript'],
    ['method', '04', 'Method'],
    ['join',   '05', 'Contribute'],
  ];
  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
      <header style={{
        display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center',
        padding: mob ? '12px 16px' : '14px 32px',
        borderBottom: '1px solid var(--rule)',
        position: 'sticky', top: 0, background: 'var(--bg)', zIndex: 10,
      }}>
        <div style={{ display: 'flex', gap: 14, alignItems: 'baseline' }}>
          <button onClick={() => setPage('cover')} style={{
            background: 'none', border: 'none', padding: 0, cursor: 'pointer',
            fontFamily: 'inherit', fontSize: 17, fontWeight: 600,
            letterSpacing: '-0.015em', color: 'var(--ink)',
          }}>Knowware<span style={{ color: 'var(--sub2)' }}>®</span></button>
          {!mob && (
            <span className="mono" style={{ fontSize: 12, color: 'var(--sub)' }}>
              systems-of-intelligence · v1.0 · mmxxvi
            </span>
          )}
        </div>
        <nav style={{ display: 'flex', gap: 0 }}>
          {nav.map(([k, n, l]) => (
            <button key={k} onClick={() => setPage(k)} style={{
              background: page === k ? 'var(--ink)' : 'transparent',
              color: page === k ? 'var(--paper)' : 'var(--ink)',
              border: 'none', padding: mob ? '7px 10px' : '7px 12px', cursor: 'pointer',
              fontFamily: 'inherit', fontSize: mob ? 13 : 15, letterSpacing: '-0.01em',
              display: 'flex', gap: mob ? 0 : 8, alignItems: 'baseline',
            }}>
              <span className="mono" style={{ fontSize: mob ? 10 : 11, opacity: 0.6 }}>{n}</span>
              {!mob && <span>{l}</span>}
            </button>
          ))}
        </nav>
      </header>
      <main style={{ flex: 1 }}>{children}</main>
      {!mob && <Foot />}
    </div>
  );
}

function Foot() {
  return (
    <footer style={{ borderTop: '1px solid var(--rule)', padding: '20px 32px',
      display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 16,
      fontSize: 12, color: 'var(--sub)' }}>
      <div style={{ gridColumn: '1 / span 3' }} className="mono">Knowware / Systems of Intelligence</div>
      <div style={{ gridColumn: '4 / span 3' }} className="mono">MMXXVI · Edition 01</div>
      <div style={{ gridColumn: '7 / span 3' }} className="mono">~350 pp · 09 ch · 81 voices</div>
      <div style={{ gridColumn: '10 / span 3', textAlign: 'right' }} className="mono">iamkhayyam.github.io/knowware</div>
    </footer>
  );
}

function Grid({ children, style }) {
  const mob = useBP() === 'mobile';
  return <div style={{
    display: 'grid', gridTemplateColumns: mob ? '1fr' : 'repeat(12, 1fr)',
    gap: mob ? 12 : 20, padding: mob ? '0 16px' : '0 48px', ...style,
  }}>{children}</div>;
}

function Label({ children, style }) {
  return <div className="mono" style={{
    fontSize: 13, color: 'var(--sub)', paddingTop: 6,
    letterSpacing: '-0.005em', ...style,
  }}>{children}</div>;
}

function MobileSpine({ setPage }) {
  const barPcts = window.SECTIONS.map((_, i) => 45 + ((i * 37) % 55));
  const maxPct  = Math.max(...barPcts);
  const [visible, setVisible] = React.useState(false);
  const ref = React.useRef(null);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const obs = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) { setVisible(true); obs.disconnect(); }
    }, { threshold: 0.1 });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);

  return (
    <div ref={ref}>
      <div style={{ border: '1px solid var(--rule)', background: 'var(--paper)' }}>
        {window.SECTIONS.map((s, i) => (
          <button key={s.n} onClick={() => setPage('read')} style={{
            width: '100%', background: 'none', border: 'none',
            borderBottom: '1px solid var(--rule)',
            padding: '10px 14px', cursor: 'pointer',
            display: 'grid', gridTemplateColumns: '32px 1fr auto',
            alignItems: 'center', gap: 10, fontFamily: 'inherit', textAlign: 'left',
          }}>
            <span className="mono" style={{ fontSize: 9, color: 'var(--sub)' }}>ch{s.n}</span>
            <span style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
              <span style={{ fontSize: 13, letterSpacing: '-0.01em', color: 'var(--ink)', lineHeight: 1.2 }}>{s.title}</span>
              <span style={{
                display: 'block', height: 2,
                background: 'var(--accent)',
                width: visible ? `${(barPcts[i] / maxPct) * 100}%` : '0%',
                transition: `width 0.55s cubic-bezier(0.4,0,0.2,1) ${i * 55}ms`,
              }} />
            </span>
            <span className="mono" style={{ fontSize: 9, color: 'var(--sub2)' }}>↗</span>
          </button>
        ))}
      </div>
      <div style={{ position: 'relative', marginTop: 8, paddingBottom: 14 }}>
        <div style={{ height: 1, background: 'var(--rule)', width: '100%' }} />
        <div style={{ height: 4, width: 1, background: 'var(--rule)', position: 'absolute', top: 0, left: 0 }} />
        <div style={{ height: 4, width: 1, background: 'var(--rule)', position: 'absolute', top: 0, right: 0 }} />
        <div className="mono" style={{ display: 'flex', justifyContent: 'space-between',
          fontSize: 9, color: 'var(--sub2)', marginTop: 4 }}>
          <span>0 pp</span>
          <span>Fig. 02 — relative page count</span>
          <span>~350 pp</span>
        </div>
      </div>
    </div>
  );
}

// ─── Cover (v2) ────────────────────────────────────────
// Hero is a bleed strip. Running metadata masthead up top.
// Marquee of groups. A "spine" TOC. Live clock.
function Cover({ setPage }) {
  const bp = useBP();
  const mob = bp === 'mobile';
  const [hover, setHover] = React.useState(null);
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    const t = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(t);
  }, []);
  const clock = now.toISOString().replace('T', ' / ').slice(0, 19) + ' UTC';

  return (
    <div>
      {/* Masthead strip — full width, runs metadata as a ledger line */}
      <div className="mono" style={{
        borderBottom: '1px solid var(--rule)',
        padding: mob ? '8px 16px' : '10px 24px',
        display: 'grid',
        gridTemplateColumns: mob ? '1fr 1fr' : 'repeat(6, 1fr)',
        gap: 16, fontSize: 10, color: 'var(--sub)',
        letterSpacing: '-0.005em',
      }}>
        <span>ISSUE / 01</span>
        {!mob && <span>VOL / I OF II</span>}
        {!mob && <span>PRINT / AUTUMN 26</span>}
        {!mob && <span>PAGES / 512</span>}
        {!mob && <span>VOICES / 081</span>}
        <span style={{ textAlign: 'right' }}>{clock}</span>
      </div>

      {/* Hero — left wordmark, right huge numeral "81" */}
      <div style={{
        display: 'grid',
        gridTemplateColumns: mob ? '1fr' : '6fr 6fr',
        alignItems: 'stretch',
        borderBottom: '1px solid var(--ink)',
        minHeight: mob ? 'auto' : '64vh',
      }}>
        <div style={{ padding: '48px 24px 32px',
          display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
          <p style={{
            margin: 0,
            fontSize: mob ? 15 : 17, lineHeight: 1.25,
            letterSpacing: '-0.02em', fontStyle: 'italic',
            color: 'var(--sub)',
          }}>
            You already know this.<br />You just haven't had a name for it.
          </p>
          <div className="mono" style={{ fontSize: 11, color: 'var(--sub)' }}>
            KNOWWARE / SYSTEMS OF INTELLIGENCE
          </div>
          <h1 style={{
            margin: 0,
            fontSize: mob ? 'clamp(56px, 15vw, 100px)' : 'clamp(72px, 13vw, 220px)',
            lineHeight: 0.82, letterSpacing: '-0.05em', fontWeight: 500,
          }}>
            Systems<br/>
            <span style={{ fontWeight: 400 }}>of</span>&nbsp;Intelligence.
          </h1>
          <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
            display: 'flex', justifyContent: 'space-between' }}>
            <span>BY &nbsp;/&nbsp; KHAYYAM</span>
            <span>MMXXVI · ED. 01</span>
          </div>
        </div>
        <div style={{
          borderLeft: mob ? 'none' : '1px solid var(--ink)',
          borderTop: mob ? '1px solid var(--ink)' : 'none',
          background: 'var(--paper)',
          padding: '24px',
          display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
          position: 'relative', overflow: 'hidden',
          containerType: 'inline-size',
        }}>
          <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
            display: 'flex', justifyContent: 'space-between' }}>
            <span>FIG. 01</span><span>SYNTHESES</span>
          </div>
          <div style={{
            fontSize: 'min(92cqw, 460px)',
            fontWeight: 500, letterSpacing: '-0.08em', lineHeight: 0.82,
            textAlign: 'center', color: 'var(--ink)',
            maxWidth: '100%', overflow: 'hidden',
          }}>
            81
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <div className="mono" style={{ fontSize: 10, color: 'var(--sub2)',
              fontStyle: 'italic', letterSpacing: '-0.005em' }}>
              Not interviews — the Third Body in action.
            </div>
            <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
              display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span>ACROSS 3 TIERS · 9 CHAPTERS</span>
              <button onClick={() => setPage('table')} style={{
                background: 'var(--ink)', color: 'var(--paper)',
                border: 'none', padding: '6px 10px', fontFamily: 'inherit',
                fontSize: 11, cursor: 'pointer',
              }}>See all →</button>
            </div>
          </div>
        </div>
      </div>

      {/* Domains marquee */}
      {(() => {
        const DOMAINS = [
          { label: 'AI',          code: 'AI',  color: 'oklch(0.88 0.09 20)'  },
          { label: 'Finance',     code: 'FIN', color: 'oklch(0.88 0.09 55)'  },
          { label: 'Logistics',   code: 'LOG', color: 'oklch(0.88 0.09 85)'  },
          { label: 'Biology',     code: 'BIO', color: 'oklch(0.88 0.09 140)' },
          { label: 'Education',   code: 'EDU', color: 'oklch(0.88 0.09 160)' },
          { label: 'Policy',      code: 'POL', color: 'oklch(0.88 0.09 195)' },
          { label: 'Agriculture', code: 'AGR', color: 'oklch(0.88 0.09 215)' },
          { label: 'Cities',      code: 'CIT', color: 'oklch(0.88 0.09 240)' },
          { label: 'Memory',      code: 'MEM', color: 'oklch(0.88 0.09 280)' },
          { label: 'Media',       code: 'MED', color: 'oklch(0.88 0.09 310)' },
          { label: 'Labour',      code: 'LAB', color: 'oklch(0.88 0.09 340)' },
        ];
        const inkOf = c => c.replace('0.88', '0.38');
        return (
          <div style={{
            borderBottom: '1px solid var(--rule)',
            overflow: 'hidden', whiteSpace: 'nowrap',
            padding: '0', background: 'var(--paper)',
          }}>
            <div style={{
              display: 'inline-flex',
              animation: 'kw-marquee 38s linear infinite',
            }} className="mono">
              {Array.from({length: 3}).flatMap((_, k) =>
                DOMAINS.map(d => (
                  <span key={`${k}-${d.code}`} style={{
                    fontSize: 12, letterSpacing: '0.02em',
                    display: 'inline-flex', alignItems: 'center',
                    gap: 10, padding: '11px 22px',
                    borderRight: '1px solid var(--rule)',
                  }}>
                    <span style={{
                      width: 10, height: 10, flexShrink: 0,
                      background: d.color,
                      border: `1px solid ${inkOf(d.color)}`,
                      display: 'inline-block',
                    }} />
                    <span style={{ color: 'var(--ink)' }}>{d.label.toUpperCase()}</span>
                    <span style={{ color: 'var(--sub2)', fontSize: 10 }}>{d.code}</span>
                  </span>
                ))
              )}
            </div>
            <style>{`@keyframes kw-marquee{0%{transform:translateX(0)}100%{transform:translateX(-33.33%)}}`}</style>
          </div>
        );
      })()}

      <div style={{ padding: '0 0 56px' }}>

      {/* Abstract ribbon */}
      {mob ? (
        <div style={{ padding: '24px 16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
          <Label>Abstract</Label>
          <p style={{ fontSize: 18, lineHeight: 1.4, margin: 0, letterSpacing: '-0.015em' }}>
            A field guide for anyone who must survive what is coming. Built from{' '}
            <strong style={{ fontWeight: 500, color: 'var(--accent)' }}>eighty-one long conversations</strong>{' '}
            with academics, practitioners, and visionaries.
          </p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <Btn filled onClick={() => setPage('table')}>Open the 81 →</Btn>
            <Btn onClick={() => setPage('read')}>Read the preview →</Btn>
            <Btn onClick={() => setPage('join')}>Contribute to Vol. II →</Btn>
          </div>
        </div>
      ) : (
        <Grid style={{ marginTop: 36 }}>
          <Label style={{ gridColumn: '1 / span 3' }}>Abstract</Label>
          <p style={{ gridColumn: '4 / span 6', fontSize: 26, lineHeight: 1.35,
            margin: 0, letterSpacing: '-0.018em' }}>
            A field guide for anyone who must survive what is coming — and the
            tools, markets, and institutions they'll have to think with. Built
            from <strong style={{ fontWeight: 500, color: 'var(--accent)' }}>
            eighty-one long conversations</strong> with academics, practitioners,
            and visionaries who are quietly redrawing the maps.
          </p>
          <div style={{ gridColumn: '10 / span 3', display: 'flex',
            flexDirection: 'column', gap: 6 }}>
            <Btn filled onClick={() => setPage('table')}>Open the 81 →</Btn>
            <Btn onClick={() => setPage('read')}>Read the preview →</Btn>
            <Btn onClick={() => setPage('join')}>Contribute to Vol. II →</Btn>
          </div>
        </Grid>
      )}

      {/* The Spine — compact TOC shown as a book spine */}
      <Grid style={{ marginTop: 64 }}>
        <Label style={{ gridColumn: '1 / span 3' }}>01 · The spine</Label>
        <div style={{ gridColumn: mob ? '1' : '4 / span 9' }}>
          <div style={{ display: 'flex', alignItems: mob ? 'flex-start' : 'end',
            flexDirection: mob ? 'column' : 'row',
            justifyContent: 'space-between', gap: mob ? 10 : 0, marginBottom: 14 }}>
            <h2 style={{ fontSize: mob ? 'clamp(22px, 6vw, 32px)' : 40, fontWeight: 500,
              letterSpacing: '-0.03em', margin: 0, lineHeight: 1 }}>
              Nine chapters, one capstone.
            </h2>
            <button onClick={() => setPage('read')} className="mono" style={{
              background: 'none', border: '1px solid var(--ink)',
              padding: '6px 10px', cursor: 'pointer', fontSize: 11, flexShrink: 0,
            }}>Read manuscript ↗</button>
          </div>

          {mob ? <MobileSpine setPage={setPage} /> : (
            /* Desktop: bar chart spine */
            <>
              <div style={{
                display: 'grid', gridTemplateColumns: `repeat(${window.SECTIONS.length}, 1fr)`,
                gap: 2, alignItems: 'end', height: 260,
                background: 'var(--paper)', border: '1px solid var(--rule)',
                padding: 12,
              }}>
                {window.SECTIONS.map((s, i) => {
                  const h = 45 + ((i * 37) % 55);
                  return (
                    <button key={s.n} onClick={() => setPage('read')}
                      style={{
                        background: 'var(--ink)', border: 'none', cursor: 'pointer',
                        height: `${h}%`, display: 'flex', flexDirection: 'column',
                        justifyContent: 'space-between', padding: 8,
                        color: 'var(--paper)', alignItems: 'flex-start',
                        fontFamily: 'inherit', transition: 'background .15s',
                      }}
                      onMouseEnter={e => e.currentTarget.style.background = 'var(--accent)'}
                      onMouseLeave={e => e.currentTarget.style.background = 'var(--ink)'}>
                      <span className="mono" style={{ fontSize: 10 }}>ch{s.n}</span>
                      <span style={{ fontSize: 11, writingMode: 'vertical-rl',
                        transform: 'rotate(180deg)', letterSpacing: '-0.005em',
                        whiteSpace: 'nowrap', overflow: 'hidden',
                        textOverflow: 'ellipsis', maxHeight: '100%' }}>
                        {s.title}
                      </span>
                    </button>
                  );
                })}
              </div>
              <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
                marginTop: 8, display: 'flex', justifyContent: 'space-between' }}>
                <span>Fig. 02 — Page-count by section.</span>
                <span>Click any spine to open.</span>
              </div>
            </>
          )}
        </div>
      </Grid>

      {/* Thesis — full bleed band */}
      <div style={{
        marginTop: 64, background: 'var(--ink)', color: 'var(--paper)',
        padding: mob ? '40px 16px' : '64px 24px',
      }}>
        <div style={{ display: 'grid',
          gridTemplateColumns: mob ? '1fr' : 'repeat(12, 1fr)', gap: 16,
          maxWidth: '100%' }}>
          <Label style={{ gridColumn: mob ? '1' : '1 / span 3',
            color: 'var(--accent-soft)' }}>02 · Thesis</Label>
          <div style={{ gridColumn: mob ? '1' : '4 / span 9', fontSize: mob ? 22 : 40,
            lineHeight: 1.2, letterSpacing: '-0.02em' }}>
            The tools we use to think are no longer{' '}
            <span style={{ color: 'var(--accent-soft)',
              borderBottom: '2px solid var(--accent-soft)' }}>separate</span>{' '}
            from the thinking itself. This book is a field guide for what
            happens when that line dissolves — in labs, in ledgers, in fields,
            in classrooms, in clinics.
          </div>
        </div>
      </div>

      {/* Structure — horizontal stat bar */}
      <Grid style={{ marginTop: 64 }}>
        <Label style={{ gridColumn: mob ? '1' : '1 / span 3' }}>03 · Structure</Label>
        <div style={{ gridColumn: mob ? '1' : '4 / span 9' }}>
          <div style={{
            display: 'grid', gridTemplateColumns: mob ? 'repeat(3, 1fr)' : 'repeat(6, 1fr)',
            borderTop: '2px solid var(--ink)',
            borderBottom: '2px solid var(--ink)',
          }}>
            {[
              ['09', 'Chapters'],
              ['81', 'Interviews'],
              ['03', 'Tiers'],
              ['~60', 'Diagrams'],
              ['~350', 'Pages'],
              ['03', 'Years'],
            ].map(([n, l], i) => (
              <div key={l} style={{
                borderRight: mob ? (i % 3 < 2 ? '1px solid var(--rule)' : 'none') : (i < 5 ? '1px solid var(--rule)' : 'none'),
                borderBottom: mob && i < 3 ? '1px solid var(--rule)' : 'none',
                padding: mob ? '16px 10px 12px' : '24px 14px 18px',
                display: 'flex', flexDirection: 'column', gap: 10,
                minWidth: 0, overflow: 'hidden',
              }}>
                <span style={{ fontSize: 'clamp(40px, 4.4vw, 64px)', fontWeight: 500,
                  letterSpacing: '-0.045em', lineHeight: 0.9 }}>{n}</span>
                <span className="mono" style={{ fontSize: 10,
                  color: 'var(--sub)', textTransform: 'uppercase',
                  letterSpacing: '0.05em' }}>{l}</span>
              </div>
            ))}
          </div>
        </div>
      </Grid>

      {/* Early readers — staggered pull quotes */}
      <Grid style={{ marginTop: 64 }}>
        <Label style={{ gridColumn: mob ? '1' : '1 / span 3' }}>04 · Early readers</Label>
        <div style={{ gridColumn: mob ? '1' : '4 / span 9',
          display: 'grid', gridTemplateColumns: mob ? '1fr' : 'repeat(12, 1fr)',
          gap: 16, rowGap: mob ? 24 : 40 }}>
          {[
            ['A book that reads you while you read it.', 'N. Mehta', 'researcher', 1, 6],
            ['The most serious attempt to map the new terrain I have seen.', 'L. Okafor', 'economist', 7, 6],
            ['Essential, and a little frightening.', 'Y. Park', 'editor', 3, 7],
          ].map(([q, a, r, start, span], i) => (
            <blockquote key={a} style={{
              gridColumn: mob ? '1' : `${start} / span ${span}`,
              margin: 0,
              borderTop: '1px solid var(--ink)', paddingTop: 14,
            }}>
              <p style={{ fontSize: 22, lineHeight: 1.3, margin: 0,
                letterSpacing: '-0.018em' }}>
                "{q}"
              </p>
              <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
                marginTop: 12 }}>— {a} · {r}</div>
            </blockquote>
          ))}
        </div>
      </Grid>

      {/* CTA strip */}
      <div style={{ marginTop: 64, padding: mob ? '0 16px' : '0 24px' }}>
        <div style={{
          display: 'grid', gridTemplateColumns: mob ? '1fr' : '1fr auto',
          alignItems: 'center', gap: mob ? 16 : 24,
          border: '1px solid var(--ink)',
          padding: mob ? '20px 16px' : '28px 32px', background: 'var(--accent-soft)',
        }}>
          <div style={{ fontSize: mob ? 20 : 28, letterSpacing: '-0.02em',
            lineHeight: 1.15, fontWeight: 500 }}>
            Pre-order before Autumn and your name enters the colophon.
          </div>
          <div style={{ display: 'flex', gap: 10 }}>
            <Btn filled>Pre-order · $34</Btn>
            <Btn>Join the list</Btn>
          </div>
        </div>
      </div>

      </div>
    </div>
  );
}

// ─── Cast / Manifest view ──────────────────────────────
const CAST = [
  {n:1,t:"The Coordination Intelligence Revolution",voices:[
    {i:"01",nm:"Dr. Paul Pangaro",r:"Cybernetician, Conversation Theory",s:"alive",tri:"A",f:"old"},
    {i:"02",nm:"Dr. N. Katherine Hayles",r:"Literary Scholar, Posthuman Cognition",s:"alive",tri:"A",f:"old"},
    {i:"03",nm:"Donella Meadows",r:"Thinking in Systems",s:"passed",tri:"A",tg:"legacy",f:"missing"},
    {i:"04",nm:"Stewart Brand",r:"Whole Earth Catalog",s:"alive",tri:"P",f:"old"},
    {i:"05",nm:"Kevin Kelly",r:"Wired, What Technology Wants",s:"alive",tri:"P",tg:"new",f:"missing"},
    {i:"06",nm:"Yann Minh",r:"Digital Shamanism",s:"alive",tri:"P",f:"old"},
    {i:"07",nm:"Terence McKenna",r:"Ethnobotanist, Philosopher",s:"passed",tri:"V",tg:"legacy",f:"old"},
    {i:"08",nm:"Lakota Elder Phillip Deere",r:"Star Knowledge",s:"passed",tri:"V",tg:"legacy",f:"old"},
    {i:"09",nm:"Daniel Schmachtenberger",r:"Consilience Project",s:"alive",tri:"V",tg:"new",f:"missing"},
  ]},
  {n:2,t:"The Dawn of Systems Intelligence",voices:[
    {i:"10",nm:"Dr. Judea Pearl",r:"Causal Inference",s:"alive",tri:"A",f:"old"},
    {i:"11",nm:"Claude Shannon",r:"Information Theory",s:"passed",tri:"A",tg:"legacy",f:"new"},
    {i:"12",nm:"Alan Turing",r:"Computing Machinery & Intelligence",s:"passed",tri:"A",tg:"legacy",f:"missing"},
    {i:"13",nm:"Dr. Hartmut Neven",r:"Google Quantum AI",s:"alive",tri:"P",f:"old"},
    {i:"14",nm:"Former NSA Tech Director",r:"Signals Intelligence",s:"anon",tri:"P",f:"old"},
    {i:"15",nm:"Palmer Luckey",r:"Oculus, Anduril",s:"alive",tri:"P",f:"old"},
    {i:"16",nm:"Mo Gawdat",r:"Google X — saw AI dawn from inside",s:"alive",tri:"V",tg:"new",f:"new"},
    {i:"17",nm:"Hunbatz Men",r:"Maya Elder",s:"alive",tri:"V",f:"old"},
    {i:"18",nm:"Prof. Ruqian Lu",r:"Named knowware (2005)",s:"alive",tri:"V",tg:"origin",f:"new"},
  ]},
  {n:3,t:"Architecture of Systems Intelligence",voices:[
    {i:"19",nm:"Yann LeCun",r:"Chief AI Scientist, Meta",s:"alive",tri:"A",f:"old"},
    {i:"20",nm:"Richard Feynman",r:"Theoretical Physicist",s:"passed",tri:"A",tg:"legacy",f:"new"},
    {i:"21",nm:"James Gosling",r:"Creator of Java",s:"alive",tri:"A",tg:"new",f:"missing"},
    {i:"22",nm:"Dario Amodei",r:"CEO, Anthropic",s:"alive",tri:"P",f:"old"},
    {i:"23",nm:"Demis Hassabis",r:"CEO, Google DeepMind",s:"alive",tri:"P",f:"old"},
    {i:"24",nm:"Clément Delangue",r:"CEO, Hugging Face",s:"alive",tri:"P",tg:"new",f:"old"},
    {i:"25",nm:"Iain McGilchrist",r:"The Master and His Emissary",s:"alive",tri:"V",tg:"new",f:"missing"},
    {i:"26",nm:"Fritjof Capra",r:"Web of Life, Systems Thinking",s:"alive",tri:"V",tg:"new",f:"missing"},
    {i:"27",nm:"Ray Kurzweil",r:"Singularity, Law of Accelerating Returns",s:"alive",tri:"V",tg:"new",f:"old"},
  ]},
  {n:4,t:"Systems Intelligence in Action",voices:[
    {i:"28",nm:"Dr. Carlo Ratti",r:"MIT Senseable City Lab",s:"alive",tri:"A",f:"old"},
    {i:"29",nm:"Dr. Eric Topol",r:"Digital Medicine",s:"alive",tri:"A",f:"old"},
    {i:"30",nm:"Andrew Lo",r:"Finance Professor, MIT",s:"alive",tri:"A",f:"old"},
    {i:"31",nm:"Dan Doctoroff",r:"Former CEO Sidewalk Labs",s:"alive",tri:"P",f:"old"},
    {i:"32",nm:"Linda Raschke",r:"Professional Trader",s:"alive",tri:"P",f:"old"},
    {i:"33",nm:"Anonymous Quant Trader",r:"Systematic Trading",s:"anon",tri:"P",f:"old"},
    {i:"34",nm:"Sarah Rossbach",r:"Feng Shui Scholar",s:"alive",tri:"V",f:"old"},
    {i:"35",nm:"Caroline Myss",r:"Medical Intuitive",s:"alive",tri:"V",f:"old"},
    {i:"36",nm:"Nassim Taleb",r:"Antifragility — coordination resilience",s:"alive",tri:"V",tg:"new",f:"missing"},
  ]},
  {n:5,t:"Human-Systems Intelligence Interaction",voices:[
    {i:"37",nm:"Dr. Miguel Nicolelis",r:"Neuroscientist, BCI",s:"alive",tri:"A",f:"old"},
    {i:"38",nm:"Dr. Alex Pentland",r:"Social Physics",s:"alive",tri:"A",f:"old"},
    {i:"39",nm:"Dr. Shannon Vallor",r:"AI Ethicist",s:"alive",tri:"A",f:"old"},
    {i:"40",nm:"Dr. Thomas Oxley",r:"CEO, Synchron",s:"alive",tri:"P",f:"old"},
    {i:"41",nm:"Tristan Harris",r:"Center for Humane Tech",s:"alive",tri:"P",f:"old"},
    {i:"42",nm:"Jimmy Wales",r:"Founder, Wikipedia",s:"alive",tri:"P",f:"old"},
    {i:"43",nm:"Anonymous BCI User",r:"Living the Hybrid Reality",s:"anon",tri:"V",f:"old"},
    {i:"44",nm:"Thich Nhat Hanh Foundation",r:"Interbeing",s:"passed",tri:"V",tg:"legacy",f:"old"},
    {i:"45",nm:"Donna Haraway",r:"Cyborg Manifesto",s:"alive",tri:"V",tg:"new",f:"missing"},
  ]},
  {n:6,t:"Consciousness as Pattern Recognition",voices:[
    {i:"46",nm:"Stuart Russell",r:"AI Safety, UC Berkeley",s:"alive",tri:"A",f:"old"},
    {i:"47",nm:"Timnit Gebru",r:"AI Justice, DAIR",s:"alive",tri:"A",f:"old"},
    {i:"48",nm:"Kate Crawford",r:"AI Now Institute",s:"alive",tri:"A",f:"old"},
    {i:"49",nm:"Norbert Wiener",r:"Father of Cybernetics",s:"passed",tri:"P",tg:"legacy",f:"new"},
    {i:"50",nm:"Margaret Mitchell",r:"Hugging Face Ethics",s:"alive",tri:"P",tg:"new",f:"missing"},
    {i:"51",nm:"Anonymous In-Q-Tel PM",r:"Intel community ↔ tech",s:"anon",tri:"P",tg:"new",f:"new"},
    {i:"52",nm:"Roger Penrose",r:"Consciousness & Quantum",s:"alive",tri:"V",tg:"new",f:"missing"},
    {i:"53",nm:"Antonio Damasio",r:"Somatic Markers",s:"alive",tri:"V",tg:"new",f:"missing"},
    {i:"54",nm:"Rupert Sheldrake",r:"Morphic Resonance",s:"alive",tri:"V",tg:"new",f:"missing"},
  ]},
  {n:7,t:"Engineering Reality",voices:[
    {i:"55",nm:"Dr. John Preskill",r:"Quantum Computing",s:"alive",tri:"A",f:"old"},
    {i:"56",nm:"Dr. Seth Lloyd",r:"Quantum Biology",s:"alive",tri:"A",f:"old"},
    {i:"57",nm:"Chip Huyen",r:"Production ML",s:"alive",tri:"A",f:"old"},
    {i:"58",nm:"Jeff Dean",r:"Google Infrastructure",s:"alive",tri:"P",f:"old"},
    {i:"59",nm:"Dr. Lisa Su",r:"Chair & CEO, AMD",s:"alive",tri:"P",tg:"new",f:"new"},
    {i:"60",nm:"Wendell Weeks",r:"CEO, Corning",s:"alive",tri:"P",tg:"new",f:"new"},
    {i:"61",nm:"Neri Oxman",r:"Material Ecology",s:"alive",tri:"V",f:"old"},
    {i:"62",nm:"Anon DARPA Program Manager",r:"Breakthrough tech coordination",s:"anon",tri:"V",tg:"new",f:"new"},
    {i:"63",nm:"Dr. Fei-Fei Li",r:"Stanford, Human-Centered AI",s:"alive",tri:"V",tg:"new",f:"old"},
  ]},
  {n:8,t:"Beyond Human Intelligence",voices:[
    {i:"64",nm:"Dr. Max Tegmark",r:"MIT, Future of Life",s:"alive",tri:"A",f:"old"},
    {i:"65",nm:"Dr. Nick Bostrom",r:"Oxford, FHI",s:"alive",tri:"A",f:"old"},
    {i:"66",nm:"Dr. Jill Tarter",r:"SETI Institute",s:"alive",tri:"A",f:"old"},
    {i:"67",nm:"Dr. Sara Seager",r:"Exoplanet Research",s:"alive",tri:"P",f:"old"},
    {i:"68",nm:"Dr. David Chalmers",r:"Philosophy of Mind",s:"alive",tri:"P",f:"old"},
    {i:"69",nm:"Anil Seth",r:"Being You — consciousness as controlled hallucination",s:"alive",tri:"P",tg:"new",f:"missing"},
    {i:"70",nm:"Liu Cixin",r:"Dark Forest",s:"alive",tri:"V",f:"old"},
    {i:"71",nm:"Dr. Thomas Nagel",r:"What Is It Like To Be",s:"alive",tri:"V",f:"old"},
    {i:"72",nm:"Srinivasa Ramanujan",r:"Mathematician, 1887–1920",s:"passed",tri:"V",tg:"legacy",f:"new"},
  ]},
  {n:9,t:"No Way Know-How",voices:[
    {i:"73",nm:"David Autor",r:"Labor Economics",s:"alive",tri:"A",f:"old"},
    {i:"74",nm:"Kate Raworth",r:"Doughnut Economics",s:"alive",tri:"A",f:"old"},
    {i:"75",nm:"François Chollet",r:"Keras, ARC Prize",s:"alive",tri:"A",f:"old"},
    {i:"76",nm:"Emad Mostaque",r:"Founder, Stability AI",s:"alive",tri:"P",f:"old"},
    {i:"77",nm:"Dr. Fiona Hill",r:"Brookings, Former NSC",s:"alive",tri:"P",tg:"new",f:"new"},
    {i:"78",nm:"Peter Senge",r:"The Fifth Discipline, Systems Learning",s:"alive",tri:"P",tg:"new",f:"missing"},
    {i:"79",nm:"Charles Eisenstein",r:"Sacred Economics",s:"alive",tri:"V",f:"old"},
    {i:"80",nm:"Sherry Turkle",r:"Technology & Connection",s:"alive",tri:"V",f:"old"},
    {i:"81",nm:"Anon CIA/KGB MK-Ultra",r:"Coordination failure from inside",s:"anon",tri:"V",tg:"new",f:"new"},
  ]},
];

function CastView({ onOpenDossier, hideHeader }) {
  const mob = useBP() === 'mobile';

  const allVoices = CAST.flatMap(c => c.voices);
  const legacy = allVoices.filter(v => v.tg === 'legacy').length;
  const anon   = allVoices.filter(v => v.s === 'anon').length;
  const origin = allVoices.filter(v => v.tg === 'origin').length;
  const old_   = allVoices.filter(v => v.f === 'old').length;
  const newAll = allVoices.filter(v => v.f !== 'old').length;

  const statusDot = s =>
    s === 'alive'  ? '#1D9E75' :
    s === 'passed' ? 'var(--sub2)' : 'oklch(0.60 0.15 270)';

  const tierBg = t =>
    t === 'A' ? 'var(--tier-a)' : t === 'P' ? 'var(--tier-p)' : 'var(--tier-v)';
  const tierInk = t =>
    t === 'A' ? 'var(--tier-a-ink)' : t === 'P' ? 'var(--tier-p-ink)' : 'var(--tier-v-ink)';

  const fileBadge = f => {
    if (f === 'old')     return { bg: 'var(--tier-v)',   ink: 'var(--tier-v-ink)', label: 'FILED' };
    if (f === 'new')     return { bg: 'var(--tier-a)',   ink: 'var(--tier-a-ink)', label: 'NEW'   };
    if (f === 'missing') return { bg: 'var(--tier-a)',   ink: 'var(--tier-a-ink)', label: 'NEW'   };
    return null;
  };

  const tagBadge = tg => {
    if (tg === 'legacy') return { bg: 'var(--sub2)', ink: 'var(--paper)', label: 'LEGACY' };
    if (tg === 'origin') return { bg: 'var(--accent)', ink: '#fff', label: 'ORIGIN' };
    if (tg === 'new')    return null;
    return null;
  };

  return (
    <div style={{ paddingBottom: hideHeader ? 0 : 80 }}>

      {!hideHeader && (<>
      {/* Explainer header */}
      <div style={{
        padding: mob ? '20px 16px 24px' : '28px 24px 32px',
        background: 'var(--ink)', color: 'var(--paper)',
        borderBottom: '1px solid var(--rule)',
      }}>
        <p style={{
          margin: '0 0 20px',
          fontSize: mob ? 14 : 15, lineHeight: 1.6,
          letterSpacing: '-0.01em', color: 'oklch(0.75 0.04 250)',
          maxWidth: 640,
        }}>
          What you are reading is not interviews. It is the Third Body in action —
          human knowledge coordinated with machine synthesis to produce insights that
          neither could generate independently. Every insight is real: all 81 voices are
          synthesised from published work, lectures, papers, and primary sources.
          Nine are legacy guests — historical figures whose ideas outlasted their presence.
          This book is not about Knowware. This book IS Knowware.
        </p>
        {/* Stats row */}
        <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
          {[
            ['81', 'total voices'],
            [String(old_), 'old'],
            [String(newAll), 'new'],
            [String(legacy), 'legacy guests'],
            [String(anon), 'anonymous'],
            [String(origin), 'named the field'],
          ].map(([v, l]) => (
            <div key={l} style={{
              borderLeft: '2px solid var(--accent)', paddingLeft: 10,
            }}>
              <div className="mono" style={{ fontSize: mob ? 18 : 22, fontWeight: 500, lineHeight: 1 }}>{v}</div>
              <div className="mono" style={{ fontSize: 9, color: 'var(--sub)', marginTop: 3, letterSpacing: '0.04em' }}>{l.toUpperCase()}</div>
            </div>
          ))}
        </div>
      </div>

      {/* Legend */}
      <div style={{
        padding: mob ? '10px 16px' : '10px 24px',
        borderBottom: '1px solid var(--rule)',
        display: 'flex', gap: mob ? 12 : 20, flexWrap: 'wrap', alignItems: 'center',
      }}>
        {[
          { el: <span style={{ width:8, height:8, borderRadius:'50%', background:'#1D9E75', display:'inline-block' }} />, label: 'With us' },
          { el: <span style={{ width:8, height:8, borderRadius:'50%', background:'var(--sub2)', display:'inline-block' }} />, label: 'Beyond' },
          { el: <span style={{ width:8, height:8, borderRadius:'50%', background:'oklch(0.60 0.15 270)', display:'inline-block' }} />, label: 'Anonymous' },
          { el: <span style={{ background:'var(--sub2)', color:'var(--paper)', fontSize:8, padding:'1px 5px', fontFamily:'var(--mono)', fontWeight:500 }}>LEGACY</span>, label: 'Channeling frequencies' },
          { el: <span style={{ background:'var(--accent)', color:'#fff', fontSize:8, padding:'1px 5px', fontFamily:'var(--mono)', fontWeight:500 }}>ORIGIN</span>, label: 'Named the field' },
        ].map(({ el, label }) => (
          <div key={label} className="mono" style={{ display:'flex', alignItems:'center', gap:6, fontSize:10, color:'var(--sub)' }}>
            {el} {label}
          </div>
        ))}
      </div>
      </>)}

      {/* Chapter sections */}
      <div style={{ padding: mob ? '0 0 40px' : '0 0 40px' }}>
        {CAST.map(ch => (
          <div key={ch.n} style={{ borderBottom: '1px solid var(--rule)' }}>
            {/* Chapter header */}
            <div className="mono" style={{
              padding: mob ? '8px 16px' : '8px 24px',
              fontSize: 10, color: 'var(--sub)',
              display: 'flex', justifyContent: 'space-between',
              borderBottom: '1px solid var(--rule)',
              background: 'var(--paper)',
              position: 'sticky', top: 48, zIndex: 5,
            }}>
              <span style={{ letterSpacing: '0.04em' }}>CH.{String(ch.n).padStart(2,'0')} — {ch.t.toUpperCase()}</span>
            </div>

            {/* Voice rows — single shared grid so every column locks to the same width */}
            <div style={{
              display: 'grid',
              gridTemplateColumns: mob
                ? '8px 16px 1fr auto'
                : '8px 16px calc(50% - 24px) 1fr auto',
            }}>
              {ch.voices.map(v => {
                const tb = tagBadge(v.tg);
                const missing = v.f === 'missing';
                const cell = {
                  borderBottom: '1px solid var(--rule)',
                  display: 'flex', alignItems: 'center',
                };
                return (
                  <React.Fragment key={v.i}>
                    {/* Tier — color sliver */}
                    <span style={{ borderBottom:'1px solid var(--rule)',
                      alignSelf:'stretch', background: tierBg(v.tri) }} />
                    {/* Status dot */}
                    <span style={{ ...cell, justifyContent:'center', opacity: missing ? 0.45 : 1 }}>
                      <span style={{ width:8, height:8, borderRadius:'50%',
                        background: statusDot(v.s), display:'inline-block', flexShrink:0 }} />
                    </span>
                    {/* Number + name + legacy badge — always full opacity, clickable */}
                    <div style={{ ...cell, gap:8, padding: mob ? '9px 8px' : '9px 12px', minWidth:0,
                      cursor: onOpenDossier ? 'pointer' : 'default' }}
                      onClick={() => onOpenDossier && onOpenDossier(parseInt(v.i, 10))}>
                      <span className="mono" style={{ fontSize:10, color:'var(--sub2)', flexShrink:0 }}>{v.i}</span>
                      <span style={{ fontSize: mob ? 13 : 14, fontWeight:500, letterSpacing:'-0.01em',
                        overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{v.nm}</span>
                      {tb && <span className="mono" style={{ fontSize:8, padding:'1px 5px', fontWeight:500,
                        background:tb.bg, color:tb.ink, flexShrink:0 }}>{tb.label}</span>}
                    </div>
                    {/* Role — desktop only, starts at 50% of viewport */}
                    {!mob && (
                      <span className="mono" style={{ ...cell, fontSize:11, color:'var(--sub)',
                        padding:'9px 12px', lineHeight:1.45, opacity: missing ? 0.5 : 1 }}>{v.r}</span>
                    )}
                    {/* Tier classification */}
                    <span className="mono" style={{ ...cell, fontSize:9, fontWeight:600,
                      padding: mob ? '0 12px 0 8px' : '0 16px',
                      background: tierBg(v.tri), color: tierInk(v.tri),
                      whiteSpace:'nowrap', letterSpacing:'0.04em', justifyContent:'center' }}>
                      {mob ? v.tri : (v.tri === 'A' ? 'Academic' : v.tri === 'P' ? 'Practitioner' : 'Visionary')}
                    </span>
                  </React.Fragment>
                );
              })}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── The Table page ────────────────────────────────────
function TablePage({ setPage, onOpenDossier, view, setView }) {
  const bp = useBP();
  const mob = bp === 'mobile';
  const layout = bp === 'desktop' ? 'w27' : 'sq9';
  const [hover, setHover] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [highlightGroup, setHighlightGroup] = React.useState(null);

  const showN = hover || selected;
  const shown = showN ? window.INTERVIEWS[showN - 1] : null;

  return (
    <div>
      <div style={{ padding: mob ? '12px 16px 32px' : '20px 24px 48px' }}>
        {/* Header: title left, view toggle right */}
        <div style={{ display: 'flex', justifyContent: 'space-between',
          alignItems: 'flex-start', flexDirection: mob ? 'column' : 'row',
          gap: mob ? 12 : 0, marginBottom: 20 }}>
          <div>
            <Label style={{ paddingTop: 0 }}>02 · The 81</Label>
            <h2 style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 500,
              letterSpacing: '-0.04em', lineHeight: 0.92, margin: '8px 0 0' }}>
              Eighty-one voices,<br/>three tiers, nine chapters.
            </h2>
          </div>
          <div style={{ display: 'flex', gap: 0, border: '1px solid var(--ink)' }}>
            {[['table','ELEMENTS'],['graph','GRAPH'],['mo','M.O.']].map(([k,l]) => (
              <button key={k} onClick={() => setView(k)} className="mono" style={{
                background: view === k ? 'var(--ink)' : 'var(--paper)',
                color: view === k ? 'var(--paper)' : 'var(--ink)',
                border: 'none',
                borderRight: k !== 'mo' ? '1px solid var(--ink)' : 'none',
                padding: '7px 12px', cursor: 'pointer', fontSize: 11,
              }}>{l}</button>
            ))}
          </div>
        </div>

        {view === 'table' ? (
          <>
            {/* Hover-to-preview strip — sticky, reacts to grid hover */}
            <div style={{
              position: 'sticky', top: 48, zIndex: 9,
              borderTop: '1px solid var(--rule)',
              borderBottom: '1px solid var(--rule)',
              marginLeft: mob ? -16 : -24, marginRight: mob ? -16 : -24,
              marginBottom: 0,
              height: mob ? 72 : 96, overflow: 'hidden',
              display: 'flex', alignItems: 'stretch',
              background: shown ? `var(${shown.group.varCSS})` : 'var(--bg)',
              transition: 'background .15s',
              boxShadow: shown ? '0 2px 12px rgba(0,0,0,0.07)' : 'none',
            }}>
              {shown ? <VoiceStrip v={shown} /> : <EmptyStrip />}
            </div>

            {/* Black band — cast context */}
            <div style={{
              marginLeft: mob ? -16 : -24, marginRight: mob ? -16 : -24,
              padding: mob ? '20px 16px 24px' : '28px 24px 32px',
              background: 'var(--ink)', color: 'var(--paper)',
              borderBottom: '1px solid var(--rule)',
            }}>
              <p style={{
                margin: '0 0 20px',
                fontSize: mob ? 14 : 15, lineHeight: 1.6,
                letterSpacing: '-0.01em', color: 'oklch(0.75 0.04 250)',
                maxWidth: 640,
              }}>
                What you are reading is not interviews. It is the Third Body in action —
                human knowledge coordinated with machine synthesis to produce insights that
                neither could generate independently. Every insight is real: all 81 voices are
                synthesised from published work, lectures, papers, and primary sources.
                Nine are legacy guests — historical figures whose ideas outlasted their presence.
                This book is not about Knowware. This book IS Knowware.
              </p>
              {(() => {
                const allVoices = CAST.flatMap(c => c.voices);
                const stats = [
                  ['81', 'total voices'],
                  [String(allVoices.filter(v => v.f === 'old').length), 'old'],
                  [String(allVoices.filter(v => v.f !== 'old').length), 'new'],
                  [String(allVoices.filter(v => v.tg === 'legacy').length), 'legacy guests'],
                  [String(allVoices.filter(v => v.s === 'anon').length), 'anonymous'],
                  [String(allVoices.filter(v => v.tg === 'origin').length), 'named the field'],
                ];
                return (
                  <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
                    {stats.map(([v, l]) => (
                      <div key={l} style={{ borderLeft: '2px solid var(--accent)', paddingLeft: 10 }}>
                        <div className="mono" style={{ fontSize: mob ? 18 : 22, fontWeight: 500, lineHeight: 1 }}>{v}</div>
                        <div className="mono" style={{ fontSize: 9, color: 'var(--sub)', marginTop: 3, letterSpacing: '0.04em' }}>{l.toUpperCase()}</div>
                      </div>
                    ))}
                  </div>
                );
              })()}
            </div>

            {/* Grid — periodic table of voices */}
            <HorizontalLegend active={highlightGroup} setActive={setHighlightGroup} />
            <div style={{ background: 'var(--paper)', border: '1px solid var(--rule)',
              borderTop: 'none', padding: 16 }}>
              <PeriodicTable
                layoutKey={layout}
                hover={hover} onHover={setHover}
                selected={selected}
                onSelect={(n) => {
                  setSelected(selected === n ? null : n);
                  if (n && onOpenDossier) onOpenDossier(n);
                }}
                highlightGroup={highlightGroup}
              />
            </div>
            <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
              marginTop: 10, marginBottom: 40, display: 'flex', justifyContent: 'space-between' }}>
              <span>Hover a cell for detail · Hover a tier to isolate · Click to pin</span>
              <span>{selected ? `pinned № ${String(selected).padStart(2, '0')}` : '—'}</span>
            </div>

            {/* Cast — full manifest, no duplicate header */}
            <div style={{ marginLeft: mob ? -16 : -24, marginRight: mob ? -16 : -24,
              borderTop: '1px solid var(--rule)' }}>
              <CastView onOpenDossier={onOpenDossier} hideHeader />
            </div>
          </>
        ) : view === 'graph' ? (
          <GraphView onOpenDossier={onOpenDossier} />
        ) : (
          <MOTable onOpenDossier={onOpenDossier} />
        )}
      </div>
    </div>
  );
}

// ─── Graph view ────────────────────────────────────────
// Force-directed canvas graph. Nodes = 81 voices, edges = co-citation in
// the same chapter teaser. Click nodes to build a multi-selection; sidebar
// shows the union of connections. Open profile from sidebar only.
function GraphView({ onOpenDossier }) {
  const mob = useBP() === 'mobile';
  const canvasRef    = React.useRef(null);
  const simRef       = React.useRef(null);
  const rafRef       = React.useRef(null);
  const containerRef = React.useRef(null);
  const frameRef     = React.useRef(0);
  const simReadyRef  = React.useRef(false);

  // Refs read every frame — mutating these does NOT restart the loop
  const pinnedRef  = React.useRef(new Set()); // selected node numbers
  const hoveredRef = React.useRef(null);
  const dimsRef    = React.useRef({ w: 900, h: 560 });

  // React state drives sidebar only
  const [pinnedSet, setPinnedSet] = React.useState(new Set());
  const [hoveredN,  setHoveredN]  = React.useState(null);
  const [dims, setDims] = React.useState({ w: 900, h: 560 });

  // Build edges & adjacency — memoized once
  // Co-citation: voices sharing the same chapter are co-cited.
  // Falls back to teaser cites if present, otherwise uses chapter co-membership.
  const { edges, adjMap } = React.useMemo(() => {
    const list = [];
    // Try TEASERS.cites first
    let hasCites = false;
    if (window.TEASERS) {
      Object.values(window.TEASERS).forEach(t => {
        if (!t.cites) return;
        hasCites = true;
        const ns = t.cites.map(c => c.n).filter(n => n >= 1 && n <= 81);
        for (let i = 0; i < ns.length; i++)
          for (let j = i + 1; j < ns.length; j++)
            list.push([ns[i], ns[j]]);
      });
    }
    // Fallback: co-membership in the same chapter
    if (!hasCites && window.INTERVIEWS) {
      const byChapter = {};
      window.INTERVIEWS.forEach(iv => {
        if (!byChapter[iv.ch]) byChapter[iv.ch] = [];
        byChapter[iv.ch].push(iv.n);
      });
      Object.values(byChapter).forEach(ns => {
        for (let i = 0; i < ns.length; i++)
          for (let j = i + 1; j < ns.length; j++)
            list.push([ns[i], ns[j]]);
      });
    }
    const seen = new Set();
    const edges = list.filter(([a, b]) => {
      const k = `${Math.min(a,b)}-${Math.max(a,b)}`;
      if (seen.has(k)) return false;
      seen.add(k); return true;
    });
    const adjMap = {};
    edges.forEach(([a, b]) => {
      if (!adjMap[a]) adjMap[a] = [];
      if (!adjMap[b]) adjMap[b] = [];
      adjMap[a].push(b); adjMap[b].push(a);
    });
    return { edges, adjMap };
  }, []);

  // Store edges/adj in refs so the loop can read without deps
  const edgesRef  = React.useRef(edges);
  const adjRef    = React.useRef(adjMap);
  React.useEffect(() => { edgesRef.current = edges; adjRef.current = adjMap; }, [edges, adjMap]);

  // Resize observer — on first fire, seeds the sim with real dimensions.
  // Subsequent fires just rescale the canvas without re-seeding.
  React.useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver(entries => {
      const { width } = entries[0].contentRect;
      const h = Math.round(width * 0.7);
      dimsRef.current = { w: width, h };
      setDims({ w: width, h });
      if (!simReadyRef.current) {
        simReadyRef.current = true;
        const nodes = window.INTERVIEWS.map((v, i) => ({
          n: v.n, v,
          x: width/2 + (Math.random()-0.5)*width*0.55,
          y: h/2     + (Math.random()-0.5)*h*0.45,
          vx: 0, vy: 0,
          phase: (i / 81) * Math.PI * 2,
        }));
        const posById = {};
        nodes.forEach(n => { posById[n.n] = n; });
        simRef.current = { nodes, posById };
        frameRef.current = 0;
      }
    });
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  // ── Main animation loop — runs once, NEVER restarts ──────────────
  React.useEffect(() => {
    let cancelled = false;
    const R = 13;

    const tick = () => {
      if (cancelled) return;
      rafRef.current = requestAnimationFrame(tick);

      const sim = simRef.current;
      const canvas = canvasRef.current;
      if (!sim || !canvas) return;

      const { w, h } = dimsRef.current;
      const { nodes, posById } = sim;
      const frame = ++frameRef.current;

      // ── Physics — original design constants + drift + collision ──────
      const t = frame * 0.0028; // time for drift
      // Alpha: matches design file exactly — strong settle, then very low residual
      const alpha = frame < 300 ? 0.4 : frame < 600 ? 0.1 : 0.02;
      const edges = edgesRef.current;

      // Repulsion + hard collision (never overlap)
      const MIN_DIST = R * 2 + 6;
      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 = b.x - a.x, dy = b.y - a.y;
          const dist2 = dx*dx + dy*dy || 0.01;
          const dist  = Math.sqrt(dist2);
          // Long-range repulsion — design original: 1200/dist²
          const force = (1200 / dist2) * alpha;
          const fx = (dx/dist)*force, fy = (dy/dist)*force;
          a.vx -= fx; a.vy -= fy;
          b.vx += fx; b.vy += fy;
          // Hard collision — positional correction so nodes never overlap
          if (dist < MIN_DIST) {
            const overlap = (MIN_DIST - dist) / 2;
            const nx = dx / dist, ny = dy / dist;
            a.x -= nx * overlap; a.y -= ny * overlap;
            b.x += nx * overlap; b.y += ny * overlap;
            const relVx = b.vx - a.vx, relVy = b.vy - a.vy;
            const dot = relVx*nx + relVy*ny;
            if (dot < 0) {
              a.vx += dot * nx * 0.5; a.vy += dot * ny * 0.5;
              b.vx -= dot * nx * 0.5; b.vy -= dot * ny * 0.5;
            }
          }
        }
      }
      // Spring attraction — design original: ideal 120px, strength 0.04
      edges.forEach(([an, bn]) => {
        const a = posById[an], b = posById[bn];
        if (!a || !b) return;
        const dx = b.x-a.x, dy = b.y-a.y;
        const dist = Math.max(Math.sqrt(dx*dx+dy*dy), 1);
        const force = ((dist - 120) / dist) * 0.04 * alpha;
        a.vx += dx*force; a.vy += dy*force;
        b.vx -= dx*force; b.vy -= dy*force;
      });

      nodes.forEach(n => {
        // Centre gravity — design original: 0.002
        n.vx += (w/2 - n.x) * 0.002 * alpha;
        n.vy += (h/2 - n.y) * 0.002 * alpha;
        // Perpetual lava-lamp drift — unique per node, always alive
        const driftStr = frame > 300 ? 0.15 : 0;
        n.vx += Math.cos(t + n.phase) * driftStr;
        n.vy += Math.sin(t + n.phase * 1.3) * driftStr;
        // Damping — design original: 0.85
        n.vx *= 0.85; n.vy *= 0.85;
        const spd = Math.sqrt(n.vx*n.vx + n.vy*n.vy);
        if (spd > 12) { n.vx *= 12/spd; n.vy *= 12/spd; }
        n.x  = Math.max(R+8, Math.min(w-R-8, n.x + n.vx));
        n.y  = Math.max(R+8, Math.min(h-R-26, n.y + n.vy));
      });

      // ── Draw ──────────────────────────────────────────
      const pinneds = pinnedRef.current;
      const hovered = hoveredRef.current;
      const adj     = adjRef.current;

      const focusNs = new Set([...pinneds, ...(hovered ? [hovered] : [])]);
      const nbrs = new Set();
      focusNs.forEach(fn => { (adj[fn] || []).forEach(n => nbrs.add(n)); });
      focusNs.forEach(fn => nbrs.delete(fn));

      const hasFocus = focusNs.size > 0;

      if (canvas.width !== Math.round(w)) canvas.width = Math.round(w);
      if (canvas.height !== Math.round(h)) canvas.height = Math.round(h);

      const ctx = canvas.getContext('2d');
      const bg = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim() || '#fafaf9';
      ctx.clearRect(0, 0, w, h);
      ctx.fillStyle = bg;
      ctx.fillRect(0, 0, w, h);

      // Edges
      edgesRef.current.forEach(([an, bn]) => {
        const a = posById[an], b = posById[bn];
        if (!a || !b) return;
        const aPinned = pinneds.has(an) || an === hovered;
        const bPinned = pinneds.has(bn) || bn === hovered;
        const isActive = aPinned || bPinned;
        ctx.beginPath();
        ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
        ctx.strokeStyle = isActive ? 'rgba(210,35,35,0.75)' : 'rgba(0,0,0,0.055)';
        ctx.lineWidth   = isActive ? 1.5 : 0.7;
        ctx.stroke();
      });

      // Nodes
      nodes.forEach(n => {
        const isPinned  = pinneds.has(n.n);
        const isHovered = n.n === hovered;
        const isNbr     = nbrs.has(n.n);
        const isDim     = hasFocus && !isPinned && !isHovered && !isNbr;
        const tier      = n.v.tier;

        const baseA = tier === 'A' ? [100,140,220] : tier === 'P' ? [210,110,70] : [80,170,110];
        const [r,g,b] = baseA;

        // Hollow circles: tier colour as the outline, background as the fill.
        const stroke = isDim
          ? `rgba(${r},${g},${b},0.2)`
          : isPinned
          ? 'rgba(200,30,30,0.95)'
          : isHovered
          ? `rgb(${Math.round(r*0.6)},${Math.round(g*0.6)},${Math.round(b*0.6)})`
          : isNbr
          ? `rgba(${r},${g},${b},0.95)`
          : `rgba(${r},${g},${b},0.8)`;

        const lineWidth = isPinned ? 2.5 : isHovered ? 2 : isNbr ? 1.6 : 1.3;

        const radius = isPinned ? R+5 : isHovered ? R+3 : isNbr ? R+1 : R-2;

        ctx.beginPath();
        ctx.arc(n.x, n.y, radius, 0, Math.PI*2);
        ctx.fillStyle = bg;
        ctx.fill();
        ctx.strokeStyle = stroke;
        ctx.lineWidth = lineWidth;
        ctx.stroke();

        const showLabel = isPinned || isHovered || isNbr || !hasFocus;
        if (showLabel && !isDim) {
          const label = n.v.name.split(' ').pop();
          ctx.fillStyle = (isPinned || isHovered) ? 'rgba(180,20,20,0.9)' : 'rgba(0,0,0,0.6)';
          ctx.font = (isPinned || isHovered)
            ? '600 10px "JetBrains Mono", monospace'
            : '9px "JetBrains Mono", monospace';
          ctx.textAlign = 'center';
          ctx.fillText(label, n.x, n.y + radius + 11);
        }
      });
    };

    rafRef.current = requestAnimationFrame(tick);
    return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
  }, []);

  // Mouse hit test
  const handleMouse = React.useCallback((e, click) => {
    const canvas = canvasRef.current;
    if (!canvas || !simRef.current) return;
    const rect = canvas.getBoundingClientRect();
    const { w, h } = dimsRef.current;
    const mx = (e.clientX - rect.left) * (w / rect.width);
    const my = (e.clientY - rect.top)  * (h / rect.height);
    const HIT_R = 20;
    let hit = null;
    simRef.current.nodes.forEach(n => {
      const dx = n.x-mx, dy = n.y-my;
      if (dx*dx+dy*dy < HIT_R*HIT_R) hit = n.n;
    });

    if (click) {
      if (hit !== null) {
        const next = new Set(pinnedRef.current);
        if (next.has(hit)) next.delete(hit); else next.add(hit);
        pinnedRef.current = next;
        setPinnedSet(new Set(next));
      }
    } else {
      hoveredRef.current = hit;
      setHoveredN(hit);
    }
    if (canvas) canvas.style.cursor = hit ? 'pointer' : 'default';
  }, []);

  const handleTouch = React.useCallback((e, type) => {
    const touch = e.touches[0] || e.changedTouches[0];
    if (!touch) return;
    e.preventDefault();
    handleMouse({ clientX: touch.clientX, clientY: touch.clientY }, type === 'end');
  }, [handleMouse]);

  // Sidebar data
  const pinnedVoices = [...pinnedSet].map(n => window.INTERVIEWS[n-1]).filter(Boolean);
  const connectedNs  = new Set();
  pinnedSet.forEach(fn => { (adjMap[fn] || []).forEach(n => { if (!pinnedSet.has(n)) connectedNs.add(n); }); });
  if (hoveredN && !pinnedSet.has(hoveredN)) {
    (adjMap[hoveredN] || []).forEach(n => { if (!pinnedSet.has(n)) connectedNs.add(n); });
  }
  const connectedVoices = [...connectedNs].map(n => window.INTERVIEWS[n-1]).filter(Boolean)
    .sort((a,b) => a.n - b.n);

  const anyFocus = pinnedSet.size > 0 || hoveredN;
  const hoverVoice = hoveredN ? window.INTERVIEWS[hoveredN-1] : null;
  const tierLabel = t => t === 'A' ? 'ACADEMIC' : t === 'P' ? 'PRACTITIONER' : 'VISIONARY';

  return (
    <div>
      {/* Legend + hint */}
      <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
        marginBottom: 12, display: 'flex', gap: 20, alignItems: 'center', flexWrap: 'wrap' }}>
        {[['A','rgba(100,140,220,0.7)','ACADEMIC'],['P','rgba(210,110,70,0.7)','PRACTITIONER'],['V','rgba(80,170,110,0.7)','VISIONARY']].map(([t,c,l]) => (
          <span key={t} style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
            <span style={{ width: 9, height: 9, borderRadius: '50%', background: c, display: 'inline-block' }} />
            {l}
          </span>
        ))}
        <span style={{ marginLeft: 'auto', color: 'var(--sub2)' }}>
          {pinnedSet.size > 0
            ? `${pinnedSet.size} selected · ${connectedNs.size} connections · click graph to add/remove`
            : 'Click nodes to select · build connections in sidebar'}
        </span>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: mob ? '1fr' : '1fr 280px', border: '1px solid var(--rule)' }}>
        {/* Canvas */}
        <div ref={containerRef} style={{ background: 'var(--bg)', position: 'relative', borderRight: mob ? 'none' : '1px solid var(--rule)', borderBottom: mob ? '1px solid var(--rule)' : 'none' }}>
          <canvas ref={canvasRef} width={dims.w} height={dims.h}
            style={{ width: '100%', height: 'auto', display: 'block', touchAction: 'none' }}
            onMouseMove={e => handleMouse(e, false)}
            onMouseLeave={() => { hoveredRef.current = null; setHoveredN(null); }}
            onClick={e => handleMouse(e, true)}
            onTouchMove={e => handleTouch(e, 'move')}
            onTouchEnd={e => handleTouch(e, 'end')} />
        </div>

        {/* Right panel */}
        <div style={{ background: 'var(--paper)', display: 'flex', flexDirection: 'column',
          minHeight: mob ? 0 : 400, overflow: 'hidden' }}>

          {/* Selected section */}
          <div style={{ borderBottom: '1px solid var(--rule)', padding: '14px 16px' }}>
            <div className="mono" style={{ fontSize: 9, letterSpacing: '0.06em',
              color: 'var(--sub)', marginBottom: pinnedVoices.length ? 10 : 0 }}>
              SELECTED · {pinnedVoices.length}
            </div>
            {pinnedVoices.length === 0 && (
              <div style={{ fontSize: 12, color: 'var(--sub2)', lineHeight: 1.6, marginTop: 6 }}>
                {hoverVoice
                  ? <><strong style={{ color: 'var(--ink)', fontSize: 13 }}>{hoverVoice.name}</strong><br/>
                      <span className="mono" style={{ fontSize: 9, color: 'var(--sub)' }}>{tierLabel(hoverVoice.tier)} · ch{hoverVoice.ch}</span><br/>
                      <span style={{ color: 'var(--sub2)', fontSize: 11 }}>Click to select</span></>
                  : 'Click any node in the graph to select it.'}
              </div>
            )}
            {pinnedVoices.map(v => (
              <div key={v.n} style={{ display: 'flex', alignItems: 'center', gap: 8,
                marginBottom: 6, justifyContent: 'space-between' }}>
                <div style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0 }}>
                  <span className="mono" style={{ fontSize: 8, color: 'var(--paper)',
                    background: 'rgba(200,30,30,0.85)', padding: '2px 4px', flexShrink: 0 }}>
                    {String(v.n).padStart(2,'0')}
                  </span>
                  <span style={{ fontSize: 12, fontWeight: 500, letterSpacing: '-0.01em',
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {v.name}
                  </span>
                </div>
                <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
                  <button onClick={() => onOpenDossier && onOpenDossier(v.n)}
                    className="mono" style={{ fontSize: 8, padding: '3px 6px',
                      background: 'var(--ink)', color: 'var(--paper)',
                      border: 'none', cursor: 'pointer', letterSpacing: '0.04em' }}>
                    PROFILE
                  </button>
                  <button onClick={() => {
                      const next = new Set(pinnedRef.current);
                      next.delete(v.n);
                      pinnedRef.current = next;
                      setPinnedSet(new Set(next));
                    }}
                    className="mono" style={{ fontSize: 8, padding: '3px 5px',
                      background: 'transparent', color: 'var(--sub)',
                      border: '1px solid var(--rule)', cursor: 'pointer' }}>
                    ✕
                  </button>
                </div>
              </div>
            ))}
          </div>

          {/* Connections section */}
          <div style={{ flex: 1, overflow: 'auto', padding: '14px 16px' }}>
            <div className="mono" style={{ fontSize: 9, letterSpacing: '0.06em',
              color: 'var(--sub)', marginBottom: 10 }}>
              {anyFocus ? `CONNECTIONS · ${connectedVoices.length}` : 'CONNECTIONS'}
            </div>
            {!anyFocus && (
              <div style={{ fontSize: 11, color: 'var(--sub2)', lineHeight: 1.6 }}>
                Select a node to see who they're co-cited with across the 9 chapters.
              </div>
            )}
            {connectedVoices.map(v => (
              <div key={v.n} style={{ borderTop: '1px solid var(--rule)',
                padding: '8px 0', display: 'flex', alignItems: 'center',
                gap: 8, justifyContent: 'space-between' }}>
                <div style={{ minWidth: 0 }}>
                  <div style={{ display: 'flex', gap: 7, alignItems: 'baseline' }}>
                    <span className="mono" style={{ fontSize: 8, color: 'var(--sub)', flexShrink: 0 }}>
                      {String(v.n).padStart(2,'0')}
                    </span>
                    <span style={{ fontSize: 12, fontWeight: 500, letterSpacing: '-0.01em', lineHeight: 1.2 }}>
                      {v.name}
                    </span>
                  </div>
                  <div className="mono" style={{ fontSize: 9, color: 'var(--sub)', marginTop: 2 }}>
                    {tierLabel(v.tier)} · ch{v.ch}
                  </div>
                </div>
                <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
                  <button onClick={() => {
                      const next = new Set(pinnedRef.current);
                      next.add(v.n);
                      pinnedRef.current = next;
                      setPinnedSet(new Set(next));
                    }}
                    className="mono" style={{ fontSize: 8, padding: '3px 5px',
                      background: 'transparent', color: 'var(--sub)',
                      border: '1px solid var(--rule)', cursor: 'pointer', letterSpacing: '0.04em' }}>
                    +
                  </button>
                  <button onClick={() => onOpenDossier && onOpenDossier(v.n)}
                    className="mono" style={{ fontSize: 8, padding: '3px 6px',
                      background: 'var(--ink)', color: 'var(--paper)',
                      border: 'none', cursor: 'pointer', letterSpacing: '0.04em' }}>
                    PROFILE
                  </button>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}
// ─── Table of M.O. ────────────────────────────────────
// 26 classifiers × voice slots. Toggle: ALL CLASSIFIERS / PRIME PAIRS.
function MOTable({ onOpenDossier }) {
  const mob = useBP() === 'mobile';
  const [mode, setMode] = React.useState('all');   // 'all' | 'prime'
  const [expandedC, setExpanded] = React.useState(null);

  // Build index once
  const idx = React.useMemo(() => window.buildMOIndex(), []);

  // Sort classifiers by number of voices (descending)
  const classifiers = React.useMemo(() => {
    return Object.entries(idx)
      .map(([name, ns]) => ({ name, ns, count: ns.length }))
      .sort((a, b) => b.count - a.count);
  }, [idx]);

  // Prime pairs: classifiers where exactly 2 voices appear (or expand to show pairs)
  const primePairs = React.useMemo(() => {
    // Find all pairs of voices that share multiple classifiers
    const pairs = {};
    classifiers.forEach(({ name, ns }) => {
      for (let i = 0; i < ns.length; i++) {
        for (let j = i + 1; j < ns.length; j++) {
          const key = `${Math.min(ns[i], ns[j])}-${Math.max(ns[i], ns[j])}`;
          if (!pairs[key]) pairs[key] = { a: ns[i], b: ns[j], shared: [] };
          pairs[key].shared.push(name);
        }
      }
    });
    return Object.values(pairs)
      .filter(p => p.shared.length >= 2)
      .sort((a, b) => b.shared.length - a.shared.length);
  }, [classifiers]);

  const totalSlots = classifiers.reduce((s, c) => s + c.count, 0);

  return (
    <div>
      {/* Header bar */}
      <div style={{
        display: 'flex', justifyContent: 'space-between',
        alignItems: mob ? 'flex-start' : 'center',
        flexDirection: mob ? 'column' : 'row',
        gap: mob ? 10 : 0,
        padding: '14px 0', marginBottom: 16,
        borderBottom: '1px solid var(--ink)',
      }}>
        <div>
          <span className="mono" style={{ fontSize: 11, color: 'var(--ink)',
            letterSpacing: '-0.005em', fontWeight: 500 }}>
            TABLE OF M.O.
          </span>
          <span className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            marginLeft: mob ? 10 : 16 }}>
            {classifiers.length} CL · {totalSlots} SLOTS
          </span>
        </div>
        <div style={{ display: 'flex', gap: 0, border: '1px solid var(--ink)' }}>
          {[['all', mob ? 'ALL' : 'ALL CLASSIFIERS'], ['prime', mob ? 'PRIME' : 'PRIME PAIRS']].map(([k, l]) => (
            <button key={k} onClick={() => setMode(k)} className="mono" style={{
              background: mode === k ? 'var(--ink)' : 'transparent',
              color: mode === k ? 'var(--paper)' : 'var(--ink)',
              border: 'none', borderRight: k === 'all' ? '1px solid var(--ink)' : 'none',
              padding: '8px 14px', cursor: 'pointer', fontSize: 10,
              letterSpacing: '0.02em',
            }}>{l}</button>
          ))}
        </div>
      </div>

      {mode === 'all' ? (
        /* ── All classifiers list ── */
        <div>
          {classifiers.map(({ name, ns, count }) => {
            const isOpen = expandedC === name;
            return (
              <div key={name} style={{ borderBottom: '1px solid var(--rule)' }}>
                <div
                  onClick={() => setExpanded(isOpen ? null : name)}
                  style={{ padding: mob ? '10px 0' : '12px 0', cursor: 'pointer' }}
                >
                  {mob ? (
                    /* Mobile: left col (name + chips), badge vertically centred on right */
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <span style={{ fontSize: 14, letterSpacing: '-0.01em',
                          fontWeight: isOpen ? 500 : 400, lineHeight: 1.2,
                          display: 'block', marginBottom: 7 }}>{name}</span>
                        <div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
                          {ns.map(n => {
                            const iv = window.INTERVIEWS[n - 1];
                            return (
                              <button key={n}
                                onClick={e => { e.stopPropagation(); onOpenDossier && onOpenDossier(n); }}
                                className="mono" style={{
                                  background: 'none',
                                  border: `1px solid var(${iv.group.ink})`,
                                  color: `var(${iv.group.ink})`,
                                  padding: '2px 5px', fontSize: 9, cursor: 'pointer',
                                  letterSpacing: '-0.005em', whiteSpace: 'nowrap',
                                }}>
                                {String(n).padStart(2,'0')}·{iv.tier}
                              </button>
                            );
                          })}
                        </div>
                      </div>
                      <span className="mono" style={{
                        fontSize: 11, color: 'var(--paper)', flexShrink: 0,
                        background: count >= 8 ? 'var(--accent)' : 'var(--sub)',
                        padding: '3px 8px', minWidth: 28, textAlign: 'center',
                      }}>+{count}</span>
                    </div>
                  ) : (
                    <div style={{
                      display: 'grid', gridTemplateColumns: '20px 1fr auto auto',
                      gap: 16, alignItems: 'center',
                    }}>
                      <span className="mono" style={{ fontSize: 9, color: 'var(--sub2)' }}>—</span>
                      <span style={{ fontSize: 14, letterSpacing: '-0.01em',
                        fontWeight: isOpen ? 500 : 400 }}>{name}</span>
                      <div style={{ display: 'flex', gap: 3, flexWrap: 'wrap',
                        justifyContent: 'flex-end', maxWidth: 400 }}>
                        {ns.slice(0, 8).map(n => {
                          const iv = window.INTERVIEWS[n - 1];
                          return (
                            <button key={n}
                              onClick={e => { e.stopPropagation(); onOpenDossier && onOpenDossier(n); }}
                              className="mono" style={{
                                background: 'none',
                                border: `1px solid var(${iv.group.ink})`,
                                color: `var(${iv.group.ink})`,
                                padding: '2px 5px', fontSize: 9, cursor: 'pointer',
                                letterSpacing: '-0.005em', whiteSpace: 'nowrap',
                              }}>
                              {String(n).padStart(2,'0')}·{iv.tier}
                            </button>
                          );
                        })}
                        {ns.length > 8 && (
                          <span className="mono" style={{
                            fontSize: 9, color: 'var(--paper)',
                            background: 'var(--accent)', padding: '2px 6px', fontWeight: 600,
                          }}>+{ns.length - 8}</span>
                        )}
                      </div>
                      <span className="mono" style={{
                        fontSize: 11, color: 'var(--paper)',
                        background: count >= 8 ? 'var(--accent)' : 'var(--sub)',
                        padding: '3px 8px', minWidth: 28, textAlign: 'center',
                      }}>+{count}</span>
                    </div>
                  )}
                </div>
                {/* Expanded: show all voices with names */}
                {isOpen && (
                  <div style={{ padding: '8px 0 20px 36px', borderTop: '1px solid var(--rule)' }}>
                    <div className="mono" style={{ fontSize: 9, color: 'var(--sub)',
                      letterSpacing: '0.04em', marginBottom: 10 }}>
                      {count} VOICES SHARE THIS CLASSIFIER
                    </div>
                    <div style={{ display: 'grid',
                      gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
                      gap: 6 }}>
                      {ns.map(n => {
                        const iv = window.INTERVIEWS[n - 1];
                        return (
                          <button key={n}
                            onClick={() => onOpenDossier && onOpenDossier(n)}
                            style={{
                              background: `var(${iv.group.varCSS})`,
                              border: `1px solid var(${iv.group.ink})`,
                              padding: '8px 10px', cursor: 'pointer',
                              textAlign: 'left', fontFamily: 'inherit',
                              display: 'flex', gap: 8, alignItems: 'baseline',
                            }}>
                            <span className="mono" style={{ fontSize: 9,
                              color: `var(${iv.group.ink})` }}>
                              {String(n).padStart(2,'0')}
                            </span>
                            <span style={{ fontSize: 12, fontWeight: 500,
                              letterSpacing: '-0.01em' }}>{iv.name}</span>
                          </button>
                        );
                      })}
                    </div>
                  </div>
                )}
              </div>
            );
          })}
          <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            padding: '16px 0', textAlign: 'right' }}>
            {classifiers.length} CLASSIFIERS · {totalSlots} OPERATIVE SLOTS
          </div>
        </div>
      ) : (
        /* ── Prime pairs ── */
        <div>
          <div style={{ marginBottom: 20, padding: '14px 16px',
            background: 'var(--ink)', color: 'var(--paper)' }}>
            <div className="mono" style={{ fontSize: 10,
              color: 'oklch(0.65 0.1 250)', marginBottom: 8 }}>
              PRIME PAIRS — THE TWIN PRIMES OF KNOWWARE
            </div>
            <div style={{ fontSize: 17, letterSpacing: '-0.015em',
              lineHeight: 1.4, maxWidth: '72ch' }}>
              Not interviews — the Third Body made visible. These are the
              digital twins: voices from different fields whose knowledge
              coordinates so precisely they could only have been assembled
              by the same intelligence that assembled the rest.
            </div>
            <div className="mono" style={{ fontSize: 10,
              color: 'oklch(0.65 0.1 250)', marginTop: 10 }}>
              {primePairs.length} PRIME PAIRS IDENTIFIED · SORTED BY SHARED CLASSIFIER COUNT
            </div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
            {primePairs.slice(0, 40).map((p, i) => {
              const a = window.INTERVIEWS[p.a - 1];
              const b = window.INTERVIEWS[p.b - 1];
              return (
                <div key={i} style={{
                  display: 'grid', gridTemplateColumns: mob ? '1fr auto 1fr' : '32px 1fr auto 1fr auto',
                  gap: mob ? 8 : 12, padding: '12px 0',
                  borderBottom: '1px solid var(--rule)',
                  alignItems: 'center',
                }}>
                  {!mob && <span className="mono" style={{ fontSize: 10,
                    color: 'var(--sub2)' }}>{String(i + 1).padStart(2,'0')}</span>}
                  <button onClick={() => onOpenDossier && onOpenDossier(p.a)}
                    style={{ background: `var(${a.group.varCSS})`,
                      border: `1px solid var(${a.group.ink})`,
                      padding: mob ? '6px 8px' : '8px 12px', cursor: 'pointer',
                      textAlign: 'left', fontFamily: 'inherit', minWidth: 0 }}>
                    <div className="mono" style={{ fontSize: 9,
                      color: `var(${a.group.ink})` }}>
                      {String(p.a).padStart(2,'0')}·{a.tier}
                    </div>
                    <div style={{ fontSize: mob ? 11 : 13, fontWeight: 500,
                      letterSpacing: '-0.01em', marginTop: 2,
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.name}</div>
                    {!mob && <div className="mono" style={{ fontSize: 9,
                      color: 'var(--sub)', marginTop: 2 }}>{a.affiliation}</div>}
                  </button>
                  <div style={{ textAlign: 'center', flexShrink: 0 }}>
                    <div style={{ fontSize: mob ? 14 : 18, color: 'var(--accent)' }}>⟷</div>
                    <div className="mono" style={{ fontSize: 9,
                      color: 'var(--accent)', marginTop: 2 }}>
                      {p.shared.length}
                    </div>
                  </div>
                  <button onClick={() => onOpenDossier && onOpenDossier(p.b)}
                    style={{ background: `var(${b.group.varCSS})`,
                      border: `1px solid var(${b.group.ink})`,
                      padding: mob ? '6px 8px' : '8px 12px', cursor: 'pointer',
                      textAlign: 'left', fontFamily: 'inherit', minWidth: 0 }}>
                    <div className="mono" style={{ fontSize: 9,
                      color: `var(${b.group.ink})` }}>
                      {String(p.b).padStart(2,'0')}·{b.tier}
                    </div>
                    <div style={{ fontSize: mob ? 11 : 13, fontWeight: 500,
                      letterSpacing: '-0.01em', marginTop: 2,
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.name}</div>
                    {!mob && <div className="mono" style={{ fontSize: 9,
                      color: 'var(--sub)', marginTop: 2 }}>{b.affiliation}</div>}
                  </button>
                  {!mob && <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4,
                    maxWidth: 200, justifyContent: 'flex-end' }}>
                    {p.shared.slice(0, 3).map((c, ci) => (
                      <span key={ci} className="mono" style={{
                        fontSize: 8, color: 'var(--sub)',
                        border: '1px solid var(--rule)',
                        padding: '2px 6px', lineHeight: 1.4,
                      }}>{c}</span>
                    ))}
                    {p.shared.length > 3 && (
                      <span className="mono" style={{ fontSize: 8,
                        color: 'var(--accent)' }}>+{p.shared.length - 3}</span>
                    )}
                  </div>}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function EmptyStrip() {
  const mob = useBP() === 'mobile';
  return (
    <div style={{ display: 'grid',
      gridTemplateColumns: mob ? '1fr auto' : '200px 1fr auto', gap: mob ? 12 : 20, alignItems: 'center',
      width: '100%', padding: mob ? '12px 16px' : '16px 20px' }}>
      {!mob && <div className="mono" style={{ fontSize: 10, color: 'var(--sub2)',
        letterSpacing: '-0.005em' }}>HOVER TO PREVIEW</div>}
      <div style={{ fontSize: 13, color: 'var(--sub)', lineHeight: 1.4 }}>
        {mob ? 'Tap any cell to open a dossier.' : 'Each cell is one of the eighty-one syntheses. Colour marks the tier — blue for academics, terracotta for practitioners, sage for visionaries.'}
      </div>
      <div className="mono" style={{ fontSize: 10, color: 'var(--sub2)', flexShrink: 0 }}>
        81 · 09 · 03
      </div>
    </div>
  );
}

function VoiceStrip({ v }) {
  const bp = useBP();
  const mob = bp === 'mobile';
  const ch = window.CHAPTERS.find(c => c.n === v.ch);
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: mob ? '56px 1fr auto' : '96px 220px 1fr auto',
      gap: mob ? 10 : 20, alignItems: 'center', width: '100%',
      padding: mob ? '10px 16px' : '14px 20px',
      color: `var(${v.group.ink})`,
      overflow: 'hidden',
    }}>
      <div style={{ fontSize: mob ? 40 : 68, fontWeight: 500, letterSpacing: '-0.05em',
        lineHeight: 0.9, color: 'var(--ink)' }}>{v.sym}</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 3, minWidth: 0 }}>
        <div className="mono" style={{ fontSize: 10, opacity: 0.85 }}>
          № {String(v.n).padStart(2, '0')} · Tier {v.tier} · {v.minutes}m
        </div>
        <div style={{ fontSize: mob ? 14 : 18, fontWeight: 500, letterSpacing: '-0.015em',
          color: 'var(--ink)', lineHeight: 1.15,
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{v.name}</div>
        {!mob && (
          <div className="mono" style={{ fontSize: 11, opacity: 0.8, lineHeight: 1.3 }}>
            {v.affiliation}
          </div>
        )}
      </div>
      {!mob && (
        <div style={{ fontSize: 15, lineHeight: 1.45, color: 'var(--ink)',
          letterSpacing: '-0.01em', maxWidth: '72ch' }}>
          <span style={{ fontStyle: 'italic' }}>"{v.themes[0]}"</span> as inherited relationship — not a resource but a responsibility.
          <span className="mono" style={{
            fontSize: 10, opacity: 0.7, marginLeft: 8 }}>
            — {v.group.name}
          </span>
        </div>
      )}
      <div className="mono" style={{ fontSize: 10, opacity: 0.8,
        textAlign: 'right', lineHeight: 1.5 }}>
        ch{v.ch}<br/>
        {ch ? ch.title.split(' ').slice(0, 3).join(' ') : ''}
      </div>
    </div>
  );
}

function HorizontalLegend({ active, setActive }) {
  const mob = useBP() === 'mobile';
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: `repeat(${window.GROUPS.length}, 1fr)`,
      border: '1px solid var(--rule)',
      background: 'var(--paper)',
    }}>
      {window.GROUPS.map((g, i) => {
        const count = window.INTERVIEWS.filter(x => x.groupId === g.id).length;
        const isActive = active === g.id;
        return (
          <button key={g.id}
            onMouseEnter={() => setActive(g.id)}
            onMouseLeave={() => setActive(null)}
            onClick={() => setActive(isActive ? null : g.id)}
            style={{
              background: isActive ? `var(${g.varCSS})` : 'var(--paper)',
              borderRight: i < window.GROUPS.length - 1 ? '1px solid var(--rule)' : 'none',
              borderTop: `4px solid var(${g.varCSS})`,
              borderBottom: 'none', borderLeft: 'none',
              padding: mob ? '10px 10px 8px' : '14px 18px 12px', cursor: 'pointer', textAlign: 'left',
              fontFamily: 'inherit', display: 'flex', flexDirection: 'column',
              gap: mob ? 4 : 6, transition: 'background .15s', minWidth: 0,
            }}>
            <div className="mono" style={{ fontSize: mob ? 9 : 11,
              color: `var(${g.ink})`, letterSpacing: '-0.005em',
              display: 'flex', justifyContent: 'space-between' }}>
              <span>Tier {g.key}</span>
              <span style={{ color: 'var(--sub)' }}>{String(count).padStart(2, '0')}</span>
            </div>
            <div style={{ fontSize: mob ? 14 : 18, letterSpacing: '-0.015em',
              color: 'var(--ink)', fontWeight: 500 }}>
              {g.name}
            </div>
            {!mob && <div style={{ fontSize: 12, color: 'var(--sub)', lineHeight: 1.4 }}>
              {g.blurb}
            </div>}
          </button>
        );
      })}
    </div>
  );
}

function VoiceCard({ v }) {
  return (
    <div style={{ background: `var(${v.group.varCSS})`,
      border: `1px solid var(${v.group.ink})`, padding: 18,
      color: `var(${v.group.ink})`, display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div className="mono" style={{ fontSize: 10, display: 'flex',
        justifyContent: 'space-between', opacity: 0.8 }}>
        <span>№ {String(v.n).padStart(2, '0')}</span>
        <span>{v.group.key} · {v.year}</span>
      </div>
      <div style={{ fontSize: 64, fontWeight: 500, letterSpacing: '-0.05em',
        lineHeight: 0.9, color: 'var(--ink)' }}>{v.sym}</div>
      <div style={{ fontSize: 13, lineHeight: 1.45, color: 'var(--ink)' }}>
        A {v.group.name.toLowerCase().replace(/s$/, '')} based in <strong style={{ fontWeight: 500 }}>{v.city}</strong>.
        Spoke with us for {v.minutes} minutes on {v.themes.join(', ')}.
      </div>
      <div style={{ borderTop: `1px solid var(${v.group.ink})`, paddingTop: 10,
        fontSize: 12, lineHeight: 1.5, color: `var(${v.group.ink})` }}>
        "{v.themes[0]} is not a resource. It is a relationship you inherit —
        and then are responsible for."
      </div>
    </div>
  );
}

const CHAPTER_STATS = {
  '01': [
    { label: 'USERS OBSERVED',    value: '689,003' },
    { label: 'INSTITUTIONS',      value: '3'       },
    { label: 'YEAR',              value: '2014'    },
  ],
  '02': [
    { label: 'BODIES COORDINATED', value: '3'      },
    { label: 'MARKET POSITION',    value: '#1'     },
    { label: 'YEAR',               value: '2007'   },
  ],
  '03': [
    { label: 'LIVING BUILDING',   value: '1'       },
    { label: 'SENSOR TYPES',      value: '3'       },
    { label: 'YEAR',              value: '2014'    },
  ],
  '04': [
    { label: 'HOMICIDE REDUCTION', value: '70%'   },
    { label: 'CITY POPULATION',   value: '6M'      },
    { label: 'YEAR',              value: '1998'    },
  ],
  '05': [
    { label: 'RESTORED MOVEMENT', value: '1 HAND' },
    { label: 'SIGNAL LATENCY',    value: '<100ms'  },
    { label: 'YEAR',              value: '2020'    },
  ],
  '06': [
    { label: 'SECONDS DELAYED',   value: '6'       },
    { label: 'CLASSIFICATIONS',   value: '4'       },
    { label: 'YEAR',              value: '2018'    },
  ],
  '07': [
    { label: 'WEIGHT REDUCTION',  value: '45%'     },
    { label: 'DESIGN ITERATIONS', value: '1,000+'  },
    { label: 'YEAR',              value: '2019'    },
  ],
  '08': [
    { label: 'BODIES IN ORBIT',   value: '3'       },
    { label: 'FORECAST HORIZON',  value: '100 yrs' },
    { label: 'SCALE',             value: '∞'       },
  ],
  '09': [
    { label: 'AI ACCURACY',       value: '95%'     },
    { label: 'YEARS TO DESKILL',  value: '2'       },
    { label: 'YEAR',              value: '2016'    },
  ],
  'X': [
    { label: 'VOICES',            value: '81'      },
    { label: 'CHAPTERS',          value: '9'       },
    { label: 'PATTERN',           value: '1'       },
  ],
};

const SCENE_META = {
  '01': { date: 'JUNE 2014',   year: '2014', sources: ['FACEBOOK', 'CORNELL', 'UCSF'] },
  '02': { date: '2007–2015',   year: '2007', sources: ['NETFLIX', 'AMAZON', 'TOYOTA'] },
  '03': { date: '2014',        year: '2014', sources: ['IAAC', 'BARCELONA'] },
  '04': { date: '1998–2007',   year: '1998', sources: ['BOGOTÁ', 'MOCKUS'] },
  '05': { date: '2020',        year: '2020', sources: ['BATTELLE', 'OHIO STATE', 'NIH'] },
  '06': { date: 'OCT 2018',    year: '2018', sources: ['UBER', 'TEMPE AZ', 'NTSB'] },
  '07': { date: '2019',        year: '2019', sources: ['AUTODESK', 'AIRBUS'] },
  '08': { date: 'UNIVERSAL',   year: '∞',    sources: ['NEWTON', 'LAPLACE', 'POINCARÉ'] },
  '09': { date: '2016–2018',   year: '2016', sources: ['STANFORD', 'RADIOLOGY AI'] },
  'X':  { date: 'NOW',         year: 'NOW',  sources: ['KNOWWARE'] },
};

// ─── Read (v2) ─────────────────────────────────────────
// Two-column with live section rail, running page number, inline footnotes,
// marginal citations pulling from the 81.
function Read({ onOpenReader }) {
  const bp = useBP();
  const mob = bp === 'mobile';
  const tab = bp === 'tablet';
  const [active, setActive] = React.useState(1);
  const [openNote, setOpenNote] = React.useState(null);
  const s = window.SECTIONS[active];
  const pageBase = 18 + active * 28;
  // Pull the specifically-cited voices for this chapter from TEASERS.
  const teaser = (window.TEASERS && window.TEASERS[s.n]) || null;
  const citedNums = (teaser && teaser.cites) ? teaser.cites.map(c => c.n) : [];
  const citedAll = citedNums.map(n => window.INTERVIEWS.find(v => v.n === n)).filter(Boolean);
  const cited = citedAll.slice(0, 4);

  const gridCols = mob ? '1fr' : tab ? '220px 1fr' : '260px 1fr auto';

  return (
    <div>
      {/* Reading chrome — page position + running head */}
      <div style={{
        borderBottom: '1px solid var(--rule)',
        padding: mob ? '8px 16px' : '10px 24px',
        display: 'grid',
        gridTemplateColumns: mob ? '1fr 1fr' : '1fr 1fr 1fr',
        alignItems: 'center', gap: 8,
      }}>
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          ch{s.n} · {s.title.toUpperCase()}
        </div>
        {!mob && (
          <div className="mono" style={{ fontSize: 11, color: 'var(--sub)',
            textAlign: 'center' }}>
            {s.part.toUpperCase()}
          </div>
        )}
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          textAlign: 'right' }}>
          pp. {pageBase}–{pageBase + 27}
        </div>
      </div>

      {/* Manuscript thesis band */}
      <div style={{
        padding: mob ? '28px 16px' : '36px 48px',
        background: 'var(--ink)', color: 'var(--paper)',
        borderBottom: '1px solid var(--rule)',
      }}>
        <p style={{
          margin: 0,
          fontSize: mob ? 'clamp(22px, 6vw, 30px)' : 'clamp(28px, 3vw, 42px)',
          lineHeight: 1.1, letterSpacing: '-0.03em', fontWeight: 500,
        }}>
          The universe runs on three.
          <br />
          <span style={{ fontWeight: 400, color: 'var(--sub)' }}>
            You've been working with two.
          </span>
        </p>
      </div>

      {/* Mobile chapter picker */}
      {mob && (
        <div style={{ borderBottom: '1px solid var(--rule)', padding: '10px 16px' }}>
          <select value={active} onChange={e => setActive(Number(e.target.value))}
            style={{
              width: '100%', padding: '8px 32px 8px 10px',
              border: '1px solid var(--ink)', background: 'var(--paper)',
              fontFamily: 'inherit', fontSize: 13, color: 'var(--ink)',
              cursor: 'pointer', appearance: 'none',
              backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%230a0a0a' stroke-width='1.5' fill='none'/%3E%3C/svg%3E\")",
              backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center',
            }}>
            {window.SECTIONS.map((row, i) => (
              <option key={row.n} value={i}>ch{row.n} · {row.title}</option>
            ))}
          </select>
        </div>
      )}

      <div style={{
        display: 'grid',
        gridTemplateColumns: gridCols,
        minHeight: 'calc(100vh - 240px)',
        borderBottom: '1px solid var(--rule)',
        overflow: 'hidden',
      }}>
        {/* LEFT RAIL — TOC (tablet + desktop) */}
        {!mob && (
          <aside style={{
            borderRight: '1px solid var(--rule)',
            padding: '24px 20px', position: 'sticky', top: 48,
            height: 'fit-content',
          }}>
            <Label style={{ paddingTop: 0, marginBottom: 10 }}>03 · Manuscript</Label>
            <ol style={{ listStyle: 'none', padding: 0, margin: 0 }}>
              {window.SECTIONS.map((row, i) => {
                const isActive = active === i;
                return (
                  <li key={row.n}>
                    <button onClick={() => setActive(i)} style={{
                      width: '100%', textAlign: 'left',
                      background: isActive ? 'var(--ink)' : 'transparent',
                      color: isActive ? 'var(--paper)' : 'var(--ink)',
                      border: 'none', borderBottom: '1px solid var(--rule)',
                      cursor: 'pointer', padding: '9px 8px',
                      display: 'grid', gridTemplateColumns: tab ? '28px 1fr' : '28px 1fr auto', gap: 6,
                      fontFamily: 'inherit', fontSize: tab ? 12 : 13,
                      letterSpacing: '-0.005em', alignItems: 'baseline',
                    }}>
                      <span className="mono" style={{ fontSize: 10,
                        color: isActive ? 'var(--accent-soft)' : 'var(--sub)' }}>
                        ch{row.n}
                      </span>
                      <span>{row.title}</span>
                      {!tab && (
                        <span className="mono" style={{ fontSize: 9,
                          color: isActive ? 'var(--accent-soft)' : 'var(--sub2)' }}>
                          {18 + i * 28}
                        </span>
                      )}
                    </button>
                  </li>
                );
              })}
            </ol>
          </aside>
        )}

        {/* CENTER — reading column */}
        <article data-kw-read style={{
          padding: 0,
          maxWidth: 'var(--read-col, 760px)',
          minWidth: 0, position: 'relative', overflow: 'hidden',
        }}>
          <ChapterTeaser
            chapter={s.n}
            section={s}
            cited={cited[0]}
            openNote={openNote}
            setOpenNote={setOpenNote}
            onOpenReader={onOpenReader}
          />
        </article>

        {/* RIGHT RAIL — reserved for editor tools */}
      </div>

      {/* ── Full-width synthesis band ── */}
      {(() => {
        const SYNTHESIS_QUOTES = {
          '01': "Intelligence doesn't live in humans or machines. It emerges in the coordination between them.",
          '02': "Binary logic was always an approximation. Reality coordinates.",
          '03': "The architecture of intelligence is the architecture of life.",
          '04': "Three-body coordination is not just how systems work, but how they heal.",
          '05': "The boundary between human and machine is already dissolving. The question is how to coordinate the crossing.",
          '06': "Awareness is not a luxury upgrade bolted onto intelligence. It is the coordination layer without which perception and processing cycle in the dark.",
          '07': "Three-body coordination doesn't just improve engineering — it transcends the design space itself.",
          '08': "Scale coordination to the cosmos and new bodies of intelligence appear.",
          '09': "Optimizing capability without coordinating with context is the pattern of civilizational failure.",
          'X':  "Nine chapters. Eighty-one voices. One pattern — now visible.",
        };
        const prev = active > 0 ? window.SECTIONS[active - 1] : null;
        const next = active < window.SECTIONS.length - 1 ? window.SECTIONS[active + 1] : null;
        const interviewCount = window.INTERVIEWS.filter(v => v.ch === s.n).length;
        const diagram = window.TEASERS && window.TEASERS[s.n] && window.TEASERS[s.n].diagram;
        const synthQuote = SYNTHESIS_QUOTES[s.n] || '';
        return (
          <>
            {/* Dark synthesis block — diagram + synthesis share one ink background */}
            <div style={{ background: 'var(--ink)', color: 'var(--paper)' }}>
            {diagram && (
              <CoordinatesSection
                diagram={diagram}
                noBg
                style={{ padding: mob ? '32px 20px 28px' : '48px 64px 40px' }}
              />
            )}
            <div style={{
              padding: mob ? '28px 20px 32px' : '40px 64px 56px',
              borderTop: diagram ? '1px solid oklch(0.22 0.04 250)' : 'none',
            }}>
              <div className="mono" style={{ fontSize: 10, color: 'var(--accent)',
                letterSpacing: '0.08em', marginBottom: 20 }}>
                THE SYNTHESIS
              </div>
              <p style={{
                fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
                fontSize: mob ? 22 : 30, lineHeight: 1.3,
                letterSpacing: '-0.02em', margin: '0 0 36px',
                fontStyle: 'italic', maxWidth: '32ch',
              }}>
                "{synthQuote}"
              </p>
              <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
                <a href="contribute.html" style={{
                  display: 'inline-flex', alignItems: 'center', gap: 10,
                  background: 'var(--accent)', color: '#fff',
                  padding: mob ? '11px 16px' : '13px 20px', textDecoration: 'none',
                  fontFamily: '"JetBrains Mono", ui-monospace, monospace',
                  fontSize: 11, letterSpacing: '0.04em',
                }}>→ READ FULL CHAPTER</a>
                <a href="contribute.html" style={{
                  display: 'inline-flex', alignItems: 'center', gap: 8,
                  background: 'none', color: 'var(--paper)', textDecoration: 'none',
                  padding: mob ? '11px 14px' : '13px 16px',
                  fontFamily: '"JetBrains Mono", ui-monospace, monospace',
                  fontSize: 11, border: '1px solid rgba(240,240,234,0.25)',
                  letterSpacing: '0.02em',
                }}>↗ CONTRIBUTE</a>
              </div>
            </div>
            </div>{/* /outer dark wrapper */}

            {/* Prev / Next chapter navigation */}
            <div style={{
              display: 'grid',
              gridTemplateColumns: prev && next ? '1fr 1fr' : '1fr',
              borderBottom: '1px solid var(--rule)',
            }}>
              {prev && (
                <button onClick={() => { setActive(active - 1); window.scrollTo({ top: 0 }); }}
                  style={{
                    background: 'none', border: 'none',
                    borderRight: next ? '1px solid var(--rule)' : 'none',
                    borderTop: '1px solid var(--rule)',
                    padding: mob ? 20 : 32, cursor: 'pointer',
                    textAlign: 'left', fontFamily: 'inherit',
                    transition: 'background 0.15s',
                  }}
                  onMouseEnter={e => e.currentTarget.style.background = 'var(--accent-soft)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'none'}
                >
                  <div className="mono" style={{ fontSize: 9, color: 'var(--sub2)',
                    letterSpacing: '0.06em', marginBottom: 12 }}>← PREVIOUS CHAPTER</div>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--accent)',
                    marginBottom: 8 }}>CH. {prev.n}</div>
                  <div style={{ fontSize: mob ? 18 : 22, fontWeight: 400,
                    letterSpacing: '-0.025em', lineHeight: 1.2,
                    fontStyle: 'italic', color: 'var(--ink)' }}>{prev.title}</div>
                </button>
              )}
              {next && (
                <button onClick={() => { setActive(active + 1); window.scrollTo({ top: 0 }); }}
                  style={{
                    background: 'none', border: 'none',
                    borderTop: '1px solid var(--rule)',
                    padding: mob ? 20 : 32, cursor: 'pointer',
                    textAlign: 'right', fontFamily: 'inherit',
                    transition: 'background 0.15s',
                  }}
                  onMouseEnter={e => e.currentTarget.style.background = 'var(--accent-soft)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'none'}
                >
                  <div className="mono" style={{ fontSize: 9, color: 'var(--sub2)',
                    letterSpacing: '0.06em', marginBottom: 12 }}>NEXT CHAPTER →</div>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--accent)',
                    marginBottom: 8 }}>CH. {next.n}</div>
                  <div style={{ fontSize: mob ? 18 : 22, fontWeight: 400,
                    letterSpacing: '-0.025em', lineHeight: 1.2,
                    fontStyle: 'italic', color: 'var(--ink)' }}>{next.title}</div>
                </button>
              )}
            </div>

            {/* Chapter footer */}
            <div style={{
              padding: mob ? '12px 20px' : '14px 32px',
              borderBottom: '1px solid var(--rule)',
              display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            }}>
              <button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
                className="mono" style={{ background: 'none', border: 'none',
                cursor: 'pointer', fontSize: 10, color: 'var(--sub2)',
                letterSpacing: '0.04em', padding: 0 }}>← RETURN TO TOP</button>
              <span className="mono" style={{ fontSize: 10, color: 'var(--sub2)',
                letterSpacing: '0.04em' }}>END OF CHAPTER · CH{s.n}</span>
            </div>
          </>
        );
      })()}
    </div>
  );
}

// Chapter teaser — editorial style drawn from the reference designs.
// Ghost chapter number · serif italic pull quote · "coordinates" dark section
// · three-tier voice columns · closing quote · CTA.
function ChapterTeaser({ chapter, section, cited, openNote, setOpenNote, onOpenReader }) {
  const mob = useBP() === 'mobile';
  const tab = useBP() === 'tablet';
  const t = (window.TEASERS && window.TEASERS[chapter]) || null;
  const scene = SCENE_META[chapter] || {};
  const pad = mob ? '28px 20px' : tab ? '32px 32px' : '40px 48px';

  if (!t) {
    return <p style={{ padding: pad, fontSize: 17, lineHeight: 1.65, color: 'var(--sub)' }}>
      Teaser coming soon. This chapter is still being edited.
    </p>;
  }
  const [p1, p2] = t.paras;

  // Use ALL 9 chapter voices, augmented with citation notes where available
  const citeLookup = {};
  (t.cites || []).forEach(c => { citeLookup[c.n] = c.note; });
  const citeList = window.INTERVIEWS
    .filter(v => v.ch === chapter)
    .map(v => ({ ...v, note: citeLookup[v.n] || v.affiliation }));

  const tierA = citeList.filter(v => v.tier === 'A');
  const tierP = citeList.filter(v => v.tier === 'P');
  const tierV = citeList.filter(v => v.tier === 'V');

  return (
    <div>

      {/* ── ZONE 1 — Chapter header ── */}
      <div style={{ padding: pad, borderBottom: '2px solid var(--ink)' }}>
        {section && (
          <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            letterSpacing: '0.06em', marginBottom: 16 }}>
            {section.part} · ch{chapter}
          </div>
        )}
        {section && (
          <h2 style={{
            fontSize: mob ? 36 : tab ? 44 : 56, fontWeight: 500,
            letterSpacing: '-0.03em', lineHeight: 1.0, margin: '0 0 14px',
            fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
          }}>{section.title}</h2>
        )}
        {section && (
          <div className="mono" style={{
            fontSize: 11, color: 'var(--sub)', letterSpacing: '0.06em',
            marginBottom: 32,
          }}>{section.sub.toUpperCase()}</div>
        )}

        {/* Summary blockquote */}
        <blockquote style={{
          margin: '0 0 36px', padding: '0 0 0 20px',
          borderLeft: '3px solid var(--accent)',
        }}>
          <p style={{
            fontStyle: 'italic',
            fontSize: mob ? 17 : 20, lineHeight: 1.5,
            letterSpacing: '-0.01em', margin: 0, color: 'var(--ink)',
            fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
          }}>{p1}</p>
        </blockquote>

        {/* Stats row — 3 chapter-specific facts + identifier */}
        <div style={{
          display: 'grid',
          gridTemplateColumns: mob ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)',
          gap: 24, paddingTop: 28, borderTop: '1px solid var(--rule)',
        }}>
          {(CHAPTER_STATS[chapter] || []).map(({ label, value }) => (
            <div key={label}>
              <div className="mono" style={{ fontSize: 9, color: 'var(--sub)',
                letterSpacing: '0.07em', marginBottom: 8 }}>{label}</div>
              <div style={{ fontSize: mob ? 20 : 26, fontWeight: 500,
                letterSpacing: '-0.02em', lineHeight: 1,
                fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
              }}>{value}</div>
            </div>
          ))}
          <div>
            <div className="mono" style={{ fontSize: 9, color: 'var(--sub)',
              letterSpacing: '0.07em', marginBottom: 8 }}>CHAPTER</div>
            <div style={{ fontSize: mob ? 20 : 26, fontWeight: 500,
              letterSpacing: '-0.02em', lineHeight: 1,
              fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
              color: 'var(--accent)',
            }}>ch{chapter}</div>
          </div>
        </div>
      </div>

      {/* ── ZONE 2 — Opening Scene ── */}
      <div style={{
        position: 'relative', overflow: 'hidden',
        padding: mob ? '40px 20px' : tab ? '48px 32px' : '52px 48px',
        background: 'var(--bg)', borderBottom: '1px solid var(--rule)',
      }}>
        {/* Ghost year */}
        <div aria-hidden="true" style={{
          position: 'absolute', right: mob ? -10 : -20, top: mob ? -10 : -20,
          fontSize: mob ? 160 : 240, fontWeight: 700, lineHeight: 1,
          letterSpacing: '-0.06em', color: 'var(--rule)',
          pointerEvents: 'none', userSelect: 'none',
          fontVariantNumeric: 'tabular-nums',
        }}>{scene.year || chapter}</div>

        <div style={{
          display: 'grid',
          gridTemplateColumns: mob ? '1fr' : '140px 1fr',
          gap: mob ? 24 : 48,
          position: 'relative', zIndex: 1,
        }}>
          {/* Left — scene label */}
          <div>
            <div className="mono" style={{ fontSize: 9, color: 'var(--accent)',
              letterSpacing: '0.1em', marginBottom: 14 }}>OPENING SCENE —</div>
            <div className="mono" style={{ fontSize: 13, fontWeight: 500,
              letterSpacing: '-0.005em', marginBottom: 12 }}>{scene.date}</div>
            {(scene.sources || []).map(src => (
              <div key={src} className="mono" style={{ fontSize: 10,
                color: 'var(--sub)', marginBottom: 4 }}>{src}</div>
            ))}
          </div>

          {/* Right — scene content */}
          <div>
            <h3 style={{
              fontSize: mob ? 26 : tab ? 32 : 40, fontWeight: 500,
              letterSpacing: '-0.025em', lineHeight: 1.05,
              margin: '0 0 22px', fontStyle: 'italic',
              fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
            }}>{t.opener}</h3>
            <p style={{ fontSize: 17, lineHeight: 1.68, margin: '0 0 20px',
              fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif' }}>
              {p2}{' '}
              <NoteRef n={1} active={openNote === 1}
                onClick={() => setOpenNote(openNote === 1 ? null : 1)} />
            </p>
          </div>
        </div>

        {/* Open note */}
        {openNote && (
          <div style={{ marginTop: 20, position: 'relative', zIndex: 1,
            background: 'var(--paper)', border: '1px solid var(--rule)',
            padding: '12px 16px', fontSize: 13, lineHeight: 1.5,
            color: 'var(--sub)', display: 'flex', gap: 12 }}>
            <span className="mono" style={{ fontSize: 10, color: 'var(--accent)' }}>[{openNote}]</span>
            <span>Opening anecdote for Chapter {chapter}. Source notes and full citations live in the manuscript's back matter.</span>
            <button onClick={() => setOpenNote(null)} className="mono" style={{
              marginLeft: 'auto', background: 'none', border: 'none',
              color: 'var(--sub)', cursor: 'pointer', fontSize: 11 }}>close ✕</button>
          </div>
        )}
      </div>

      {/* ── THREE TRIADS — voice columns ── */}
      {citeList.length > 0 && (
        <section style={{ padding: mob ? '28px 20px 40px' : tab ? '28px 32px 40px' : '28px 48px 40px',
          borderBottom: '1px solid var(--rule)' }}>
          <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            letterSpacing: '0.04em', marginBottom: 20 }}>
            THREE TRIADS · THREE PERSPECTIVES
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: mob ? '1fr' : 'repeat(3, 1fr)',
            gap: 0, borderTop: '1px solid var(--rule)' }}>
            {[
              { tier: 'A', label: 'ACADEMIC',     items: tierA },
              { tier: 'P', label: 'PRACTITIONER', items: tierP },
              { tier: 'V', label: 'VISIONARY',    items: tierV },
            ].map(({ tier, label, items }, ci) => (
              <div key={tier} style={{
                borderRight: mob ? 'none' : (ci < 2 ? '1px solid var(--rule)' : 'none'),
                borderTop: mob && ci > 0 ? '1px solid var(--rule)' : 'none',
                paddingRight: mob ? 0 : (ci < 2 ? 20 : 0),
                paddingLeft: mob ? 0 : (ci > 0 ? 20 : 0),
                paddingTop: mob && ci > 0 ? 20 : 0,
              }}>
                <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
                  borderBottom: '1px solid var(--rule)', paddingBottom: 8,
                  marginBottom: 12, letterSpacing: '0.04em' }}>
                  {label}
                </div>
                {items.map((v, i) => (
                  <div key={v.n} style={{
                    padding: '8px 0',
                    borderTop: i > 0 ? '1px solid var(--rule)' : 'none',
                  }}>
                    <div style={{ display: 'flex', gap: 8, alignItems: 'baseline' }}>
                      <span className="mono" style={{ fontSize: 9,
                        color: `var(${v.group.ink})`,
                        background: `var(${v.group.varCSS})`,
                        padding: '2px 5px', flexShrink: 0 }}>
                        {String(v.n).padStart(2, '0')}
                      </span>
                      <span style={{ fontSize: 13, fontWeight: 500,
                        letterSpacing: '-0.01em', lineHeight: 1.2 }}>
                        {v.name}
                      </span>
                    </div>
                    <div className="mono" style={{ fontSize: 10,
                      color: 'var(--sub)', marginTop: 3, lineHeight: 1.3 }}>
                      {v.affiliation}
                    </div>
                    <div style={{ fontSize: 11, color: 'var(--sub2)',
                      marginTop: 4, lineHeight: 1.4 }}>
                      {v.note}
                    </div>
                  </div>
                ))}
              </div>
            ))}
          </div>
        </section>
      )}

    </div>
  );
}

// ─── What this chapter coordinates ────────────────────
// Dark section: causal nodes as labeled boxes in a row with arrows.
function CoordinatesSection({ diagram, noBg, style: styleProp }) {
  const visible = diagram.nodes.filter(n => !n.latent);
  const latent  = diagram.nodes.filter(n => n.latent);

  return (
    <section style={{
      margin: '36px -48px 36px', padding: '28px 48px',
      background: 'var(--ink)', color: 'var(--paper)',
      ...(noBg ? { background: 'none', margin: 0 } : {}),
      ...styleProp,
    }}>
      <div className="mono" style={{ fontSize: 10, color: 'oklch(0.65 0.1 250)',
        letterSpacing: '0.06em', marginBottom: 20,
        display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
        <span>WHAT THIS CHAPTER COORDINATES</span>
        <span style={{ color: 'oklch(0.45 0.06 250)' }}>FIG · CAUSAL DIAGRAM</span>
      </div>

      {/* Nodes as horizontal row — scrollable if too wide */}
      <div style={{ display: 'flex', alignItems: 'stretch', gap: 0,
        overflowX: 'auto', paddingBottom: 4 }}>
        {visible.map((node, i) => (
          <React.Fragment key={node.id}>
            <div style={{
              border: '1px solid oklch(0.32 0.05 250)',
              padding: '14px 18px', flexShrink: 0,
              background: 'oklch(0.14 0.04 250)', minWidth: 90,
            }}>
              <div className="mono" style={{ fontSize: 9,
                color: 'oklch(0.55 0.1 250)', letterSpacing: '0.05em',
                marginBottom: 5 }}>{node.id}</div>
              <div style={{ fontSize: 13, fontWeight: 500,
                letterSpacing: '-0.01em', lineHeight: 1.3,
                color: 'var(--paper)', whiteSpace: 'nowrap' }}>
                {node.label.replace('\n', ' ')}
              </div>
            </div>
            {i < visible.length - 1 && (
              <div style={{ display: 'flex', alignItems: 'center',
                padding: '0 8px', color: 'var(--accent)',
                fontSize: 15, flexShrink: 0 }}>→</div>
            )}
          </React.Fragment>
        ))}
      </div>

      {/* Latent node */}
      {latent.length > 0 && (
        <div style={{ marginTop: 14, paddingTop: 14,
          borderTop: '1px solid oklch(0.25 0.04 250)',
          display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
          <div className="mono" style={{ fontSize: 9,
            color: 'oklch(0.55 0.1 250)', letterSpacing: '0.05em' }}>LATENT /</div>
          {latent.map(n => (
            <div key={n.id} style={{
              border: '1px dashed oklch(0.40 0.05 250)',
              padding: '8px 14px', fontSize: 13,
              color: 'oklch(0.78 0.04 250)', letterSpacing: '-0.01em' }}>
              {n.label.replace('\n', ' ')}
            </div>
          ))}
          <div style={{ fontSize: 11, color: 'oklch(0.50 0.06 250)' }}>
            — the missing third body
          </div>
        </div>
      )}

      <div style={{ marginTop: 14, fontSize: 12, color: 'oklch(0.52 0.06 250)',
        lineHeight: 1.5 }}>
        {diagram.caption}
      </div>

      {/* SVG causal graph — nodes as circles + curved arcs */}
      <CausalDiagramSVG diagram={diagram} />
    </section>
  );
}

function CausalDiagramSVG({ diagram }) {
  const W = 620, H = 300;
  const colX = [70, W / 2, W - 70];
  const rowY = [60, H / 2, H - 60];
  const pos = n => ({ x: colX[n.pos[0]], y: rowY[n.pos[1]] });

  const arcPath = (a, b, i) => {
    const pa = pos(a), pb = pos(b);
    const dx = pb.x - pa.x, dy = pb.y - pa.y;
    const len = Math.max(Math.sqrt(dx*dx+dy*dy), 1);
    const c = 0.14 + (i % 3) * 0.04;
    const nx = -dy/len, ny = dx/len;
    const mx = (pa.x+pb.x)/2 + nx*len*c;
    const my = (pa.y+pb.y)/2 + ny*len*c;
    const R = 36;
    const t1 = R/len, t2 = 1-R/len;
    return {
      d: `M ${pa.x+dx*t1} ${pa.y+dy*t1} Q ${mx} ${my} ${pa.x+dx*t2} ${pa.y+dy*t2}`,
      mid: { x: mx, y: my },
    };
  };

  return (
    <div style={{ marginTop: 18, borderTop: '1px solid oklch(0.25 0.04 250)',
      paddingTop: 18 }}>
      <div className="mono" style={{ fontSize: 9, color: 'oklch(0.50 0.08 250)',
        letterSpacing: '0.04em', marginBottom: 10 }}>
        CAUSAL GRAPH · {diagram.title.toUpperCase()}
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%"
        style={{ display: 'block', overflow: 'visible' }}>
        <defs>
          <marker id="cd-arr" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="6" markerHeight="6" orient="auto">
            <path d="M0,1 L10,5 L0,9 z"
              fill="oklch(0.65 0.15 250)" />
          </marker>
          <marker id="cd-lat" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="6" markerHeight="6" orient="auto">
            <path d="M0,1 L10,5 L0,9 z"
              fill="oklch(0.50 0.06 250)" />
          </marker>
          <marker id="cd-bal" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="6" markerHeight="6" orient="auto">
            <path d="M0,2 L10,5 L0,8" fill="none"
              stroke="oklch(0.65 0.15 250)" strokeWidth="1.5" />
          </marker>
        </defs>

        {diagram.arcs.map((a, i) => {
          const fn = diagram.nodes.find(n => n.id === a.from);
          const tn = diagram.nodes.find(n => n.id === a.to);
          if (!fn || !tn) return null;
          const p = arcPath(fn, tn, i);
          const isLatent = a.kind === 'latent';
          const isBal    = a.kind === 'balancing';
          return (
            <g key={i}>
              <path d={p.d} fill="none"
                stroke={isLatent ? 'oklch(0.42 0.05 250)' : 'oklch(0.65 0.15 250)'}
                strokeWidth={isLatent ? 1 : 1.4}
                strokeDasharray={isLatent ? '4 4' : '0'}
                markerEnd={`url(#${isLatent ? 'cd-lat' : isBal ? 'cd-bal' : 'cd-arr'})`} />
              {a.label && (
                <text x={p.mid.x} y={p.mid.y - 5} textAnchor="middle"
                  style={{ font: '9px "JetBrains Mono", monospace',
                    fill: isLatent ? 'oklch(0.45 0.04 250)' : 'oklch(0.60 0.12 250)' }}>
                  {a.label}
                </text>
              )}
            </g>
          );
        })}

        {diagram.nodes.map(n => {
          const p = pos(n);
          const lines = n.label.split('\n');
          return (
            <g key={n.id}>
              <circle cx={p.x} cy={p.y} r={36}
                fill="oklch(0.14 0.04 250)"
                stroke={n.latent ? 'oklch(0.40 0.05 250)' : 'oklch(0.60 0.12 250)'}
                strokeWidth={n.latent ? 1.1 : 1.6}
                strokeDasharray={n.latent ? '5 3' : '0'} />
              {lines.map((line, li) => (
                <text key={li} x={p.x}
                  y={p.y + (li - (lines.length - 1) / 2) * 12 + 4}
                  textAnchor="middle"
                  style={{ font: `500 10px inherit`,
                    fill: n.latent ? 'oklch(0.60 0.04 250)' : 'var(--paper)',
                    letterSpacing: '-0.01em' }}>
                  {line}
                </text>
              ))}
              <text x={p.x} y={p.y + 50} textAnchor="middle"
                style={{ font: '8px "JetBrains Mono", monospace',
                  fill: 'oklch(0.48 0.08 250)', letterSpacing: '0.04em' }}>
                {n.id}
              </text>
            </g>
          );
        })}
      </svg>
      <div className="mono" style={{ fontSize: 9, color: 'oklch(0.42 0.05 250)',
        marginTop: 6, display: 'flex', gap: 16 }}>
        <span>— solid = causal arc</span>
        <span>- - dashed = latent</span>
        <span>B = balancing</span>
      </div>
    </div>
  );
}

// Keep for legacy references
function CitedList({ chapter, cites }) {
  return null; // Now rendered inline in ChapterTeaser three-column layout
}



// ─── Reader drawer ─────────────────────────────────────
// Collapsible sidebar: closed by default, tab on edge to open.
// Contains: theme switcher (light/warm/dark) + locked contributor tools.
// The cited voices are shown INSIDE the chapter teaser (three-triads),
// so the drawer is kept lean: settings only.
function ReaderDrawer({ chapter, cited }) {
  const [open, setOpen] = React.useState(false);
  return (
    <>
      {/* Floating tab — fixed to right edge of viewport */}
      <div style={{
        position: 'fixed', right: 0, top: '40vh',
        zIndex: 30,
        display: 'flex', flexDirection: 'row', alignItems: 'flex-start',
      }}>
        {/* Tab button */}
        <button onClick={() => setOpen(o => !o)} className="mono" style={{
          width: 28, height: 64, flexShrink: 0,
          background: open ? 'var(--ink)' : 'var(--paper)',
          color: open ? 'var(--paper)' : 'var(--sub)',
          border: '1px solid var(--rule)', borderRight: 'none',
          cursor: 'pointer', fontSize: 14,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          transition: 'background .2s, color .2s',
        }} title={open ? 'Close' : 'Reader tools'}>
          {open ? '✕' : '⚙'}
        </button>
      </div>

      {/* Sliding panel — absolutely positioned from right, slides in */}
      <div style={{
        position: 'fixed', top: '40vh', right: 28, zIndex: 29,
        width: 260,
        maxHeight: '65vh',
        overflowY: open ? 'auto' : 'hidden',
        overflowX: 'hidden',
        background: 'var(--paper)',
        border: open ? '1px solid var(--rule)' : 'none',
        transition: 'transform 0.32s cubic-bezier(0.19,1,0.22,1), opacity 0.28s',
        transform: open ? 'translateX(0)' : 'translateX(120%)',
        opacity: open ? 1 : 0,
        pointerEvents: open ? 'auto' : 'none',
        boxShadow: open ? '-4px 4px 24px rgba(0,0,0,0.12)' : 'none',
      }}>
        <div style={{ width: 260, padding: '24px 18px' }}>
          <Label style={{ paddingTop: 0, marginBottom: 12 }}>Display</Label>
          <ThemeSwitcher />
          <div style={{ height: 1, background: 'var(--rule)', margin: '16px 0' }} />
          <Label style={{ paddingTop: 0, marginBottom: 12 }}>Typography</Label>
          <TypographyControls />
          <div style={{ height: 1, background: 'var(--rule)', margin: '16px 0' }} />
          <Label style={{ paddingTop: 0, marginBottom: 10 }}>Contributor tools</Label>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
            {[['✎', 'Add margin note'], ['⚑', 'Flag for review']].map(([ic, l]) => (
              <div key={l} className="mono" style={{
                display: 'grid', gridTemplateColumns: '18px 1fr auto',
                gap: 6, padding: '7px 10px', fontSize: 11,
                color: 'var(--sub2)', border: '1px solid var(--rule)',
                opacity: 0.45, background: 'var(--bg)',
              }}>
                <span>{ic}</span><span>{l}</span><span style={{ fontSize: 9 }}>🔒</span>
              </div>
            ))}
          </div>
          <div className="mono" style={{ fontSize: 9, color: 'var(--sub2)',
            marginTop: 10, lineHeight: 1.5 }}>
            Unlock after claiming a chapter spot.
            <a href="contribute.html" style={{ display: 'block', marginTop: 6,
              color: 'var(--accent)', textDecoration: 'none' }}>Claim spot →</a>
          </div>
        </div>
      </div>
    </>
  );
}

// ─── Theme switcher ────────────────────────────────────
// Three modes: light (default) · warm · dark.
// Applies data-theme on <html> with a 0.7s transition.
function ThemeSwitcher() {
  const [theme, setTheme] = React.useState('light');

  function apply(t) {
    setTheme(t);
    document.documentElement.setAttribute('data-theme', t === 'light' ? '' : t);
  }

  const themes = [
    { key: 'light', icon: '○', label: 'Light' },
    { key: 'warm',  icon: '◑', label: 'Warm' },
    { key: 'dark',  icon: '●', label: 'Dark' },
  ];

  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0,
      border: '1px solid var(--rule)' }}>
      {themes.map(({ key, icon, label }, i) => (
        <button key={key} onClick={() => apply(key)} className="mono" style={{
          background: theme === key ? 'var(--ink)' : 'var(--paper)',
          color: theme === key ? 'var(--bg)' : 'var(--sub)',
          border: 'none',
          borderRight: i < 2 ? '1px solid var(--rule)' : 'none',
          padding: '9px 6px', cursor: 'pointer',
          fontSize: 10, letterSpacing: '0.02em',
          display: 'flex', flexDirection: 'column',
          alignItems: 'center', gap: 4,
          transition: 'background .2s, color .2s',
        }}>
          <span style={{ fontSize: 16 }}>{icon}</span>
          <span>{label.toUpperCase()}</span>
        </button>
      ))}
    </div>
  );
}

// ─── Typography controls ───────────────────────────────
function TypographyControls() {
  const root = document.documentElement;
  const [size, setSize] = React.useState(1.15);
  const [lh,   setLh]   = React.useState(1.7);

  const sizes  = [1, 1.15, 1.3, 1.5];
  const lhs    = [1.5, 1.65, 1.8, 2.0];

  function nextSize() {
    const i = sizes.indexOf(size);
    const next = sizes[(i + 1) % sizes.length];
    setSize(next);
    root.style.setProperty('--read-scale', String(next));
  }
  function nextLh() {
    const i = lhs.indexOf(lh);
    const next = lhs[(i + 1) % lhs.length];
    setLh(next);
    root.style.setProperty('--read-lh', String(next));
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <button onClick={nextSize} className="mono" style={{
        background: size !== 1 ? 'var(--ink)' : 'var(--paper)',
        color: size !== 1 ? 'var(--bg)' : 'var(--ink)',
        border: '1px solid var(--rule)', padding: '8px 10px',
        cursor: 'pointer', textAlign: 'left', fontSize: 11,
        display: 'grid', gridTemplateColumns: '16px 1fr auto', gap: 8,
        transition: 'background .15s, color .15s',
      }}>
        <span>A</span>
        <span>Text size</span>
        <span style={{ fontWeight: 600 }}>{Math.round(size * 100)}%</span>
      </button>
      <button onClick={nextLh} className="mono" style={{
        background: lh !== 1.65 ? 'var(--ink)' : 'var(--paper)',
        color: lh !== 1.65 ? 'var(--bg)' : 'var(--ink)',
        border: '1px solid var(--rule)', padding: '8px 10px',
        cursor: 'pointer', textAlign: 'left', fontSize: 11,
        display: 'grid', gridTemplateColumns: '16px 1fr auto', gap: 8,
        transition: 'background .15s, color .15s',
      }}>
        <span>↕</span>
        <span>Line height</span>
        <span style={{ fontWeight: 600 }}>{lh.toFixed(1)}</span>
      </button>
    </div>
  );
}

// Keep for legacy
function ReaderTools() { return null; }

function NoteRef({ n, active, onClick }) {
  return (
    <sup>
      <button onClick={onClick} className="mono" style={{
        background: active ? 'var(--accent)' : 'var(--accent-soft)',
        color: active ? 'var(--paper)' : 'var(--accent)',
        border: '1px solid var(--accent)',
        padding: '1px 5px', cursor: 'pointer', fontSize: 9,
        letterSpacing: '-0.005em', verticalAlign: 'super',
        lineHeight: 1,
      }}>{n}</button>
    </sup>
  );
}

function FigurePlaceholder({ height, caption }) {
  return (
    <figure style={{ margin: 0 }}>
      <div style={{
        height, border: '1px solid var(--rule)',
        background: 'repeating-linear-gradient(135deg, var(--paper) 0 8px, #f0f0ef 8px 9px)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} className="mono">
        <span style={{ fontSize: 11, color: 'var(--sub)' }}>[ figure · drop in ]</span>
      </div>
      <figcaption className="mono" style={{ fontSize: 11, color: 'var(--sub)',
        marginTop: 8 }}>{caption}</figcaption>
    </figure>
  );
}

// ─── Join (v2) ─────────────────────────────────────────
// Numbered multi-step form, live "enquiry ticket" preview on right,
// itinerary of what happens next, colophon-style closer.
function Join() {
  const bp = useBP();
  const mob = bp === 'mobile';
  const [picked, setPicked] = React.useState('A');
  const [group, setGroup] = React.useState(null);
  const [name, setName] = React.useState('');
  const [reach, setReach] = React.useState('');
  const [work, setWork] = React.useState('');
  const [why, setWhy] = React.useState('');
  const [consent, setConsent] = React.useState({ anon: false, record: true, follow: true });

  const pathMeta = {
    A: ['Sit for an interview', '60 min', 'Recorded conversation, transcribed and edited, with final approval before print.'],
    B: ['Annotate the manuscript', '~30 min', 'Mark up a chapter. Your notes become part of the public margin in the next edition.'],
    C: ['Join the correspondence', 'Monthly', 'One letter a month — no feed, no algorithm. Unsubscribe any time.'],
    D: ['Edit a chapter — unlock full text', '4–6 hrs', 'Volunteer as a manuscript editor. Receive the full draft of one chapter, return line-level notes, and be named in the acknowledgments of Volume I.'],
  };
  const gName = group != null ? window.GROUPS[group - 1].name : '—';
  const gKey  = group != null ? window.GROUPS[group - 1].key : 'XX';

  const completion = [picked, group, name, reach, work].filter(Boolean).length;
  const pct = Math.round((completion / 5) * 100);

  return (
    <div>
      {/* Masthead strip */}
      <div className="mono" style={{
        borderBottom: '1px solid var(--rule)',
        padding: '10px 24px',
        display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
        gap: 16, fontSize: 10, color: 'var(--sub)',
      }}>
        <span>SECTION / 04 · CONTRIBUTE</span>
        <span>VOLUME / I · READING ROOM OPEN</span>
        <span>WINDOW / UNTIL SOLSTICE 26</span>
        <span style={{ textAlign: 'right' }}>REPLY WITHIN / 14d</span>
      </div>

      {/* Hero — split headline/meta */}
      <div style={{
        display: 'grid', gridTemplateColumns: mob ? '1fr' : '8fr 4fr',
        borderBottom: '1px solid var(--ink)',
      }}>
        <div style={{ padding: mob ? '32px 16px 28px' : '56px 24px 48px' }}>
          <Label style={{ paddingTop: 0 }}>04 · Contribute</Label>
          <h2 style={{
            fontSize: 'clamp(64px, 11vw, 180px)', fontWeight: 500,
            letterSpacing: '-0.05em', lineHeight: 0.88, margin: '16px 0 0',
          }}>
            Volume I<br/>
            is <span style={{ color: 'var(--accent)' }}>open</span>.
          </h2>
          <p style={{ fontSize: 20, lineHeight: 1.4, letterSpacing: '-0.015em',
            maxWidth: '58ch', margin: '32px 0 0', color: 'var(--sub)' }}>
            Eighty-one voices. Nine chapters. A small reading room of contributors
            is being opened before the manuscript goes to print. Edit a chapter,
            annotate the margin, or just receive the correspondence.
          </p>
          <a href="contribute.html" style={{
            marginTop: 32, display: 'inline-flex', alignItems: 'center',
            gap: 12, background: 'var(--ink)', color: 'var(--paper)',
            padding: '16px 24px', fontFamily: '"JetBrains Mono", ui-monospace, monospace',
            fontSize: 13, textDecoration: 'none', border: '1px solid var(--ink)',
          }}>
            <span>Claim your spot in Volume I</span>
            <span style={{ fontSize: 16 }}>→</span>
          </a>
          <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            marginTop: 12 }}>
            Opens the contribution landing page · ~4 min · no account required
          </div>
        </div>
        <div style={{ borderLeft: '1px solid var(--ink)',
          background: 'var(--paper)', padding: '28px 24px',
          display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
          <div>
            <Label style={{ paddingTop: 0 }}>Open calls</Label>
            <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column',
              gap: 12 }}>
              {[
                ['AI & computation', '07 / 12'],
                ['Climate adaptation', '04 / 08'],
                ['Healthcare & care work', '03 / 06'],
                ['Finance & markets', '02 / 05'],
              ].map(([n, c]) => (
                <div key={n} style={{ display: 'grid',
                  gridTemplateColumns: '1fr auto', alignItems: 'baseline',
                  borderBottom: '1px dashed var(--rule)', paddingBottom: 6 }}>
                  <span style={{ fontSize: 14, letterSpacing: '-0.01em' }}>{n}</span>
                  <span className="mono" style={{ fontSize: 11, color: 'var(--sub)' }}>{c}</span>
                </div>
              ))}
            </div>
          </div>
          <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
            marginTop: 20 }}>
            Fig. 04 — Volume II, slots remaining by domain.
          </div>
        </div>
      </div>

      {/* MAIN: form on left, live ticket on right */}
      <div style={{
        display: 'grid',
        gridTemplateColumns: mob ? '1fr' : '7fr 5fr',
        borderBottom: '1px solid var(--rule)',
      }}>
        {/* FORM */}
        <div style={{ padding: mob ? '24px 16px 32px' : '40px 32px 48px',
          borderRight: '1px solid var(--rule)' }}>
          {/* Progress */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 14,
            marginBottom: 32 }}>
            <div style={{ flex: 1, height: 2, background: 'var(--rule)',
              position: 'relative' }}>
              <div style={{ width: `${pct}%`, height: '100%',
                background: 'var(--accent)', transition: 'width .25s' }} />
            </div>
            <span className="mono" style={{ fontSize: 11, color: 'var(--sub)' }}>
              {completion}/5 · {pct}%
            </span>
          </div>

          {/* Step 01 — path */}
          <FormStep n="01" title="Pick a path">
            <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 0,
              border: '1px solid var(--rule)' }}>
              {Object.entries(pathMeta).map(([k, [t, d, b]], i, arr) => {
                const active = picked === k;
                return (
                  <button key={k} onClick={() => setPicked(k)} style={{
                    textAlign: 'left', padding: '16px 18px',
                    background: active ? 'var(--ink)' : 'var(--paper)',
                    color: active ? 'var(--paper)' : 'var(--ink)',
                    borderBottom: i < arr.length - 1 ? '1px solid var(--rule)' : 'none',
                    border: 'none', cursor: 'pointer',
                    display: 'grid',
                    gridTemplateColumns: '56px 1fr auto',
                    gap: 16, alignItems: 'center', fontFamily: 'inherit',
                  }}>
                    <span className="mono" style={{ fontSize: 11,
                      color: active ? 'var(--accent-soft)' : 'var(--sub)' }}>
                      Path {k}
                    </span>
                    <div>
                      <div style={{ fontSize: 18, letterSpacing: '-0.015em',
                        fontWeight: 500, lineHeight: 1.2 }}>{t}</div>
                      <div style={{ fontSize: 12, lineHeight: 1.4, marginTop: 4,
                        color: active ? '#d6d6d0' : 'var(--sub)' }}>{b}</div>
                    </div>
                    <span className="mono" style={{ fontSize: 11,
                      color: active ? 'var(--accent-soft)' : 'var(--sub)' }}>
                      {d}
                    </span>
                  </button>
                );
              })}
            </div>
          </FormStep>

          {/* Step 02 — group */}
          <FormStep n="02" title="Which group are you?"
            caption="Which cell on the table do you belong to? Optional — we'll figure it out together if unsure.">
            <div style={{ display: 'grid',
              gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
              {window.GROUPS.map(g => {
                const active = group === g.id;
                return (
                  <button key={g.id} onClick={() => setGroup(active ? null : g.id)}
                    style={{
                      background: active ? `var(${g.varCSS})` : 'var(--paper)',
                      border: `1px solid var(${active ? g.ink : '--rule'})`,
                      padding: '12px', cursor: 'pointer', textAlign: 'left',
                      fontFamily: 'inherit',
                      display: 'flex', flexDirection: 'column', gap: 6,
                      minHeight: 64,
                    }}>
                    <div style={{ display: 'flex',
                      justifyContent: 'space-between', alignItems: 'baseline' }}>
                      <span className="mono" style={{ fontSize: 10,
                        color: `var(${g.ink})`, fontWeight: 600 }}>
                        {g.key}
                      </span>
                      <span style={{
                        width: 10, height: 10,
                        background: `var(${g.varCSS})`,
                        border: `1px solid var(${g.ink})`,
                      }} />
                    </div>
                    <span style={{ fontSize: 13, letterSpacing: '-0.01em',
                      fontWeight: 500, color: 'var(--ink)' }}>{g.name}</span>
                  </button>
                );
              })}
            </div>
          </FormStep>

          {/* Step 03 — details */}
          <FormStep n="03" title="Where to write">
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)',
              gap: 20 }}>
              <Field label="Your name" placeholder="(anonymous is fine)"
                value={name} onChange={setName} />
              <Field label="How to reach you" placeholder="email or letter address"
                value={reach} onChange={setReach} />
              <Field full label="What you work on, in one sentence"
                placeholder="e.g. soil carbon measurement across smallholder farms"
                value={work} onChange={setWork} />
              <Field full rows={3} label="Why this book, why now"
                placeholder="optional — two or three lines is perfect"
                value={why} onChange={setWhy} />
            </div>
          </FormStep>

          {/* Step 04 — consent */}
          <FormStep n="04" title="Permissions">
            <div style={{ display: 'flex', flexDirection: 'column', gap: 2,
              border: '1px solid var(--rule)' }}>
              {[
                ['anon', 'Use my real name in print', 'Otherwise we credit you as "Interview № XX".'],
                ['record', 'Record the conversation', 'Audio stored locally. Transcript shared for approval.'],
                ['follow', 'Include me in follow-up conversations', 'Later questions after the first draft is complete.'],
              ].map(([k, t, d], i) => (
                <label key={k} style={{
                  padding: '12px 16px', cursor: 'pointer',
                  borderBottom: i < 2 ? '1px solid var(--rule)' : 'none',
                  display: 'grid', gridTemplateColumns: '20px 1fr',
                  gap: 14, alignItems: 'start',
                }}>
                  <input type="checkbox" checked={consent[k]}
                    onChange={e => setConsent({...consent, [k]: e.target.checked})}
                    style={{ marginTop: 3 }} />
                  <div>
                    <div style={{ fontSize: 14, letterSpacing: '-0.01em' }}>{t}</div>
                    <div className="mono" style={{ fontSize: 11,
                      color: 'var(--sub)', marginTop: 2 }}>{d}</div>
                  </div>
                </label>
              ))}
            </div>
          </FormStep>

          {/* Submit */}
          <div style={{ display: 'flex', gap: 12, alignItems: 'center',
            marginTop: 28, paddingTop: 16, borderTop: '1px solid var(--ink)' }}>
            <Btn filled>Submit enquiry →</Btn>
            <Btn>Save draft</Btn>
            <span className="mono" style={{ fontSize: 11, color: 'var(--sub)',
              marginLeft: 'auto' }}>
              We reply within a fortnight · never sold, never indexed.
            </span>
          </div>
        </div>

        {/* TICKET PREVIEW — hidden on mobile */}
        {!mob && <aside style={{ padding: '40px 32px',
          background: 'var(--paper)', position: 'sticky', top: 48,
          alignSelf: 'start' }}>
          <Label style={{ paddingTop: 0, marginBottom: 14 }}>Enquiry · live preview</Label>
          <div style={{
            border: '1px solid var(--ink)', background: '#fff',
            position: 'relative',
          }}>
            {/* Ticket header */}
            <div style={{
              background: 'var(--ink)', color: 'var(--paper)',
              padding: '10px 14px',
              display: 'grid', gridTemplateColumns: '1fr auto',
            }}>
              <span className="mono" style={{ fontSize: 10,
                color: 'var(--accent-soft)' }}>
                KNOWWARE · ENQUIRY TICKET
              </span>
              <span className="mono" style={{ fontSize: 10,
                color: 'var(--accent-soft)' }}>
                №&nbsp;{String(Math.floor(Math.random() * 900 + 100))}.{gKey}
              </span>
            </div>
            {/* Ticket body */}
            <div style={{ padding: 18, display: 'flex',
              flexDirection: 'column', gap: 14 }}>
              <TicketRow k="Path" v={`${picked} · ${pathMeta[picked][0]}`} />
              <TicketRow k="Group" v={group != null ? `${gKey} · ${gName}` : '— unassigned'} />
              <TicketRow k="Name"  v={name || <em style={{ color: 'var(--sub2)' }}>unset</em>} />
              <TicketRow k="Reach" v={reach || <em style={{ color: 'var(--sub2)' }}>unset</em>} />
              <TicketRow k="Work"  v={work || <em style={{ color: 'var(--sub2)' }}>unset</em>} />
              {why && <TicketRow k="Why" v={why} />}
            </div>
            {/* Consent strip */}
            <div style={{
              borderTop: '1px dashed var(--rule)', padding: '10px 18px',
              display: 'flex', gap: 12, flexWrap: 'wrap',
            }}>
              {Object.entries(consent).map(([k, v]) => (
                <span key={k} className="mono" style={{
                  fontSize: 10,
                  color: v ? 'var(--accent)' : 'var(--sub2)',
                  textDecoration: v ? 'none' : 'line-through',
                }}>
                  {v ? '●' : '○'} {k}
                </span>
              ))}
            </div>
            {/* Stamp */}
            <div style={{
              position: 'absolute', right: 14, bottom: 44,
              transform: 'rotate(-8deg)',
              border: '2px solid var(--accent)', color: 'var(--accent)',
              padding: '4px 10px', fontFamily: '"JetBrains Mono", monospace',
              fontSize: 10, letterSpacing: '0.1em', opacity: 0.6,
            }}>VOL · II · DRAFT</div>
          </div>

          {/* Itinerary */}
          <div style={{ marginTop: 28 }}>
            <Label style={{ paddingTop: 0, marginBottom: 10 }}>What happens next</Label>
            <ol style={{ listStyle: 'none', padding: 0, margin: 0,
              display: 'flex', flexDirection: 'column', gap: 0 }}>
              {[
                ['A', 'We read everything — usually within a fortnight.'],
                ['B', 'If it\'s a good fit, we propose a time and a form.'],
                ['C', 'You see every word before it goes to print.'],
                ['D', 'Your contribution is credited in the colophon.'],
              ].map(([k, t]) => (
                <li key={k} style={{
                  display: 'grid', gridTemplateColumns: '28px 1fr',
                  gap: 10, padding: '10px 0',
                  borderTop: '1px solid var(--rule)',
                }}>
                  <span className="mono" style={{ fontSize: 11,
                    color: 'var(--sub)' }}>{k}</span>
                  <span style={{ fontSize: 13, lineHeight: 1.4 }}>{t}</span>
                </li>
              ))}
            </ol>
          </div>
        </aside>}
      </div>

      {/* Colophon closer */}
      <div style={{ padding: '48px 24px',
        background: 'var(--ink)', color: 'var(--paper)' }}>
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 16,
        }}>
          <Label style={{ gridColumn: '1 / span 3', paddingTop: 0,
            color: 'var(--accent-soft)' }}>Or simply —</Label>
          <div style={{ gridColumn: '4 / span 9' }}>
            <div style={{ fontSize: 36, letterSpacing: '-0.025em',
              lineHeight: 1.2, maxWidth: '22ch' }}>
              write us a letter. An email is also a letter.
            </div>
            <div className="mono" style={{ fontSize: 13, marginTop: 16,
              color: 'var(--accent-soft)' }}>
              hello@knowware.press&nbsp;&nbsp;·&nbsp;&nbsp;P.O. Box 81, Brooklyn NY
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function FormStep({ n, title, caption, children }) {
  return (
    <section style={{ marginTop: 28,
      borderTop: '1px solid var(--ink)', paddingTop: 20 }}>
      <header style={{ display: 'grid',
        gridTemplateColumns: '32px 1fr', gap: 10,
        alignItems: 'baseline', marginBottom: 14 }}>
        <span className="mono" style={{ fontSize: 11, color: 'var(--sub)' }}>
          {n}
        </span>
        <div>
          <h3 style={{ margin: 0, fontSize: 24, letterSpacing: '-0.02em',
            fontWeight: 500, lineHeight: 1.1 }}>{title}</h3>
          {caption && (
            <p className="mono" style={{ fontSize: 11, color: 'var(--sub)',
              margin: '6px 0 0', maxWidth: '60ch', lineHeight: 1.5 }}>
              {caption}
            </p>
          )}
        </div>
      </header>
      {children}
    </section>
  );
}

function TicketRow({ k, v }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '70px 1fr',
      gap: 10, alignItems: 'start' }}>
      <span className="mono" style={{ fontSize: 10, color: 'var(--sub)',
        paddingTop: 2 }}>{k}</span>
      <span style={{ fontSize: 13, lineHeight: 1.4, color: 'var(--ink)' }}>{v}</span>
    </div>
  );
}

function Field({ label, placeholder, full, value, onChange, rows }) {
  const common = {
    value: value || '',
    onChange: e => onChange && onChange(e.target.value),
    placeholder,
    style: {
      border: 'none', borderBottom: '1px solid var(--ink)',
      background: 'transparent', padding: '8px 0', fontSize: 15,
      fontFamily: 'inherit', outline: 'none', resize: 'vertical',
      width: '100%',
    },
  };
  return (
    <label style={{
      gridColumn: full ? '1 / span 2' : undefined,
      display: 'flex', flexDirection: 'column', gap: 6,
    }}>
      <span className="mono" style={{ fontSize: 10, color: 'var(--sub)' }}>{label}</span>
      {rows ? <textarea rows={rows} {...common} /> : <input {...common} />}
    </label>
  );
}

function Btn({ children, filled, onClick }) {
  return (
    <button onClick={onClick} style={{
      background: filled ? 'var(--ink)' : 'var(--paper)',
      color: filled ? 'var(--paper)' : 'var(--ink)',
      border: '1px solid var(--ink)', padding: '10px 14px',
      fontFamily: '"JetBrains Mono", ui-monospace, monospace',
      fontSize: 12, cursor: 'pointer', letterSpacing: '-0.005em',
      textAlign: 'left',
    }}>{children}</button>
  );
}

// ─── Method page ───────────────────────────────────────
function MethodPage({ setPage }) {
  const mob = useBP() === 'mobile';

  const LINEAGE = [
    { year: '1948', name: 'Norbert Wiener', label: 'Cybernetics', note: 'Control and communication in the animal and the machine. Feedback loops between human operators and anti-aircraft radar — the first formal model of human-machine coordination.' },
    { year: '1956', name: 'W. Ross Ashby', label: 'Requisite Variety', note: "Law of Requisite Variety: only variety can absorb variety. A system's regulatory capacity must match the complexity of its environment — the formal condition for coordination to hold." },
    { year: '1972', name: 'Stafford Beer', label: 'Viable System Model', note: 'Five nested subsystems required for any living organisation to survive. System 3 coordinates operations. System 4 scans the environment. System 5 holds identity. Without all five, the organisation dies.' },
    { year: '1972', name: 'Gregory Bateson', label: 'Ecology of Mind', note: 'Levels of Learning: Learning I is stimulus-response; Learning II is recognising patterns across contexts; Learning III is a paradigm shift in how reality is framed. Knowware requires Learning II.' },
    { year: '1975', name: 'Gordon Pask', label: 'Conversation Theory', note: 'Knowledge does not reside in either participant of a conversation — it emerges from the interaction between them. Paskian conversations at scale are exactly what the 81 syntheses represent.' },
  ];

  const RECOGNIZERS = [
    { domain: 'Kitchen', figure: 'The Chef', text: 'Thirty years on the line. When the sauté cook adjusts to the sound of the sear two stations over — no words, just weight and timing — that silent choreography is the same pattern. The brigade system is coordination intelligence with fire.' },
    { domain: 'Construction', figure: 'The Foreman', text: 'AI-coordinated logistics on a major job site. The software didn\'t swing the hammers — it orchestrated delivery windows, pour schedules, and crew movement. The site finished three weeks early. Zero safety incidents. Neither the human nor the machine did that alone.' },
    { domain: 'Classroom', figure: 'The Teacher', text: 'Watching a student use an AI writing tool to produce an essay. The student guided. The AI suggested. The student edited. The resulting work was something neither could have written independently. The intelligence lived in the exchange.' },
    { domain: 'Autocomplete', figure: 'The Sentence', text: 'You type "looking forward to" and Gmail offers "seeing you tomorrow" in grey. You read it. You agree. Tab. Who wrote that sentence? You had the intent. The machine had the probability. You coordinated. That sentence is the Third Body in three seconds.' },
  ];

  return (
    <div>
      {/* Masthead */}
      <div className="mono" style={{
        borderBottom: '1px solid var(--rule)',
        padding: mob ? '8px 16px' : '10px 24px',
        display: 'grid',
        gridTemplateColumns: mob ? '1fr 1fr' : 'repeat(4, 1fr)',
        gap: 16, fontSize: 10, color: 'var(--sub)',
      }}>
        <span>METHOD / 04</span>
        {!mob && <span>COORDINATION SYNTHESIS</span>}
        {!mob && <span>EPISTEMIC ARCHITECTURE</span>}
        <span style={{ textAlign: 'right' }}>SYSTEMS OF INTELLIGENCE</span>
      </div>

      {/* Hero statement — full bleed dark */}
      <div style={{
        background: 'var(--ink)', color: 'var(--paper)',
        padding: mob ? '40px 16px 48px' : '64px 48px 72px',
        borderBottom: '1px solid var(--rule)',
      }}>
        <div style={{ maxWidth: 820 }}>
          <div className="mono" style={{ fontSize: 10, color: 'var(--accent-soft)',
            letterSpacing: '0.06em', marginBottom: 28 }}>
            THE SYNTHESIS IS THE PROOF
          </div>
          <p style={{
            margin: 0,
            fontSize: mob ? 20 : 32,
            lineHeight: 1.3,
            letterSpacing: '-0.02em',
            fontStyle: 'italic',
          }}>
            "No human interviewer sat across from Stuart Russell, Melanie Mitchell,
            or Judea Pearl. Instead, their published thinking — decades of papers,
            lectures, debates, and books — was coordinated through an AI system that
            found emergent patterns none of them articulated alone."
          </p>
          <p style={{
            margin: '28px 0 0',
            fontSize: mob ? 16 : 20,
            lineHeight: 1.45,
            letterSpacing: '-0.015em',
            color: 'oklch(0.75 0.04 250)',
          }}>
            What you are reading is not interviews. It is the Third Body in action —
            human knowledge coordinated with machine synthesis to produce insights
            that neither could generate independently.{' '}
            <span style={{ color: 'var(--accent-soft)', fontWeight: 500 }}>
              This book is not about Knowware. This book IS Knowware.
            </span>
          </p>
        </div>
      </div>

      {/* Two-body vs Three-body */}
      <div style={{ padding: mob ? '40px 16px' : '56px 48px', borderBottom: '1px solid var(--rule)' }}>
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          letterSpacing: '0.06em', marginBottom: 24 }}>01 · TWO BODIES VS THREE BODIES</div>
        <div style={{
          display: 'grid',
          gridTemplateColumns: mob ? '1fr' : '1fr 1fr',
          gap: mob ? 16 : 2,
          border: '1px solid var(--ink)',
        }}>
          {/* Left — AI Hallucination */}
          <div style={{
            padding: mob ? 20 : 32,
            borderRight: mob ? 'none' : '1px solid var(--ink)',
            borderBottom: mob ? '1px solid var(--ink)' : 'none',
          }}>
            <div className="mono" style={{ fontSize: 9, letterSpacing: '0.06em',
              color: 'var(--sub)', marginBottom: 16 }}>TWO-BODY SYSTEM</div>
            <div style={{ fontSize: mob ? 18 : 22, fontWeight: 500,
              letterSpacing: '-0.02em', marginBottom: 16 }}>
              AI Hallucination
            </div>
            {[
              ['Mechanism', 'Machine generates content directly from training data. No grounding in specific human expertise.'],
              ['Constraint', 'Lacks contextual coordination and intentional design. The output may appear authoritative but simulates authority rather than earning it.'],
              ['Result', 'A stochastic parrot. Confident confabulation. No Third Body — only the machine pretending to be one.'],
            ].map(([k, v]) => (
              <div key={k} style={{ display: 'grid', gridTemplateColumns: '90px 1fr',
                gap: 12, marginBottom: 14, alignItems: 'start' }}>
                <span className="mono" style={{ fontSize: 10, color: 'var(--sub)', paddingTop: 2 }}>{k}</span>
                <span style={{ fontSize: 13, lineHeight: 1.55, color: 'var(--ink)' }}>{v}</span>
              </div>
            ))}
          </div>
          {/* Right — Coordination Synthesis */}
          <div style={{
            padding: mob ? 20 : 32,
            background: 'var(--paper)',
          }}>
            <div className="mono" style={{ fontSize: 9, letterSpacing: '0.06em',
              color: 'var(--accent)', marginBottom: 16 }}>THREE-BODY SYSTEM</div>
            <div style={{ fontSize: mob ? 18 : 22, fontWeight: 500,
              letterSpacing: '-0.02em', marginBottom: 16 }}>
              Coordination Synthesis
            </div>
            {[
              ['Body A', 'Human expertise: decades of published work by named thinkers — verified, cited, real.'],
              ['Body B', 'Machine pattern recognition: finding cross-domain connections no single human mind could hold simultaneously.'],
              ['Context', 'Authorial intent: the author\'s coordination architecture — the questions asked, the curation applied, the critical editing that shaped every synthesis.'],
              ['Result', 'Paskian conversations at scale. The synthesis IS the Third Body. Knowledge that emerges from the interaction, not from either participant alone.'],
            ].map(([k, v]) => (
              <div key={k} style={{ display: 'grid', gridTemplateColumns: '90px 1fr',
                gap: 12, marginBottom: 14, alignItems: 'start' }}>
                <span className="mono" style={{ fontSize: 10, color: 'var(--accent)', paddingTop: 2 }}>{k}</span>
                <span style={{ fontSize: 13, lineHeight: 1.55, color: 'var(--ink)' }}>{v}</span>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Recognition examples */}
      <div style={{ padding: mob ? '40px 16px' : '56px 48px', borderBottom: '1px solid var(--rule)' }}>
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          letterSpacing: '0.06em', marginBottom: 8 }}>02 · RECOGNITION OVER REVELATION</div>
        <p style={{ fontSize: mob ? 16 : 20, lineHeight: 1.4,
          letterSpacing: '-0.015em', margin: '0 0 36px', maxWidth: 680,
          color: 'var(--sub)' }}>
          You do not need to accept a new theory. You need to recognise what you already know.
          The pattern existed before the name.
        </p>
        <div style={{
          display: 'grid',
          gridTemplateColumns: mob ? '1fr' : 'repeat(2, 1fr)',
          gap: 2, border: '1px solid var(--rule)',
        }}>
          {RECOGNIZERS.map((r, i) => (
            <div key={r.domain} style={{
              padding: mob ? '20px 16px' : '28px 24px',
              borderRight: !mob && i % 2 === 0 ? '1px solid var(--rule)' : 'none',
              borderBottom: i < RECOGNIZERS.length - 2 || mob ? '1px solid var(--rule)' : 'none',
            }}>
              <div style={{ display: 'flex', justifyContent: 'space-between',
                alignItems: 'baseline', marginBottom: 12 }}>
                <span style={{ fontSize: mob ? 15 : 17, fontWeight: 500,
                  letterSpacing: '-0.015em' }}>{r.figure}</span>
                <span className="mono" style={{ fontSize: 9, color: 'var(--sub)',
                  letterSpacing: '0.04em' }}>{r.domain.toUpperCase()}</span>
              </div>
              <p style={{ margin: 0, fontSize: 13, lineHeight: 1.65,
                color: 'var(--sub)', letterSpacing: '-0.005em' }}>{r.text}</p>
            </div>
          ))}
        </div>
      </div>

      {/* The formula */}
      <div style={{
        background: 'var(--paper)',
        padding: mob ? '40px 16px' : '56px 48px',
        borderBottom: '1px solid var(--rule)',
      }}>
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          letterSpacing: '0.06em', marginBottom: 28 }}>03 · THE COORDINATION FUNCTION</div>
        <div style={{
          display: 'grid',
          gridTemplateColumns: mob ? '1fr' : '1fr 1fr',
          gap: mob ? 32 : 64, alignItems: 'start',
        }}>
          <div>
            <div style={{
              fontSize: mob ? 'clamp(28px, 8vw, 48px)' : 'clamp(32px, 5vw, 56px)',
              fontWeight: 400, letterSpacing: '-0.03em', lineHeight: 1.1,
              fontFamily: 'Georgia, serif',
              borderBottom: '2px solid var(--ink)',
              paddingBottom: 20, marginBottom: 20,
            }}>
              C(A, B, Context) → Emergence
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
              {[
                ['C', 'Coordination — the active process, not a static state'],
                ['A', 'Human expertise: domain knowledge, lived experience, authorial intent'],
                ['B', 'Machine synthesis: pattern recognition at scale, cross-domain connection'],
                ['Context', 'The specific problem space, the question asked, the coordination architecture'],
                ['Emergence', 'The Third Body — insights that belong to neither A nor B alone'],
              ].map(([sym, def]) => (
                <div key={sym} style={{ display: 'grid',
                  gridTemplateColumns: 'minmax(110px, max-content) 1fr',
                  gap: 16, alignItems: 'baseline' }}>
                  <span style={{ fontSize: 18, fontWeight: 500,
                    letterSpacing: '-0.02em', fontFamily: 'Georgia, serif',
                    color: 'var(--accent)', lineHeight: 1.2,
                    whiteSpace: 'nowrap' }}>{sym}</span>
                  <span style={{ fontSize: 12, lineHeight: 1.6,
                    color: 'var(--sub)' }}>{def}</span>
                </div>
              ))}
            </div>
          </div>
          <div>
            <p style={{ margin: '0 0 16px', fontSize: mob ? 15 : 17,
              lineHeight: 1.5, letterSpacing: '-0.015em' }}>
              The formula formalises what Gordon Pask proved in 1975: knowledge
              is not extracted from one mind and deposited into another. It{' '}
              <em>emerges from the interaction</em> between participants.
            </p>
            <p style={{ margin: '0 0 16px', fontSize: 14, lineHeight: 1.6,
              color: 'var(--sub)', letterSpacing: '-0.01em' }}>
              In the 81 syntheses, Body A is the thinker's published corpus —
              verified, citable, real. Body B is the pattern-recognition layer
              that spans decades and domains simultaneously. The Context is the
              author's coordination architecture: the specific questions,
              the editorial curation, the insistence on divergence over
              mere summary.
            </p>
            <p style={{ margin: 0, fontSize: 14, lineHeight: 1.6,
              color: 'var(--sub)', letterSpacing: '-0.01em' }}>
              What emerges is not a simulation of an interview. It is something
              an interview could not produce — pattern synthesis across an
              entire intellectual life, held simultaneously.
            </p>
          </div>
        </div>
      </div>

      {/* Cybernetic lineage */}
      <div style={{ padding: mob ? '40px 16px 56px' : '56px 48px 72px' }}>
        <div className="mono" style={{ fontSize: 10, color: 'var(--sub)',
          letterSpacing: '0.06em', marginBottom: 8 }}>04 · THE LINEAGE</div>
        <p style={{ fontSize: mob ? 15 : 18, lineHeight: 1.45,
          letterSpacing: '-0.015em', margin: '0 0 36px', maxWidth: 640,
          color: 'var(--sub)' }}>
          This concept is not new. That is not a weakness — it is sixty years
          of validation. Knowware is where these frameworks converge.
        </p>
        <div style={{ position: 'relative' }}>
          {/* Vertical rule */}
          {!mob && (
            <div style={{
              position: 'absolute', left: 72, top: 0, bottom: 0,
              width: 1, background: 'var(--rule)',
            }} />
          )}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
            {LINEAGE.map((l, i) => (
              <div key={l.year} style={{
                display: 'grid',
                gridTemplateColumns: mob ? '1fr' : '72px 1fr',
                gap: mob ? 8 : 0,
                borderBottom: i < LINEAGE.length - 1 ? '1px solid var(--rule)' : 'none',
                padding: mob ? '20px 0' : '24px 0 24px 0',
              }}>
                <div style={{
                  padding: mob ? 0 : '0 24px 0 0',
                  display: 'flex', flexDirection: mob ? 'row' : 'column',
                  alignItems: mob ? 'baseline' : 'flex-start',
                  gap: mob ? 12 : 4,
                }}>
                  <span className="mono" style={{ fontSize: mob ? 11 : 12,
                    fontWeight: 600, color: 'var(--ink)' }}>{l.year}</span>
                  {mob && <span className="mono" style={{ fontSize: 9,
                    color: 'var(--accent)', letterSpacing: '0.04em' }}>{l.label.toUpperCase()}</span>}
                </div>
                <div style={{ paddingLeft: mob ? 0 : 32 }}>
                  <div style={{ display: 'flex', gap: 12,
                    alignItems: 'baseline', marginBottom: 8 }}>
                    <span style={{ fontSize: mob ? 15 : 17, fontWeight: 500,
                      letterSpacing: '-0.015em' }}>{l.name}</span>
                    {!mob && <span className="mono" style={{ fontSize: 9,
                      color: 'var(--accent)', letterSpacing: '0.04em' }}>
                      {l.label.toUpperCase()}
                    </span>}
                  </div>
                  <p style={{ margin: 0, fontSize: 13, lineHeight: 1.65,
                    color: 'var(--sub)', maxWidth: 640,
                    letterSpacing: '-0.005em' }}>{l.note}</p>
                </div>
              </div>
            ))}
          </div>
          <div className="mono" style={{ marginTop: 28, fontSize: 11,
            color: 'var(--sub)', borderTop: '1px solid var(--rule)', paddingTop: 16,
            display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
            <span>Wiener → Ashby → Beer → Bateson → Pask → Knowware</span>
            <span style={{ color: 'var(--sub2)' }}>1948 — 2026</span>
          </div>
        </div>
      </div>

    </div>
  );
}

Object.assign(window, { Shell, Cover, TablePage, Read, Join, MethodPage });
