/* BountyBloomberg Terminal — live edition.
   Ported from the Claude Design variant-bloomberg.jsx, rewired to consume the
   real /api/events + /api/stats endpoints. Same Bloomberg-terminal aesthetic
   (amber/green on black, JetBrains Mono, ticker, ASCII-style decay chart,
   slash-feed table, sentiment grid, footer), with the satire copy stripped
   per the user's call — every name and number is real. */

const BB_BG = '#0a0a0a';
const BB_PANEL = '#111';
const BB_BORDER = '#2a2a2a';
const BB_AMBER = '#ffb000';
const BB_GREEN = '#4ade80';
const BB_RED = '#ff3b30';
const BB_DIM = '#6b6b6b';
const BB_TEXT = '#d4d4d4';
const BB_FONT = '"JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace';

const BB_EVENT_LABEL = {
  slash:    'BOUNTY SLASHED',
  raise:    'BOUNTY RAISED',
  pause:    'PROGRAM PAUSED',
  close:    'PROGRAM CLOSED',
  rescope:  'SCOPE REWRITTEN',
  expand:   'SCOPE EXPANDED',
  modified: 'PROGRAM MODIFIED',
  added:    'PROGRAM ADDED',
};

// Polarity → color. Used by row borders, ticker tags, and the DETAIL cell so
// the whole UI tells the same hostile/friendly/mixed story at a glance.
function polColor(polarity) {
  if (polarity === 'friendly') return BB_GREEN;
  if (polarity === 'hostile')  return BB_RED;
  if (polarity === 'mixed')    return BB_AMBER;
  return BB_DIM;
}

const POLL_MS = 30_000;

function fmtUSD(n) {
  if (n == null) return '—';
  if (n >= 1_000_000) return '$' + (n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 2) + 'M';
  if (n >= 1_000) return '$' + (n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1) + 'K';
  return '$' + Math.round(n).toLocaleString();
}

function pctCut(before, after) {
  if (!before || before <= 0) return null;
  return Math.round(((before - after) / before) * 100);
}

function bbDate(iso) { return new Date(iso).toISOString().slice(0, 10).replace(/-/g, '.'); }
function bbTime(iso) { return new Date(iso).toISOString().slice(11, 16); }

// One UI context — every component that needs to open a program overlay or
// expand an event row reads handlers from here. Keeps prop drilling sane.
const UI = React.createContext({
  openProgram: () => {},
  expandEvent: () => {},
  expandedEventId: null,
});

function useLiveData() {
  const [stats, setStats] = React.useState(null);
  const [events, setEvents] = React.useState([]);
  const [health, setHealth] = React.useState({ immunefi: false, hackenproof: false });
  const [error, setError] = React.useState(null);

  const fetchAll = React.useCallback(async () => {
    try {
      const [sR, eR, hR] = await Promise.all([
        fetch('/api/stats'),
        fetch('/api/events'),
        fetch('/api/health'),
      ]);
      if (!sR.ok || !eR.ok) throw new Error(`HTTP ${sR.status}/${eR.status}`);
      const s = await sR.json();
      const e = await eR.json();
      const h = hR.ok ? await hR.json() : {};
      setStats(s);
      setEvents(e.events || []);
      setHealth({ immunefi: !!h.immumon, hackenproof: !!h.hackenproofmon });
      setError(null);
    } catch (err) {
      setError(err.message || String(err));
    }
  }, []);

  React.useEffect(() => {
    fetchAll();
    const id = setInterval(fetchAll, POLL_MS);
    return () => clearInterval(id);
  }, [fetchAll]);

  return { stats, events, health, error };
}

// ───────────────────────────── header / ticker ─────────────────────────────

