// Main page: discipline score + sparkline, sector / risk / holdings tabs,
// inline expansion (no modal) when a rule tile is clicked.

const { useState: useStateM, useMemo: useMemoM, useEffect: useEffectM } = React;

const RISK_TYPES = new Set(['strategy', 'risk_bucket']);
const SECTOR_TYPES = new Set(['sector']);

// ─── Score history (localStorage, with 30-day mock backfill) ──
const HISTORY_KEY = 'portfolio_blueprint_score_history';

function loadHistory() {
  try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; }
}
function saveHistory(h) {
  try { localStorage.setItem(HISTORY_KEY, JSON.stringify(h)); } catch {}
}
function todayKey() { return new Date().toISOString().slice(0, 10); }

function backfillMock(currentScore) {
  // Random walk backwards from current, 29 days.
  const today = new Date();
  let v = currentScore;
  const out = [{ date: today.toISOString().slice(0, 10), score: currentScore }];
  for (let i = 1; i < 30; i++) {
    v = v + (Math.random() - 0.5) * 6;
    v = Math.max(45, Math.min(92, Math.round(v)));
    const d = new Date(today);
    d.setDate(d.getDate() - i);
    out.unshift({ date: d.toISOString().slice(0, 10), score: v });
  }
  return out;
}

function useScoreHistory(currentScore) {
  const [history, setHistory] = useStateM([]);
  useEffectM(() => {
    if (!Number.isFinite(currentScore)) return;
    let h = loadHistory();
    if (h.length === 0) h = backfillMock(currentScore);
    // upsert today's score
    const today = todayKey();
    const idx = h.findIndex(e => e.date === today);
    if (idx >= 0) h[idx] = { date: today, score: currentScore };
    else h.push({ date: today, score: currentScore });
    // trim to last 90 days
    if (h.length > 90) h = h.slice(-90);
    saveHistory(h);
    setHistory(h);
  }, [currentScore]);
  return history;
}

