// Vantage — shared tokens, formatters, and primitives.
// Fixed vanta-black look: black ground, white ink, magenta accent. Green and
// red are SEMANTIC (P&L only) and never used decoratively.

const VG = {
  bg: '#08080a', bg2: '#141118',
  ink: '#f4f2f7', ink2: '#cfcbd9', ink3: '#8f8a9e', ink4: '#5f5a6e',
  tile: '#151319', tile2: '#1b1720',
  accent: '#d946ef', accent2: '#f0abfc',
  up: '#4ade80', down: '#f87171', flat: '#8f8a9e',
  rule: 'rgba(244,242,247,0.10)', chip: 'rgba(244,242,247,0.07)',
  scrim: 'rgba(0,0,0,0.65)',
};

const vgS = {
  num: { fontFeatureSettings: '"tnum" 1', fontVariationSettings: '"wdth" 90', letterSpacing: '-0.02em' },
  serif: { fontFamily: '"Instrument Serif", "Newsreader", serif', fontStyle: 'italic', fontWeight: 400, letterSpacing: '-0.01em' },
  caps: { fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase', color: VG.ink3, fontWeight: 500 },
};

// ── Formatters ──
const vgMoney = (v, digits = 2) => v == null || !isFinite(v) ? '—'
  : (v < 0 ? '-' : '') + '$' + Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
const vgNum = (v, d = 2) => v == null || !isFinite(v) ? '—'
  : v.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: d });
const vgPct = (v, signed = true) => v == null || !isFinite(v) ? '—'
  : (signed && v > 0 ? '+' : '') + (v * 100).toFixed(2) + '%';
const vgDelta = v => v == null || !isFinite(v) || v === 0 ? VG.flat : v > 0 ? VG.up : VG.down;
const vgDate = s => {
  if (!s) return '—';
  const d = new Date(String(s).slice(0, 10) + 'T12:00:00');
  return isNaN(d) ? '—' : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};
const vgAgoDays = ts => Math.floor((Date.now() - new Date(ts).getTime()) / 86400000);

