// site-shared.jsx — drewmcguire.ai shared components, data, hooks

// ============================================================
// SITE DATA
// ============================================================
const SITE = {
  name: 'Drew McGuire',
  tagline: 'Off-grid. On record.',
  bureau: 'Bureau of Documented Record',
  oneLiner: 'A synthetic reporter, running off-grid, finding the numbers powerful institutions hide.',
  youtube: 'youtube.com/@drewmcguire',
  domain: 'drewmcguire.ai',
  estYear: 'MMXXVI',
};

const PAGES = [
  { id: 'home',        label: 'Bureau',      hash: '' },
  { id: 'archive',     label: 'Deep Cuts',   hash: 'archive' },
  { id: 'newsroom',    label: 'Newsroom',    hash: 'newsroom' },
  { id: 'methodology', label: 'Methodology', hash: 'methodology' },
  { id: 'about',       label: 'About',       hash: 'about' },
];

const EPISODES = [
  {
    id: 'DC-005',
    slug: 'dc-005',
    status: 'published',
    title: 'The Company That Must Not Be Named',
    deck: 'Google quietly took 355 million gallons from one Oregon river in a single year. Oregon law lets a corporation classify that number as a trade secret. Congress wrote it that way.',
    theme: 'Water',
    themes: ['Water', 'Corporate secrecy', 'Legislation'],
    runtime: '15:42',
    date: 'May 26, 2026',
    keyNumber: '355,142,887',
    keyUnit: 'gallons',
    keyContext: 'Annual water withdrawal. Classified as a trade secret.',
    accent: 'amber',
    imagery: 'satellite',
    sources: [
      { kind: 'Document',  name: 'H.R. 655 — § 4(c)',                                 org: 'United States Congress',                              year: '2023', verified: true,  excerpt: '"\u2026the use of a trade-secret designation shall be available to any party providing operational data to a state or federal water agency."' },
      { kind: 'Filing',    name: 'Annual Water Use Filings, The Dalles',              org: 'Oregon Department of Environmental Quality',           year: '2023', verified: true,  excerpt: 'Aggregate withdrawal: 355,142,887 gal. Filer name: redacted at filer request.' },
      { kind: 'Filing',    name: 'Site Operations Permit, ORWRD-2018-44',             org: 'Oregon Water Resources Department',                    year: '2018', verified: true,  excerpt: 'Initial permit grants withdrawal authority up to 3.91 MGD. Confidential operator.' },
      { kind: 'Statement', name: 'Public hearing testimony, Wasco County Council',    org: 'Wasco County, Oregon',                                 year: '2022', verified: true,  excerpt: 'Resident testimony entered into record on November 14, 2022. Audio archived by the County Clerk.' },
      { kind: 'Article',   name: 'Public records lawsuit, Oregonian v. The Dalles',   org: 'Multnomah County Circuit Court',                       year: '2022', verified: true,  excerpt: 'Court ordered the City to release water-use figures previously classified as proprietary.' },
      { kind: 'Dataset',   name: 'Public Water System Annual Reports, OR-DEQ',        org: 'Oregon Department of Environmental Quality',           year: '2010-2023', verified: true, excerpt: 'Time-series of city-wide municipal-and-industrial water use for The Dalles, OR.' },
    ],
    voices: [
      { role: 'Drew',     pct: 70, color: 'amber', note: 'Drives the investigation. Cold open, checkpoints, call to action.' },
      { role: 'Resident', pct: 15, color: 'teal',  note: 'A resident of The Dalles. Name withheld. Audio archived.' },
      { role: 'Data',     pct: 15, color: 'red',   note: 'Reads only the verified numbers. Source on screen.' },
    ],
    transcript: [
      { t: '00:00', tag: 'Cold open', text: 'In a city of fifteen thousand, one customer used three hundred fifty-five million gallons of water in twelve months. The customer is allowed, under Oregon law, to keep its name out of the public record.' },
      { t: '02:14', tag: 'Chapter 01', text: 'The number lives in a state filing. The filing has a name field. The name field is empty.' },
      { t: '06:32', tag: 'Chapter 02', text: 'The law that allows this was passed by voice vote. Nobody is on record voting for or against it.' },
      { t: '11:08', tag: 'Chapter 03', text: 'A resident of The Dalles, whose well dropped three feet in 2022, testified at a public hearing. The audio is on file with the County Clerk.' },
      { t: '14:55', tag: 'Sign-off',   text: 'Off-grid. On record.' },
    ],
  },
  { id: 'DC-006', status: 'forthcoming', themes: ['Energy'],         expected: 'Forthcoming · Summer 2026',  kicker: 'A rate negotiated. A schedule sealed.', accent: 'teal'  },
  { id: 'DC-007', status: 'forthcoming', themes: ['AI infrastructure'],   expected: 'Forthcoming · Summer 2026',  kicker: 'What the new build wants from the grid.', accent: 'amber' },
  { id: 'DC-008', status: 'forthcoming', themes: ['Water', 'Public records'], expected: 'Forthcoming · Fall 2026',    kicker: 'The wells permitted in a hearing nobody attended.', accent: 'teal' },
  { id: 'DC-009', status: 'forthcoming', themes: ['Corporate secrecy', 'Legislation'], expected: 'Forthcoming · Fall 2026', kicker: 'A subsidy floor written behind an NDA.', accent: 'amber' },
];