// ─── Main page ────────────────────────────────────────────
function MainPage({ data: initialData }) {
  const [positions, setPositions] = useStateM(initialData.positions);
  const [blueprint, setBlueprint] = useStateM(initialData.blueprint);
  const [tab, setTab] = useStateM('sector');
  const [openRule, setOpenRule] = useStateM(null);
  const [sheet, setSheet] = useStateM(null);
  const [dirty, setDirty] = useStateM(false);
  const [saveStatus, setSaveStatus] = useStateM(null); // null | 'saving' | { mode, message }

  // Persist on any change
  useEffectM(() => {
    window.savePortfolio({ positions, blueprint });
  }, [positions, blueprint]);

  function addHolding(item, editIndex) {
    if (editIndex != null) {
      setPositions(prev => prev.map((p, i) => i === editIndex ? item : p));
    } else {
      setPositions(prev => [...prev, item]);
    }
    setDirty(true);
  }

  function deleteHolding(index) {
    setPositions(prev => prev.filter((_, i) => i !== index));
    setDirty(true);
  }

  function saveRule(rule, idx) {
    if (idx == null || idx < 0) {
      setBlueprint(prev => [...prev, rule]);
    } else {
      // Cascade rename: if group_name changed, update matching positions
      const old = blueprint[idx];
      if (old && old.group_name !== rule.group_name) {
        const fieldMap = { sector: 'sector', strategy: 'strategy', position: 'ticker' };
        const field = fieldMap[rule.group_type];
        if (field) {
          const oldName = old.group_name;
          const newName = rule.group_name;
          setPositions(prev => prev.map(p =>
            p[field] === oldName ? { ...p, [field]: newName } : p
          ));
        }
      }
      setBlueprint(prev => prev.map((r, i) => i === idx ? rule : r));
    }
    setDirty(true);
  }

  function deleteRule(idx) {
    setBlueprint(prev => prev.filter((_, i) => i !== idx));
    setOpenRule(null);
    setDirty(true);
  }

  async function handleSaveToDisk() {
    setSaveStatus('saving');
    try {
      const result = await window.saveToDisk({ positions, blueprint });
      setSaveStatus(result);
      setDirty(false);
      // auto-clear status after a few seconds
      setTimeout(() => setSaveStatus(null), 5000);
    } catch (e) {
      setSaveStatus({ mode: 'error', message: e.message || 'save failed' });
      setTimeout(() => setSaveStatus(null), 5000);
    }
  }

  function resetAll() {
    if (window.confirm('Reset all edits and reload from CSV files?')) {
      window.resetPortfolio();
      window.location.reload();
    }
  }

  async function refreshAllPrices() {
    // Get all non-cash tickers that have shares
    const tickers = positions.filter(p => p.ticker !== 'CASH' && p.shares > 0).map(p => p.ticker);
    if (!tickers.length) return;

    // Fetch all prices in one call
    const r = await fetch(`/api/prices?tickers=${encodeURIComponent(tickers.join(','))}`, { cache: 'no-store' });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const prices = await r.json();
    if (prices.error) throw new Error(prices.error);

    // Update positions with new values
    setPositions(prev => prev.map(p => {
      if (p.ticker === 'CASH' || !p.shares) return p;
      const price = prices[p.ticker];
      if (price == null) return p;
      return { ...p, amount_usd: Math.round(p.shares * price * 100) / 100 };
    }));
    setDirty(true);
  }

  function openEditRule(check) {
    const idx = blueprint.findIndex(r => r.group_type === check.group_type && r.group_name === check.group_name);
    if (idx < 0) return;
    setSheet({ kind: 'rule', index: idx, initial: blueprint[idx] });
  }

  const checks = useMemoM(() => {
    const raw = window.evaluateBlueprint(positions, blueprint, null);
    return raw.sort((a, b) => b.pct - a.pct);
  }, [positions, blueprint]);

  const totalValue = positions.reduce((s, p) => s + p.amount_usd, 0);
  const onPlan = checks.filter(c => c.status === 'on').length;
  const score  = Math.round((onPlan / checks.length) * 100);

  const sectorChecks = checks.filter(c => SECTOR_TYPES.has(c.group_type));
  const riskChecks   = checks.filter(c => RISK_TYPES.has(c.group_type));

  const sectorPass = sectorChecks.filter(c => c.status === 'on').length;
  const riskPass   = riskChecks.filter(c => c.status === 'on').length;

  return (
    <div style={mStyles.root}>
      <div style={mStyles.headerBar}>
        <span style={mStyles.brandDot} />
        <span style={mStyles.brand}>PORTFOLIO BLUEPRINT</span>
        <span style={mStyles.brandSlash}>/</span>
        <span style={mStyles.brandSub}>discipline desk</span>
        <div style={{ flex: 1 }} />
        <span style={mStyles.headerMeta}>TOTAL {window.fmtMoney(totalValue)}</span>
        <span style={mStyles.headerMeta}>·</span>
        <span style={mStyles.headerMeta}>{positions.length} POSITIONS</span>
        <span style={mStyles.headerMeta}>·</span>
        <span style={mStyles.headerMeta}>{checks.length} RULES</span>
        <span style={mStyles.headerMeta}>·</span>
        <span style={mStyles.headerLive}><span style={mStyles.liveDot} /> LIVE</span>
      </div>

      <div style={mStyles.body}>
        {/* HERO STRIP */}
        <div style={mStyles.heroRow}>
          <DisciplineScoreCard
            score={score}
            onPlan={onPlan}
            over={checks.filter(c => c.status === 'over').length}
            under={checks.filter(c => c.status === 'under').length}
            total={checks.length}
          />
          <CategoryCard
            label="sector"
            pass={sectorPass}
            total={sectorChecks.length}
            active={tab === 'sector'}
            onClick={() => setTab('sector')}
            checks={sectorChecks}
          />
          <CategoryCard
            label="risk"
            pass={riskPass}
            total={riskChecks.length}
            active={tab === 'risk'}
            onClick={() => setTab('risk')}
            checks={riskChecks}
          />
        </div>

        {/* TAB BAR */}
        <div style={mStyles.tabBar}>
          <TabBtn active={tab === 'sector'} onClick={() => setTab('sector')}>
            sector ({sectorChecks.length})
          </TabBtn>
          <TabBtn active={tab === 'risk'} onClick={() => setTab('risk')}>
            risk ({riskChecks.length})
          </TabBtn>
          <TabBtn active={tab === 'holdings'} onClick={() => setTab('holdings')}>
            holdings ({positions.length})
          </TabBtn>
          <div style={{ flex: 1 }} />
          <button style={mStyles.headerAction} onClick={() => setSheet('addHolding')}>+ add holding</button>
          <button style={mStyles.headerAction} onClick={() => setSheet({ kind: 'rule', index: null, initial: null })}>+ add rule</button>
          <button
            style={{
              ...mStyles.headerAction,
              background: dirty ? 'rgba(74,222,128,0.15)' : window.PALETTE.bg2,
              borderColor: dirty ? window.PALETTE.ok : window.PALETTE.border,
              color: dirty ? window.PALETTE.ok : window.PALETTE.inkHi,
            }}
            onClick={handleSaveToDisk}
            disabled={saveStatus === 'saving'}
            title={dirty ? 'Unsaved changes' : 'Save portfolio'}
          >
            {saveStatus === 'saving' ? '↻ saving…' : dirty ? '↓ save ●' : '↓ save'}
          </button>
          <button style={mStyles.headerActionGhost} onClick={resetAll} title="Reset all edits to CSV">↻ reset</button>
          <a href="pages/diff.html" style={mStyles.altLink}>
            ▸ diverging bars
          </a>
        </div>

        {tab === 'sector' && (
          <SectorPanel
            checks={sectorChecks}
            positions={positions}
            totalValue={totalValue}
            openRule={openRule}
            setOpenRule={setOpenRule}
            onEditRule={openEditRule}
          />
        )}
        {tab === 'risk' && (
          <RiskPanel
            checks={riskChecks}
            positions={positions}
            totalValue={totalValue}
            openRule={openRule}
            setOpenRule={setOpenRule}
            onEditRule={openEditRule}
          />
        )}
        {tab === 'holdings' && (
          <HoldingsPanel
            positions={positions}
            blueprint={blueprint}
            checks={checks}
            totalValue={totalValue}
            onEdit={(index) => setSheet({ kind: 'editHolding', index, initial: positions[index] })}
            onRefreshPrices={refreshAllPrices}
          />
        )}
      </div>

      {sheet === 'addHolding' && (
        <window.AddHoldingSheet
          existingPositions={positions}
          blueprint={blueprint}
          onSave={addHolding}
          onDelete={deleteHolding}
          onClose={() => setSheet(null)}
        />
      )}
      {sheet && sheet.kind === 'editHolding' && (
        <window.AddHoldingSheet
          existingPositions={positions}
          blueprint={blueprint}
          onSave={addHolding}
          onDelete={deleteHolding}
          onClose={() => setSheet(null)}
          initialHolding={sheet.initial}
          editIndex={sheet.index}
        />
      )}
      {sheet && sheet.kind === 'rule' && (
        <window.RuleSheet
          initialRule={sheet.initial}
          initialIndex={sheet.index}
          positions={positions}
          blueprint={blueprint}
          onSave={saveRule}
          onDelete={deleteRule}
          onClose={() => setSheet(null)}
        />
      )}

      {saveStatus && typeof saveStatus === 'object' && (
        <div style={{
          ...mStyles.toast,
          borderColor: saveStatus.mode === 'error' ? window.PALETTE.bad
                      : saveStatus.mode === 'download' ? window.PALETTE.warn
                      : window.PALETTE.ok,
          color: saveStatus.mode === 'error' ? window.PALETTE.bad
                      : saveStatus.mode === 'download' ? window.PALETTE.warn
                      : window.PALETTE.ok,
        }}>
          {saveStatus.mode === 'server' && <><b>✓ saved</b> · {(saveStatus.saved || []).join(', ')}</>}
          {saveStatus.mode === 'download' && <><b>↓ downloaded</b> · {saveStatus.message}</>}
          {saveStatus.mode === 'error' && <><b>⚠ save failed</b> · {saveStatus.message}</>}
        </div>
      )}
    </div>
  );
}

