/* HLP charting suite
   - CompositionChart: stacked area of vault AV by tier (+ cash) over time
   - CumulativeTierPnL: cumulative PnL per tier
   - DailyTierPnL: daily PnL stacked bars by tier (+ cascade-clip toggle)
   - CumulativeAssetPnL: multi-line cumulative PnL with asset legend + search
   - AssetLeaderboard: ranked diverging bars + sparkline + sortable + searchable
   - ShareTierChart: HLP fill-volume share, tier mean + min/max band
   Shared: brush-zoom, hover crosshair, click-to-toggle legend.
*/
const { useMemo, useState, useEffect, useRef } = React;
const D = window.HLP_DATA;

const TIERS = ['major', 'mid', 'longtail', 'meme'];
const TIER_LABEL = { major: 'Majors', mid: 'Mid-cap', longtail: 'Long-tail', meme: 'Memes' };
const TIER_COLOR = {
  cash:     'var(--tier-cash)',
  major:    'var(--tier-major)',
  mid:      'var(--tier-mid)',
  longtail: 'var(--tier-longtail)',
  meme:     'var(--tier-meme)',
};
const LAYER_LABEL = { ...TIER_LABEL, cash: 'Cash (USDC)' };

// 24-color categorical palette for distinguishing many lines within one tier
const ASSET_PALETTE = [
  '#c8a96a', '#8aa5b8', '#c47a6e', '#b07ec0', '#8eb88a', '#d4b785',
  '#a07da3', '#7da89e', '#bf8a5a', '#9eaecb', '#cd9d9c', '#7c9b7a',
  '#d8c08a', '#7a91b3', '#a96f60', '#937b9f', '#8db09b', '#c7a474',
  '#aa9ec2', '#c89a82', '#9bb4a3', '#b58f57', '#7a8db0', '#b88e80',
];
function colorForSym(sym, ix) { return ASSET_PALETTE[ix % ASSET_PALETTE.length]; }

// ---------- Formatting ----------
function fmtUSD(n, dp = 0) {
  const abs = Math.abs(n);
  const sign = n < 0 ? '−' : '';
  if (abs >= 1e9) return `${sign}$${(abs/1e9).toFixed(2)}B`;
  if (abs >= 1e6) return `${sign}$${(abs/1e6).toFixed(dp >= 1 ? dp : 2)}M`;
  if (abs >= 1e3) return `${sign}$${(abs/1e3).toFixed(1)}k`;
  return `${sign}$${abs.toFixed(0)}`;
}
function fmtPnL(n) {
  const sign = n >= 0 ? '+' : '−';
  const abs = Math.abs(n);
  if (abs >= 1e6) return `${sign}$${(abs/1e6).toFixed(2)}M`;
  if (abs >= 1e3) return `${sign}$${(abs/1e3).toFixed(1)}k`;
  return `${sign}$${abs.toFixed(0)}`;
}
function shortDate(d) {
  const [_, m, dd] = d.split('-');
  return `${m}/${dd}`;
}
function monthLabel(d) {
  const [y, m] = d.split('-');
  const mn = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][parseInt(m,10)-1];
  return `${mn} '${y.slice(2)}`;
}
function fullDateLabel(d) {
  const [y, m, dd] = d.split('-');
  const mn = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][parseInt(m,10)-1];
  return `${mn} ${parseInt(dd,10)}, ${y}`;
}

// ---------- Event windows ----------
const EVENT_WINDOWS = (D && D.eventWindows) || [];

function EventMarkers({ dates, x, height }) {
  const getDate = (r) => (typeof r === 'string' ? r : r.date);
  return EVENT_WINDOWS.map((ev) => {
    const i0 = dates.findIndex(r => getDate(r) === ev.start);
    if (i0 < 0) return null;
    let i1 = dates.findIndex(r => getDate(r) === ev.end);
    if (i1 < 0) i1 = i0;
    const x0 = x(i0), x1 = x(i1);
    const lx = x0 + 4;
    const ly = 6;
    return (
      <g key={ev.id} className="cascade-marker">
        <rect x={x0} y={0} width={Math.max(2, x1 - x0)} height={height} fill="var(--accent)" opacity="0.10" />
        <line x1={x0} x2={x0} y1={0} y2={height} stroke="var(--accent)" strokeWidth="1" strokeDasharray="2 2" opacity="0.7" />
        <text
          x={lx}
          y={ly}
          fontFamily="var(--mono)"
          fontSize="9.5"
          fill="var(--accent)"
          letterSpacing="0.1em"
          transform={`rotate(90 ${lx} ${ly})`}
        >{ev.label.toUpperCase()}</text>
      </g>
    );
  });
}

// ---------- Axis helpers ----------
function niceTicks(min, max, n = 5) {
  const range = max - min;
  if (range === 0) return [min];
  const step = Math.pow(10, Math.floor(Math.log10(range / n)));
  const candidates = [1, 2, 2.5, 5, 10].map(m => m * step);
  let best = candidates[0];
  let bestDiff = Infinity;
  candidates.forEach(c => {
    const ticks = Math.round(range / c);
    if (Math.abs(ticks - n) < bestDiff) { bestDiff = Math.abs(ticks - n); best = c; }
  });
  const ticks = [];
  let v = Math.ceil(min / best) * best;
  while (v <= max + 1e-9) { ticks.push(v); v += best; }
  return ticks;
}
function dateTicks(dates, n = 8) {
  const out = [];
  const step = Math.max(1, Math.floor(dates.length / n));
  for (let i = 0; i < dates.length; i += step) out.push(i);
  return out;
}

// ---------- Hooks ----------
function useResize(initial = 700) {
  const ref = useRef(null);
  const [w, setW] = useState(initial);
  useEffect(() => {
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    if (ref.current) ro.observe(ref.current);
    return () => ro.disconnect();
  }, []);
  return [ref, w];
}

// Brush: returns range as data indices [start, end] inclusive, plus drag state and SVG event handlers.
function useBrush(plotL, plotR, count) {
  const [range, setRange] = useState(null); // [i0, i1]
  const [drag, setDrag] = useState(null);   // { px0, px }
  const ref = useRef(null);
  const pxToIx = (px) => {
    const t = (px - plotL) / Math.max(1, plotR - plotL);
    return Math.max(0, Math.min(count - 1, Math.round(t * (count - 1))));
  };
  const handlers = {
    onMouseDown: (e) => {
      const r = ref.current && ref.current.getBoundingClientRect();
      if (!r) return;
      const px = e.clientX - r.left;
      if (px < plotL || px > plotR) return;
      setDrag({ px0: px, px });
    },
    onMouseMove: (e) => {
      if (!drag) return;
      const r = ref.current && ref.current.getBoundingClientRect();
      if (!r) return;
      const px = Math.max(plotL, Math.min(plotR, e.clientX - r.left));
      setDrag({ ...drag, px });
    },
    onMouseUp: () => {
      if (!drag) return;
      const i0 = pxToIx(Math.min(drag.px0, drag.px));
      const i1 = pxToIx(Math.max(drag.px0, drag.px));
      if (i1 - i0 >= 2) setRange([i0, i1]);
      setDrag(null);
    },
    onMouseLeave: () => setDrag(null),
  };
  return { ref, range, setRange, drag, handlers };
}

// Hover: returns nearest data index based on mouse x, or null
function useHover(plotL, plotR, count) {
  const [ix, setIx] = useState(null);
  const ref = useRef(null);
  const pxToIx = (px) => {
    if (px < plotL || px > plotR) return null;
    const t = (px - plotL) / Math.max(1, plotR - plotL);
    return Math.max(0, Math.min(count - 1, Math.round(t * (count - 1))));
  };
  const handlers = {
    onMouseMove: (e) => {
      const r = ref.current && ref.current.getBoundingClientRect();
      if (!r) return;
      setIx(pxToIx(e.clientX - r.left));
    },
    onMouseLeave: () => setIx(null),
  };
  return { ref, ix, setIx, handlers };
}

