// app.jsx — Tessera Mono specimen / design doc

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

function useTweaks(defaults) {
  const [values, setValues] = useState(defaults);
  const setTweak = useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues(prev => ({ ...prev, ...edits }));
  }, []);
  return [values, setTweak];
}

// ─── Tweak defaults ──────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "weight": 0,
  "rounding": 0,
  "mode": "default",
  "specimenPx": 2,
  "specimenText": "the quick brown fox\njumps over the lazy dog\nTHE QUICK BROWN FOX\nJUMPS OVER THE LAZY DOG\n0123456789 !@#$%^&*()\nif x != 0 -> result <= max\nasync fn(a, b) => a + b ~~ b"
}/*EDITMODE-END*/;

// ─── Color tokens — Base16 colorschemes ──────────────────────────────────────
// Each palette maps base16 roles: bg=base00 panel=base01 rule=base02 muted=base03
// ink=base05 accent=base0D; powerline derives from base08/0A/0B/0E
const PALETTES = {
  default: { label: 'Default Dark',
    bg:'#181818', panel:'#282828', rule:'#383838', muted:'#888888', ink:'#d8d8d8',
    grid:'rgba(216,216,216,0.06)', baseline:'rgba(124,175,194,0.45)',
    accent:'#7cafc2', accentSoft:'#383838' },
  monokai: { label: 'Monokai',
    bg:'#272822', panel:'#383830', rule:'#49483e', muted:'#a09c88', ink:'#f8f8f2',
    grid:'rgba(248,248,242,0.06)', baseline:'rgba(102,217,232,0.45)',
    accent:'#66d9e8', accentSoft:'#49483e' },
  solarized: { label: 'Solarized Dark',
    bg:'#002b36', panel:'#073642', rule:'#586e75', muted:'#657b83', ink:'#93a1a1',
    grid:'rgba(147,161,161,0.08)', baseline:'rgba(38,139,210,0.45)',
    accent:'#268bd2', accentSoft:'#073642' },
  'solarized-l': { label: 'Solarized Light',
    bg:'#fdf6e3', panel:'#eee8d5', rule:'#93a1a1', muted:'#839496', ink:'#586e75',
    grid:'rgba(88,110,117,0.12)', baseline:'rgba(38,139,210,0.45)',
    accent:'#268bd2', accentSoft:'#eee8d5' },
  nord: { label: 'Nord',
    bg:'#2e3440', panel:'#3b4252', rule:'#434c5e', muted:'#8992a3', ink:'#e5e9f0',
    grid:'rgba(229,233,240,0.06)', baseline:'rgba(129,161,193,0.45)',
    accent:'#81a1c1', accentSoft:'#434c5e' },
  gruvbox: { label: 'Gruvbox Dark',
    bg:'#282828', panel:'#3c3836', rule:'#504945', muted:'#a89984', ink:'#d5c4a1',
    grid:'rgba(213,196,161,0.06)', baseline:'rgba(69,133,136,0.45)',
    accent:'#458588', accentSoft:'#504945' },
  dracula: { label: 'Dracula',
    bg:'#282a36', panel:'#343746', rule:'#44475a', muted:'#8f9bc8', ink:'#f8f8f2',
    grid:'rgba(248,248,242,0.06)', baseline:'rgba(189,147,249,0.45)',
    accent:'#bd93f9', accentSoft:'#44475a' },
  'one-dark': { label: 'One Dark',
    bg:'#282c34', panel:'#353b45', rule:'#3e4451', muted:'#818896', ink:'#abb2bf',
    grid:'rgba(171,178,191,0.06)', baseline:'rgba(97,175,239,0.45)',
    accent:'#61afef', accentSoft:'#3e4451' },
  tomorrow: { label: 'Tomorrow Night',
    bg:'#1d1f21', panel:'#282a2e', rule:'#373b41', muted:'#969896', ink:'#c5c8c6',
    grid:'rgba(197,200,198,0.06)', baseline:'rgba(129,162,190,0.45)',
    accent:'#81a2be', accentSoft:'#373b41' },
  ocean: { label: 'Ocean',
    bg:'#2b303b', panel:'#343d46', rule:'#4f5b66', muted:'#8d99a5', ink:'#c0c5ce',
    grid:'rgba(192,197,206,0.06)', baseline:'rgba(143,161,179,0.45)',
    accent:'#8fa1b3', accentSoft:'#4f5b66' },
};

// ─── Tab bar ─────────────────────────────────────────────────────────────────
function TabBar({ tabs, active, onSelect }) {
  return (
    <div className="tab-bar">
      {tabs.map((t, i) => (
        <button key={i} className={'tab' + (active === i ? ' tab-on' : '')} onClick={() => onSelect(i)}>
          {t}
        </button>
      ))}
    </div>
  );
}

// ─── Section nav + inline controls ───────────────────────────────────────────
function SectionNav({ pal, t, setTweak }) {
  const links = [
    ['specimen','Specimen'],['proofs','Proofs'],['specs','System'],['atlas','Atlas'],['box','Box drawing'],
    ['math','Math'],['icons','Icons'],['nerd','Nerd Fonts'],
    ['lig','Ligatures'],
  ];
  return (
    <nav className="section-nav" style={{ background: pal.bg, borderBottomColor: pal.rule }}>
      <div className="nav-links">
        {links.map(([id, label]) => (
          <a key={id} href={'#' + id} style={{ color: pal.muted }}>{label}</a>
        ))}
      </div>
      <div className="nav-controls">
        <select className="nav-scheme-select"
          value={t.mode} onChange={e => setTweak('mode', e.target.value)}
          style={{ color: pal.ink, borderColor: pal.rule, background: pal.panel }}>
          {Object.entries(PALETTES).map(([key, p]) => (
            <option key={key} value={key}>{p.label}</option>
          ))}
        </select>
      </div>
    </nav>
  );
}

// ─── Section header ──────────────────────────────────────────────────────────
function Section({ id, num, title, sub, children, accent }) {
  return (
    <section id={id} className="section" data-screen-label={`${num} ${title}`}>
      <div className="section-hd">
        <div className="section-hd-l">
          <span className="section-num" style={{ color: accent }}>§{num}</span>
          <h2 className="section-title">{title}</h2>
        </div>
        {sub && <p className="section-sub">{sub}</p>}
      </div>
      <div className="section-body">{children}</div>
    </section>
  );
}

// ─── Hero ────────────────────────────────────────────────────────────────────
function Hero({ pal, weight, rounding }) {
  const nerdDrawn = NERD_CATEGORIES.reduce((a, c) => a + rangeDrawn(c), 0);
  return (
    <header className="hero">
      <div className="hero-top">
        <div className="hero-mark">
          <div className="mark-square" style={{ borderColor: pal.ink }}>
            <TextRun text="Tm" px={6} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.bg} />
          </div>
          <div className="hero-meta">
            <div className="hero-meta-row"><span>family</span><b>Tessera Mono</b></div>
            <div className="hero-meta-row"><span>version</span><b>0.1.0-draft</b></div>
            <div className="hero-meta-row"><span>cell</span><b>8 × 16 px</b></div>
            <div className="hero-meta-row"><span>cuts</span><b>3</b></div>
            <div className="hero-meta-row"><span>license</span><b>SIL OFL 1.1 (planned)</b></div>
          </div>
        </div>
      </div>

      <div className="hero-display">
        <TextRun
          text={"Tessera\nMono"}
          px={18} bold={weight} rounding={rounding}
          ink={pal.ink} paper={pal.bg}
          lineHeight={17}
        />
      </div>

      <div className="hero-bottom">
        <p className="hero-tagline">
          A neoretro bitmap monospace. Pixel-perfect at native cells,
          legible from 1080p up to 4K, designed to carry the full Nerd Fonts glyph set.
        </p>
        <div className="hero-stats">
          <div><b>95</b><span>ASCII printable, hand-drawn</span></div>
          <div><b>52</b><span>Latin diacritic composites</span></div>
          <div><b>128</b><span>Box drawing, algorithmic</span></div>
          <div><b>32</b><span>Block elements</span></div>
          <div><b>{nerdDrawn}</b><span>Nerd Fonts drawn so far</span></div>
        </div>
      </div>
    </header>
  );
}

