// ============================================================
// 18% CLUB · TRADE DESK · v6
// chart.jsx — Chart, scrubber, selected trade bar, trace cascade
// Exports to window so app.jsx can use them.
// ============================================================

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

// ---------- formatters ----------
function fmtUSD(n, { signed = false, abs = false } = {}) {
  if (n === null || n === undefined) return '—';
  const v = abs ? Math.abs(n) : n;
  const s = Math.abs(v).toLocaleString(undefined, { maximumFractionDigits: 0 });
  const sign = signed ? (n > 0 ? '+' : (n < 0 ? '-' : '')) : (n < 0 && !abs ? '-' : '');
  return sign + '$' + (abs ? s : Math.abs(v).toLocaleString(undefined, { maximumFractionDigits: 0 }));
}
function fmtPrice(n) {
  if (n === null || n === undefined) return '—';
  return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function fmtShort(dateStr) {
  // 2026-05-18 → 05-18
  return dateStr ? dateStr.slice(5) : '';
}
function fmtHuman(dateStr) {
  // 2026-05-18 → 18 May 2026
  if (!dateStr) return '';
  const [y, m, d] = dateStr.split('-');
  const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  return `${parseInt(d,10)} ${months[parseInt(m,10)-1]} ${y}`;
}

// ---------- light count-up hook (local to chart.jsx) ----------
function useChartCountUp(target, durationMs = 700, deps = []) {
  const [v, setV] = useState(target);
  useEffect(() => {
    const t0 = performance.now();
    const start = 0;
    let raf;
    const tick = (now) => {
      const p = Math.min(1, (now - t0) / durationMs);
      const eased = 1 - Math.pow(1 - p, 3);
      setV(start + (target - start) * eased);
      if (p < 1) raf = requestAnimationFrame(tick);
      else setV(target);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [target, ...deps]);
  return v;
}

// ---------- module metadata for the SBS hub ----------
const MODULE_INFO = {
  trend:     { name: '§3 Trend',      desc: 'Direction of QQQ vs key moving averages. Reads +1 when above 50EMA on 2 consecutive sessions.' },
  structure: { name: '§4 Structure',  desc: 'Weekly higher-highs intact AND no gap-down fill. Reads +1 when structure is bullish.' },
  deriv:     { name: '§5 Derivatives',desc: 'Call wall / put wall positioning and net gamma. Reads +1 when positioning is supportive.' },
  macro:     { name: '§6 Macro',      desc: 'DXY weakness + 10Y stable = risk-on backdrop. Reads +1 when macro is supportive.' },
  breadth:   { name: '§7 Breadth',    desc: 'Advance-decline ratio and new highs vs new lows. Reads +1 when breadth is positive.' },
  leader:    { name: '§8 Leadership', desc: 'Mag7 / large-cap leadership health. Reads +1 when leaders lead; 0 when split.' },
  sentiment: { name: '§9 Sentiment',  desc: 'AAII bullish % and VIX9D positioning. Reads +1 when sentiment is constructive.' }
};

// ============================================================
// CHART CARD — uses TradingView's Lightweight Charts library
// Real candlesticks, crosshair, right-edge price labels,
// native markers + horizontal price lines for entry/stop/target/exit.
// ============================================================
function ChartCard({ chart, trades = [], trade, currentTradeId, onSelect, hasTrade, chartType = 'candle' }) {
  const containerRef = useRef(null);
  const chartRef = useRef(null);
  const seriesRef = useRef(null);
  const priceLinesRef = useRef([]);
  const onSelectRef = useRef(onSelect);
  const hasTradeRef = useRef(hasTrade);
  onSelectRef.current = onSelect;
  hasTradeRef.current = hasTrade;

  // ─── New interaction state (added for v6.2 animation pass) ─────────
  const [replay, setReplay] = useState({ active: false, day: null, pnl: 0, step: 0, total: 0 });
  const [hover, setHover] = useState(null);
  const [autoFramed, setAutoFramed] = useState(false);
  const [bandCoords, setBandCoords] = useState(null);
  // v6.7: outcome filter for the 30-day view. 'all' | 'win' | 'loss' | 'open'.
  // When auto-framed on a single trade the filter is moot (only the selected
  // trade's markers show); when zoomed out, the filter narrows the marker set
  // and the chip's count gives at-a-glance pipeline awareness. Replaces the
  // separate "Show all 30D" button + legend strip + scrubber.
  const [tradeFilter, setTradeFilter] = useState('all');
  const tradeFilterRef = useRef(tradeFilter);
  tradeFilterRef.current = tradeFilter;
  // v6.8: trade picker dropdown — lets the user jump directly to any trade
  // from a list, not just by tapping a chart marker. Filtered by the active
  // outcome chip so picker + filters stay consistent.
  const [pickerOpen, setPickerOpen] = useState(false);
  const pickerRef = useRef(null);
  useEffect(() => {
    if (!pickerOpen) return;
    const onDocClick = (e) => {
      if (pickerRef.current && !pickerRef.current.contains(e.target)) setPickerOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setPickerOpen(false); };
    document.addEventListener('mousedown', onDocClick);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDocClick);
      document.removeEventListener('keydown', onKey);
    };
  }, [pickerOpen]);
  const replayTimerRef = useRef(null);

  // (replayLineRef removed — replay now uses a DOM vertical line + dot instead
  // of a horizontal price-line, so it reads as "current day" not "current price".)

  // Refs to current trade/currentTradeId/marks so the chart-create effect can pull fresh values
  // without depending on them (re-creating the chart on every trade switch would be wasteful).
  const tradeRef = useRef(trade);
  const currentTradeIdRef = useRef(currentTradeId);
  const marksRef = useRef(chart.marks);
  tradeRef.current = trade;
  currentTradeIdRef.current = currentTradeId;
  marksRef.current = chart.marks;

  // Pure function: push markers + price lines for whatever the refs/args say is current.
  // Called both at chart-create time and on every trade-switch.
  //
  // v6.6: marker display depends on whether the chart is auto-framed on a single
  // trade (showAll=false → only that trade's markers, large + labeled) or zoomed
  // out to the full 30-day view (showAll=true → all trades visible, selected one
  // emphasized) — so "Show all 30D" actually reveals other trades to pick.
  const applyMarkersAndLines = (series, marks, currentId, t, showAll = false, filter = 'all') => {
    if (!series) return;

    // Wipe existing price lines first
    priceLinesRef.current.forEach(pl => { try { series.removePriceLine(pl); } catch (_) {} });
    priceLinesRef.current = [];

    // Markers. Two-stage filtering:
    //   1. showAll=false → only the selected trade's markers (auto-framed view)
    //   2. showAll=true  → all trades, then narrowed by `filter` outcome
    let source = showAll ? marks : marks.filter(mk => mk.id === currentId);
    if (showAll && filter !== 'all') {
      // Build set of trade ids matching the outcome filter, then keep all
      // markers (entry+exit) for those trades so price-lines + arrows pair up.
      const matchingIds = new Set();
      marks.forEach(mk => {
        if (filter === 'win'  && mk.kind === 'win')  matchingIds.add(mk.id);
        if (filter === 'loss' && mk.kind === 'loss') matchingIds.add(mk.id);
        if (filter === 'open' && mk.kind === 'open') matchingIds.add(mk.id);
      });
      // Always keep the currently selected trade's markers regardless of filter,
      // so the dashed entry/exit lines never disappear under the user's cursor.
      if (currentId) matchingIds.add(currentId);
      source = marks.filter(mk => matchingIds.has(mk.id));
    }
    const markerArr = source
      .slice()
      .map(mk => {
        // In FOCUSED mode (showAll=false) the selected trade gets the big-size
        // + labeled treatment because it's the subject. In FILTER mode (showAll
        // = true) we treat every marker equally — no labels, uniform size —
        // because no single trade is the subject; otherwise the previously-
        // selected trade would visually shout louder than its filtered peers.
        const focused = !showAll;
        const isSel = focused && mk.id === currentId;
        let color, shape, position, text = '';
        const size = focused ? (isSel ? 1.4 : 0.8) : 1;
        if (mk.kind === 'entry') {
          color = '#AB6A57'; shape = 'arrowUp';   position = 'belowBar';
          if (isSel) text = `BUY ${fmtPrice(mk.price)}`;
        } else if (mk.kind === 'win') {
          color = '#606C5A'; shape = 'arrowDown'; position = 'aboveBar';
          if (isSel) text = 'WIN';
        } else if (mk.kind === 'loss') {
          color = '#AB6A57'; shape = 'square';    position = 'aboveBar';
          if (isSel) text = 'LOSS';
        } else if (mk.kind === 'open') {
          color = '#DCB482'; shape = 'circle';    position = 'belowBar';
          if (isSel) text = 'OPEN';
        }
        return { time: mk.date, position, color, shape, text, size };
      })
      .sort((a, b) => a.time.localeCompare(b.time));
    series.setMarkers(markerArr);

    // Price lines (entry + exit only) for the selected trade.
    // v6.5: stop and target removed — keep the chart simple, focus on what
    // actually happened. Risk/opportunity numbers live in the trace cascade.
    // v6.8: only drawn in FOCUSED mode (showAll=false). When the user is in a
    // filter view ("All / Wins / Losses / Open") no specific trade is the
    // subject, so entry/exit lines + band would lie about which one matters.
    if (t && !showAll) {
      const LW = window.LightweightCharts;
      const Dashed = (LW && LW.LineStyle && LW.LineStyle.Dashed) || 2;

      priceLinesRef.current.push(series.createPriceLine({
        price: t.entry_price, color: '#AB6A57', lineWidth: 1, lineStyle: Dashed,
        axisLabelVisible: true, title: `entry · ${t.plan?.rulebook_section || ''}`.trim(),
      }));

      if (t.exit_price != null) {
        priceLinesRef.current.push(series.createPriceLine({
          price: t.exit_price,
          color: t.result === 'win' ? '#606C5A' : '#AB6A57',
          lineWidth: 1, lineStyle: Dashed,
          axisLabelVisible: true, title: `exit · ${t.result}`,
        }));
      }
    }
  };

  // ──────────────────────────────────────────────────────────────────
  // Helpers used by replay, auto-frame, and hold-band overlay.
  // ──────────────────────────────────────────────────────────────────
  const findTradeWindow = (t, marks, ohlc) => {
    if (!t) return null;
    const tMarks = marks.filter(m => m.id === t.id);
    const entry = tMarks.find(m => m.kind === 'entry') || tMarks.find(m => m.kind === 'open');
    const exit  = tMarks.find(m => m.kind === 'win' || m.kind === 'loss');
    if (!entry) return null;
    const exitDate = exit ? exit.date : ohlc[ohlc.length - 1].date;
    return { entry, exit, entryDate: entry.date, exitDate };
  };

  const recomputeBand = () => {
    const chartInst = chartRef.current;
    const t = tradeRef.current;
    if (!chartInst || !t) { setBandCoords(null); return; }
    const win = findTradeWindow(t, marksRef.current, chart.ohlc);
    if (!win) { setBandCoords(null); return; }
    const ts = chartInst.timeScale();
    let x1 = ts.timeToCoordinate(win.entryDate);
    let x2 = ts.timeToCoordinate(win.exitDate);
    if (x1 == null || x2 == null) { setBandCoords(null); return; }
    if (x1 > x2) [x1, x2] = [x2, x1];
    // Colors are derived at render time from replay.pnl (live) or trade.result (static),
    // so the band reflects current profit state — green = in profit, red = in loss.
    setBandCoords({ x1, x2 });
  };

  // ──────────────────────────────────────────────────────────────────
  // Single effect: create chart + series + attach observers + push initial markers.
  // Deps include chartType and chart.ohlc — anything that requires a fresh series.
  // Bug history: previously split into TWO effects; chart re-creation in StrictMode
  // or on App re-render left the second effect's deps unchanged, so the new chart
  // never got a series. One effect = one source of truth.
  // ──────────────────────────────────────────────────────────────────
  useEffect(() => {
    const LW = window.LightweightCharts;
    if (!LW || !containerRef.current) return;

    const width = containerRef.current.clientWidth || 360;
    const chartInst = LW.createChart(containerRef.current, {
      width,
      height: 320,
      autoSize: false,
      layout: {
        background: { type: 'solid', color: '#FFFFFF' },
        textColor: '#5E5E5E',
        fontFamily: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
        fontSize: 10,
        attributionLogo: false,
      },
      rightPriceScale: {
        borderColor: '#DBD3CD',
        scaleMargins: { top: 0.12, bottom: 0.08 },
      },
      timeScale: {
        borderColor: '#DBD3CD',
        timeVisible: false,
        secondsVisible: false,
        fixLeftEdge: true,
        fixRightEdge: true,
        rightOffset: 4,
        barSpacing: 14,
      },
      grid: {
        vertLines: { color: 'rgba(219,211,205,0.45)' },
        horzLines: { color: 'rgba(219,211,205,0.65)' },
      },
      crosshair: {
        mode: LW.CrosshairMode ? LW.CrosshairMode.Normal : 1,
        vertLine: { color: '#917F6C', width: 1, style: 3, labelBackgroundColor: '#213744' },
        horzLine: { color: '#917F6C', width: 1, style: 3, labelBackgroundColor: '#213744' },
      },
      handleScroll: { mouseWheel: false, pressedMouseMove: false, horzTouchDrag: false, vertTouchDrag: false },
      handleScale:  { axisPressedMouseMove: false, mouseWheel: false, pinch: false },
      kineticScroll: { mouse: false, touch: false },
    });
    chartRef.current = chartInst;

    // Build series for the requested chartType
    let series;
    if (chartType === 'line') {
      series = chartInst.addLineSeries({
        color: '#213744', lineWidth: 2, priceLineVisible: false, lastValueVisible: true,
      });
      series.setData(chart.ohlc.map(d => ({ time: d.date, value: d.close })));
    } else if (chartType === 'area') {
      series = chartInst.addAreaSeries({
        lineColor: '#213744', topColor: 'rgba(33,55,68,0.18)', bottomColor: 'rgba(33,55,68,0.0)',
        lineWidth: 2, priceLineVisible: false, lastValueVisible: true,
      });
      series.setData(chart.ohlc.map(d => ({ time: d.date, value: d.close })));
    } else {
      series = chartInst.addCandlestickSeries({
        upColor: '#606C5A', downColor: '#AB6A57',
        borderUpColor: '#3F4937', borderDownColor: '#7D4A3B',
        wickUpColor: '#3F4937', wickDownColor: '#7D4A3B',
        priceLineVisible: false, lastValueVisible: true,
      });
      series.setData(chart.ohlc.map(d => ({
        time: d.date, open: d.open, high: d.high, low: d.low, close: d.close
      })));
    }
    seriesRef.current = series;
    chartInst.timeScale().fitContent();

    // Push initial markers + price lines from refs (covers initial mount AND
    // any subsequent chart re-creation; the dedicated markers-effect below handles
    // trade switches that don't need a chart rebuild).
    applyMarkersAndLines(series, marksRef.current, currentTradeIdRef.current, tradeRef.current, false, tradeFilterRef.current);

    // Click handler — snap to a trade entry on the clicked bar's date
    chartInst.subscribeClick((param) => {
      if (!param.time) return;
      const dateStr = typeof param.time === 'string' ? param.time : null;
      if (!dateStr) return;
      const marks = marksRef.current;
      const entry = marks.find(m => m.date === dateStr && m.kind === 'entry');
      const anyMk = entry || marks.find(m => m.date === dateStr);
      if (anyMk && hasTradeRef.current(anyMk.id)) onSelectRef.current(anyMk.id);
    });

    // Crosshair move → floating OHLC tooltip
    chartInst.subscribeCrosshairMove((param) => {
      if (!param.time || !param.point) { setHover(null); return; }
      const dateStr = typeof param.time === 'string' ? param.time : null;
      if (!dateStr) { setHover(null); return; }
      const dataPoint = chart.ohlc.find(d => d.date === dateStr);
      if (!dataPoint) { setHover(null); return; }
      const events = chart.marks.filter(m => m.date === dateStr);
      setHover({ date: dateStr, ohlc: dataPoint, events, x: param.point.x, y: param.point.y });
    });

    // Visible time-range changed (auto-frame, manual zoom) → recompute band coords
    chartInst.timeScale().subscribeVisibleTimeRangeChange(() => {
      recomputeBand();
    });

    // Initial band paint
    recomputeBand();

    // Width follows the container
    const ro = new ResizeObserver(() => {
      if (containerRef.current && chartRef.current) {
        chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
        recomputeBand();
      }
    });
    ro.observe(containerRef.current);

    return () => {
      ro.disconnect();
      priceLinesRef.current = [];
      seriesRef.current = null;
      try { chartInst.remove(); } catch (_) {}
      if (chartRef.current === chartInst) chartRef.current = null;
    };
  }, [chartType, chart.ohlc]);

  // Update markers + price lines on trade switch + auto-frame the chart window
  useEffect(() => {
    let willAutoFrame = false;
    if (chartRef.current && trade) {
      const win = findTradeWindow(trade, chart.marks, chart.ohlc);
      if (win) {
        const dates = chart.ohlc.map(d => d.date);
        const startI = Math.max(0, dates.indexOf(win.entryDate) - 3);
        const endI = Math.min(dates.length - 1, dates.indexOf(win.exitDate) + 3);
        if (startI !== 0 || endI !== dates.length - 1) {
          try { chartRef.current.timeScale().setVisibleRange({ from: dates[startI], to: dates[endI] }); } catch (_) {}
          willAutoFrame = true;
        }
      }
      setAutoFramed(willAutoFrame);
    }
    // Show only selected trade when auto-framed; show all (with filter) when zoomed out
    applyMarkersAndLines(seriesRef.current, chart.marks, currentTradeId, trade, !willAutoFrame, tradeFilterRef.current);
    setTimeout(recomputeBand, 30);
  }, [trade, currentTradeId, chart.marks]);

  // ──────────────────────────────────────────────────────────────────
  // Replay control — animates day-by-day from entry to exit
  // ──────────────────────────────────────────────────────────────────
  const stopReplay = () => {
    if (replayTimerRef.current) { clearTimeout(replayTimerRef.current); replayTimerRef.current = null; }
    setReplay({ active: false, day: null, pnl: 0, step: 0, total: 0, labelX: null, labelY: null, dayX: null, dayY: null });
  };

  const startReplay = () => {
    const t = tradeRef.current;
    if (!t || replay.active) return;
    const win = findTradeWindow(t, marksRef.current, chart.ohlc);
    if (!win) return;
    const dates = chart.ohlc.map(d => d.date);
    const startIdx = dates.indexOf(win.entryDate);
    const endIdx = dates.indexOf(win.exitDate);
    if (startIdx < 0 || endIdx < 0) return;

    const POINT_VALUE = 20;
    const dir = t.direction === 'long' ? 1 : -1;
    const totalSteps = endIdx - startIdx + 1;

    let i = startIdx;
    const tick = () => {
      if (i > endIdx) {
        stopReplay();
        return;
      }
      const day = chart.ohlc[i];
      const pnl = Math.round((day.close - t.entry_price) * POINT_VALUE * (t.size || 1) * dir);

      // Compute label position + current-day vertical line position.
      // v6.4: label now travels with the moving dot day-by-day (not pinned to
      // the mid-window) — reads as "today's P&L on this day". The vertical
      // dashed line + dot mark the current day in time.
      const chartInst = chartRef.current;
      const series = seriesRef.current;
      let labelX = null, labelY = null, dayX = null, dayY = null;
      if (chartInst && series) {
        const ts = chartInst.timeScale();
        const xD = ts.timeToCoordinate(day.date);
        const y  = series.priceToCoordinate ? series.priceToCoordinate(day.close) : null;
        if (xD != null) { labelX = xD; dayX = xD; }
        if (y  != null) { labelY = Math.max(18, y - 44); dayY = y; }
      }

      setReplay({
        active: true, day: day.date, pnl, step: i - startIdx + 1, total: totalSteps,
        labelX, labelY, dayX, dayY,
      });

      i++;
      replayTimerRef.current = setTimeout(tick, 480);
    };
    tick();
  };

  // Stop replay if trade switches mid-flight
  useEffect(() => {
    if (replay.active) stopReplay();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentTradeId]);

  // Cleanup any in-flight replay on unmount
  useEffect(() => () => stopReplay(), []);

  // Filter chip click — fits chart to 30D AND narrows markers to that outcome.
  // Doubles as "Show all 30D" when filter='all'. Tapping the active chip also
  // works as a fit-to-content reset.
  const setFilter = (f) => {
    setTradeFilter(f);
    if (chartRef.current) {
      try { chartRef.current.timeScale().fitContent(); } catch (_) {}
    }
    setAutoFramed(false);
    applyMarkersAndLines(seriesRef.current, chart.marks, currentTradeId, trade, true, f);
    setTimeout(recomputeBand, 30);
  };

  // Trade outcome counts — drives the chip labels and lives on the marks data,
  // not the trades list, so it stays in sync with whatever the chart sees.
  const tradeCounts = useMemo(() => {
    const byId = new Map();
    chart.marks.forEach(mk => {
      if (!byId.has(mk.id)) byId.set(mk.id, null);
      if (mk.kind === 'win'  || mk.kind === 'loss' || mk.kind === 'open') {
        byId.set(mk.id, mk.kind);
      }
    });
    const list = [...byId.values()];
    return {
      all:  list.length,
      win:  list.filter(r => r === 'win').length,
      loss: list.filter(r => r === 'loss').length,
      open: list.filter(r => r === 'open').length,
    };
  }, [chart.marks]);

  return (
    <div className="chart-card" id="chart-card">
      <div className="ch-head">
        <span className="ch-title">NQ · daily · last 30 days</span>
      </div>
      {/* Action row: replay + trade picker + outcome filters (chips double as the chart legend).
          Filter chips replace the old "Show all 30D" button: tapping any chip
          fits the chart back to 30D and narrows markers to that outcome. The
          picker dropdown jumps directly to any trade and is also filtered by
          the active chip so the two controls stay consistent. */}
      <div className="chart-actions">
        {!replay.active ? (
          <button
            className="ch-replay-btn"
            onClick={startReplay}
            disabled={!trade || !findTradeWindow(trade, chart.marks, chart.ohlc)}
            aria-label="Replay this trade day by day"
          >
            ▶ Replay this trade
          </button>
        ) : (
          <button className="ch-replay-btn live" onClick={stopReplay} aria-label="Stop replay">
            ■ Stop replay
          </button>
        )}

        {!replay.active && trades.length > 0 && (() => {
          // Build the picker list — filtered by the active outcome chip, sorted
          // newest first. The currently selected trade is always kept visible
          // even when it doesn't match the filter, so the trigger label stays
          // truthful and tapping the trigger never opens an empty list.
          const filterPredicate = (t) =>
            tradeFilter === 'all' ? true : t.result === tradeFilter;
          const list = trades
            .filter(t => filterPredicate(t) || t.id === currentTradeId)
            .slice()
            .sort((a, b) => b.date.localeCompare(a.date));
          return (
            <div className="trade-picker" ref={pickerRef}>
              <button
                className={`tp-trigger ${pickerOpen ? 'open' : ''}`}
                onClick={() => setPickerOpen(o => !o)}
                aria-haspopup="listbox"
                aria-expanded={pickerOpen}
              >
                <span className={`tp-dot ${trade?.result || ''}`} aria-hidden="true" />
                <span className="tp-lbl">
                  {trade ? (
                    <>
                      <span className="tp-date">{fmtShort(trade.date)}</span>
                      <span className="tp-tier">{trade.tier}</span>
                    </>
                  ) : 'Pick a trade'}
                </span>
                <span className="tp-caret" aria-hidden="true">▾</span>
              </button>
              {pickerOpen && (
                <div className="tp-menu" role="listbox" aria-label="Select a trade">
                  <div className="tp-menu-head">
                    {list.length} {tradeFilter === 'all' ? 'trade' : tradeFilter} {list.length === 1 ? '' : 's'} · newest first
                  </div>
                  {list.map(t => {
                    const isSel = t.id === currentTradeId;
                    const pnl = t.result === 'open' ? t.unrealized_pnl_usd : t.realized_pnl_usd;
                    const sign = (pnl ?? 0) >= 0 ? '+' : '−';
                    return (
                      <button
                        key={t.id}
                        className={`tp-row ${t.result} ${isSel ? 'sel' : ''}`}
                        role="option"
                        aria-selected={isSel}
                        onClick={() => {
                          onSelect(t.id);
                          setPickerOpen(false);
                        }}
                      >
                        <span className={`tp-dot ${t.result}`} aria-hidden="true" />
                        <span className="tp-row-date">{fmtShort(t.date)}</span>
                        <span className="tp-row-tier">{t.tier}</span>
                        <span className={`tp-row-result ${t.result}`}>
                          {t.result === 'win' ? 'WIN' : t.result === 'loss' ? 'LOSS' : 'OPEN'}
                        </span>
                        <span className={`tp-row-pnl ${(pnl ?? 0) >= 0 ? 'pos' : 'neg'}`}>
                          {sign}${Math.abs(pnl ?? 0).toLocaleString()}
                        </span>
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
          );
        })()}

        {!replay.active && (
          <div className="trade-filter" role="radiogroup" aria-label="Filter trades by outcome">
            {autoFramed && (
              <span className="tf-zoom-tag" title="Chart is zoomed to selected trade">
                <span className="tf-zoom-dot" />zoomed
              </span>
            )}
            <button
              className={`tf-chip all ${tradeFilter === 'all' ? 'sel' : ''}`}
              onClick={() => setFilter('all')}
              role="radio"
              aria-checked={tradeFilter === 'all'}
            >
              <span className="tf-glyph all" />All<span className="tf-n">{tradeCounts.all}</span>
            </button>
            <button
              className={`tf-chip win ${tradeFilter === 'win' ? 'sel' : ''}`}
              onClick={() => setFilter('win')}
              role="radio"
              aria-checked={tradeFilter === 'win'}
              disabled={tradeCounts.win === 0}
            >
              <span className="tf-glyph win" />Wins<span className="tf-n">{tradeCounts.win}</span>
            </button>
            <button
              className={`tf-chip loss ${tradeFilter === 'loss' ? 'sel' : ''}`}
              onClick={() => setFilter('loss')}
              role="radio"
              aria-checked={tradeFilter === 'loss'}
              disabled={tradeCounts.loss === 0}
            >
              <span className="tf-glyph loss" />Losses<span className="tf-n">{tradeCounts.loss}</span>
            </button>
            <button
              className={`tf-chip open ${tradeFilter === 'open' ? 'sel' : ''}`}
              onClick={() => setFilter('open')}
              role="radio"
              aria-checked={tradeFilter === 'open'}
              disabled={tradeCounts.open === 0}
            >
              <span className="tf-glyph open" />Open<span className="tf-n">{tradeCounts.open}</span>
            </button>
          </div>
        )}

        {replay.active && (
          <div className="replay-status" role="status" aria-live="polite">
            <span className="rs-eb">DAY {replay.step}/{replay.total}</span>
            <span className="rs-day">{fmtHuman(replay.day)}</span>
            <span className={`rs-pnl ${replay.pnl >= 0 ? 'pos' : 'neg'}`}>
              {replay.pnl >= 0 ? '+' : '−'}${Math.abs(replay.pnl).toLocaleString()}
            </span>
          </div>
        )}
      </div>

      {/* Chart container — DOM overlays for hold-band + OHLC tooltip */}
      <div className="tv-chart-wrap" style={{ position: 'relative' }}>
        <div ref={containerRef} className="tv-chart" style={{ width: '100%', height: 320, position: 'relative' }} />

        {/* Hold-duration band — translucent vertical strip between entry and exit dates.
            Only painted in FOCUSED mode (v6.8) so it never appears underneath the
            "all / wins / losses / open" filter views where no single trade is the subject. */}
        {bandCoords && autoFramed && (() => {
          // Pick the pnl that drives the color:
          //   - during replay → the running pnl of the current day
          //   - otherwise   → the trade's final realized OR unrealized pnl
          let pnl;
          if (replay.active) pnl = replay.pnl;
          else if (trade) pnl = trade.result === 'open' ? trade.unrealized_pnl_usd : trade.realized_pnl_usd;
          const inProfit = (pnl ?? 0) > 0;
          const inLoss = (pnl ?? 0) < 0;
          const flat = !inProfit && !inLoss;
          const fill = inProfit ? 'rgba(96,140,80,0.16)'   // greenish — "in profit"
                     : inLoss   ? 'rgba(171,106,87,0.13)'  // terracotta — "in loss"
                                : 'rgba(220,180,130,0.10)'; // mustard — flat / pre-move
          const edge = inProfit ? 'rgba(96,140,80,0.55)'
                     : inLoss   ? 'rgba(171,106,87,0.50)'
                                : 'rgba(220,180,130,0.55)';
          return (
            <div
              className="hold-band"
              style={{
                left: bandCoords.x1,
                width: Math.max(2, bandCoords.x2 - bandCoords.x1),
                background: fill,
                borderLeft: `1px dashed ${edge}`,
                borderRight: `1px dashed ${edge}`,
              }}
              aria-hidden="true"
            />
          );
        })()}

        {/* Live P&L chip — floats in the MIDDLE of the trade duration during replay,
            so it doesn't compete visually with the right-axis NQ price labels. */}
        {replay.active && replay.labelX != null && (
          <div
            className="replay-mid-label"
            style={{ left: replay.labelX, top: replay.labelY ?? 24 }}
            aria-hidden="true"
          >
            <div className="rml-day">day {replay.step}/{replay.total} · {fmtShort(replay.day)}</div>
            <div className={`rml-pnl ${replay.pnl >= 0 ? 'pos' : 'neg'}`}>
              {replay.pnl >= 0 ? '+' : '−'}${Math.abs(replay.pnl).toLocaleString()}
            </div>
          </div>
        )}

        {/* Vertical "current day" line during replay — sweeps day-by-day from
            entry to exit. Position is the day's x; height spans the whole chart
            pane (above the time axis). Color matches the trade outcome. */}
        {replay.active && replay.dayX != null && (() => {
          const t = tradeRef.current;
          const color = t?.result === 'win'  ? '#606C5A'
                      : t?.result === 'loss' ? '#AB6A57'
                                             : '#DCB482';
          return (
            <>
              <div
                className="replay-vline"
                style={{ left: replay.dayX, borderColor: color }}
                aria-hidden="true"
              />
              {replay.dayY != null && (
                <div
                  className="replay-dot"
                  style={{ left: replay.dayX, top: replay.dayY, background: color, boxShadow: `0 0 0 1px ${color}` }}
                  aria-hidden="true"
                />
              )}
            </>
          );
        })()}

        {/* Floating OHLC tooltip — follows the crosshair */}
        {hover && (() => {
          const cw = containerRef.current?.clientWidth || 400;
          const left = Math.max(8, Math.min(hover.x + 14, cw - 180));
          const top = Math.max(8, hover.y - 90);
          return (
            <div className="ohlc-tip" style={{ left, top }} aria-hidden="true">
              <div className="oh-date">{fmtHuman(hover.date)}</div>
              <div className="oh-grid">
                <span className="oh-k">O</span><b>{fmtPrice(hover.ohlc.open ?? hover.ohlc.close)}</b>
                <span className="oh-k">H</span><b>{fmtPrice(hover.ohlc.high ?? hover.ohlc.close)}</b>
                <span className="oh-k">L</span><b>{fmtPrice(hover.ohlc.low  ?? hover.ohlc.close)}</b>
                <span className="oh-k">C</span><b>{fmtPrice(hover.ohlc.close)}</b>
              </div>
              {hover.events.length > 0 && (
                <div className="oh-events">
                  {hover.events.map((e, i) => (
                    <div key={i} className={`oh-event ${e.kind}`}>
                      {e.kind === 'entry' ? '↑ BUY' : e.kind === 'win' ? '✓ WIN' : e.kind === 'loss' ? '✕ LOSS' : '○ OPEN'} · {e.id}
                    </div>
                  ))}
                </div>
              )}
            </div>
          );
        })()}
      </div>

      {/* Legend is the filter row above — outcome chips share the chart's marker
          glyphs. Scrubber removed in v6.7 (marker-tap + filter chips cover nav). */}
    </div>
  );
}

// ============================================================
// TRADE JOURNEY OVERLAY — removed (now native to lightweight-charts markers)
// ============================================================

// ============================================================
// SCRUBBER
// ============================================================
function Scrubber({ chart, currentTradeId, onSelect, hasTrade }) {
  const trackRef = useRef(null);
  const ohlc = chart.ohlc;
  const nDays = ohlc.length;

  // Index trades by their dominant marker — pick entry marks as scrubbable anchors
  const anchorMarks = chart.marks.filter(m => m.kind === 'entry' || m.kind === 'open');

  const idxForDate = (d) => ohlc.findIndex(o => o.date === d);
  const pctForIdx = (i) => (i / Math.max(1, nDays - 1)) * 100;

  const currentMark = chart.marks.find(m => m.id === currentTradeId && (m.kind === 'entry' || m.kind === 'open')) ||
                      chart.marks.find(m => m.id === currentTradeId);
  const currentIdx = currentMark ? idxForDate(currentMark.date) : 0;
  const currentPct = pctForIdx(currentIdx);

  // Drag → snap to nearest anchor mark
  const onDrag = (clientX) => {
    const track = trackRef.current;
    if (!track) return;
    const rect = track.getBoundingClientRect();
    const x = Math.max(0, Math.min(rect.width, clientX - rect.left));
    const pct = (x / rect.width) * 100;
    // Find nearest anchor
    let best = anchorMarks[0], bestDist = Infinity;
    anchorMarks.forEach(m => {
      const d = Math.abs(pctForIdx(idxForDate(m.date)) - pct);
      if (d < bestDist && hasTrade(m.id)) { bestDist = d; best = m; }
    });
    if (best && best.id !== currentTradeId) onSelect(best.id);
  };

  const dragging = useRef(false);
  const onPointerDown = (e) => {
    dragging.current = true;
    e.currentTarget.setPointerCapture(e.pointerId);
    onDrag(e.clientX);
  };
  const onPointerMove = (e) => {
    if (!dragging.current) return;
    onDrag(e.clientX);
  };
  const onPointerUp = (e) => {
    dragging.current = false;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
  };

  return (
    <div className="scrub">
      <div className="scrub-l">
        <span>▸ scrub the last 30 days</span>
        <span className="hint">drag · or tap a marker</span>
      </div>
      <div
        className="scrub-track"
        ref={trackRef}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
      >
        {chart.marks.map((m, i) => {
          if (m.kind !== 'entry' && m.kind !== 'open') return null;
          const idx = idxForDate(m.date);
          const pct = pctForIdx(idx);
          const cls = m.kind === 'open' ? '' : (chart.marks.find(x => x.id === m.id && x.kind === 'win') ? 'win' : (chart.marks.find(x => x.id === m.id && x.kind === 'loss') ? 'loss' : ''));
          return <div key={i} className={`scrub-mk ${cls}`} style={{ left: `${pct}%` }} />;
        })}
        <div className="scrub-handle" style={{ left: `${currentPct}%` }} aria-label="Selected day" />
      </div>
      <div className="scrub-dates">
        <span>{fmtShort(ohlc[0].date)}</span>
        <span className="sel">▸ {currentMark ? fmtShort(currentMark.date) : ''} selected</span>
        <span>{fmtShort(ohlc[ohlc.length - 1].date)}</span>
      </div>
    </div>
  );
}

// ============================================================
// SELECTED TRADE BAR
// ============================================================
function SelectedTradeBar({ trade }) {
  const status = trade.result === 'open'
    ? 'open · live'
    : (trade.result === 'win' ? 'closed · win' : 'closed · loss');
  const pnl = trade.result === 'open' ? trade.unrealized_pnl_usd : trade.realized_pnl_usd;
  // Count up the P&L from 0 each time the trade changes
  const animated = useChartCountUp(pnl ?? 0, 800, [trade.id]);
  const shown = Math.round(animated);
  const sign = (pnl ?? 0) >= 0 ? '+' : '−';
  return (
    <div className="sel-bar" id="sel-bar">
      <div>
        <div className="sel-eb">selected trade · tap any marker to switch</div>
        <div className="sel-t">{fmtHuman(trade.date)} · {trade.direction} {trade.contract} · <em>{trade.tier}</em></div>
      </div>
      <div className="sel-pl-wrap">
        <div className="sel-pl-l">{status}</div>
        <div className={`sel-pl-v ${trade.result}`}>{sign}${Math.abs(shown).toLocaleString()}</div>
      </div>
    </div>
  );
}

// ============================================================
// TRACE CASCADE
// ============================================================
function TraceCascade({ trade, mode }) {
  const [expandedModule, setExpandedModule] = useState(null);
  const [showKlWhy, setShowKlWhy] = useState(false);
  const [expandedInput, setExpandedInput] = useState(null);

  // Re-collapse all when trade switches
  useEffect(() => {
    setExpandedModule(null);
    setShowKlWhy(false);
    setExpandedInput(null);
  }, [trade.id]);

  const isLoss = trade.result === 'loss';
  const isOpen = trade.result === 'open';
  const pnl = trade.result === 'open' ? trade.unrealized_pnl_usd : trade.realized_pnl_usd;
  const sign = (pnl ?? 0) >= 0 ? '+' : '−';

  // Step 1 — THE TRADE
  const step1Plain = trade.exit_price
    ? <>We bought one {trade.contract} contract at <em>{fmtPrice(trade.entry_price)}</em>. Sold at <em>{fmtPrice(trade.exit_price)}</em> · held {trade.hold_duration}.</>
    : <>We bought one {trade.contract} contract at <em>{fmtPrice(trade.entry_price)}</em>. <em>Still open</em> · held {trade.hold_duration}.</>;
  const step1Trader = trade.exit_price
    ? `BUY ${trade.size} ${trade.contract} @ ${trade.entry_price} · exit ${trade.exit_price} · ${trade.hold_duration} hold · P&L ${sign}$${Math.abs(pnl).toLocaleString()} · ${trade.plan.rulebook_section}`
    : `BUY ${trade.size} ${trade.contract} @ ${trade.entry_price} · open at mark · unrealized ${sign}$${Math.abs(pnl).toLocaleString()} · ${trade.plan.rulebook_section}`;

  // Step 2 — THE PLAN
  const step2Plain = <>The morning's plan: <em>"if {trade.plan.condition_human.replace(/^if /,'').replace(/, buy.*/, '')}, buy one contract."</em> Valid for the first {trade.plan.validity} of session.</>;
  const step2Trader = `pre-market plan · ${fmtShort(trade.date)} · activator: 5-min close > ${trade.plan.activator_level} · valid ${trade.plan.validity} · rule ${trade.plan.rulebook_section}`;

  // SBS module ordering — risks (score <=0) left, opportunities (score > 0) right
  const allModuleKeys = Object.keys(trade.analysis.modules);
  const riskList = allModuleKeys.filter(k => trade.analysis.modules[k].score <= 0);
  const oppList = allModuleKeys.filter(k => trade.analysis.modules[k].score > 0);
  const oppCount = oppList.length;
  const riskCount = riskList.length;

  // Step 3 — THE ANALYSIS
  const sbs = trade.analysis.sbs_score;
  const step3Plain = <>The system scored seven things about the market. <em>{oppCount} looked positive, {riskCount === 0 ? 'none raised concerns' : `${riskCount} raised ${riskCount === 1 ? 'a concern' : 'concerns'}`}.</em></>;
  const step3Trader = `SBS = ${sbs >= 0 ? '+' : ''}${sbs} / 7 · ${sbs >= 5 ? 'upper structural-bull band · §11B unlocked' : sbs >= 3 ? 'mid band · §11A only' : 'weak band · size reduced'}`;

  const klPlain = trade.key_level.rationale;
  const klTrader = trade.key_level.rationale_trader;

  const modShown = expandedModule ? trade.analysis.modules[expandedModule] : null;
  const modInfo = expandedModule ? MODULE_INFO[expandedModule] : null;
  const modIsRisk = modShown && modShown.score <= 0;

  return (
    <div className="trace-wrap" id="trace-wrap">
      <p className="trace-head">↓ the trade trace · tap any item to drill in</p>

      {/* STEP 1 */}
      <div className="step" id="step-trade">
        <div className="step-eb"><span>step 1 · what happened</span><span className="tag">THE TRADE</span></div>
        <div className="step-h">{mode === 'plain' ? step1Plain : step1Trader}</div>
        {mode === 'plain' && <div className="step-d">{step1Trader}</div>}
      </div>

      <div className="arrow-down">why did this trade fire?</div>

      {/* STEP 2 */}
      <div className="step" id="step-plan">
        <div className="step-eb"><span>step 2 · what the plan said</span><span className="tag">THE PLAN</span></div>
        <div className="step-h">{mode === 'plain' ? step2Plain : step2Trader}</div>
        {mode === 'plain' && <div className="step-d">{step2Trader}</div>}
      </div>

      <div className="arrow-down">what did this plan target?</div>

      {/* KEY LEVEL row */}
      <div className="ro-bar">
        <span className="l">← RISK (downside)</span>
        <span className="r">OPPORTUNITY (upside) →</span>
      </div>

      <div className="kl-row">
        <div className="kl-side risk">
          <div className="kl-eb">if we are wrong</div>
          <div className="kl-h">Break &lt; {fmtPrice(trade.key_level.stop_loss_price)}</div>
          <div className="kl-d">stop-loss · −${trade.key_level.stop_loss_usd.toLocaleString()} risk</div>
        </div>
        <div className="kl-hub">
          <div className="kl-hub-eb">key level</div>
          <div className="kl-hub-v">{fmtPrice(trade.key_level.value)}</div>
          <div className="kl-hub-d">entry trigger</div>
        </div>
        <div className="kl-side opp">
          <div className="kl-eb">if we are right</div>
          <div className="kl-h">Hold &gt; {fmtPrice(trade.key_level.target_price)}</div>
          <div className="kl-d">target · +${trade.key_level.target_usd.toLocaleString()} reach</div>
        </div>
      </div>

      <div className={`reveal-cta ${showKlWhy ? 'open' : ''}`} onClick={() => setShowKlWhy(v => !v)}>
        <span><b>How was {fmtPrice(trade.key_level.value)} chosen?</b> — tap to {showKlWhy ? 'hide' : 'show'} the reasoning</span>
      </div>
      {showKlWhy && (
        <div className="reveal-open">
          <div className="ro-eb">key level reasoning</div>
          <p>{mode === 'plain' ? klPlain : klTrader}</p>
        </div>
      )}

      <div className="arrow-down">how was this plan decided?</div>

      {/* STEP 3 — ANALYSIS */}
      <div className="step" id="step-analysis">
        <div className="step-eb"><span>step 3 · what the analysis said</span><span className="tag">THE ANALYSIS</span></div>
        <div className="step-h">{mode === 'plain' ? step3Plain : step3Trader}</div>
        {mode === 'plain' && <div className="step-d">{step3Trader}</div>}
      </div>

      {/* MODULE HUB — risks left, plan center, opps right */}
      <div className="module-hub-row" style={{ marginTop: 12 }}>
        <div className="mods risk">
          <div className="mods-h">{riskCount + (trade.analysis.sensor.level === 'caution' ? 1 : 0)} risk flag{riskCount + (trade.analysis.sensor.level === 'caution' ? 1 : 0) === 1 ? '' : 's'}</div>
          {riskList.map(k => {
            const m = trade.analysis.modules[k];
            const info = MODULE_INFO[k];
            return (
              <button key={k} className={`mod-chip risk ${expandedModule === k ? 'sel' : ''}`} onClick={() => setExpandedModule(expandedModule === k ? null : k)}>
                {info.name} <span className="mod-s">{m.score === 0 ? '0' : m.score}</span>
              </button>
            );
          })}
        </div>

        <div className="plan-hub">
          <div className="plan-hub-eb">the plan</div>
          <div className="plan-hub-h">{fmtShort(trade.date)}<br />{trade.tier}</div>
          <div className="plan-hub-d">SBS {sbs >= 0 ? '+' : ''}{sbs} / 7</div>
        </div>

        <div className="mods opp">
          <div className="mods-h">{oppCount} opportunity score{oppCount === 1 ? '' : 's'}</div>
          {oppList.map(k => {
            const m = trade.analysis.modules[k];
            const info = MODULE_INFO[k];
            return (
              <button key={k} className={`mod-chip opp ${expandedModule === k ? 'sel' : ''}`} onClick={() => setExpandedModule(expandedModule === k ? null : k)}>
                {info.name} <span className="mod-s">+{m.score}</span>
              </button>
            );
          })}
        </div>
      </div>

      {expandedModule && modShown && (
        <div className={`mod-detail ${modIsRisk ? 'risk-side' : ''}`}>
          <div className="mod-detail-eb">{modIsRisk ? 'risk flag' : 'opportunity score'} · {modShown.section}</div>
          <h4>{modInfo.name}</h4>
          <p>{modInfo.desc}</p>
          <p><strong>For this trade:</strong> {modShown.note}</p>
        </div>
      )}

      {/* TSS spectrum */}
      <div className="tss">
        <div className="tss-eb">trader-state spectrum · where the system sat</div>
        <div className="tss-bar">
          <div className="tss-seg ob">overbought</div>
          <div className="tss-seg nat">natural</div>
          <div className="tss-seg os">oversold</div>
          <div className="tss-marker" style={{ left: `${trade.analysis.tss.position}%` }}>▼</div>
          <div className="tss-marker-label" style={{ left: `${trade.analysis.tss.position}%` }}>{trade.analysis.tss.label}</div>
        </div>
      </div>

      {/* Markov roadmap */}
      <div className="roadmap">
        <div>
          <div className="roadmap-eb">markov roadmap · 5-day posterior</div>
          <div className="roadmap-bar">
            <div className="roadmap-seg bull" style={{ flex: trade.analysis.roadmap.bull_pct }}>{trade.analysis.roadmap.bull_pct}% bull</div>
            <div className="roadmap-seg neut" style={{ flex: trade.analysis.roadmap.neutral_pct }}>{trade.analysis.roadmap.neutral_pct}% neut</div>
            <div className="roadmap-seg bear" style={{ flex: trade.analysis.roadmap.bear_pct }}>{trade.analysis.roadmap.bear_pct}% bear</div>
          </div>
        </div>
        <div className="markov-badge">Markov<br /><span>module readings</span></div>
      </div>

      {/* Sensor */}
      <div className={`sensor-row ${trade.analysis.sensor.level}`}>
        <div className="sensor-icon">{trade.analysis.sensor.level === 'caution' ? '!' : '✓'}</div>
        <div style={{ flex: 1 }}>
          <div className="sensor-l">drift / news sensor · {trade.analysis.sensor.level}</div>
          <div className="sensor-h">{trade.analysis.sensor.level === 'caution' ? 'Caution flagged' : 'All clear'}</div>
          <div className="sensor-d">{trade.analysis.sensor.note}</div>
        </div>
      </div>

      {/* Post-mortem (for losses or when there's a lesson) */}
      {trade.post_mortem && (trade.post_mortem.lesson || trade.post_mortem.rule_violated) && (
        <div className={isLoss ? 'pm-box' : 'sensor-row normal'}>
          {isLoss ? (
            <>
              <div className="pm-eb">post-mortem · the rulebook view</div>
              <div className="pm-h">
                {trade.post_mortem.rule_violated
                  ? <>Rule violated: <em>{trade.post_mortem.rule_violated}</em></>
                  : <>Clean rule firing — the system did its job.</>}
              </div>
              <div className="pm-d">
                {trade.post_mortem.fix_shipped && <><b>Tomorrow's fix:</b> {trade.post_mortem.fix_shipped}<br /></>}
                {trade.post_mortem.lesson}
              </div>
            </>
          ) : (
            <>
              <div className="sensor-icon">★</div>
              <div style={{ flex: 1 }}>
                <div className="sensor-l">post-mortem</div>
                <div className="sensor-h">{trade.post_mortem.rule_violated ? 'Rule violated' : 'Clean execution'}</div>
                <div className="sensor-d">{trade.post_mortem.lesson}</div>
              </div>
            </>
          )}
        </div>
      )}

      <div className="arrow-down">what was this plan built from?</div>

      {/* RAW INPUTS */}
      <div className="step" style={{ marginBottom: 10 }}>
        <div className="step-eb"><span>step 4 · what the analysis was built on</span><span className="tag">THE RAW DATA</span></div>
        <div className="step-h">Four independent inputs feed the plan. Each is dated, sourced, and audited. Tap to inspect.</div>
      </div>
      <div className="inputs-grid">
        {trade.raw_inputs.map((inp, i) => (
          <div
            key={i}
            className="input-card"
            onClick={() => setExpandedInput(expandedInput === i ? null : i)}
            role="button"
            tabIndex={0}
          >
            <div className="input-eb">input · {inp.status}</div>
            <div className="input-h">{inp.label}</div>
            <div className="input-d">{inp.source}</div>
            <div className={`input-status ${inp.status === 'stale' ? 'stale' : ''}`}>{inp.details}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ---------- export to window ----------
Object.assign(window, {
  ChartCard, SelectedTradeBar, TraceCascade,
  fmtUSD, fmtPrice, fmtShort, fmtHuman, MODULE_INFO
});