// Brush + zoom controller: combines brush over a chart with a reset button.
// Charts call this to get [viewStart, viewEnd] for slicing their date axis.
function useBrushZoom(plotL, plotR, count) {
  const brush = useBrush(plotL, plotR, count);
  const view = brush.range || [0, count - 1];
  const reset = () => brush.setRange(null);
  const isZoomed = brush.range != null;
  return { ...brush, view, reset, isZoomed };
}

// Toggleable legend hook: returns { hidden:Set, toggle }
function useToggleSet(initial = []) {
  const [hidden, setHidden] = useState(new Set(initial));
  const toggle = (k) => {
    setHidden(prev => {
      const next = new Set(prev);
      if (next.has(k)) next.delete(k); else next.add(k);
      return next;
    });
  };
  return [hidden, toggle];
}

// ---------- Brush rectangle (drawn while user drags) ----------
function BrushRect({ drag, height, plotL, plotR }) {
  if (!drag) return null;
  const x0 = Math.min(drag.px0, drag.px);
  const x1 = Math.max(drag.px0, drag.px);
  return (
    <g pointerEvents="none">
      <rect x={x0} y={0} width={x1 - x0} height={height} fill="var(--ink)" opacity="0.06" />
      <line x1={x0} x2={x0} y1={0} y2={height} stroke="var(--ink-2)" strokeDasharray="2 2" opacity="0.7" />
      <line x1={x1} x2={x1} y1={0} y2={height} stroke="var(--ink-2)" strokeDasharray="2 2" opacity="0.7" />
    </g>
  );
}

// ---------- Hover crosshair ----------
function HoverCrosshair({ ix, x, top, height }) {
  if (ix == null) return null;
  return (
    <line x1={x(ix)} x2={x(ix)} y1={top} y2={top + height} stroke="var(--ink-2)" strokeWidth="1" strokeDasharray="2 2" opacity="0.65" pointerEvents="none" />
  );
}

// ---------- Tooltip (DOM-positioned, sits to the right of crosshair) ----------
function Tooltip({ visible, xPx, containerW, children }) {
  if (!visible) return null;
  const offset = 12;
  const flipped = xPx + 220 > containerW;
  const style = flipped
    ? { right: containerW - xPx + offset }
    : { left: xPx + offset };
  return <div className="hover-tip" style={style}>{children}</div>;
}

// ---------- Reset zoom button ----------
function ZoomBadge({ isZoomed, dates, view, reset }) {
  if (!isZoomed) return null;
  const a = dates[view[0]], b = dates[view[1]];
  return (
    <button className="zoom-badge" onClick={reset} title="Reset zoom">
      <span className="mono">{shortDate(a)} → {shortDate(b)}</span>
      <span className="zoom-x">×</span>
    </button>
  );
}

// ============================================================
// 1. CompositionChart — stacked area + tier toggle + brush + hover
// ============================================================
function CompositionChart({ vault = 'TOTAL', height = 320, mode = 'absolute' }) {
  const [containerRef, w] = useResize();
  const [hidden, toggleHidden] = useToggleSet();

  const fullSeries = useMemo(() => {
    if (vault === 'TOTAL') return D.totalAV;
    return D.vaultAV[vault] || D.totalAV;
  }, [vault]);

  const padL = 64, padR = 16, padT = 16, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, fullSeries.length);
  const series = fullSeries.slice(zoom.view[0], zoom.view[1] + 1);

  const x = (i) => padL + (i / Math.max(1, series.length - 1)) * innerW;

  const layers = ['cash', 'major', 'mid', 'longtail', 'meme'].filter(l => !hidden.has(l));
  const allLayers = ['cash', 'major', 'mid', 'longtail', 'meme'];

  const stacks = series.map(r => {
    const rowTotal = layers.reduce((s, k) => s + (r[k] || 0), 0) || 1;
    let cum = 0;
    const out = {};
    layers.forEach(k => {
      const raw = r[k] || 0;
      const v = mode === 'percent' ? raw / rowTotal : raw;
      out[k] = [cum, cum + v];
      cum += v;
    });
    out.total = cum;
    return out;
  });

  const yMax = mode === 'percent' ? 1 : Math.max(...stacks.map(s => s.total), 1);
  const y = (v) => padT + innerH - (v / yMax) * innerH;

  const paths = layers.map(layer => {
    let top = `M ${x(0)} ${y(stacks[0][layer][1])}`;
    for (let i = 1; i < series.length; i++) top += ` L ${x(i)} ${y(stacks[i][layer][1])}`;
    let bot = '';
    for (let i = series.length - 1; i >= 0; i--) bot += ` L ${x(i)} ${y(stacks[i][layer][0])}`;
    return { layer, d: top + bot + ' Z' };
  });

  const yTicks = mode === 'percent'
    ? [0, 0.25, 0.5, 0.75, 1]
    : niceTicks(0, yMax, 4);
  const fmtY = mode === 'percent' ? (t) => `${Math.round(t * 100)}%` : (t) => fmtUSD(t);
  const xIxs = dateTicks(series.map(r => r.date), 7);

  // Hover
  const hover = useHover(padL, W - padR, series.length);
  // Bind brush+hover to the SAME container ref (svg)
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;

  const hoverRow = hover.ix != null ? series[hover.ix] : null;

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={fullSeries.map(r => r.date)} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label="HLP composition over time"
           {...zoom.handlers} onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" strokeDasharray="2 3" opacity="0.5" />
            <text x={padL - 8} y={y(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtY(t)}</text>
          </g>
        ))}
        {paths.map(p => (
          <path key={p.layer} d={p.d} fill={TIER_COLOR[p.layer]} stroke="var(--paper)" strokeWidth="0.5" opacity="0.9" />
        ))}
        {series.length > 1 && (
          <path d={`M ${series.map((_, i) => `${x(i)},${y(stacks[i].total)}`).join(' L ')}`} stroke="var(--ink)" strokeWidth="1" fill="none" />
        )}
        <EventMarkers dates={series} x={x} height={padT + innerH} />
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{monthLabel(series[i].date)}</text>
        ))}
        <line x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} stroke="var(--ink-3)" strokeWidth="1" />
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}{hoverRow.uncertain ? <span className="tip-flag"> *</span> : null}</div>
            <div className="tip-rows">
              {allLayers.slice().reverse().map(l => (
                <div key={l} className={`tip-row ${hidden.has(l) ? 'is-hidden' : ''}`}>
                  <span className="tip-sw" style={{ background: TIER_COLOR[l] }}></span>
                  <span className="tip-lab">{LAYER_LABEL[l]}</span>
                  <span className="tip-val mono">{mode === 'percent'
                    ? `${((hoverRow[l] || 0) / Math.max(1, allLayers.reduce((s, k) => s + (hoverRow[k] || 0), 0)) * 100).toFixed(1)}%`
                    : fmtUSD(hoverRow[l] || 0)}</span>
                </div>
              ))}
              <div className="tip-row tip-total">
                <span className="tip-sw" style={{ background: 'var(--ink)' }}></span>
                <span className="tip-lab">Total AV</span>
                <span className="tip-val mono">{fmtUSD(hoverRow.av)}</span>
              </div>
            </div>
          </div>
        )}
      </Tooltip>
      <div className="chart-legend">
        {allLayers.slice().reverse().map(l => (
          <button key={l} className={`legend-item legend-toggle ${hidden.has(l) ? 'is-off' : ''}`} onClick={() => toggleHidden(l)}>
            <span className="swatch" style={{ background: TIER_COLOR[l] }}></span>
            <span className="legend-label">{LAYER_LABEL[l]}</span>
          </button>
        ))}
        <span className="legend-hint">click to toggle · drag chart to zoom</span>
      </div>
    </div>
  );
}