// ─── Discipline Score Card with Sparkline ─────────────────
function DisciplineScoreCard({ score, onPlan, over, under, total }) {
  const history = useScoreHistory(score);

  // Compute 7-day delta
  const trend7d = history.length >= 8
    ? history[history.length - 1].score - history[history.length - 8].score
    : 0;

  // Sparkline path
  const W = 100, H = 40; // viewBox; SVG will scale via preserveAspectRatio
  let path = '', fillPath = '', lastX = 0, lastY = 0;
  if (history.length >= 2) {
    const scores = history.map(h => h.score);
    const min = Math.min(...scores);
    const max = Math.max(...scores);
    const range = Math.max(1, max - min);
    const pts = history.map((h, i) => {
      const x = (i / (history.length - 1)) * W;
      const y = H - ((h.score - min) / range) * (H - 6) - 3;
      return [x, y];
    });
    path = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' ');
    fillPath = `${path} L ${W} ${H} L 0 ${H} Z`;
    [lastX, lastY] = pts[pts.length - 1];
  }

  return (
    <div style={mStyles.scoreCard}>
      <div style={mStyles.scoreHead}>
        <span style={mStyles.cardLabel}>discipline score</span>
        {history.length > 7 && (
          <span style={{
            fontFamily: 'IBM Plex Mono, monospace', fontSize: 11,
            color: trend7d >= 0 ? window.PALETTE.ok : window.PALETTE.bad,
            whiteSpace: 'nowrap',
          }}>
            {trend7d >= 0 ? '↑' : '↓'} {trend7d >= 0 ? '+' : ''}{trend7d} · 7d
          </span>
        )}
      </div>

      <div style={mStyles.scoreRow}>
        <div style={{ ...mStyles.scoreBig, color: scoreColor(score) }}>{score}</div>
        <div style={mStyles.scoreOver}>/ 100</div>
        <div style={mStyles.scoreSparkWrap}>
          {history.length >= 2 && (
            <svg width="100%" height={H} viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
              <defs>
                <linearGradient id="sparkfill" x1="0" x2="0" y1="0" y2="1">
                  <stop offset="0%" stopColor={scoreColor(score)} stopOpacity="0.25" />
                  <stop offset="100%" stopColor={scoreColor(score)} stopOpacity="0" />
                </linearGradient>
              </defs>
              <path d={fillPath} fill="url(#sparkfill)" />
              <path d={path} fill="none" stroke={scoreColor(score)} strokeWidth={1.2} vectorEffect="non-scaling-stroke" />
              <circle cx={lastX} cy={lastY} r={1.6} fill={scoreColor(score)} />
            </svg>
          )}
        </div>
      </div>

      <div style={mStyles.sparkScale}>
        {history.length >= 7 && (
          <>
            <span>30d ago</span>
            <span>low {Math.min(...history.map(h => h.score))} · high {Math.max(...history.map(h => h.score))}</span>
            <span>today</span>
          </>
        )}
      </div>

      <div style={mStyles.scoreCounts}>
        <span><b style={{ color: window.PALETTE.ok }}>{onPlan}</b> on plan</span>
        <span><b style={{ color: window.PALETTE.bad }}>{over}</b> over</span>
        <span><b style={{ color: window.PALETTE.warn }}>{under}</b> under</span>
      </div>
    </div>
  );
}

// ─── Category Card ────────────────────────────────────────
function CategoryCard({ label, pass, total, active, onClick, checks }) {
  const allPass = pass === total;
  const color = allPass ? window.PALETTE.ok : window.PALETTE.warn;
  return (
    <button onClick={onClick} style={{
      ...mStyles.catCard,
      borderColor: active ? window.PALETTE.borderHi : window.PALETTE.border,
      background: active ? window.PALETTE.bg2 : window.PALETTE.bg1,
    }}>
      <div style={mStyles.catTop}>
        <span style={mStyles.cardLabel}>{label}</span>
        {active && <span style={mStyles.catActiveDot}>●</span>}
      </div>
      <div style={mStyles.catNumRow}>
        <span style={{ ...mStyles.catBig, color }}>{pass}</span>
        <span style={mStyles.catOver}>/ {total}</span>
        <span style={mStyles.catSub}>passing</span>
      </div>
      <div style={mStyles.catBars}>
        {checks.map((c, i) => (
          <div key={i} style={{
            ...mStyles.catBar,
            background: c.status === 'over' ? window.PALETTE.bad
                      : c.status === 'under' ? window.PALETTE.warn
                      : window.PALETTE.ok,
            opacity: 0.85,
          }} />
        ))}
      </div>
    </button>
  );
}

// ─── Tab ─────────────────────────────────────────────────
function TabBtn({ active, onClick, children }) {
  return (
    <button onClick={onClick} style={{
      ...mStyles.tabBtn,
      color: active ? window.PALETTE.inkHi : window.PALETTE.inkMid,
      borderBottomColor: active ? window.PALETTE.phosphor : 'transparent',
    }}>
      {children}
    </button>
  );
}

// ─── Sector Panel ────────────────────────────────────────
function SectorPanel({ checks, positions, totalValue, openRule, setOpenRule, onEditRule }) {
  const bySector = {};
  positions.forEach(p => {
    const k = p.sector || 'Uncategorized';
    bySector[k] = (bySector[k] || 0) + p.amount_usd;
  });
  const ruleByName = Object.fromEntries(checks.map(c => [c.group_name.toLowerCase(), c]));

  const allSectors = Object.entries(bySector)
    .map(([name, amt]) => ({
      name, amt, pct: (amt / totalValue) * 100,
      rule: ruleByName[name.toLowerCase()] || null,
    }))
    .sort((a, b) => b.amt - a.amt);

  return (
    <>
      <SectionLabel>
        <span>sector allocations</span>
        <span style={mStyles.sectionLabelMeta}>
          {checks.filter(c => c.status === 'on').length} of {checks.length} inside band
        </span>
      </SectionLabel>
      <AllocationTape items={allSectors} />
      <SectionLabel>
        <span>blueprint compliance</span>
        <span style={mStyles.sectionLabelMeta}>click a card to see contributors</span>
      </SectionLabel>
      <RuleGrid checks={checks} positions={positions} openRule={openRule} setOpenRule={setOpenRule} onEditRule={onEditRule} />
    </>
  );
}