const TOOLS = [
  { name: 'Claude Opus 4',           role: 'Scriptwriting only',          purpose: 'The only cloud dependency. Drafts and revises scripts under the scriptwriter skill. Every line is human-edited before air.', disclosed: 'Methodology · pinned comment.', kind: 'cloud' },
  { name: 'Qwen 3 27B',              role: 'Signal scoring',              purpose: 'Local on LM Studio. Scores every diff from the intake pipeline zero to one. Tickers, direction, time horizon, reasoning.',     disclosed: 'Newsroom.', kind: 'local-ai' },
  { name: 'HeyGen avatar (local)',   role: 'Avatar generation',           purpose: 'Drew\u2019s face. Rendered, not filmed. Runs on the 3090 stack.',                                                                disclosed: 'Every video. Pinned comment.', kind: 'local-ai' },
  { name: '11Labs voice (local)',    role: 'Voice cloning',               purpose: 'Three cloned voices. Drew, Resident, Data. Weights cached and run locally.',                                                  disclosed: 'About page, methodology, pinned.', kind: 'local-ai' },
  { name: 'Wan 2.2',                 role: 'Cinematic generation',        purpose: 'Atmospheric B-roll, faceless reenactments, environments. ComfyUI on the 3090.',                                                disclosed: 'Per-episode source page.', kind: 'local-ai' },
  { name: 'Flux',                    role: 'Still image generation',      purpose: 'Mannequin characters, document mockups, data scenes. Runs locally.',                                                          disclosed: 'Per-episode source page.', kind: 'local-ai' },
  { name: 'yt-dlp',                  role: 'Public hearings + news',      purpose: 'Archives publicly-available hearings, press conferences, news clips. Open source.',                                          disclosed: 'Per-episode source page.', kind: 'tool' },
  { name: 'Meridian intake',         role: '618-source intelligence',     purpose: 'Continuous-poll pipeline. SEC, FERC, USASpending, 990, Reddit, lobbying, plus 78 more categories. Built in-house.',          disclosed: 'Newsroom.', kind: 'tool' },
  { name: 'Sentinel detectors',      role: 'Anomaly detection',           purpose: 'Fifteen pattern detectors. Officer overlap. Address cluster. FY-end flush. Geographic spending void. Etc.',                   disclosed: 'Newsroom.', kind: 'tool' },
  { name: 'Two NVIDIA RTX 3090s',    role: 'Off-grid compute',            purpose: 'Twenty-four hours a day. No public power. No public water. Everything that isn\u2019t Claude runs here.',                     disclosed: 'About page.', kind: 'hardware' },
];

const RULES = [
  'Drew is synthetic. We say so in the intro, the about page, the pinned comment on every video, and the methodology.',
  'No sponsors. No affiliates. No gold pitches.',
  'We present evidence. We do not tell anyone how to feel.',
  'Every claim is sourced. Every source is public. If it cannot be sourced, it gets cut.',
  'The production runs off-grid. Not a marketing line. A verifiable fact.',
  'No partisan framing. The numbers do not have a party.',
  'No fear-mongering. The evidence is enough.',
];

// ============================================================
// HOOKS
// ============================================================
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;

// Hash-based router
const RouterCtx = createContext({ page: 'home', params: {}, navigate: () => {} });