// ============================================================
// 2. CumulativeTierPnL — 4 lines + tier toggle + brush + hover
// ============================================================
function CumulativeTierPnL({ height = 280 }) {
  const [containerRef, w] = useResize();
  const [hidden, toggleHidden] = useToggleSet();

  const fullSeries = useMemo(() => {
    const cum = { major: 0, mid: 0, longtail: 0, meme: 0 };
    return D.tierPnLSeries.map(r => {
      cum.major += r.major; cum.mid += r.mid; cum.longtail += r.longtail; cum.meme += r.meme;
      return { date: r.date, ...cum, uncertain: r.uncertain };
    });
  }, []);

  const padL = 64, padR = 110, padT = 16, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, fullSeries.length);
  const series = fullSeries.slice(zoom.view[0], zoom.view[1] + 1);
  const x = (i) => padL + (i / Math.max(1, series.length - 1)) * innerW;

  const visibleTiers = TIERS.filter(t => !hidden.has(t));
  let yMin = Infinity, yMax = -Infinity;
  series.forEach(r => visibleTiers.forEach(t => { yMin = Math.min(yMin, r[t]); yMax = Math.max(yMax, r[t]); }));
  if (visibleTiers.length === 0) { yMin = 0; yMax = 1; }
  yMin = Math.min(0, yMin); yMax = Math.max(0, yMax);
  if (yMin === yMax) yMax = yMin + 1;
  const y = (v) => padT + innerH - ((v - yMin) / (yMax - yMin)) * innerH;
  const yTicks = niceTicks(yMin, yMax, 4);
  const xIxs = dateTicks(series.map(r => r.date), 7);
  const lastIx = series.length - 1;

  const hover = useHover(padL, W - padR, series.length);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverRow = hover.ix != null ? series[hover.ix] : null;

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={fullSeries.map(r => r.date)} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label="Cumulative PnL by tier"
           {...zoom.handlers} onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.5" />
            <text x={padL - 8} y={y(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtPnL(t)}</text>
          </g>
        ))}
        <line x1={padL} x2={W - padR} y1={y(0)} y2={y(0)} stroke="var(--ink-3)" strokeWidth="1" />
        <EventMarkers dates={series} x={x} height={padT + innerH} />
        {visibleTiers.map(t => {
          if (series.length < 2) return null;
          const d = `M ${series.map((r, i) => `${x(i)},${y(r[t])}`).join(' L ')}`;
          return <path key={t} d={d} stroke={TIER_COLOR[t]} strokeWidth="1.5" fill="none" />;
        })}
        {visibleTiers.map(t => (
          <g key={t}>
            <line x1={x(lastIx)} x2={x(lastIx) + 6} y1={y(series[lastIx][t])} y2={y(series[lastIx][t])} stroke={TIER_COLOR[t]} />
            <text x={x(lastIx) + 10} y={y(series[lastIx][t]) + 3} fontFamily="var(--mono)" fontSize="10" fill={TIER_COLOR[t]}>
              {TIER_LABEL[t]} {fmtPnL(series[lastIx][t])}
            </text>
          </g>
        ))}
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{monthLabel(series[i].date)}</text>
        ))}
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}{hoverRow.uncertain ? <span className="tip-flag"> *</span> : null}</div>
            <div className="tip-rows">
              {TIERS.map(t => (
                <div key={t} className={`tip-row ${hidden.has(t) ? 'is-hidden' : ''}`}>
                  <span className="tip-sw" style={{ background: TIER_COLOR[t] }}></span>
                  <span className="tip-lab">{TIER_LABEL[t]}</span>
                  <span className="tip-val mono">{fmtPnL(hoverRow[t])}</span>
                </div>
              ))}
            </div>
          </div>
        )}
      </Tooltip>
      <div className="chart-legend">
        {TIERS.map(t => (
          <button key={t} className={`legend-item legend-toggle ${hidden.has(t) ? 'is-off' : ''}`} onClick={() => toggleHidden(t)}>
            <span className="swatch" style={{ background: TIER_COLOR[t] }}></span>
            <span className="legend-label">{TIER_LABEL[t]}</span>
          </button>
        ))}
        <span className="legend-hint">click to toggle · drag chart to zoom</span>
      </div>
    </div>
  );
}

// ============================================================
// 3. DailyTierPnL — diverging bars + tier toggle + brush + hover + outlier clip
// ============================================================
function DailyTierPnL({ height = 220, range = 'full', clipCascade = false }) {
  const [containerRef, w] = useResize();
  const [hidden, toggleHidden] = useToggleSet();

  const fullSeries = useMemo(() => {
    if (range === 'cascade') {
      const cIx = D.tierPnLSeries.findIndex(r => r.date === '2025-10-10');
      return D.tierPnLSeries.slice(Math.max(0, cIx - 14), cIx + 15);
    }
    return D.tierPnLSeries;
  }, [range]);

  const padL = 64, padR = 16, padT = 12, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, fullSeries.length);
  const series = fullSeries.slice(zoom.view[0], zoom.view[1] + 1);
  const visibleTiers = TIERS.filter(t => !hidden.has(t));

  const isClipDate = (d) => clipCascade && (d === '2025-10-10' || d === '2025-10-11');

  // Compute y-bounds excluding clipped days if requested
  let yMin = Infinity, yMax = -Infinity;
  series.forEach(r => {
    if (isClipDate(r.date)) return;
    let pos = 0, neg = 0;
    visibleTiers.forEach(t => { if (r[t] >= 0) pos += r[t]; else neg += r[t]; });
    yMax = Math.max(yMax, pos); yMin = Math.min(yMin, neg);
  });
  if (!isFinite(yMin) || !isFinite(yMax)) { yMin = 0; yMax = 1; }
  if (yMin === yMax) yMax = yMin + 1;

  const barW = Math.max(1.2, innerW / series.length - 0.6);
  const y = (v) => padT + innerH - ((v - yMin) / (yMax - yMin)) * innerH;
  const yTicks = niceTicks(yMin, yMax, 4);
  const xIxs = dateTicks(series.map(r => r.date), Math.min(8, series.length));
  const x = (i) => padL + (i / Math.max(1, series.length - 1)) * innerW;

  const hover = useHover(padL, W - padR, series.length);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverRow = hover.ix != null ? series[hover.ix] : null;

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={fullSeries.map(r => r.date)} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img"
           {...zoom.handlers} onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.4" />
            <text x={padL - 8} y={y(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtPnL(t)}</text>
          </g>
        ))}
        <line x1={padL} x2={W - padR} y1={y(0)} y2={y(0)} stroke="var(--ink-3)" />
        <EventMarkers dates={series} x={x} height={padT + innerH} />
        {series.map((r, i) => {
          const cx = x(i);
          const clipped = isClipDate(r.date);
          let posCum = 0, negCum = 0;
          return (
            <g key={i}>
              {visibleTiers.map(t => {
                const v = r[t];
                if (!v) return null;
                let y0Raw, y1Raw;
                if (v >= 0) { y0Raw = y(posCum); y1Raw = y(posCum + v); posCum += v; }
                else { y0Raw = y(negCum); y1Raw = y(negCum + v); negCum += v; }
                // Clamp to plot area when clipping outliers
                const top = Math.max(padT, Math.min(y0Raw, y1Raw));
                const bot = Math.min(padT + innerH, Math.max(y0Raw, y1Raw));
                if (bot <= top) return null;
                return <rect key={t} x={cx - barW/2} y={top} width={barW} height={bot - top} fill={TIER_COLOR[t]} opacity={clipped ? 0.55 : 0.85} />;
              })}
              {clipped && (
                <text x={cx} y={padT + 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="9" fill="var(--accent)">↑</text>
              )}
            </g>
          );
        })}
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{range === 'cascade' ? shortDate(series[i].date) : monthLabel(series[i].date)}</text>
        ))}
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}{hoverRow.uncertain ? <span className="tip-flag"> *</span> : null}</div>
            <div className="tip-rows">
              {TIERS.map(t => (
                <div key={t} className={`tip-row ${hidden.has(t) ? 'is-hidden' : ''}`}>
                  <span className="tip-sw" style={{ background: TIER_COLOR[t] }}></span>
                  <span className="tip-lab">{TIER_LABEL[t]}</span>
                  <span className="tip-val mono">{fmtPnL(hoverRow[t])}</span>
                </div>
              ))}
              <div className="tip-row tip-total">
                <span className="tip-sw" style={{ background: 'var(--ink)' }}></span>
                <span className="tip-lab">Total</span>
                <span className="tip-val mono">{fmtPnL(TIERS.reduce((s, t) => s + hoverRow[t], 0))}</span>
              </div>
            </div>
          </div>
        )}
      </Tooltip>
      <div className="chart-legend">
        {TIERS.map(t => (
          <button key={t} className={`legend-item legend-toggle ${hidden.has(t) ? 'is-off' : ''}`} onClick={() => toggleHidden(t)}>
            <span className="swatch" style={{ background: TIER_COLOR[t] }}></span>
            <span className="legend-label">{TIER_LABEL[t]}</span>
          </button>
        ))}
        <span className="legend-hint">click to toggle · drag chart to zoom</span>
      </div>
    </div>
  );
}