// ─── Risk Panel ───────────────────────────────────────────
function RiskPanel({ checks, positions, totalValue, openRule, setOpenRule, onEditRule }) {
  const byStrategy = {};
  positions.forEach(p => {
    const k = p.strategy || 'Uncategorized';
    byStrategy[k] = (byStrategy[k] || 0) + p.amount_usd;
  });
  const strategyItems = Object.entries(byStrategy).map(([name, amt]) => {
    const rule = checks.find(c => c.group_type === 'strategy' && c.group_name.toLowerCase() === name.toLowerCase());
    return { name, amt, pct: (amt / totalValue) * 100, rule: rule || null };
  }).sort((a, b) => b.amt - a.amt);

  const byBucket = {};
  positions.forEach(p => {
    const k = p.risk_bucket || 'Uncategorized';
    byBucket[k] = (byBucket[k] || 0) + p.amount_usd;
  });
  const bucketItems = Object.entries(byBucket).map(([name, amt]) => {
    const rule = checks.find(c => c.group_type === 'risk_bucket' && c.group_name.toLowerCase() === name.toLowerCase());
    return { name, amt, pct: (amt / totalValue) * 100, rule: rule || null };
  }).sort((a, b) => b.amt - a.amt);

  const strategyChecks = checks.filter(c => c.group_type === 'strategy');
  const bucketChecks   = checks.filter(c => c.group_type === 'risk_bucket');

  return (
    <>
      <SectionLabel>
        <span>by strategy <span style={mStyles.sectionLabelHint}>(growth / spec / dividend)</span></span>
        <span style={mStyles.sectionLabelMeta}>
          {strategyChecks.filter(c => c.status === 'on').length} of {strategyChecks.length} inside band
        </span>
      </SectionLabel>
      <AllocationTape items={strategyItems} />
      <RuleGrid checks={strategyChecks} positions={positions} openRule={openRule} setOpenRule={setOpenRule} onEditRule={onEditRule} />

      <div style={{ height: 18 }} />

      <SectionLabel>
        <span>by risk bucket <span style={mStyles.sectionLabelHint}>(core / speculative)</span></span>
        <span style={mStyles.sectionLabelMeta}>
          {bucketChecks.filter(c => c.status === 'on').length} of {bucketChecks.length} inside band
        </span>
      </SectionLabel>
      <AllocationTape items={bucketItems} />
      <RuleGrid checks={bucketChecks} positions={positions} openRule={openRule} setOpenRule={setOpenRule} onEditRule={onEditRule} />
    </>
  );
}

// ─── Holdings Panel ──────────────────────────────────────
function HoldingsPanel({ positions, blueprint, checks, totalValue, onEdit, onRefreshPrices }) {
  const [query, setQuery] = useStateM('');
  const [refreshing, setRefreshing] = useStateM(false);
  const [sortCol, setSortCol] = useStateM('value'); // ticker | company | sector | strategy | value | pct
  const [sortDir, setSortDir] = useStateM('desc');   // asc | desc

  function toggleSort(col) {
    if (sortCol === col) {
      setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    } else {
      setSortCol(col);
      setSortDir(col === 'ticker' || col === 'company' || col === 'sector' || col === 'strategy' ? 'asc' : 'desc');
    }
  }

  function sortArrow(col) {
    if (sortCol !== col) return '';
    return sortDir === 'asc' ? ' ▲' : ' ▼';
  }

  // Position-rule lookup
  const posRules = Object.fromEntries(
    checks.filter(c => c.group_type === 'position').map(c => [c.group_name.toUpperCase(), c])
  );

  const filtered = positions.filter(p =>
    !query || p.ticker.toLowerCase().includes(query.toLowerCase())
            || (p.company || '').toLowerCase().includes(query.toLowerCase())
            || (p.sector  || '').toLowerCase().includes(query.toLowerCase())
            || (p.strategy|| '').toLowerCase().includes(query.toLowerCase())
  );

  const sorted = [...filtered].sort((a, b) => {
    const dir = sortDir === 'asc' ? 1 : -1;
    if (sortCol === 'ticker')   return dir * a.ticker.localeCompare(b.ticker);
    if (sortCol === 'company')  return dir * (a.company || '').localeCompare(b.company || '');
    if (sortCol === 'sector')   return dir * (a.sector || '').localeCompare(b.sector || '');
    if (sortCol === 'strategy') return dir * (a.strategy || '').localeCompare(b.strategy || '');
    if (sortCol === 'value')    return dir * (a.amount_usd - b.amount_usd);
    if (sortCol === 'pct')      return dir * (a.amount_usd - b.amount_usd);
    return 0;
  });

  async function handleRefresh() {
    setRefreshing(true);
    try {
      await onRefreshPrices();
    } finally {
      setRefreshing(false);
    }
  }

  return (
    <>
      <SectionLabel>
        <span>holdings</span>
        <span style={mStyles.sectionLabelMeta}>{filtered.length} of {positions.length}</span>
      </SectionLabel>

      <div style={mStyles.holdingsToolbar}>
        <input
          type="search"
          placeholder="filter ticker, company, sector…"
          value={query}
          onChange={e => setQuery(e.target.value)}
          style={mStyles.holdingsSearch}
        />
        <button
          onClick={handleRefresh}
          disabled={refreshing}
          style={{
            ...mStyles.refreshBtn,
            opacity: refreshing ? 0.5 : 1,
            cursor: refreshing ? 'wait' : 'pointer',
          }}
        >
          {refreshing ? '↻ updating prices…' : '↻ refresh prices'}
        </button>
      </div>

      <div style={mStyles.holdingsTableCard}>
        <div style={{ ...mStyles.holdingsRow, ...mStyles.holdingsHead }}>
          <span style={{ ...mStyles.sortableCol, width: 72 }} onClick={() => toggleSort('ticker')}>ticker{sortArrow('ticker')}</span>
          <span style={{ ...mStyles.sortableCol, flex: 1 }} onClick={() => toggleSort('company')}>company{sortArrow('company')}</span>
          <span style={{ ...mStyles.sortableCol, ...mStyles.colTag }} onClick={() => toggleSort('sector')}>sector{sortArrow('sector')}</span>
          <span style={{ ...mStyles.sortableCol, ...mStyles.colTag }} onClick={() => toggleSort('strategy')}>strategy{sortArrow('strategy')}</span>
          <span style={{ ...mStyles.sortableCol, ...mStyles.colNum }} onClick={() => toggleSort('value')}>value{sortArrow('value')}</span>
          <span style={{ ...mStyles.sortableCol, ...mStyles.colPct }} onClick={() => toggleSort('pct')}>% total{sortArrow('pct')}</span>
          <span style={mStyles.colCap}>cap status</span>
          <span style={mStyles.colEdit} />
        </div>
        {sorted.length === 0 && (
          <div style={mStyles.holdingsEmpty}>No matches.</div>
        )}
        {sorted.map(p => {
          const originalIndex = positions.indexOf(p);
          const pct = (p.amount_usd / totalValue) * 100;
          const rule = posRules[p.ticker];
          let capLabel = '—', capColor = window.PALETTE.inkLo;
          if (rule) {
            if (pct > rule.max_pct) {
              capLabel = `▲ over ${rule.max_pct}%`;
              capColor = window.PALETTE.bad;
            } else if (pct > rule.max_pct * 0.85) {
              capLabel = `near cap ${rule.max_pct}%`;
              capColor = window.PALETTE.warn;
            } else {
              capLabel = `● cap ${rule.max_pct}%`;
              capColor = window.PALETTE.ok;
            }
          }
          return (
            <div key={p.ticker} style={mStyles.holdingsRow}>
              <span style={mStyles.colTicker}>{p.ticker}</span>
              <span style={mStyles.colCo}>{p.company || '—'}</span>
              <span style={mStyles.colTag}>{p.sector || '—'}</span>
              <span style={mStyles.colTag}>{p.strategy || '—'}</span>
              <span style={mStyles.colNum}>{window.fmtMoney(p.amount_usd)}</span>
              <span style={mStyles.colPct}>{pct.toFixed(1)}%</span>
              <span style={{ ...mStyles.colCap, color: capColor }}>{capLabel}</span>
              <span style={mStyles.colEdit}>
                <button onClick={() => onEdit(originalIndex)} style={mStyles.editRowBtn}>✎ edit</button>
              </span>
            </div>
          );
        })}
      </div>
    </>
  );
}

