/* global React, WORKS, SOCIETIES, RELEASES, Ic, Pill, Section, useS2 */
// ───────────────────────────────────────────────────────────── ROYALTIES
// Top-level "Statement Inbox" — every parsed royalty statement we have on file,
// per period, with reconciliation status. Sources, periods, gross USD, line counts,
// match rates — everything is computed from window.__STMT_INDEX (real parsed CSVs
// in db/statements/). No synthetic data.

(function () {
  const { useState, useMemo } = React;

  // Live override: if the user has marked a statement reconciled in the drawer,
  // the decision-store on window has the new status — read it instead of the seed value.
  const effStatus = (s) => window.__STMT_DECISIONS && window.__STMT_DECISIONS[s.id] && window.__STMT_DECISIONS[s.id].status || s.status;

  // ─────────────────────────── source registry — REAL DATA ONLY
  // Sources are derived from window.__STMT_INDEX (real parsed statements). No synthetic
  // sources, no sub-pubs, no DSPs we don't actually receive statements from.
  // Each (statement-issuing) entity gets one source row. DSPs surface as a separate
  // dimension via line-level data — see DSPBreakdown.
  const SOURCE_META = {
    // Statement-issuing distributors / agents / societies. Color + cadence are display only.
    src_rsd: { color: '#ff5b1f', kind: 'DSP', freq: 'quarterly', cadenceDays: 30 },
    src_rs_llc: { color: '#0891b2', kind: 'DSP', freq: 'quarterly', cadenceDays: 30 },
    src_tos: { color: '#db2777', kind: 'DSP', freq: 'quarterly', cadenceDays: 30 },
    src_true956: { color: '#f59e0b', kind: 'DSP', freq: 'quarterly', cadenceDays: 30 },
    src_bmi: { color: '#c8302a', kind: 'PRO', freq: 'quarterly', cadenceDays: 90 },
    src_ascap_intl: { color: '#3344ff', kind: 'PRO', freq: 'quarterly', cadenceDays: 120 },
    src_ascap_foreign: { color: '#1ea4a4', kind: 'PRO', freq: 'quarterly', cadenceDays: 140 },
    src_hfa_lyricfind: { color: '#7c3aed', kind: 'MRO', freq: 'quarterly', cadenceDays: 90 },
    src_hfa_spotify_usa_inc: { color: '#7c3aed', kind: 'MRO', freq: 'monthly', cadenceDays: 60 },
    src_tt: { color: '#000000', kind: 'DSP', freq: 'quarterly', cadenceDays: 75 }
  };

  // Build sources from the statement index — one row per distinct (sourceId).
  function getAllSources() {
    const idx = window.__STMT_INDEX;
    if (!idx || !idx.statements) return [];
    const seen = new Map();
    for (const real of idx.statements) {
      if (seen.has(real.sourceId)) continue;
      const meta = SOURCE_META[real.sourceId] || {};
      seen.set(real.sourceId, {
        id: real.sourceId,
        name: real.sourceName,
        kind: meta.kind || real.sourceKind || 'DSP',
        freq: meta.freq || 'quarterly',
        currency: real.currency || 'USD',
        cadenceDays: meta.cadenceDays || 90,
        color: meta.color || real.sourceColor || '#1f4ed8'
      });
    }
    return [...seen.values()];
  }

  // Periods covered. Newest first.
  const PERIODS = [
  { id: '2026Q1', label: '2026 Q1', start: '2026-01-01', end: '2026-03-31', closed: '2026-04-15' },
  { id: '2025Q4', label: '2025 Q4', start: '2025-10-01', end: '2025-12-31', closed: '2026-01-15' },
  { id: '2025Q3', label: '2025 Q3', start: '2025-07-01', end: '2025-09-30', closed: '2025-10-15' },
  { id: '2025Q2', label: '2025 Q2', start: '2025-04-01', end: '2025-06-30', closed: '2025-07-15' },
  { id: '2021Q4', label: '2021 Q4', start: '2021-10-01', end: '2021-12-31', closed: '2022-01-15' }];


  // Royalty types per source kind (for the line-type breakdown column)
  const TYPE_BY_KIND = {
    DSP: 'mechanical+performance',
    PRO: 'performance',
    MRO: 'mechanical',
    NRO: 'neighboring'
  };

  // FX → USD (fixed for prototype; in production reads from astro.exchange_rates)
  const FX = { USD: 1, EUR: 1.08, GBP: 1.27, JPY: 0.0066 };

  // Status taxonomy mirrors what astro.royalty_statements.import_status would carry.
  // Plus our own reconciliation status overlaid on top.
  //   pending      — expected but not received
  //   received     — uploaded, raw file present, not yet parsed
  //   parsing      — parser running
  //   parsed       — lines extracted, not yet matched to works/recordings
  //   matched      — lines matched, anomalies flagged
  //   reconciled   — accepted, ready for payout
  //   paid         — payout cut, statement archived
  //   failed       — parse or match failed, needs human review
  //   late         — past expected arrival date


  // Build statement-inbox rows from real data ONLY. No synthetic generation.
  // Every row corresponds to a parsed CSV/XLSX in db/statements/.
  function buildStatements() {
    const out = [];
    applyRealStatements(out);
    return out.sort((a, b) => (b.processedAt || b.expectedDate) - (a.processedAt || a.expectedDate));
  }

  // Replace or insert real-statement rows from window.__STMT_INDEX. Synthetic rows
  // that match (sourceId, period) are dropped in favor of the real one — keeps the
  // inbox honest about what we actually have.
  function applyRealStatements(out) {
    const idx = window.__STMT_INDEX;
    if (!idx || !idx.statements) return;
    const allSources = getAllSources();
    for (const real of idx.statements) {
      const src = allSources.find((s) => s.id === real.sourceId) || {
        id: real.sourceId, name: real.sourceName, kind: real.sourceKind,
        freq: 'quarterly', currency: real.currency || 'USD',
        cadenceDays: 90, color: real.sourceColor || '#1f4ed8'
      };
      const per = PERIODS.find((p) => p.id === real.period);
      if (!per) continue; // statement period not in our viewable window
      // Drop existing synthetic row for this (source, period)
      const existingIdx = out.findIndex((r) => r.source.id === real.sourceId && r.period.id === real.period);
      const today = new Date('2026-04-30');
      const closedDate = new Date(real.closedDate || per.closed);
      const processedAt = real.processedDate ? new Date(real.processedDate) : null;
      const expectedDate = new Date(closedDate.getTime() + (src.cadenceDays || 90) * 86400000);
      const ageDays = Math.round((today - expectedDate) / 86400000);

      const realRow = {
        id: real.id,
        source: src,
        period: per,
        // If a real statement is on file, treat it as PAID/RECONCILED automatically —
        // these are historical statements the user has uploaded. (When real ingest
        // is wired the status will come from the import record.)
        status: 'reconciled',
        lines: real.lineCount,
        gross: real.grossUsd, // already in USD
        grossUsd: real.grossUsd,
        currency: real.currency || 'USD',
        anomalies: real.lines.length - (real.matchedLineCount || 0),
        expectedDate,
        processedAt: processedAt || closedDate,
        ageDays,
        // Real-statement payload for the drawer
        isReal: true,
        realStmtId: real.id,
        realLines: real.lines.length,
        realMatched: real.matchedLineCount,
        realLinkedWorks: real.linkedWorks?.length || 0,
        realLinkedRecordings: real.linkedRecordings?.length || 0,
        legalEntity: real.legalEntity,
        periodLabel: real.periodLabel
      };
      if (existingIdx >= 0) out[existingIdx] = realRow;else out.push(realRow);
    }
  }

  // ─────────────────────────── helpers
  const fmt = {
    cur: (v, ccy = 'USD') => {
      if (v == null) return '—';
      if (ccy === 'JPY') return '¥' + Math.round(v).toLocaleString();
      if (ccy === 'EUR') return '€' + Math.round(v).toLocaleString();
      if (ccy === 'GBP') return '£' + Math.round(v).toLocaleString();
      return '$' + Math.round(v).toLocaleString();
    },
    short: (v) => v >= 1e6 ? (v / 1e6).toFixed(1) + 'M' : v >= 1e3 ? (v / 1e3).toFixed(1) + 'K' : String(v),
    date: (d) => d ? d.toISOString().slice(0, 10) : '—',
    ago: (days) => {
      if (days == null) return '—';
      const a = Math.abs(days);
      const dir = days < 0 ? 'in ' : '';
      const suf = days < 0 ? '' : ' ago';
      if (a < 1) return 'today';
      if (a < 7) return dir + a + 'd' + suf;
      if (a < 30) return dir + Math.round(a / 7) + 'w' + suf;
      if (a < 365) return dir + Math.round(a / 30) + 'mo' + suf;
      return dir + Math.round(a / 365) + 'y' + suf;
    }
  };

  const STATUS_META = {
    pending: { label: 'PENDING', fg: 'var(--ink-3)', bg: 'transparent', bd: 'var(--rule)', dot: 'var(--ink-4)' },
    late: { label: 'LATE', fg: '#a04432', bg: 'rgba(160,68,50,.08)', bd: '#a04432', dot: '#a04432' },
    received: { label: 'RECEIVED', fg: 'var(--ink)', bg: 'var(--bg-2)', bd: 'var(--rule)', dot: 'var(--ink-2)' },
    parsing: { label: 'PARSING', fg: 'var(--ink)', bg: 'var(--bg-2)', bd: 'var(--rule)', dot: 'var(--ink-2)' },
    parsed: { label: 'PARSED', fg: '#1f4ed8', bg: 'rgba(31,78,216,.07)', bd: '#1f4ed8', dot: '#1f4ed8' },
    matched: { label: 'MATCHED', fg: '#7a4cb0', bg: 'rgba(122,76,176,.07)', bd: '#7a4cb0', dot: '#7a4cb0' },
    reconciled: { label: 'RECONCILED', fg: '#2d6a3f', bg: 'rgba(45,106,63,.07)', bd: '#2d6a3f', dot: '#2d6a3f' },
    paid: { label: 'PAID OUT', fg: 'var(--bg)', bg: 'var(--ink)', bd: 'var(--ink)', dot: 'var(--ink)' },
    failed: { label: 'FAILED', fg: '#a04432', bg: '#a04432', bd: '#a04432', dot: 'var(--bg)', invert: true }
  };
  function StatusChip({ s }) {
    const m = STATUS_META[s];
    return (
      <span className="ff-mono upper" style={{
        display: 'inline-flex', alignItems: 'center', gap: 6, padding: '3px 8px', fontSize: 9,
        letterSpacing: '.1em', fontWeight: 600,
        color: m.invert ? 'var(--bg)' : m.fg,
        background: m.bg, border: `1px solid ${m.bd}`
      }}>
      <span style={{ width: 5, height: 5, background: m.dot, display: 'inline-block', borderRadius: m.invert ? 0 : 0 }} />
      {m.label}
    </span>);

  }

  // ─────────────────────────── small components
  function Stat({ label, value, sub, tone, accent }) {
    return (
      <div style={{ padding: '18px 22px', borderRight: '1px solid var(--rule)' }}>
      <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 10 }}>{label}</div>
      <div className="ff-display num" style={{ fontSize: 36, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1, color: tone || 'var(--ink)' }}>{value}</div>
      {sub && <div className="ff-mono" style={{ fontSize: 10, color: accent || 'var(--ink-3)', marginTop: 8, letterSpacing: '.04em' }}>{sub}</div>}
    </div>);

  }

  // Pipeline funnel — counts per stage for the active period
  function Pipeline({ stmts }) {
    const stages = [
    { k: 'expected', label: 'EXPECTED' },
    { k: 'received', label: 'RECEIVED' },
    { k: 'parsed', label: 'PARSED' },
    { k: 'matched', label: 'MATCHED' },
    { k: 'reconciled', label: 'RECONCILED' },
    { k: 'paid', label: 'PAID OUT' }];

    const counts = {
      expected: stmts.length,
      received: stmts.filter((s) => !['pending', 'late'].includes(effStatus(s))).length,
      parsed: stmts.filter((s) => ['parsed', 'matched', 'reconciled', 'paid'].includes(effStatus(s))).length,
      matched: stmts.filter((s) => ['matched', 'reconciled', 'paid'].includes(effStatus(s))).length,
      reconciled: stmts.filter((s) => ['reconciled', 'paid'].includes(effStatus(s))).length,
      paid: stmts.filter((s) => effStatus(s) === 'paid').length
    };
    const max = counts.expected || 1;
    return (
      <div style={{ border: '1px solid var(--rule)', marginBottom: 32, background: 'var(--bg)' }}>
      <div style={{ padding: '10px 16px', borderBottom: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink-3)' }}>RECONCILIATION PIPELINE</span>
        <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>conversion · {counts.expected} → {counts.paid} ({Math.round(counts.paid / counts.expected * 100)}%)</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${stages.length},1fr)`, gap: 0 }}>
        {stages.map((st, i) => {
            const v = counts[st.k];
            const pct = Math.round(v / max * 100);
            const lost = i > 0 ? counts[stages[i - 1].k] - v : 0;
            return (
              <div key={st.k} style={{ padding: '18px 18px 16px', borderRight: i < stages.length - 1 ? '1px solid var(--rule)' : 'none', position: 'relative' }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 10 }}>{st.label}</div>
              <div className="ff-display num" style={{ fontSize: 42, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1 }}>{v}</div>
              <div style={{ height: 4, background: 'var(--bg-2)', marginTop: 12, position: 'relative', overflow: 'hidden' }}>
                <div style={{ position: 'absolute', inset: 0, width: `${pct}%`, background: i === stages.length - 1 ? 'var(--accent)' : 'var(--ink)' }} />
              </div>
              <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginTop: 8, display: 'flex', justifyContent: 'space-between', letterSpacing: '.05em' }}>
                <span>{pct}%</span>
                {i > 0 && lost > 0 && <span>−{lost} drop-off</span>}
              </div>
            </div>);

          })}
      </div>
    </div>);

  }

  // Source mini-chart for sidebar — sources ranked by gross USD this period
  function SourceLeaders({ stmts, onPick }) {
    const byKind = {};
    stmts.forEach((s) => {
      if (!s.grossUsd) return;
      const k = s.source.kind;
      if (!byKind[k]) byKind[k] = { kind: k, total: 0, sources: {} };
      byKind[k].total += s.grossUsd;
      if (!byKind[k].sources[s.source.id]) byKind[k].sources[s.source.id] = { src: s.source, total: 0 };
      byKind[k].sources[s.source.id].total += s.grossUsd;
    });
    const all = stmts.reduce((a, s) => a + s.grossUsd, 0) || 1;
    const sorted = Object.values(byKind).sort((a, b) => b.total - a.total);
    return (
      <div>
      <Section num="03">By source · this period</Section>
      {sorted.map((g) =>
        <div key={g.kind} style={{ marginBottom: 18 }}>
          <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
            <span>{g.kind === 'DSP' ? 'DISTRIBUTORS' : g.kind === 'PRO' ? 'PERFORMANCE SOCIETIES' : g.kind === 'MRO' ? 'MECHANICAL AGENTS' : g.kind === 'NRO' ? 'NEIGHBORING RIGHTS' : g.kind}</span>
            <span className="num">{fmt.cur(g.total)} · {Math.round(g.total / all * 100)}%</span>
          </div>
          {Object.values(g.sources).sort((a, b) => b.total - a.total).map((s) =>
          <div key={s.src.id} onClick={() => onPick(s.src.id)} style={{ display: 'grid', gridTemplateColumns: '140px 1fr 80px', gap: 10, padding: '7px 0', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center', cursor: 'pointer' }}>
              <span style={{ fontSize: 12, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ width: 8, height: 8, background: s.src.color, display: 'inline-block' }} />{s.src.name}
              </span>
              <div style={{ height: 4, background: 'var(--bg-2)', position: 'relative' }}>
                <div style={{ position: 'absolute', inset: 0, width: `${Math.min(100, s.total / g.total * 100)}%`, background: s.src.color }} />
              </div>
              <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right' }}>{fmt.cur(s.total)}</span>
            </div>
          )}
        </div>
        )}
    </div>);

  }

  // Anomaly callouts — items the parser flagged for human review.
  // Tiered by severity: BLOCKING (failed parses, money-stuck) → WARNING (late, high anomaly count)
  // → FYI (variance, FX). Mirrors the Issues page treatment so blockers always lead.
  function Anomalies({ stmts }) {
    const blocking = [],warning = [],fyi = [];
    stmts.forEach((s) => {
      if (s.status === 'failed') blocking.push({ kind: 'failed', stmt: s, msg: `${s.source.name} · parse failed — schema mismatch on column 14`, action: 'reparse' });
      if (s.anomalies && s.anomalies > 7) blocking.push({ kind: 'anom-high', stmt: s, msg: `${s.source.name} · ${s.anomalies} unmatched lines blocking reconcile`, action: 'review' });
      if (s.status === 'late') warning.push({ kind: 'late', stmt: s, msg: `${s.source.name} · ${s.period.label} statement ${Math.abs(s.ageDays)}d overdue`, action: 'chase' });
      if (s.anomalies && s.anomalies > 4 && s.anomalies <= 7) warning.push({ kind: 'anom', stmt: s, msg: `${s.source.name} · ${s.anomalies} unmatched lines pending review`, action: 'review' });
    });
    // FYI-tier callouts — derived from real statement data
    stmts.forEach((s) => {
      if (s.isReal && s.realMatched != null && s.realLines > 50) {
        const matchPct = s.realMatched / s.realLines * 100;
        if (matchPct < 50) fyi.push({ kind: 'low-match', stmt: s, msg: `${s.source.name} · only ${Math.round(matchPct)}% of ${s.realLines} lines matched to catalog`, action: 'review' });
      }
    });

    const total = blocking.length + warning.length + fyi.length;
    if (!total) return null;

    const Tier = ({ items, label, color, accent }) => items.length === 0 ? null :
    <div>
      <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color, padding: '10px 16px', background: accent, borderBottom: '1px solid var(--rule-soft)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span><span style={{ display: 'inline-block', width: 6, height: 6, background: color, marginRight: 8, verticalAlign: 'middle' }} />{label} · {items.length}</span>
      </div>
      {items.slice(0, 6).map((it, i) =>
      <div key={i} style={{ padding: '11px 16px', borderBottom: i < Math.min(items.length - 1, 5) ? '1px solid var(--rule-soft)' : 'none', display: 'flex', gap: 12, alignItems: 'center' }}>
          <span style={{ flexShrink: 0, width: 3, height: 24, background: color }} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, fontWeight: 500, letterSpacing: '-0.005em' }}>{it.msg}</div>
            <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', marginTop: 3 }}>{it.kind} · {it.stmt ? it.stmt.period.label : 'cross-period'}</div>
          </div>
          <button className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.08em', color: 'var(--ink-2)', cursor: 'pointer', flexShrink: 0, padding: '4px 8px', border: '1px solid var(--rule)', background: 'var(--bg)' }}
        onClick={(e) => {e.stopPropagation();if (it.stmt) window.dispatchEvent(new CustomEvent('astro-open-statement', { detail: { id: it.stmt.id } }));}}>
            {it.action} →
          </button>
        </div>
      )}
    </div>;


    return (
      <div style={{ border: '1px solid var(--rule)', marginBottom: 32 }}>
      <div style={{ padding: '10px 16px', borderBottom: '1px solid var(--rule)', background: 'var(--bg-2)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink)' }}>
          FLAGS · {total} items
          {blocking.length > 0 && <span style={{ color: '#a04432', marginLeft: 10 }}>· {blocking.length} blocking</span>}
        </span>
        <button className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.08em', color: 'var(--ink-3)', cursor: 'pointer', background: 'none', border: 0, padding: 0 }}>dismiss all →</button>
      </div>
      <Tier items={blocking} label="BLOCKING · money stuck" color="#a04432" accent="rgba(160,68,50,.05)" />
      <Tier items={warning} label="WARNING · needs attention" color="#c79538" accent="rgba(199,149,56,.05)" />
      <Tier items={fyi} label="FYI · informational" color="var(--ink-3)" accent="var(--bg-2)" />
    </div>);

  }

  // DSP / platform breakdown — drills into the line-level data of every real statement
  // in the active period, so Spotify, Apple Music, YouTube etc. surface as their own
  // rows even though they arrive bundled inside RSD or HFA statements. This is the
  // "where did the money come from at the platform level" view.
  function DSPBreakdown({ stmts }) {
    const idx = window.__STMT_INDEX;
    if (!idx || !stmts.length) return null;
    // For each line in each real statement, derive a "platform" key based on parser:
    //   rsd-distributor → l.dsp (Spotify, Apple Music, YouTube, etc.)
    //   bmi             → l.perfSource
    //   ascap-intl      → l.musicUser || l.perfSource
    //   ascap-foreign   → l.licensor (society of origin abroad)
    //   hfa-mech        → l.licenseeName (Spotify USA, LyricFind, etc.)
    //   tiktok-mri      → 'TikTok'
    const byPlatform = new Map();
    stmts.filter((s) => s.isReal).forEach((s) => {
      const real = idx.statements.find((r) => r.id === s.realStmtId);
      if (!real) return;
      real.lines.forEach((l) => {
        let key, terr;
        if (real.parser === 'rsd-distributor') {key = l.dsp || 'Unknown';terr = l.territory;} else
        if (real.parser === 'bmi') {key = l.perfSource || 'Unknown';terr = l.country;} else
        if (real.parser === 'ascap-intl') {key = l.musicUser || l.perfSource || 'Unknown';terr = l.territory;} else
        if (real.parser === 'ascap-foreign') {key = l.licensor || 'Unknown';terr = l.country || l.territory;} else
        if (real.parser === 'hfa-mech') {key = l.licenseeName || 'Unknown';terr = l.territory;} else
        if (real.parser === 'tiktok-mri') {key = 'TikTok';terr = null;} else
        {key = 'Unknown';terr = null;}
        if (!byPlatform.has(key)) byPlatform.set(key, { name: key, gross: 0, lines: 0, sources: new Set(), territories: new Set() });
        const x = byPlatform.get(key);
        x.gross += l.royaltyUsd || l.grossAmount || l.royaltyAmount || 0;
        x.lines += 1;
        x.sources.add(real.sourceName);
        if (terr) x.territories.add(terr);
      });
    });
    const rows = [...byPlatform.values()].sort((a, b) => b.gross - a.gross);
    if (!rows.length) return null;
    const total = rows.reduce((a, r) => a + r.gross, 0) || 1;

    return (
      <div style={{ marginTop: 32 }}>
      <Section num="06">By platform · per-line drilldown</Section>
      <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 14 }}>
        {rows.length} PLATFORMS · {fmt.cur(total)} CONSOLIDATED · derived from {stmts.filter((s) => s.isReal).reduce((a, s) => a + s.realLines, 0).toLocaleString()} statement lines
      </div>
      <div style={{ border: '1px solid var(--rule)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1.3fr 80px 1fr 100px 70px', gap: 0, padding: '9px 14px', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)' }}>
          {['PLATFORM', 'LINES', 'VIA', 'GROSS', 'SHARE'].map((l, i) =>
            <span key={l} className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', textAlign: i >= 3 ? 'right' : 'left' }}>{l}</span>
            )}
        </div>
        {rows.slice(0, 20).map((r, i) => {
            const pct = r.gross / total * 100;
            return (
              <div key={r.name} style={{ display: 'grid', gridTemplateColumns: '1.3fr 80px 1fr 100px 70px', gap: 0, padding: '10px 14px', borderBottom: i < Math.min(rows.length - 1, 19) ? '1px solid var(--rule-soft)' : 'none', alignItems: 'center' }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.name}</div>
                {r.territories.size > 0 &&
                  <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginTop: 2, letterSpacing: '.04em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {r.territories.size} territor{r.territories.size === 1 ? 'y' : 'ies'}
                  </div>
                  }
              </div>
              <span className="ff-mono num" style={{ fontSize: 11, color: 'var(--ink-2)' }}>{r.lines.toLocaleString()}</span>
              <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', letterSpacing: '.04em' }}>
                {[...r.sources].join(', ')}
              </span>
              <span className="ff-mono num" style={{ fontSize: 13, fontWeight: 600, textAlign: 'right' }}>{fmt.cur(r.gross)}</span>
              <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right', color: 'var(--ink-3)' }}>{pct < 0.1 ? '<0.1' : pct.toFixed(1)}%</span>
            </div>);

          })}
        {rows.length > 20 &&
          <div className="ff-mono" style={{ padding: '10px 14px', background: 'var(--bg-2)', fontSize: 10, color: 'var(--ink-3)', textAlign: 'center', letterSpacing: '.04em' }}>
            + {rows.length - 20} more platforms
          </div>
          }
      </div>
    </div>);

  }

  // Currency exposure — what % of incoming royalties is in each currency, with FX context.
  // Replaces the old static "Recent payouts" stub; this is computed from live statement data.
  function CurrencyExposure({ stmts }) {
    const byCcy = {};
    stmts.forEach((s) => {
      if (!s.grossUsd) return;
      if (!byCcy[s.currency]) byCcy[s.currency] = { ccy: s.currency, native: 0, usd: 0, count: 0, sources: new Set() };
      byCcy[s.currency].native += s.gross;
      byCcy[s.currency].usd += s.grossUsd;
      byCcy[s.currency].count += 1;
      byCcy[s.currency].sources.add(s.source.name);
    });
    const totalUsd = Object.values(byCcy).reduce((a, r) => a + r.usd, 0) || 1;
    const rows = Object.values(byCcy).sort((a, b) => b.usd - a.usd);
    if (rows.length === 0) return null;

    const ccyMeta = {
      USD: { flag: '🇺🇸', color: '#2d6a3f' },
      EUR: { flag: '🇪🇺', color: '#1f4ed8' },
      GBP: { flag: '🇬🇧', color: '#7a4cb0' },
      JPY: { flag: '🇯🇵', color: '#bc002d' }
    };

    return (
      <div style={{ marginTop: 32 }}>
      <Section num="04">Currency exposure</Section>
      <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 14 }}>
        {rows.length} CURRENCIES · {fmt.cur(totalUsd)} CONSOLIDATED · LIVE FX
      </div>
      {rows.map((r) => {
          const pct = r.usd / totalUsd * 100;
          const meta = ccyMeta[r.ccy] || { flag: '·', color: 'var(--ink-2)' };
          return (
            <div key={r.ccy} style={{ padding: '12px 0', borderBottom: '1px solid var(--rule-soft)' }}>
            <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 10, alignItems: 'baseline', marginBottom: 8 }}>
              <span style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                <span style={{ fontSize: 14 }}>{meta.flag}</span>
                <span style={{ fontSize: 13, fontWeight: 600 }}>{r.ccy}</span>
              </span>
              <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.04em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                {r.count} stmt · {[...r.sources].slice(0, 2).join(', ')}{r.sources.size > 2 ? ` +${r.sources.size - 2}` : ''}
              </span>
              <span className="ff-mono num" style={{ fontSize: 13, fontWeight: 600, textAlign: 'right' }}>{fmt.cur(r.usd)}</span>
            </div>
            <div style={{ height: 6, background: 'var(--bg-2)', position: 'relative' }}>
              <div style={{ position: 'absolute', inset: 0, width: `${pct}%`, background: meta.color }} />
            </div>
            <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 5, display: 'flex', justifyContent: 'space-between', letterSpacing: '.04em' }}>
              <span>{r.ccy !== 'USD' ? `${fmt.cur(r.native, r.ccy)} native · 1 ${r.ccy} = $${FX[r.ccy].toFixed(4)}` : 'no conversion'}</span>
              <span>{Math.round(pct)}% of gross</span>
            </div>
          </div>);

        })}
      {rows.length > 1 &&
        <div style={{ marginTop: 14, padding: '10px 14px', background: 'var(--bg-2)', borderLeft: '3px solid var(--ink-3)' }}>
          <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 4 }}>FX RISK</div>
          <div style={{ fontSize: 11, lineHeight: 1.5, color: 'var(--ink-2)' }}>
            {Math.round(rows.filter((r) => r.ccy !== 'USD').reduce((a, r) => a + r.usd, 0) / totalUsd * 100)}% of gross is non-USD. Consider FX hedging on EUR/GBP/JPY positions before payout.
          </div>
        </div>
        }
    </div>);

  }

  // ─────────────────────────── main screen
  function ScreenRoyalties({ go }) {
    const allStmts = useMemo(buildStatements, []);
    const [period, setPeriod] = useState('2025Q4');
    const [kindFilter, setKindFilter] = useState('all'); // all | DSP | PRO | MRO | NRO
    const [statusFilter, setStatusFilter] = useState('all');
    const [sourceFilter, setSourceFilter] = useState('all');
    const [search, setSearch] = useState('');
    const [sort, setSort] = useState({ k: 'gross', dir: 'desc' });
    // Statement-inbox drawer — opens from the "STATEMENTS" pill in the hero.
    // The page's primary view is now analytical ("what did I earn?"); ingest /
    // reconcile detail lives in this slide-out drawer ("did the money arrive?").
    const [drawerOpen, setDrawerOpen] = useState(false);
    // Re-render when the statement drawer mutates a decision (e.g. mark reconciled)
    const [, forceTick] = useState(0);
    React.useEffect(() => {
      const onChange = () => forceTick((x) => x + 1);
      window.addEventListener('astro-stmt-decisions-changed', onChange);
      return () => window.removeEventListener('astro-stmt-decisions-changed', onChange);
    }, []);

    const stmts = useMemo(() => allStmts.filter((s) => s.period.id === period), [allStmts, period]);

    const filtered = useMemo(() => {
      let r = stmts;
      if (kindFilter !== 'all') r = r.filter((s) => s.source.kind === kindFilter);
      if (statusFilter !== 'all') r = r.filter((s) => effStatus(s) === statusFilter);
      if (sourceFilter !== 'all') r = r.filter((s) => s.source.id === sourceFilter);
      if (search.trim()) {
        const q = search.toLowerCase();
        r = r.filter((s) => s.source.name.toLowerCase().includes(q));
      }
      r = [...r].sort((a, b) => {
        const dir = sort.dir === 'asc' ? 1 : -1;
        if (sort.k === 'gross') return (a.grossUsd - b.grossUsd) * dir;
        if (sort.k === 'lines') return (a.lines - b.lines) * dir;
        if (sort.k === 'source') return a.source.name.localeCompare(b.source.name) * dir;
        if (sort.k === 'status') return a.status.localeCompare(b.status) * dir;
        return 0;
      });
      return r;
    }, [stmts, kindFilter, statusFilter, sourceFilter, search, sort]);

    // Hero metrics
    const totals = useMemo(() => {
      const grossUsd = stmts.reduce((a, s) => a + s.grossUsd, 0);
      const linesTotal = stmts.reduce((a, s) => a + s.lines, 0);
      const reconciled = stmts.filter((s) => ['reconciled', 'paid'].includes(effStatus(s)));
      const reconciledUsd = reconciled.reduce((a, s) => a + s.grossUsd, 0);
      const awaiting = stmts.filter((s) => ['parsed', 'matched', 'received', 'parsing'].includes(effStatus(s)));
      const flags = stmts.filter((s) => ['failed', 'late'].includes(effStatus(s))).length +
      stmts.reduce((a, s) => a + (s.anomalies > 4 ? 1 : 0), 0);
      return { grossUsd, linesTotal, reconciled: reconciled.length, reconciledUsd, awaiting: awaiting.length, flags };
    }, [stmts]);

    // Non-pipeline metrics for the stat strip — things the funnel can't tell you.
    const insights = useMemo(() => {
      // Variance vs prior period
      const periodIdx = PERIODS.findIndex((p) => p.id === period);
      const prior = periodIdx >= 0 && periodIdx < PERIODS.length - 1 ? PERIODS[periodIdx + 1] : null;
      const priorTotal = prior ? allStmts.filter((s) => s.period.id === prior.id).reduce((a, s) => a + s.grossUsd, 0) : 0;
      const grossUsd = stmts.reduce((a, s) => a + s.grossUsd, 0);
      const variancePct = priorTotal ? Math.round((grossUsd - priorTotal) / priorTotal * 100) : null;

      // FX exposure: % of gross arriving in non-USD
      const nonUsdUsd = stmts.filter((s) => s.currency !== 'USD').reduce((a, s) => a + s.grossUsd, 0);
      const fxPct = grossUsd ? Math.round(nonUsdUsd / grossUsd * 100) : 0;

      // Oldest stuck statement (parsed/matched but not reconciled)
      const stuck = stmts.filter((s) => ['parsed', 'matched'].includes(effStatus(s)) && s.processedAt);
      const oldest = stuck.sort((a, b) => a.processedAt - b.processedAt)[0];
      const oldestDays = oldest ? Math.round((new Date('2026-04-30') - oldest.processedAt) / 86400000) : null;

      // Top source by gross
      const bySource = {};
      stmts.forEach((s) => {bySource[s.source.id] = (bySource[s.source.id] || 0) + s.grossUsd;});
      const topId = Object.keys(bySource).sort((a, b) => bySource[b] - bySource[a])[0];
      const topSrc = topId ? getAllSources().find((x) => x.id === topId) : null;
      const topShare = topSrc && grossUsd ? Math.round(bySource[topSrc.id] / grossUsd * 100) : 0;

      // Avg days from period close to processed (for arrived statements)
      const arrived = stmts.filter((s) => s.processedAt);
      const avgDays = arrived.length ? Math.round(arrived.reduce((a, s) => {
        return a + Math.round((s.processedAt - new Date(s.period.end)) / 86400000);
      }, 0) / arrived.length) : null;

      return { variancePct, priorLabel: prior ? prior.label : null, fxPct, oldestDays, oldestSrc: oldest ? oldest.source.name : null, topSrc, topShare, avgDays };
    }, [stmts, allStmts, period]);

    const reconcilePct = totals.grossUsd ? Math.round(totals.reconciledUsd / totals.grossUsd * 100) : 0;

    const toggleSort = (k) => setSort((s) => s.k === k ? { k, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { k, dir: 'desc' });

    return (
      <div>
      {/* ─────── Hero */}
      <div style={{ marginBottom: 28 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 10, flexWrap: 'wrap', gap: 18 }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 6, letterSpacing: '.1em' }}>
              EARNINGS · {PERIODS.find((p) => p.id === period).label.toUpperCase()}
            </div>
            {stmts.length === 0 ?
              <>
                <h1 className="heading-swap ff-display" style={{ fontSize: 'clamp(64px,9.5vw,150px)', fontWeight: 700, letterSpacing: '-0.05em', lineHeight: .85, margin: 0, color: 'var(--ink-3)' }}>
                  Awaiting
                </h1>
                <div className="ff-mono" style={{ fontSize: 13, color: 'var(--ink-3)', marginTop: 14, maxWidth: 640, lineHeight: 1.5 }}>
                  No statements have arrived for {PERIODS.find((p) => p.id === period).label} yet. DSPs typically deliver 30–60 days after period close
                  ({PERIODS.find((p) => p.id === period).end}); PROs run 90–120 days behind. Switch to{' '}
                  <a style={{ borderBottom: '1px solid var(--ink)', cursor: 'pointer' }} onClick={() => setPeriod('2025Q4')}>2025 Q4</a>{' '}
                  to see closed statements, or <a style={{ borderBottom: '1px solid var(--ink)', cursor: 'pointer' }} onClick={() => go('analytics')}>view earnings →</a>
                </div>
              </> :

              <>
                <h1 className="heading-swap ff-display" style={{ fontWeight: 700, letterSpacing: '-0.05em', lineHeight: .85, margin: 0, padding: "10px 0px", fontSize: "96px", height: "81.176458px", width: "1230.38598px" }}>
                  {fmt.cur(totals.grossUsd)}
                </h1>
                <div className="ff-mono" style={{ fontSize: 13, color: 'var(--ink-3)', marginTop: 14 }}>
                  GROSS RECEIVED · USD CONSOLIDATED · {totals.linesTotal.toLocaleString()} LINES PARSED · {reconcilePct}% RECONCILED ·{' '}
                  <a style={{ borderBottom: '1px solid var(--ink)', cursor: 'pointer' }} onClick={() => go('analytics')}>view earnings →</a>
                </div>
              </>
              }
          </div>
          {/* Hero actions: Import (primary) + Statements drawer (secondary) + period switcher */}
          <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', flexWrap: 'wrap' }}>
            <button
                onClick={() => window.dispatchEvent(new CustomEvent('astro-import-statement'))}
                className="ff-mono upper"
                style={{
                  fontSize: 11, fontWeight: 600, letterSpacing: '.1em',
                  padding: '10px 18px',
                  background: 'var(--ink)', color: 'var(--bg)',
                  border: '1px solid var(--ink)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8
                }}>
              <span style={{ fontSize: 14, lineHeight: 1 }}>↑</span> Import statement
            </button>
            <button
                onClick={() => setDrawerOpen(true)}
                className="ff-mono upper"
                title="Open the statement inbox — ingest, parse, reconcile."
                style={{
                  fontSize: 11, fontWeight: 500, letterSpacing: '.1em',
                  padding: '10px 16px',
                  background: 'transparent', color: 'var(--ink)',
                  border: '1px solid var(--rule)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8
                }}>
              Statements <span className="num" style={{ opacity: .55 }}>{stmts.length}</span> ↗
            </button>
            <button
                onClick={() => go && go('reconciliation')}
                className="ff-mono upper"
                title="Open the reconciliation queue — match income lines to your catalog. The matcher learns from your decisions."
                style={{
                  fontSize: 11, fontWeight: 500, letterSpacing: '.1em',
                  padding: '10px 16px',
                  background: 'var(--accent)', color: 'var(--accent-ink)',
                  border: '1px solid var(--accent)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8, fontWeight: 600
                }}>
              Reconcile ↗ <span className="ff-mono" style={{ fontSize: 8, padding:'1px 4px', background:'var(--ink)', color:'var(--bg)', letterSpacing:'.08em' }}>NEW</span>
            </button>
            <button
                onClick={() => go && go('royalty-viz')}
                className="ff-mono upper"
                title="Open the visualizer — 9 cross-filtered charts across time, source, work, territory."
                style={{
                  fontSize: 11, fontWeight: 600, letterSpacing: '.1em',
                  padding: '10px 16px',
                  background: 'var(--ink)', color: 'var(--bg)',
                  border: '1px solid var(--ink)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8
                }}>
              Visualize ↗
            </button>
            <button
                onClick={() => window.dispatchEvent(new CustomEvent('astro-open-royalty-optimizer'))}
                className="ff-mono upper"
                title="Scan statements + catalog for ranked optimization strategies."
                style={{
                  fontSize: 11, fontWeight: 600, letterSpacing: '.1em',
                  padding: '10px 16px',
                  background: 'transparent', color: 'var(--ink)',
                  border: '1px solid var(--ink)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8
                }}>
              Optimize ↗ <span className="ff-mono" style={{ fontSize: 8, padding:'1px 4px', background:'var(--accent)', color:'var(--accent-ink)', letterSpacing:'.08em' }}>NEW</span>
            </button>
            <button
                onClick={() => go('statement-out')}
                className="ff-mono upper"
                title="Generate a counterparty-facing royalty statement PDF."
                style={{
                  fontSize: 11, fontWeight: 600, letterSpacing: '.1em',
                  padding: '10px 14px', border: 0,
                  background: 'var(--ink)', color: 'var(--bg)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', gap: 8
                }}>
              Send Statement <span className="ff-mono" style={{ fontSize: 8, padding:'1px 4px', background:'var(--accent)', color:'var(--accent-ink)', letterSpacing:'.08em' }}>NEW</span>
            </button>
            <div style={{ display: 'flex', gap: 0, border: '1px solid var(--rule)' }}>
              {PERIODS.map((p, i) =>
                <button key={p.id} onClick={() => setPeriod(p.id)} className="ff-mono upper" style={{
                  fontSize: 11, fontWeight: 500, letterSpacing: '.08em',
                  padding: '10px 16px',
                  background: period === p.id ? 'var(--ink)' : 'transparent',
                  color: period === p.id ? 'var(--bg)' : 'var(--ink-2)',
                  borderRight: i < PERIODS.length - 1 ? '1px solid var(--rule)' : 'none',
                  cursor: 'pointer'
                }}>
                  {p.label}
                </button>
                )}
            </div>
          </div>
        </div>

        {/* Stat strip — non-pipeline insights (variance / FX / stuck / top source / avg days).
                                                      Pipeline funnel below covers count-by-stage; these answer questions the funnel can't. */}
        <div style={{ borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', marginTop: 20 }}>
          <Stat
              label="VS PRIOR QUARTER"
              value={insights.variancePct == null ? '—' : insights.variancePct >= 0 ? `+${insights.variancePct}%` : `${insights.variancePct}%`}
              tone={insights.variancePct == null ? 'var(--ink-3)' : insights.variancePct >= 0 ? '#2d6a3f' : '#a04432'}
              sub={insights.priorLabel ? `${insights.priorLabel} baseline` : 'no prior period'} />
          <Stat
              label="FX EXPOSURE"
              value={`${insights.fxPct}%`}
              tone={insights.fxPct > 30 ? '#c79538' : 'var(--ink)'}
              sub="non-USD inflows" />
          <Stat
              label="OLDEST STUCK"
              value={insights.oldestDays == null ? '—' : `${insights.oldestDays}d`}
              tone={insights.oldestDays == null ? 'var(--ink)' : insights.oldestDays > 60 ? '#a04432' : insights.oldestDays > 30 ? '#c79538' : 'var(--ink)'}
              sub={insights.oldestSrc ? `${insights.oldestSrc} · awaiting reconcile` : 'queue is clear'} />
          <Stat
              label="TOP SOURCE"
              value={insights.topSrc ? insights.topSrc.name.split(' ')[0] : '—'}
              sub={insights.topSrc ? `${insights.topShare}% of gross · ${insights.topSrc.kind}` : '—'} />
          <Stat
              label="AVG ARRIVAL"
              value={insights.avgDays == null ? '—' : `${insights.avgDays}d`}
              sub="days after period close" />
        </div>
      </div>

      {/* ─────── Analytics primary: by source / by currency / over time / anomalies.
                                                    Operational pipeline + statement inbox table now live in the StatementsDrawer
                                                    (toggled from the "Statements" pill in the hero). The page's job here is
                                                    "what did I earn"; reconciliation detail is one click away.   */}

      {/* By source — full width (was sidebar). The "Earnings" question's first answer:
                                                    which sources brought what, ranked. */}
      <div style={{ marginTop: 8 }}>
        <SourceLeaders stmts={stmts} onPick={(id) => {setSourceFilter(id === sourceFilter ? 'all' : id);setDrawerOpen(true);}} />
      </div>

      {/* Two-up: by currency + over-time trend. Rounds out the analytical picture. */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40, marginTop: 8 }}>
        <CurrencyExposure stmts={stmts} />
        <EarningsTrend allStmts={allStmts} currentPeriod={period} onPickPeriod={setPeriod} />
      </div>

      {/* Per-DSP / platform drilldown — derived from line-level data. Surfaces Spotify,
                                                    Apple Music, YouTube, etc. as their own rows even though they ride into the
                                                    inbox bundled inside RSD or HFA statements. */}
      <DSPBreakdown stmts={stmts} />

      {/* Anomalies — demoted to the bottom. Real ops work, but lower than analytics
                                                    on the page because the StatementsDrawer is the canonical home for it. */}
      <Anomalies stmts={stmts} />

      <RoyaltyVizPreview stmts={stmts} go={go} />

      {/* ─────── Statements slide-out drawer */}
      <StatementsDrawer
          open={drawerOpen}
          onClose={() => setDrawerOpen(false)}
          stmts={stmts}
          filtered={filtered}
          period={period}
          kindFilter={kindFilter} setKindFilter={setKindFilter}
          statusFilter={statusFilter} setStatusFilter={setStatusFilter}
          search={search} setSearch={setSearch}
          sort={sort} toggleSort={toggleSort} />
        
    </div>);

  }

  // ─────────────────────────── Earnings trend (over time)
  // Sparkline-style bar chart of gross USD across all 4 known periods.
  // Click a bar to swap the active period — this is the only "time travel" affordance
  // on the Earnings page; the period switcher in the hero is the chart's twin.
  function EarningsTrend({ allStmts, currentPeriod, onPickPeriod }) {
    const byPeriod = PERIODS.map((p) => {
      const periodStmts = allStmts.filter((s) => s.period.id === p.id);
      const gross = periodStmts.reduce((a, s) => a + s.grossUsd, 0);
      return { period: p, gross, count: periodStmts.filter((s) => s.grossUsd > 0).length };
    }).reverse(); // oldest → newest left → right
    const max = Math.max(1, ...byPeriod.map((b) => b.gross));
    // Variance vs prior period — same calculation as hero stat strip but shown as a delta line per bar
    return (
      <div style={{ marginTop: 32 }}>
      <Section num="05">Earnings over time</Section>
      <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 14 }}>
        TRAILING {byPeriod.length} QUARTERS · USD CONSOLIDATED · click a bar to switch period
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${byPeriod.length},1fr)`, gap: 10, alignItems: 'end', height: 180 }}>
        {byPeriod.map((b, i) => {
            const h = b.gross ? Math.max(4, b.gross / max * 160) : 2;
            const isCur = b.period.id === currentPeriod;
            const prior = i > 0 ? byPeriod[i - 1].gross : 0;
            const dPct = prior ? Math.round((b.gross - prior) / prior * 100) : null;
            return (
              <div key={b.period.id} onClick={() => onPickPeriod(b.period.id)}
              style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', alignItems: 'stretch', gap: 6 }}>
              <div className="ff-mono num" style={{ fontSize: 11, fontWeight: 600, letterSpacing: '-0.01em', textAlign: 'center' }}>
                {b.gross ? fmt.cur(b.gross) : '—'}
              </div>
              {dPct != null &&
                <div className="ff-mono num" style={{ fontSize: 9, letterSpacing: '.04em', textAlign: 'center',
                  color: dPct >= 0 ? '#2d6a3f' : '#a04432' }}>
                  {dPct >= 0 ? `+${dPct}%` : `${dPct}%`}
                </div>
                }
              <div style={{ height: h, background: isCur ? 'var(--ink)' : 'var(--ink-3)', transition: 'background .15s' }} />
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', textAlign: 'center',
                  color: isCur ? 'var(--ink)' : 'var(--ink-3)', fontWeight: isCur ? 600 : 400 }}>
                {b.period.label}
              </div>
              <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', textAlign: 'center' }}>{b.count} stmt</div>
            </div>);

          })}
      </div>
    </div>);

  }

  // ─────────────────────────── Statements slide-out drawer
  // Holds the operational view: pipeline funnel + filterable inbox table.
  // Open via the "Statements" pill in the Earnings hero, or by clicking a SourceLeaders row.
  function StatementsDrawer({ open, onClose, stmts, filtered, period, kindFilter, setKindFilter,
    statusFilter, setStatusFilter, search, setSearch, sort, toggleSort }) {
    if (!open) return null;
    return (
      <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.55)', zIndex: 5000, display: 'flex', justifyContent: 'flex-end' }}>
      <div onClick={(e) => e.stopPropagation()} style={{
          background: 'var(--bg)', borderLeft: '1px solid var(--rule)',
          width: 'min(1100px, 92vw)', height: '100%', overflow: 'auto',
          boxShadow: '-30px 0 80px rgba(0,0,0,.35)' }}>
        {/* drawer header */}
        <div style={{ position: 'sticky', top: 0, background: 'var(--bg)', borderBottom: '1px solid var(--rule)',
            padding: '18px 28px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', zIndex: 1 }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 4 }}>
              STATEMENT INBOX · {PERIODS.find((p) => p.id === period).label.toUpperCase()}
            </div>
            <div className="ff-display" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em' }}>
              {stmts.length} statements · {fmt.cur(stmts.reduce((a, s) => a + s.grossUsd, 0))} consolidated
            </div>
          </div>
          <button onClick={onClose} className="ff-mono upper" style={{
              fontSize: 10, letterSpacing: '.1em', padding: '8px 14px',
              background: 'transparent', color: 'var(--ink-2)',
              border: '1px solid var(--rule)', cursor: 'pointer' }}>
            close ✕
          </button>
        </div>

        <div style={{ padding: '24px 28px' }}>
          {/* Pipeline funnel — operational. Lives inside the drawer because it's
                                                        about "where in the ingest pipeline is each statement", not earnings. */}
          <Pipeline stmts={stmts} />

          {/* Filter bar */}
          <div style={{ display: 'flex', gap: 6, marginBottom: 14, marginTop: 24, flexWrap: 'wrap', alignItems: 'center' }}>
            {[
              { k: 'all', l: 'ALL', n: stmts.length },
              { k: 'DSP', l: 'DSPS', n: stmts.filter((s) => s.source.kind === 'DSP').length },
              { k: 'PRO', l: 'SOCIETIES', n: stmts.filter((s) => s.source.kind === 'PRO').length },
              { k: 'MRO', l: 'MECHANICAL', n: stmts.filter((s) => s.source.kind === 'MRO').length },
              { k: 'NRO', l: 'NEIGHBORING', n: stmts.filter((s) => s.source.kind === 'NRO').length }].
              map((f) =>
              <button key={f.k} onClick={() => setKindFilter(f.k)} className="ff-mono upper" style={{
                fontSize: 10, letterSpacing: '.08em', padding: '5px 10px',
                background: kindFilter === f.k ? 'var(--ink)' : 'transparent',
                color: kindFilter === f.k ? 'var(--bg)' : 'var(--ink-2)',
                border: '1px solid var(--rule)', cursor: 'pointer'
              }}>
                {f.l} <span style={{ opacity: .55, marginLeft: 4 }}>{f.n}</span>
              </button>
              )}
            <span style={{ flex: 1 }} />
            <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search source…" className="ff-mono"
              style={{ fontSize: 11, padding: '5px 10px', border: '1px solid var(--rule)', background: 'var(--bg)', color: 'var(--ink)', width: 160, outline: 'none' }} />
            <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="ff-mono upper" style={{
                fontSize: 10, letterSpacing: '.08em', padding: '5px 10px', background: 'var(--bg)', color: 'var(--ink-2)', border: '1px solid var(--rule)', cursor: 'pointer', outline: 'none'
              }}>
              <option value="all">ALL STATUSES</option>
              {Object.keys(STATUS_META).map((s) => <option key={s} value={s}>{STATUS_META[s].label}</option>)}
            </select>
          </div>

          {/* Table */}
          <div style={{ border: '1px solid var(--rule)' }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1.5fr 80px 170px 100px 70px', gap: 0,
                padding: '9px 14px', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)' }}>
              {[
                { k: 'source', l: 'SOURCE', a: 'left' },
                { k: 'lines', l: 'LINES', a: 'right' },
                { k: 'gross', l: 'GROSS', a: 'right' },
                { k: 'status', l: 'STATUS', a: 'left' },
                { k: 'age', l: 'AGE', a: 'right' }].
                map((c) =>
                <span key={c.k} onClick={() => toggleSort(c.k)} className="ff-mono upper" style={{
                  fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', textAlign: c.a, cursor: 'pointer',
                  fontWeight: sort.k === c.k ? 700 : 400
                }}>
                  {c.l}{sort.k === c.k && <span style={{ marginLeft: 4 }}>{sort.dir === 'asc' ? '↑' : '↓'}</span>}
                </span>
                )}
            </div>
            {filtered.length === 0 &&
              <div style={{ padding: '40px 20px', textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>No statements match these filters.</div>
              }
            {filtered.map((s) =>
              <div key={s.id}
              onClick={() => window.dispatchEvent(new CustomEvent('astro-open-statement', { detail: { id: s.id } }))}
              style={{ display: 'grid', gridTemplateColumns: '1.5fr 80px 170px 100px 70px', gap: 0,
                padding: '12px 14px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center', cursor: 'pointer' }}
              onMouseEnter={(e) => {e.currentTarget.style.background = 'var(--bg-2)';}}
              onMouseLeave={(e) => {e.currentTarget.style.background = 'transparent';}}>
                {/* source */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
                  <span style={{ width: 6, height: 24, background: s.source.color, flexShrink: 0 }} />
                  <div style={{ minWidth: 0 }}>
                    <div
                      onClick={(e) => {e.stopPropagation();window.dispatchEvent(new CustomEvent('astro-open-source-history', { detail: { id: s.source.id } }));}}
                      title="View source history"
                      style={{ fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6 }}>
                      {s.source.name}
                      {s.isReal &&
                      <span title={`Real statement on file · ${s.realLines} lines · ${s.realMatched} matched to catalog`} style={{
                        display: 'inline-flex', alignItems: 'center', gap: 3, padding: '1px 6px',
                        fontSize: 8, fontWeight: 600, letterSpacing: '.1em',
                        background: 'var(--ink)', color: 'var(--bg)',
                        fontFamily: 'IBM Plex Mono, monospace'
                      }}>📎 ON FILE</span>
                      }
                    </div>
                    <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginTop: 2, letterSpacing: '.04em' }}>
                      {s.source.kind} · {s.source.freq} · {s.source.currency}
                      {s.isReal && s.realMatched != null &&
                      <span style={{ marginLeft: 8, color: 'var(--ink-3)' }}>
                          · {s.realMatched}/{s.realLines} matched
                        </span>
                      }
                      {s.anomalies > 0 && ['matched', 'parsed'].includes(effStatus(s)) && <span style={{ marginLeft: 8, color: '#c79538' }}>· {s.anomalies} flagged</span>}
                    </div>
                  </div>
                </div>
                {/* lines */}
                <span className="ff-mono num" style={{ fontSize: 12, textAlign: 'right', color: s.lines ? 'var(--ink)' : 'var(--ink-4)' }}>{s.lines ? s.lines.toLocaleString() : '—'}</span>
                {/* gross merged: USD primary + native ccy below if not USD */}
                <div style={{ textAlign: 'right' }}>
                  <div className="ff-mono num" style={{ fontSize: 13, fontWeight: 600, color: s.grossUsd ? 'var(--ink)' : 'var(--ink-4)', letterSpacing: '-0.01em' }}>{s.grossUsd ? fmt.cur(s.grossUsd) : '—'}</div>
                  {s.gross > 0 && s.currency !== 'USD' &&
                  <div className="ff-mono num" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2 }}>{fmt.cur(s.gross, s.currency)}</div>
                  }
                </div>
                {/* status */}
                <div style={{ display: 'flex', justifyContent: 'flex-start' }}><StatusChip s={effStatus(s)} /></div>
                {/* age */}
                <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right', color: effStatus(s) === 'late' ? '#a04432' : 'var(--ink-3)' }}>
                  {s.processedAt ? fmt.ago(Math.round((new Date('2026-04-30') - s.processedAt) / 86400000)) : effStatus(s) === 'late' ? `+${Math.abs(s.ageDays)}d` : fmt.ago(s.ageDays)}
                </span>
              </div>
              )}
            <div style={{ padding: '14px 14px', background: 'var(--bg-2)', borderTop: '2px solid var(--ink)', display: 'grid', gridTemplateColumns: '1.5fr 80px 170px 100px 70px', gap: 0, alignItems: 'center' }}>
              <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink)', fontWeight: 600 }}>TOTAL · {filtered.length} OF {stmts.length}</span>
              <span className="ff-mono num" style={{ fontSize: 13, textAlign: 'right', fontWeight: 700 }}>{filtered.reduce((a, s) => a + s.lines, 0).toLocaleString()}</span>
              <span className="ff-mono num" style={{ fontSize: 14, textAlign: 'right', fontWeight: 700, letterSpacing: '-0.01em' }}>{fmt.cur(filtered.reduce((a, s) => a + s.grossUsd, 0))}</span>
              <span />
              <span />
            </div>
          </div>
        </div>
      </div>
    </div>);

  }

  // ─────────────────────────── Import Statement modal
  // 3-step: file pick → source/period detection → mapping confirm → toast.
  // Listens to `astro-import-statement`; mounted globally below.
  function ImportStatementModal() {
    const [open, setOpen] = useState(false);
    const [step, setStep] = useState(1);
    const [file, setFile] = useState(null);
    const [detected, setDetected] = useState(null); // { source, period, confidence }
    const [override, setOverride] = useState({ sourceId: '', periodId: '' });

    React.useEffect(() => {
      const onOpen = () => {setOpen(true);setStep(1);setFile(null);setDetected(null);setOverride({ sourceId: '', periodId: '' });};
      window.addEventListener('astro-import-statement', onOpen);
      return () => window.removeEventListener('astro-import-statement', onOpen);
    }, []);

    if (!open) return null;

    const close = () => setOpen(false);

    const handleFile = (f) => {
      setFile(f);
      const sources = getAllSources();
      // Mock detection: scan name for source acronym + period.
      const lname = (f.name || '').toLowerCase();
      let src = sources.find((s) => lname.includes(s.name.toLowerCase().split(' ')[0]));
      if (!src) src = sources.find((s) => lname.includes(s.id.replace('src_', '')));
      if (!src) src = sources[0];
      let per = PERIODS.find((p) => lname.includes(p.id.toLowerCase()) || lname.includes(p.label.toLowerCase().replace(' ', '')));
      if (!per) per = PERIODS[1]; // default 2025 Q4
      const confidence = src && per ? 'high' : src || per ? 'medium' : 'low';
      setDetected({ source: src, period: per, confidence });
      setOverride({ sourceId: src.id, periodId: per.id });
      setTimeout(() => setStep(2), 350);
    };

    const submit = () => {
      const src = getAllSources().find((s) => s.id === override.sourceId);
      const per = PERIODS.find((p) => p.id === override.periodId);
      setStep(3);
      setTimeout(() => {
        window.dispatchEvent(new CustomEvent('astro-toast', { detail: {
            msg: `Statement queued · ${src.name} · ${per.label} · parsing…`,
            kind: 'success'
          } }));
        close();
      }, 900);
    };

    return (
      <div onClick={close} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', zIndex: 9000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div onClick={(e) => e.stopPropagation()} style={{ background: 'var(--bg)', border: '1px solid var(--rule)', width: '100%', maxWidth: 560, maxHeight: '85vh', overflow: 'auto' }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em' }}>STEP {step} OF 3</div>
            <div className="ff-display" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4 }}>
              {step === 1 ? 'Import royalty statement' : step === 2 ? 'Confirm source & period' : 'Queuing…'}
            </div>
          </div>
          <button onClick={close} className="ff-mono" style={{ fontSize: 18, background: 'none', border: 0, cursor: 'pointer', color: 'var(--ink-3)', padding: 4 }}>×</button>
        </div>

        {step === 1 &&
          <div style={{ padding: '24px 20px' }}>
            <label
              onDragOver={(e) => {e.preventDefault();e.currentTarget.style.background = 'var(--bg-2)';}}
              onDragLeave={(e) => {e.currentTarget.style.background = 'transparent';}}
              onDrop={(e) => {
                e.preventDefault();
                e.currentTarget.style.background = 'transparent';
                const f = e.dataTransfer.files[0];
                if (f) handleFile(f);
              }}
              style={{ display: 'block', border: '2px dashed var(--rule)', padding: '40px 20px', textAlign: 'center', cursor: 'pointer' }}>
              <div className="ff-display" style={{ fontSize: 32, fontWeight: 600, letterSpacing: '-0.03em', color: 'var(--ink-3)' }}>↑</div>
              <div style={{ fontSize: 14, fontWeight: 500, marginTop: 10 }}>Drop a statement file or click to browse</div>
              <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6, letterSpacing: '.04em' }}>CSV · TSV · XLSX · PDF · ZIP</div>
              <input type="file" accept=".csv,.tsv,.xlsx,.pdf,.zip" style={{ display: 'none' }}
              onChange={(e) => e.target.files[0] && handleFile(e.target.files[0])} />
            </label>
            <div style={{ marginTop: 18, padding: '12px 14px', background: 'var(--bg-2)', borderLeft: '3px solid var(--ink-3)' }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 6 }}>SUPPORTED SOURCES</div>
              <div style={{ fontSize: 12, lineHeight: 1.6, color: 'var(--ink-2)' }}>
                ASCAP, BMI, SESAC, PRS, GEMA, SACEM, JASRAC · MLC, HFA · SoundExchange · Spotify, Apple Music, Amazon, YouTube, TikTok, Tidal · Symphonic, Kobalt, Peermusic
              </div>
            </div>
          </div>
          }

        {step === 2 && file && detected &&
          <div style={{ padding: '24px 20px' }}>
            <div style={{ display: 'flex', gap: 12, padding: '12px 14px', border: '1px solid var(--rule)', marginBottom: 18, alignItems: 'center' }}>
              <span style={{ fontSize: 18 }}>📄</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{file.name}</div>
                <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2 }}>{(file.size / 1024).toFixed(1)} KB · detected confidence: {detected.confidence}</div>
              </div>
            </div>

            <div style={{ display: 'grid', gridTemplateColumns: '90px 1fr', gap: 14, alignItems: 'center', marginBottom: 14 }}>
              <label className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink-3)' }}>SOURCE</label>
              <select value={override.sourceId} onChange={(e) => setOverride((o) => ({ ...o, sourceId: e.target.value }))}
              style={{ padding: '8px 10px', border: '1px solid var(--rule)', background: 'var(--bg)', color: 'var(--ink)', fontSize: 13, outline: 'none' }}>
                {getAllSources().map((s) => <option key={s.id} value={s.id}>{s.name} · {s.kind}</option>)}
              </select>
              <label className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink-3)' }}>PERIOD</label>
              <select value={override.periodId} onChange={(e) => setOverride((o) => ({ ...o, periodId: e.target.value }))}
              style={{ padding: '8px 10px', border: '1px solid var(--rule)', background: 'var(--bg)', color: 'var(--ink)', fontSize: 13, outline: 'none' }}>
                {PERIODS.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </div>

            <div style={{ padding: '12px 14px', background: 'var(--bg-2)', borderLeft: '3px solid var(--ink)' }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 6 }}>NEXT</div>
              <div style={{ fontSize: 12, lineHeight: 1.5, color: 'var(--ink-2)' }}>
                Parser identifies columns, extracts {detected.source.kind === 'DSP' ? '~9,000' : '~400'} lines, and matches each to a Work or Recording. Anomalies queue for review.
              </div>
            </div>
          </div>
          }

        {step === 3 &&
          <div style={{ padding: '40px 20px', textAlign: 'center' }}>
            <div className="ff-display" style={{ fontSize: 48, fontWeight: 600, letterSpacing: '-0.04em', color: 'var(--ink)' }}>···</div>
            <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.1em', color: 'var(--ink-3)', marginTop: 14 }}>QUEUING FOR PARSE</div>
          </div>
          }

        <div style={{ padding: '14px 20px', borderTop: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', gap: 10 }}>
          <button onClick={close} className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.08em', padding: '8px 14px', background: 'none', border: '1px solid var(--rule)', color: 'var(--ink-2)', cursor: 'pointer' }}>Cancel</button>
          {step === 2 &&
            <button onClick={submit} className="ff-mono upper" style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.08em', padding: '8px 16px', background: 'var(--ink)', color: 'var(--bg)', border: '1px solid var(--ink)', cursor: 'pointer' }}>
              Queue for parse →
            </button>
            }
        </div>
      </div>
    </div>);

  }

  // ─────────────────────────── Source history mini-drawer
  // Shows a single source across all 4 periods — answers "how is ASCAP trending?"
  function SourceHistoryDrawer() {
    const [srcId, setSrcId] = useState(null);
    React.useEffect(() => {
      const onOpen = (e) => setSrcId(e.detail.id);
      window.addEventListener('astro-open-source-history', onOpen);
      return () => window.removeEventListener('astro-open-source-history', onOpen);
    }, []);
    if (!srcId) return null;
    const src = getAllSources().find((s) => s.id === srcId);
    if (!src) return null;
    const all = buildStatements().filter((s) => s.source.id === srcId).sort((a, b) => PERIODS.findIndex((p) => p.id === a.period.id) - PERIODS.findIndex((p) => p.id === b.period.id));
    const max = Math.max(1, ...all.map((s) => s.grossUsd));
    const close = () => setSrcId(null);
    return (
      <div onClick={close} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.5)', zIndex: 8500, display: 'flex', justifyContent: 'flex-end' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ background: 'var(--bg)', width: '100%', maxWidth: 480, height: '100%', overflow: 'auto', borderLeft: '1px solid var(--rule)' }}>
        <div style={{ padding: '18px 22px', borderBottom: '1px solid var(--rule)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 6 }}>SOURCE HISTORY · {src.kind}</div>
            <div className="ff-display" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em' }}>{src.name}</div>
            <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6, letterSpacing: '.04em' }}>{src.freq} · {src.currency} · cadence {src.cadenceDays}d after close</div>
          </div>
          <button onClick={close} style={{ fontSize: 18, background: 'none', border: 0, cursor: 'pointer', color: 'var(--ink-3)', padding: 4 }}>×</button>
        </div>
        <div style={{ padding: '18px 22px' }}>
          <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 14 }}>QUARTERLY GROSS · USD</div>
          {all.map((s, i) => {
              const pct = s.grossUsd / max * 100;
              const prior = i > 0 ? all[i - 1] : null;
              const delta = prior && prior.grossUsd ? Math.round((s.grossUsd - prior.grossUsd) / prior.grossUsd * 100) : null;
              return (
                <div key={s.id} style={{ padding: '14px 0', borderBottom: '1px solid var(--rule-soft)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
                  <span style={{ fontSize: 13, fontWeight: 600 }}>{s.period.label}</span>
                  <div style={{ display: 'flex', gap: 10, alignItems: 'baseline' }}>
                    {delta != null &&
                      <span className="ff-mono" style={{ fontSize: 11, color: delta >= 0 ? '#2d6a3f' : '#a04432' }}>{delta >= 0 ? '+' : ''}{delta}%</span>
                      }
                    <span className="ff-mono num" style={{ fontSize: 14, fontWeight: 600 }}>{fmt.cur(s.grossUsd)}</span>
                  </div>
                </div>
                <div style={{ height: 6, background: 'var(--bg-2)', position: 'relative' }}>
                  <div style={{ position: 'absolute', inset: 0, width: `${pct}%`, background: src.color }} />
                </div>
                <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 6, display: 'flex', justifyContent: 'space-between', letterSpacing: '.04em' }}>
                  <span>{s.lines ? s.lines.toLocaleString() + ' lines' : 'no statement'} · {s.currency !== 'USD' ? fmt.cur(s.gross, s.currency) : '—'}</span>
                  <StatusChip s={effStatus(s)} />
                </div>
              </div>);

            })}
          {all.length === 0 &&
            <div style={{ padding: '30px 0', textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>No statements on file from this source.</div>
            }
        </div>
      </div>
    </div>);

  }

  // ─────────────────────────── Royalty Viz preview
  // Teaser strip on the dashboard showing a treemap + sparkline grid
  // pulled from the dedicated Visualizer engine. Click → full screen.
  function RoyaltyVizPreview({ stmts, go }) {
    const RVE = window.RVE, RVC = window.RVCharts;
    if (!RVE || !RVC) return null;
    const lines = RVE.fetchLines();
    if (!lines.length) return null;
    const tree = RVE.treemap(lines, 'work', { limit: 20 });
    const ts = RVE.timeSeries(lines, { granularity: 'month' });
    const territories = RVE.choropleth(lines).slice(0, 30);

    return (
      <div style={{ marginTop: 32 }}>
        <Section num="07">Visualize deeper</Section>
        <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 14, display: 'flex', gap: 8, alignItems: 'center' }}>
          <span>{lines.length.toLocaleString()} INCOME LINES · 7 DIMENSIONS · 9 CROSS-FILTERED CHARTS</span>
          <span style={{ flex: 1 }}/>
          <button onClick={() => go && go('royalty-viz')} style={{ padding: '6px 12px', border: '1px solid var(--ink)', background: 'var(--ink)', color: 'var(--bg)', fontSize: 10, cursor: 'pointer', textTransform: 'uppercase', letterSpacing: '.08em' }}>Open visualizer →</button>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
          <section style={{ border: '1px solid var(--rule)', background: 'var(--bg)' }}>
            <header style={{ padding: '12px 16px', borderBottom: '1px solid var(--rule-soft)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
              <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em' }}>TOP WORKS · TREEMAP</span>
              <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>area = $</span>
            </header>
            <div style={{ padding: 14, overflow: 'hidden' }}>
              <RVC.Treemap rows={tree} w={620} h={300} onCellClick={() => go && go('royalty-viz')} />
            </div>
          </section>
          <section style={{ border: '1px solid var(--rule)', background: 'var(--bg)' }}>
            <header style={{ padding: '12px 16px', borderBottom: '1px solid var(--rule-soft)' }}>
              <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em' }}>EARNINGS · MONTHLY</span>
            </header>
            <div style={{ padding: 14, overflow: 'hidden' }}>
              <RVC.TimeSeries points={ts} w={420} h={140} />
            </div>
            <header style={{ padding: '12px 16px', borderTop: '1px solid var(--rule-soft)', borderBottom: '1px solid var(--rule-soft)' }}>
              <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em' }}>TOP TERRITORIES</span>
            </header>
            <div style={{ padding: 14, overflow: 'hidden' }}>
              <RVC.Choropleth rows={territories} w={420} h={170} />
            </div>
          </section>
        </div>
      </div>
    );
  }

  // expose
  window.ScreenRoyalties = ScreenRoyalties;
  window.RoyaltiesImportModal = ImportStatementModal;
  window.RoyaltiesSourceHistory = SourceHistoryDrawer;

  window.__STMT_PERIODS = PERIODS;
  window.__STMT_FX = FX;
  window.__STMT_STATUS_META = STATUS_META;
  window.__STMT_BUILD = buildStatements;
  window.__STMT_FMT = fmt;
})();