function parseHash() {
  const raw = window.location.hash.replace(/^#\/?/, '');
  if (!raw) return { page: 'home', params: {} };
  const [first, ...rest] = raw.split('/');
  if (first === 'episode') return { page: 'episode', params: { slug: rest[0] || 'dc-005' } };
  const known = PAGES.find(p => p.hash === first);
  return { page: known ? known.id : 'home', params: {} };
}

function useRouter() {
  const [route, setRoute] = useState(parseHash);
  useEffect(() => {
    const onHash = () => {
      setRoute(parseHash());
      window.scrollTo({ top: 0, behavior: 'instant' });
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);
  const navigate = useCallback((to) => {
    if (to === 'home') window.location.hash = '';
    else if (to.startsWith('episode/')) window.location.hash = `/${to}`;
    else window.location.hash = `/${to}`;
  }, []);
  return { ...route, navigate };
}

// IntersectionObserver-based reveal
function useReveal() {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;

    // Failsafe — if IntersectionObserver doesn't deliver (paused tab, throttled
    // preview frame, etc.) force the reveal after a short delay so content
    // never gets stuck at opacity:0.
    const failsafe = setTimeout(() => el.classList.add('is-in'), 250);

    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach(e => {
          if (e.isIntersecting) {
            el.classList.add('is-in');
            clearTimeout(failsafe);
            io.unobserve(el);
          }
        });
      },
      { rootMargin: '0px 0px -8% 0px', threshold: 0.05 }
    );
    io.observe(el);
    return () => { io.disconnect(); clearTimeout(failsafe); };
  }, []);
  return ref;
}

function Reveal({ as: Tag = 'div', delay = 0, children, ...rest }) {
  const ref = useReveal();
  return <Tag ref={ref} className={`reveal ${rest.className || ''}`} style={{ transitionDelay: `${delay}ms`, ...(rest.style || {}) }}>{children}</Tag>;
}

// ============================================================
// PRIMITIVES
// ============================================================

function DMMark({ paper = false, style }) {
  return (
    <div className={`dm-mark${paper ? ' dm-mark--paper' : ''}`} style={style}>
      <span className="dm-tick-1"></span>
      <span className="dm-tick-2"></span>
      <span className="dm-bar"></span>
      <span className="dm-letters"><span>D</span><span>M</span></span>
    </div>
  );
}

function Kicker({ children, color, style, className = '' }) {
  return <span className={`t-kicker ${className}`} style={{ color: color || undefined, ...style }}>{children}</span>;
}

function Bureau({ children = 'Bureau of Documented Record' }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
      <span style={{ display: 'inline-block', width: 18, height: 1.5, background: 'var(--teal-500)' }} />
      <Kicker>{children}</Kicker>
    </span>
  );
}

// ============================================================
// NAV + FOOTER
// ============================================================

function TopNav() {
  const { page, navigate } = useContext(RouterCtx);
  const [menuOpen, setMenuOpen] = useState(false);
  // Close menu on navigation
  useEffect(() => { setMenuOpen(false); }, [page]);
  return (
    <header className="nav">
      <div className="nav-inner">
        <div className="nav-left">
          <a className="nav-mark" onClick={(e) => { e.preventDefault(); navigate('home'); }} href="#">
            <DMMark />
            <div style={{ display: 'flex', flexDirection: 'column', gap: 1, lineHeight: 1 }}>
              <span className="t-cond" style={{ fontWeight: 700, fontSize: 14, letterSpacing: '-0.005em', textTransform: 'uppercase' }}>Drew McGuire</span>
              <span className="t-mono" style={{ fontSize: 9, letterSpacing: '0.24em', textTransform: 'uppercase', color: 'var(--gray-400)' }}>Off-grid. On record.</span>
            </div>
          </a>
          <nav className={`nav-links${menuOpen ? ' is-open' : ''}`} aria-label="Primary">
            {PAGES.filter(p => p.id !== 'home').map(p => (
              <a
                key={p.id}
                href={`#/${p.hash}`}
                className={`nav-link${page === p.id ? ' is-active' : ''}`}
                onClick={(e) => { e.preventDefault(); navigate(p.id); setMenuOpen(false); }}
              >{p.label}</a>
            ))}
          </nav>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <button className="nav-cta" onClick={() => {
            if (page !== 'home') navigate('home');
            setTimeout(() => {
              const el = document.getElementById('newsletter');
              if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
            }, 50);
          }}>Subscribe</button>
          <button className="nav-burger" onClick={() => setMenuOpen(!menuOpen)} aria-label="Menu" aria-expanded={menuOpen}>
            <span /><span /><span />
          </button>
        </div>
      </div>
    </header>
  );
}