// ─── Live specimen ───────────────────────────────────────────────────────────
function Specimen({ pal, weight, rounding, specimenPx, setTweak, text, ligatures, alts }) {
  return (
    <Section id="specimen" num="01" title="Specimen" accent={pal.accent}
      sub="Type anything. Scrub the pixel scale. The font renders the same bitmap at every integer multiple — no antialiasing, no hinting, just the grid.">
      <div className="specimen-grid">
        <div className="specimen-pane" style={{ background: pal.panel, borderColor: pal.rule }}>
          <TextRun
            text={text}
            px={specimenPx} bold={weight} rounding={rounding}
            ink={pal.ink} paper={pal.panel}
            lineHeight={20}
            maxWidth={1100}
            ligatures={ligatures} alts={alts} />
        </div>
        <div className="specimen-controls">
          <label className="ctrl">
            <span>Sample text</span>
            <textarea
              value={text}
              onChange={(e) => setTweak('specimenText', e.target.value)}
              rows={6}
              style={{ background: pal.panel, color: pal.ink, borderColor: pal.rule }}
            />
          </label>
          <label className="ctrl">
            <span>Pixel scale <b>{specimenPx}×</b> → {8 * specimenPx}×{16 * specimenPx}px cell</span>
            <input type="range" min="1" max="10" step="1"
              value={specimenPx}
              onChange={(e) => setTweak('specimenPx', +e.target.value)} />
          </label>
          <div className="ctrl-foot">
            <span>Recommended scales</span>
            <div className="chip-row">
              {[
                { px: 2, label: '2× — code editor' },
                { px: 3, label: '3× — comfy 1080p' },
                { px: 4, label: '4× — 1440p body' },
                { px: 6, label: '6× — 4K body' }
              ].map(s => (
                <button key={s.px}
                  className={'chip' + (specimenPx === s.px ? ' chip-on' : '')}
                  style={{ borderColor: pal.rule, color: specimenPx === s.px ? pal.bg : pal.ink, background: specimenPx === s.px ? pal.ink : 'transparent' }}
                  onClick={() => setTweak('specimenPx', s.px)}>
                  {s.label}
                </button>
              ))}
            </div>
          </div>
        </div>
      </div>
    </Section>
  );
}

// ─── Recognition and real-code proofs ───────────────────────────────────────
const CONFUSABLE_PROOFS = [
  ['round forms', '0 O o Q  00 OO  O0 0O'],
  ['verticals', '1 l I i |  Il1|  iIl1'],
  ['similar shapes', '2 Z z  5 S s  6 G  8 B'],
  ['joins', 'rn m  cl d  vv w'],
  ['punctuation', ". , : ;  ' ` \"  - _ = ~"],
  ['brackets', '() [] {} <>  [({<>})]'],
];

const CODE_PROOFS = [
  ['JavaScript', "const foo_bar = value?.items[0] ?? null;\nif (foo_bar !== null) return x => x * 0x10ff;"],
  ['Zig', 'pub fn read(io: *Io, buf: []u8) !usize {\n    return try io.read(buf[0..]);\n}'],
  ['shell + paths', "git diff -- src/io_01.zig ../../README.md\nprintf '%s\\n' \"$PATH\" | sed -n '1,5p'"],
  ['JSON + diff', '{\"cell\": [8, 16], \"pixelPerfect\": true}\n- old_value != 0\n+ new_value >= 0'],
];

function Proofs({ pal, weight, rounding }) {
  const run = (label, text, px = 2) => (
    <div className="proof-run" key={label}>
      <span className="proof-label">{label}</span>
      <TextRun text={text} px={px} bold={weight} rounding={rounding}
        ink={pal.ink} paper={pal.panel} />
    </div>
  );
  return (
    <Section id="proofs" num="02" title="Recognition proofs" accent={pal.accent}
      sub="Permanent stress tests for character ambiguity, punctuation rhythm, and dense code. Review at native 1× and at comfortable 2× before accepting glyph changes.">
      <div className="proof-grid">
        <div className="proof-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Confusable characters</h3>
          <p>Pairs that must remain distinguishable during fast reading.</p>
          {CONFUSABLE_PROOFS.map(([label, text]) => run(label, text, 2))}
        </div>
        <div className="proof-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Real code</h3>
          <p>Language, path, number, bracket, and diff punctuation in context.</p>
          {CODE_PROOFS.map(([label, text]) => run(label, text, 2))}
        </div>
      </div>
    </Section>
  );
}

// ─── Atlas ───────────────────────────────────────────────────────────────────
function AtlasGroup({ title, codepoints, pal, weight, rounding, px = 6, alts = false }) {
  return (
    <div className="atlas-group">
      <div className="atlas-group-hd">
        <span className="atlas-group-title">{title}</span>
        <span className="atlas-group-count">{codepoints.length} glyphs</span>
      </div>
      <div className="atlas-grid">
        {codepoints.map(cp => (
          <GlyphCell key={cp} cp={cp} px={px} bold={weight} rounding={rounding}
            ink={pal.ink} paper={pal.bg} grid={pal.grid} baseline={pal.baseline} alts={alts} />
        ))}
      </div>
    </div>
  );
}

function Atlas({ pal, weight, rounding, alts }) {
  const upper = []; for (let i = 0x41; i <= 0x5a; i++) upper.push(i);
  const lower = []; for (let i = 0x61; i <= 0x7a; i++) lower.push(i);
  const digits = []; for (let i = 0x30; i <= 0x39; i++) digits.push(i);
  const punct1 = [0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f];
  const punct2 = [0x3a,0x3b,0x3c,0x3d,0x3e,0x3f,0x40,0x5b,0x5c,0x5d,0x5e,0x5f,0x60,0x7b,0x7c,0x7d,0x7e];
  const ext = [
    0xa1,0xa2,0xa3,0xa5,0xa6,0xa7,0xa8,0xa9,0xab,0xac,0xae,0xaf,
    0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0xbb,0xbc,0xbd,0xbe,
    0xbf,0xc6,0xd7,0xd8,0xdf,0xe6,0xf7,0xf8
  ];
  const dia = Object.keys(TESSERA.DIACRITICS).map(k => +k);

  const groups = [
    { title: 'Uppercase',   codepoints: upper },
    { title: 'Lowercase',   codepoints: lower },
    { title: 'Digits',      codepoints: digits },
    { title: 'Punctuation', codepoints: [...punct1, ...punct2] },
    { title: 'Extended',    codepoints: ext },
    { title: 'Diacritics',  codepoints: dia },
  ];
  const [tab, setTab] = useState(0);

  return (
    <Section id="atlas" num="04" title="Atlas" accent={pal.accent}
      sub="Every glyph on its 8×16 grid. Rounding clips outside corners; bold dilates one pixel.">
      <TabBar tabs={groups.map(g => `${g.title} · ${g.codepoints.length}`)} active={tab} onSelect={setTab} />
      <AtlasGroup {...groups[tab]} pal={pal} weight={weight} rounding={rounding} alts={alts} />
    </Section>
  );
}