function BBHeader({ health }) {
  const [now, setNow] = React.useState(new Date());
  React.useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(i);
  }, []);
  const dot = (ok) => ({ color: ok ? BB_GREEN : BB_RED });
  return (
    <div style={{
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '12px 20px', background: '#000', borderBottom: `1px solid ${BB_AMBER}`,
      fontFamily: BB_FONT, fontSize: 12,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
        <span style={{ color: BB_AMBER, fontWeight: 700, letterSpacing: 2 }}>◆ BOUNTYBLOOMBERG</span>
        <span style={{ color: BB_DIM }}>live · immumon + hackenproofmon</span>
        <a href="https://t.me/web3bugbountyalerts"
           target="_blank" rel="noopener noreferrer"
           style={{
             color: BB_GREEN,
             textDecoration: 'none',
             borderBottom: `1px dashed ${BB_GREEN}`,
             paddingBottom: 1,
             fontSize: 11,
             letterSpacing: 0.5,
           }}>
          ▶ alerts · @web3bugbountyalerts
        </a>
      </div>
      <div style={{ display: 'flex', gap: 18, color: BB_DIM }}>
        <span><span style={dot(health.immunefi)}>●</span> immumon {health.immunefi ? 'ok' : 'down'}</span>
        <span><span style={dot(health.hackenproof)}>●</span> hackenproofmon {health.hackenproof ? 'ok' : 'down'}</span>
        <span style={{ color: BB_AMBER }}>{now.toISOString().slice(11, 19)} UTC</span>
      </div>
    </div>
  );
}

function BBTicker({ events }) {
  // Mixed-polarity ticker — each item gets its own color span. Picks the most
  // recent 12 events, then a few biggest cuts and biggest raises so the strip
  // always carries some positive headlines too.
  const items = React.useMemo(() => {
    const recent = (events || []).slice(0, 12);
    const cuts = (events || [])
      .filter((e) => (e.type === 'slash' || e.type === 'close') && e.before > 0 && e.after != null)
      .sort((a, b) => (b.before - b.after) - (a.before - a.after))
      .slice(0, 3);
    const raises = (events || [])
      .filter((e) => e.type === 'raise' && e.before != null && e.after != null && e.after > e.before)
      .sort((a, b) => (b.after - b.before) - (a.after - a.before))
      .slice(0, 3);
    const seen = new Set();
    const out = [];
    for (const e of [...recent, ...cuts, ...raises]) {
      if (seen.has(e.id)) continue;
      seen.add(e.id);
      out.push(e);
    }
    return out;
  }, [events]);

  if (!items.length) {
    return (
      <div style={{ background: '#000', borderBottom: `1px solid ${BB_BORDER}`, padding: '10px 18px', fontFamily: BB_FONT, fontSize: 13, color: BB_DIM }}>
        NO DATA · WAITING ON IMMUMON / HACKENPROOFMON
      </div>
    );
  }

  const renderItem = (e, i) => {
    const cut = pctCut(e.before, e.after);
    const tag = cut != null
      ? (cut > 0 ? `−${cut}%` : `+${Math.abs(cut)}%`)
      : (BB_EVENT_LABEL[e.type] || 'EVENT');
    const color = polColor(e.polarity);
    return (
      <span key={e.id + '-' + i} style={{ color, marginRight: 18 }}>
        <span style={{ color: BB_AMBER }}>{e.program.toUpperCase().replace(/\s+/g, '')}</span>
        {' '}{tag}
        <span style={{ color: BB_DIM, margin: '0 8px' }}>◆</span>
      </span>
    );
  };

  return (
    <div style={{
      background: '#000', borderBottom: `1px solid ${BB_BORDER}`,
      overflow: 'hidden', padding: '8px 0', position: 'relative',
    }}>
      <div style={{
        whiteSpace: 'nowrap', fontFamily: BB_FONT, fontSize: 13,
        animation: 'bb-ticker 90s linear infinite', display: 'inline-block',
        paddingLeft: '100%',
      }}>
        {items.map(renderItem)}
        {items.map((e, i) => renderItem(e, i + 1000))}
      </div>
      <style>{`@keyframes bb-ticker { from { transform: translateX(0); } to { transform: translateX(-100%); } }`}</style>
    </div>
  );
}

// ───────────────────────────── stats / chart ─────────────────────────────

function BBStat({ label, value, sub, subColor }) {
  return (
    <div style={{ padding: '14px 18px', borderRight: `1px solid ${BB_BORDER}`, flex: 1, minWidth: 0 }}>
      <div style={{ fontFamily: BB_FONT, fontSize: 10, color: BB_DIM, letterSpacing: 1.5, textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontFamily: BB_FONT, fontSize: 22, color: BB_AMBER, marginTop: 4, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{value}</div>
      {sub && <div style={{ fontFamily: BB_FONT, fontSize: 11, color: subColor || BB_DIM, marginTop: 2 }}>{sub}</div>}
    </div>
  );
}

// Variant of BBStat with a custom value color (used to paint the headline
// number red/green/amber instead of the default amber).
function BBStatColored({ label, value, valueColor, sub, subColor }) {
  return (
    <div style={{ padding: '14px 18px', borderRight: `1px solid ${BB_BORDER}`, flex: 1, minWidth: 0 }}>
      <div style={{ fontFamily: BB_FONT, fontSize: 10, color: BB_DIM, letterSpacing: 1.5, textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontFamily: BB_FONT, fontSize: 22, color: valueColor || BB_AMBER, marginTop: 4, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{value}</div>
      {sub && <div style={{ fontFamily: BB_FONT, fontSize: 11, color: subColor || BB_DIM, marginTop: 2 }}>{sub}</div>}
    </div>
  );
}

function BBStatRow({ stats }) {
  const s = stats || {};
  const net = s.netChange || 0;
  const netColor = net > 0 ? BB_GREEN : net < 0 ? BB_RED : BB_AMBER;
  const netSign = net > 0 ? '+' : '';
  return (
    <div style={{ display: 'flex', background: BB_PANEL, borderBottom: `1px solid ${BB_BORDER}`, flexWrap: 'wrap' }}>
      <BBStatColored
        label="BNTYSLASHED"
        value={'−' + fmtUSD(s.totalSlashed || 0)}
        valueColor={BB_RED}
        sub={`avg cut: ${s.averageSlashPct || 0}%`}
        subColor={BB_RED}
      />
      <BBStatColored
        label="BNTYRAISED"
        value={'+' + fmtUSD(s.totalRaised || 0)}
        valueColor={BB_GREEN}
        sub={s.averageRaisePct ? `avg raise: ${s.averageRaisePct}%` : 'launches excluded from avg'}
        subColor={BB_GREEN}
      />
      <BBStatColored
        label="NET.CHANGE"
        value={netSign + fmtUSD(net)}
        valueColor={netColor}
        sub="raised − slashed (lifetime)"
        subColor={BB_DIM}
      />
      <BBStat label="PROGRAMS.TRACKED" value={(s.programsTracked || 0).toLocaleString()} sub="immumon + hackenproofmon" subColor={BB_DIM} />
      <BBStat label="EVENTS.30D" value={(s.events30d || 0).toLocaleString()} sub="raw monitor events, 30d" subColor={BB_DIM} />
      <BBStatColored
        label="REMOVED / ADDED"
        value={(s.programsRemoved || 0) + ' / ' + (s.programsAdded || 0)}
        valueColor={BB_AMBER}
        sub={`raises: ${s.programsRaised || 0} · expands: ${s.programsExpanded || 0}`}
        subColor={BB_DIM}
      />
    </div>
  );
}

function BBChart({ events }) {
  // Two-sided cumulative: raises bump up, slashes bump down. The final point
  // is the lifetime net (matches NET.CHANGE in the stat row). Time axis
  // bucketed into 24 even slices across the visible event window.
  const data = React.useMemo(() => {
    const points = (events || [])
      .filter((e) => e.before != null && e.after != null)
      .map((e) => {
        if (e.type === 'slash' || e.type === 'close') {
          return { ts: new Date(e.ts).getTime(), delta: -(e.before - (e.after || 0)) };
        }
        if (e.type === 'raise') {
          return { ts: new Date(e.ts).getTime(), delta: e.after - e.before };
        }
        return null;
      })
      .filter(Boolean)
      .filter((p) => p.delta !== 0)
      .sort((a, b) => a.ts - b.ts);
    if (points.length < 2) return null;
    const start = points[0].ts;
    const end = points[points.length - 1].ts;
    const span = Math.max(1, end - start);
    const buckets = 24;
    const series = new Array(buckets).fill(0);
    for (const p of points) {
      const i = Math.min(buckets - 1, Math.floor(((p.ts - start) / span) * buckets));
      series[i] += p.delta;
    }
    for (let i = 1; i < buckets; i++) series[i] += series[i - 1];
    return { series, start, end, finalNet: series[series.length - 1] };
  }, [events]);

  const W = 100, H = 38;
  if (!data) {
    return (
      <div style={{ padding: 18, background: BB_PANEL, borderBottom: `1px solid ${BB_BORDER}`, color: BB_DIM, fontFamily: BB_FONT, fontSize: 12 }}>
        ◆ BNTYUSD · NET BOUNTY CHANGE — insufficient data for chart
      </div>
    );
  }
  // Symmetric vertical scale so positive/negative both visible from a zero axis.
  const absMax = Math.max(1, ...data.series.map((v) => Math.abs(v)));
  const yFor = (v) => H / 2 - (v / absMax) * (H / 2);
  const zeroY = H / 2;
  const fmtDay = (t) => new Date(t).toISOString().slice(0, 10);
  const finalNet = data.finalNet;
  const headlineColor = finalNet >= 0 ? BB_GREEN : BB_RED;
  const headlineSign = finalNet >= 0 ? '+' : '−';

  // Polygon fill above zero (green), below zero (red).
  const ptStr = (i, v) => `${(i / (data.series.length - 1)) * W},${yFor(v)}`;
  return (
    <div style={{ padding: 18, background: BB_PANEL, borderBottom: `1px solid ${BB_BORDER}` }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
        <div style={{ fontFamily: BB_FONT, fontSize: 12, color: BB_AMBER, letterSpacing: 1.5 }}>
          ◆ BNTYUSD · NET CUMULATIVE BOUNTY CHANGE · {fmtDay(data.start)} → {fmtDay(data.end)}
        </div>
        <div style={{ fontFamily: BB_FONT, fontSize: 11, color: headlineColor, fontWeight: 700 }}>
          {finalNet >= 0 ? '▲' : '▼'} {headlineSign}{fmtUSD(Math.abs(finalNet))} net
        </div>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: '100%', height: 180, background: '#000', border: `1px solid ${BB_BORDER}` }}>
        {/* gridlines: ±100%, ±50%, 0 */}
        {[1, 0.5, 0, -0.5, -1].map((g) => (
          <line key={g} x1={0} x2={W} y1={H / 2 - g * (H / 2)} y2={H / 2 - g * (H / 2)}
            stroke={g === 0 ? BB_DIM : BB_BORDER} strokeWidth={g === 0 ? 0.25 : 0.12} />
        ))}
        {/* Single polyline, but a clip-path splits it at y=zero so we can
            paint above-zero green and below-zero red without rebuilding
            geometry. defs are unique-per-render via React. */}
        <defs>
          <clipPath id="bb-above-zero">
            <rect x="0" y="0" width={W} height={zeroY} />
          </clipPath>
          <clipPath id="bb-below-zero">
            <rect x="0" y={zeroY} width={W} height={H - zeroY} />
          </clipPath>
        </defs>
        <polygon
          points={data.series.map((v, i) => ptStr(i, v)).join(' ') + ` ${W},${zeroY} 0,${zeroY}`}
          fill={BB_GREEN} opacity={0.12} clipPath="url(#bb-above-zero)"
        />
        <polygon
          points={data.series.map((v, i) => ptStr(i, v)).join(' ') + ` ${W},${zeroY} 0,${zeroY}`}
          fill={BB_RED} opacity={0.12} clipPath="url(#bb-below-zero)"
        />
        <polyline
          points={data.series.map((v, i) => ptStr(i, v)).join(' ')}
          fill="none" stroke={BB_GREEN} strokeWidth={0.4} clipPath="url(#bb-above-zero)"
        />
        <polyline
          points={data.series.map((v, i) => ptStr(i, v)).join(' ')}
          fill="none" stroke={BB_RED} strokeWidth={0.4} clipPath="url(#bb-below-zero)"
        />
        {data.series.map((v, i) => (
          <circle key={i} cx={(i / (data.series.length - 1)) * W} cy={yFor(v)} r={0.5} fill={BB_AMBER} />
        ))}
      </svg>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: BB_FONT, fontSize: 10, color: BB_DIM, marginTop: 6 }}>
        <span>{fmtDay(data.start)} · zero-line = no net change</span>
        <span>{fmtDay(data.end)}</span>
      </div>
    </div>
  );
}

// ───────────────────────── diff renderer (shared) ─────────────────────────

// Render a single parsed change line in three styles depending on op:
//   modify  → red strike "before"   →   green "after"
//   add     → green "+ <after>"
//   remove  → red strike "- <before>"
//   text    → dim mono raw line
function ChangeLine({ c }) {
  const fieldStyle = { color: BB_AMBER, marginRight: 8, fontWeight: 600 };
  const base = { fontFamily: BB_FONT, fontSize: 12, padding: '4px 12px', display: 'block', overflowWrap: 'anywhere' };
  if (c.op === 'modify') {
    return (
      <div style={{ ...base, background: '#0a0a0a', borderLeft: `2px solid ${BB_AMBER}` }}>
        <span style={fieldStyle}>{c.field}</span>
        <span style={{ color: BB_RED, textDecoration: 'line-through', marginRight: 8 }}>{c.before}</span>
        <span style={{ color: BB_DIM, marginRight: 8 }}>→</span>
        <span style={{ color: BB_GREEN }}>{c.after}</span>
      </div>
    );
  }
  if (c.op === 'add') {
    return (
      <div style={{ ...base, background: 'rgba(74,222,128,0.06)', borderLeft: `2px solid ${BB_GREEN}` }}>
        <span style={fieldStyle}>{c.field}</span>
        <span style={{ color: BB_GREEN }}>+ {c.after}</span>
      </div>
    );
  }
  if (c.op === 'remove') {
    return (
      <div style={{ ...base, background: 'rgba(255,59,48,0.06)', borderLeft: `2px solid ${BB_RED}` }}>
        <span style={fieldStyle}>{c.field}</span>
        <span style={{ color: BB_RED, textDecoration: 'line-through' }}>− {c.before}</span>
      </div>
    );
  }
  return (
    <div style={{ ...base, color: BB_DIM, borderLeft: `2px solid ${BB_BORDER}` }}>{c.raw}</div>
  );
}

function DiffBlock({ detail, rawJson, loading, error }) {
  if (loading) return <div style={{ padding: 12, color: BB_DIM, fontFamily: BB_FONT, fontSize: 11 }}>loading diff…</div>;
  if (error) return <div style={{ padding: 12, color: BB_RED, fontFamily: BB_FONT, fontSize: 11 }}>diff error: {error}</div>;
  if (!detail) return null;
  const { changes, keptCount, rawCount, noiseCount } = detail;
  return (
    <div style={{ padding: '6px 0', background: '#000', borderTop: `1px solid ${BB_BORDER}` }}>
      <div style={{
        padding: '4px 12px', color: BB_DIM, fontFamily: BB_FONT,
        fontSize: 10, letterSpacing: 1, display: 'flex', justifyContent: 'space-between',
      }}>
        <span>◆ DIFF · {keptCount} kept · {noiseCount} noise filtered · {rawCount} total</span>
      </div>
      {changes.length === 0 && (
        <div style={{ padding: 12, color: BB_DIM, fontFamily: BB_FONT, fontSize: 11 }}>
          no parseable changes (removed program / noise-only)
        </div>
      )}
      {changes.map((c, i) => <ChangeLine key={i} c={c} />)}
    </div>
  );
}

// Hook: lazy-fetch a single event's diff. Memoizes per event id.
function useEventDetail(platform, rawId, enabled) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  React.useEffect(() => {
    if (!enabled || !platform || rawId == null) return;
    let off = false;
    setLoading(true); setError(null);
    fetch(`/api/event/${encodeURIComponent(platform)}/${encodeURIComponent(rawId)}`)
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((j) => { if (!off) setData(j); })
      .catch((e) => { if (!off) setError(e.message); })
      .finally(() => { if (!off) setLoading(false); });
    return () => { off = true; };
  }, [platform, rawId, enabled]);
  return { data, loading, error };
}

// ───────────────────────────── event table ─────────────────────────────

function EventRow({ e, alt }) {
  const ui = React.useContext(UI);
  const cut = pctCut(e.before, e.after);
  const open = ui.expandedEventId === e.id;
  const det = useEventDetail(e.platform, e.rawId, open);

  const onProgram = (ev) => { ev.stopPropagation(); ui.openProgram(e.platform, e.slug); };
  const onToggle = () => ui.expandEvent(open ? null : e.id);

  // Polarity drives row coloring (left border + DETAIL cell). hostility and
  // friendliness scores drive emphasis within each polarity.
  const polC = polColor(e.polarity);
  const detailColor =
    e.polarity === 'hostile'  ? (e.hostility  >= 3 ? BB_RED   : e.hostility  >= 1 ? BB_AMBER : BB_DIM) :
    e.polarity === 'friendly' ? (e.friendliness >= 3 ? BB_GREEN : e.friendliness >= 1 ? BB_GREEN : BB_DIM) :
    e.polarity === 'mixed'    ? BB_AMBER :
    BB_DIM;
  const detailBold = e.hostility >= 3 || e.friendliness >= 3;
  return (
    <>
      <tr onClick={onToggle} style={{
        background: open ? '#1a1505' : (alt ? '#0d0d0d' : BB_PANEL),
        cursor: 'pointer',
        borderLeft: open ? `3px solid ${BB_AMBER}` : `3px solid ${polC === BB_DIM ? 'transparent' : polC}`,
      }}>
        <td style={{ padding: '10px 12px', color: BB_DIM, whiteSpace: 'nowrap' }}>
          <span style={{ display: 'inline-block', width: 10, color: BB_AMBER, marginRight: 4 }}>{open ? '▾' : '▸'}</span>
          {bbDate(e.ts)} {bbTime(e.ts)}
        </td>
        <td style={{ padding: '10px 12px', color: BB_AMBER, fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={e.slug}>
          <a onClick={onProgram} style={{ color: BB_AMBER, textDecoration: 'underline', textDecorationStyle: 'dotted', textUnderlineOffset: 3, cursor: 'pointer' }}>
            {e.program}
          </a>
        </td>
        <td style={{ padding: '10px 12px', color: BB_DIM, fontSize: 10 }}>{e.platform === 'immunefi' ? 'IMNF' : 'HKNP'}</td>
        <td style={{ padding: '10px 12px' }}>
          <span style={{
            color: e.severity === 'critical' ? (e.polarity === 'friendly' ? BB_GREEN : BB_RED)
              : e.severity === 'high' ? BB_AMBER : BB_DIM,
            fontWeight: 700, fontSize: 10, letterSpacing: 1,
          }}>{(e.severity || '').slice(0, 4).toUpperCase()}</span>
        </td>
        <td style={{ padding: '10px 12px', color: polC === BB_DIM ? BB_TEXT : polC, fontWeight: 600, whiteSpace: 'nowrap' }}>{BB_EVENT_LABEL[e.type] || e.type.toUpperCase()}</td>
        <td style={{ padding: '10px 12px', color: BB_DIM, textDecoration: e.before ? 'line-through' : 'none', whiteSpace: 'nowrap' }}>{e.before ? fmtUSD(e.before) : '—'}</td>
        <td style={{ padding: '10px 12px', color: e.after === 0 ? BB_RED : BB_TEXT, fontWeight: 700, whiteSpace: 'nowrap' }}>
          {e.after == null ? '—' : (e.after === 0 ? '$0' : fmtUSD(e.after))}
        </td>
        <td style={{ padding: '10px 12px', color: cut == null ? BB_DIM : (cut > 0 ? BB_RED : BB_GREEN), fontWeight: 700, whiteSpace: 'nowrap' }}>
          {cut == null ? '—' : (cut > 0 ? '−' + cut + '%' : '+' + Math.abs(cut) + '%')}
        </td>
        <td style={{
          padding: '10px 12px',
          color: e.topImpact ? detailColor : BB_DIM,
          fontSize: 11, lineHeight: 1.4,
          fontWeight: detailBold ? 700 : 500,
          whiteSpace: 'normal', overflowWrap: 'anywhere',
        }} title={e.topImpact || e.summary}>
          {e.topImpact || e.summary || '—'}
        </td>
      </tr>
      {open && (
        <tr style={{ background: '#000' }}>
          <td colSpan={9} style={{ padding: 0, borderBottom: `1px solid ${BB_AMBER}` }}>
            <DiffBlock detail={det.data && det.data.detail} loading={det.loading} error={det.error} />
          </td>
        </tr>
      )}
    </>
  );
}

function BBEventTable({ events }) {
  const [filter, setFilter] = React.useState('all');
  const [limit, setLimit] = React.useState(100);
  const filtered = React.useMemo(() => {
    let out = events || [];
    if (filter === 'slash')        out = out.filter((e) => e.type === 'slash' || e.type === 'close');
    else if (filter === 'raise')   out = out.filter((e) => e.type === 'raise' || e.type === 'expand' || e.type === 'added');
    else if (filter === 'rescope') out = out.filter((e) => e.type === 'rescope');
    else if (filter === 'immunefi')    out = out.filter((e) => e.platform === 'immunefi');
    else if (filter === 'hackenproof') out = out.filter((e) => e.platform === 'hackenproof');
    return out;
  }, [events, filter]);

  const Btn = ({ id, label }) => (
    <button onClick={() => setFilter(id)} style={{
      background: filter === id ? BB_AMBER : 'transparent',
      color: filter === id ? '#000' : BB_DIM,
      border: `1px solid ${filter === id ? BB_AMBER : BB_BORDER}`,
      fontFamily: BB_FONT, fontSize: 10, letterSpacing: 1, padding: '4px 10px',
      cursor: 'pointer', textTransform: 'uppercase',
    }}>{label}</button>
  );

  return (
    <div style={{ background: BB_PANEL, borderBottom: `1px solid ${BB_BORDER}` }}>
      <div style={{
        padding: '10px 18px', borderBottom: `1px solid ${BB_BORDER}`,
        fontFamily: BB_FONT, fontSize: 12, color: BB_AMBER, letterSpacing: 1.5,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap',
      }}>
        <span>◆ FEED · {filtered.length.toLocaleString()} EVENTS · click row for diff · click name for program</span>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          <Btn id="all" label="ALL" />
          <Btn id="slash" label="SLASH+CLOSE" />
          <Btn id="raise" label="RAISE+EXPAND" />
          <Btn id="rescope" label="RESCOPE" />
          <Btn id="immunefi" label="IMNF" />
          <Btn id="hackenproof" label="HKNP" />
        </div>
      </div>
      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', minWidth: 1100, borderCollapse: 'collapse', fontFamily: BB_FONT, fontSize: 12, color: BB_TEXT, tableLayout: 'fixed' }}>
          <colgroup>
            <col style={{ width: 140 }} />{/* TIME    */}
            <col style={{ width: 200 }} />{/* PROGRAM */}
            <col style={{ width: 60 }} /> {/* SRC     */}
            <col style={{ width: 70 }} /> {/* SEV     */}
            <col style={{ width: 150 }} />{/* EVENT   */}
            <col style={{ width: 90 }} /> {/* WAS     */}
            <col style={{ width: 90 }} /> {/* NOW     */}
            <col style={{ width: 70 }} /> {/* Δ%      */}
            <col />                      {/* DETAIL — takes the rest */}
          </colgroup>
          <thead>
            <tr style={{ background: '#000' }}>
              {['TIME', 'PROGRAM', 'SRC', 'SEV', 'EVENT', 'WAS', 'NOW', 'Δ%', 'DETAIL'].map((h) => (
                <th key={h} style={{
                  textAlign: 'left', padding: '8px 12px', color: BB_DIM,
                  fontWeight: 500, letterSpacing: 1, fontSize: 10,
                  borderBottom: `1px solid ${BB_BORDER}`,
                }}>{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {filtered.slice(0, limit).map((e, i) => (
              <EventRow key={e.id} e={e} alt={i % 2 === 1} />
            ))}
          </tbody>
        </table>
      </div>
      {filtered.length > limit && (
        <div style={{ padding: 12, textAlign: 'center', borderTop: `1px solid ${BB_BORDER}` }}>
          <button onClick={() => setLimit((l) => l + 100)} style={{
            background: 'transparent', color: BB_AMBER, border: `1px solid ${BB_AMBER}`,
            fontFamily: BB_FONT, fontSize: 11, letterSpacing: 1, padding: '6px 16px', cursor: 'pointer',
          }}>LOAD +100 · {(filtered.length - limit).toLocaleString()} REMAINING</button>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────── top movers ─────────────────────────────

function MoverCard({ rank, e, accent, deltaText }) {
  const ui = React.useContext(UI);
  // Color the rank badge gold for #1, silver for #2, bronze for #3, dim for rest.
  const rankColor =
    rank === 1 ? '#ffd700' :
    rank === 2 ? '#c0c0c0' :
    rank === 3 ? '#cd7f32' :
    BB_DIM;
  return (
    <div
      onClick={() => ui.openProgram(e.platform, e.slug)}
      style={{
        padding: 14, border: `1px solid ${BB_BORDER}`, background: '#000',
        fontFamily: BB_FONT, fontSize: 12, color: BB_TEXT, lineHeight: 1.45,
        cursor: 'pointer', borderLeft: `3px solid ${accent}`,
        display: 'flex', alignItems: 'flex-start', gap: 12,
      }}
      onMouseEnter={(ev) => { ev.currentTarget.style.borderColor = BB_AMBER; ev.currentTarget.style.borderLeftColor = accent; }}
      onMouseLeave={(ev) => { ev.currentTarget.style.borderColor = BB_BORDER; ev.currentTarget.style.borderLeftColor = accent; }}>
      <div style={{
        flex: '0 0 36px', textAlign: 'right',
        fontFamily: BB_FONT, fontSize: 18, fontWeight: 700, color: rankColor,
        letterSpacing: 0, lineHeight: 1.1,
      }}>
        #{rank}
      </div>
      <div style={{ flex: '1 1 auto', minWidth: 0 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6, gap: 8 }}>
          <span style={{ color: BB_AMBER, fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={e.slug}>{e.program}</span>
          <span style={{ color: BB_DIM, fontSize: 10, flex: '0 0 auto' }}>{e.platform === 'immunefi' ? 'IMNF' : 'HKNP'}</span>
        </div>
        <div style={{ color: accent, fontSize: 14, fontWeight: 700, marginBottom: 4, overflowWrap: 'anywhere' }}>
          {deltaText}
        </div>
        <div style={{ color: BB_DIM, fontSize: 10 }}>{bbDate(e.ts)} · {BB_EVENT_LABEL[e.type]}</div>
      </div>
    </div>
  );
}

function BBTopMovers({ events }) {
  // A program is "currently closed" if its newest event (events are sorted
  // newest-first by the server) is `close`. A re-opened program (close →
  // added) is NOT considered closed; the latest `added` rules.
  const closedSlugs = React.useMemo(() => {
    const seen = new Map();
    for (const e of (events || [])) {
      const key = `${e.platform}:${e.slug}`;
      if (!seen.has(key)) seen.set(key, e.type);
    }
    const out = new Set();
    for (const [k, t] of seen) if (t === 'close') out.add(k);
    return out;
  }, [events]);

  // Take the largest event per program (already sorted by metric desc), so a
  // program with multiple raises only appears once with its biggest.
  function dedupeByProgram(list) {
    const seen = new Set();
    const out = [];
    for (const e of list) {
      const k = `${e.platform}:${e.slug}`;
      if (seen.has(k)) continue;
      seen.add(k);
      out.push(e);
    }
    return out;
  }

  const cuts = React.useMemo(() => {
    const ranked = (events || [])
      .filter((e) => (e.type === 'slash' || e.type === 'close') && e.before > 0 && e.after != null)
      .map((e) => ({ ...e, _delta: e.before - e.after, _pct: pctCut(e.before, e.after) }))
      .sort((a, b) => b._delta - a._delta);
    return dedupeByProgram(ranked).slice(0, 10);
  }, [events]);

  const raises = React.useMemo(() => {
    const ranked = (events || [])
      .filter((e) =>
        e.type === 'raise' && e.before != null && e.after != null && e.after > e.before)
      // Drop programs that have since been closed — a raise on a now-dead
      // program isn't a current opportunity for hunters.
      .filter((e) => !closedSlugs.has(`${e.platform}:${e.slug}`))
      .map((e) => ({ ...e, _delta: e.after - e.before, _pct: pctCut(e.before, e.after) }))
      .sort((a, b) => b._delta - a._delta);
    return dedupeByProgram(ranked).slice(0, 10);
  }, [events, closedSlugs]);

  const expands = React.useMemo(() => {
    const ranked = (events || [])
      .filter((e) => e.type === 'expand')
      .filter((e) => !closedSlugs.has(`${e.platform}:${e.slug}`))
      .sort((a, b) => (b.friendliness || 0) - (a.friendliness || 0));
    return dedupeByProgram(ranked).slice(0, 10);
  }, [events, closedSlugs]);

  if (!cuts.length && !raises.length && !expands.length) return null;

  // Each section header gets a `· N excluded (closed)` annotation when
  // closed-slug filtering removed something, so users can see the filter is
  // working without having to dig.
  function rankList(items, header, accent, hint, deltaFn) {
    if (!items.length) return null;
    return (
      <div>
        <div style={{
          fontFamily: BB_FONT, fontSize: 12, color: accent,
          letterSpacing: 1.5, marginBottom: 12,
          display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 8,
        }}>
          <span>{header} · TOP {items.length} · click to drill in</span>
          <span style={{ color: BB_DIM, fontSize: 10 }}>{hint}</span>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 12 }}>
          {items.map((e, i) => (
            <MoverCard key={e.id} rank={i + 1} e={e} accent={accent} deltaText={deltaFn(e)} />
          ))}
        </div>
      </div>
    );
  }

  return (
    <div style={{ background: BB_PANEL, padding: 18, borderBottom: `1px solid ${BB_BORDER}`, display: 'grid', gap: 24, gridTemplateColumns: '1fr' }}>
      {rankList(cuts, '▼ TOP CUTS', BB_RED,
        `ranked by $ slashed · sorted desc`,
        (e) => <>{fmtUSD(e.before)} → {fmtUSD(e.after)} <span style={{ marginLeft: 8 }}>−{e._pct}%</span></>)}
      {rankList(raises, '▲ TOP RAISES', BB_GREEN,
        `currently-active programs only · ranked by $ raised`,
        (e) => <>{fmtUSD(e.before)} → {fmtUSD(e.after)} <span style={{ marginLeft: 8 }}>+{Math.min(99999, e._pct || 0)}%</span></>)}
      {rankList(expands, '⊕ TOP EXPANSIONS', BB_GREEN,
        `currently-active programs only · ranked by friendliness score`,
        (e) => e.topImpact || '(scope expanded)')}
    </div>
  );
}

// ───────────────────────── program overlay ─────────────────────────

// Step chart of max_bounty over time. Renders one horizontal step per
// chronologically-ordered change. Y axis = max_bounty (USD), x axis = ts.
// Always anchored to the program's currentMaxBounty as the trailing point.
function BountyHistoryChart({ history, currentMaxBounty, programFirstSeen, programLastSeen }) {
  const points = React.useMemo(() => {
    const pts = [];
    if (!history.length) {
      if (currentMaxBounty != null && programFirstSeen) {
        pts.push({ t: new Date(programFirstSeen).getTime(), v: currentMaxBounty });
        pts.push({ t: new Date(programLastSeen || programFirstSeen).getTime(), v: currentMaxBounty });
      }
      return pts;
    }
    // Pre-history: programFirstSeen at the original `before` value.
    const first = history[0];
    pts.push({ t: programFirstSeen ? new Date(programFirstSeen).getTime() : new Date(first.ts).getTime() - 86400000, v: first.before });
    for (const h of history) {
      const t = new Date(h.ts).getTime();
      pts.push({ t, v: h.before });
      pts.push({ t, v: h.after });
    }
    // Final: current value (if program still tracked) at lastSeen, else
    // freeze on the last after.
    const lastAfter = history[history.length - 1].after;
    const finalV = currentMaxBounty != null ? currentMaxBounty : lastAfter;
    pts.push({ t: programLastSeen ? new Date(programLastSeen).getTime() : new Date(history[history.length - 1].ts).getTime(), v: finalV });
    return pts;
  }, [history, currentMaxBounty, programFirstSeen, programLastSeen]);

  if (points.length < 2) {
    return (
      <div style={{ padding: 18, color: BB_DIM, fontFamily: BB_FONT, fontSize: 12 }}>
        ◆ BOUNTY HISTORY — insufficient data (program has no recorded maxBounty changes)
      </div>
    );
  }

  const W = 100, H = 38;
  const tMin = points[0].t;
  const tMax = points[points.length - 1].t;
  const tSpan = Math.max(1, tMax - tMin);
  const vMax = Math.max(1, ...points.map((p) => p.v || 0));
  const xy = (p) => ({ x: ((p.t - tMin) / tSpan) * W, y: H - ((p.v || 0) / vMax) * H });
  const poly = points.map(xy).map((p) => `${p.x},${p.y}`).join(' ');
  const fmtDay = (t) => new Date(t).toISOString().slice(0, 10);

  return (
    <div style={{ padding: 18, background: '#000', borderTop: `1px solid ${BB_BORDER}`, borderBottom: `1px solid ${BB_BORDER}` }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
        <div style={{ fontFamily: BB_FONT, fontSize: 12, color: BB_AMBER, letterSpacing: 1.5 }}>
          ◆ MAX BOUNTY · {fmtDay(tMin)} → {fmtDay(tMax)}
        </div>
        <div style={{ fontFamily: BB_FONT, fontSize: 11, color: BB_DIM }}>
          current: <span style={{ color: BB_AMBER, fontWeight: 700 }}>{fmtUSD(currentMaxBounty)}</span>
          {' · '}peak: <span style={{ color: BB_AMBER, fontWeight: 700 }}>{fmtUSD(vMax)}</span>
        </div>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: '100%', height: 180, background: '#0a0a0a', border: `1px solid ${BB_BORDER}` }}>
        {[0, 25, 50, 75, 100].map((g) => (
          <line key={g} x1={0} x2={W} y1={H - (g / 100) * H} y2={H - (g / 100) * H} stroke={BB_BORDER} strokeWidth={0.15} />
        ))}
        <polyline points={poly} fill="none" stroke={BB_AMBER} strokeWidth={0.4} />
        {points.map((p, i) => {
          const xyp = xy(p);
          return <circle key={i} cx={xyp.x} cy={xyp.y} r={0.6} fill={BB_AMBER} />;
        })}
      </svg>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: BB_FONT, fontSize: 10, color: BB_DIM, marginTop: 6 }}>
        <span>{fmtDay(tMin)}</span>
        <span>{fmtDay(tMin + tSpan * 0.5)}</span>
        <span>{fmtDay(tMax)}</span>
      </div>
    </div>
  );
}

function ProgramEventRow({ e, alt }) {
  const [open, setOpen] = React.useState(false);
  const cut = pctCut(e.before, e.after);
  const polC = polColor(e.polarity);
  const detailColor =
    e.polarity === 'hostile'  ? (e.hostility    >= 3 ? BB_RED   : e.hostility    >= 1 ? BB_AMBER : BB_DIM) :
    e.polarity === 'friendly' ? (e.friendliness >= 3 ? BB_GREEN : e.friendliness >= 1 ? BB_GREEN : BB_DIM) :
    e.polarity === 'mixed'    ? BB_AMBER : BB_DIM;
  const detailBold = (e.hostility || 0) >= 3 || (e.friendliness || 0) >= 3;
  return (
    <>
      <tr onClick={() => setOpen((o) => !o)} style={{
        background: open ? '#1a1505' : (alt ? '#0d0d0d' : BB_PANEL),
        cursor: 'pointer',
        borderLeft: open ? `3px solid ${BB_AMBER}` : `3px solid ${polC === BB_DIM ? 'transparent' : polC}`,
      }}>
        <td style={{ padding: '10px 12px', color: BB_DIM, whiteSpace: 'nowrap' }}>
          <span style={{ color: BB_AMBER, marginRight: 4 }}>{open ? '▾' : '▸'}</span>
          {bbDate(e.ts)} {bbTime(e.ts)}
        </td>
        <td style={{ padding: '10px 12px' }}>
          <span style={{
            color: e.severity === 'critical' ? (e.polarity === 'friendly' ? BB_GREEN : BB_RED)
              : e.severity === 'high' ? BB_AMBER : BB_DIM,
            fontWeight: 700, fontSize: 10, letterSpacing: 1,
          }}>{(e.severity || '').slice(0, 4).toUpperCase()}</span>
        </td>
        <td style={{ padding: '10px 12px', color: polC === BB_DIM ? BB_TEXT : polC, fontWeight: 600, whiteSpace: 'nowrap' }}>{BB_EVENT_LABEL[e.type] || e.type.toUpperCase()}</td>
        <td style={{ padding: '10px 12px', color: BB_DIM, textDecoration: e.before ? 'line-through' : 'none', whiteSpace: 'nowrap' }}>{e.before ? fmtUSD(e.before) : '—'}</td>
        <td style={{ padding: '10px 12px', color: e.after === 0 ? BB_RED : BB_TEXT, fontWeight: 700, whiteSpace: 'nowrap' }}>{e.after == null ? '—' : (e.after === 0 ? '$0' : fmtUSD(e.after))}</td>
        <td style={{ padding: '10px 12px', color: cut == null ? BB_DIM : (cut > 0 ? BB_RED : BB_GREEN), fontWeight: 700, whiteSpace: 'nowrap' }}>{cut == null ? '—' : (cut > 0 ? '−' + cut + '%' : '+' + Math.abs(cut) + '%')}</td>
        <td style={{
          padding: '10px 12px',
          color: e.topImpact ? detailColor : BB_DIM,
          fontSize: 11, lineHeight: 1.4,
          fontWeight: detailBold ? 700 : 500,
          whiteSpace: 'normal', overflowWrap: 'anywhere',
        }} title={e.topImpact || e.summary}>{e.topImpact || e.summary || '—'}</td>
      </tr>
      {open && (
        <tr style={{ background: '#000' }}>
          <td colSpan={7} style={{ padding: 0, borderBottom: `1px solid ${BB_AMBER}` }}>
            <DiffBlock detail={e.detail} />
          </td>
        </tr>
      )}
    </>
  );
}

function ProgramOverlay({ platform, slug, onClose }) {
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let off = false;
    setLoading(true); setError(null);
    fetch(`/api/program?platform=${encodeURIComponent(platform)}&slug=${encodeURIComponent(slug)}`)
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((j) => { if (!off) setData(j); })
      .catch((e) => { if (!off) setError(e.message); })
      .finally(() => { if (!off) setLoading(false); });
    return () => { off = true; };
  }, [platform, slug]);

  // Esc closes; lock body scroll while open.
  React.useEffect(() => {
    const k = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', k);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.removeEventListener('keydown', k); document.body.style.overflow = prev; };
  }, [onClose]);

  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 100,
      background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(6px)',
      display: 'flex', alignItems: 'stretch', justifyContent: 'center', padding: 24,
      fontFamily: BB_FONT, color: BB_TEXT,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: 1280, background: BB_BG,
        border: `1px solid ${BB_AMBER}`, display: 'flex', flexDirection: 'column',
        maxHeight: '100%', overflow: 'hidden',
      }}>
        {/* Header */}
        <div style={{
          padding: '14px 20px', borderBottom: `1px solid ${BB_AMBER}`, background: '#000',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
        }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, minWidth: 0 }}>
            <span style={{ color: BB_AMBER, fontWeight: 700, letterSpacing: 2, fontSize: 13 }}>◆ PROGRAM</span>
            <span style={{ color: BB_TEXT, fontWeight: 700, fontSize: 16, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
              {data ? data.title : slug}
            </span>
            <span style={{ color: BB_DIM, fontSize: 11 }}>
              {platform === 'immunefi' ? 'IMNF' : 'HKNP'} · {slug}
            </span>
          </div>
          <button onClick={onClose} style={{
            background: 'transparent', color: BB_AMBER, border: `1px solid ${BB_AMBER}`,
            fontFamily: BB_FONT, fontSize: 11, letterSpacing: 1, padding: '4px 12px', cursor: 'pointer',
          }}>CLOSE [ESC]</button>
        </div>

        <div style={{ overflow: 'auto', flex: 1 }}>
          {loading && <div style={{ padding: 40, color: BB_DIM, fontSize: 12 }}>loading…</div>}
          {error && <div style={{ padding: 40, color: BB_RED, fontSize: 12 }}>error: {error}</div>}
          {data && (
            <>
              {/* metadata row */}
              <div style={{ display: 'flex', flexWrap: 'wrap', background: BB_PANEL, borderBottom: `1px solid ${BB_BORDER}` }}>
                <BBStat label="CURRENT MAX" value={fmtUSD(data.currentMaxBounty)} sub={data.removed ? 'program removed' : 'live'} subColor={data.removed ? BB_RED : BB_GREEN} />
                <BBStat label="EVENTS" value={data.eventCount.toLocaleString()} sub="classified, noise-filtered" subColor={BB_DIM} />
                <BBStat label="FIRST SEEN" value={data.firstSeen ? data.firstSeen.slice(0, 10) : '—'} sub="monitor onboarding" subColor={BB_DIM} />
                <BBStat label="LAST CHANGED" value={data.lastChanged ? data.lastChanged.slice(0, 10) : '—'} sub="most recent diff" subColor={BB_DIM} />
              </div>

              <BountyHistoryChart
                history={data.bountyHistory}
                currentMaxBounty={data.currentMaxBounty}
                programFirstSeen={data.firstSeen}
                programLastSeen={data.lastSeen}
              />

              {/* event list (chronological, newest first) */}
              <div style={{ padding: '10px 18px', fontFamily: BB_FONT, fontSize: 12, color: BB_AMBER, letterSpacing: 1.5, borderBottom: `1px solid ${BB_BORDER}` }}>
                ◆ EVENT TIMELINE · {data.eventCount} events · click row for diff
              </div>
              <div style={{ overflowX: 'auto' }}>
                <table style={{ width: '100%', minWidth: 900, borderCollapse: 'collapse', fontFamily: BB_FONT, fontSize: 12, color: BB_TEXT, tableLayout: 'fixed' }}>
                  <colgroup>
                    <col style={{ width: 140 }} />{/* TIME   */}
                    <col style={{ width: 70 }} /> {/* SEV    */}
                    <col style={{ width: 160 }} />{/* EVENT  */}
                    <col style={{ width: 90 }} /> {/* WAS    */}
                    <col style={{ width: 90 }} /> {/* NOW    */}
                    <col style={{ width: 70 }} /> {/* Δ%     */}
                    <col />                       {/* DETAIL */}
                  </colgroup>
                  <thead>
                    <tr style={{ background: '#000' }}>
                      {['TIME', 'SEV', 'EVENT', 'WAS', 'NOW', 'Δ%', 'DETAIL'].map((h) => (
                        <th key={h} style={{ textAlign: 'left', padding: '8px 12px', color: BB_DIM, fontWeight: 500, letterSpacing: 1, fontSize: 10, borderBottom: `1px solid ${BB_BORDER}` }}>{h}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody>
                    {[...data.events].reverse().map((e, i) => (
                      <ProgramEventRow key={e.id} e={e} alt={i % 2 === 1} />
                    ))}
                  </tbody>
                </table>
              </div>
            </>
          )}
        </div>
      </div>
    </div>,
    document.body,
  );
}

// ───────────────────────────── footer / root ─────────────────────────────

function BBFooter({ error }) {
  return (
    <div style={{
      padding: '12px 20px', background: '#000', borderTop: `1px solid ${BB_AMBER}`,
      fontFamily: BB_FONT, fontSize: 10, color: BB_DIM, display: 'flex',
      justifyContent: 'space-between', letterSpacing: 1, gap: 12, flexWrap: 'wrap',
    }}>
      <span>poll: 30s · data: immumon + hackenproofmon · factual, unedited</span>
      <span style={{ color: error ? BB_RED : BB_DIM }}>
        {error ? `error: ${error}` : 'connected'}
      </span>
    </div>
  );
}

function BountyBloomberg() {
  const { stats, events, health, error } = useLiveData();
  const [expandedEventId, setExpandedEventId] = React.useState(null);
  const [openProg, setOpenProg] = React.useState(null);

  const api = React.useMemo(() => ({
    expandedEventId,
    expandEvent: (id) => setExpandedEventId(id),
    openProgram: (platform, slug) => setOpenProg({ platform, slug }),
  }), [expandedEventId]);

  return (
    <UI.Provider value={api}>
      <div style={{ background: BB_BG, minHeight: '100%' }}>
        <BBHeader health={health} />
        <BBTicker events={events} />
        <BBStatRow stats={stats} />
        <BBChart events={events} />
        <BBEventTable events={events} />
        <BBTopMovers events={events} />
        <BBFooter error={error} />
      </div>
      {openProg && (
        <ProgramOverlay
          platform={openProg.platform}
          slug={openProg.slug}
          onClose={() => setOpenProg(null)}
        />
      )}
    </UI.Provider>
  );
}

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