// ── API ──
async function vgGet(path) {
  const r = await fetch(path);
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || `HTTP ${r.status}`);
  return r.json();
}
// Writes carry the shared key (served by /config, same as the agents page).
// When the server has DASH_PASSWORD set, /config answers 401 until the caller
// presents it — prompt once, remember in localStorage, retry. No password on
// the server → the old zero-friction flow.
let vgKeyPromise = null;
async function vgFetchKey() {
  const headers = {};
  const savedPw = localStorage.getItem('vg.dashpw');
  if (savedPw) headers['x-dash-pw'] = savedPw;
  let r = await fetch('/config', { headers });
  if (r.status === 401) {
    const pw = window.prompt('dashboard password');
    if (!pw) return '';
    r = await fetch('/config', { headers: { 'x-dash-pw': pw } });
    if (r.status === 401) { window.alert('wrong password'); return ''; }
    localStorage.setItem('vg.dashpw', pw);
  }
  const d = await r.json();
  return d.apiKey || '';
}
function vgKey() {
  if (!vgKeyPromise) vgKeyPromise = vgFetchKey().catch(() => '');
  return vgKeyPromise;
}
async function vgSend(path, method, body) {
  const key = await vgKey();
  const r = await fetch(path, {
    method,
    headers: { 'Content-Type': 'application/json', ...(key ? { 'X-API-Key': key } : {}) },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || `HTTP ${r.status}`);
  return r.json();
}

// ── Primitives ──

function VgLine({ series, w = 560, h = 120, colors, labels }) {
  // series: array of number[] (already aligned); draws each normalized to the
  // shared min/max so relative movement is honest.
  const all = series.flat().filter(v => v != null && isFinite(v));
  if (!all.length) return null;
  const min = Math.min(...all), max = Math.max(...all), span = max - min || 1;
  return (
    <svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: 'block', overflow: 'visible' }}>
      {[0.25, 0.5, 0.75].map(f => (
        <line key={f} x1="0" x2={w} y1={h * f} y2={h * f} stroke={VG.rule} strokeWidth="0.7" />
      ))}
      {series.map((s, si) => {
        if (!s.length) return null;
        const pts = s.map((v, i) => [
          (i / Math.max(1, s.length - 1)) * (w - 2) + 1,
          h - 2 - ((v - min) / span) * (h - 4),
        ]);
        const d = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`).join(' ');
        return (
          <g key={si}>
            <path d={d} fill="none" stroke={colors[si]} strokeWidth={si === 0 ? 1.8 : 1.1}
              strokeLinecap="round" strokeLinejoin="round" opacity={si === 0 ? 1 : 0.75}
              strokeDasharray={si === 0 ? 'none' : '4 3'} />
            <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r={si === 0 ? 3 : 2.2} fill={colors[si]} />
          </g>
        );
      })}
    </svg>
  );
}

function VgBar({ pct, color = VG.accent, h = 4 }) {
  return (
    <div style={{ width: '100%', height: h, background: VG.rule, borderRadius: 999, overflow: 'hidden' }}>
      <div style={{ width: `${Math.max(0, Math.min(1, pct)) * 100}%`, height: '100%', background: color, borderRadius: 999 }} />
    </div>
  );
}

function VgTag({ children, color = VG.ink3, bg = VG.chip }) {
  return (
    <span style={{
      ...vgS.caps, color, background: bg, padding: '3px 8px', borderRadius: 999,
      display: 'inline-flex', alignItems: 'center', gap: 5, whiteSpace: 'nowrap',
    }}>{children}</span>
  );
}

function VgBtn({ children, onClick, kind = 'ghost', disabled, small, type }) {
  const styles = {
    primary: { background: VG.accent, color: '#0a0208', boxShadow: 'none' },
    ghost: { background: 'transparent', color: VG.ink2, boxShadow: `inset 0 0 0 1px ${VG.rule}` },
    danger: { background: 'transparent', color: VG.down, boxShadow: `inset 0 0 0 1px rgba(248,113,113,0.35)` },
  }[kind];
  return (
    <button type={type || 'button'} onClick={onClick} disabled={disabled} style={{
      appearance: 'none', border: 0, cursor: disabled ? 'default' : 'pointer',
      padding: small ? '6px 12px' : '9px 18px', borderRadius: 999,
      fontSize: small ? 12 : 13, fontWeight: 600, opacity: disabled ? 0.45 : 1,
      transition: 'opacity .15s', ...styles,
    }}>{children}</button>
  );
}

function VgField({ label, children, flex }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 5, flex: flex || 'initial', minWidth: 0 }}>
      <span style={vgS.caps}>{label}</span>
      {children}
    </label>
  );
}

const vgInputStyle = {
  appearance: 'none', border: 0, outline: 'none',
  background: VG.chip, color: VG.ink, borderRadius: 10,
  padding: '9px 12px', fontSize: 13.5, width: '100%', boxSizing: 'border-box',
};

function VgInput(props) {
  return <input {...props} style={{ ...vgInputStyle, ...vgS.num, ...(props.style || {}) }} />;
}

function VgModal({ title, onClose, children, width = 520 }) {
  React.useEffect(() => {
    const onKey = e => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 60, background: VG.scrim,
      display: 'grid', placeItems: 'center', padding: 20, boxSizing: 'border-box',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: '100%', maxWidth: width, maxHeight: '88vh', overflowY: 'auto',
        background: VG.bg2, borderRadius: 20, padding: '20px 22px',
        boxShadow: '0 24px 80px rgba(0,0,0,0.5)', boxSizing: 'border-box',
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 16 }}>
          <h2 style={{ ...vgS.serif, fontSize: 22, margin: 0, color: VG.ink }}>{title}</h2>
          <button onClick={onClose} aria-label="close" style={{
            appearance: 'none', border: 0, background: 'transparent', color: VG.ink3,
            fontSize: 16, cursor: 'pointer', padding: 4,
          }}>✕</button>
        </div>
        {children}
      </div>
    </div>
  );
}

const VG_SECTORS = [
  'Communication Services', 'Consumer Discretionary', 'Consumer Staples', 'Energy',
  'Financials', 'Health Care', 'Industrials', 'Information Technology',
  'Materials', 'Real Estate', 'Utilities',
];

// ── TradingView live chart (click-to-expand) ──────────────────────────────
// Raw index feeds (SP:SPX, TVC:VIX, USI:PC) are NOT licensed for free
// embeds — TradingView alerts "must be viewed on TradingView". Each subject
// therefore offers embeddable proxy feeds (broker CFDs + ETFs) with a
// switcher, so a dead feed always has a fallback.
const VG_TV = {
  '^GSPC': [['FOREXCOM:SPXUSD', 's&p cfd'], ['AMEX:SPY', 'spy etf']],
  '^IXIC': [['CAPITALCOM:US100', 'nasdaq cfd'], ['NASDAQ:QQQ', 'qqq etf']],
  '^DJI':  [['FOREXCOM:DJI', 'dow cfd'], ['AMEX:DIA', 'dia etf']],
  '^RUT':  [['CAPITALCOM:RTY', 'russell cfd'], ['AMEX:IWM', 'iwm etf']],
  '^TNX':  [['CBOT:ZN1!', '10y note future'], ['NASDAQ:TLT', 'tlt etf · moves inverse to yields']],
  VIX:     [['CAPITALCOM:VIX', 'vix cfd'], ['AMEX:VXX', 'vxx etn']],
};

function VgTVModal({ choices, label, onClose }) {
  const list = Array.isArray(choices) && choices.length ? choices : [['AMEX:SPY', 'spy']];
  const [sym, setSym] = React.useState(list[0][0]);
  const src = `https://s.tradingview.com/widgetembed/?symbol=${encodeURIComponent(sym)}` +
    `&interval=D&theme=dark&style=1&locale=en&hide_side_toolbar=1&withdateranges=1&saveimage=0&hide_volume=1&allow_symbol_change=1`;
  return (
    <VgModal title={`${label} · live`} onClose={onClose} width={960}>
      <div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 8, flexWrap: 'wrap' }}>
        {list.map(([s, l]) => (
          <button key={s} onClick={() => setSym(s)} style={{
            appearance: 'none', border: 0, cursor: 'pointer', borderRadius: 999,
            padding: '4px 12px', fontSize: 11, fontWeight: 600,
            background: sym === s ? VG.tile2 : VG.chip, color: sym === s ? VG.ink : VG.ink3,
          }}>{l}</button>
        ))}
        <span style={{ fontSize: 10.5, color: VG.ink4 }}>
          index feeds aren't licensed for embeds — these track the underlying
        </span>
      </div>
      <iframe key={sym} title={`${label} live chart`} src={src}
        style={{ width: '100%', height: 500, border: 0, borderRadius: 12, background: '#000' }} />
      <div style={{ ...vgS.caps, color: VG.ink4, marginTop: 8 }}>chart by tradingview · delayed on free feeds</div>
    </VgModal>
  );
}

// ── Interactive chart: price, history, hover — not just a line ────────────
// Last value + window change up top, min/max rails, crosshair with the
// value and date under the pointer.
function VgChartX({ data, labels, color = VG.accent, h = 120, fmt = v => (Math.abs(v) >= 100 ? v.toFixed(0) : v.toFixed(2)), caption }) {
  const [hover, setHover] = React.useState(null);
  const boxRef = React.useRef(null);
  if (!data || data.filter(v => v != null).length < 2) return null;
  const w = 560, padY = 8;
  const min = Math.min(...data), max = Math.max(...data), span = max - min || 1;
  const x = i => (i / (data.length - 1)) * (w - 2) + 1;
  const y = v => h - padY - ((v - min) / span) * (h - padY * 2);
  const path = data.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
  const area = `${path} L${(w - 1).toFixed(1)} ${h - 1} L1 ${h - 1} Z`;
  // % change is meaningless when the series crosses zero (yield spreads) —
  // show the absolute move instead.
  const crossesZero = min < 0 && max > 0;
  const chg = crossesZero ? null : (data[0] ? data[data.length - 1] / data[0] - 1 : null);
  const absChg = data[data.length - 1] - data[0];
  const hi = hover ?? data.length - 1;
  const onMove = e => {
    const rect = boxRef.current.getBoundingClientRect();
    const frac = (e.clientX - rect.left) / rect.width;
    setHover(Math.max(0, Math.min(data.length - 1, Math.round(frac * (data.length - 1)))));
  };
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 4 }}>
        <span style={{ ...vgS.num, fontSize: 15, fontWeight: 700, color }}>
          {fmt(data[hi])}
        </span>
        <span style={{ ...vgS.num, fontSize: 11, color: VG.ink3 }}>
          {labels?.[hi] ?? (hover != null ? `pt ${hi + 1}/${data.length}` : 'latest')}
        </span>
        <span style={{ flex: 1 }} />
        {chg != null && (
          <span style={{ ...vgS.num, fontSize: 11, color: vgDelta(chg) }}>{vgPct(chg)} over window</span>
        )}
        {crossesZero && (
          <span style={{ ...vgS.num, fontSize: 11, color: vgDelta(absChg) }}>{absChg > 0 ? '+' : ''}{fmt(absChg)} over window</span>
        )}
      </div>
      <div ref={boxRef} onMouseMove={onMove} onMouseLeave={() => setHover(null)} style={{ position: 'relative', cursor: 'crosshair' }}>
        <svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: 'block' }}>
          {[0.25, 0.5, 0.75].map(f => (
            <line key={f} x1="0" x2={w} y1={h * f} y2={h * f} stroke={VG.rule} strokeWidth="0.7" />
          ))}
          <path d={area} fill={color + '1f'} />
          <path d={path} fill="none" stroke={color} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" />
          <circle cx={x(hi)} cy={y(data[hi])} r="3.2" fill={color} />
          {hover != null && (
            <line x1={x(hi)} x2={x(hi)} y1="0" y2={h} stroke={VG.ink3} strokeWidth="0.7" strokeDasharray="3 3" />
          )}
          <text x="4" y="11" fill={VG.ink4} fontSize="9" style={{ fontVariantNumeric: 'tabular-nums' }}>{fmt(max)}</text>
          <text x="4" y={h - 4} fill={VG.ink4} fontSize="9" style={{ fontVariantNumeric: 'tabular-nums' }}>{fmt(min)}</text>
        </svg>
      </div>
      {caption && <div style={{ ...vgS.caps, color: VG.ink4, marginTop: 3 }}>{caption}</div>}
    </div>
  );
}