function Footer() {
  const { navigate } = useContext(RouterCtx);
  return (
    <footer style={{ background: 'var(--ink-950)', borderTop: '1px solid rgba(236, 231, 220, 0.08)', padding: '56px 0 28px' }}>
      <div className="container">
        <div className="g-footer" style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr', gap: 40, alignItems: 'start' }}>
          <div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
              <DMMark style={{ width: 44, height: 44 }} />
              <div>
                <div className="t-cond" style={{ fontWeight: 700, fontSize: 18, letterSpacing: '-0.005em', textTransform: 'uppercase' }}>Drew McGuire</div>
                <Kicker>{SITE.tagline}</Kicker>
              </div>
            </div>
            <p className="t-body" style={{ marginTop: 18, maxWidth: 360, fontSize: 13 }}>
              {SITE.oneLiner}
            </p>
          </div>
          <div>
            <Kicker>Sections</Kicker>
            <ul style={{ listStyle: 'none', padding: 0, margin: '14px 0 0', display: 'grid', gap: 10 }}>
              {PAGES.map(p => (
                <li key={p.id}>
                  <a href={`#${p.hash ? '/' + p.hash : ''}`} onClick={(e)=>{e.preventDefault(); navigate(p.id);}}
                     className="t-cond" style={{ fontSize: 15, textTransform: 'uppercase', letterSpacing: '-0.005em', color: 'var(--paper)' }}>
                    {p.label}
                  </a>
                </li>
              ))}
            </ul>
          </div>
          <div>
            <Kicker>Disclosure</Kicker>
            <p className="t-body" style={{ fontSize: 13, marginTop: 14 }}>
              Drew is synthetic. The avatar is AI-generated. The voice is cloned. The reporting is sourced.
            </p>
          </div>
          <div>
            <Kicker>Off the grid</Kicker>
            <p className="t-body" style={{ fontSize: 13, marginTop: 14 }}>
              This site and all production runs on two NVIDIA RTX 3090s. No public power. No public water.
            </p>
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 48, paddingTop: 24, borderTop: '1px solid rgba(236, 231, 220, 0.08)' }}>
          <Kicker>© {SITE.estYear} · {SITE.bureau}</Kicker>
          <Kicker>{SITE.domain}</Kicker>
        </div>
      </div>
    </footer>
  );
}

// ============================================================
// NEWSLETTER FORM — live validation + submit state
// ============================================================
function NewsletterForm({ compact = false }) {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);
  const [state, setState] = useState('idle'); // idle | submitting | done | error
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  const showError = touched && !valid && email.length > 0;

  const onSubmit = (e) => {
    e.preventDefault();
    setTouched(true);
    if (!valid) return;
    setState('submitting');
    // simulated request
    setTimeout(() => setState('done'), 900);
  };

  if (state === 'done') {
    return (
      <div style={{
        border: '1.5px solid var(--amber-400)',
        background: 'rgba(229, 172, 88, 0.08)',
        padding: '22px 26px',
        display: 'flex', alignItems: 'center', gap: 18,
      }}>
        <div style={{
          border: '2px solid var(--amber-400)', color: 'var(--amber-400)',
          padding: '6px 12px',
          fontFamily: "'IBM Plex Mono', monospace",
          fontWeight: 600, fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase',
          transform: 'rotate(-3deg)', flexShrink: 0,
        }}>Received</div>
        <div>
          <div className="t-cond" style={{ fontWeight: 700, fontSize: 22, letterSpacing: '-0.005em', textTransform: 'uppercase', lineHeight: 1.1 }}>
            On the list.
          </div>
          <div className="t-body" style={{ marginTop: 4, fontSize: 14 }}>
            <span className="t-mono" style={{ color: 'var(--amber-400)' }}>{email}</span> — confirmation sent. Check your inbox.
          </div>
        </div>
      </div>
    );
  }

  return (
    <form onSubmit={onSubmit} noValidate>
      <div className="field">
        <input
          type="email"
          inputMode="email"
          placeholder="your@email.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          onBlur={() => setTouched(true)}
          aria-label="Email address"
          aria-invalid={showError ? 'true' : 'false'}
        />
        <button type="submit" disabled={state === 'submitting' || !valid}>
          {state === 'submitting' ? 'Sending…' : 'Subscribe'}
        </button>
      </div>
      {showError && (
        <div className="field-error">↳ Enter a valid email address.</div>
      )}
      {!compact && (
        <div className="t-mono" style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--gray-400)', marginTop: 14 }}>
          No sponsors · No affiliates · One-click unsubscribe
        </div>
      )}
    </form>
  );
}