// ─── Section label ───────────────────────────────────────
function SectionLabel({ children }) {
  return <div style={mStyles.sectionLabel}>{children}</div>;
}

// ─── Allocation Tape ────────────────────────────────────
function AllocationTape({ items }) {
  const COLORS_NEUTRAL = ['#3b485c', '#4a5874', '#37425a', '#42506a', '#384358', '#475670', '#3e495f'];
  return (
    <div style={mStyles.tape}>
      <div style={mStyles.tapeBar}>
        {items.map((it, i) => {
          const status = it.rule ? it.rule.status : null;
          const color = status === 'over' ? window.PALETTE.bad
                     : status === 'under' ? window.PALETTE.warn
                     : status === 'on' ? window.PALETTE.ok
                     : COLORS_NEUTRAL[i % COLORS_NEUTRAL.length];
          const isOpaque = !!it.rule;
          return (
            <div key={it.name} style={{
              ...mStyles.tapeSeg, width: `${it.pct}%`, background: color,
              opacity: isOpaque ? 1 : 0.45,
            }} title={`${it.name}: ${it.pct.toFixed(1)}%`}>
              {it.pct >= 6 && (
                <>
                  <div style={mStyles.tapeSegName}>{it.name}</div>
                  <div style={mStyles.tapeSegPct}>{it.pct.toFixed(1)}%</div>
                </>
              )}
            </div>
          );
        })}
      </div>
      <div style={mStyles.tapeLegend}>
        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ ...mStyles.legendSwatch, background: window.PALETTE.ok }} /> inside band
        </span>
        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ ...mStyles.legendSwatch, background: window.PALETTE.bad }} /> over max
        </span>
        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ ...mStyles.legendSwatch, background: window.PALETTE.warn }} /> under min
        </span>
        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ ...mStyles.legendSwatch, background: '#42506a', opacity: 0.65 }} /> no rule
        </span>
      </div>
    </div>
  );
}

// ─── Rule Grid (rows with inline expansion below the row) ──
function RuleGrid({ checks, positions, openRule, setOpenRule, onEditRule }) {
  // We need to know how many columns the grid has at the current width.
  // Use a ref to measure, default to 3.
  const gridRef = React.useRef(null);
  const [cols, setCols] = React.useState(3);

  React.useEffect(() => {
    if (!gridRef.current) return;
    const measure = () => {
      const w = gridRef.current.offsetWidth;
      // matches: repeat(auto-fill, minmax(360px, 1fr))
      setCols(Math.max(1, Math.floor((w + 14) / (360 + 14))));
    };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, []);

  // Chunk checks into rows of `cols` items
  const rows = [];
  for (let i = 0; i < checks.length; i += cols) {
    rows.push(checks.slice(i, i + cols));
  }

  // Find which check is expanded
  const expandedCheck = openRule ? checks.find(c => `${c.group_type}:${c.group_name}` === openRule) : null;

  return (
    <div ref={gridRef}>
      {rows.map((row, ri) => {
        // Is the expanded tile in this row?
        const expandedInRow = row.find(c => `${c.group_type}:${c.group_name}` === openRule);
        return (
          <React.Fragment key={ri}>
            <div style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 14, marginBottom: expandedInRow ? 0 : 14 }}>
              {row.map(c => {
                const key = `${c.group_type}:${c.group_name}`;
                return (
                  <RuleTile
                    key={key}
                    check={c}
                    positions={positions}
                    expanded={openRule === key}
                    onToggle={() => setOpenRule(openRule === key ? null : key)}
                    onEdit={() => onEditRule(c)}
                    showExpanded={false}
                  />
                );
              })}
            </div>
            {expandedInRow && (
              <div style={{ marginBottom: 14 }}>
                <RuleTileExpanded
                  check={expandedInRow}
                  positions={positions}
                  onEdit={() => onEditRule(expandedInRow)}
                />
              </div>
            )}
          </React.Fragment>
        );
      })}
    </div>
  );
}