// ============================================================
// 4. AssetLeaderboard — sortable, searchable, with sparkline + diverging bars
// ============================================================
function AssetLeaderboard({ tier = 'all' }) {
  const [sortKey, setSortKey] = useState('total');
  const [sortDir, setSortDir] = useState('desc');
  const [query, setQuery] = useState('');

  const rows = useMemo(() => {
    let r = D.assetTotals.slice();
    if (tier !== 'all') r = r.filter(x => x.tier === tier);
    if (query.trim()) {
      const q = query.trim().toUpperCase();
      r = r.filter(x => x.sym.includes(q));
    }
    r.sort((a, b) => {
      const av = a[sortKey], bv = b[sortKey];
      if (typeof av === 'string') return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
      return sortDir === 'asc' ? av - bv : bv - av;
    });
    return r;
  }, [tier, query, sortKey, sortDir]);

  const toggleSort = (k) => {
    if (sortKey === k) setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
    else { setSortKey(k); setSortDir('desc'); }
  };
  const arrow = (k) => sortKey === k ? (sortDir === 'asc' ? '↑' : '↓') : '';

  return (
    <div>
      <div className="lb-search">
        <input
          type="text"
          className="lb-search-input"
          placeholder="Search asset (e.g. ETH, POPCAT)…"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <span className="lb-search-count smallcaps">{rows.length} {rows.length === 1 ? 'asset' : 'assets'}</span>
      </div>
      <div className="leaderboard-x">
        <div className="lbx-head">
          <div onClick={() => toggleSort('sym')} className="lbx-sortable">Asset {arrow('sym')}</div>
          <div onClick={() => toggleSort('tier')} className="lbx-sortable">Tier {arrow('tier')}</div>
          <div onClick={() => toggleSort('total')} className="lbx-sortable">Cumulative PnL {arrow('total')}</div>
          <div onClick={() => toggleSort('exposureMean')} className="lbx-sortable">Mean exposure {arrow('exposureMean')}</div>
          <div onClick={() => toggleSort('volMean')} className="lbx-sortable">Daily turnover {arrow('volMean')}</div>
        </div>
        <div className="lbx-body">
          {rows.map(r => (
            <div key={r.sym} className="lbx-row">
              <div className="lbx-sym mono">{r.sym}</div>
              <div className="lbx-tier">
                <span className="tier-pill" style={{ background: TIER_COLOR[r.tier], color: 'var(--paper)' }}>{TIER_LABEL[r.tier]}</span>
              </div>
              <div className={`lbx-val mono ${r.total >= 0 ? 'gain' : 'loss'}`}>{fmtPnL(r.total)}</div>
              <div className="lbx-num mono">{fmtUSD(r.exposureMean)}</div>
              <div className="lbx-num mono">{fmtUSD(r.volMean)}</div>
            </div>
          ))}
          {rows.length === 0 && (
            <div className="lbx-empty">No assets match — clear search or tier filter.</div>
          )}
        </div>
      </div>
    </div>
  );
}

// ============================================================
// 5. AssetSparkline (used inline in the leaderboard)
// ============================================================
function AssetSparkline({ sym, height = 28, width = 120 }) {
  const series = D.assetSeries[sym] || [];
  if (!series.length) return null;
  let cum = 0;
  const cs = series.map(r => { cum += r.pnl; return cum; });
  const min = Math.min(...cs), max = Math.max(...cs);
  const yScale = (v) => height - ((v - min) / (max - min || 1)) * (height - 4) - 2;
  const xScale = (i) => (i / Math.max(1, cs.length - 1)) * width;
  const d = `M ${cs.map((v, i) => `${xScale(i)},${yScale(v)}`).join(' L ')}`;
  const last = cs[cs.length - 1];
  return (
    <svg width={width} height={height} className="sparkline">
      <line x1="0" x2={width} y1={yScale(0)} y2={yScale(0)} stroke="var(--ink-3)" strokeDasharray="2 2" opacity="0.5" />
      <path d={d} stroke={last >= 0 ? 'var(--gain)' : 'var(--loss)'} strokeWidth="1.2" fill="none" />
    </svg>
  );
}