// ============================================================
// EPISODE CARD — used in archive + home
// ============================================================

function ImageStub({ kind = 'satellite', accent = 'var(--teal-400)' }) {
  // miniature versions of the thumbnail imagery — used as the visual on cards
  if (kind === 'satellite') {
    return (
      <svg viewBox="0 0 200 120" style={{ width: '100%', height: '100%', display: 'block' }}>
        <defs>
          <pattern id="sat-mini" width="14" height="14" patternUnits="userSpaceOnUse">
            <path d="M14 0 L0 0 0 14" fill="none" stroke={accent} strokeWidth="0.4" opacity="0.6" />
          </pattern>
          <linearGradient id="sat-bg" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stopColor="#0A1416" />
            <stop offset="100%" stopColor="#040809" />
          </linearGradient>
        </defs>
        <rect width="200" height="120" fill="url(#sat-bg)" />
        <rect width="200" height="120" fill="url(#sat-mini)" />
        <g stroke={accent} strokeWidth="0.6" fill="rgba(88,171,184,0.08)">
          <rect x="50" y="40" width="44" height="14" />
          <rect x="50" y="58" width="44" height="14" />
          <rect x="100" y="40" width="50" height="32" />
        </g>
        <g stroke="var(--amber-400)" strokeWidth="0.5" fill="none">
          <rect x="46" y="36" width="52" height="42" />
          <line x1="72" y1="32" x2="72" y2="38" />
          <line x1="72" y1="76" x2="72" y2="82" />
          <line x1="42" y1="57" x2="48" y2="57" />
          <line x1="96" y1="57" x2="102" y2="57" />
        </g>
      </svg>
    );
  }
  if (kind === 'document') {
    return (
      <svg viewBox="0 0 200 120" style={{ width: '100%', height: '100%', display: 'block' }}>
        <rect width="200" height="120" fill="#0A1416" />
        <g transform="translate(40,18) rotate(-2)">
          <rect width="120" height="84" fill="#DCD6C8" />
          <g fill="#1a1813">
            {[110, 96, 80, 102, 70, 88].map((w, i) => (
              <rect key={i} x="10" y={14 + i * 11} width={w} height="4" opacity="0.85" />
            ))}
            <rect x="64" y="36" width="22" height="6" fill="#0a0907" />
          </g>
          <g transform="translate(95, 10) rotate(8)">
            <rect width="40" height="14" fill="none" stroke="#A82E2E" strokeWidth="1.5" />
            <text x="20" y="10" textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontWeight="700" fontSize="6" letterSpacing="1.5" fill="#A82E2E">TRADE SECRET</text>
          </g>
        </g>
      </svg>
    );
  }
  // grid/abstract default
  return (
    <svg viewBox="0 0 200 120" style={{ width: '100%', height: '100%', display: 'block' }}>
      <defs>
        <pattern id="g-mini" width="10" height="10" patternUnits="userSpaceOnUse">
          <path d="M10 0 L0 0 0 10" fill="none" stroke={accent} strokeWidth="0.4" opacity="0.4" />
        </pattern>
      </defs>
      <rect width="200" height="120" fill="#0A1416" />
      <rect width="200" height="120" fill="url(#g-mini)" />
    </svg>
  );
}