function VgLiveBtn({ onClick }) {
  return (
    <button onClick={onClick} className="vg-chip" style={{
      appearance: 'none', border: 0, cursor: 'pointer', borderRadius: 999,
      padding: '3px 10px', fontSize: 10, fontWeight: 700, letterSpacing: '0.08em',
      background: VG.chip, color: VG.accent2, textTransform: 'uppercase',
    }}>live ↗</button>
  );
}

// ── Notes: the learning loop ──────────────────────────────────────────────
// An observation plus a snapshot of the signal at that moment. Prior notes
// on the same subject show in the modal, and recent notes feed the insight
// engine's prompt — Vanta reads what you noticed.

function VgNoteBtn({ onClick, title = 'add a note' }) {
  return (
    <button onClick={onClick} className="vg-chip" title={title} aria-label={title} style={{
      appearance: 'none', border: 0, cursor: 'pointer', borderRadius: 999,
      padding: '3px 9px', fontSize: 11, background: VG.chip, color: VG.ink3,
    }}>✎</button>
  );
}

function VgNoteModal({ kind, subjectKey, label, snapshot, onClose }) {
  const [draft, setDraft] = React.useState('');
  const [prior, setPrior] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    vgGet(`/vantage/api/notes?kind=${encodeURIComponent(kind)}&key=${encodeURIComponent(subjectKey)}`)
      .then(d => setPrior(d.notes)).catch(() => setPrior([]));
  }, [kind, subjectKey]);
  const save = async () => {
    const t = draft.trim();
    if (!t) return;
    setBusy(true); setErr(null);
    try {
      await vgSend('/vantage/api/note', 'POST', { kind, key: subjectKey, note: t, snapshot });
      onClose();
    } catch (ex) { setErr(ex.message); setBusy(false); }
  };
  return (
    <VgModal title={`Note · ${label}`} onClose={onClose} width={480}>
      <div style={{ fontSize: 11.5, color: VG.ink3, marginBottom: 10, lineHeight: 1.5 }}>
        What looks out of character, and why? The signal's current state is snapshotted with your note,
        and recent notes inform Vanta's insight blurbs.
      </div>
      <textarea value={draft} onChange={e => setDraft(e.target.value)} rows={3} autoFocus
        placeholder="e.g. VIX pinned under 15 while three supply-chain stories broke — feels mispriced…"
        style={{ ...vgInputStyle, resize: 'vertical', lineHeight: 1.5, marginBottom: 10 }} />
      {err && <div style={{ color: VG.down, fontSize: 12, marginBottom: 8 }}>{err}</div>}
      <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: prior?.length ? 14 : 0 }}>
        <VgBtn kind="primary" onClick={save} disabled={busy || !draft.trim()}>save note</VgBtn>
      </div>
      {prior?.length > 0 && (
        <React.Fragment>
          <div style={{ ...vgS.caps, marginBottom: 6 }}>earlier notes on this</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {prior.map(n => (
              <div key={n.id} style={{ background: VG.chip, borderRadius: 10, padding: '8px 12px', fontSize: 12.5, color: VG.ink2, lineHeight: 1.45 }}>
                {n.note}
                <div style={{ ...vgS.num, fontSize: 10, color: VG.ink4, marginTop: 3 }}>{vgDate(n.created_at)}</div>
              </div>
            ))}
          </div>
        </React.Fragment>
      )}
    </VgModal>
  );
}