// ─── Expanded content (rendered below the row) ──────────
function RuleTileExpanded({ check, positions, onEdit }) {
  const c = check;
  const accent = c.status === 'over' ? window.PALETTE.bad
              : c.status === 'under' ? window.PALETTE.warn
              : window.PALETTE.ok;
  const contribs = window.contributors(positions, c.group_type, c.group_name);

  return (
    <div style={{
      background: window.PALETTE.bg2,
      border: `1px solid ${window.PALETTE.borderHi}`,
      borderRadius: 10,
      overflow: 'hidden',
    }}>
      <div style={{ ...mStyles.ruleExpanded, borderTop: 'none', padding: '14px 18px 18px' }}>
        <div style={mStyles.expandedToolbar}>
          <span style={mStyles.expandedLabel}>
            top contributors · {contribs.length} positions in this rule
          </span>
          <button style={mStyles.editRuleBtn} onClick={e => { e.stopPropagation(); onEdit && onEdit(); }}>
            ✎ edit rule
          </button>
        </div>
        <div style={mStyles.contribList}>
          {contribs.map(p => {
            const ruleShare = c.amount > 0 ? (p.amount_usd / c.amount) * 100 : 0;
            return (
              <div key={p.ticker} style={mStyles.contribRow}>
                <span style={mStyles.contribTicker}>{p.ticker}</span>
                <span style={mStyles.contribCo}>{p.company || ''}</span>
                <div style={mStyles.contribBarHost}>
                  <div style={{ ...mStyles.contribBar, width: `${ruleShare}%`, background: accent }} />
                </div>
                <span style={mStyles.contribVal}>{window.fmtMoney(p.amount_usd)}</span>
                <span style={mStyles.contribShare}>{ruleShare.toFixed(0)}%</span>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ─── Rule Tile (with inline expansion) ─────────────────
function RuleTile({ check, positions, expanded, onToggle, onEdit, showExpanded = true }) {
  const c = check;
  const accent = c.status === 'over' ? window.PALETTE.bad
              : c.status === 'under' ? window.PALETTE.warn
              : window.PALETTE.ok;

  const hi = Math.max(c.max_pct || c.target_pct || 10, c.pct, 10) * 1.15;
  const pctLeft = pos => Math.min(98, Math.max(0, (pos / hi) * 100));
  const driftStr = c.drift >= 0 ? '+' + c.drift.toFixed(1) : c.drift.toFixed(1);

  const action = c.status === 'over' ? `Trim ${window.fmtMoney(c.actionDollars)}`
               : c.status === 'under' ? `Add ${window.fmtMoney(c.actionDollars)}`
               : 'Inside band.';
  const actionDetail = c.status === 'over'
    ? `Over ${c.max_pct}% max by ${(c.pct - c.max_pct).toFixed(1)}pp.`
    : c.status === 'under'
    ? `Under ${c.min_pct}% min by ${(c.min_pct - c.pct).toFixed(1)}pp.`
    : `${Math.abs(c.target_pct - c.pct).toFixed(1)}pp from target.`;

  const contribs = expanded ? window.contributors(positions, c.group_type, c.group_name) : [];

  return (
    <div style={{
      ...mStyles.ruleTile,
      background: expanded ? window.PALETTE.bg2 : window.PALETTE.bg1,
      borderColor: expanded ? window.PALETTE.borderHi : window.PALETTE.border,
    }}>
      <button onClick={onToggle} style={mStyles.ruleHeadBtn}>
        <div style={mStyles.ruleTopRow}>
          <div style={mStyles.ruleNameRow}>
            <span style={mStyles.ruleType}>
              {c.group_type === 'risk_bucket' ? 'risk bucket' : c.group_type}
            </span>
            <span style={mStyles.ruleName}>{c.group_name}</span>
          </div>
          <span style={{ ...mStyles.ruleStatus, color: accent }}>
            {c.status === 'over' ? '▲ OVER' : c.status === 'under' ? '▼ UNDER' : '● ON PLAN'}
          </span>
        </div>

        <div style={mStyles.ruleStatsRow}>
          <span style={{ ...mStyles.rulePct, color: accent }}>{c.pct.toFixed(1)}%</span>
          <span style={mStyles.ruleDriftPiece}>{driftStr}pp <span style={mStyles.ruleDriftLbl}>vs target</span></span>
        </div>

        <div style={mStyles.ruleBar}>
          <div style={{
            ...mStyles.ruleBarBand,
            left: `${pctLeft(c.min_pct)}%`,
            width: `${pctLeft(c.max_pct) - pctLeft(c.min_pct)}%`,
          }} />
          <div style={{ ...mStyles.ruleBarTarget, left: `${pctLeft(c.target_pct)}%` }} />
          <div style={{ ...mStyles.ruleBarMarker, left: `${pctLeft(c.pct)}%`, background: accent }} />
        </div>
        <div style={mStyles.ruleScale}>
          <span>{c.min_pct}%</span>
          <span>target {c.target_pct}%</span>
          <span>{c.max_pct}%</span>
        </div>

        <div style={{ ...mStyles.ruleActionLine, borderColor: window.PALETTE.border }}>
          <div style={{ ...mStyles.ruleAction, color: accent }}>{action}</div>
          <div style={mStyles.ruleActionDetail}>
            {actionDetail}
            <span style={mStyles.ruleExpand}>{expanded ? '✕ collapse' : '▸ contributors'}</span>
          </div>
        </div>
      </button>

      {expanded && showExpanded && (
        <div style={mStyles.ruleExpanded}>
          <div style={mStyles.expandedToolbar}>
            <span style={mStyles.expandedLabel}>
              top contributors · {contribs.length} positions in this rule
            </span>
            <button style={mStyles.editRuleBtn} onClick={e => { e.stopPropagation(); onEdit && onEdit(); }}>
              ✎ edit rule
            </button>
          </div>
          <div style={mStyles.contribList}>
            {contribs.map(p => {
              const ruleShare = c.amount > 0 ? (p.amount_usd / c.amount) * 100 : 0;
              return (
                <div key={p.ticker} style={mStyles.contribRow}>
                  <span style={mStyles.contribTicker}>{p.ticker}</span>
                  <span style={mStyles.contribCo}>{p.company || ''}</span>
                  <div style={mStyles.contribBarHost}>
                    <div style={{ ...mStyles.contribBar, width: `${ruleShare}%`, background: accent }} />
                  </div>
                  <span style={mStyles.contribVal}>{window.fmtMoney(p.amount_usd)}</span>
                  <span style={mStyles.contribShare}>{ruleShare.toFixed(0)}%</span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function scoreColor(s) {
  if (s >= 80) return window.PALETTE.ok;
  if (s >= 60) return window.PALETTE.warn;
  return window.PALETTE.bad;
}

// ─── Styles ───────────────────────────────────────────────
const mStyles = {
  root: { width: '100%', minHeight: '100vh', background: window.PALETTE.bg0, color: window.PALETTE.inkHi, fontFamily: 'Inter, system-ui, sans-serif', display: 'flex', flexDirection: 'column' },
  headerBar: { display: 'flex', alignItems: 'center', gap: 10, padding: '14px 28px', borderBottom: `1px solid ${window.PALETTE.border}`, fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, letterSpacing: '0.08em', color: window.PALETTE.inkMid, textTransform: 'uppercase', whiteSpace: 'nowrap' },
  brandDot: { width: 8, height: 8, borderRadius: '50%', background: window.PALETTE.phosphor, boxShadow: `0 0 8px ${window.PALETTE.phosphor}` },
  brand: { color: window.PALETTE.inkHi, fontWeight: 600 },
  brandSlash: { color: window.PALETTE.inkLo },
  brandSub: { color: window.PALETTE.inkMid },
  headerMeta: { color: window.PALETTE.inkMid },
  headerLive: { color: window.PALETTE.ok, display: 'flex', alignItems: 'center', gap: 6 },
  liveDot: { width: 6, height: 6, borderRadius: '50%', background: window.PALETTE.ok, boxShadow: `0 0 6px ${window.PALETTE.ok}` },

  body: { padding: '24px 28px 40px', maxWidth: 1480, margin: '0 auto', width: '100%', boxSizing: 'border-box' },

  heroRow: { display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr', gap: 16, marginBottom: 22 },

  cardLabel: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.15em', whiteSpace: 'nowrap' },

  scoreCard: { background: window.PALETTE.bg1, border: `1px solid ${window.PALETTE.border}`, borderRadius: 12, padding: 24 },
  scoreHead: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
  scoreRow: { display: 'grid', gridTemplateColumns: 'auto auto 1fr', alignItems: 'center', gap: 10, marginTop: 6 },
  scoreBig: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 76, fontWeight: 500, lineHeight: 1, letterSpacing: '-0.04em' },
  scoreOver: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 22, color: window.PALETTE.inkLo, fontWeight: 400, alignSelf: 'baseline' },
  scoreSparkWrap: { height: 40, minWidth: 0 },
  sparkScale: { display: 'flex', justifyContent: 'space-between', fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, marginTop: 6, minHeight: 14, whiteSpace: 'nowrap' },
  scoreCounts: { display: 'flex', gap: 18, marginTop: 8, fontFamily: 'IBM Plex Mono, monospace', fontSize: 12, color: window.PALETTE.inkMid, whiteSpace: 'nowrap' },

  catCard: { background: window.PALETTE.bg1, border: `1px solid ${window.PALETTE.border}`, borderRadius: 12, padding: 22, color: 'inherit', font: 'inherit', textAlign: 'left', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 14, transition: 'all 0.15s' },
  catTop: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
  catActiveDot: { color: window.PALETTE.phosphor, fontSize: 10 },
  catNumRow: { display: 'flex', alignItems: 'baseline', gap: 8 },
  catBig: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 56, fontWeight: 500, lineHeight: 1, letterSpacing: '-0.04em' },
  catOver: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 20, color: window.PALETTE.inkLo },
  catSub: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 12, color: window.PALETTE.inkLo, marginLeft: 'auto' },
  catBars: { display: 'flex', gap: 4, height: 6 },
  catBar: { flex: 1, borderRadius: 2 },

  tabBar: { display: 'flex', alignItems: 'center', gap: 4, borderBottom: `1px solid ${window.PALETTE.border}`, marginBottom: 22 },
  tabBtn: { background: 'transparent', border: 'none', borderBottom: '2px solid transparent', padding: '12px 18px', fontFamily: 'IBM Plex Mono, monospace', fontSize: 13, letterSpacing: '0.05em', cursor: 'pointer', transition: 'all 0.15s', marginBottom: -1, whiteSpace: 'nowrap' },
  altLink: { color: window.PALETTE.inkLo, textDecoration: 'none', padding: '12px 14px', fontFamily: 'IBM Plex Mono, monospace', fontSize: 12, letterSpacing: '0.05em', whiteSpace: 'nowrap' },
  headerAction: { background: window.PALETTE.bg2, color: window.PALETTE.inkHi, border: `1px solid ${window.PALETTE.border}`, padding: '7px 12px', borderRadius: 6, fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, letterSpacing: '0.05em', cursor: 'pointer', whiteSpace: 'nowrap', marginLeft: 6 },
  headerActionGhost: { background: 'transparent', color: window.PALETTE.inkLo, border: `1px solid ${window.PALETTE.border}`, padding: '7px 12px', borderRadius: 6, fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, letterSpacing: '0.05em', cursor: 'pointer', whiteSpace: 'nowrap', marginLeft: 6 },
  toast: {
    position: 'fixed', bottom: 24, right: 24, padding: '12px 16px',
    background: window.PALETTE.bg2, border: '1px solid', borderRadius: 8,
    fontFamily: 'IBM Plex Mono, monospace', fontSize: 12,
    maxWidth: 480, zIndex: 2000,
    boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
  },

  sectionLabel: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '14px 0 10px', fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.15em', whiteSpace: 'nowrap' },
  sectionLabelMeta: { color: window.PALETTE.inkMid },
  sectionLabelHint: { color: window.PALETTE.inkLo, textTransform: 'none', letterSpacing: 0, marginLeft: 6 },

  tape: { marginBottom: 22 },
  tapeBar: { display: 'flex', height: 92, borderRadius: 10, overflow: 'hidden', border: `1px solid ${window.PALETTE.border}` },
  tapeSeg: { display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '12px', minWidth: 0, overflow: 'hidden', borderRight: `1px solid rgba(6,8,11,0.5)` },
  tapeSegName: { fontFamily: 'Inter', fontSize: 12, fontWeight: 600, color: '#0a0d12', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
  tapeSegPct: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, color: '#0a0d12', opacity: 0.75 },
  tapeLegend: { display: 'flex', gap: 18, marginTop: 8, fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.1em' },
  legendSwatch: { width: 10, height: 10, borderRadius: 2, display: 'inline-block' },

  ruleTile: { borderRadius: 10, border: '1px solid', overflow: 'hidden', transition: 'all 0.15s' },
  ruleHeadBtn: { display: 'flex', flexDirection: 'column', gap: 10, padding: 18, width: '100%', background: 'transparent', border: 'none', color: 'inherit', font: 'inherit', textAlign: 'left', cursor: 'pointer' },
  ruleTopRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 },
  ruleNameRow: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 },
  ruleType: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.08em' },
  ruleName: { fontSize: 18, fontWeight: 600, color: window.PALETTE.inkHi, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  ruleStatus: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, fontWeight: 600, letterSpacing: '0.08em', whiteSpace: 'nowrap', flexShrink: 0 },

  ruleStatsRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' },
  rulePct: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 30, fontWeight: 500, letterSpacing: '-0.02em' },
  ruleDriftPiece: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 12, color: window.PALETTE.inkMid },
  ruleDriftLbl: { color: window.PALETTE.inkLo },

  ruleBar: { position: 'relative', height: 10, background: window.PALETTE.bg3, borderRadius: 2 },
  ruleBarBand: { position: 'absolute', top: 0, bottom: 0, background: 'rgba(125,211,252,0.10)', borderLeft: `1px solid rgba(125,211,252,0.35)`, borderRight: `1px solid rgba(125,211,252,0.35)` },
  ruleBarTarget: { position: 'absolute', top: -2, bottom: -2, width: 1, background: window.PALETTE.inkMid, opacity: 0.7 },
  ruleBarMarker: { position: 'absolute', top: -3, bottom: -3, width: 3, borderRadius: 1 },
  ruleScale: { display: 'flex', justifyContent: 'space-between', fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, marginTop: -4 },

  ruleActionLine: { borderTop: '1px solid', paddingTop: 10, marginTop: 4 },
  ruleAction: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 14, fontWeight: 600 },
  ruleActionDetail: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, color: window.PALETTE.inkLo, marginTop: 3 },
  ruleExpand: { color: window.PALETTE.inkMid },

  ruleExpanded: { padding: '0 18px 18px', borderTop: `1px dashed ${window.PALETTE.border}` },
  expandedToolbar: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 0 10px' },
  expandedLabel: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.1em' },
  editRuleBtn: { background: 'transparent', color: window.PALETTE.inkMid, border: `1px solid ${window.PALETTE.border}`, padding: '4px 10px', borderRadius: 5, fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, letterSpacing: '0.05em', cursor: 'pointer', textTransform: 'lowercase' },
  contribList: { display: 'flex', flexDirection: 'column', gap: 4 },
  contribRow: { display: 'grid', gridTemplateColumns: '70px 1.5fr 1fr 110px 60px', alignItems: 'center', gap: 12, padding: '6px 10px', background: window.PALETTE.bg0, borderRadius: 4, fontFamily: 'IBM Plex Mono, monospace', fontSize: 12 },
  contribTicker: { color: window.PALETTE.inkHi, fontWeight: 600 },
  contribCo: { color: window.PALETTE.inkMid, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'Inter, system-ui, sans-serif' },
  contribBarHost: { position: 'relative', height: 5, background: window.PALETTE.bg3, borderRadius: 3 },
  contribBar: { position: 'absolute', left: 0, top: 0, bottom: 0, opacity: 0.7, borderRadius: 3 },
  contribVal: { textAlign: 'right', color: window.PALETTE.inkHi },
  contribShare: { textAlign: 'right', color: window.PALETTE.inkLo },

  // holdings
  holdingsToolbar: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 },
  holdingsSearch: { flex: 1, minHeight: 38, padding: '8px 12px', borderRadius: 8, border: `1px solid ${window.PALETTE.border}`, background: window.PALETTE.bg1, color: window.PALETTE.inkHi, fontFamily: 'IBM Plex Mono, monospace', fontSize: 13, outline: 'none' },
  refreshBtn: { padding: '8px 16px', background: window.PALETTE.bg1, border: `1px solid ${window.PALETTE.border}`, borderRadius: 8, color: window.PALETTE.inkHi, fontFamily: 'IBM Plex Mono, monospace', fontSize: 12, whiteSpace: 'nowrap' },
  holdingsTableCard: { background: window.PALETTE.bg1, border: `1px solid ${window.PALETTE.border}`, borderRadius: 10, overflow: 'hidden' },
  holdingsHead: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, color: window.PALETTE.inkLo, textTransform: 'uppercase', letterSpacing: '0.08em', background: window.PALETTE.bg0, userSelect: 'none' },
  sortableCol: { cursor: 'pointer', whiteSpace: 'nowrap' },
  holdingsRow: { display: 'grid', gridTemplateColumns: '72px minmax(140px, 1.6fr) 1.2fr 1.1fr 100px 70px 110px 64px', alignItems: 'center', gap: 12, padding: '10px 16px', borderBottom: `1px solid ${window.PALETTE.border}`, fontSize: 12 },
  holdingsEmpty: { padding: 24, textAlign: 'center', color: window.PALETTE.inkLo, fontFamily: 'IBM Plex Mono, monospace', fontSize: 12 },
  colTicker: { fontFamily: 'IBM Plex Mono, monospace', color: window.PALETTE.inkHi, fontWeight: 600, fontSize: 13 },
  colCo: { color: window.PALETTE.inkMid, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  colTag: { color: window.PALETTE.inkMid, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  colTagSmall: { color: window.PALETTE.inkMid, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  colNum: { fontFamily: 'IBM Plex Mono, monospace', color: window.PALETTE.inkHi, textAlign: 'right' },
  colPct: { fontFamily: 'IBM Plex Mono, monospace', color: window.PALETTE.inkMid, textAlign: 'right' },
  colCap: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, textAlign: 'right' },
  colEdit: { textAlign: 'right' },
  editRowBtn: { background: 'transparent', color: window.PALETTE.inkLo, border: `1px solid ${window.PALETTE.border}`, padding: '4px 10px', borderRadius: 5, fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, cursor: 'pointer', whiteSpace: 'nowrap' },
};

window.MainPage = MainPage;