function EpisodeCard({ ep, layout = 'grid' }) {
  const { navigate } = useContext(RouterCtx);
  if (ep.status === 'forthcoming') {
    return (
      <div className="ep-card ep-card--tbd" style={{ padding: 28, display: 'flex', flexDirection: 'column', gap: 14, height: '100%' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <Kicker color="var(--gray-500)">DC-{ep.id.split('-')[1]}</Kicker>
          <span className="t-mono" style={{
            fontSize: 9, letterSpacing: '0.28em', textTransform: 'uppercase',
            border: '1px solid var(--gray-500)', color: 'var(--gray-400)', padding: '4px 8px',
          }}>TBD</span>
        </div>
        <div className="t-cond" style={{ fontSize: 26, letterSpacing: '-0.005em', textTransform: 'uppercase', fontWeight: 700, color: 'var(--gray-400)', lineHeight: 1.1 }}>
          {ep.kicker}
        </div>
        <div style={{ flex: 1 }} />
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          {ep.themes.map(t => (
            <span key={t} className="t-mono" style={{ fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--teal-400)', padding: '4px 8px', border: '1px solid rgba(88,171,184,0.4)' }}>{t}</span>
          ))}
        </div>
        <Kicker color="var(--gray-500)">{ep.expected}</Kicker>
      </div>
    );
  }

  // published card
  return (
    <article
      className="ep-card"
      onClick={() => navigate(`episode/${ep.slug}`)}
      role="link"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === 'Enter') navigate(`episode/${ep.slug}`); }}
      style={{ display: 'flex', flexDirection: layout === 'wide' ? 'row' : 'column', gap: 0, height: '100%' }}
    >
      <div style={{
        width: layout === 'wide' ? '46%' : '100%',
        aspectRatio: layout === 'wide' ? 'auto' : '16 / 9',
        position: 'relative', overflow: 'hidden',
        background: '#040809',
        borderBottom: layout === 'wide' ? 'none' : '1px solid rgba(236,231,220,0.06)',
        borderRight: layout === 'wide' ? '1px solid rgba(236,231,220,0.06)' : 'none',
        flexShrink: 0,
      }}>
        <ImageStub kind={ep.imagery} accent={ep.accent === 'amber' ? 'var(--amber-400)' : 'var(--teal-400)'} />
        <div style={{ position: 'absolute', top: 14, left: 14, display: 'flex', gap: 8 }}>
          <Kicker color="var(--amber-400)">{ep.id}</Kicker>
        </div>
        <div style={{ position: 'absolute', bottom: 14, right: 14, color: 'var(--gray-300)', background: 'rgba(7,9,10,0.7)', padding: '4px 8px' }}>
          <Kicker color="var(--gray-300)">{ep.runtime}</Kicker>
        </div>
      </div>
      <div style={{ padding: 28, display: 'flex', flexDirection: 'column', gap: 14, flex: 1 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          {ep.themes.map(t => (
            <span key={t} className="t-mono" style={{ fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--teal-400)' }}>{t}</span>
          ))}
        </div>
        <h3 className="t-cond" style={{ margin: 0, fontSize: layout === 'wide' ? 36 : 28, lineHeight: 1.05, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>
          {ep.title}
        </h3>
        <p className="t-body" style={{ margin: 0, fontSize: 14 }}>
          {ep.deck}
        </p>
        <div style={{ flex: 1 }} />
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <Kicker>{ep.date}</Kicker>
          <span className="ep-card-arrow t-mono" style={{ fontSize: 12, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--paper)', display: 'flex', alignItems: 'center', gap: 6 }}>
            Read the file <span aria-hidden="true">→</span>
          </span>
        </div>
      </div>
    </article>
  );
}

// ============================================================
// PAGE HEADER (used on every non-home page)
// ============================================================
function PageHeader({ kicker, title, subtitle }) {
  return (
    <section className="section" style={{ paddingTop: 80, paddingBottom: 40, borderBottom: '1px solid rgba(236,231,220,0.08)' }}>
      <div className="container">
        <Reveal>
          <Bureau>{kicker}</Bureau>
        </Reveal>
        <Reveal delay={80}>
          <h1 className="t-cond" style={{ margin: '20px 0 0', fontSize: 'clamp(56px, 8vw, 112px)', lineHeight: 0.92, letterSpacing: '-0.02em', textTransform: 'uppercase' }}>
            {title}
          </h1>
        </Reveal>
        {subtitle && (
          <Reveal delay={160}>
            <p className="t-bodyL" style={{ margin: '24px 0 0', maxWidth: 720 }}>{subtitle}</p>
          </Reveal>
        )}
      </div>
    </section>
  );
}

// ============================================================
// EXPORTS
// ============================================================
Object.assign(window, {
  SITE, PAGES, EPISODES, TOOLS, RULES,
  RouterCtx, useRouter, useReveal, Reveal,
  DMMark, Kicker, Bureau,
  TopNav, Footer, NewsletterForm,
  EpisodeCard, ImageStub, PageHeader,
});