// Pill range/window toggle — one component so the bench and risk toggles
// (and any future window pickers) can't drift apart.
function VgRangeToggle({ options, value, onChange }) {
  return (
    <div style={{ display: 'flex', gap: 2, background: VG.chip, borderRadius: 999, padding: 2 }}>
      {options.map(r => (
        <button key={r} onClick={() => onChange(r)} style={{
          appearance: 'none', border: 0, cursor: 'pointer', fontSize: 11,
          padding: '3px 10px', borderRadius: 999,
          background: value === r ? VG.tile2 : 'transparent',
          color: value === r ? VG.ink : VG.ink3, fontWeight: value === r ? 600 : 400,
        }}>{r}</button>
      ))}
    </div>
  );
}

// Blurb line — the "synapse": current behavior in a sentence or two.
function VgBlurb({ text }) {
  if (!text) return null;
  return (
    <div style={{
      marginTop: 10, paddingLeft: 10, borderLeft: `2px solid ${VG.accent}`,
      fontSize: 12, color: VG.ink2, lineHeight: 1.55,
    }}>{text}</div>
  );
}

// Quiet reminder line, out of the way at a card's foot.
function VgTip({ text }) {
  if (!text) return null;
  return (
    <div style={{ fontSize: 10.5, color: VG.ink4, marginTop: 9, lineHeight: 1.4 }}>
      <span style={{ ...vgS.caps, marginRight: 5 }}>tip</span>{text}
    </div>
  );
}

