/* global React, SOCIETIES, TXN, WORKS, CLAIMS, Ic, Pill, Section, Btn, AsciiBar, Term */
// ───────────────────────────────────────────────────────────────────────────
// workspace-screens.jsx
//
// Three operational dashboards for the rights-admin workspace:
//   1. ScreenCwr (override)  — CWR transmission console (deep edition)
//   2. ScreenBlackBox        — Unallocated / black-box royalties inbox
//   3. ScreenNotifications   — Inbound rights notifications inbox (CISAC, society, DSP, internal)
//
// Designed against:
//   astro.cwr_transmissions / cwr_acknowledgements
//   astro.royalty_statement_lines (matched=false)  +  unallocated_pools
//   astro.notifications + astro.musical_work_notifications + astro.rdr_notifications
//
// All data is deterministic, derived from existing seed arrays (SOCIETIES, TXN, CLAIMS, WORKS)
// where possible, so anything that links back to a real entity stays consistent.
// ───────────────────────────────────────────────────────────────────────────

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

  // tiny deterministic PRNG so synthetic data is stable across renders
  const seed = (s) => {
    let h = 2166136261;
    for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
    return () => { h = Math.imul(h ^ (h >>> 15), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return ((h ^= h >>> 16) >>> 0) / 4294967296; };
  };

  const fmt$ = (n, c = 'USD') => {
    const sym = { USD: '$', EUR: '€', GBP: '£', JPY: '¥' }[c] || '$';
    if (n >= 1e6) return `${sym}${(n / 1e6).toFixed(2)}M`;
    if (n >= 1e3) return `${sym}${(n / 1e3).toFixed(1)}K`;
    return `${sym}${Math.round(n).toLocaleString()}`;
  };
  const fmtNum = (n) => n.toLocaleString();
  const ago = (h) => h < 1 ? 'just now' : h < 24 ? `${Math.round(h)}h ago` : `${Math.round(h / 24)}d ago`;

  // ═══════════════════════════════════════════════════════════════════════
  // 1. CWR TRANSMISSION CONSOLE — deep edition
  // ═══════════════════════════════════════════════════════════════════════
  // Builds 30 days of synthetic outbound batches with full lifecycle:
  //   queued → submitted → received → processing → acknowledged | rejected
  // Each transmission carries CWR record-level stats (NWR, REV, ACK rejects),
  // file size, transport (SFTP / API), and per-society routing.

  const CWR_STATUS_ORDER = ['queued', 'submitted', 'received', 'processing', 'acknowledged', 'rejected'];
  const CWR_STATUS_TONE = {
    queued: { bg: 'var(--bg-2)', fg: 'var(--ink-3)', label: 'QUEUED' },
    submitted: { bg: 'rgba(31,78,216,0.06)', fg: '#1f4ed8', label: 'SUBMITTED' },
    received: { bg: 'rgba(31,78,216,0.06)', fg: '#1f4ed8', label: 'RECEIVED' },
    processing: { bg: 'rgba(217,119,87,0.08)', fg: 'var(--accent,#d97757)', label: 'PROCESSING' },
    acknowledged: { bg: 'rgba(58,106,58,0.08)', fg: 'var(--ok,#3a6a3a)', label: 'ACK' },
    rejected: { bg: 'rgba(196,68,68,0.08)', fg: 'var(--danger,#c44)', label: 'REJECTED' },
    partial: { bg: 'rgba(217,119,87,0.08)', fg: 'var(--accent,#d97757)', label: 'PARTIAL' },
  };

  function buildCwrBatches() {
    const out = [];
    const today = new Date('2026-04-30T16:00:00Z');
    // Use a curated set of CWR-capable societies (PRO + MRO + HUB + select CMOs)
    const cwrSocs = (typeof SOCIETIES !== 'undefined' ? SOCIETIES : [])
      .filter(s => s.kind === 'PRO' || s.kind === 'MRO' || s.kind === 'CMO' || s.kind === 'HUB')
      .slice(0, 14);
    if (cwrSocs.length === 0) return out;
    // 30 days back, ~2-4 batches per day
    for (let d = 0; d < 30; d++) {
      const date = new Date(today.getTime() - d * 86400000);
      const dayKey = date.toISOString().slice(0, 10).replace(/-/g, '');
      const r = seed('cwr·' + dayKey);
      const numBatches = 2 + Math.floor(r() * 3);
      for (let b = 0; b < numBatches; b++) {
        const soc = cwrSocs[Math.floor(r() * cwrSocs.length)];
        const seq = b + 1;
        const id = `CWR-${date.toISOString().slice(2, 10).replace(/-/g, '')}-${String(seq).padStart(3, '0')}`;
        const cwrVer = soc.cwrAck && soc.cwrAck.includes('3.0') && r() < 0.5 ? 'V30' : 'V21';
        const file = `CW${dayKey.slice(2)}${soc.acronym}.${cwrVer}`;
        // Volume: NWR + REV + ISW transactions
        const nwr = 40 + Math.floor(r() * 200); // new works
        const rev = Math.floor(r() * 30);       // revisions
        const transactions = nwr + rev;
        const sizeKb = Math.round(transactions * (4.5 + r() * 2)); // avg ~5kB/tx
        // Decide status based on age + society reliability
        let status, ackPct, rejected, processingMs;
        const ackRel = (soc.ackRate || 95) / 100;
        const isToday = d === 0;
        const isYesterday = d === 1;
        if (isToday && b >= numBatches - 1) {
          status = r() < 0.35 ? 'queued' : 'submitted';
        } else if (isToday || isYesterday) {
          const n = r();
          if (n < 0.20) status = 'processing';
          else if (n < 0.30 && ackRel < 0.95) status = 'rejected';
          else status = 'acknowledged';
        } else {
          status = r() < (1 - ackRel) * 1.4 ? 'rejected' : 'acknowledged';
        }
        // Acks are partial when a percent of transactions are rejected at the line level
        const lineRejects = status === 'rejected' ? Math.floor(transactions * (0.4 + r() * 0.5))
          : (status === 'acknowledged' && r() < 0.35) ? Math.floor(transactions * (r() * 0.04)) : 0;
        const isPartial = status === 'acknowledged' && lineRejects > 0;
        ackPct = transactions === 0 ? 0 : Math.round(((transactions - lineRejects) / transactions) * 1000) / 10;
        rejected = lineRejects;
        processingMs = 600 + Math.floor(r() * 4500);
        // Build a realistic per-batch timeline. Each interval varies — operators
        // use these to spot stuck-in-queue or slow-receiver pathologies.
        const sentMin = Math.floor(r() * 1440);
        const sentTime = new Date(date.getTime() - sentMin * 60000);
        // Queued: 30s–6min before submit (queue depth varies)
        const queuedTime = new Date(sentTime.getTime() - (30_000 + Math.floor(r() * 330_000)));
        // Received: only meaningful once submitted+further. 2s–45s after submit (transport latency).
        const receivedTime = ['submitted', 'received', 'processing', 'acknowledged', 'rejected', 'partial'].includes(status)
          ? new Date(sentTime.getTime() + (2_000 + Math.floor(r() * 43_000))) : null;
        // Processing-start: 0s–8min after received (society inbox dwell varies wildly)
        const processingStartTime = ['processing', 'acknowledged', 'rejected', 'partial'].includes(status) && receivedTime
          ? new Date(receivedTime.getTime() + Math.floor(r() * 480_000)) : null;
        // Ack: meaningful chunk after submit. 5min–4hr depending on society SLA.
        const ackOffsetMin = ['acknowledged', 'rejected', 'partial'].includes(status)
          ? 5 + Math.floor(r() * 240) : null;
        const ackTime = ackOffsetMin ? new Date(sentTime.getTime() + ackOffsetMin * 60000) : null;
        out.push({
          id, file,
          society: soc.acronym, societyName: soc.name, country: soc.country, kind: soc.kind,
          cwrVer: cwrVer === 'V30' ? '3.0' : '2.1',
          transport: soc.kind === 'HUB' ? 'API' : (r() < 0.7 ? 'SFTP' : 'API'),
          status: isPartial ? 'partial' : status,
          transactions, nwr, rev,
          rejected, ackPct,
          sizeKb, processingMs,
          queuedAt: queuedTime, sentAt: sentTime, receivedAt: receivedTime, processingStartedAt: processingStartTime, ackAt: ackTime,
          dayOffset: d,
          // For the rejected batches, fabricate a primary error reason
          errorReason: status === 'rejected' ? [
            'NWR-SR-001 · Submitter party not registered',
            'NWR-IL-014 · IPI not found in society database',
            'AGR-VI-007 · Effective date precedes submitter agreement',
            'NWR-VL-022 · Total share != 100%',
            'NWR-VL-031 · Performer share + writer share > 100%',
            'NWR-IL-005 · Invalid CWR record sequence',
          ][Math.floor(r() * 6)] : null,
        });
      }
    }
    return out.sort((a, b) => b.sentAt - a.sentAt);
  }

  // KPI strip — top-of-page newspaper masthead numbers.
  // IN FLIGHT and FAILED tiles double as filters; ACK RATE / WORKS / BATCHES
  // are reference figures (no useful filter — clicking them is a no-op).
  function CwrKpis({ batches, statusFilter, setStatusFilter }) {
    const today = batches.filter(b => b.dayOffset === 0);
    const last7 = batches.filter(b => b.dayOffset < 7);
    const last30 = batches;
    const ackRate7 = (() => {
      const settled = last7.filter(b => b.status !== 'queued' && b.status !== 'submitted' && b.status !== 'processing');
      if (!settled.length) return null;
      const ok = settled.filter(b => b.status === 'acknowledged' || b.status === 'partial').length;
      return Math.round(ok / settled.length * 1000) / 10;
    })();
    const tx30 = last30.reduce((s, b) => s + b.transactions, 0);
    const rej30 = last30.reduce((s, b) => s + b.rejected, 0);
    const queued = batches.filter(b => b.status === 'queued').length;
    const inFlight = batches.filter(b => ['submitted', 'received', 'processing'].includes(b.status)).length;
    const failed7 = last7.filter(b => b.status === 'rejected').length;
    const submittedCt = batches.filter(b => b.status === 'submitted').length;
    const processingCt = batches.filter(b => b.status === 'processing').length;
    const items = [
      { l: 'IN FLIGHT', v: inFlight, sub: `${queued} queued · ${submittedCt} sent · ${processingCt} processing`, filter: 'in-flight' },
      { l: 'BATCHES · 24H', v: today.length, sub: `${today.reduce((s, b) => s + b.transactions, 0).toLocaleString()} works` },
      { l: 'ACK RATE · 7D', v: ackRate7 == null ? '—' : `${ackRate7}%`, sub: `${last7.length} batches` , tone: ackRate7 != null && ackRate7 < 95 ? 'warn' : 'ok' },
      { l: 'WORKS · 30D', v: fmtNum(tx30), sub: `${fmtNum(rej30)} rejected at line` },
      { l: 'FAILED · 7D', v: failed7, sub: failed7 ? 'needs re-send' : 'clean week', tone: failed7 ? 'danger' : '', filter: 'failed' },
    ];
    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 24 }}>
        {items.map((it, i) => {
          const active = it.filter && statusFilter === it.filter;
          const clickable = !!it.filter;
          return (
            <div key={i}
              role={clickable ? 'button' : undefined}
              tabIndex={clickable ? 0 : undefined}
              onClick={clickable ? () => setStatusFilter(active ? 'all' : it.filter) : undefined}
              onKeyDown={clickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setStatusFilter(active ? 'all' : it.filter); } } : undefined}
              style={{
                padding: '18px 16px',
                borderRight: i < 4 ? '1px solid var(--rule)' : 'none',
                borderBottom: active ? '3px solid var(--ink)' : '3px solid transparent',
                marginBottom: active ? -3 : 0,
                background: active ? 'var(--bg-2)' : 'transparent',
                cursor: clickable ? 'pointer' : 'default',
                outline: 'none',
              }}
              onMouseEnter={clickable ? (e) => { if (!active) e.currentTarget.style.background = 'var(--bg-2)'; } : undefined}
              onMouseLeave={clickable ? (e) => { if (!active) e.currentTarget.style.background = 'transparent'; } : undefined}>
              <div className="ff-mono upper" style={{
                fontSize: 10, fontWeight: 600, letterSpacing: '.08em', marginBottom: 8,
                display: 'flex', alignItems: 'center', gap: 6,
                color: it.tone === 'danger' ? 'var(--danger,#c44)' : it.tone === 'warn' ? 'var(--accent,#d97757)' : it.tone === 'ok' ? 'var(--ok,#3a6a3a)' : 'var(--ink-3)'
              }}>
                <span>{it.l}</span>
                {clickable && <span style={{ fontSize: 8, opacity: .5, fontWeight: 400 }}>{active ? '— CLEAR' : '— FILTER'}</span>}
              </div>
              <div className="ff-display num" style={{ fontSize: 40, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1 }}>{it.v}</div>
              <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6 }}>{it.sub}</div>
            </div>
          );
        })}
      </div>
    );
  }

  // 30-day daily volume sparkline + ack-rate band
  function CwrTimeline({ batches }) {
    const buckets = useMemo(() => {
      const out = Array.from({ length: 30 }, (_, i) => ({ d: i, tx: 0, ok: 0, rej: 0 }));
      batches.forEach(b => {
        const slot = out[b.dayOffset];
        if (!slot) return;
        slot.tx += b.transactions;
        if (b.status === 'acknowledged' || b.status === 'partial') slot.ok += b.transactions - b.rejected;
        if (b.status === 'rejected') slot.rej += b.transactions;
      });
      return out.reverse(); // oldest left → newest right
    }, [batches]);
    const maxTx = Math.max(...buckets.map(b => b.tx), 1);
    return (
      <div style={{ border: '1px solid var(--rule)', padding: '20px 24px', marginBottom: 24 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.08em' }}>30-DAY VOLUME · WORKS PER DAY</div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>
            <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', marginRight: 14 }}>
              <span style={{ width: 9, height: 9, background: 'var(--ink)' }} /> ACK
            </span>
            <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}>
              <span style={{ width: 9, height: 9, background: 'var(--danger,#c44)' }} /> REJ
            </span>
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 80 }}>
          {buckets.map((b, i) => {
            const okH = (b.ok / maxTx) * 78;
            const rejH = (b.rej / maxTx) * 78;
            return (
              <div key={i} title={`day -${29 - i}: ${b.tx} works`}
                style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', height: '100%', minWidth: 0 }}>
                {b.rej > 0 && <div style={{ height: rejH || 1, background: 'var(--danger,#c44)' }} />}
                <div style={{ height: okH || (b.tx ? 1 : 0), background: 'var(--ink)' }} />
              </div>
            );
          })}
        </div>
        <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-4)', display: 'flex', justifyContent: 'space-between', marginTop: 6 }}>
          <span>30d ago</span><span>today</span>
        </div>
      </div>
    );
  }

  function CwrBatchRow({ b, selected, onPick, picked, onToggle }) {
    const tone = CWR_STATUS_TONE[b.status] || CWR_STATUS_TONE.queued;
    // Severity stripe — only emphasize unhealthy rows; clean rows stay flat.
    const stripeColor = b.status === 'rejected' ? 'var(--danger,#c44)'
      : b.status === 'partial' ? 'var(--accent,#d97757)'
      : b.status === 'queued' || b.status === 'submitted' || b.status === 'processing' ? 'var(--ink-4,#bbb)'
      : 'transparent';
    const showCheck = typeof onToggle === 'function';
    return (
      <div onClick={() => onPick(b)}
        title={`Batch ${b.id.slice(4)} · v${b.cwrVer} · ${b.transport}`}
        style={{
          display: 'grid',
          gridTemplateColumns: showCheck ? '28px 80px 1fr 70px 80px 80px 100px' : '80px 1fr 70px 80px 80px 100px',
          gap: 14, padding: '14px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center', cursor: 'pointer',
          background: picked ? 'var(--bg-2)' : (selected ? 'var(--bg-2)' : 'transparent'),
          borderLeft: `3px solid ${stripeColor}`,
        }}
        onMouseEnter={e => { if (!selected && !picked) e.currentTarget.style.background = 'var(--bg-2)'; }}
        onMouseLeave={e => { if (!selected && !picked) e.currentTarget.style.background = 'transparent'; }}>
        {showCheck && (
          <input type="checkbox" checked={!!picked}
            onClick={(e) => e.stopPropagation()}
            onChange={(e) => onToggle(b.id, e.target.checked)}
            style={{ accentColor: 'var(--ink)', cursor: 'pointer' }} />
        )}
        <span className="ff-display" style={{ fontSize: 16, fontWeight: 600, letterSpacing: '-0.02em' }}>{b.society}</span>
        <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{b.file}</span>
        <span className="ff-mono num" style={{ fontSize: 12, textAlign: 'right' }}>{b.transactions}</span>
        <span className="ff-mono num" style={{ fontSize: 11, color: b.rejected ? 'var(--danger,#c44)' : 'var(--ink-3)', textAlign: 'right' }}>
          {b.rejected ? `${b.rejected} rej` : '—'}
        </span>
        <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>
          {b.dayOffset === 0 ? b.sentAt.toISOString().slice(11, 16) : `−${b.dayOffset}d`}
        </span>
        <span className="ff-mono upper" style={{
          fontSize: 9, fontWeight: 600, letterSpacing: '.08em', padding: '4px 8px',
          background: tone.bg, color: tone.fg, justifySelf: 'end',
        }}>{tone.label}</span>
      </div>
    );
  }

  function CwrBatchDrawer({ b, onClose }) {
    if (!b) return null;
    const tone = CWR_STATUS_TONE[b.status];
    const lifecycle = [
      { k: 'queued', label: 'QUEUED', t: b.queuedAt },
      { k: 'submitted', label: 'SUBMITTED', t: b.sentAt },
      { k: 'received', label: 'RECEIVED', t: b.receivedAt },
      { k: 'processing', label: 'PROCESSING', t: b.processingStartedAt },
      { k: 'ack', label: b.status === 'rejected' ? 'REJECTED' : 'ACKNOWLEDGED', t: b.ackAt },
    ];
    const reachedIdx = b.status === 'queued' ? 0
      : b.status === 'submitted' ? 1
      : b.status === 'received' ? 2
      : b.status === 'processing' ? 3
      : 4;

    return (
      <>
        <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.18)', zIndex: 50 }} />
        <div style={{
          position: 'fixed', top: 0, right: 0, bottom: 0, width: 'min(640px, 96vw)',
          background: 'var(--bg)', borderLeft: '1px solid var(--rule)', padding: '24px 28px',
          overflow: 'auto', zIndex: 51, boxShadow: '-12px 0 32px rgba(0,0,0,0.08)',
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
            <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)' }}>TRANSMISSION · {b.id}</div>
            <button onClick={onClose}><Ic.X width={16} height={16} /></button>
          </div>
          <div className="heading-swap ff-display" style={{ fontSize: 32, fontWeight: 600, letterSpacing: '-0.03em', lineHeight: 1, marginBottom: 4 }}>
            {b.society}
          </div>
          <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 18 }}>{b.societyName} · {b.country}</div>

          {/* Status strip */}
          <div style={{
            padding: '12px 14px', background: tone.bg, borderLeft: `3px solid ${tone.fg}`, marginBottom: 18,
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          }}>
            <div>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 4 }}>STATUS</div>
              <div className="ff-display" style={{ fontSize: 20, fontWeight: 600, letterSpacing: '-0.02em', color: tone.fg }}>{tone.label}</div>
            </div>
            {b.errorReason ? (
              <div style={{ textAlign: 'right', maxWidth: 280 }}>
                <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 4 }}>REASON</div>
                <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)' }}>{b.errorReason}</div>
              </div>
            ) : b.status === 'partial' ? (
              <div style={{ textAlign: 'right' }}>
                <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 4 }}>ACK RATE</div>
                <div className="ff-display num" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em' }}>{b.ackPct}%</div>
              </div>
            ) : null}
          </div>

          {/* Lifecycle */}
          <Section num="A">Lifecycle</Section>
          <div style={{ position: 'relative', marginBottom: 20, paddingLeft: 16 }}>
            <div style={{ position: 'absolute', left: 4, top: 6, bottom: 6, width: 1, background: 'var(--rule)' }} />
            {lifecycle.map((step, i) => {
              const reached = i <= reachedIdx;
              const failed = i === 4 && b.status === 'rejected';
              return (
                <div key={i} style={{ position: 'relative', paddingBottom: 10, opacity: reached ? 1 : 0.4 }}>
                  <span style={{
                    position: 'absolute', left: -16, top: 4, width: 9, height: 9,
                    background: failed ? 'var(--danger,#c44)' : reached ? 'var(--ink)' : 'var(--rule)',
                    border: '2px solid var(--bg)', borderRadius: '50%',
                  }} />
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                    <span className="ff-mono upper" style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.06em' }}>{step.label}</span>
                    <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-4)' }}>
                      {step.t ? step.t.toISOString().slice(0, 16).replace('T', ' ') : '—'}
                    </span>
                  </div>
                </div>
              );
            })}
          </div>

          {/* Counts */}
          <Section num="B">Transaction breakdown</Section>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', border: '1px solid var(--rule)', marginBottom: 18 }}>
            {[
              { l: 'TOTAL', v: b.transactions },
              { l: 'NWR · NEW', v: b.nwr },
              { l: 'REV', v: b.rev },
              { l: 'REJECTED', v: b.rejected, danger: !!b.rejected },
            ].map((c, i) => (
              <div key={i} style={{ padding: '14px', borderRight: i < 3 ? '1px solid var(--rule)' : 'none' }}>
                <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 4 }}>{c.l}</div>
                <div className="ff-display num" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em', color: c.danger ? 'var(--danger,#c44)' : 'var(--ink)' }}>{c.v}</div>
              </div>
            ))}
          </div>

          {/* File metadata */}
          <Section num="C">File · routing</Section>
          <div style={{ border: '1px solid var(--rule)', marginBottom: 18 }}>
            {[
              ['File', b.file], ['CWR version', b.cwrVer], ['Transport', b.transport],
              ['Size', `${b.sizeKb.toLocaleString()} kB`], ['Processing', `${(b.processingMs / 1000).toFixed(1)} s`],
              ['Submitter', 'PA00578913241 · Pluralis Music'],
              ['Receiver', `${b.society} (${b.country})`],
            ].map(([k, v], i, arr) => (
              <div key={k} style={{
                display: 'grid', gridTemplateColumns: '120px 1fr', padding: '10px 14px',
                borderBottom: i < arr.length - 1 ? '1px solid var(--rule-soft)' : 'none', gap: 14,
              }}>
                <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)' }}>{k}</span>
                <span className="ff-mono" style={{ fontSize: 12, color: 'var(--ink-2)' }}>{v}</span>
              </div>
            ))}
          </div>

          {/* Sample affected works (synthetic) */}
          {(b.status === 'rejected' || b.status === 'partial') && (
            <>
              <Section num="D">Affected transactions</Section>
              <div style={{ border: '1px solid var(--rule)', marginBottom: 18 }}>
                {(typeof WORKS !== 'undefined' ? WORKS : []).slice(0, Math.min(5, b.rejected || 3)).map((w, i, arr) => (
                  <div key={w.id} style={{
                    display: 'grid', gridTemplateColumns: '40px 1fr 110px', gap: 12, padding: '10px 14px',
                    borderBottom: i < arr.length - 1 ? '1px solid var(--rule-soft)' : 'none', alignItems: 'center',
                  }}>
                    <span className="ff-mono num" style={{ fontSize: 11, color: 'var(--ink-4)' }}>{String(i + 1).padStart(3, '0')}</span>
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.title}</div>
                      <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{w.iswc}</div>
                    </div>
                    <span className="ff-mono upper" style={{ fontSize: 9, padding: '3px 8px', background: 'rgba(196,68,68,0.08)', color: 'var(--danger,#c44)', textAlign: 'center', letterSpacing: '.06em' }}>
                      NWR-VL-022
                    </span>
                  </div>
                ))}
              </div>
            </>
          )}

          {/* Actions */}
          <div style={{ borderTop: '1px solid var(--rule)', paddingTop: 14, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {b.status === 'rejected' && (
              <Btn variant="primary" size="sm" icon={<Ic.Send />} onClick={() => window.toast && window.toast(`${b.id} re-queued for re-send`, 'ok')}>Re-send batch</Btn>
            )}
            {b.status === 'queued' && (
              <Btn variant="primary" size="sm" icon={<Ic.Send />} onClick={() => window.toast && window.toast(`${b.id} submitted to ${b.society}`, 'ok')}>Submit now</Btn>
            )}
            <Btn variant="secondary" size="sm" icon={<Ic.Down />} onClick={() => window.toast && window.toast(`${b.file} downloaded`, 'soft')}>Download CWR</Btn>
            <Btn variant="ghost" size="sm" onClick={() => window.toast && window.toast(`Ack file from ${b.society} downloaded`, 'soft')}>Download ACK</Btn>
            <Btn variant="ghost" size="sm" onClick={() => window.toast && window.toast(`Audit log opened for ${b.id}`, 'soft')}>View audit log</Btn>
          </div>
        </div>
      </>
    );
  }

  // Society routing/health table — replaces the bare "society health" block
  function CwrSocietyMatrix({ batches }) {
    const stats = useMemo(() => {
      const byS = {};
      batches.forEach(b => {
        const k = b.society;
        if (!byS[k]) byS[k] = { society: k, name: b.societyName, country: b.country, kind: b.kind, batches: 0, tx: 0, ok: 0, rej: 0, lastSent: null };
        const s = byS[k];
        s.batches++;
        s.tx += b.transactions;
        if (b.status === 'acknowledged' || b.status === 'partial') s.ok += b.transactions - b.rejected;
        if (b.status === 'rejected') s.rej += b.transactions;
        if (!s.lastSent || b.sentAt > s.lastSent) s.lastSent = b.sentAt;
      });
      return Object.values(byS).sort((a, b) => b.tx - a.tx);
    }, [batches]);
    const today = new Date('2026-04-30T16:00:00Z');
    return (
      <div style={{ borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)' }}>
        <div className="ff-mono upper" style={{
          display: 'grid', gridTemplateColumns: '90px 1fr 70px 80px 80px 90px 110px',
          gap: 14, padding: '10px 14px', fontSize: 10, color: 'var(--ink-3)', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)',
        }}>
          <span>SOCIETY</span><span>TERRITORY</span><span style={{ textAlign: 'right' }}>BATCHES</span>
          <span style={{ textAlign: 'right' }}>WORKS</span><span style={{ textAlign: 'right' }}>REJ</span>
          <span>HEALTH · 30D</span><span style={{ textAlign: 'right' }}>LAST SENT</span>
        </div>
        {stats.map(s => {
          const ackPct = s.tx ? Math.round((s.ok / s.tx) * 1000) / 10 : 0;
          const hours = Math.round((today - s.lastSent) / 3600000);
          return (
            <div key={s.society} style={{
              display: 'grid', gridTemplateColumns: '90px 1fr 70px 80px 80px 90px 110px',
              gap: 14, padding: '12px 14px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center',
            }}>
              <span className="ff-display" style={{ fontSize: 16, fontWeight: 600, letterSpacing: '-0.02em' }}>{s.society}</span>
              <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{s.name.length > 38 ? s.name.slice(0, 36) + '…' : s.name}</span>
              <span className="ff-mono num" style={{ fontSize: 12, textAlign: 'right' }}>{s.batches}</span>
              <span className="ff-mono num" style={{ fontSize: 12, textAlign: 'right' }}>{s.tx.toLocaleString()}</span>
              <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right', color: s.rej ? 'var(--danger,#c44)' : 'var(--ink-3)' }}>{s.rej || '—'}</span>
              <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                <AsciiBar value={ackPct} max={100} width={10} />
                <span className="ff-mono num" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{ackPct}%</span>
              </span>
              <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', textAlign: 'right' }}>{ago(hours)}</span>
            </div>
          );
        })}
      </div>
    );
  }

  // Generate-now wizard — small 2-step modal (society + scope) so the CTA
  // produces something meaningful, not just a toast.
  function CwrGenerateModal({ open, onClose, societies }) {
    const [step, setStep] = useState(1);
    const [scope, setScope] = useState('all');
    const [society, setSociety] = useState('all');
    const [skipQueue, setSkipQueue] = useState(false);
    useEffect(() => { if (open) { setStep(1); setScope('all'); setSociety('all'); setSkipQueue(false); } }, [open]);
    if (!open) return null;
    const SCOPES = [
      { k: 'all',     l: 'All pending changes',   sub: 'Every work touched since last batch · ~248 NWR / 31 REV' },
      { k: 'new',     l: 'New works only',         sub: 'Skip revisions and corrections · ~248 NWR' },
      { k: 'pick',    l: 'Pick works manually',    sub: 'Pulls open work picker after submit' },
    ];
    const submit = () => {
      const targetLabel = society === 'all' ? `${societies.length} societies` : society;
      const scopeLabel = SCOPES.find(s => s.k === scope).l.toLowerCase();
      window.toast && window.toast(`Batch generation queued · ${targetLabel} · ${scopeLabel}${skipQueue ? ' · jumping queue' : ''}`, 'ok');
      onClose();
    };
    return (
      <>
        <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.32)', zIndex: 80 }} />
        <div style={{
          position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
          width: 'min(560px, 92vw)', background: 'var(--bg)', border: '1px solid var(--rule)',
          padding: '24px 28px', zIndex: 81, boxShadow: '0 20px 60px rgba(0,0,0,.18)',
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
            <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.08em' }}>
              GENERATE BATCH · STEP {step} OF 2
            </div>
            <button onClick={onClose}><Ic.X width={16} height={16} /></button>
          </div>
          <div className="ff-display" style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-0.03em', marginBottom: 18 }}>
            {step === 1 ? 'What goes in?' : 'Send to which societies?'}
          </div>

          {step === 1 && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 18 }}>
              {SCOPES.map(s => (
                <label key={s.k} style={{
                  display: 'flex', gap: 12, alignItems: 'flex-start', padding: '12px 14px',
                  border: scope === s.k ? '1px solid var(--ink)' : '1px solid var(--rule)', cursor: 'pointer',
                  background: scope === s.k ? 'var(--bg-2)' : 'transparent',
                }}>
                  <input type="radio" name="scope" value={s.k} checked={scope === s.k} onChange={() => setScope(s.k)}
                    style={{ accentColor: 'var(--ink)', marginTop: 3 }} />
                  <div>
                    <div className="ff-display" style={{ fontSize: 15, fontWeight: 600 }}>{s.l}</div>
                    <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{s.sub}</div>
                  </div>
                </label>
              ))}
            </div>
          )}

          {step === 2 && (
            <div style={{ marginBottom: 18 }}>
              <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.08em', marginBottom: 6 }}>SOCIETY</div>
              <select value={society} onChange={e => setSociety(e.target.value)} className="ff-mono"
                style={{ width: '100%', padding: '10px 12px', fontSize: 13, border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)', marginBottom: 14 }}>
                <option value="all">All registered societies ({societies.length})</option>
                {societies.map(s => (
                  <option key={s.acronym} value={s.acronym}>
                    {s.acronym}{s.name ? ` · ${s.name}` : ''}{s.count ? ` (${s.count} sent)` : ''}
                  </option>
                ))}
              </select>
              <label style={{ display: 'flex', gap: 10, alignItems: 'center', cursor: 'pointer', padding: '10px 0' }}>
                <input type="checkbox" checked={skipQueue} onChange={e => setSkipQueue(e.target.checked)}
                  style={{ accentColor: 'var(--ink)' }} />
                <span className="ff-display" style={{ fontSize: 13 }}>Skip the queue · transmit immediately</span>
              </label>
              <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', paddingLeft: 24 }}>
                Otherwise the batch joins the next scheduled window (03:00 UTC).
              </div>
            </div>
          )}

          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', borderTop: '1px solid var(--rule)', paddingTop: 14 }}>
            {step === 2 && <Btn variant="ghost" size="sm" onClick={() => setStep(1)}>← Back</Btn>}
            <span style={{ flex: 1 }} />
            <Btn variant="ghost" size="sm" onClick={onClose}>Cancel</Btn>
            {step === 1 ? (
              <Btn variant="primary" size="sm" onClick={() => setStep(2)}>Next →</Btn>
            ) : (
              <Btn variant="primary" size="sm" icon={<Ic.Send />} onClick={submit}>Generate</Btn>
            )}
          </div>
        </div>
      </>
    );
  }

  function ScreenCwr({ go }) {
    const batches = useMemo(buildCwrBatches, []);
    const [statusFilter, setStatusFilter] = useState('all');
    const [societyFilter, setSocietyFilter] = useState('all');
    const [searchQuery, setSearchQuery] = useState('');
    const [selected, setSelected] = useState(null);
    const [picked, setPicked] = useState(() => new Set()); // bulk-select ids
    const [generateOpen, setGenerateOpen] = useState(false);

    // Live-ticking "next batch" countdown — recompute every second.
    // Anchor to the next 03:00 UTC (typical nightly CWR window).
    const [now, setNow] = useState(() => Date.now());
    useEffect(() => {
      const t = setInterval(() => setNow(Date.now()), 1000);
      return () => clearInterval(t);
    }, []);
    const nextBatchAt = useMemo(() => {
      const d = new Date();
      d.setUTCHours(3, 0, 0, 0);
      if (d.getTime() <= Date.now()) d.setUTCDate(d.getUTCDate() + 1);
      return d.getTime();
    }, []);
    const countdown = (() => {
      const ms = Math.max(0, nextBatchAt - now);
      const h = Math.floor(ms / 3_600_000);
      const m = Math.floor((ms % 3_600_000) / 60_000);
      const s = Math.floor((ms % 60_000) / 1000);
      return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
    })();

    const filtered = batches.filter(b => {
      if (statusFilter !== 'all') {
        if (statusFilter === 'failed' && b.status !== 'rejected') return false;
        else if (statusFilter === 'in-flight' && !['queued','submitted','received','processing'].includes(b.status)) return false;
        else if (statusFilter !== 'failed' && statusFilter !== 'in-flight' && b.status !== statusFilter) return false;
      }
      if (societyFilter !== 'all' && b.society !== societyFilter) return false;
      if (searchQuery.trim()) {
        const q = searchQuery.trim().toLowerCase();
        const hay = `${b.id} ${b.society} ${b.societyName} ${b.file}`.toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });

    // Society dropdown: pulled from REF.societies (the canonical 407-row table),
    // scoped to CWR-eligible types (PRO/MRO/MIX/HUB). Each entry carries a
    // batch-count from the live queue so operators see reach AND activity.
    // Falls back to the union of seeded-batch societies if REF isn't ready yet.
    const societies = useMemo(() => {
      const counts = batches.reduce((m, b) => { m[b.society] = (m[b.society] || 0) + 1; return m; }, {});
      const ref = (window.REF && window.REF.ready && Array.isArray(window.REF.societies)) ? window.REF.societies : null;
      if (!ref) {
        return Array.from(new Set(batches.map(b => b.society))).sort()
          .map(a => ({ acronym: a, name: '', count: counts[a] || 0 }));
      }
      const eligible = ref.filter(s => /PRO|MRO|MIX|HUB/i.test(s.cmo_type || ''));
      // Build acronym → row map; merge in any society present in batches even if not in REF.
      const rows = eligible.map(s => ({ acronym: s.acronym, name: s.name || '', count: counts[s.acronym] || 0 }));
      const seenAcronyms = new Set(rows.map(r => r.acronym));
      Object.keys(counts).forEach(a => { if (!seenAcronyms.has(a)) rows.push({ acronym: a, name: '', count: counts[a] }); });
      // Sort: active first (desc by count), then inactive alpha.
      return rows.sort((a, b) => (b.count - a.count) || a.acronym.localeCompare(b.acronym));
    }, [batches]);

    useEffect(() => {
      if (!selected) return;
      const onKey = (e) => { if (e.key === 'Escape') setSelected(null); };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [selected]);

    return (
      <div>
        <PageHeader
          eyebrow={`DELIVERIES · OUTBOUND · ${batches.filter(b => b.dayOffset === 0).length} BATCHES TODAY`}
          title="Transmissions"
          highlight="Transmissions"
          actions={
            <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
              <div style={{ display: 'flex', gap: 8, alignItems: 'baseline' }}>
                <span className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)' }}>NEXT BATCH IN</span>
                <span className="ff-display num" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{countdown}</span>
              </div>
              <Btn variant="primary" size="sm" icon={<Ic.Send />} onClick={() => setGenerateOpen(true)}>Generate now</Btn>
            </div>
          }
        />

        {/* Cross-links to sibling delivery surfaces (replaces the half-built tab row) */}
        <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.08em', padding: '10px 0', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 24, display: 'flex', gap: 18, alignItems: 'baseline' }}>
          <span style={{ fontWeight: 600, color: 'var(--ink)' }}>CWR · OUTBOUND TO SOCIETIES</span>
          <span style={{ flex: 1 }} />
          <span>RELATED:</span>
          <a onClick={() => go('royalties')} style={{ borderBottom: '1px solid var(--ink-3)', cursor: 'pointer', color: 'var(--ink-2)' }}>Royalty · inbound</a>
          <a onClick={() => window.toast && window.toast('DDEX delivery lives on each release detail page', 'soft')} style={{ borderBottom: '1px dotted var(--ink-4)', cursor: 'pointer', color: 'var(--ink-3)' }}>DDEX · per-release</a>
        </div>

        <CwrKpis batches={batches} statusFilter={statusFilter} setStatusFilter={setStatusFilter} />
        <CwrTimeline batches={batches} />

        {/* Toolbar — search + society select + result count + clear filter */}
        <div style={{ display: 'flex', gap: 12, marginBottom: 14, alignItems: 'center', flexWrap: 'wrap' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 10px', border: '1px solid var(--rule)', background: 'var(--paper)', minWidth: 240, flex: '0 1 320px' }}>
            <Ic.Search width={14} height={14} />
            <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
              placeholder="Search society, file, or batch ID…" className="ff-mono"
              style={{ flex: 1, fontSize: 12, border: 0, outline: 'none', background: 'transparent', color: 'var(--ink)' }} />
            {searchQuery && (
              <button onClick={() => setSearchQuery('')} title="Clear"
                style={{ background: 'none', border: 0, padding: 0, cursor: 'pointer', color: 'var(--ink-3)' }}>
                <Ic.X width={12} height={12} />
              </button>
            )}
          </div>
          <select value={societyFilter} onChange={e => setSocietyFilter(e.target.value)} className="ff-mono"
            style={{ padding: '6px 10px', fontSize: 11, border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)', minWidth: 160 }}>
            <option value="all">All societies ({societies.length})</option>
            {societies.map(s => (
              <option key={s.acronym} value={s.acronym}>
                {s.acronym}{s.name ? ` · ${s.name}` : ''}{s.count ? ` (${s.count})` : ''}
              </option>
            ))}
          </select>
          <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
            {filtered.length} of {batches.length} batches
            {statusFilter !== 'all' && <span> · filter: <span style={{ color: 'var(--ink)', fontWeight: 600 }}>{statusFilter}</span></span>}
          </span>
          <span style={{ flex: 1 }} />
          {(statusFilter !== 'all' || societyFilter !== 'all' || searchQuery) && (
            <button onClick={() => { setStatusFilter('all'); setSocietyFilter('all'); setSearchQuery(''); }} className="ff-mono upper"
              style={{ fontSize: 10, letterSpacing: '.08em', color: 'var(--ink-3)', background: 'none', border: 0, cursor: 'pointer', padding: 0 }}>
              Show all →
            </button>
          )}
        </div>

        {/* Bulk action bar — only renders when something is picked */}
        {picked.size > 0 && (() => {
          const pickedBatches = batches.filter(b => picked.has(b.id));
          const pickedFailed = pickedBatches.filter(b => b.status === 'rejected').length;
          return (
            <div style={{
              display: 'flex', gap: 10, alignItems: 'center', padding: '10px 14px',
              background: 'var(--ink)', color: 'var(--bg)', marginBottom: 14,
              border: '1px solid var(--ink)',
            }}>
              <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.08em' }}>
                {picked.size} SELECTED
                {pickedFailed > 0 && <span style={{ opacity: .7 }}> · {pickedFailed} REJECTED</span>}
              </span>
              <span style={{ flex: 1 }} />
              <button onClick={() => {
                if (pickedFailed === 0) { window.toast && window.toast('No rejected batches in selection', 'soft'); return; }
                window.toast && window.toast(`Re-queued ${pickedFailed} rejected batch${pickedFailed === 1 ? '' : 'es'}`, 'ok');
                setPicked(new Set());
              }} className="ff-mono upper"
                style={{ fontSize: 10, letterSpacing: '.08em', padding: '6px 10px', background: 'transparent', color: 'var(--bg)', border: '1px solid rgba(255,255,255,.4)', cursor: 'pointer' }}>
                Re-queue rejected
              </button>
              <button onClick={() => {
                window.toast && window.toast(`Exported ${picked.size} batch manifest${picked.size === 1 ? '' : 's'} to CSV`, 'ok');
              }} className="ff-mono upper"
                style={{ fontSize: 10, letterSpacing: '.08em', padding: '6px 10px', background: 'transparent', color: 'var(--bg)', border: '1px solid rgba(255,255,255,.4)', cursor: 'pointer' }}>
                Export CSV
              </button>
              <button onClick={() => setPicked(new Set())} className="ff-mono upper"
                style={{ fontSize: 10, letterSpacing: '.08em', padding: '6px 10px', background: 'transparent', color: 'var(--bg)', border: 0, cursor: 'pointer', opacity: .7 }}>
                Clear
              </button>
            </div>
          );
        })()}

        {/* Batch list */}
        <Section num="01">Recent transmissions</Section>
        <div style={{ borderTop: '1px solid var(--rule)', marginBottom: 32 }}>
          <div className="ff-mono upper" style={{
            display: 'grid', gridTemplateColumns: '28px 80px 1fr 70px 80px 80px 100px',
            gap: 14, padding: '10px 14px', fontSize: 10, color: 'var(--ink-3)',
            background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)', borderLeft: '3px solid transparent',
            alignItems: 'center',
          }}>
            <input type="checkbox"
              checked={filtered.length > 0 && filtered.slice(0, 50).every(b => picked.has(b.id))}
              onChange={(e) => {
                const next = new Set(picked);
                if (e.target.checked) filtered.slice(0, 50).forEach(b => next.add(b.id));
                else filtered.slice(0, 50).forEach(b => next.delete(b.id));
                setPicked(next);
              }}
              title="Select visible"
              style={{ accentColor: 'var(--ink)', cursor: 'pointer', justifySelf: 'center' }} />
            <span>SOC</span><span>FILE</span><span style={{ textAlign: 'right' }}>WORKS</span>
            <span style={{ textAlign: 'right' }}>REJECTS</span><span>SENT</span><span style={{ textAlign: 'right' }}>STATUS</span>
          </div>
          {filtered.slice(0, 50).map(b => (
            <CwrBatchRow key={b.id} b={b}
              selected={selected?.id === b.id}
              onPick={setSelected}
              picked={picked.has(b.id)}
              onToggle={(id, on) => {
                const next = new Set(picked);
                if (on) next.add(id); else next.delete(id);
                setPicked(next);
              }} />
          ))}
          {filtered.length > 50 && (
            <div className="ff-mono" style={{ padding: '14px', textAlign: 'center', color: 'var(--ink-3)', fontSize: 11, borderBottom: '1px solid var(--rule-soft)' }}>
              + {filtered.length - 50} earlier batches · refine filters above
            </div>
          )}
          {filtered.length === 0 && (
            <div className="ff-mono" style={{ padding: '32px 14px', textAlign: 'center', color: 'var(--ink-3)', fontSize: 12 }}>
              No batches match the current filter.
            </div>
          )}
        </div>

        {/* Society routing matrix */}
        <Section num="02">Society routing · 30-day</Section>
        <CwrSocietyMatrix batches={batches} />

        <CwrBatchDrawer b={selected} onClose={() => setSelected(null)} />
        <CwrGenerateModal open={generateOpen} onClose={() => setGenerateOpen(false)} societies={societies} />
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // 2. BLACK-BOX / UNALLOCATED INBOX
  // ═══════════════════════════════════════════════════════════════════════
  // "Black-box" = royalties society holds because they couldn't identify
  // the rightsholder. Distributed by market share if not claimed.
  // Each row = a pool from one society for one period; lines underneath
  // are the unmatched works. Operator's job: claim within window or forfeit.

  const BB_REASONS = [
    { code: 'NO_IPI', label: 'IPI not registered with society', tone: 'warn' },
    { code: 'TITLE_ONLY', label: 'Title-only match · no ISWC',     tone: 'warn' },
    { code: 'MISSING_PUB', label: 'Publisher chain unconfirmed', tone: 'danger' },
    { code: 'TERR_GAP', label: 'No representation in territory', tone: 'danger' },
    { code: 'UNCLAIMED_WORK', label: 'Work not registered locally', tone: 'danger' },
    { code: 'NAME_VARIANT', label: 'Writer name variant unrecognized', tone: 'warn' },
    { code: 'PRE_CWR', label: 'Pre-CWR work · paper-only', tone: 'soft' },
  ];

  function buildBlackBoxPools() {
    const out = [];
    const targetSocs = (typeof SOCIETIES !== 'undefined' ? SOCIETIES : []).filter(s => s.kind === 'PRO' || s.kind === 'CMO' || s.kind === 'NRO').slice(0, 12);
    const periods = [
      { id: '2025Q4', label: '2025 Q4', distDate: '2026-04-30', windowDays: -1 },  // already passed
      { id: '2025Q3', label: '2025 Q3', distDate: '2026-01-31', windowDays: -90 }, // long passed
      { id: '2026Q1', label: '2026 Q1', distDate: '2026-07-31', windowDays: 92 }, // active
      { id: '2025_H2', label: '2025 H2', distDate: '2026-06-30', windowDays: 61 },
    ];
    targetSocs.forEach(soc => {
      periods.forEach(per => {
        const r = seed(soc.acronym + '·' + per.id);
        if (r() < 0.35) return; // not every society holds black-box every period
        const lineCount = 2 + Math.floor(r() * 8);
        const fxRate = { USD: 1, EUR: 1.08, GBP: 1.27, JPY: 0.0066 }[soc.kind === 'CMO' ? (soc.country === 'DE' || soc.country === 'FR' ? 'EUR' : 'USD') : 'USD'] || 1;
        const currency = soc.country === 'DE' || soc.country === 'FR' || soc.country === 'IT' || soc.country === 'ES' || soc.country === 'NL' ? 'EUR'
          : soc.country === 'GB' ? 'GBP'
          : soc.country === 'JP' ? 'JPY'
          : 'USD';
        // Build candidate lines from real WORKS so claims feel grounded
        const works = (typeof WORKS !== 'undefined' ? WORKS : []);
        const lines = [];
        let total = 0;
        for (let i = 0; i < lineCount; i++) {
          const w = works[Math.floor(r() * works.length)];
          if (!w) continue;
          const reason = BB_REASONS[Math.floor(r() * BB_REASONS.length)];
          const baseAmount = currency === 'JPY' ? 80000 + Math.floor(r() * 400000) : 800 + Math.floor(r() * 8400);
          const units = 5000 + Math.floor(r() * 90000);
          const confidence = 0.5 + r() * 0.49;
          lines.push({
            id: `bb_${soc.acronym}_${per.id}_${i}`,
            workId: w.id,
            workTitle: w.title,
            iswc: w.iswc,
            writers: w.writers,
            amount: baseAmount,
            units,
            reason: reason.code,
            reasonLabel: reason.label,
            reasonTone: reason.tone,
            confidence,
            // Suggested action: high confidence = auto-claim, mid = file ISWC, low = manual
            action: confidence > 0.85 ? 'auto-claim' : confidence > 0.65 ? 'file-iswc' : 'manual-review',
            firstSeen: new Date(2026, 3 - Math.floor(r() * 4), 5 + Math.floor(r() * 20)).toISOString().slice(0, 10),
          });
          total += baseAmount;
        }
        const distDate = new Date(per.distDate);
        const today = new Date('2026-04-30');
        const daysToDistribution = Math.round((distDate - today) / 86400000);
        out.push({
          id: `${soc.acronym}·${per.id}`,
          society: soc.acronym, societyName: soc.name, country: soc.country, kind: soc.kind,
          period: per.label, periodId: per.id,
          currency,
          totalAmount: total,
          totalUsd: Math.round(total * fxRate),
          lineCount: lines.length,
          lines,
          distDate: per.distDate,
          daysToDistribution,
          status: daysToDistribution < 0 ? 'forfeited' : daysToDistribution < 30 ? 'urgent' : 'open',
          claimRate: r() * 0.4, // pretend we've already claimed some portion
        });
      });
    });
    return out.sort((a, b) => {
      // urgent first, then by amount
      const wA = a.status === 'urgent' ? 0 : a.status === 'open' ? 1 : 2;
      const wB = b.status === 'urgent' ? 0 : b.status === 'open' ? 1 : 2;
      if (wA !== wB) return wA - wB;
      return b.totalUsd - a.totalUsd;
    });
  }

  function BlackBoxKpis({ pools }) {
    const open = pools.filter(p => p.status !== 'forfeited');
    const urgent = pools.filter(p => p.status === 'urgent');
    const forfeited = pools.filter(p => p.status === 'forfeited');
    const totalOpenUsd = open.reduce((s, p) => s + p.totalUsd, 0);
    const urgentUsd = urgent.reduce((s, p) => s + p.totalUsd, 0);
    const forfeitedUsd = forfeited.reduce((s, p) => s + p.totalUsd, 0);
    const lineCount = open.reduce((s, p) => s + p.lineCount, 0);
    const items = [
      { l: 'AT STAKE', v: fmt$(totalOpenUsd), sub: `${lineCount} lines · ${open.length} pools`, big: true },
      { l: 'URGENT · <30D', v: fmt$(urgentUsd), sub: `${urgent.length} pool${urgent.length === 1 ? '' : 's'}`, tone: 'warn' },
      { l: 'FORFEITED · 12M', v: fmt$(forfeitedUsd), sub: 'reallocated by market share', tone: 'danger' },
      { l: 'AVG TIME-TO-CLAIM', v: '4.8d', sub: 'goal · 7 days', tone: 'ok' },
    ];
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 24 }}>
        {items.map((it, i) => (
          <div key={i} style={{ padding: '20px 18px', borderRight: i < 3 ? '1px solid var(--rule)' : 'none' }}>
            <div className="ff-mono upper" style={{
              fontSize: 10, fontWeight: 600, letterSpacing: '.08em', marginBottom: 8,
              color: it.tone === 'danger' ? 'var(--danger,#c44)' : it.tone === 'warn' ? 'var(--accent,#d97757)' : it.tone === 'ok' ? 'var(--ok,#3a6a3a)' : 'var(--ink-3)',
            }}>{it.l}</div>
            <div className="ff-display num" style={{ fontSize: it.big ? 64 : 40, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1 }}>{it.v}</div>
            <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 6 }}>{it.sub}</div>
          </div>
        ))}
      </div>
    );
  }

  function BlackBoxPoolRow({ p, expanded, onToggle, onClaim, claimedIds }) {
    const tone = p.status === 'urgent' ? 'var(--accent,#d97757)' : p.status === 'forfeited' ? 'var(--ink-4)' : 'var(--ok,#3a6a3a)';
    // Live claim-rate: lines actually claimed via UI take precedence over seed claimRate
    const claimedCount = p.lines.filter(l => claimedIds && claimedIds.has(l.id)).length;
    const claimedPct = p.lines.length > 0
      ? Math.max(p.claimRate, claimedCount / p.lines.length)
      : 0;
    const isForfeited = p.status === 'forfeited';
    return (
      <div style={{ borderBottom: '1px solid var(--rule)', opacity: isForfeited ? 0.55 : 1 }}>
        <div onClick={onToggle}
          style={{
            display: 'grid', gridTemplateColumns: '24px 80px 1fr 130px 130px 110px 90px 30px',
            gap: 14, padding: '18px 14px', alignItems: 'center', cursor: 'pointer',
            background: expanded ? 'var(--bg-2)' : 'transparent',
            borderLeft: p.status === 'urgent' ? '3px solid var(--accent,#d97757)' : isForfeited ? '3px solid var(--ink-4)' : '3px solid transparent',
          }}>
          <Ic.Right width={14} height={14} style={{ color: 'var(--ink-3)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }} />
          <span className="ff-display" style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.02em' }}>{p.society}</span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 600 }}>{p.period} unallocated pool</div>
            <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>
              {p.lineCount} lines · {p.country} · distribution {p.distDate}
            </div>
          </div>
          {/* AMOUNT — native + USD stacked in one cell */}
          <div style={{ textAlign: 'right' }}>
            <div className="ff-display num" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', lineHeight: 1 }}>
              {fmt$(p.totalAmount, p.currency)}
            </div>
            {p.currency !== 'USD' && (
              <div className="ff-mono num" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 3 }}>
                ≈ {fmt$(p.totalUsd, 'USD')}
              </div>
            )}
          </div>
          {/* CLAIM RATE — progress bar */}
          <div>
            <div className="ff-mono num" style={{ fontSize: 11, fontWeight: 600, marginBottom: 4, display: 'flex', justifyContent: 'space-between' }}>
              <span style={{ color: claimedPct >= 0.95 ? 'var(--ok,#3a6a3a)' : 'var(--ink)' }}>{Math.round(claimedPct * 100)}%</span>
              <span className="ff-mono upper" style={{ fontSize: 8, color: 'var(--ink-4)', letterSpacing: '.08em', fontWeight: 500 }}>CLAIMED</span>
            </div>
            <div style={{ height: 4, background: 'var(--bg-2)', position: 'relative' }}>
              <div style={{
                position: 'absolute', inset: 0, width: `${claimedPct * 100}%`,
                background: claimedPct >= 0.95 ? 'var(--ok,#3a6a3a)' : isForfeited ? 'var(--ink-4)' : 'var(--ink)',
              }} />
            </div>
          </div>
          {/* DEADLINE */}
          <div className="ff-mono num" style={{ fontSize: 11 }}>
            <div style={{ color: 'var(--ink)', fontWeight: 500 }}>
              {new Date(p.distDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
            </div>
            <div style={{ color: tone, fontSize: 10, marginTop: 2, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
              <span style={{ width: 6, height: 6, background: tone, borderRadius: '50%' }} />
              {p.daysToDistribution < 0 ? `${Math.abs(p.daysToDistribution)}d past` : p.daysToDistribution === 0 ? 'today' : `${p.daysToDistribution}d left`}
            </div>
          </div>
          {/* STATUS — only shown for URGENT and FORFEITED; OPEN gets no chrome */}
          {p.status === 'urgent' ? (
            <span className="ff-mono upper" style={{
              fontSize: 9, fontWeight: 700, letterSpacing: '.08em', textAlign: 'center',
              padding: '4px 8px',
              background: 'var(--accent,#d97757)', color: 'var(--accent-ink,#fff)',
            }}>URGENT</span>
          ) : isForfeited ? (
            <span className="ff-mono upper" style={{
              fontSize: 9, fontWeight: 500, letterSpacing: '.08em', textAlign: 'center',
              color: 'var(--ink-4)',
            }}>FORFEITED</span>
          ) : (
            <span />
          )}
          <Ic.Down width={14} height={14} style={{ color: 'var(--ink-4)', transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
        </div>
        {expanded && (
          <div style={{ background: 'var(--bg-2)', borderTop: '1px solid var(--rule)', padding: '0' }}>
            {/* Lines header */}
            <div className="ff-mono upper" style={{
              display: 'grid', gridTemplateColumns: '40px 1fr 100px 100px 110px 90px 110px',
              gap: 12, padding: '10px 18px', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', borderBottom: '1px solid var(--rule-soft)',
            }}>
              <span>#</span><span>WORK</span><span>ISWC</span><span style={{ textAlign: 'right' }}>AMOUNT</span>
              <span>REASON</span><span>CONFIDENCE</span><span style={{ textAlign: 'right' }}>ACTION</span>
            </div>
            {p.lines.map((l, i) => (
              <div key={l.id} style={{
                display: 'grid', gridTemplateColumns: '40px 1fr 100px 100px 110px 90px 110px',
                gap: 12, padding: '10px 18px', alignItems: 'center', borderBottom: i < p.lines.length - 1 ? '1px solid var(--rule-soft)' : 'none',
              }}>
                <span className="ff-mono num" style={{ fontSize: 10, color: 'var(--ink-4)' }}>{String(i + 1).padStart(2, '0')}</span>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{l.workTitle}</div>
                  <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {l.writers.slice(0, 2).join(' · ')}
                  </div>
                </div>
                <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{l.iswc.replace('T-', '')}</span>
                <span className="ff-mono num" style={{ fontSize: 12, textAlign: 'right' }}>{fmt$(l.amount, p.currency)}</span>
                <span className="ff-mono upper" style={{
                  fontSize: 9, padding: '3px 7px', letterSpacing: '.06em',
                  background: l.reasonTone === 'danger' ? 'rgba(196,68,68,0.08)' : l.reasonTone === 'warn' ? 'rgba(217,119,87,0.08)' : 'var(--bg)',
                  color: l.reasonTone === 'danger' ? 'var(--danger,#c44)' : l.reasonTone === 'warn' ? 'var(--accent,#d97757)' : 'var(--ink-3)',
                  textAlign: 'center', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }} title={l.reasonLabel}>{l.reason.replace('_', ' ')}</span>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <div style={{ flex: 1, height: 4, background: 'var(--bg)', position: 'relative' }}>
                    <div style={{ position: 'absolute', inset: 0, width: `${l.confidence * 100}%`, background: l.confidence > 0.85 ? 'var(--ok,#3a6a3a)' : l.confidence > 0.65 ? 'var(--accent,#d97757)' : 'var(--ink-3)' }} />
                  </div>
                  <span className="ff-mono num" style={{ fontSize: 9, color: 'var(--ink-3)', minWidth: 22, textAlign: 'right' }}>{Math.round(l.confidence * 100)}</span>
                </div>
                <button onClick={(e) => { e.stopPropagation(); onClaim(p, l); }} className="ff-mono upper" style={{
                  fontSize: 9, padding: '4px 8px', letterSpacing: '.06em', fontWeight: 600,
                  background: l.action === 'auto-claim' ? 'var(--ink)' : 'transparent',
                  color: l.action === 'auto-claim' ? 'var(--bg)' : 'var(--ink)',
                  border: '1px solid var(--ink)', cursor: 'pointer',
                }}>
                  {l.action === 'auto-claim' ? 'CLAIM' : l.action === 'file-iswc' ? 'FILE ISWC' : 'REVIEW'}
                </button>
              </div>
            ))}
            <div style={{ display: 'flex', gap: 8, padding: '14px 18px', borderTop: '1px solid var(--rule-soft)', flexWrap: 'wrap' }}>
              <Btn variant="primary" size="sm" icon={<Ic.Check />} onClick={() => onClaim(p, null)}>Claim entire pool</Btn>
              <Btn variant="secondary" size="sm" onClick={() => window.toast && window.toast(`${p.society} ${p.period} · evidence pack downloaded`, 'soft')}>Export evidence pack</Btn>
              <Btn variant="ghost" size="sm" onClick={() => window.toast && window.toast(`Filed claim with ${p.society} for ${p.period}`, 'soft')}>File society claim form</Btn>
              <span style={{ flex: 1 }} />
              <Btn variant="ghost" size="sm" onClick={() => window.toast && window.toast('Pool dismissed', 'soft')}>Dismiss</Btn>
            </div>
          </div>
        )}
      </div>
    );
  }

  function ScreenBlackBox({ go }) {
    const pools = useMemo(buildBlackBoxPools, []);
    const [filter, setFilter] = useState('all'); // all | urgent | open | forfeited
    const [societyFilter, setSocietyFilter] = useState('all');
    const [expanded, setExpanded] = useState({});
    const [claimedIds, setClaimedIds] = useState(new Set());

    const filtered = pools.filter(p => {
      if (filter === 'all') return true;
      return p.status === filter;
    }).filter(p => societyFilter === 'all' ? true : p.society === societyFilter);

    const allSocs = useMemo(() => Array.from(new Set(pools.map(p => p.society))).sort(), [pools]);

    const toggle = (id) => setExpanded(e => ({ ...e, [id]: !e[id] }));

    const claim = (pool, line) => {
      if (line) {
        setClaimedIds(prev => new Set([...prev, line.id]));
        window.toast && window.toast(`Claim filed · ${line.workTitle} · ${fmt$(line.amount, pool.currency)}`, 'ok');
      } else {
        const ids = new Set([...claimedIds, ...pool.lines.map(l => l.id)]);
        setClaimedIds(ids);
        window.toast && window.toast(`Pool claim filed · ${pool.society} ${pool.period} · ${fmt$(pool.totalAmount, pool.currency)}`, 'ok');
      }
    };

    return (
      <div>
        <PageHeader
          eyebrow={`UNALLOCATED · BLACK-BOX RECOVERY · ${pools.filter(p => p.status === 'urgent').length} URGENT`}
          title="Black-box"
          highlight="Black-box"
          sub="Royalties societies hold because the rightsholder couldn't be identified. Unclaimed pools are redistributed by market share at the distribution date — find yours, claim them, recover."
          actions={
            <Btn variant="primary" size="sm" icon={<Ic.Down />} onClick={() => window.toast && window.toast('Bulk claim package generated · all OPEN pools', 'ok')}>
              Bulk claim · all
            </Btn>
          }
        />

        <BlackBoxKpis pools={pools} />

        {/* How it works strip */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', border: '1px solid var(--rule)', marginBottom: 32 }}>
          {[
            { n: '01', title: 'Society holds', body: 'PRO/CMO collects royalty for an unidentified work. Held in unallocated pool for the period.' },
            { n: '02', title: 'We match', body: 'ASTRO scans pool against your IPI / ISWC / writer-name index. Suggests claims with confidence score.' },
            { n: '03', title: 'You file', body: 'Submit a claim within the society\'s window — typically 60-180 days post-distribution.' },
            { n: '04', title: 'Or forfeit', body: 'Past the window the pool is distributed by market share to all society members. Lost.' },
          ].map((s, i, arr) => (
            <div key={s.n} style={{ padding: '18px 16px', borderRight: i < arr.length - 1 ? '1px solid var(--rule)' : 'none' }}>
              <div className="ff-mono num" style={{ fontSize: 32, fontWeight: 600, letterSpacing: '-0.04em', color: 'var(--ink-4)', lineHeight: 1, marginBottom: 8 }}>{s.n}</div>
              <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 6, letterSpacing: '-0.01em' }}>{s.title}</div>
              <div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5 }}>{s.body}</div>
            </div>
          ))}
        </div>

        {/* Filters */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 14, alignItems: 'center', flexWrap: 'wrap' }}>
          {['all', 'urgent', 'open', 'forfeited'].map(f => {
            const count = f === 'all' ? pools.length : pools.filter(p => p.status === f).length;
            return (
              <button key={f} onClick={() => setFilter(f)} className="ff-mono upper" style={{
                padding: '6px 12px', fontSize: 11, fontWeight: 500, letterSpacing: '.08em',
                background: filter === f ? 'var(--ink)' : 'transparent',
                color: filter === f ? 'var(--bg)' : 'var(--ink-2)',
                border: '1px solid var(--rule)',
              }}>{f} <span className="num" style={{ opacity: .55, marginLeft: 4 }}>{count}</span></button>
            );
          })}
          <span style={{ flex: 1 }} />
          <select value={societyFilter} onChange={e => setSocietyFilter(e.target.value)} className="ff-mono"
            style={{ padding: '6px 10px', fontSize: 11, border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)', minWidth: 160 }}>
            <option value="all">All societies ({allSocs.length})</option>
            {allSocs.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
        </div>

        {/* Pool table */}
        <Section num="01">Pools — by deadline</Section>
        <div style={{ borderTop: '1px solid var(--rule)' }}>
          <div className="ff-mono upper" style={{
            display: 'grid', gridTemplateColumns: '24px 80px 1fr 130px 130px 110px 90px 30px',
            gap: 14, padding: '10px 14px', fontSize: 10, color: 'var(--ink-3)', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)',
          }}>
            <span /><span>SOCIETY</span><span>POOL</span><span style={{ textAlign: 'right' }}>AMOUNT</span>
            <span>CLAIM RATE</span><span>CLAIM BY</span><span style={{ textAlign: 'center' }}>STATUS</span><span />
          </div>
          {filtered.map(p => (
            <BlackBoxPoolRow key={p.id} p={p} expanded={!!expanded[p.id]} onToggle={() => toggle(p.id)} onClaim={claim} claimedIds={claimedIds} />
          ))}
          {filtered.length === 0 && (
            <div style={{ padding: '60px 20px', textAlign: 'center', color: 'var(--ink-3)' }}>
              <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.08em', marginBottom: 6 }}>NO POOLS</div>
              <div style={{ fontSize: 13 }}>Nothing matches the current filter.</div>
            </div>
          )}
        </div>

        <div style={{ marginTop: 32, padding: '16px 20px', background: 'var(--bg-2)', border: '1px solid var(--rule-soft)', display: 'flex', gap: 14, alignItems: 'center' }}>
          <Ic.Eye width={20} height={20} style={{ color: 'var(--ink-3)', flexShrink: 0 }} />
          <div style={{ flex: 1, fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.5 }}>
            <b style={{ fontWeight: 600 }}>Tip:</b> sweep is run nightly against {(typeof SOCIETIES !== 'undefined' ? SOCIETIES.length : 30)} society distribution reports.
            New matches show up here. <Term k="CISAC">CISAC</Term> ISR-22 reciprocity covers most multi-territory disputes.
          </div>
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // 3. NOTIFICATIONS INBOX
  // ═══════════════════════════════════════════════════════════════════════
  // System & inbound messages: society notifications, DSP messages,
  // catalog activity, claim updates, statement events, RDR notifications,
  // teammate mentions. Email-client style two-pane.

  const NOTIF_KINDS = {
    'work-notif': { label: 'WORK NOTIF', tone: '#1f4ed8', icon: 'Branch' },
    'cwr-ack':    { label: 'CWR ACK',    tone: '#3a6a3a', icon: 'Send' },
    'cwr-rej':    { label: 'CWR REJ',    tone: '#c44',    icon: 'X' },
    'rdr':        { label: 'RDR',        tone: '#a04432', icon: 'Disc' },
    'claim':      { label: 'CLAIM',      tone: '#d97757', icon: 'Shield' },
    'statement':  { label: 'STATEMENT',  tone: '#1f4ed8', icon: 'Coin' },
    'mention':    { label: 'MENTION',    tone: '#7a4ed8', icon: 'User' },
    'system':     { label: 'SYSTEM',     tone: '#666',    icon: 'Settings' },
    'dsp':        { label: 'DSP',        tone: '#1ed760', icon: 'Globe' },
    'edit':       { label: 'EDIT',       tone: '#7a4ed8', icon: 'Eye' },
  };

  function buildNotifications() {
    // Snapshot of "now" — generated once, deterministic
    const today = new Date('2026-04-30T16:30:00Z');
    const works = (typeof WORKS !== 'undefined' ? WORKS : []);
    const claims = (typeof CLAIMS !== 'undefined' ? CLAIMS : []);
    const cmos = (typeof SOCIETIES !== 'undefined' ? SOCIETIES : []);
    const r = seed('notif-2026-04-30');
    const out = [];

    const push = (n) => out.push({ id: `n-${out.length}`, ts: today.getTime() - n.minsAgo * 60000, ...n });

    // CWR acks (from TXN if present)
    if (typeof TXN !== 'undefined') {
      TXN.forEach((t, i) => {
        if (t.status === 'rejected') {
          push({ kind: 'cwr-rej', minsAgo: 30 + i * 18, society: t.recv, title: `${t.recv} rejected ${t.file}`, body: `Batch ${t.id} · 121 transactions · primary error: NWR-VL-022 · Total share != 100%. Re-send required.`, action: { label: 'Open transmission', go: 'cwr' }, severity: 'high', read: false });
        } else if (t.status === 'acknowledged') {
          push({ kind: 'cwr-ack', minsAgo: 60 + i * 22, society: t.recv, title: `${t.recv} acknowledged ${t.count} works`, body: `Batch ${t.id} processed cleanly · ack received in ${i + 1}h. All transactions accepted.`, action: { label: 'View ACK', go: 'cwr' }, read: i > 1 });
        }
      });
    }

    // Society work notifications (notification of registration by another publisher)
    cmos.slice(0, 6).forEach((s, i) => {
      const w = works[i % works.length];
      if (!w) return;
      push({
        kind: 'work-notif', minsAgo: 90 + i * 110, society: s.acronym,
        title: `${s.acronym} · CISAC notification on "${w.title}"`,
        body: `Counter-publisher ${['Sony/ATV','Universal MP','Concord','BMG','Kobalt'][i % 5]} (${s.acronym}, ${s.country}) registered ${[33, 50, 25, 15, 100, 60][i % 6]}% on ${w.iswc}. Conflicts with our ${[67, 50, 75, 85, 100, 40][i % 6]}% claim.`,
        action: { label: 'Review claim', go: 'claims' },
        severity: i < 2 ? 'high' : 'mid',
        entity: { kind: 'work', title: w.title, iswc: w.iswc },
        read: false,
      });
    });

    // Claim activity from CLAIMS
    claims.slice(0, 8).forEach((c, i) => {
      const isResolved = c.status === 'resolved';
      const isReview = c.status === 'review';
      push({
        kind: 'claim', minsAgo: 200 + i * 60, society: c.method === 'cwr' ? '—' : c.party,
        title: isResolved ? `${c.id} resolved` : isReview ? `${c.id} · counter-proposal received` : `${c.party} disputes ${c.work} (${c.pct}%)`,
        body: isResolved
          ? `Claim closed · ${c.work} · ${c.pct}% allocated to ${c.claimant}.`
          : isReview
            ? `${c.party} responded with a counter-proposal. Review the new split before accepting.`
            : `${c.party} filed a counter-claim against our ${c.pct}% share on ${c.work}. ${c.severity === 'high' ? 'High severity — response due in 5d.' : 'Standard response window.'}`,
        action: { label: 'Open claim', go: 'claims' },
        severity: c.severity,
        entity: { kind: 'claim', id: c.id },
        read: i > 1,
      });
    });

    // Statement events
    [
      { src: 'Spotify', period: 'Mar 2026', action: 'parsed', minsAgo: 240, lines: 28447, gross: 84221 },
      { src: 'GEMA',    period: '2025 Q4', action: 'received', minsAgo: 480, lines: 1188, gross: 12340 },
      { src: 'PRS for Music', period: '2025 Q4', action: 'parse-failed', minsAgo: 720, lines: 0, gross: 0 },
      { src: 'The MLC', period: 'Mar 2026', action: 'reconciled', minsAgo: 1440, lines: 9211, gross: 18302 },
    ].forEach((s, i) => {
      const isFail = s.action === 'parse-failed';
      push({
        kind: 'statement', minsAgo: s.minsAgo, society: s.src,
        title: `${s.src} ${s.period} statement · ${s.action}`,
        body: isFail
          ? `Schema mismatch on column 14. Auto-parser unable to ingest the file. Manual mapping required.`
          : `${s.lines.toLocaleString()} lines · ${fmt$(s.gross)} gross. ${s.action === 'reconciled' ? 'Ready for payout.' : 'Awaiting line-match step.'}`,
        action: { label: 'Open statement', go: 'royalties' },
        severity: isFail ? 'high' : '',
        read: i > 0,
      });
    });

    // RDR / neighboring rights
    push({
      kind: 'rdr', minsAgo: 800, society: 'SoundExchange',
      title: 'SoundExchange · RDR-N delivery posted',
      body: 'Q1 2026 neighboring-rights statement available for download. 14,892 lines across 1,108 ISRCs.',
      action: { label: 'Open royalties', go: 'royalties' }, read: false,
    });

    // DSP delivery confirmations
    push({
      kind: 'dsp', minsAgo: 60, society: 'Spotify',
      title: 'Spotify ingested 2 new releases',
      body: 'DDEX ERN 4.3 · NewReleaseMessage · "Vermilion EP" + "Citrine — Singles" · live in 14 territories.',
      action: { label: 'Open release', go: 'catalog' }, read: false,
    });
    push({
      kind: 'dsp', minsAgo: 1620, society: 'YouTube Content ID',
      title: 'YouTube Content ID matched 1,204 ISRCs',
      body: 'Channel UC0918 · auto-matched master recordings to your catalog. Royalty stream activated.',
      action: { label: 'Open Content ID', go: 'royalties' }, read: true,
    });

    // Edit proposals
    push({
      kind: 'edit', minsAgo: 35, society: '—',
      title: 'k.davis proposed 3 edits to "Cranes In The Sky"',
      body: 'Updated copyright line, added 1 alternate title, changed primary writer share from 60% → 62%. Awaiting your review.',
      action: { label: 'Review proposals', go: 'issues' }, severity: 'mid', read: false,
    });

    // Teammate mention
    push({
      kind: 'mention', minsAgo: 18, society: '—',
      title: '@a.cohen mentioned you on claim C-2026-04-1882',
      body: '"Need your sign-off on the disputed share — Sony evidence looks weak but I want a second opinion before we counter."',
      action: { label: 'Open thread', go: 'claims' }, read: false,
    });

    // System
    push({ kind: 'system', minsAgo: 1320, society: '—', title: 'Catalog sweep complete', body: '+212 new works ingested in the last 30d · 18 new conflicts opened · 146 statements queued.', action: null, read: true });
    push({ kind: 'system', minsAgo: 2880, society: '—', title: 'API token expires in 14 days', body: 'Token "ASTRO_PROD_R2" issued 2025-08-14 will expire on 2026-05-14. Rotate to avoid service interruption.', action: { label: 'Open settings', go: 'settings' }, severity: 'mid', read: true });

    return out.sort((a, b) => b.ts - a.ts);
  }

  function NotifBadge({ kind }) {
    const k = NOTIF_KINDS[kind] || NOTIF_KINDS.system;
    return (
      <span className="ff-mono upper" style={{
        fontSize: 9, padding: '3px 7px', letterSpacing: '.06em', fontWeight: 600,
        background: `${k.tone}14`, color: k.tone, border: `1px solid ${k.tone}30`, whiteSpace: 'nowrap',
      }}>{k.label}</span>
    );
  }

  function NotifRow({ n, selected, onPick, onToggleRead }) {
    const minsAgo = (Date.now() - n.ts) / 60000;
    const since = new Date(n.ts);
    const today = new Date('2026-04-30T16:30:00Z');
    const hoursAgo = (today.getTime() - n.ts) / 3600000;
    return (
      <div onClick={() => onPick(n)}
        style={{
          display: 'grid', gridTemplateColumns: '8px 110px 1fr 70px',
          gap: 10, padding: '14px 16px', cursor: 'pointer', alignItems: 'flex-start',
          background: selected ? 'var(--bg-2)' : 'transparent',
          borderBottom: '1px solid var(--rule-soft)',
          borderLeft: selected ? '3px solid var(--ink)' : '3px solid transparent',
          paddingLeft: selected ? 13 : 16,
        }}
        onMouseEnter={e => { if (!selected) e.currentTarget.style.background = 'var(--bg-2)'; }}
        onMouseLeave={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }}>
        <span style={{
          width: 8, height: 8, marginTop: 5, borderRadius: '50%',
          background: n.read ? 'transparent' : (n.severity === 'high' ? 'var(--danger,#c44)' : 'var(--accent,#d97757)'),
          border: n.read ? '1px solid var(--rule)' : 'none',
        }} />
        <div>
          <NotifBadge kind={n.kind} />
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 6 }}>{n.society}</div>
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: n.read ? 500 : 600, lineHeight: 1.35, marginBottom: 4, letterSpacing: '-0.005em' }}>{n.title}</div>
          <div style={{ fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.45, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{n.body}</div>
        </div>
        <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', textAlign: 'right', whiteSpace: 'nowrap' }}>{ago(hoursAgo)}</span>
      </div>
    );
  }

  function NotifDetail({ n, go, onMarkRead, onArchive, onClose }) {
    if (!n) {
      return (
        <div style={{ padding: '80px 24px', textAlign: 'center', color: 'var(--ink-3)' }}>
          <Ic.Bell width={32} height={32} style={{ margin: '0 auto 14px', display: 'block', color: 'var(--ink-4)' }} />
          <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.08em', marginBottom: 8 }}>NO NOTIFICATION SELECTED</div>
          <div style={{ fontSize: 13 }}>Pick a row from the inbox to read it here.</div>
        </div>
      );
    }
    const today = new Date('2026-04-30T16:30:00Z');
    const hoursAgo = (today.getTime() - n.ts) / 3600000;
    return (
      <div style={{ padding: '24px 28px', overflow: 'auto' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14 }}>
          <NotifBadge kind={n.kind} />
          <div style={{ display: 'flex', gap: 6 }}>
            <Btn variant="ghost" size="sm" onClick={onMarkRead}>{n.read ? 'Mark unread' : 'Mark read'}</Btn>
            <Btn variant="ghost" size="sm" onClick={onArchive}>Archive</Btn>
            <button onClick={onClose} style={{ padding: 6, color: 'var(--ink-3)' }}><Ic.X width={14} height={14} /></button>
          </div>
        </div>
        <div className="heading-swap ff-display" style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', lineHeight: 1.15, marginBottom: 8 }}>{n.title}</div>
        <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 18, display: 'flex', gap: 14 }}>
          <span>FROM · {n.society}</span>
          <span>{new Date(n.ts).toISOString().slice(0, 16).replace('T', ' ')}Z</span>
          <span>· {ago(hoursAgo)}</span>
        </div>
        <div style={{
          fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.65, marginBottom: 22,
          padding: '16px 18px', background: 'var(--bg-2)', borderLeft: '3px solid var(--ink)',
        }}>
          {n.body}
        </div>

        {n.entity && n.entity.kind === 'work' && (
          <div style={{ border: '1px solid var(--rule)', padding: '14px 16px', marginBottom: 18 }}>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 6 }}>RELATED WORK</div>
            <div style={{ fontSize: 14, fontWeight: 600 }}>{n.entity.title}</div>
            <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{n.entity.iswc}</div>
          </div>
        )}
        {n.entity && n.entity.kind === 'claim' && (
          <div style={{ border: '1px solid var(--rule)', padding: '14px 16px', marginBottom: 18 }}>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', marginBottom: 6 }}>RELATED CLAIM</div>
            <div className="ff-mono" style={{ fontSize: 13, fontWeight: 600 }}>{n.entity.id}</div>
          </div>
        )}

        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {n.action && <Btn variant="primary" size="md" onClick={() => go(n.action.go)}>{n.action.label}</Btn>}
          <Btn variant="ghost" size="md" onClick={() => window.toast && window.toast('Notification snoozed for 24h', 'soft')}>Snooze 24h</Btn>
          <Btn variant="ghost" size="md" onClick={() => window.toast && window.toast('Forwarded to k.davis', 'soft')}>Forward to teammate</Btn>
        </div>
      </div>
    );
  }

  function ScreenNotifications({ go }) {
    const initial = useMemo(buildNotifications, []);
    const [items, setItems] = useState(initial);
    const [selected, setSelected] = useState(initial.find(n => !n.read) || initial[0] || null);
    const [filter, setFilter] = useState('all'); // all | unread | mentions | high
    const [kindFilter, setKindFilter] = useState('all');
    const [archived, setArchived] = useState(new Set());

    const visible = items
      .filter(n => !archived.has(n.id))
      .filter(n => {
        if (filter === 'unread') return !n.read;
        if (filter === 'mentions') return n.kind === 'mention';
        if (filter === 'high') return n.severity === 'high';
        return true;
      })
      .filter(n => kindFilter === 'all' ? true : n.kind === kindFilter);

    const unreadCount = items.filter(n => !n.read && !archived.has(n.id)).length;

    const setRead = (id, val) => setItems(prev => prev.map(n => n.id === id ? { ...n, read: val } : n));
    const onPick = (n) => {
      setSelected(n);
      if (!n.read) setRead(n.id, true);
    };
    const onArchive = () => {
      if (!selected) return;
      setArchived(prev => new Set([...prev, selected.id]));
      window.toast && window.toast('Notification archived', 'soft');
      setSelected(null);
    };
    const markAllRead = () => {
      setItems(prev => prev.map(n => ({ ...n, read: true })));
      window.toast && window.toast(`Marked ${unreadCount} as read`, 'ok');
    };

    // group by day for inbox section headers
    const grouped = useMemo(() => {
      const groups = [];
      let curDay = null;
      visible.forEach(n => {
        const d = new Date(n.ts).toISOString().slice(0, 10);
        if (d !== curDay) {
          curDay = d;
          groups.push({ day: d, items: [] });
        }
        groups[groups.length - 1].items.push(n);
      });
      return groups;
    }, [visible]);
    const today = new Date('2026-04-30').toISOString().slice(0, 10);
    const yest = new Date('2026-04-29').toISOString().slice(0, 10);
    const dayLabel = (d) => d === today ? 'TODAY' : d === yest ? 'YESTERDAY' : d;

    return (
      <div>
        <PageHeader
          eyebrow={`INBOX · ${unreadCount} UNREAD · ${items.length - archived.size} TOTAL`}
          title="Notifications"
          highlight="Notifications"
          actions={
            <>
              <Btn variant="ghost" size="sm" onClick={markAllRead} disabled={!unreadCount}>Mark all read</Btn>
              <Btn variant="secondary" size="sm" icon={<Ic.Settings />} onClick={() => go('settings', { tab: 'notifications' })}>Notification settings</Btn>
            </>
          }
        />

        {/* Filter strip */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 0, alignItems: 'center', flexWrap: 'wrap', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', padding: '12px 0' }}>
          {[
            { k: 'all', l: 'All', n: items.length - archived.size },
            { k: 'unread', l: 'Unread', n: unreadCount },
            { k: 'high', l: 'High priority', n: items.filter(n => n.severity === 'high' && !archived.has(n.id)).length },
            { k: 'mentions', l: 'Mentions', n: items.filter(n => n.kind === 'mention' && !archived.has(n.id)).length },
          ].map(f => (
            <button key={f.k} onClick={() => setFilter(f.k)} className="ff-mono upper" style={{
              padding: '6px 12px', fontSize: 11, fontWeight: 500, letterSpacing: '.08em',
              background: filter === f.k ? 'var(--ink)' : 'transparent',
              color: filter === f.k ? 'var(--bg)' : 'var(--ink-2)',
              border: '1px solid var(--rule)',
            }}>{f.l} <span className="num" style={{ opacity: .55, marginLeft: 4 }}>{f.n}</span></button>
          ))}
          <span style={{ flex: 1 }} />
          <select value={kindFilter} onChange={e => setKindFilter(e.target.value)} className="ff-mono"
            style={{ padding: '6px 10px', fontSize: 11, border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)', minWidth: 140 }}>
            <option value="all">All types</option>
            {Object.entries(NOTIF_KINDS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
          </select>
        </div>

        {/* Two-pane */}
        <div style={{ display: 'grid', gridTemplateColumns: 'minmax(380px, 1fr) 1.4fr', gap: 0, border: '1px solid var(--rule)', borderTop: 'none', minHeight: 600 }}>
          {/* Inbox list */}
          <div style={{ borderRight: '1px solid var(--rule)', overflow: 'auto', maxHeight: '70vh' }}>
            {grouped.length === 0 && (
              <div style={{ padding: '60px 20px', textAlign: 'center', color: 'var(--ink-3)' }}>
                <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.08em', marginBottom: 6 }}>INBOX EMPTY</div>
                <div style={{ fontSize: 13 }}>Nothing matches the current filter.</div>
              </div>
            )}
            {grouped.map(group => (
              <div key={group.day}>
                <div className="ff-mono upper" style={{
                  padding: '10px 16px', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em',
                  background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)', position: 'sticky', top: 0,
                }}>
                  {dayLabel(group.day)} <span className="num" style={{ opacity: .5, marginLeft: 8 }}>· {group.items.length}</span>
                </div>
                {group.items.map(n => (
                  <NotifRow key={n.id} n={n} selected={selected?.id === n.id} onPick={onPick}
                    onToggleRead={() => setRead(n.id, !n.read)} />
                ))}
              </div>
            ))}
          </div>
          {/* Detail pane */}
          <div>
            <NotifDetail n={selected} go={go}
              onMarkRead={() => selected && setRead(selected.id, !selected.read)}
              onArchive={onArchive}
              onClose={() => setSelected(null)} />
          </div>
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // 4. OPERATIONS PULSE — dashboard band
  // ═══════════════════════════════════════════════════════════════════════
  // 6-tile newspaper-style row that lights up the operational surfaces
  // (transmissions, black-box, claims, issues, notifications) at a glance.
  // Each tile is a live link.

  function OperationsPulse({ go }) {
    // Pull live counts where we have them
    const stats = window.CATALOG_STATS || {};
    const claimsOpen = stats.claims?.open || 0;
    const claimsAtRisk = stats.claims?.atRisk || 184_220; // fallback
    const issuesOpen = window.__ISSUES_OPEN_COUNT || 0;
    const blackboxUrgent = window.__BLACKBOX_URGENT || 0;
    const notifUnread = window.__NOTIFICATIONS_UNREAD || 0;
    const subpubPartners = window.__SUBPUB_ACTIVE_PARTNERS || 0;
    const subpubExpiring = window.__SUBPUB_EXPIRING || 0;
    const subpubUrgent   = window.__SUBPUB_URGENT || null;
    // Synthesize a few CWR numbers from the same builder if available
    let cwrToday = 0, cwrAck = null, cwrFailed = 0, cwrInFlight = 0;
    try {
      // Re-derive cheaply — single iteration
      const today = new Date('2026-04-30T16:00:00Z');
      const cwrSocs = (typeof SOCIETIES !== 'undefined' ? SOCIETIES : []).filter(s => s.kind === 'PRO' || s.kind === 'MRO' || s.kind === 'CMO' || s.kind === 'HUB').slice(0, 14);
      let ok = 0, settled = 0;
      for (let d = 0; d < 7; d++) {
        const dayKey = new Date(today.getTime() - d * 86400000).toISOString().slice(0,10).replace(/-/g, '');
        const r = seed('cwr·' + dayKey);
        const numBatches = 2 + Math.floor(r() * 3);
        for (let b = 0; b < numBatches; b++) {
          const soc = cwrSocs[Math.floor(r() * cwrSocs.length)];
          if (!soc) continue;
          const ackRel = (soc.ackRate || 95) / 100;
          let status;
          const isToday = d === 0;
          if (isToday && b >= numBatches - 1) status = r() < 0.35 ? 'queued' : 'submitted';
          else if (isToday || d === 1) {
            const n = r();
            if (n < 0.20) status = 'processing';
            else if (n < 0.30 && ackRel < 0.95) status = 'rejected';
            else status = 'acknowledged';
          } else {
            status = r() < (1 - ackRel) * 1.4 ? 'rejected' : 'acknowledged';
          }
          if (d === 0) cwrToday++;
          if (['submitted', 'processing', 'queued'].includes(status)) cwrInFlight++;
          if (d < 7) {
            if (['acknowledged','rejected'].includes(status)) {
              settled++;
              if (status === 'acknowledged') ok++;
              if (status === 'rejected' && d < 7) cwrFailed++;
            }
          }
        }
      }
      cwrAck = settled > 0 ? Math.round(ok / settled * 1000) / 10 : null;
    } catch { /* silent */ }

    const tiles = [
      {
        l: 'CWR · TODAY',
        v: cwrToday || '—',
        sub: cwrAck != null ? `${cwrAck}% ack · 7d` : `${cwrInFlight} in flight`,
        tone: cwrFailed > 0 ? 'warn' : '',
        tag: cwrFailed > 0 ? `${cwrFailed} FAIL` : null,
        go: 'cwr',
      },
      {
        l: 'BLACK-BOX',
        v: '$348K',
        sub: `${blackboxUrgent} urgent pool${blackboxUrgent === 1 ? '' : 's'}`,
        tone: blackboxUrgent > 0 ? 'warn' : '',
        tag: blackboxUrgent > 0 ? 'CLAIM' : null,
        go: 'issues',
      },
      {
        l: 'SUB-PUB NETWORK',
        v: subpubPartners || '—',
        sub: subpubUrgent ? subpubUrgent.label : 'all deals current',
        tone: subpubUrgent ? (subpubUrgent.kind === 'expiring' ? 'warn' : 'crit') : '',
        tag: subpubUrgent ? (subpubUrgent.kind === 'late' ? 'LATE' : subpubUrgent.kind === 'audit' ? 'AUDIT' : 'RENEW') : null,
        go: 'subpubs',
      },
      {
        l: 'ISSUES · INBOX',
        v: issuesOpen || '—',
        sub: claimsOpen
          ? `${claimsOpen} ownership conflict${claimsOpen === 1 ? '' : 's'} · ${fmt$(claimsAtRisk)} at risk`
          : 'metadata · quality · rights',
        tone: issuesOpen > 8 ? 'danger' : issuesOpen > 0 ? 'warn' : 'ok',
        tag: claimsOpen > 0 ? `${claimsOpen} CLAIMS` : null,
        go: 'issues',
      },
      {
        l: 'INBOX',
        v: notifUnread || '—',
        sub: notifUnread > 0 ? 'CWR · CLAIM · MENTION' : 'all caught up',
        tone: notifUnread > 6 ? 'warn' : '',
        go: 'notifications',
      },
    ];

    const toneFg = (t) => t === 'danger' ? 'var(--danger,#c44)' : t === 'warn' ? 'var(--accent,#d97757)' : t === 'ok' ? 'var(--ok,#3a6a3a)' : 'var(--ink-3)';

    return (
      <div style={{ marginTop: 32 }}>
        <Section num="02" action={
          <a className="ff-mono upper" onClick={() => go('cwr')} style={{ fontSize: 10, color: 'var(--ink-3)', cursor: 'pointer', letterSpacing: '.08em' }}>
            Operate →
          </a>
        }>Operations · today</Section>
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))',
          borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)',
        }}>
          {tiles.map((t, i) => (
            <button key={t.l} onClick={() => go(t.go)} className="op-pulse-tile"
              style={{
                padding: '20px 16px', borderRight: '1px solid var(--rule)', borderBottom: '1px solid var(--rule-soft)',
                background: 'transparent', textAlign: 'left', cursor: 'pointer',
                display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
                minHeight: 120, position: 'relative', transition: 'background .15s',
              }}
              onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-2)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                <div className="ff-mono upper" style={{
                  fontSize: 9, fontWeight: 600, letterSpacing: '.1em', color: toneFg(t.tone),
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1,
                }}>{t.l}</div>
                {t.tag && (
                  <span className="ff-mono upper" style={{
                    fontSize: 8, padding: '2px 5px', letterSpacing: '.06em', fontWeight: 600,
                    background: t.tone === 'danger' ? 'rgba(196,68,68,0.1)' : 'rgba(217,119,87,0.1)',
                    color: toneFg(t.tone), marginLeft: 6, whiteSpace: 'nowrap',
                  }}>{t.tag}</span>
                )}
              </div>
              <div className="ff-display num" style={{
                fontSize: 40, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1, marginTop: 14,
                color: t.tone === 'danger' ? 'var(--danger,#c44)' : 'var(--ink)',
              }}>{t.v}</div>
              <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                {t.sub}
              </div>
            </button>
          ))}
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // EXPOSE
  // ═══════════════════════════════════════════════════════════════════════
  // Override existing ScreenCwr (loaded earlier in screens2.jsx) with the
  // deeper edition built here. Add ScreenBlackBox + ScreenNotifications.
  window.ScreenCwr = ScreenCwr;
  window.ScreenBlackBox = ScreenBlackBox;
  window.ScreenNotifications = ScreenNotifications;
  window.OperationsPulse = OperationsPulse;

  // Expose builder + count for header bell badge
  window.__NOTIFICATIONS_BUILDER = buildNotifications;
  try {
    const all = buildNotifications();
    window.__NOTIFICATIONS_UNREAD = all.filter(n => !n.read).length;
  } catch { window.__NOTIFICATIONS_UNREAD = 0; }

  // Expose builder for black-box urgent count (rail badge)
  try {
    const pools = buildBlackBoxPools();
    window.__BLACKBOX_URGENT = pools.filter(p => p.status === 'urgent').length;
    window.__BLACKBOX_POOLS = pools;
    // Recompute Issues open count now that black-box pools are available, so
    // the rail badge reflects black-box + claims + anomalies together.
    if (window.__buildAllAnomalies) {
      const all = window.__buildAllAnomalies();
      window.__ISSUES_OPEN_COUNT = all.filter(a => a.status==='open' || a.status==='investigating').length;
    }
  } catch { window.__BLACKBOX_URGENT = 0; }
})();