// ─── Box drawing demo ────────────────────────────────────────────────────────
function BoxDemo({ pal, weight, rounding }) {
  const tui = [
    '┌──────────────── tessera@dev ─────────────────┐',
    '│ ╭─ files ─────────────╮ ╭─ preview ────────╮ │',
    '│ │ ▸ glyphs.js   28K   │ │ ████░░░░░░  42%  │ │',
    '│ │ ▸ renderer.jsx  6K  │ │ rendering atlas. │ │',
    '│ │ ▸ app.jsx     11K   │ │ cell 8×16  cap 9 │ │',
    '│ │ ▸ index.html  4K    │ │ baseline @ row10 │ │',
    '│ │ ▾ assets/           │ │ ascend  desc  2  │ │',
    '│ │   logo.svg          │ │ ─────────────────│ │',
    '│ ╰─────────────────────╯ ╰──────────────────╯ │',
    '│ ░▒▓██▓▒░  ◆ ready  ◆ 3 axes  ◆ 95 glyphs     │',
    '└──────────────────────────────────────────────┘'
  ].join('\n');

  const box = [
    '┌─┬─┐  ╔═╦═╗  ╭─┬─╮  ┏━┳━┓',
    '├─┼─┤  ╠═╬═╣  ├─┼─┤  ┣━╋━┫',
    '└─┴─┘  ╚═╩═╝  ╰─┴─╯  ┗━┻━┛'
  ].join('\n');

  return (
    <Section id="box" num="05" title="Box drawing" accent={pal.accent}
      sub="The TUI test. U+2500–U+257F generated from a 4-arm encoding (left, right, up, down × light / heavy / double). Block elements (U+2580–U+259F) drive shading.">
      <div className="box-grid">
        <div className="box-panel" style={{ background: pal.panel, borderColor: pal.rule }}>
          <div className="box-panel-label" style={{ color: pal.muted }}>terminal mock</div>
          <TextRun text={tui} px={2} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} lineHeight={17} />
        </div>
        <div className="box-panel" style={{ background: pal.panel, borderColor: pal.rule }}>
          <div className="box-panel-label" style={{ color: pal.muted }}>joinery — light / double / rounded / heavy</div>
          <TextRun text={box} px={2} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} lineHeight={20} />
        </div>
      </div>
    </Section>
  );
}

// ─── Math & arrows ───────────────────────────────────────────────────────────
function MathDemo({ pal, weight, rounding, ligatures, alts }) {
  const a = '∀x ∈ N: x ≥ 0  ∧  x ≠ ∞';
  const b = '∑(i=1..n) i = n(n+1)/2';
  const c = '⇐  ←  ↑  ↓  →  ⇒  ↔';
  const d = '√(a² + b²) ≈ c  ±  ε';
  const e = '∩ ∪ ∈ ∃ ∀ ∏ × ÷ ≤ ≥';

  return (
    <Section id="math" num="06" title="Math & arrows" accent={pal.accent}
      sub="A curated selection covering common operators, set notation, and arrow forms. Drawn to the same 8×16 cell — math reads at terminal pitch.">
      <div className="math-stack" style={{ background: pal.panel, borderColor: pal.rule }}>
        {[a, b, c, d, e].map((s, i) => (
          <TextRun key={i} text={s} px={2} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} lineHeight={20} ligatures={ligatures} alts={alts} />
        ))}
      </div>
    </Section>
  );
}

// ─── Nerd Fonts roadmap ──────────────────────────────────────────────────────
const NERD_CATEGORIES = [
  { name: 'Powerline core',        phase: 'P0', ranges: [[0xE0A0, 0xE0A3], [0xE0B0, 0xE0B3]] },
  { name: 'Powerline extra',       phase: 'P0', ranges: [[0xE0A4, 0xE0AB], [0xE0B4, 0xE0C8]] },
  { name: 'IEC Power Symbols',     phase: 'P0', ranges: [[0x23FB, 0x23FE]] },
  { name: 'Pomicons',              phase: 'P0', ranges: [[0xE000, 0xE00A]] },
  { name: 'Devicons',              phase: 'P0', ranges: [[0xE700, 0xE7C5]] },
  { name: 'Codicons (VSCode)',     phase: 'P0', ranges: [[0xEA60, 0xEC1E]] },
  { name: 'Seti-UI + Custom',      phase: 'P0', ranges: [[0xE5FA, 0xE62B]] },
  { name: 'Font Logos',            phase: 'P1', ranges: [[0xF300, 0xF375]] },
  { name: 'Octicons',              phase: 'P1', ranges: [[0xF400, 0xF532]] },
  { name: 'Font Awesome',          phase: 'P1', ranges: [[0xED00, 0xF2FF]] },
  { name: 'Font Awesome Ext',      phase: 'P2', ranges: [[0xE200, 0xE2A9]] },
  { name: 'Weather Icons',         phase: 'P2', ranges: [[0xE300, 0xE3E3]] },
  { name: 'Material Design Icons', phase: 'P2', ranges: [[0xF0001, 0xF1AF0]] }
];

function rangeCount(cat) {
  return cat.ranges.reduce((s, [lo, hi]) => s + (hi - lo + 1), 0);
}
function rangeDrawn(cat) {
  let n = 0;
  for (const [lo, hi] of cat.ranges) {
    for (let cp = lo; cp <= hi; cp++) if (TESSERA.GLYPHS[cp]) n++;
  }
  return n;
}
function rangeString(cat) {
  return cat.ranges.map(([lo, hi]) =>
    lo === hi
      ? 'U+' + lo.toString(16).toUpperCase().padStart(4, '0')
      : 'U+' + lo.toString(16).toUpperCase().padStart(4, '0') + '–U+' + hi.toString(16).toUpperCase().padStart(4, '0')
  ).join(', ');
}

// ─── Coverage grid — one cell per codepoint, lit if drawn ────────────────────
function CoverageGrid({ ranges, pal, accent }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;

    const cps = [];
    for (const [lo, hi] of ranges) {
      for (let cp = lo; cp <= hi; cp++) cps.push(cp);
    }
    const total = cps.length;

    // Adaptive cell size — keep total width sane.
    const cell = total > 2000 ? 2 : total > 600 ? 3 : 4;
    const cols = total > 2000 ? 96 : total > 600 ? 64 : 40;
    const usedCols = Math.min(cols, total);
    const rows = Math.ceil(total / usedCols);

    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const w = usedCols * cell;
    const h = rows * cell;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';

    const ctx = canvas.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.imageSmoothingEnabled = false;

    for (let i = 0; i < total; i++) {
      const cp = cps[i];
      const x = (i % usedCols) * cell;
      const y = Math.floor(i / usedCols) * cell;
      const has = !!TESSERA.GLYPHS[cp];
      ctx.fillStyle = has ? accent : pal.rule;
      ctx.fillRect(x, y, cell - 1, cell - 1);
    }
  }, [ranges, pal, accent]);

  return <canvas ref={ref} style={{ display: 'block', imageRendering: 'pixelated' }} />;
}