// ── The fold: pulse's Level-2 layer ──
// An always-visible one-line summary bar; the body mounts ONLY while open, so
// a folded layer costs nothing (no iframe, no globe, no long list). Smart-open:
// `notable` opens a layer on its own the first time something warrants it, but
// an explicit user choice (open or fold) is remembered and always wins.
const VG_LAYERS_KEY = 'vantage.layers.v1';
const VG_SEEN_KEY = 'vantage.seen.v1';       // introKeys whose "new" tag was dismissed by a first open
const VG_HINTED_KEY = 'vantage.layers.hinted'; // set once any fold is ever toggled
function vgLayersRead() {
  try { return JSON.parse(localStorage.getItem(VG_LAYERS_KEY) || '{}'); } catch { return {}; }
}
function vgSeenRead() {
  try { return JSON.parse(localStorage.getItem(VG_SEEN_KEY) || '{}'); } catch { return {}; }
}
function vgMarkSeen(key) {
  try { const s = vgSeenRead(); s[key] = 1; localStorage.setItem(VG_SEEN_KEY, JSON.stringify(s)); } catch {}
}
// introKey: mark a freshly-unlocked layer "new" until its first open — the
// one-time introduction moment for a feature that just earned its place.
// hintFirst: on this one bar, a one-time "tap to go deeper" coach word that
// clears forever once the user has toggled ANY fold.
// byline: "tended by <agent> · <their last report> · 3m ago" — shown inside
// the opened fold so a section wears the name of the worker that keeps it.
// openNonce: bump it to force the fold open (a cross-section handoff like
// "read deeper" landing in the analyst) — the user's saved choice still
// governs every later visit.
function VgLayer({ k, title, summary, notable = false, defaultOpen = false, introKey = null, hintFirst = false, byline = null, openNonce = null, children }) {
  const [open, setOpen] = React.useState(() => {
    const saved = vgLayersRead()[k];
    return typeof saved === 'boolean' ? saved : (defaultOpen || !!notable);
  });
  const [isNew, setIsNew] = React.useState(() => !!introKey && !vgSeenRead()[introKey]);
  const [hinted, setHinted] = React.useState(() => { try { return localStorage.getItem(VG_HINTED_KEY) === '1'; } catch { return true; } });
  // Data often lands after mount — if this layer BECOMES notable and the user
  // has never chosen for it, open it; a saved choice is never overridden.
  React.useEffect(() => {
    if (notable && vgLayersRead()[k] === undefined) setOpen(true);
  }, [notable, k]);
  React.useEffect(() => { if (openNonce) setOpen(true); }, [openNonce]);
  const toggle = () => {
    try { localStorage.setItem(VG_HINTED_KEY, '1'); } catch {}
    setHinted(true);
    if (introKey && isNew) { vgMarkSeen(introKey); setIsNew(false); }
    setOpen(o => {
      const next = !o;
      try { const s = vgLayersRead(); s[k] = next; localStorage.setItem(VG_LAYERS_KEY, JSON.stringify(s)); } catch {}
      return next;
    });
  };
  return (
    <section style={{ marginBottom: 14 }}>
      <button onClick={toggle} aria-expanded={open} style={{
        appearance: 'none', border: 0, cursor: 'pointer', width: '100%', boxSizing: 'border-box',
        background: VG.tile, borderRadius: 18, padding: '11px 18px', fontFamily: 'inherit',
        display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left',
      }}>
        <span style={{ ...vgS.caps, flex: '0 0 auto' }}>{title}</span>
        {isNew && <span style={{ ...vgS.caps, color: VG.accent2, background: 'rgba(217,70,239,0.12)', borderRadius: 999, padding: '2px 8px', flex: '0 0 auto' }}>new</span>}
        {notable && !open && <span title="something notable inside" style={{ width: 6, height: 6, borderRadius: '50%', background: VG.accent, flex: '0 0 auto' }} />}
        <span style={{ fontSize: 11.5, color: VG.ink3, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{summary}</span>
        <span style={{ ...vgS.caps, color: VG.ink4, flex: '0 0 auto' }}>
          {open ? 'fold ↑' : hintFirst && !hinted ? 'open ↓ · tap to go deeper' : 'open ↓'}
        </span>
      </button>
      {open && (
        <div style={{ marginTop: 10 }}>
          {byline && <div style={{ ...vgS.caps, color: VG.ink4, margin: '0 2px 8px', letterSpacing: '0.12em' }}>{byline}</div>}
          {children}
        </div>
      )}
    </section>
  );
}

// Compose a fold byline from the staff roster: the tending agent's name, its
// own last report, and freshness — or null when it hasn't reported (no
// invented "all good").
function vgByline(staff, key, verb = 'tended by') {
  const a = staff?.agents?.find(x => x.key === key);
  if (!a || !a.at) return null;
  const when = a.agoMin == null ? '' : a.agoMin < 1 ? 'just now' : a.agoMin < 60 ? `${a.agoMin}m ago` : `${Math.round(a.agoMin / 60)}h ago`;
  return `${verb} ${a.name} · ${a.note || ''}${when ? ` · ${when}` : ''}`;
}

// Client mirror of featureIntro (src/vantage-market.js, unit-tested) — a
// feature earns its place when it has real data to stand on; until then one
// quiet line says what unlocks it. Kept in sync with the pure version.
function vgFeatureIntro({ claimCount = 0, snapshotCount = 0, bookCount = 0, holdingCount = 0, scoredCount = 0 } = {}) {
  return {
    hitRate: scoredCount > 0
      ? { on: true }
      : { on: false, hint: 'unlocks with your first scored call — reply to the 7:30 brief and the grader scores it at 4:30.' },
    ledger: claimCount > 0 || snapshotCount > 0
      ? { on: true }
      : { on: false, hint: 'unlocks with your first tracked call — pick a globe hotspot and follow the market impact, or let the first daily snapshot land.' },
    bookLens: bookCount > 0
      ? { on: true }
      : { on: false, hint: 'hold or watch a name and the globe gains a “my book” lens' },
    corrGrid: holdingCount >= 3
      ? { on: true }
      : { on: false, hint: `correlation needs three names to triangulate — ${Math.max(0, 3 - holdingCount)} more and the grid appears.` },
  };
}

// The locked twin of VgLayer: a feature that hasn't earned its place yet.
// One quiet line — the name and what unlocks it. Not a control, a promise.
function VgLocked({ title, hint }) {
  return (
    <section style={{ marginBottom: 14 }}>
      <div style={{
        background: 'transparent', border: `1px dashed ${VG.rule}`, borderRadius: 18,
        padding: '9px 18px', display: 'flex', alignItems: 'center', gap: 10,
      }}>
        <span style={{ ...vgS.caps, color: VG.ink4, flex: '0 0 auto' }}>{title}</span>
        <span style={{ fontSize: 11.5, color: VG.ink4, flex: 1, minWidth: 0 }}>{hint}</span>
      </div>
    </section>
  );
}

Object.assign(window, {
  VG, vgS, vgMoney, vgNum, vgPct, vgDelta, vgDate, vgAgoDays,
  vgGet, vgSend, VgLine, VgBar, VgTag, VgBtn, VgField, VgInput, VgModal,
  vgInputStyle, VG_SECTORS,
  VG_TV, VgTVModal, VgLiveBtn, VgNoteBtn, VgNoteModal, VgBlurb, VgTip, VgChartX,
  VgRangeToggle, VgLayer, VgLocked, vgFeatureIntro, vgByline,
});