// ============================================================
// 6. CumulativeAssetPnL — distinct colors when filtered + asset legend + search + brush + hover
// ============================================================
function CumulativeAssetPnL({ height = 320, scope = 'top', tierFilter = 'all', topN = 8 }) {
  const [containerRef, w] = useResize();
  const [pinned, setPinned] = useState([]);     // user-pinned syms (override topN)
  const [hidden, toggleHidden] = useToggleSet(); // toggled-off syms (within current set)
  const [query, setQuery] = useState('');
  const [searchOpen, setSearchOpen] = useState(false);

  // Auto-pick from topN/scope/tier
  const autoSyms = useMemo(() => {
    let pool = D.assetTotals.slice();
    if (tierFilter !== 'all') pool = pool.filter(r => r.tier === tierFilter);
    if (scope === 'gainers') return pool.filter(r => r.total > 0).slice(0, topN).map(r => r.sym);
    if (scope === 'losers')  return pool.filter(r => r.total < 0).sort((a,b)=>a.total-b.total).slice(0, topN).map(r => r.sym);
    return pool.slice().sort((a,b)=>Math.abs(b.total)-Math.abs(a.total)).slice(0, topN).map(r => r.sym);
  }, [scope, tierFilter, topN]);

  // Effective syms: pinned overrides auto when present
  const effectiveSyms = pinned.length > 0 ? pinned : autoSyms;

  // Build cumulative series
  const lines = useMemo(() => {
    return effectiveSyms.map((sym, ix) => {
      const s = D.assetSeries[sym] || [];
      const meta = D.assets.find(a => a.sym === sym);
      let cum = 0;
      const cs = s.map(r => { cum += r.pnl; return cum; });
      // Color: by tier when 'all', distinct categorical per-line otherwise
      const color = tierFilter === 'all' && pinned.length === 0
        ? TIER_COLOR[meta ? meta.tier : 'major']
        : colorForSym(sym, ix);
      return { sym, tier: meta ? meta.tier : 'major', cs, color };
    });
  }, [effectiveSyms, tierFilter, pinned]);

  const visibleLines = lines.filter(l => !hidden.has(l.sym));

  const dates = D.dates;
  const padL = 64, padR = 110, padT = 16, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, dates.length);
  const x = (i) => padL + ((i - zoom.view[0]) / Math.max(1, zoom.view[1] - zoom.view[0])) * innerW;

  // Slice each line to view range for y-axis calc
  let yMin = 0, yMax = 0;
  visibleLines.forEach(l => {
    for (let i = zoom.view[0]; i <= zoom.view[1]; i++) {
      const v = l.cs[i];
      if (v < yMin) yMin = v;
      if (v > yMax) yMax = v;
    }
  });
  if (yMin === yMax) { yMin -= 1; yMax += 1; }
  const y = (v) => padT + innerH - ((v - yMin) / (yMax - yMin)) * innerH;
  const yTicks = niceTicks(yMin, yMax, 4);
  const xIxs = dateTicks(dates.slice(zoom.view[0], zoom.view[1] + 1), 7).map(i => i + zoom.view[0]);

  const hover = useHover(padL, W - padR, zoom.view[1] - zoom.view[0] + 1);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverGlobalIx = hover.ix != null ? hover.ix + zoom.view[0] : null;

  // Stagger end-labels
  const lastIx = zoom.view[1];
  const ends = visibleLines.map(l => ({ sym: l.sym, color: l.color, v: l.cs[lastIx], yv: y(l.cs[lastIx]) }));
  ends.sort((a, b) => a.yv - b.yv);
  const minGap = 12;
  for (let i = 1; i < ends.length; i++) {
    if (ends[i].yv - ends[i-1].yv < minGap) ends[i].yv = ends[i-1].yv + minGap;
  }

  // Search results: assets matching query, not already pinned
  const searchResults = useMemo(() => {
    if (!query.trim()) return [];
    const q = query.trim().toUpperCase();
    return D.assetTotals.filter(r => r.sym.includes(q) && !pinned.includes(r.sym)).slice(0, 12);
  }, [query, pinned]);

  const addPin = (sym) => {
    setPinned(prev => prev.includes(sym) ? prev : [...prev, sym]);
    setQuery('');
  };
  const removePin = (sym) => setPinned(prev => prev.filter(s => s !== sym));
  const clearPins = () => setPinned([]);

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={dates} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label="Cumulative PnL by asset"
           {...zoom.handlers} onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.5" />
            <text x={padL - 8} y={y(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtPnL(t)}</text>
          </g>
        ))}
        <line x1={padL} x2={W - padR} y1={y(0)} y2={y(0)} stroke="var(--ink-3)" />
        <EventMarkers dates={dates.slice(zoom.view[0], zoom.view[1] + 1)} x={(i) => x(i + zoom.view[0])} height={padT + innerH} />
        {visibleLines.map(l => {
          let pathD = '';
          for (let i = zoom.view[0]; i <= zoom.view[1]; i++) {
            const v = l.cs[i];
            pathD += (i === zoom.view[0] ? 'M ' : ' L ') + `${x(i)},${y(v)}`;
          }
          return <path key={l.sym} d={pathD} stroke={l.color} strokeWidth="1.3" fill="none" opacity="0.95" />;
        })}
        {ends.map(e => (
          <g key={e.sym}>
            <line x1={x(lastIx)} x2={x(lastIx) + 6} y1={y(e.v)} y2={e.yv} stroke={e.color} opacity="0.6" />
            <text x={x(lastIx) + 10} y={e.yv + 3} fontFamily="var(--mono)" fontSize="10" fill={e.color}>
              {e.sym} {fmtPnL(e.v)}
            </text>
          </g>
        ))}
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{monthLabel(dates[i])}</text>
        ))}
        <HoverCrosshair ix={hoverGlobalIx} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverGlobalIx != null} xPx={hoverGlobalIx != null ? x(hoverGlobalIx) : 0} containerW={W}>
        {hoverGlobalIx != null && (
          <div>
            <div className="tip-date">{fullDateLabel(dates[hoverGlobalIx])}</div>
            <div className="tip-rows tip-rows-scroll">
              {visibleLines
                .map(l => ({ sym: l.sym, color: l.color, v: l.cs[hoverGlobalIx] }))
                .sort((a, b) => Math.abs(b.v) - Math.abs(a.v))
                .slice(0, 12)
                .map(item => (
                  <div key={item.sym} className="tip-row">
                    <span className="tip-sw" style={{ background: item.color }}></span>
                    <span className="tip-lab mono">{item.sym}</span>
                    <span className="tip-val mono">{fmtPnL(item.v)}</span>
                  </div>
                ))}
              {visibleLines.length > 12 && <div className="tip-more">+{visibleLines.length - 12} more</div>}
            </div>
          </div>
        )}
      </Tooltip>

      {/* Asset legend + search */}
      <div className="asset-legend">
        <div className="asset-legend-search">
          <input
            type="text"
            className="lb-search-input"
            placeholder="Add asset (e.g. PUMP, LINK)…"
            value={query}
            onChange={(e) => { setQuery(e.target.value); setSearchOpen(true); }}
            onFocus={() => setSearchOpen(true)}
            onBlur={() => setTimeout(() => setSearchOpen(false), 150)}
          />
          {searchOpen && searchResults.length > 0 && (
            <div className="asset-legend-dropdown">
              {searchResults.map(r => (
                <button key={r.sym} className="asset-legend-result" onMouseDown={() => addPin(r.sym)}>
                  <span className="mono">{r.sym}</span>
                  <span className="tier-pill tier-pill-sm" style={{ background: TIER_COLOR[r.tier], color: 'var(--paper)' }}>{TIER_LABEL[r.tier]}</span>
                  <span className={`mono ${r.total >= 0 ? 'gain' : 'loss'}`} style={{ marginLeft: 'auto', fontSize: 11 }}>{fmtPnL(r.total)}</span>
                </button>
              ))}
            </div>
          )}
          {pinned.length > 0 && (
            <button className="asset-legend-clear" onClick={clearPins}>clear pins · use top {topN}</button>
          )}
        </div>
        <div className="asset-legend-chips">
          {lines.map(l => (
            <button
              key={l.sym}
              className={`asset-chip ${hidden.has(l.sym) ? 'is-off' : ''}`}
              onClick={() => toggleHidden(l.sym)}
              title="Click to toggle line"
            >
              <span className="asset-chip-sw" style={{ background: l.color }}></span>
              <span className="mono">{l.sym}</span>
              {pinned.includes(l.sym) && (
                <span className="asset-chip-x" onClick={(e) => { e.stopPropagation(); removePin(l.sym); }}>×</span>
              )}
            </button>
          ))}
          {lines.length === 0 && <span className="legend-hint">No assets selected</span>}
          {pinned.length > 0 && <span className="legend-hint">{pinned.length} pinned</span>}
        </div>
      </div>
    </div>
  );
}