function NerdRoadmap({ pal }) {
  const total = NERD_CATEGORIES.reduce((a, c) => a + rangeCount(c), 0);
  const drawn = NERD_CATEGORIES.reduce((a, c) => a + rangeDrawn(c), 0);
  const phases = ['P0', 'P1', 'P2'];

  const phaseLabel = {
    P0: 'Phase 0 — essential terminal',
    P1: 'Phase 1 — developer breadth',
    P2: 'Phase 2 — full coverage'
  };

  return (
    <Section id="nerd" num="08" title="Nerd Fonts coverage" accent={pal.accent}
      sub={`${total.toLocaleString()} codepoints across ${NERD_CATEGORIES.length} sources — one cell per codepoint, lit when drawn. Terminal essentials first.`}>
      <div className="cov-summary" style={{ background: pal.panel, borderColor: pal.rule }}>
        <div className="cov-summary-l">
          <span className="cov-num">{drawn.toLocaleString()}</span>
          <span className="cov-of">/ {total.toLocaleString()} drawn</span>
        </div>
        <div className="cov-bar">
          <div className="cov-bar-fill" style={{ width: `${(drawn / total * 100).toFixed(2)}%`, background: pal.accent }} />
        </div>
        <div className="cov-summary-r" style={{ color: pal.muted }}>
          <span>v0.1.0-draft · {((drawn / total) * 100).toFixed(2)}%</span>
        </div>
      </div>

      {phases.map(p => {
        const cats = NERD_CATEGORIES.filter(c => c.phase === p);
        const phaseTotal = cats.reduce((a, c) => a + rangeCount(c), 0);
        const phaseDrawn = cats.reduce((a, c) => a + rangeDrawn(c), 0);
        return (
          <div key={p} className="phase">
            <div className="phase-hd">
              <span className="phase-pill" style={{ background: pal.ink, color: pal.bg }}>{p}</span>
              <span className="phase-title">{phaseLabel[p]}</span>
              <span className="phase-count" style={{ color: pal.muted }}>
                {phaseDrawn.toLocaleString()} / {phaseTotal.toLocaleString()} across {cats.length} sets
              </span>
            </div>
            <div className="h-card-row">
              {cats.map(c => {
                const cnt = rangeCount(c);
                const drw = rangeDrawn(c);
                const pct = (drw / cnt * 100);
                return (
                  <div key={c.name} className="cov-card" style={{ background: pal.panel, borderColor: pal.rule }}>
                    <div className="cov-card-hd">
                      <span className="cov-card-name">{c.name}</span>
                      <span className="cov-card-count">
                        <b style={{ color: pal.ink }}>{drw.toLocaleString()}</b>
                        <span style={{ color: pal.muted }}> / {cnt.toLocaleString()}</span>
                      </span>
                    </div>
                    <div className="cov-card-range" style={{ color: pal.muted }}>{rangeString(c)}</div>
                    <div className="cov-card-bar">
                      <div className="cov-card-bar-fill" style={{
                        width: `${pct.toFixed(2)}%`,
                        background: pal.accent
                      }} />
                    </div>
                    <div style={{ paddingTop: 4 }}>
                      <CoverageGrid ranges={c.ranges} pal={pal} accent={pal.accent} />
                    </div>
                    <div className="cov-card-status" style={{ color: pal.muted }}>
                      {drw === 0 ? 'planned · 0%' :
                       drw === cnt ? `complete · 100%` :
                       `${pct.toFixed(1)}%`}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}
    </Section>
  );
}

// ─── Powerline & icons ───────────────────────────────────────────────────────
// Powerline segment colors keyed to match PALETTES; fg chosen for contrast on each bg
const POWERLINE_PALETTES = {
  default:     { user:{bg:'#ba8baf',fg:'#181818'}, path:{bg:'#7cafc2',fg:'#181818'}, branch:{bg:'#ab4642',fg:'#f8f8f8'}, status:{bg:'#383838',fg:'#d8d8d8'}, ok:{bg:'#a1b56c',fg:'#181818'}, err:{bg:'#ab4642',fg:'#f8f8f8'}, warn:{bg:'#f7ca88',fg:'#181818'} },
  monokai:     { user:{bg:'#ae81ff',fg:'#272822'}, path:{bg:'#66d9e8',fg:'#272822'}, branch:{bg:'#f92672',fg:'#f9f8f5'}, status:{bg:'#49483e',fg:'#f8f8f2'}, ok:{bg:'#a6e22e',fg:'#272822'}, err:{bg:'#f92672',fg:'#f9f8f5'}, warn:{bg:'#f4bf75',fg:'#272822'} },
  solarized:   { user:{bg:'#6c71c4',fg:'#fdf6e3'}, path:{bg:'#268bd2',fg:'#fdf6e3'}, branch:{bg:'#dc322f',fg:'#fdf6e3'}, status:{bg:'#586e75',fg:'#93a1a1'}, ok:{bg:'#859900',fg:'#fdf6e3'}, err:{bg:'#dc322f',fg:'#fdf6e3'}, warn:{bg:'#b58900',fg:'#002b36'} },
  'solarized-l':{ user:{bg:'#6c71c4',fg:'#fdf6e3'}, path:{bg:'#268bd2',fg:'#fdf6e3'}, branch:{bg:'#dc322f',fg:'#fdf6e3'}, status:{bg:'#93a1a1',fg:'#002b36'}, ok:{bg:'#859900',fg:'#fdf6e3'}, err:{bg:'#dc322f',fg:'#fdf6e3'}, warn:{bg:'#b58900',fg:'#002b36'} },
  nord:        { user:{bg:'#b48ead',fg:'#2e3440'}, path:{bg:'#81a1c1',fg:'#2e3440'}, branch:{bg:'#bf616a',fg:'#eceff4'}, status:{bg:'#434c5e',fg:'#e5e9f0'}, ok:{bg:'#a3be8c',fg:'#2e3440'}, err:{bg:'#bf616a',fg:'#eceff4'}, warn:{bg:'#ebcb8b',fg:'#2e3440'} },
  gruvbox:     { user:{bg:'#b16286',fg:'#282828'}, path:{bg:'#458588',fg:'#fbf1c7'}, branch:{bg:'#cc241d',fg:'#fbf1c7'}, status:{bg:'#504945',fg:'#d5c4a1'}, ok:{bg:'#98971a',fg:'#fbf1c7'}, err:{bg:'#cc241d',fg:'#fbf1c7'}, warn:{bg:'#d79921',fg:'#282828'} },
  dracula:     { user:{bg:'#ff79c6',fg:'#282a36'}, path:{bg:'#8be9fd',fg:'#282a36'}, branch:{bg:'#ff5555',fg:'#f8f8f2'}, status:{bg:'#44475a',fg:'#f8f8f2'}, ok:{bg:'#50fa7b',fg:'#282a36'}, err:{bg:'#ff5555',fg:'#f8f8f2'}, warn:{bg:'#f1fa8c',fg:'#282a36'} },
  'one-dark':  { user:{bg:'#c678dd',fg:'#282c34'}, path:{bg:'#61afef',fg:'#282c34'}, branch:{bg:'#e06c75',fg:'#c8ccd4'}, status:{bg:'#3e4451',fg:'#abb2bf'}, ok:{bg:'#98c379',fg:'#282c34'}, err:{bg:'#e06c75',fg:'#c8ccd4'}, warn:{bg:'#e5c07b',fg:'#282c34'} },
  tomorrow:    { user:{bg:'#b294bb',fg:'#1d1f21'}, path:{bg:'#81a2be',fg:'#1d1f21'}, branch:{bg:'#cc6666',fg:'#ffffff'}, status:{bg:'#373b41',fg:'#c5c8c6'}, ok:{bg:'#b5bd68',fg:'#1d1f21'}, err:{bg:'#cc6666',fg:'#ffffff'}, warn:{bg:'#f0c674',fg:'#1d1f21'} },
  ocean:       { user:{bg:'#b48ead',fg:'#2b303b'}, path:{bg:'#8fa1b3',fg:'#2b303b'}, branch:{bg:'#bf616a',fg:'#eff1f5'}, status:{bg:'#4f5b66',fg:'#c0c5ce'}, ok:{bg:'#a3be8c',fg:'#2b303b'}, err:{bg:'#bf616a',fg:'#eff1f5'}, warn:{bg:'#ebcb8b',fg:'#2b303b'} },
};

function PowerlineBar({ segments, pageBg, px = 4, weight = 0, rounding = 0, capRight = true }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const W = TESSERA.CELL_W, H = TESSERA.CELL_H;

    // Build cell list: each segment's text padded with a space on each side,
    // followed by a right-pointing solid arrow transitioning to the next bg.
    const cells = [];
    for (let i = 0; i < segments.length; i++) {
      const s = segments[i];
      const text = ' ' + s.text + ' ';
      for (const ch of text) {
        cells.push({ cp: ch.codePointAt(0), bg: s.bg, fg: s.fg });
      }
      const next = segments[i + 1];
      if (next || capRight) {
        cells.push({ cp: 0xe0b0, bg: next ? next.bg : pageBg, fg: s.bg });
      }
    }

    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const cw = W * px, ch = H * px;
    const Wpx = cells.length * cw, Hpx = ch;
    canvas.width = Wpx * dpr;
    canvas.height = Hpx * dpr;
    canvas.style.width = Wpx + 'px';
    canvas.style.height = Hpx + 'px';
    const ctx = canvas.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.imageSmoothingEnabled = false;

    for (let i = 0; i < cells.length; i++) {
      const c = cells[i];
      ctx.fillStyle = c.bg;
      ctx.fillRect(i * cw, 0, cw, ch);
      const g = TESSERA.getGlyph(c.cp);
      if (g) drawGlyph(ctx, g, i * cw, 0, px, { color: c.fg, bold: weight, rounding });
    }
  }, [segments, pageBg, px, weight, rounding, capRight]);

  return <canvas ref={ref} style={{ display: 'block' }} />;
}

// ─── Starship-style prompts ──────────────────────────────────────────────────
const STARSHIP_COLORS = {
  default:     { dir:'#7cafc2', branch:'#ba8baf', ok:'#a1b56c', warn:'#f7ca88', err:'#ab4642', muted:'#585858', vi:'#86c1b9', dur:'#dc9656' },
  monokai:     { dir:'#66d9e8', branch:'#ae81ff', ok:'#a6e22e', warn:'#f4bf75', err:'#f92672', muted:'#75715e', vi:'#a1efe4', dur:'#fd971f' },
  solarized:   { dir:'#268bd2', branch:'#6c71c4', ok:'#859900', warn:'#b58900', err:'#dc322f', muted:'#657b83', vi:'#2aa198', dur:'#cb4b16' },
  'solarized-l':{ dir:'#268bd2', branch:'#6c71c4', ok:'#859900', warn:'#b58900', err:'#dc322f', muted:'#839496', vi:'#2aa198', dur:'#cb4b16' },
  nord:        { dir:'#81a1c1', branch:'#b48ead', ok:'#a3be8c', warn:'#ebcb8b', err:'#bf616a', muted:'#4c566a', vi:'#88c0d0', dur:'#d08770' },
  gruvbox:     { dir:'#458588', branch:'#b16286', ok:'#98971a', warn:'#d79921', err:'#cc241d', muted:'#7c6f64', vi:'#689d6a', dur:'#d65d0e' },
  dracula:     { dir:'#8be9fd', branch:'#ff79c6', ok:'#50fa7b', warn:'#f1fa8c', err:'#ff5555', muted:'#6272a4', vi:'#bd93f9', dur:'#ffb86c' },
  'one-dark':  { dir:'#61afef', branch:'#c678dd', ok:'#98c379', warn:'#e5c07b', err:'#e06c75', muted:'#545862', vi:'#56b6c2', dur:'#d19a66' },
  tomorrow:    { dir:'#81a2be', branch:'#b294bb', ok:'#b5bd68', warn:'#f0c674', err:'#cc6666', muted:'#969896', vi:'#8abeb7', dur:'#de935f' },
  ocean:       { dir:'#8fa1b3', branch:'#b48ead', ok:'#a3be8c', warn:'#ebcb8b', err:'#bf616a', muted:'#65737e', vi:'#96b5b4', dur:'#d08770' },
};

function StarshipPiece({ text, ink, paper, px, weight, rounding }) {
  return <TextRun text={text} px={px} bold={weight} rounding={rounding} ink={ink} paper={paper} />;
}

function StarshipLine({ pieces, paper, px, weight, rounding }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', flexWrap: 'wrap' }}>
      {pieces.map((p, i) => (
        <StarshipPiece key={i} text={p.text} ink={p.ink} paper={paper} px={px} weight={weight} rounding={rounding} />
      ))}
    </div>
  );
}

function StarshipCard({ pal, mode, weight, rounding }) {
  const c = STARSHIP_COLORS[mode] || STARSHIP_COLORS.default;
  const px = 2;
  const paper = pal.panel;

  const prompts = [
    {
      label: 'clean — main branch, fast',
      line1: [
        { text: '~/code/tessera', ink: c.dir },
        { text: '  ', ink: c.muted },
        { text: '\uf126 main',   ink: c.branch },
        { text: '  ', ink: c.muted },
        { text: '\u2713',        ink: c.ok },
        { text: '  ',            ink: c.muted },
        { text: '\u231a 1.2s',   ink: c.dur }
      ],
      promptChar: '\u276f',
      promptInk: c.ok
    },
    {
      label: 'dirty tree — staged + modified + ahead of origin',
      line1: [
        { text: '~/code/tessera', ink: c.dir },
        { text: '  ', ink: c.muted },
        { text: '\uf126 feat/icons', ink: c.branch },
        { text: ' [', ink: c.muted },
        { text: '\u2731 2',  ink: c.warn },
        { text: ' ',       ink: c.muted },
        { text: '\u271a 3', ink: c.ok },
        { text: ' ',       ink: c.muted },
        { text: '\u21e1 1', ink: c.branch },
        { text: ']',       ink: c.muted },
        { text: '  ', ink: c.muted },
        { text: '\u231a 12.4s', ink: c.dur }
      ],
      promptChar: '\u276f',
      promptInk: c.ok
    },
    {
      label: 'behind & diverged — needs pull / rebase',
      line1: [
        { text: '/etc/nginx', ink: c.dir },
        { text: '  ', ink: c.muted },
        { text: '\uf126 main', ink: c.branch },
        { text: ' [', ink: c.muted },
        { text: '\u21e3 4',    ink: c.warn },
        { text: ' ',           ink: c.muted },
        { text: '\u21d5 2',    ink: c.err },
        { text: ']',           ink: c.muted },
        { text: '  ', ink: c.muted },
        { text: '\u231a 3.0s', ink: c.dur }
      ],
      promptChar: '\u276f',
      promptInk: c.ok
    },
    {
      label: 'failed — last command exited non-zero',
      line1: [
        { text: '~/code/tessera', ink: c.dir },
        { text: '  ', ink: c.muted },
        { text: '\uf126 main', ink: c.branch },
        { text: ' [', ink: c.muted },
        { text: '\u2718 exit 127', ink: c.err },
        { text: ']', ink: c.muted },
        { text: '  ', ink: c.muted },
        { text: '\u231b 2.8s', ink: c.dur }
      ],
      promptChar: '\u276f',
      promptInk: c.err
    },
    {
      label: 'vi normal mode — left chevron, ready for nav',
      line1: [
        { text: '~/code/tessera', ink: c.dir },
        { text: '  ', ink: c.muted },
        { text: '\uf126 main', ink: c.branch },
        { text: '  ', ink: c.muted },
        { text: '\u2713', ink: c.ok },
        { text: '  ',     ink: c.muted },
        { text: '\u231a 1.2s', ink: c.dur }
      ],
      promptChar: '\u276e',
      promptInk: c.vi
    }
  ];

  const symbols = [
    { cp: 0x276f, name: 'prompt' },
    { cp: 0x276e, name: 'vi-mode' },
    { cp: 0x2713, name: 'clean' },
    { cp: 0x2717, name: 'fail' },
    { cp: 0x2718, name: 'deleted' },
    { cp: 0x271a, name: 'staged' },
    { cp: 0x2731, name: 'modified' },
    { cp: 0x21e1, name: 'ahead' },
    { cp: 0x21e3, name: 'behind' },
    { cp: 0x21d5, name: 'diverged' },
    { cp: 0x21bb, name: 'redo' },
    { cp: 0x231a, name: 'watch' },
    { cp: 0x231b, name: 'hourglass' }
  ];

  return (
    <div style={{ background: paper, borderColor: pal.rule, border: '1px solid', padding: 24,
                  display: 'flex', flexDirection: 'column', gap: 22 }}>
      <div className="box-panel-label" style={{ color: pal.muted }}>
        starship prompts — ❯ at the prompt, ✓ ✘ ✚ ⇡ ⇣ ⇕ for git status
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
        {prompts.map((p, i) => (
          <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <StarshipLine pieces={p.line1} paper={paper} px={px} weight={weight} rounding={rounding} />
            <StarshipPiece text={p.promptChar + ' '} ink={p.promptInk} paper={paper}
              px={px} weight={weight} rounding={rounding} />
            <span style={{ fontFamily: 'var(--mono-font)', fontSize: 10, color: pal.muted,
                           letterSpacing: '.04em', textTransform: 'uppercase' }}>
              {p.label}
            </span>
          </div>
        ))}
      </div>

      {/* Symbol legend */}
      <div style={{ paddingTop: 18, borderTop: `1px solid ${pal.rule}` }}>
        <div className="box-panel-label" style={{ color: pal.muted, marginBottom: 14 }}>
          symbol legend — U+27xx dingbats &amp; U+21xx arrows
        </div>
        <div style={{ display: 'grid',
                      gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))',
                      gap: 14 }}>
          {symbols.map(s => (
            <div key={s.cp} style={{ display: 'flex', gap: 10, alignItems: 'center',
                                     border: `1px solid ${pal.rule}`, padding: '8px 10px', background: pal.bg }}>
              <GlyphCell cp={s.cp} px={4} bold={weight} rounding={rounding}
                ink={pal.ink} paper={pal.bg} label={false} />
              <div style={{ fontFamily: 'var(--mono-font)', fontSize: 10, lineHeight: 1.5,
                            display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1 }}>
                <span style={{ color: pal.ink, fontWeight: 500 }}>{s.name}</span>
                <span style={{ color: pal.muted }}>U+{s.cp.toString(16).toUpperCase()}</span>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function IconsPowerline({ pal, mode, weight, rounding }) {
  const plp = POWERLINE_PALETTES[mode] || POWERLINE_PALETTES.default;
  const ch = (cp) => String.fromCodePoint(cp);

  const prompts = [
    {
      label: 'developer prompt — happy path',
      segs: [
        { ...plp.user,   text: ch(0xf007) + ' alex@laptop' },
        { ...plp.path,   text: ch(0xf07c) + ' ~/code/tessera' },
        { ...plp.branch, text: ch(0xe0a0) + ' main' },
        { ...plp.ok,     text: ch(0xf00c) + ' clean' }
      ]
    },
    {
      label: 'feature branch — dirty tree',
      segs: [
        { ...plp.user,   text: ch(0xf2db) + ' dev-rig' },
        { ...plp.path,   text: ch(0xf07c) + ' src/glyphs' },
        { ...plp.branch, text: ch(0xe0a0) + ' feat/icons  +12 ' + ch(0xf071) + ' 2' },
        { ...plp.warn,   text: ch(0xe0a2) + ' staged' }
      ]
    },
    {
      label: 'root shell — failing build',
      segs: [
        { ...plp.user,   text: ch(0xf120) + ' root@build' },
        { ...plp.path,   text: ch(0xf07c) + ' /etc/nginx' },
        { ...plp.err,    text: ch(0xf00d) + ' build failed' },
        { ...plp.status, text: ch(0xf0e7) + ' 6.24s' }
      ]
    },
    {
      label: 'system status bar',
      segs: [
        { ...plp.status, text: ch(0xe712) + ' linux 6.8.2' },
        { ...plp.user,   text: ch(0xf013) + ' tessera-mono' },
        { ...plp.path,   text: ch(0xf0f3) + ' 3 notifications' },
        { ...plp.ok,     text: ch(0x23fd) + ' 92%' }
      ]
    }
  ];

  const separatorRow = [
    { cp: 0xe0b0, name: 'right solid' },
    { cp: 0xe0b1, name: 'right thin' },
    { cp: 0xe0b2, name: 'left solid' },
    { cp: 0xe0b3, name: 'left thin' }
  ];

  // Group all registered icons
  const byGroup = {};
  for (const cp of TESSERA.ICON_ORDER) {
    const info = TESSERA.ICONS[cp];
    if (!info) continue;
    (byGroup[info.group] = byGroup[info.group] || []).push(cp);
  }
  const groupOrder = [
    'Powerline', 'Power', 'Pomicons', 'Codicons', 'Octicons', 'Seti',
    'Files', 'Actions', 'Status', 'Symbols', 'Hardware', 'Places', 'People', 'Logos', 'Media'
  ];

  const [tab, setTab] = useState(0);

  return (
    <Section id="icons" num="07" title="Powerline & icons" accent={pal.accent}
      sub="Phase-0 Nerd Fonts icons — Powerline, IEC power symbols, Pomicons, Codicons, Seti. Same 8×16 cell.">
      <TabBar tabs={['Powerline', 'Starship', 'Icon atlas']} active={tab} onSelect={setTab} />

      {tab === 0 && <>
        <div style={{ background: pal.panel, borderColor: pal.rule, border: '1px solid', padding: 24, display: 'flex', flexDirection: 'column', gap: 18, overflowX: 'auto' }}>
          <div className="box-panel-label" style={{ color: pal.muted }}>powerline prompts — colored segments connected by U+E0B0 separators</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
            {prompts.map((p, i) => (
              <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                <PowerlineBar segments={p.segs} pageBg={pal.panel} px={2} weight={weight} rounding={rounding} />
                <span style={{ fontFamily: 'var(--mono-font)', fontSize: 10, color: pal.muted, letterSpacing: '.04em', textTransform: 'uppercase' }}>{p.label}</span>
              </div>
            ))}
          </div>
        </div>
        <div style={{ background: pal.panel, borderColor: pal.rule, border: '1px solid', padding: 24 }}>
          <div className="box-panel-label" style={{ color: pal.muted, marginBottom: 16 }}>separator family — U+E0B0…E0B3 at 10×</div>
          <div style={{ display: 'flex', gap: 28, flexWrap: 'wrap', alignItems: 'flex-start' }}>
            {separatorRow.map(s => (
              <div key={s.cp} style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
                <GlyphCell cp={s.cp} px={10} bold={weight} rounding={rounding}
                  ink={pal.ink} paper={pal.panel} grid={pal.grid} baseline={pal.baseline} label={false} />
                <div style={{ fontFamily: 'var(--mono-font)', fontSize: 10, color: pal.muted, textAlign: 'center', lineHeight: 1.5 }}>
                  <div style={{ color: pal.ink }}>U+{s.cp.toString(16).toUpperCase()}</div>
                  <div>{s.name}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </>}

      {tab === 1 && <StarshipCard pal={pal} mode={mode} weight={weight} rounding={rounding} />}

      {tab === 2 &&
        <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
          {groupOrder.filter(g => byGroup[g]).map(group => (
            <div key={group} className="atlas-group">
              <div className="atlas-group-hd">
                <span className="atlas-group-title">{group}</span>
                <span className="atlas-group-count" style={{ color: pal.muted }}>{byGroup[group].length} drawn</span>
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 10 }}>
                {byGroup[group].map(cp => {
                  const info = TESSERA.ICONS[cp];
                  const hex = cp.toString(16).toUpperCase().padStart(4, '0');
                  return (
                    <div key={cp} style={{ display: 'flex', gap: 12, alignItems: 'center', border: '1px solid', borderColor: pal.rule, padding: '10px 12px', background: pal.panel }}>
                      <GlyphCell cp={cp} px={5} bold={weight} rounding={rounding}
                        ink={pal.ink} paper={pal.bg} grid={pal.grid} label={false} />
                      <div style={{ fontFamily: 'var(--mono-font)', fontSize: 10, lineHeight: 1.5, display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1 }}>
                        <span style={{ color: pal.ink, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{info.name}</span>
                        <span style={{ color: pal.muted }}>U+{hex}</span>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      }
    </Section>
  );
}

// ─── Specs (metrics + axes) ──────────────────────────────────────────────────
function MetricsDiagram({ pal, weight, rounding }) {
  const ref = useRef(null);
  const px = 12;

  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const W = 8 * px * 3 + 60;  // 3 letters + padding
    const H = 16 * px + 40;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    canvas.style.width = W + 'px';
    canvas.style.height = H + 'px';
    const ctx = canvas.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.imageSmoothingEnabled = false;

    ctx.fillStyle = pal.panel;
    ctx.fillRect(0, 0, W, H);

    // 3 letters: 'Hpg' to show cap, x, descender
    const letters = ['H', 'p', 'g'];
    const x0 = 40;
    const y0 = 20;
    for (let i = 0; i < 3; i++) {
      drawGrid(ctx, x0 + i * 8 * px, y0, px, pal.grid);
      const g = TESSERA.getGlyph(letters[i].charCodeAt(0));
      drawGlyph(ctx, g, x0 + i * 8 * px, y0, px, { color: pal.ink, bold: weight, rounding });
    }

    // Metric lines
    ctx.strokeStyle = pal.accent;
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 3]);
    const drawLine = (rowY, label) => {
      const y = Math.round(y0 + rowY * px) + 0.5;
      ctx.beginPath();
      ctx.moveTo(x0 - 6, y);
      ctx.lineTo(x0 + 24 * px + 6, y);
      ctx.stroke();
      ctx.setLineDash([]);
      ctx.fillStyle = pal.accent;
      ctx.font = '10px ui-monospace, monospace';
      ctx.textAlign = 'right';
      ctx.fillText(label, x0 - 8, y + 3);
      ctx.setLineDash([4, 3]);
    };
    drawLine(0, 'top');
    drawLine(2, 'cap');
    drawLine(4, 'x-h');
    drawLine(11, 'base');
    drawLine(13, 'desc');
    ctx.setLineDash([]);
  }, [pal, weight, rounding]);

  return <canvas ref={ref} className="metrics-canvas" />;
}

function AxesGrid({ pal, rounding, weight, setTweak }) {
  return (
    <div className="axes-grid">
      <div className="axes-hd"></div>
      {[0, 0.5, 1].map(r => (
        <div key={'h' + r} className="axes-col-hd" style={{ color: pal.muted }}>
          rounding {r}
        </div>
      ))}
      {[0, 1, 2].map(w => (
        <React.Fragment key={'r' + w}>
          <div className="axes-row-hd" style={{ color: pal.muted }}>
            {['regular', 'bold', 'extra-bold'][w]}
          </div>
          {[0, 0.5, 1].map(r => (
            <button key={w + '-' + r}
              className={'axes-cell' + (weight === w && rounding === r ? ' axes-cell-on' : '')}
              style={{ background: pal.panel, borderColor: weight === w && rounding === r ? pal.accent : pal.rule }}
              onClick={() => { setTweak('weight', w); setTweak('rounding', r); }}>
              <TextRun text="Hpg&" px={4} bold={w} rounding={r} ink={pal.ink} paper={pal.panel} alts={false} />
            </button>
          ))}
        </React.Fragment>
      ))}
    </div>
  );
}

function Specs({ pal, weight, rounding, setTweak }) {
  return (
    <Section id="specs" num="03" title="System" accent={pal.accent}
      sub="Metrics, axes, and the rendering contract. Designed once on the bitmap grid; rendered as outlined paths so corner rounding and integer scaling fall out for free.">
      <div className="specs-grid">
        <div className="spec-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Metrics</h3>
          <p style={{ color: pal.muted }}>8 × 16 px cell. Caps occupy rows 2–10 (9 rows). x-height rows 4–10 (7 rows). Descenders use 2 rows. Accent reserve lives inside the cell, keeping the default external line gap at zero.</p>
          <MetricsDiagram pal={pal} weight={weight} rounding={rounding} />
          <dl className="spec-dl">
            <dt>cell</dt><dd>8 × 16 px</dd>
            <dt>cap height</dt><dd>9 px</dd>
            <dt>x-height</dt><dd>7 px</dd>
            <dt>ascender</dt><dd>10 px above baseline</dd>
            <dt>descender</dt><dd>2 px below baseline</dd>
            <dt>line gap</dt><dd>0 px (reserve is inside cell)</dd>
            <dt>units/em</dt><dd>1024 (16 × 64 grid)</dd>
          </dl>
        </div>

        <div className="spec-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Renderer previews</h3>
          <p style={{ color: pal.muted }}>The specimen can preview structural dilation and cosmetic rounding. The distributed font is currently a single Regular cut; these controls are design studies, not shipped OpenType variation axes.</p>
          <AxesGrid pal={pal} weight={weight} rounding={rounding} setTweak={setTweak} />
          <label className="ctrl" style={{ marginTop: 10 }}>
            <span>Rounding <b>{rounding}</b></span>
            <input type="range" min="0" max="1" step="0.05" value={rounding}
              onChange={e => setTweak('rounding', +e.target.value)} />
          </label>
          <dl className="spec-dl">
            <dt>weight preview</dt><dd>regular / bold / extra-bold</dd>
            <dt>rounding preview</dt><dd>0 → 100%</dd>
          </dl>
        </div>

        <div className="spec-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Output formats</h3>
          <ul className="spec-list">
            <li><b>BDF</b> — canonical bitmap source, one per integer size.</li>
            <li><b>PCF</b> — compiled bitmap for legacy Unix terminals.</li>
            <li><b>OTB</b> — bitmap-only OpenType, modern terminals.</li>
            <li><b>TTF (vector)</b> — outlined pixels with the rounding axis.</li>
            <li><b>WOFF2</b> — for the web specimen + editors.</li>
          </ul>
          <p style={{ color: pal.muted, marginTop: 12 }}>The TTF is generated from the bitmap by tracing each cell as a single path — the same algorithm this page uses to draw. The ROND axis is a renderer-side parameter, exposed as a custom OT axis.</p>
        </div>

        <div className="spec-card" style={{ background: pal.panel, borderColor: pal.rule }}>
          <h3>Coverage targets</h3>
          <ul className="spec-list">
            <li>Basic Latin (U+0020–U+007E) — <b>complete</b></li>
            <li>Latin-1 Supplement (U+00A0–U+00FF) — <b>in progress</b></li>
            <li>Latin Extended-A — <b>planned</b></li>
            <li>Box Drawing (U+2500–U+257F) — <b>algorithmic</b></li>
            <li>Block Elements (U+2580–U+259F) — <b>algorithmic</b></li>
            <li>Math Operators (curated) — <b>in progress</b></li>
            <li>Arrows (curated) — <b>in progress</b></li>
            <li>Powerline / IEC / Pomicons — <b>P0 planned</b></li>
            <li>Nerd Fonts complete — <b>P2 planned</b></li>
          </ul>
        </div>
      </div>
    </Section>
  );
}

// ─── Ligatures showcase (toggle) ─────────────────────────────────────────────
const LIGATURES = [
  { a: '->',  b: '→' },
  { a: '=>',  b: '⇒' },
  { a: '<-',  b: '←' },
  { a: '<=',  b: '≤' },
  { a: '>=',  b: '≥' },
  { a: '!=',  b: '≠' },
  { a: '~~',  b: '≈' },
  { a: '...', b: '…' }
];

function Ligatures({ pal, weight, rounding, ligatures, alts }) {
  return (
    <Section id="lig" num="09" title="Programming ligatures" accent={pal.accent}
      sub="Optional, opt-in. Bitmap ligatures are tricky — every ligature still lives on the 8×16 grid, no horizontal trickery. Toggle the panel to see substitutions.">
      <div className="lig-grid">
        {LIGATURES.map((l, i) => (
          <div key={i} className="lig-row" style={{ borderColor: pal.rule, background: pal.panel }}>
            <div className="lig-from">
              <TextRun text={l.a} px={3} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} />
              <span style={{ color: pal.muted }}>raw</span>
            </div>
            <div className="lig-arrow" style={{ color: ligatures ? pal.accent : pal.muted }}>
              {ligatures ? '→' : '·'}
            </div>
            <div className="lig-to" style={{ opacity: ligatures ? 1 : 0.35 }}>
              <TextRun text={l.b} px={3} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} />
              <span style={{ color: pal.muted }}>ligature</span>
            </div>
          </div>
        ))}
      </div>

      <div className="alts" style={{ background: pal.panel, borderColor: pal.rule }}>
        <div className="alts-hd">
          <span style={{ color: pal.muted }}>alternates</span>
          <span style={{ color: pal.muted }}>{alts ? 'on' : 'off'}</span>
        </div>
        <div className="alts-row">
          <div className="alts-pair">
            <TextRun text="0" px={6} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} alts={alts} />
            <span style={{ color: pal.muted }}>{alts ? 'slashed zero' : 'plain zero'}</span>
          </div>
          <div className="alts-pair">
            <TextRun text="a" px={6} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} alts={alts} />
            <span style={{ color: pal.muted }}>{alts ? 'single-storey a' : 'double-storey a'}</span>
          </div>
          <div className="alts-pair">
            <TextRun text="l" px={6} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} alts={alts} />
            <span style={{ color: pal.muted }}>{alts ? 'curled l' : 'plain l'}</span>
          </div>
          <div className="alts-pair">
            <TextRun text="g" px={6} bold={weight} rounding={rounding} ink={pal.ink} paper={pal.panel} alts={alts} />
            <span style={{ color: pal.muted }}>{alts ? 'single-storey g' : 'double-storey g'}</span>
          </div>
        </div>
        <p style={{ color: pal.muted, fontSize: 11, marginTop: 10 }}>
          Alternates are drawn but not wired into the bitmap source yet — listed here as the design surface.
        </p>
      </div>
    </Section>
  );
}

// ─── App root ────────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const pal = PALETTES[t.mode] || PALETTES.default;

  // Set CSS vars on root
  useEffect(() => {
    const r = document.documentElement;
    r.style.setProperty('--bg', pal.bg);
    r.style.setProperty('--panel', pal.panel);
    r.style.setProperty('--ink', pal.ink);
    r.style.setProperty('--muted', pal.muted);
    r.style.setProperty('--rule', pal.rule);
    r.style.setProperty('--accent', pal.accent);
  }, [pal]);

  return (
    <div className="app">
      <SectionNav pal={pal} t={t} setTweak={setTweak} />
      <Hero pal={pal} weight={t.weight} rounding={t.rounding} />
      <Specimen pal={pal} weight={t.weight} rounding={t.rounding}
        specimenPx={t.specimenPx} setTweak={setTweak} text={t.specimenText}
        ligatures alts />
      <Proofs pal={pal} weight={t.weight} rounding={t.rounding} />
      <Specs pal={pal} weight={t.weight} rounding={t.rounding} setTweak={setTweak} />
      <Atlas pal={pal} weight={t.weight} rounding={t.rounding} alts />
      <BoxDemo pal={pal} weight={t.weight} rounding={t.rounding} />
      <MathDemo pal={pal} weight={t.weight} rounding={t.rounding} ligatures alts />
      <IconsPowerline pal={pal} mode={t.mode} weight={t.weight} rounding={t.rounding} />
      <NerdRoadmap pal={pal} />
      <Ligatures pal={pal} weight={t.weight} rounding={t.rounding} ligatures alts />

      <footer className="footer">
        <div>Tessera Mono · 8 × 16 · v0.1.0-draft</div>
        <div style={{ color: pal.muted }}>Design specimen — built on the bitmap, rendered as paths.</div>
      </footer>

    </div>
  );
}

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