// ============================================================
// 7. ShareTierChart — single tier mini-chart with avg + min/max band + brush + hover
// ============================================================
function ShareTierChart({ tier, metric, height = 220 }) {
  const [containerRef, w] = useResize(420);

  const dates = D.dates;
  const tierAssets = D.assets.filter(a => a.tier === tier).map(a => a.sym);
  const data = D.shareSeries[metric] || {};

  const allStats = useMemo(() => {
    return dates.map((d, i) => {
      const vals = tierAssets.map(s => data[s] ? data[s][i].share : null).filter(v => v != null);
      if (!vals.length) return { date: d, min: 0, max: 0, mean: 0, n: 0 };
      const min = Math.min(...vals);
      const max = Math.max(...vals);
      const mean = vals.reduce((s, v) => s + v, 0) / vals.length;
      return { date: d, min, max, mean, n: vals.length };
    });
  }, [tier, metric]);

  const padL = 44, padR = 12, padT = 14, padB = 24;
  const W = Math.max(280, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, dates.length);
  const stats = allStats.slice(zoom.view[0], zoom.view[1] + 1);
  const x = (i) => padL + (i / Math.max(1, stats.length - 1)) * innerW;

  const allMax = Math.max(...stats.map(s => s.max), 0.05);
  const yMax = Math.min(1, Math.max(0.05, allMax * 1.15));
  const y = (v) => padT + innerH - (v / yMax) * innerH;
  const yTicks = niceTicks(0, yMax, 3);
  const xIxs = dateTicks(stats.map(s => s.date), 4);

  const bandTop = stats.map((s, i) => `${x(i)},${y(s.max)}`).join(' L ');
  const bandBot = stats.slice().reverse().map((s, i) => `${x(stats.length - 1 - i)},${y(s.min)}`).join(' L ');
  const bandPath = `M ${bandTop} L ${bandBot} Z`;
  const meanPath = `M ${stats.map((s, i) => `${x(i)},${y(s.mean)}`).join(' L ')}`;

  const last = stats[stats.length - 1] || { mean: 0, min: 0, max: 0 };
  const color = TIER_COLOR[tier];

  const hover = useHover(padL, W - padR, stats.length);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverRow = hover.ix != null ? stats[hover.ix] : null;

  return (
    <div className="chart share-tile" ref={containerRef}>
      <div className="share-tile-head">
        <div className="share-tile-title">
          <span className="share-tile-swatch" style={{ background: color }}></span>
          <span>{TIER_LABEL[tier]}</span>
          <span className="share-tile-count">{tierAssets.length} assets</span>
        </div>
        <div className="share-tile-readout">
          <span className="share-tile-now">{(last.mean * 100).toFixed(1)}%</span>
          <span className="share-tile-range">range {(last.min * 100).toFixed(0)}–{(last.max * 100).toFixed(0)}%</span>
        </div>
      </div>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={dates} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label={`HLP ${metric} share — ${TIER_LABEL[tier]}`}
           {...zoom.handlers} onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={padL} x2={W - padR} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.4" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink-3)">{Math.round(t * 100)}%</text>
          </g>
        ))}
        <EventMarkers dates={stats} x={x} height={padT + innerH} />
        <path d={bandPath} fill={color} opacity="0.18" />
        <path d={meanPath} stroke={color} strokeWidth="1.4" fill="none" />
        <line x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} stroke="var(--ink-3)" />
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 6} textAnchor="middle" fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink-3)">{monthLabel(stats[i].date)}</text>
        ))}
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}</div>
            <div className="tip-rows">
              <div className="tip-row"><span className="tip-sw" style={{ background: color }}></span><span className="tip-lab">Mean</span><span className="tip-val mono">{(hoverRow.mean * 100).toFixed(2)}%</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: color, opacity: 0.4 }}></span><span className="tip-lab">Range</span><span className="tip-val mono">{(hoverRow.min * 100).toFixed(1)}–{(hoverRow.max * 100).toFixed(1)}%</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink-3)' }}></span><span className="tip-lab">Coins w/ data</span><span className="tip-val mono">{hoverRow.n}</span></div>
            </div>
          </div>
        )}
      </Tooltip>
    </div>
  );
}

// ============================================================
// 8. HLPShareHeadlineChart — system-overview share (vol-weighted vs per-coin median)
//    The 2%/50% asymmetry. Two y-axes: linear-stacked but each on its own scale.
// ============================================================
function HLPShareHeadlineChart({ height = 340 }) {
  const [containerRef, w] = useResize(600);
  const series = D.hlpShare || [];
  const padL = 64, padR = 64, padT = 36, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, series.length);
  const view = series.slice(zoom.view[0], zoom.view[1] + 1);
  const x = (i) => padL + (i / Math.max(1, view.length - 1)) * innerW;

  // Median scale (left): 0 → max(median, p75) of the visible window
  const medMax = Math.max(0.05, ...view.map(r => r.p75 ?? r.median ?? 0));
  const medY = (v) => padT + innerH - (v / medMax) * innerH;
  const medTicks = niceTicks(0, medMax, 4);

  // Vol-weighted scale (right): independent — typically ~0.02
  const vwMax = Math.max(0.005, ...view.map(r => r.vol_weighted ?? 0)) * 1.2;
  const vwY = (v) => padT + innerH - (v / vwMax) * innerH;
  const vwTicks = niceTicks(0, vwMax, 4);

  const validIdx = (key) => view.map((r, i) => r[key] != null ? i : -1).filter(i => i >= 0);
  const segPath = (key, yfn) => {
    const ix = validIdx(key);
    if (ix.length < 2) return '';
    let d = '';
    let prev = -2;
    ix.forEach(i => {
      d += (i === prev + 1 ? ' L ' : ' M ') + `${x(i)},${yfn(view[i][key])}`;
      prev = i;
    });
    return d.trim();
  };

  // IQR band (p25→p75) for median
  const bandTop = view.map((r, i) => r.p75 != null ? `${x(i)},${medY(r.p75)}` : null).filter(Boolean);
  const bandBot = view.slice().reverse().map((r, ii) => {
    const i = view.length - 1 - ii;
    return r.p25 != null ? `${x(i)},${medY(r.p25)}` : null;
  }).filter(Boolean);
  const bandPath = bandTop.length && bandBot.length
    ? `M ${bandTop.join(' L ')} L ${bandBot.join(' L ')} Z` : '';

  const xIxs = dateTicks(view.map(r => r.date), 8);

  const hover = useHover(padL, W - padR, view.length);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverRow = hover.ix != null ? view[hover.ix] : null;

  const last = (() => {
    for (let i = series.length - 1; i >= 0; i--) {
      if (series[i].median != null || series[i].vol_weighted != null) return series[i];
    }
    return {};
  })();

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={series.map(r => r.date)} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label="HLP share — system overview"
           {...zoom.handlers}
           onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {medTicks.map((t, i) => (
          <g key={`m${i}`}>
            <line x1={padL} x2={W - padR} y1={medY(t)} y2={medY(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.4" />
            <text x={padL - 8} y={medY(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{Math.round(t * 100)}%</text>
          </g>
        ))}
        {vwTicks.map((t, i) => (
          <text key={`v${i}`} x={W - padR + 8} y={vwY(t) + 3} textAnchor="start" fontFamily="var(--mono)" fontSize="10" fill="var(--accent)">{(t * 100).toFixed(1)}%</text>
        ))}
        <EventMarkers dates={view} x={x} height={padT + innerH} />
        {bandPath && <path d={bandPath} fill="var(--ink-2)" opacity="0.10" />}
        <path d={segPath('median', medY)} stroke="var(--ink)" strokeWidth="1.6" fill="none" />
        <path d={segPath('vol_weighted', vwY)} stroke="var(--accent)" strokeWidth="1.6" fill="none" strokeDasharray="0" />
        <line x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} stroke="var(--ink-3)" />
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{monthLabel(view[i].date)}</text>
        ))}
        <text x={padL} y={16} textAnchor="start" fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink)" letterSpacing="0.12em">PER-COIN MEDIAN</text>
        <text x={W - padR} y={16} textAnchor="end" fontFamily="var(--mono)" fontSize="9.5" fill="var(--accent)" letterSpacing="0.12em">VOL-WEIGHTED</text>
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}{hoverRow.uncertain ? <span className="tip-flag"> *</span> : null}</div>
            <div className="tip-rows">
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink)' }}></span><span className="tip-lab">Per-coin median</span><span className="tip-val mono">{hoverRow.median != null ? `${(hoverRow.median * 100).toFixed(1)}%` : '—'}</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink-2)', opacity: 0.4 }}></span><span className="tip-lab">P25–P75</span><span className="tip-val mono">{hoverRow.p25 != null ? `${(hoverRow.p25 * 100).toFixed(0)}–${(hoverRow.p75 * 100).toFixed(0)}%` : '—'}</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--accent)' }}></span><span className="tip-lab">Vol-weighted</span><span className="tip-val mono">{hoverRow.vol_weighted != null ? `${(hoverRow.vol_weighted * 100).toFixed(2)}%` : '—'}</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink-3)' }}></span><span className="tip-lab">Coins w/ data</span><span className="tip-val mono">{hoverRow.n_coins}</span></div>
            </div>
          </div>
        )}
      </Tooltip>
      <div className="share-headline-readout">
        <div className="hr-stat">
          <div className="hr-label">Latest median{last.date ? ` · ${shortDate(last.date)}` : ''}</div>
          <div className="hr-val mono">{last.median != null ? `${(last.median * 100).toFixed(1)}%` : '—'}</div>
        </div>
        <div className="hr-stat">
          <div className="hr-label">Latest vol-weighted</div>
          <div className="hr-val mono" style={{ color: 'var(--accent)' }}>{last.vol_weighted != null ? `${(last.vol_weighted * 100).toFixed(2)}%` : '—'}</div>
        </div>
        <div className="hr-stat">
          <div className="hr-label">Coins covered</div>
          <div className="hr-val mono">{last.n_coins ?? '—'}</div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// 9. UtilizationChart — Strategy A/B combined: TVL (left) + utilization (right)
//    The headline: TVL flat, utilization 2× at Nov ramp peak.
//    Hydromancer gap (Oct 24 → Dec 14) shown as dashed-fill segment.
// ============================================================
function UtilizationChart({ height = 360 }) {
  const [containerRef, w] = useResize(600);
  const series = D.utilizationSeries || [];
  const gap = D.hydromancerGap || {};

  const padL = 78, padR = 64, padT = 36, padB = 28;
  const W = Math.max(360, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  const zoom = useBrushZoom(padL, W - padR, series.length);
  const view = series.slice(zoom.view[0], zoom.view[1] + 1);
  const x = (i) => padL + (i / Math.max(1, view.length - 1)) * innerW;

  const tvlMax = Math.max(...view.map(r => r.tvl_combined ?? 0), 1);
  const utilMax = Math.max(...view.map(r => r.util_combined ?? 0), 0.1) * 1.1;
  const tvlY = (v) => padT + innerH - (v / tvlMax) * innerH;
  const utilY = (v) => padT + innerH - (v / utilMax) * innerH;
  const tvlTicks = niceTicks(0, tvlMax, 4);
  const utilTicks = niceTicks(0, utilMax, 4);

  // Build TVL path with split at gap boundaries (solid pre/post, dashed across gap)
  const buildPath = (yfn, key) => {
    if (!view.length) return { solid: '', dashed: '' };
    let solid = '', dashed = '';
    let curIsGap = null;
    let started = false;
    view.forEach((r, i) => {
      const isGap = r.in_hydro_gap;
      const cmd = (started && isGap === curIsGap) ? ' L ' : ' M ';
      const piece = `${cmd}${x(i)},${yfn(r[key])}`;
      if (isGap) dashed += piece; else solid += piece;
      curIsGap = isGap;
      started = true;
    });
    return { solid: solid.trim(), dashed: dashed.trim() };
  };
  const tvlPath = buildPath(tvlY, 'tvl_combined');
  const utilPath = buildPath(utilY, 'util_combined');
  const volMaxRaw = Math.max(...view.map(r => r.vol_combined ?? 0), 1);
  // We don't show volume directly to keep the chart readable — shown in tooltip only.

  const xIxs = dateTicks(view.map(r => r.date), 8);

  // Phase shading bounds
  const PHASES = [
    { id: 'cascade',   label: 'Cascade',   start: '2025-10-10', end: '2025-10-12', color: 'var(--accent)' },
    { id: 'ramp_peak', label: 'Ramp peak', start: '2025-11-03', end: '2025-11-16', color: 'var(--tier-major)' },
  ];
  const phaseRects = PHASES.map(p => {
    const i0 = view.findIndex(r => r.date >= p.start);
    let i1 = view.findIndex(r => r.date > p.end);
    if (i1 < 0) i1 = view.length;
    if (i0 < 0 || i1 <= i0) return null;
    return { ...p, x0: x(i0), x1: x(Math.max(0, i1 - 1)) };
  }).filter(Boolean);

  const hover = useHover(padL, W - padR, view.length);
  zoom.ref.current = containerRef.current;
  hover.ref.current = containerRef.current;
  const hoverRow = hover.ix != null ? view[hover.ix] : null;

  const last = (() => {
    for (let i = series.length - 1; i >= 0; i--) {
      if (series[i].tvl_combined != null || series[i].util_combined != null) return series[i];
    }
    return {};
  })();

  return (
    <div className="chart" ref={containerRef}>
      <ZoomBadge isZoomed={zoom.isZoomed} dates={series.map(r => r.date)} view={zoom.view} reset={zoom.reset} />
      <svg width={W} height={height} role="img" aria-label="Strategy A/B utilization vs TVL"
           {...zoom.handlers}
           onMouseMove={(e) => { zoom.handlers.onMouseMove(e); hover.handlers.onMouseMove(e); }}
           onMouseLeave={(e) => { zoom.handlers.onMouseLeave(e); hover.handlers.onMouseLeave(e); }}>
        {phaseRects.map(p => (
          <g key={p.id}>
            <rect x={p.x0} y={padT} width={Math.max(2, p.x1 - p.x0)} height={innerH} fill={p.color} opacity="0.06" />
            <text x={(p.x0 + p.x1) / 2} y={padT + 12} textAnchor="middle" fontFamily="var(--mono)" fontSize="9" fill={p.color} letterSpacing="0.12em" opacity="0.85">{p.label.toUpperCase()}</text>
          </g>
        ))}
        {tvlTicks.map((t, i) => (
          <g key={`t${i}`}>
            <line x1={padL} x2={W - padR} y1={tvlY(t)} y2={tvlY(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.4" />
            <text x={padL - 8} y={tvlY(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtUSD(t)}</text>
          </g>
        ))}
        {utilTicks.map((t, i) => (
          <text key={`u${i}`} x={W - padR + 8} y={utilY(t) + 3} textAnchor="start" fontFamily="var(--mono)" fontSize="10" fill="var(--accent)">{(t * 100).toFixed(0)}%</text>
        ))}
        <path d={tvlPath.solid} stroke="var(--ink)" strokeWidth="1.6" fill="none" />
        <path d={tvlPath.dashed} stroke="var(--ink)" strokeWidth="1.6" fill="none" strokeDasharray="3 3" opacity="0.55" />
        <path d={utilPath.solid} stroke="var(--accent)" strokeWidth="1.6" fill="none" />
        <path d={utilPath.dashed} stroke="var(--accent)" strokeWidth="1.6" fill="none" strokeDasharray="3 3" opacity="0.55" />
        {view.map((r, i) => r.tvl_source === 'vault_pnl_anchor' ? (
          <g key={`anc-${i}`}>
            <circle cx={x(i)} cy={tvlY(r.tvl_combined)} r="3.2" fill="var(--paper)" stroke="var(--ink)" strokeWidth="1.4" />
            <circle cx={x(i)} cy={utilY(r.util_combined)} r="3.2" fill="var(--paper)" stroke="var(--accent)" strokeWidth="1.4" />
          </g>
        ) : null)}
        <line x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} stroke="var(--ink-3)" />
        {xIxs.map(i => (
          <text key={i} x={x(i)} y={height - 8} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{monthLabel(view[i].date)}</text>
        ))}
        <text x={padL} y={16} textAnchor="start" fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink)" letterSpacing="0.12em">TVL · STRAT A+B</text>
        <text x={W - padR} y={16} textAnchor="end" fontFamily="var(--mono)" fontSize="9.5" fill="var(--accent)" letterSpacing="0.12em">UTILIZATION</text>
        <HoverCrosshair ix={hover.ix} x={x} top={padT} height={innerH} />
        <BrushRect drag={zoom.drag} height={padT + innerH} plotL={padL} plotR={W - padR} />
      </svg>
      <Tooltip visible={hoverRow != null} xPx={hover.ix != null ? x(hover.ix) : 0} containerW={W}>
        {hoverRow && (
          <div>
            <div className="tip-date">{fullDateLabel(hoverRow.date)}{hoverRow.tvl_source === 'vault_pnl_anchor' ? <span className="tip-flag"> · vault_pnl anchor</span> : hoverRow.tvl_source === 'interpolated' ? <span className="tip-flag"> · interp</span> : null}</div>
            <div className="tip-rows">
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink)' }}></span><span className="tip-lab">TVL combined</span><span className="tip-val mono">{hoverRow.tvl_combined != null ? fmtUSD(hoverRow.tvl_combined) : '—'}</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--accent)' }}></span><span className="tip-lab">Utilization</span><span className="tip-val mono">{hoverRow.util_combined != null ? `${(hoverRow.util_combined * 100).toFixed(0)}%` : '—'}</span></div>
              <div className="tip-row"><span className="tip-sw" style={{ background: 'var(--ink-3)' }}></span><span className="tip-lab">Daily volume</span><span className="tip-val mono">{hoverRow.vol_combined != null ? fmtUSD(hoverRow.vol_combined) : '—'}</span></div>
            </div>
          </div>
        )}
      </Tooltip>
      <div className="share-headline-readout">
        <div className="hr-stat">
          <div className="hr-label">Latest TVL{last.date ? ` · ${shortDate(last.date)}` : ''}</div>
          <div className="hr-val mono">{last.tvl_combined != null ? fmtUSD(last.tvl_combined) : '—'}</div>
        </div>
        <div className="hr-stat">
          <div className="hr-label">Latest util</div>
          <div className="hr-val mono" style={{ color: 'var(--accent)' }}>{last.util_combined != null ? `${(last.util_combined * 100).toFixed(0)}%` : '—'}</div>
        </div>
        <div className="hr-stat">
          <div className="hr-label">Latest volume</div>
          <div className="hr-val mono">{last.vol_combined != null ? fmtUSD(last.vol_combined) : '—'}</div>
        </div>
      </div>
      {gap.start && (
        <div className="util-gap-note">
          <span className="ugn-dash" />
          <span>Hydromancer gap {gap.start} → {gap.end} · TVL anchored to vault_pnl weekly snapshots (○), interpolated between</span>
        </div>
      )}
    </div>
  );
}

// ============================================================
// 10. CascadePhasesChart — bar comparison across the 5 phases.
//     Shows HLP daily volume + non-HLP long-tail participant counts side-by-side.
// ============================================================
function CascadePhasesChart({ height = 360 }) {
  const phases = D.cascadePhases || [];
  const [containerRef, w] = useResize(600);

  const padL = 88, padR = 24, padT = 20, padB = 110;
  const W = Math.max(320, w);
  const innerW = W - padL - padR;
  const innerH = height - padT - padB;

  if (!phases.length) return <div className="chart" ref={containerRef}>No cascade phase data.</div>;

  const groupCount = phases.length;
  const groupW = innerW / groupCount;
  const barW = (groupW - 16) / 2;

  const volMax = Math.max(...phases.map(p => p.hlp_volume_per_day ?? 0), 1);
  const partMax = Math.max(...phases.map(p => Math.max(p.nonhlp_makers_per_day ?? 0, p.nonhlp_takers_per_day ?? 0)), 1);

  const volY = (v) => padT + innerH - (v / volMax) * innerH;
  const partY = (v) => padT + innerH - (v / partMax) * innerH;
  const volTicks = niceTicks(0, volMax, 4);
  const partTicks = niceTicks(0, partMax, 4);

  return (
    <div className="chart" ref={containerRef}>
      <svg width={W} height={height} role="img" aria-label="Cascade phases — HLP volume vs non-HLP participant counts">
        {volTicks.map((t, i) => (
          <g key={`v${i}`}>
            <line x1={padL} x2={W - padR} y1={volY(t)} y2={volY(t)} stroke="var(--rule)" strokeDasharray="2 3" opacity="0.4" />
            <text x={padL - 8} y={volY(t) + 3} textAnchor="end" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-3)">{fmtUSD(t)}</text>
          </g>
        ))}
        {partTicks.map((t, i) => (
          <text key={`p${i}`} x={W - padR + 4} y={partY(t) + 3} textAnchor="start" fontFamily="var(--mono)" fontSize="10" fill="var(--tier-mid)">{Math.round(t)}</text>
        ))}
        <text x={padL - 8} y={padT - 6} textAnchor="end" fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink)" letterSpacing="0.1em">HLP $/DAY</text>
        <text x={W - padR + 4} y={padT - 6} textAnchor="start" fontFamily="var(--mono)" fontSize="9.5" fill="var(--tier-mid)" letterSpacing="0.1em">NON-HLP MAKERS</text>
        {phases.map((p, gi) => {
          const cx = padL + groupW * gi + groupW / 2;
          const x0 = cx - barW - 4;
          const x1 = cx + 4;
          return (
            <g key={p.id}>
              <rect x={x0} y={volY(p.hlp_volume_per_day || 0)} width={barW} height={padT + innerH - volY(p.hlp_volume_per_day || 0)} fill="var(--ink)" opacity="0.85" />
              <rect x={x1} y={partY(p.nonhlp_makers_per_day || 0)} width={barW} height={padT + innerH - partY(p.nonhlp_makers_per_day || 0)} fill="var(--tier-mid)" opacity="0.85" />
              <text x={x0 + barW / 2} y={padT + innerH + 14} textAnchor="middle" fontFamily="var(--mono)" fontSize="8.5" fill="var(--ink-3)" letterSpacing="0.1em">HLP $/DAY</text>
              <text x={x0 + barW / 2} y={padT + innerH + 28} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink)">{p.hlp_volume_per_day ? fmtUSD(p.hlp_volume_per_day) : '—'}</text>
              <text x={x1 + barW / 2} y={padT + innerH + 14} textAnchor="middle" fontFamily="var(--mono)" fontSize="8.5" fill="var(--ink-3)" letterSpacing="0.1em">MAKERS/DAY</text>
              <text x={x1 + barW / 2} y={padT + innerH + 28} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--tier-mid)">{p.nonhlp_makers_per_day ? p.nonhlp_makers_per_day.toLocaleString() : '—'}</text>
              <line x1={cx - groupW / 2 + 8} x2={cx + groupW / 2 - 8} y1={padT + innerH + 44} y2={padT + innerH + 44} stroke="var(--rule)" opacity="0.5" />
              <text x={cx} y={padT + innerH + 62} textAnchor="middle" fontFamily="var(--mono)" fontSize="10" fill="var(--ink-2)" letterSpacing="0.08em">{p.label.toUpperCase()}</text>
              <text x={cx} y={padT + innerH + 78} textAnchor="middle" fontFamily="var(--mono)" fontSize="9" fill="var(--ink-3)">{shortDate(p.start)}–{shortDate(p.end)}</text>
            </g>
          );
        })}
        <line x1={padL} x2={W - padR} y1={padT + innerH} y2={padT + innerH} stroke="var(--ink-3)" />
      </svg>
      <div className="phases-table">
        <div className="phases-row phases-head">
          <div>Phase</div>
          <div>HLP $/day</div>
          <div>Long-tail $/day</div>
          <div>Non-HLP makers</div>
          <div>Non-HLP takers</div>
          <div>System share (vw)</div>
          <div>System share (med)</div>
        </div>
        {phases.map(p => (
          <div key={p.id} className="phases-row">
            <div className="ph-name">{p.label}</div>
            <div className="mono">{p.hlp_volume_per_day ? fmtUSD(p.hlp_volume_per_day) : '—'}</div>
            <div className="mono">{p.longtail_volume_per_day ? fmtUSD(p.longtail_volume_per_day) : '—'}</div>
            <div className="mono">{p.nonhlp_makers_per_day || '—'}</div>
            <div className="mono">{p.nonhlp_takers_per_day || '—'}</div>
            <div className="mono">{p.system_share_vw != null ? `${(p.system_share_vw * 100).toFixed(2)}%` : '—'}</div>
            <div className="mono">{p.system_share_median != null ? `${(p.system_share_median * 100).toFixed(0)}%` : '—'}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ============================================================
// Exports
// ============================================================
Object.assign(window, {
  CompositionChart,
  CumulativeTierPnL,
  CumulativeAssetPnL,
  ShareTierChart,
  HLPShareHeadlineChart,
  UtilizationChart,
  CascadePhasesChart,
  DailyTierPnL,
  AssetLeaderboard,
  AssetSparkline,
  fmtUSD, fmtPnL,
  TIER_LABEL, TIER_COLOR, TIERS,
});
