// financial/src/products.jsx — the Insights tab: the aggregate data products.
//
// Each card here is a reusable derived dataset with its own lib under
// financial/lib — reproducible from stored data, citing its inputs, and
// labelled ENGINE (an engine's own output), ESTIMATE (a heuristic rollup) or
// DATA (stored as-is). Nothing on this tab re-implements an engine: the
// funded-ratio history is what the Retirement tab wrote, the attribution is
// arithmetic over stored snapshots, and every "this month" figure is only as
// fresh as the last Monarch sync — which every card says.
//
// `buildInsightContext` is shared with the Retirement tab's Answer screen so
// the insight rules (WI6) evaluate ONE context, not two.

const { useState, useEffect, useContext, useMemo, useCallback } = React;
const { ResponsiveContainer, ComposedChart, LineChart, Line, BarChart, Bar, Area, XAxis, YAxis, CartesianGrid,
        Tooltip, Legend, ReferenceLine, Cell } = window.Recharts;
const { DataContext, useToast, fmtCur, fmtDate, timeAgo, calcAge, USER_PROFILE, ExportCsv } = window.FinanceShared;

const TIP = { background: '#0f172a', border: '1px solid #334155', borderRadius: 8, fontSize: 11, color: '#e2e8f0' };
const ym = () => new Date().toISOString().slice(0, 7);
const tsIso = (t) => { try { return t ? (t.toDate ? t.toDate().toISOString() : new Date(t).toISOString()) : null; } catch (e) { return null; } };

const Tag = window.FinanceShared.ProvenanceTag;
function CardHead({ title, sub, kind, source, right }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
      <div><div className="label" style={{ marginBottom: 2 }}>{title}</div>{sub && <div style={{ fontSize: 11, color: '#64748b' }}>{sub} {source && <Tag kind={kind || 'heuristic'}>{source}</Tag>}</div>}</div>
      {right}
    </div>
  );
}

// ── The shared insight context ──────────────────────────────────────────────
function estimateCadence(payslips) {
  const dates = (payslips || []).map(p => p.payDate).filter(Boolean).sort();
  if (dates.length < 2) return 26;
  const gaps = [];
  for (let i = 1; i < dates.length; i++) gaps.push((new Date(dates[i]) - new Date(dates[i - 1])) / 86400000);
  const med = gaps.sort((a, b) => a - b)[Math.floor(gaps.length / 2)];
  return med > 25 ? 12 : med > 14.5 ? 24 : med > 8 ? 26 : 52;
}
function buildInsightContext(i) {
  const ST = window.SpendingTaxonomy, TCF = window.TrueCashFlow, C = window.Concentration, VB = window.VestBonusCalendar, TC = window.TaxConstants;
  const cfg = i.cfg || {};
  const year = i.currentYear || new Date().getFullYear();
  const tx = i.transactions || [];
  const outSign = i.spendingTaxonomy?.outSign ?? (ST && tx.length ? ST.detectOutSign(tx).outSign : -1);
  const rollup = ST && tx.length ? ST.rollup(tx, { months: 12, mapping: i.spendingTaxonomy?.mapping, dispositions: i.spendingTaxonomy?.dispositions, protection: i.spendingTaxonomy?.protection, outSign }) : null;
  const posture = (i.kidsPlan && i.kidsPlan.householdPosture) || {};
  const runway = TCF && rollup ? TCF.runway({ accounts: i.accounts, monthlySpend: rollup.spending.monthly, emergencyFundMonths: posture.emergencyFundMonths, emergencyCashTarget: cfg.emergencyCashTarget }) : null;
  // 401(k) pace from this year's payslips.
  const slips = ((i.payslipsByYear || {})[String(year)] || []).slice().sort((a, b) => String(a.payDate).localeCompare(String(b.payDate)));
  let contributions = null;
  if (slips.length && TC) {
    const age = calcAge(USER_PROFILE.birthday) ?? cfg.currentAge;
    const lim = TC.k401ForYear(year).k401;
    const limit = lim.employee + (age >= 50 ? lim.catchUp50 : 0);
    const ytd = slips.reduce((s, p) => s + (Number(p.retirement401k) || 0), 0);
    const cadence = estimateCadence(slips);
    const recent = slips.slice(-2);
    const perPeriod = recent.reduce((s, p) => s + (Number(p.retirement401k) || 0), 0) / recent.length;
    contributions = { ytd, limit, payPeriodsLeft: Math.max(0, cadence - slips.length), perPeriod, cadence, count: slips.length, source: `payslips ${year} (${slips.length})` };
  }
  // The pacing product (lib/contributionPacing) is the ONE source for the
  // 402(g) rule and the card: YTD, limit, periods left and the per-stub
  // deferral come from it when it is loaded, so the rule and the headline
  // can never disagree.
  const CPl = window.ContributionPacing;
  const pacing = CPl ? CPl.pace({ cfg, compensation: i.compensation, payslipsByYear: i.payslipsByYear, year, age: calcAge(USER_PROFILE.birthday) ?? cfg.currentAge }) : null;
  if (pacing && pacing.limit > 0 && (pacing.ytd > 0 || pacing.election.basePct != null)) {
    contributions = { ytd: pacing.ytd, limit: pacing.limit, payPeriodsLeft: pacing.periodsLeft, perPeriod: pacing.perPeriodNow, cadence: pacing.election.cadence, source: `contributionPacing (${pacing.ytdSource})` };
  }
  const concentration = C ? C.lookThrough({ holdings: i.holdings, accounts: i.accounts, equity: i.equityConfig, compensation: i.compensation, year, spouseSalary: cfg.wifeSalary }) : null;
  // How much of the invested money the holdings can see inside. Every
  // concentration / allocation figure is a share of what is covered, so it
  // travels with them (the concentration rule and card both carry it).
  const SPl = window.StatementPositions;
  const positionCoverage = SPl ? SPl.coverage({ holdings: i.holdings, accounts: i.accounts, accountMap: cfg.accountMap }) : null;
  // Config fields applied from a statement, and whether a newer-DATED
  // statement of the same kind is on file but not applied.
  const appliedFields = window.DocumentKinds && window.DocumentKinds.appliedFieldStatus ? window.DocumentKinds.appliedFieldStatus(cfg, i.documents || []) : [];
  const calendar = VB ? VB.events({ equity: i.equityConfig, compensation: i.compensation, salary: cfg.salary, assignments: i.eventAssignments, horizonYears: 5 }) : null;
  const DKl = window.DocumentKinds;
  const documentCadence = DKl ? DKl.cadenceAudit(i.documents || []) : null;
  // The data-integrity products (A1, A4, A5) and the spending spikes, so the
  // rules evaluate ONE context wherever they run.
  const TB = window.TaxBuckets, CR = window.CompReconciliation, TI = window.TransferIntegrity, BB = window.BudgetBuilder, DR = window.DocumentReconcile;
  const k401Split = TB ? TB.k401Split(cfg) : null;
  const compReconciliation = CR ? CR.reconcile({ retirementConfig: cfg, compensation: i.compensation, payslipsByYear: i.payslipsByYear, year }) : null;
  const cashOnHand = runway ? runway.cash : null;
  const transferIntegrity = TI && tx.length ? TI.match({ transactions: tx, accounts: i.accounts, outSign, mapping: i.spendingTaxonomy?.mapping, months: 12, cashOnHand }) : null;
  const monthlyRows = ST && tx.length ? ST.monthly(tx, { months: 14, mapping: i.spendingTaxonomy?.mapping, outSign }) : [];
  const spendingSpikes = BB && monthlyRows.length ? BB.spikes({ monthlyRows }) : null;
  const coverageCliff = coverageCliffFor(i.estatePlan, i.kidsPlan);
  const documentReconcile = DR ? DR.reconcile({ documents: i.documents || [], retirementConfig: cfg, accounts: i.accounts, equityConfig: i.equityConfig, netWorthSnapshots: i.netWorthSnapshots, accountBalanceHistory: i.accountBalanceHistory }) : null;
  return {
    documentCadence,
    reconciliation: i.reconciliation || null, contributions, runway, concentration, positionCoverage, appliedFields, zScores: i.zScores || [],
    unassignedEvents: VB && calendar ? VB.unassignedSoon(calendar, 45) : [],
    rollup, calendar, outSign,
    k401Split, compReconciliation, transferIntegrity, spendingSpikes, coverageCliff, documentReconcile, monthlyRows, pacing,
  };
}

// ── C6: the ANSWER screen every planning tab opens with ───────────────────
// The number, one plain-English sentence, the three assumptions it is most
// sensitive to (each with its source), and ONE "what would fix it" action.
function TabAnswer({ number, unit, sentence, assumptions, fix, kind, source, color, onFix }) {
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: color ? `${color}66` : undefined }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap' }}>
        <div style={{ textAlign: 'center', minWidth: 110 }}>
          <div style={{ fontSize: 38, fontWeight: 900, color: color || '#e2e8f0', lineHeight: 1 }}>{number}</div>
          {unit && <div style={{ fontSize: 9, color: '#64748b', letterSpacing: '0.1em', textTransform: 'uppercase', marginTop: 4 }}>{unit}</div>}
          <div style={{ marginTop: 4 }}><Tag kind={kind || 'heuristic'}>{source}</Tag></div>
        </div>
        <div style={{ flex: 1, minWidth: 240, fontSize: 13, color: '#e2e8f0', lineHeight: 1.5 }}>{sentence}</div>
      </div>
      <div className="grid-2" style={{ gap: 12, marginTop: 12, alignItems: 'start' }}>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' }}>
          <div className="label" style={{ marginBottom: 6 }}>The 3 assumptions it is most sensitive to</div>
          {(assumptions || []).slice(0, 3).map((a, i) => (
            <div key={i} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, fontSize: 11, padding: '3px 0', borderBottom: i < Math.min(3, (assumptions || []).length) - 1 ? '1px solid #161b22' : 'none' }}>
              <span style={{ color: '#e2e8f0' }}>{i + 1}. {a.label}</span>
              <span style={{ color: '#94a3b8', textAlign: 'right' }}>{a.value}<div style={{ fontSize: 9, color: '#475569' }}>{a.source}</div></span>
            </div>))}
        </div>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' }}>
          <div className="label" style={{ marginBottom: 6 }}>{fix ? 'What would fix it' : 'Nothing needs fixing'}</div>
          {fix ? <div style={{ fontSize: 11 }}><div style={{ color: '#e2e8f0', fontWeight: 600 }}>{fix.label}</div>{fix.detail && <div style={{ color: '#64748b', marginTop: 2 }}>{fix.detail}</div>}{fix.action && <a style={{ color: '#3b82f6', cursor: 'pointer', fontSize: 10, display: 'inline-block', marginTop: 4 }} onClick={onFix}>{fix.action} →</a>}</div>
               : <div style={{ fontSize: 11, color: '#64748b' }}>{fix === null ? 'Every check this tab runs passes.' : ''}</div>}
        </div>
      </div>
    </div>
  );
}

// Term policies against the children's ages: the years until each expiry
// and how old the youngest will be. The rule fires inside 15 years.
function coverageCliffFor(estatePlan, kidsPlan) {
  // Duplicate rows (the same policy applied from two documents) count once.
  const all = (estatePlan && estatePlan.policies) || [];
  const policies = window.EstatePlan && window.EstatePlan.distinctPolicies ? window.EstatePlan.distinctPolicies(all) : all;
  const duplicates = all.length - policies.length;
  const kids = ((kidsPlan && kidsPlan.kids) || []).filter(k => k && k.dob);
  const youngest = kids.slice().sort((a, b) => String(b.dob).localeCompare(String(a.dob)))[0] || null;
  const now = Date.now();
  const cliffs = policies.filter(p => p && p.kind === 'term' && p.expires && Number(p.deathBenefit) > 0).map(p => {
    const exp = new Date(p.expires);
    const yearsUntil = Math.round((exp.getTime() - now) / (365.25 * 86400000) * 10) / 10;
    const youngestAgeAtExpiry = youngest ? Math.floor((exp.getTime() - new Date(youngest.dob).getTime()) / (365.25 * 86400000)) : null;
    return { insured: p.insured === 'you' ? 'You' : p.insured === 'spouse' ? 'Spouse' : p.insured, insurer: p.insurer || null, deathBenefit: Number(p.deathBenefit) || 0, expires: String(p.expires).slice(0, 10), yearsUntil, youngestAgeAtExpiry, youngestName: youngest ? youngest.name : null };
  });
  return { cliffs, duplicates, horizonYears: 15, source: 'estate (policies) + kidsPlan (dob)' };
}

// ── WI6: the insight list ───────────────────────────────────────────────────
function InsightList({ result, onDismiss, setTab }) {
  if (!result || !result.insights.length) return null;
  const color = { danger: '#ef4444', warn: '#f59e0b', info: '#3b82f6' };
  const go = (link) => { if (!link || !setTab) return; try { if (link.screen) sessionStorage.setItem('retirementScreen', link.screen); } catch (e) {} setTab(link.tab); };
  return (
    <div style={{ marginBottom: 16 }}>
      {result.insights.map(ins => (
        <div key={ins.dismissKey} className="card" style={{ padding: '10px 14px', marginBottom: 8, borderColor: `${color[ins.severity]}66`, display: 'flex', gap: 12, alignItems: 'flex-start' }}>
          <div style={{ width: 8, height: 8, borderRadius: 4, background: color[ins.severity], marginTop: 5, flexShrink: 0 }} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, color: '#e2e8f0', fontWeight: 600 }}>{ins.title}</div>
            <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>{ins.number}</div>
            <div style={{ fontSize: 11, color: '#10b981', marginTop: 2 }}>→ {ins.action}</div>
            <div style={{ fontSize: 9, color: '#475569', marginTop: 2 }}>source: {ins.source}{ins.link && setTab ? <> · <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => go(ins.link)}>open {ins.link.tab}</a></> : null}</div>
          </div>
          <div style={{ textAlign: 'right', flexShrink: 0 }}>
            <div style={{ fontSize: 15, fontWeight: 800, color: ins.dollars == null ? '#475569' : '#e2e8f0', lineHeight: 1 }} title={ins.dollarsBasis || ''}>{ins.dollars == null ? '—' : fmtCur(ins.dollars)}</div>
            <div style={{ fontSize: 8, color: '#475569', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 4, maxWidth: 120 }}>{ins.dollars == null ? 'unpriced' : (ins.dollarsBasis || 'consequence').slice(0, 48)}</div>
            <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => onDismiss && onDismiss(ins.dismissKey)} title={`Snooze for ${result.snoozeDays || 30} days; returns sooner if the figure changes`}>Snooze {result.snoozeDays || 30}d</button>
          </div>
        </div>
      ))}
      {(result.overflow.length > 0 || result.suppressed.length > 0) && <div style={{ fontSize: 10, color: '#475569' }}>Ranked by dollar consequence (a cash-runway danger pins first). {result.overflow.length ? `${result.overflow.length} more waiting behind the three shown` : ''}{result.overflow.length && result.suppressed.length ? ' · ' : ''}{result.suppressed.length ? `${result.suppressed.length} snoozed (return when the snooze ends or the figure changes)` : ''}</div>}
    </div>
  );
}

// ── 5i: plan completeness ──────────────────────────────────────────────────
function CompletenessCard({ audit, setTab }) {
  if (!audit) return null;
  const go = (link) => { if (!setTab) return; try { if (link.screen) sessionStorage.setItem('retirementScreen', link.screen); } catch (e) {} setTab(link.tab); };
  const color = audit.score >= 80 ? '#10b981' : audit.score >= 50 ? '#f59e0b' : '#ef4444';
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: `${color}66` }}>
      <CardHead title="Plan completeness" sub="Stored data audited for gaps, ranked by consequence (5 = a court decides who raises the children; 1 = a chart is empty)." kind="data" source="all stored datasets"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 30, fontWeight: 800, color, lineHeight: 1 }}>{audit.score}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>of 100 · {audit.open.length} open</div></div>} />
      <table style={{ fontSize: 11 }}>
        <tbody>
          {audit.open.map(g => (
            <tr key={g.id}>
              <td style={{ padding: '5px 8px', width: 30, color: g.consequence >= 4 ? '#ef4444' : g.consequence === 3 ? '#f59e0b' : '#64748b', fontWeight: 700 }}>{g.consequence}</td>
              <td style={{ padding: '5px 8px', color: '#e2e8f0' }}>{g.label}<div style={{ fontSize: 10, color: '#64748b' }}>{g.why}</div></td>
              <td style={{ padding: '5px 8px', textAlign: 'right', whiteSpace: 'nowrap' }}><a style={{ color: '#3b82f6', cursor: 'pointer', fontSize: 10 }} onClick={() => go(g.link)}>{g.link.tab} → {g.link.field}</a></td>
            </tr>
          ))}
        </tbody>
      </table>
      {audit.closed.length > 0 && <div style={{ fontSize: 10, color: '#475569', marginTop: 6 }}>{audit.closed.length} check{audit.closed.length === 1 ? '' : 's'} pass.</div>}
    </div>
  );
}

// ── 5b: funded ratio history ───────────────────────────────────────────────
function FundedRatioHistoryCard({ history }) {
  const months = Object.values((history && history.months) || {}).sort((a, b) => a.month.localeCompare(b.month));
  const last = months[months.length - 1];
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Funded ratio history" sub="One reading per month with the inputs behind it, written by the Retirement tab on the headline preset. The move between months is attributed by re-solving with last month's inputs swapped back in." kind="engine" source="derived/fundedRatioHistory" />
      {months.length === 0 ? <div style={{ fontSize: 11, color: '#64748b' }}>No readings yet — open the Retirement tab once and this month's is written.</div> : (
        <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
          <ResponsiveContainer width="100%" height={180}>
            <LineChart data={months} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
              <CartesianGrid stroke="#161b22" />
              <XAxis dataKey="month" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
              <YAxis domain={[0, 'auto']} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `${v.toFixed(1)}×`} />
              <Tooltip contentStyle={TIP} formatter={(v, n) => [v == null ? '—' : `${Number(v).toFixed(2)}×`, n]} />
              <ReferenceLine y={1} stroke="#64748b" strokeDasharray="3 3" />
              <Line type="monotone" dataKey="ratio" name="Headline (preset)" stroke="#10b981" dot={{ r: 3 }} />
              <Line type="monotone" dataKey="ratioCurrent" name="Stored assumptions" stroke="#f59e0b" strokeDasharray="4 3" dot={false} />
            </LineChart>
          </ResponsiveContainer>
          <div style={{ fontSize: 11 }}>
            {last && <div style={{ color: '#e2e8f0', marginBottom: 6 }}>{last.month}: <strong>{last.ratio.toFixed(2)}×</strong> on the {last.preset} preset <span style={{ color: '#64748b' }}>· spend {fmtCur(last.inputs.spendAnnual)}/yr ({last.inputs.spendBasis}) · {last.inputs.expectedReturn}% / {last.inputs.salaryGrowth}%</span></div>}
            {months.filter(m => m.attribution).slice(-6).reverse().map(m => {
              const a = m.attribution; const pts = v => v == null ? '—' : `${v >= 0 ? '+' : ''}${Math.round(v * 100)}`;
              return (
                <div key={m.month} style={{ borderTop: '1px solid #161b22', padding: '5px 0', color: '#94a3b8' }}>
                  <strong style={{ color: a.delta >= 0 ? '#10b981' : '#ef4444' }}>{a.from} → {m.month}: {pts(a.delta)} pts</strong>
                  <span style={{ color: '#64748b' }}> — {a.market != null ? `${pts(a.market)} market, ${pts(a.contributions)} contributions` : `${pts(a.balances)} balances`}, {pts(a.assumptions)} assumptions, {pts(a.spending)} spending, {pts(a.other)} other</span>
                </div>
              );
            })}
            {months.length === 1 && <div style={{ color: '#64748b' }}>Attribution appears from the second month.</div>}
          </div>
        </div>
      )}
    </div>
  );
}

// ── 5c: net worth attribution ──────────────────────────────────────────────
function NetWorthAttributionCard({ store, transactions, accounts, rollupMonthly, payrollMonthly }) {
  const NW = window.NetWorthAttribution;
  const series = useMemo(() => {
    if (!NW) return [];
    const facts = (month) => {
      const m = (rollupMonthly || []).find(r => r.month === month);
      return { txContributions: NW.txContributionsFor(transactions, month, accounts), payrollContributions: payrollMonthly || 0, income: m ? m.income : null, spending: m ? m.spending : null };
    };
    return NW.series(store, facts);
  }, [NW, store, transactions, accounts, rollupMonthly, payrollMonthly]);
  const data = series.filter(s => s.attribution).map(s => ({ month: s.month, ...s.attribution.components, net: s.net, unexplained: s.attribution.unexplained }));
  const latest = series[series.length - 1];
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Net worth attribution" sub={`Monthly snapshots written by this app on every load (Monarch's history is empty). Each month's change is split into contributions, market, debt paydown, cash and other.${latest ? ` Latest: ${latest.date}, net ${fmtCur(latest.net)}.` : ''}`} kind="heuristic" source="derived/netWorthSnapshots + transactions" />
      {series.length < 2 ? (
        <div style={{ fontSize: 11, color: '#64748b' }}>{series.length === 0 ? 'No snapshot yet.' : `One snapshot (${latest.month}, net ${fmtCur(latest.net)}) — the first attribution appears next month. Nothing can be backfilled, which is why it is written today.`}</div>
      ) : (
        <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
          <ResponsiveContainer width="100%" height={200}>
            <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }} stackOffset="sign">
              <CartesianGrid stroke="#161b22" />
              <XAxis dataKey="month" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
              <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
              <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} />
              <Legend wrapperStyle={{ fontSize: 10 }} />
              <ReferenceLine y={0} stroke="#334155" />
              <Bar dataKey="contributions" name="Contributions" stackId="a" fill="#3b82f6" />
              <Bar dataKey="market" name="Market" stackId="a" fill="#10b981" />
              <Bar dataKey="debtPaydown" name="Debt paydown" stackId="a" fill="#8b5cf6" />
              <Bar dataKey="cashChange" name="Cash" stackId="a" fill="#06b6d4" />
              <Bar dataKey="other" name="Other" stackId="a" fill="#64748b" />
            </BarChart>
          </ResponsiveContainer>
          <div style={{ fontSize: 11 }}>
            {series.filter(s => s.attribution).slice(-4).reverse().map(s => {
              const a = s.attribution, c = a.components;
              return (
                <div key={s.month} style={{ borderBottom: '1px solid #161b22', padding: '5px 0', color: '#94a3b8' }}>
                  <strong style={{ color: a.netChange >= 0 ? '#10b981' : '#ef4444' }}>{a.from} → {s.month}: {a.netChange >= 0 ? '+' : ''}{fmtCur(a.netChange)}</strong>
                  <div style={{ fontSize: 10 }}>market {fmtCur(c.market)} · contributions {fmtCur(c.contributions)} · debt {fmtCur(c.debtPaydown)} · cash {fmtCur(c.cashChange)} · other {fmtCur(c.other)}</div>
                  {a.cashIdentity && <div style={{ fontSize: 10, color: Math.abs(a.cashIdentity.residual) > 2000 ? '#f59e0b' : '#475569' }}>cash identity: income {fmtCur(a.cashIdentity.income)} − spending {fmtCur(a.cashIdentity.spending)} → residual {fmtCur(a.cashIdentity.residual)}</div>}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// ── 5d: savings rate ───────────────────────────────────────────────────────
function SavingsRateCard({ series }) {
  if (!series) return null;
  const data = series.rows.map(r => ({ month: r.month, rate: r.ratePct, trailing: r.trailing12Pct, assumed: series.assumed.ratePct, evidence: r.evidence }));
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Savings rate, correctly computed" sub="Gross comp (both spouses) vs every retirement contribution — payroll deferrals off the payslips, employer money, IRAs, brokerage, 529s. Against the rate the projection assumes." kind="heuristic" source="compensation + payslips + spousePayslips + kidsPlan + retirementConfig"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 26, fontWeight: 800, color: series.diverges ? '#f59e0b' : '#10b981', lineHeight: 1 }}>{series.trailing12.ratePct == null ? '—' : `${series.trailing12.ratePct}%`}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>trailing 12 · assumed {series.assumed.ratePct == null ? '—' : `${series.assumed.ratePct}%`}</div></div>} />
      <ResponsiveContainer width="100%" height={180}>
        <ComposedChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
          <CartesianGrid stroke="#161b22" />
          <XAxis dataKey="month" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
          <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
          <Tooltip contentStyle={TIP} formatter={(v, n) => [v == null ? '—' : `${v}%`, n]} />
          <Legend wrapperStyle={{ fontSize: 10 }} />
          <Bar dataKey="rate" name="Monthly" fill="#3b82f6" opacity={0.6} />
          <Line type="monotone" dataKey="trailing" name="Trailing 12" stroke="#10b981" dot={false} strokeWidth={2} />
          <Line type="monotone" dataKey="assumed" name="Assumed by the projection" stroke="#f59e0b" strokeDasharray="4 3" dot={false} />
        </ComposedChart>
      </ResponsiveContainer>
      <div style={{ fontSize: 11, color: series.diverges ? '#f59e0b' : '#64748b', marginTop: 6 }}>{series.note} <span style={{ color: '#475569' }}>({series.trailing12.payslipMonths} of the last 12 months backed by payslips; the rest spread the config.)</span></div>
      <div style={{ fontSize: 10, color: '#475569', marginTop: 4 }}>Assumed: {series.assumed.lines.map(l => `${l.label} ${fmtCur(l.amount)}`).join(' · ')} over {fmtCur(series.assumed.gross)}.</div>
    </div>
  );
}

// ── 5e: true cash flow ─────────────────────────────────────────────────────
function TrueCashFlowCard({ series, runway, setTab }) {
  if (!series) return null;
  const sevColor = { critical: '#ef4444', low: '#f59e0b', ok: '#10b981', unknown: '#64748b' };
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: runway && runway.severity === 'critical' ? '#ef4444' : undefined }}>
      <CardHead title="True cash flow" sub={series.note} kind="heuristic" source={series.source}
        right={runway && <div style={{ textAlign: 'right' }}><div style={{ fontSize: 26, fontWeight: 800, color: sevColor[runway.severity], lineHeight: 1 }}>{runway.months == null ? '—' : `${runway.months} mo`}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>runway · {fmtCur(runway.cash)} cash / {fmtCur(runway.monthlySpend)} per month</div></div>} />
      {runway && (
        <div style={{ fontSize: 11, marginBottom: 8, color: '#94a3b8' }}>
          {runway.target != null ? <>Target {fmtCur(runway.target)} ({runway.targetMonths} months, from {runway.targetBasis}) — gap {fmtCur(runway.gap)}.</> : <span style={{ color: '#f59e0b' }}>No emergency-fund target is set: kidsPlan.emergencyFundMonths is blank and retirementConfig.emergencyCashTarget is 0. {setTab && <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => setTab('Kids')}>Set it on Kids →</a>}</span>}
        </div>
      )}
      <ResponsiveContainer width="100%" height={200}>
        <ComposedChart data={series.rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
          <CartesianGrid stroke="#161b22" />
          <XAxis dataKey="month" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
          <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
          <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} />
          <Legend wrapperStyle={{ fontSize: 10 }} />
          <ReferenceLine y={0} stroke="#334155" />
          <Bar dataKey="income" name="Income" fill="#10b981" opacity={0.7} />
          <Bar dataKey="spending" name="True spending" fill="#ef4444" opacity={0.7} />
          <Line type="monotone" dataKey="net" name="Income − spending" stroke="#e2e8f0" dot={false} strokeWidth={2} />
          <Line type="monotone" dataKey="cardBalanceChange" name="Card balance change (float)" stroke="#f59e0b" strokeDasharray="4 3" dot={false} />
        </ComposedChart>
      </ResponsiveContainer>
      <div style={{ fontSize: 10, color: '#475569', marginTop: 4 }}>Trailing 12: income {fmtCur(series.trailing12.income)}, spending {fmtCur(series.trailing12.spending)}, net {fmtCur(series.trailing12.net)}; card balances moved {fmtCur(series.trailing12.cardBalanceChange)} — float is not saving.</div>
    </div>
  );
}

// ── Position coverage ──────────────────────────────────────────────────────
// "Position data covers X% of invested assets", with every account that has
// less than full cover named. It is PERMANENT on the Investments tab and
// rides with every concentration figure: a percentage of the holdings is a
// percentage of what we can see inside, and when that is a quarter of the
// money the percentage says nothing about the portfolio.
function PositionCoverage({ cov, compact, setTab }) {
  const [open, setOpen] = useState(false);
  if (!cov || cov.invested <= 0) return null;
  const pct = cov.pct == null ? 0 : cov.pct;
  const color = pct >= 95 ? '#10b981' : pct >= 70 ? '#f59e0b' : '#ef4444';
  const missing = cov.missing || [];
  const label = (r) => r.state === 'none' ? 'no positions' : `${fmtCur(r.covered)} of ${fmtCur(r.balance)} itemized`;
  return (
    <div className="card" style={{ marginBottom: compact ? 8 : 16, padding: compact ? '8px 12px' : undefined, borderColor: pct < 95 ? `${color}66` : undefined }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
        <div style={{ fontSize: compact ? 12 : 13, color: '#e2e8f0', flex: 1, minWidth: 220 }}>
          Position data covers <b style={{ color }}>{pct.toFixed(1)}%</b> of invested assets
          <span style={{ color: '#64748b', fontSize: 11 }}> — {fmtCur(cov.covered)} of {fmtCur(cov.invested)}{cov.uncovered > 0 ? `; ${fmtCur(cov.uncovered)} has no fund-level data` : ''}</span>
        </div>
        <div style={{ width: 160, height: 6, background: '#1e2a3a', borderRadius: 3, overflow: 'hidden' }}>
          <div style={{ width: `${Math.min(100, pct)}%`, height: '100%', background: color }} />
        </div>
        {missing.length > 0 && <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setOpen(v => !v)}>{open ? 'Hide' : `${missing.length} account${missing.length === 1 ? '' : 's'} without positions`}</button>}
      </div>
      {!compact && missing.length > 0 && !open && (
        <div style={{ fontSize: 10, color: '#64748b', marginTop: 4 }}>{missing.slice(0, 6).map(r => `${r.name} ${fmtCur(r.gap)}`).join(' · ')}{missing.length > 6 ? ` · +${missing.length - 6} more` : ''}</div>
      )}
      {open && (
        <table style={{ fontSize: 11, marginTop: 8 }}>
          <thead><tr><th>Account</th><th style={{ textAlign: 'right' }}>Balance</th><th style={{ textAlign: 'right' }}>Not itemized</th><th>Positions</th></tr></thead>
          <tbody>{missing.map(r => (
            <tr key={r.id}><td style={{ padding: '3px 8px' }}>{r.name}</td><td style={{ padding: '3px 8px', textAlign: 'right' }}>{fmtCur(r.balance)}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right', color }}>{fmtCur(r.gap)}</td><td style={{ padding: '3px 8px', color: '#94a3b8' }}>{label(r)}</td></tr>))}</tbody>
        </table>
      )}
      {!compact && missing.length > 0 && (
        <div style={{ fontSize: 10, color: '#475569', marginTop: 4 }}>
          Allocation, concentration and asset-location figures below are shares of the covered {fmtCur(cov.covered)} only. Upload each account's latest statement on the Documents tab (the fund table is read off it){setTab ? <> — <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => setTab('Documents')}>open Documents</a></> : ''}.
        </div>
      )}
    </div>
  );
}

// ── 5f: concentration ──────────────────────────────────────────────────────
function ConcentrationCard({ c, coverage, unvestedInHoldings, onSetUnvestedInHoldings, equity, onSaveEquity }) {
  const [heldDraft, setHeldDraft] = useState(null);
  if (!c) return null;
  // No concentration percentage renders without the coverage beside it.
  if (!coverage) return null;
  const ign = c.employer && c.employer.ignoredHolding;
  const top = c.issuers.slice(0, 8);
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: c.flagged.length ? 'rgba(245,158,11,0.5)' : undefined }}>
      <CardHead title="Concentration + look-through risk" sub={`Retirement-scope holdings ${fmtCur(c.scope.total)}; ${fmtCur(c.scope.excludedTotal)} in 529/UTMA excluded from the glidepath. Unvested RSUs are shown as contingent, never as held.`} kind="heuristic" source="holdings + accounts + equity + compensation" />
      <PositionCoverage cov={coverage} compact />
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <table style={{ fontSize: 11 }}>
          <thead><tr><th>Issuer</th><th style={{ textAlign: 'right' }}>Held</th><th style={{ textAlign: 'right' }}>%</th><th style={{ textAlign: 'right' }}>+ unvested</th><th style={{ textAlign: 'right' }}>%</th></tr></thead>
          <tbody>{top.map(r => (
            <tr key={r.key} style={{ color: r.flagged ? '#f59e0b' : '#94a3b8' }}>
              <td style={{ padding: '4px 8px' }}>{r.ticker || r.name} <span style={{ fontSize: 9, color: '#475569' }}>{r.kind}{r.employer ? ' · employer' : ''}</span></td>
              <td style={{ padding: '4px 8px', textAlign: 'right' }}>{fmtCur(r.value)}</td>
              <td style={{ padding: '4px 8px', textAlign: 'right' }}>{r.pct}%</td>
              <td style={{ padding: '4px 8px', textAlign: 'right' }}>{r.contingentValue ? fmtCur(r.contingentValue) : '—'}</td>
              <td style={{ padding: '4px 8px', textAlign: 'right', fontWeight: r.flagged ? 700 : 400 }}>{r.pctWithContingent}%</td>
            </tr>))}</tbody>
        </table>
        <div style={{ fontSize: 11 }}>
          {c.employer && (
            <div style={{ border: '1px solid rgba(245,158,11,0.4)', borderRadius: 8, padding: '10px 12px', marginBottom: 8 }}>
              <div style={{ color: '#f59e0b', fontWeight: 700 }}>{c.employer.humanCapital.risk} — {c.employer.ticker}</div>
              <div style={{ color: '#94a3b8', marginTop: 4, lineHeight: 1.5 }}>{c.employer.humanCapital.note}</div>
              <div style={{ color: '#64748b', marginTop: 4, fontSize: 10 }}>Held {fmtCur(c.employer.held)} ({c.employer.pct}%) + {c.employer.unvestedShares} unvested shares ≈ {fmtCur(c.employer.unvestedValue)} at ${c.employer.sharePrice} ({c.employer.priceAsOf || 'no date'}) → {c.employer.pctWithContingent}% · pay from the issuer {fmtCur(c.employer.humanCapital.annualPayFromEmployer)} = {c.employer.humanCapital.shareOfHouseholdPay}% of household pay.</div>
              {c.employer.loggedAsHolding && <div style={{ color: '#64748b', marginTop: 4, fontSize: 10 }}>{c.employer.loggedAsHolding.note}</div>}
              {ign && <div style={{ color: '#64748b', marginTop: 4, fontSize: 10 }}>{ign.note}</div>}
              {onSaveEquity && (
                <label style={{ display: 'flex', gap: 6, alignItems: 'center', color: '#94a3b8', marginTop: 6, fontSize: 10, cursor: 'pointer' }}>
                  <input type="checkbox" checked={!!(equity && equity.ignoreEmployerHoldings)} onChange={e => onSaveEquity({ ignoreEmployerHoldings: e.target.checked })} />
                  Ignore Monarch's {c.employer.ticker} line — use the brokerage's held shares instead
                </label>
              )}
              {onSaveEquity && equity && equity.ignoreEmployerHoldings && (
                <div style={{ display: 'flex', gap: 6, alignItems: 'center', color: '#94a3b8', marginTop: 4, fontSize: 10 }}>
                  Shares held at the brokerage (vested, not sold):
                  <input type="number" step="0.0001" min="0" style={{ width: 90, fontSize: 10, padding: '1px 4px' }}
                    value={heldDraft != null ? heldDraft : (equity.heldShares != null ? String(equity.heldShares) : '')}
                    onChange={e => setHeldDraft(e.target.value)}
                    onBlur={() => { if (heldDraft == null) return; const v = Number(heldDraft); onSaveEquity({ heldShares: heldDraft === '' ? null : (v >= 0 ? v : null) }); setHeldDraft(null); }} />
                </div>
              )}
              {onSetUnvestedInHoldings && !ign && c.employer.unvestedShares > 0 && (
                <label style={{ display: 'flex', gap: 6, alignItems: 'center', color: '#94a3b8', marginTop: 6, fontSize: 10, cursor: 'pointer' }}>
                  <input type="checkbox" checked={unvestedInHoldings === true || (unvestedInHoldings == null && !!c.employer.loggedAsHolding)}
                    onChange={e => onSetUnvestedInHoldings(e.target.checked)} />
                  My unvested {c.employer.ticker} shares are logged in Monarch as a holding (count them once)
                </label>
              )}
            </div>
          )}
          {c.largestExcluded && <div style={{ color: '#64748b', fontSize: 10 }}>Out of scope: {c.largestExcluded.name} {fmtCur(c.largestExcluded.value)} ({c.largestExcluded.pctOfHousehold}% of all holdings) — {c.largestExcluded.note}.</div>}
          {c.largestFund && <div style={{ color: '#64748b', fontSize: 10, marginTop: 4 }}>Largest fund in scope: {c.largestFund.ticker || c.largestFund.name} {c.largestFund.pct}% — {c.largestFund.note}.</div>}
          <div style={{ color: '#475569', fontSize: 10, marginTop: 4 }}>{c.scope.note}</div>
        </div>
      </div>
    </div>
  );
}

// ── 5g: vest + bonus calendar ──────────────────────────────────────────────
function VestBonusCard({ cal, assignments, onSave }) {
  const VB = window.VestBonusCalendar;
  const [draft, setDraft] = useState(null);
  if (!cal || !VB) return null;
  const set = (id, patch) => { const next = { ...(assignments || {}), [id]: { ...((assignments || {})[id] || {}), ...patch, updatedAt: new Date().toISOString() } }; if (!next[id].target) delete next[id]; onSave(next); };
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Vest + bonus calendar" sub={`Every future vest and cash IC in the next ${cal.horizonYears} years with gross, withholding and net. ${cal.liquidity.note || ''}`} kind="heuristic" source="equity + compensation + taxConstants"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: '#10b981', lineHeight: 1 }}>{fmtCur(cal.next12.net)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>net in the next 12 months · {cal.liquidity.januaryShare == null ? '—' : `${cal.liquidity.januaryShare}% lands in January`}</div></div>} />
      {/* The assignment plan as a timeline: one strip per year ahead, a dot per event, coloured by its target. */}
      {cal.events.length > 0 && (() => {
        const targetColor = { rothIra: '#10b981', backdoorRoth: '#059669', debt: '#ef4444', buffer: '#06b6d4', brokerage: '#3b82f6', plan529: '#f59e0b', taxReserve: '#8b5cf6', spend: '#64748b' };
        const first = new Date(cal.asOf).getTime(), last = new Date(cal.events[cal.events.length - 1].date).getTime() + 30 * 86400000;
        const span = Math.max(1, last - first);
        const years = []; for (let y = Number(cal.asOf.slice(0, 4)) + 1; y <= Number(cal.events[cal.events.length - 1].date.slice(0, 4)); y++) years.push(y);
        return (
          <div style={{ marginBottom: 10 }}>
            <div style={{ position: 'relative', height: 44, background: '#0b1220', borderRadius: 6, border: '1px solid #1e2a3a' }}>
              {years.map(y => { const x = (new Date(`${y}-01-01`).getTime() - first) / span * 100; return x > 0 && x < 100 ? <div key={y} style={{ position: 'absolute', left: `${x}%`, top: 0, bottom: 0, borderLeft: '1px dashed #334155' }}><span style={{ position: 'absolute', top: 2, left: 3, fontSize: 8, color: '#475569' }}>{y}</span></div> : null; })}
              {(() => { const x = 45 * 86400000 / span * 100; return <div style={{ position: 'absolute', left: 0, width: `${Math.min(100, x)}%`, top: 0, bottom: 0, background: 'rgba(245,158,11,0.08)' }} title="45-day window: an unassigned event here fires the insight" />; })()}
              {cal.events.map(e => {
                const x = (new Date(e.date).getTime() - first) / span * 100;
                const size = Math.max(8, Math.min(22, Math.sqrt((e.net || 0) / 2000)));
                const col = e.assignment ? (targetColor[e.assignment.target] || '#94a3b8') : (e.daysAway <= 45 ? '#f59e0b' : '#475569');
                return <div key={e.id} title={`${e.date} ${e.label} · net ${e.net == null ? '?' : fmtCur(e.net)} · ${e.assignment ? e.assignment.targetLabel : 'UNASSIGNED'}`} style={{ position: 'absolute', left: `calc(${x}% - ${size / 2}px)`, top: e.kind === 'vest' ? 10 : 24, width: size, height: size, borderRadius: '50%', background: col, border: e.assignment ? 'none' : '1px dashed #e2e8f0', opacity: e.projected ? 0.55 : 1 }} />;
              })}
            </div>
            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 9, color: '#64748b', marginTop: 3 }}>
              <span>● vests (upper) · ● bonuses (lower) · dashed = unassigned · size ∝ net</span>
              {VB.ASSIGNMENT_TARGETS.map(t => <span key={t.key}><span style={{ color: targetColor[t.key] || '#94a3b8' }}>●</span> {t.label}</span>)}
              {cal.next12.underWithholding > 0 && <span style={{ color: '#f59e0b' }}>· under-withheld next 12 months: {fmtCur(cal.next12.underWithholding)} (22% supplemental vs the marginal rate)</span>}
            </div>
          </div>
        );
      })()}
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Date</th><th>Event</th><th style={{ textAlign: 'right' }}>Gross</th><th style={{ textAlign: 'right' }}>Withheld</th><th style={{ textAlign: 'right' }}>Net</th><th style={{ textAlign: 'right' }}>Under-withheld</th><th>Assigned to</th></tr></thead>
        <tbody>{cal.events.map(e => (
          <tr key={e.id} style={{ color: e.projected ? '#64748b' : '#94a3b8' }}>
            <td style={{ padding: '4px 8px', whiteSpace: 'nowrap' }}>{e.date} <span style={{ fontSize: 9, color: '#475569' }}>{e.daysAway}d</span></td>
            <td style={{ padding: '4px 8px' }}>{e.label}<div style={{ fontSize: 9, color: '#475569' }}>{e.source}{e.priceBasis ? ` · ${e.priceBasis}` : ''}{e.dateBasis ? ` · ${e.dateBasis}` : ''}</div></td>
            <td style={{ padding: '4px 8px', textAlign: 'right' }}>{e.gross == null ? '—' : fmtCur(e.gross)}</td>
            <td style={{ padding: '4px 8px', textAlign: 'right' }} title={e.withholdingDetail ? `fed ${fmtCur(e.withholdingDetail.federal)} · DE ${fmtCur(e.withholdingDetail.state)} · SS ${fmtCur(e.withholdingDetail.socialSecurity)} · Medicare ${fmtCur(e.withholdingDetail.medicare)}` : ''}>{e.withholding == null ? '—' : fmtCur(e.withholding)}</td>
            <td style={{ padding: '4px 8px', textAlign: 'right', color: '#e2e8f0', fontWeight: 600 }}>{e.net == null ? '—' : fmtCur(e.net)}</td>
            <td style={{ padding: '4px 8px', textAlign: 'right', color: e.underWithholding > 0 ? '#f59e0b' : '#475569' }} title={e.withholdingDetail && e.withholdingDetail.underWithholding ? e.withholdingDetail.underWithholding.note : ''}>{e.underWithholding == null ? '—' : fmtCur(e.underWithholding)}</td>
            <td style={{ padding: '4px 8px' }}>
              <select value={(e.assignment && e.assignment.target) || ''} onChange={ev => set(e.id, { target: ev.target.value })} style={{ fontSize: 11, padding: '3px 6px', borderColor: e.assignment ? '#1e2a3a' : (e.daysAway <= 45 ? '#f59e0b' : '#1e2a3a') }}>
                <option value="">— unassigned —</option>
                {VB.ASSIGNMENT_TARGETS.map(t => <option key={t.key} value={t.key}>{t.label}</option>)}
              </select>
            </td>
          </tr>))}</tbody>
      </table>
      {!cal.events.length && <div style={{ fontSize: 11, color: '#64748b' }}>No future events in the horizon.</div>}
    </div>
  );
}

// ── 5h: marginal rate curve (rendered by the Retirement tab, which has the engine rows) ──
function MarginalRateCard({ working, retirement, opportunity }) {
  if (!working) return null;
  const data = working.points.map(p => ({ income: p.income, working: p.totalPct, federal: p.federalPct }));
  if (retirement) retirement.points.forEach(p => { const row = data.find(d => d.income === p.income); if (row) row.retirement = p.totalPct; else data.push({ income: p.income, retirement: p.totalPct }); });
  data.sort((a, b) => a.income - b.income);
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(59,130,246,0.35)' }}>
      <CardHead title="Marginal rate curve — working year vs retirement year" sub={`Federal + Delaware + NIIT + additional Medicare + credit phase-outs by incremental income, ${working.year} from ${working.constantsBasis}${retirement ? `; retirement-year overlay ${retirement.year} from the engine's projected constants` : ''}.`} kind="heuristic" source="taxConstants + taxPlanner"
        right={opportunity && <div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: '#10b981', lineHeight: 1 }}>{fmtCur(opportunity.perYear || 0)}<span style={{ fontSize: 10, color: '#64748b' }}>/yr</span></div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>conversion opportunity, ages {opportunity.window.startAge}–{opportunity.window.endAge} · {fmtCur(opportunity.totalDollars)} total</div></div>} />
      <ResponsiveContainer width="100%" height={220}>
        <ComposedChart data={data} margin={{ top: 20, right: 8, left: 0, bottom: 0 }}>
          <CartesianGrid stroke="#161b22" />
          <XAxis dataKey="income" type="number" domain={['dataMin', 'dataMax']} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
          <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} domain={[0, 'auto']} />
          <Tooltip contentStyle={TIP} formatter={(v, n) => [v == null ? '—' : `${v}%`, n]} labelFormatter={v => fmtCur(v)} />
          <Legend wrapperStyle={{ fontSize: 10 }} />
          <Line type="stepAfter" dataKey="working" name={`Working ${working.year} (all-in marginal)`} stroke="#3b82f6" dot={false} strokeWidth={2} connectNulls />
          {retirement && <Line type="stepAfter" dataKey="retirement" name={`Retirement ${retirement.year}`} stroke="#10b981" dot={false} strokeWidth={2} connectNulls />}
          {working.markers.filter(m => m.income >= data[0].income && m.income <= data[data.length - 1].income).map(m => <ReferenceLine key={m.key} x={m.income} stroke={m.kind === 'bracket' ? '#475569' : m.kind === 'wall' ? '#ef4444' : '#f59e0b'} strokeDasharray="3 3" label={{ value: m.label, position: 'top', fontSize: 8, fill: '#94a3b8' }} />)}
        </ComposedChart>
      </ResponsiveContainer>
      <div style={{ fontSize: 10, color: '#64748b', marginTop: 4 }}>At today's income ({fmtCur(working.baseIncome)}): federal {working.atBase.federalPct}% + DE {working.atBase.statePct}% + surtaxes {working.atBase.niitPct + working.atBase.addlMedicarePct}% + phase-outs {working.atBase.ctcPhaseOutPct}% = <strong style={{ color: '#e2e8f0' }}>{working.atBase.totalPct}%</strong>.</div>
      {opportunity && opportunity.rows.length > 0 && (
        <div style={{ fontSize: 10, color: '#64748b', marginTop: 4 }}>
          Per window year: {opportunity.rows.map(r => `${r.age}: ${fmtCur(r.converted)} at ${r.retirementMarginalPct == null ? '?' : r.retirementMarginalPct + '%'} (gap ${r.gapPts == null ? '?' : r.gapPts + ' pts'} → ${fmtCur(r.dollarsSaved || 0)}${r.unusedRoom ? `, ${fmtCur(r.unusedRoom)} room unused` : ''}${r.limitedBy ? `, ${r.limitedBy}-bound` : ''})`).join(' · ')}. <Tag kind="engine">retirementNeeds.project rows</Tag>
        </div>
      )}
    </div>
  );
}

// ── 5j: decision log ───────────────────────────────────────────────────────
function DecisionLogCard({ decisions, cfg, fundedRatio, preset, onSave, onDelete }) {
  const DL = window.DecisionLog;
  const [title, setTitle] = useState('');
  const [rationale, setRationale] = useState('');
  const [replayId, setReplayId] = useState(null);
  if (!DL) return null;
  const replay = replayId ? DL.replay(decisions.find(d => d.id === replayId), { cfg, fundedRatio, preset }) : null;
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Decision log" sub="A decision is a date, a rationale and the assumptions in force. Replay later against what happened." kind="data" source="decisions" />
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr auto', gap: 8, marginBottom: 10 }}>
        <input placeholder="Decision (e.g. keep 401(k) deferral 100% Roth)" value={title} onChange={e => setTitle(e.target.value)} />
        <input placeholder="Rationale" value={rationale} onChange={e => setRationale(e.target.value)} />
        <button className="btn-primary" style={{ fontSize: 11 }} disabled={!title.trim()} onClick={() => { onSave(DL.create({ title, rationale, cfg, fundedRatio, preset })); setTitle(''); setRationale(''); }}>Record</button>
      </div>
      {decisions.length === 0 ? <div style={{ fontSize: 11, color: '#64748b' }}>Nothing recorded yet. The funded ratio on screen ({fundedRatio == null ? '—' : `${fundedRatio.toFixed(2)}×`} on {preset}) and the key inputs are captured with each entry.</div> : (
        <table style={{ fontSize: 11 }}>
          <tbody>{decisions.map(d => (
            <tr key={d.id}>
              <td style={{ padding: '5px 8px', whiteSpace: 'nowrap', color: '#64748b' }}>{d.date}</td>
              <td style={{ padding: '5px 8px', color: '#e2e8f0' }}>{d.title}{d.seeded && <span style={{ fontSize: 9, color: '#3b82f6', marginLeft: 6 }}>seeded</span>}<div style={{ fontSize: 10, color: '#64748b' }}>{d.rationale}</div><div style={{ fontSize: 9, color: '#475569' }}>then: ratio {d.assumptions && d.assumptions.fundedRatio != null ? `${d.assumptions.fundedRatio.toFixed(2)}×` : '—'} · {d.assumptions ? `${d.assumptions.expectedReturn}% / ${d.assumptions.salaryGrowth}% · retire ${d.assumptions.retireAge}` : ''}</div></td>
              <td style={{ padding: '5px 8px', textAlign: 'right', whiteSpace: 'nowrap' }}>
                <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px', marginRight: 4 }} onClick={() => setReplayId(replayId === d.id ? null : d.id)}>{replayId === d.id ? 'Close' : 'Replay'}</button>
                <button className="btn-danger" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => onDelete(d.id)}>✕</button>
              </td>
            </tr>))}</tbody>
        </table>
      )}
      {replay && (
        <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px', fontSize: 11 }}>
          <div style={{ color: '#e2e8f0', fontWeight: 600 }}>{replay.title} — {replay.daysSince} days on: {replay.verdict}</div>
          <div style={{ color: '#94a3b8', marginTop: 4 }}>Funded ratio {replay.ratioThen == null ? '—' : `${replay.ratioThen.toFixed(2)}×`} → {replay.ratioNow == null ? '—' : `${replay.ratioNow.toFixed(2)}×`} ({replay.ratioDelta == null ? '—' : `${replay.ratioDelta >= 0 ? '+' : ''}${replay.ratioDelta.toFixed(2)}`})</div>
          {replay.changes.map(c => <div key={c.key} style={{ color: '#64748b', fontSize: 10 }}>{c.key}: {String(c.then)} → {String(c.now)}{c.deltaPct != null ? ` (${c.deltaPct >= 0 ? '+' : ''}${c.deltaPct}%)` : ''}</div>)}
        </div>
      )}
    </div>
  );
}

// ── A5: transfer integrity ─────────────────────────────────────────────────
function TransferIntegrityCard({ t }) {
  const [showPairs, setShowPairs] = useState(false);
  if (!t) return null;
  const s = t.summary;
  const bad = t.flagged.length + t.duplicates.length;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: bad ? 'rgba(245,158,11,0.45)' : undefined }}>
      <CardHead title="Transfer integrity" sub={`Every transfer and card-payment leg in the last ${t.window ? t.window.months : 12} months, matched into pairs across accounts (same amount, within ${t.windowDays} days). ${t.verdict}`} kind="heuristic" source="transactions via transferIntegrity"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: bad ? '#f59e0b' : '#10b981', lineHeight: 1 }}>{fmtCur(s.flaggedTotal + s.duplicateTotal)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>unexplained · {s.flaggedCount} single leg{s.flaggedCount === 1 ? '' : 's'} &gt; {fmtCur(t.threshold)}, {t.duplicates.length} duplicate{t.duplicates.length === 1 ? '' : 's'}</div></div>} />
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 10, color: '#64748b', marginBottom: 8 }}>
        <span>Transfer legs: <strong style={{ color: '#e2e8f0' }}>{fmtCur(s.transferVolume)}</strong> ({t.legs})</span>
        <span>Paired: {fmtCur(s.pairedVolume)} × 2 ({s.pairedSharePct == null ? '—' : `${s.pairedSharePct}%`})</span>
        <span>Unmatched out: <strong style={{ color: s.unmatchedOut ? '#f59e0b' : '#94a3b8' }}>{fmtCur(s.unmatchedOut)}</strong></span>
        <span>Unmatched in: <strong style={{ color: s.unmatchedIn ? '#f59e0b' : '#94a3b8' }}>{fmtCur(s.unmatchedIn)}</strong></span>
        <span>Implied true movement: {fmtCur(s.impliedTrueVolume)}</span>
      </div>
      {t.unmatched.length > 0 && (
        <table style={{ fontSize: 11 }}>
          <thead><tr><th>Date</th><th>Account</th><th>Direction</th><th style={{ textAlign: 'right' }}>Amount</th><th>Category / merchant</th><th>Why it matters</th></tr></thead>
          <tbody>{t.unmatched.slice(0, 12).map(l => (
            <tr key={l.id} style={{ color: l.flagged ? '#e2e8f0' : '#94a3b8' }}>
              <td style={{ padding: '3px 8px', whiteSpace: 'nowrap' }}>{l.date}</td>
              <td style={{ padding: '3px 8px' }}>{l.accountName}</td>
              <td style={{ padding: '3px 8px' }}>{l.out ? 'out' : 'in'}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right', color: l.flagged ? '#f59e0b' : undefined, fontWeight: l.flagged ? 700 : 400 }}>{fmtCur(l.amount)}{l.flagged && <span style={{ fontSize: 9, marginLeft: 4 }}>▲</span>}</td>
              <td style={{ padding: '3px 8px', fontSize: 10 }}>{l.category}{l.merchant ? ` · ${l.merchant}` : ''}</td>
              <td style={{ padding: '3px 8px', fontSize: 10, color: '#64748b' }}>{l.why}</td>
            </tr>))}</tbody>
        </table>
      )}
      {t.unmatched.length > 12 && <div style={{ fontSize: 10, color: '#475569', marginTop: 4 }}>{t.unmatched.length - 12} more unmatched legs (largest first above).</div>}
      {t.duplicates.length > 0 && <div style={{ fontSize: 10, color: '#f59e0b', marginTop: 6 }}>Duplicates: {t.duplicates.slice(0, 6).map(dd => `${dd.date} ${dd.accountName} ${fmtCur(dd.amount)}`).join(' · ')}{t.duplicates.length > 6 ? ` +${t.duplicates.length - 6}` : ''} — one import counted twice; hide one row in Monarch.</div>}
      {t.byAccount.length > 0 && <div style={{ fontSize: 10, color: '#475569', marginTop: 6 }}>Unmatched by account: {t.byAccount.slice(0, 6).map(a => `${a.accountName} out ${fmtCur(a.out)} / in ${fmtCur(a.in)} (${a.count})`).join(' · ')}</div>}
      <div style={{ marginTop: 6, display: 'flex', gap: 6 }}><button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setShowPairs(v => !v)}>{showPairs ? 'Hide' : 'Show'} the {t.pairs.length} matched pairs</button>
        <ExportCsv name="transfer-legs" rows={t.unmatched.map(l => ({ status: 'unmatched', date: l.date, account: l.accountName, direction: l.out ? 'out' : 'in', amount: l.amount, category: l.category, merchant: l.merchant || '', flagged: l.flagged })).concat(t.pairs.map(p => ({ status: 'paired', date: p.out.date, account: `${p.out.accountName} → ${p.in.accountName}`, direction: 'pair', amount: p.amount, category: p.out.category, merchant: '', flagged: false })))} /></div>
      {showPairs && <div style={{ fontSize: 10, color: '#64748b', marginTop: 4, maxHeight: 200, overflowY: 'auto' }}>{t.pairs.map((p, k) => <div key={k}>{p.out.date} {p.out.accountName} → {p.in.accountName} {fmtCur(p.amount)}{p.gapDays ? ` (${p.gapDays}d)` : ''}</div>)}</div>}
      <div style={{ fontSize: 9, color: '#475569', marginTop: 6 }}>A flagged leg is a question, not a re-classification: if it was really an expense, re-map its raw category on the taxonomy card below and it re-enters spending; if the other account is not synced, connect it in Monarch.</div>
    </div>
  );
}

// ── M3: the 13-week cash forecast ─────────────────────────────────────────
function CashForecastCard({ forecast, lumps, onSaveLumps, setTab }) {
  const [adding, setAdding] = useState(null);
  const [showLines, setShowLines] = useState(false);
  if (!forecast) return null;
  const f = forecast;
  const data = f.rows.map(r => ({ week: `W${r.week}`, from: r.from, endCash: r.endCash, inflows: r.inflows.reduce((s, l) => s + l.amount, 0), outflows: r.outflows.reduce((s, l) => s + l.amount, 0) - r.discretionary }));
  const color = f.negativeWeek != null ? '#ef4444' : f.minCash < f.startCash * 0.5 ? '#f59e0b' : '#10b981';
  const saveLump = () => { if (!adding || !adding.label || !adding.amount || !adding.date) return; onSaveLumps([...(lumps || []), { id: `lump_${Date.now()}`, label: adding.label, amount: Number(adding.amount) || 0, date: adding.date, source: 'cashflow/lumps (typed)' }]); setAdding(null); };
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: `${color}66` }}>
      <CardHead title="13-week cash forecast" sub={`From ${fmtCur(f.startCash)} of cash today: paychecks (${f.sources.paychecks}), recurring bills detected from the ledger, the vest/bonus calendar, annual lumps (detected + stored), and a discretionary run-rate of ${fmtCur(f.discretionaryWeekly)}/week for what the recurring items do not explain.`} kind="heuristic" source="accounts + transactions + vestBonusCalendar + contributionPacing"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color, lineHeight: 1 }}>{fmtCur(f.minCash)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>low point · week {f.minWeek || 1}{f.negativeWeek != null ? ` · negative from week ${f.negativeWeek}` : ''}</div></div>} />
      <div style={{ fontSize: 11, color: color, marginBottom: 6 }}>{f.headline}.</div>
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <div>
          <ResponsiveContainer width="100%" height={200}>
            <ComposedChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
              <CartesianGrid stroke="#161b22" />
              <XAxis dataKey="week" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
              <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
              <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} labelFormatter={(w, p) => `${w} · from ${p && p[0] ? p[0].payload.from : ''}`} />
              <Legend wrapperStyle={{ fontSize: 10 }} />
              <ReferenceLine y={0} stroke="#ef4444" strokeDasharray="3 3" />
              <Bar dataKey="inflows" name="Inflows" fill="#10b981" opacity={0.6} />
              <Bar dataKey="outflows" name="Outflows (incl. run-rate)" fill="#ef4444" opacity={0.6} />
              <Line type="monotone" dataKey="endCash" name="Cash at week end" stroke="#e2e8f0" strokeWidth={2} dot={false} />
            </ComposedChart>
          </ResponsiveContainer>
        </div>
        <div style={{ fontSize: 11 }}>
          <div className="label" style={{ marginBottom: 4 }}>Annual lumps in the window</div>
          {f.lines.filter(l => /lump/.test(l.feed)).length === 0 && <div style={{ color: '#64748b' }}>None detected or stored inside the 13 weeks.</div>}
          {f.lines.filter(l => /lump/.test(l.feed)).map((l, k) => <div key={k} style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8', borderBottom: '1px solid #161b22', padding: '2px 0' }}><span>{l.date} {l.label} <span style={{ fontSize: 9, color: '#475569' }}>{l.feed}</span></span><span style={{ color: '#e2e8f0' }}>{fmtCur(l.amount)}</span></div>)}
          <div className="label" style={{ margin: '8px 0 4px' }}>Stored lumps (tuition, premiums, property tax)</div>
          {(lumps || []).length === 0 && <div style={{ color: '#64748b', fontSize: 10 }}>None stored. Detected annual recurrences: {f.recurring.lumps.length ? f.recurring.lumps.map(l => `${l.label} ${fmtCur(l.amount)} next ${l.nextDate}`).join(' · ') : 'none'}.</div>}
          {(lumps || []).map(l => <div key={l.id} style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8', padding: '2px 0' }}><span>{l.date} {l.label}</span><span>{fmtCur(l.amount)} <a style={{ color: '#ef4444', cursor: 'pointer', marginLeft: 6 }} onClick={() => onSaveLumps(lumps.filter(x => x.id !== l.id))}>✕</a></span></div>)}
          {adding ? (
            <div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
              <input placeholder="Label" value={adding.label} style={{ width: 130, fontSize: 11 }} onChange={e => setAdding(a => ({ ...a, label: e.target.value }))} />
              <input type="date" value={adding.date} style={{ width: 130, fontSize: 11 }} onChange={e => setAdding(a => ({ ...a, date: e.target.value }))} />
              <input type="number" placeholder="$" value={adding.amount} style={{ width: 90, fontSize: 11 }} onChange={e => setAdding(a => ({ ...a, amount: e.target.value }))} />
              <button className="btn-primary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={saveLump}>Add</button>
              <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setAdding(null)}>Cancel</button>
            </div>
          ) : <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px', marginTop: 4 }} onClick={() => setAdding({ label: '', date: '', amount: '' })}>Add a lump</button>}
          <div style={{ marginTop: 8, display: 'flex', gap: 6 }}><button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setShowLines(v => !v)}>{showLines ? 'Hide' : 'Show'} every line ({f.lines.length})</button>
            <ExportCsv name="cash-forecast-weeks" label="Export weeks CSV" rows={f.rows.map(r => ({ week: r.week, from: r.from, to: r.to, inflows: r.inflows.reduce((s, l) => s + l.amount, 0), outflows: r.outflows.reduce((s, l) => s + l.amount, 0), discretionary: r.discretionary, net: r.net, endCash: r.endCash, lines: r.inflows.concat(r.outflows).map(l => `${l.date} ${l.label} ${l.amount} [${l.feed}]`).join(' | ') }))} />
            <ExportCsv name="cash-forecast-lines" label="Export lines CSV" rows={f.lines.map(l => ({ date: l.date, label: l.label, amount: l.amount, feed: l.feed, source: l.source || '', note: l.note || '' }))} /></div>
          {showLines && <div style={{ fontSize: 10, color: '#64748b', marginTop: 4, maxHeight: 220, overflowY: 'auto' }}>{f.lines.map((l, k) => <div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}><span>{l.date} {l.label} <span style={{ color: '#475569' }}>{l.feed}{l.note ? ` · ${l.note}` : ''}</span></span><span style={{ color: l.amount > 0 ? '#10b981' : '#ef4444' }}>{fmtCur(l.amount)}</span></div>)}</div>}
          <div style={{ fontSize: 9, color: '#475569', marginTop: 6 }}>Recurring bills modelled: {fmtCur(f.recurringMonthly)}/mo of {fmtCur(f.monthlyTrueSpend)}/mo true spend; the rest is the run-rate line.</div>
        </div>
      </div>
    </div>
  );
}

// ── M4: 401(k) contribution pacing — the paycheck shape ───────────────────
function ContributionPacingCard({ pacing, cfg, onPatch }) {
  const [draft, setDraft] = useState(null);
  if (!pacing) return null;
  const p = pacing;
  const d = draft || { deferralBasePct: cfg.deferralBasePct ?? '', deferralBonusPct: cfg.deferralBonusPct ?? '' };
  const rows = p.schedule.rows;
  const data = rows.map(r => ({ date: r.date.slice(5), cumulative: r.cumulative, deferral: r.deferral, capped: r.capped }));
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: p.onTrack ? 'rgba(16,185,129,0.35)' : 'rgba(245,158,11,0.45)' }}>
      <CardHead title="401(k) contribution pacing — the paycheck shape" sub={`${p.election.basePct == null ? 'No election recorded' : `${p.election.basePct}% of base`}${p.election.bonus > 0 ? ` + ${p.election.bonusPct}% of the ${fmtCur(p.election.bonus)} bonus` : ''} against the ${fmtCur(p.limit)} limit (${p.limitSource}). ${p.match.note}`} kind="heuristic" source={`payslips · compensation[${p.year}] · taxConstants`}
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: p.onTrack ? '#10b981' : '#f59e0b', lineHeight: 1 }}>{p.capDate ? p.capDate : fmtCur(p.unusedSpace)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{p.capDate ? `cap date · stub ${p.capPeriod} of ${rows.length}` : `unused 402(g) space · ${p.periodsLeft} periods left`}</div></div>} />
      <div style={{ fontSize: 12, color: '#e2e8f0', marginBottom: 6 }}>{p.paycheckShape.headline}.</div>
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <div>
          <ResponsiveContainer width="100%" height={170}>
            <ComposedChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
              <CartesianGrid stroke="#161b22" />
              <XAxis dataKey="date" tick={{ fontSize: 9, fill: '#64748b' }} axisLine={false} tickLine={false} interval={3} />
              <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
              <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} />
              <ReferenceLine y={p.limit} stroke="#ef4444" strokeDasharray="4 3" label={{ value: `402(g) ${fmtCur(p.limit)}`, position: 'insideTopRight', fontSize: 9, fill: '#ef4444' }} />
              <Bar dataKey="deferral" name="Deferral on the stub" fill="#8b5cf6" opacity={0.7} />
              <Line type="stepAfter" dataKey="cumulative" name="Cumulative" stroke="#e2e8f0" dot={false} strokeWidth={2} />
            </ComposedChart>
          </ResponsiveContainer>
        </div>
        <div style={{ fontSize: 11, color: '#94a3b8' }}>
          <div>YTD <strong style={{ color: '#e2e8f0' }}>{fmtCur(p.ytd)}</strong> <span style={{ fontSize: 9, color: '#475569' }}>({p.ytdSource})</span> · projected year end <strong style={{ color: '#e2e8f0' }}>{fmtCur(p.projectedYearEnd)}</strong>{p.needPerPeriod != null && !p.onTrack ? <> · need <strong style={{ color: '#f59e0b' }}>{fmtCur(p.needPerPeriod)}</strong>/stub for the remaining {p.periodsLeft}</> : null}</div>
          <div style={{ marginTop: 4 }}>Per stub: {fmtCur(p.paycheckShape.perStubDeferral)} deferred → <strong style={{ color: '#10b981' }}>{fmtCur(p.paycheckShape.returnedPerStub)}</strong> returns to take-home after the cap{p.paycheckShape.netBefore != null ? ` (${fmtCur(p.paycheckShape.netBefore)} → ${fmtCur(p.paycheckShape.netAfter)} net)` : ''}. <span style={{ fontSize: 9, color: '#475569' }}>{p.paycheckShape.basis}</span></div>
          {p.match.atRisk > 0 && <div style={{ marginTop: 4, color: '#ef4444' }}>Match at risk: {fmtCur(p.match.atRisk)} — {p.match.note}</div>}
          {p.section415c.limit && <div style={{ marginTop: 4 }}>§415(c): {fmtCur(p.section415c.used)} of {fmtCur(p.section415c.limit)} (employee + employer + after-tax) — {fmtCur(p.section415c.room)} of room.</div>}
          <div style={{ marginTop: 8, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
            <span className="label" style={{ marginBottom: 0 }}>Election</span>
            <input type="number" step="0.5" placeholder={`base % (${p.election.basePct ?? '?'})`} value={d.deferralBasePct} style={{ width: 90, fontSize: 11 }} onChange={e => setDraft({ ...d, deferralBasePct: e.target.value })} />
            <input type="number" step="0.5" placeholder={`bonus % (${p.election.bonusPct ?? '?'})`} value={d.deferralBonusPct} style={{ width: 90, fontSize: 11 }} onChange={e => setDraft({ ...d, deferralBonusPct: e.target.value })} />
            {draft && <button className="btn-primary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => { onPatch({ deferralBasePct: draft.deferralBasePct === '' ? null : Number(draft.deferralBasePct), deferralBonusPct: draft.deferralBonusPct === '' ? null : Number(draft.deferralBonusPct) }); setDraft(null); }}>Save</button>}
            <span style={{ fontSize: 9, color: '#475569' }}>{p.election.basePctSource}; bonus: {p.election.bonusPctSource}</span>
          </div>
          <div style={{ fontSize: 9, color: '#475569', marginTop: 6 }}>The paycheck shape feeds the 13-week forecast above: the stubs after the cap carry the larger net. <ExportCsv name="401k-pacing-schedule" rows={rows.map(r => ({ stub: r.index + 1, date: r.date, wanted: r.wanted, deferral: r.deferral, cumulative: r.cumulative, capped: r.capped, note: r.note || '' }))} /></div>
        </div>
      </div>
    </div>
  );
}

// ── M7: build taxConfig from what is on file ──────────────────────────────
function TaxConfigBuilderCard({ proposal, onApply }) {
  if (!proposal) return null;
  const pr = proposal;
  const applied = pr.apply.length, dis = pr.disagreements.length;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: pr.empty ? 'rgba(59,130,246,0.45)' : undefined }}>
      <CardHead title={pr.empty ? 'Tax config is empty — build it from what is on file' : 'Tax config vs what is on file'} sub={`Every field the filed returns, both spouses' payslips, the deductions ledger and the kids plan can supply, with its source. Unset fields land with one click; a typed value that disagrees is shown, never overwritten.`} kind="data" source="taxReturns · payslips · spousePayslips · taxDeductions · kidsPlan · retirementConfig"
        right={pr.patch ? <button className="btn-primary" style={{ fontSize: 11, padding: '5px 12px' }} onClick={() => onApply(pr.patch)}>Apply {applied} field{applied === 1 ? '' : 's'}</button> : <span style={{ fontSize: 10, color: '#10b981' }}>nothing to apply</span>} />
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Field</th><th style={{ textAlign: 'right' }}>On file</th><th style={{ textAlign: 'right' }}>Config now</th><th>Source</th><th></th></tr></thead>
        <tbody>{pr.proposals.map(p => (
          <tr key={p.key + p.source} style={{ color: p.differs && !p.override ? '#f59e0b' : '#94a3b8' }}>
            <td style={{ padding: '3px 8px', color: '#e2e8f0' }}>{p.label}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right', color: '#e2e8f0' }}>{typeof p.value === 'number' ? fmtCur(p.value) : String(p.value)}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right' }}>{p.current == null || p.current === '' ? <span style={{ color: '#475569' }}>unset</span> : typeof p.current === 'number' ? fmtCur(p.current) : String(p.current)}</td>
            <td style={{ padding: '3px 8px', fontSize: 10, color: '#475569' }}>{p.source}{p.note ? ` — ${p.note}` : ''}</td>
            <td style={{ padding: '3px 8px', fontSize: 9 }}>{p.same ? <span style={{ color: '#10b981' }}>agrees</span> : p.differs && !p.override ? <span style={{ color: '#f59e0b' }}>disagrees — kept yours</span> : p.override ? <span style={{ color: '#3b82f6' }}>current-year ledger</span> : <span style={{ color: '#3b82f6' }}>will apply</span>}</td>
          </tr>))}</tbody>
      </table>
      {pr.missing.length > 0 && <div style={{ fontSize: 10, color: '#64748b', marginTop: 6 }}>Nothing on file for: {pr.missing.map(m => m.label).join(', ')} — {pr.sources.returns ? '' : 'upload last year\'s return on this tab; '}type them below.</div>}
      {dis > 0 && <div style={{ fontSize: 10, color: '#f59e0b', marginTop: 4 }}>{dis} typed value{dis === 1 ? '' : 's'} disagree{dis === 1 ? 's' : ''} with the filed figure; the Tax tab already reads filed figures over typed ones for the fields it knows.</div>}
    </div>
  );
}

// ── M5: asset location against the engine, and rebalance by tax cost ─────
function AssetLocationCard({ holdings, accounts, classify, drift, cfg }) {
  const ALA = window.AssetLocationAudit;
  const [showAll, setShowAll] = useState(false);
  const rates = useMemo(() => {
    const c = cfg || {};
    const m = window.MarginalRateCurve ? window.MarginalRateCurve.householdMarginal({ cfg: c, year: new Date().getFullYear() }) : { totalPct: 30.6, statePct: 6.6 };
    return { ordinaryPct: m.totalPct, ltcgPct: 15 + m.statePct, dividendYieldPct: c.dividendYieldPct || 1.8, equityGrowthPct: c.expectedReturn || 7 };
  }, [cfg]);
  const audit = useMemo(() => ALA ? ALA.audit({ holdings, accounts, classify, ...rates }) : null, [ALA, holdings, accounts, classify, rates]);
  const rb = useMemo(() => ALA && audit ? ALA.rebalance({ audit, drift, ltcgPct: rates.ltcgPct }) : null, [ALA, audit, drift, rates]);
  if (!audit) return null;
  const rows = showAll ? audit.rows : audit.mislocated;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: audit.totalDrag > 500 ? 'rgba(245,158,11,0.45)' : undefined }}>
      <CardHead title="Asset location vs the engine's recommendation" sub={`lib/assetLocation puts equity tax-free first and bonds deferred first. Each holding is scored where it sits; mis-located ones ranked by annual tax drag. 529/UTMA (${fmtCur(audit.excludedTotal)}) are out of scope. ${audit.swap.note}`} kind="heuristic" source="holdings · accounts · marginal rates"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: audit.totalDrag > 500 ? '#f59e0b' : '#10b981', lineHeight: 1 }}>{fmtCur(audit.totalDrag)}<span style={{ fontSize: 10, color: '#64748b' }}>/yr</span></div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>tax drag from mis-located holdings · {audit.mislocated.length} of {audit.rows.length}</div></div>} />
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 10, color: '#64748b', marginBottom: 6 }}>
        {['taxable', 'deferred', 'taxFree'].map(t => { const b = audit.byTreatment[t]; return <span key={t}>{t}: equity {fmtCur(b.equity)} · bonds {fmtCur(b.bond)}{b.other ? ` · other ${fmtCur(b.other)}` : ''}</span>; })}
        <span>rates: ordinary {audit.rates.ordinaryPct}% · LTCG {audit.rates.ltcgPct}% · bond yield {audit.rates.bondYieldPct}% · growth {audit.rates.equityGrowthPct}%</span>
      </div>
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Holding</th><th>Account</th><th>Class</th><th>Sits in</th><th>Best home</th><th style={{ textAlign: 'right' }}>Value</th><th style={{ textAlign: 'right' }}>Drag /yr</th><th>Why</th></tr></thead>
        <tbody>{rows.slice(0, 14).map((r, k) => (
          <tr key={k} style={{ color: r.mislocated ? '#e2e8f0' : '#64748b' }}>
            <td style={{ padding: '3px 8px' }}>{r.ticker || r.name}</td><td style={{ padding: '3px 8px' }}>{r.account}</td><td style={{ padding: '3px 8px' }}>{r.assetClass}</td>
            <td style={{ padding: '3px 8px' }}>{r.treatment}</td><td style={{ padding: '3px 8px', color: r.mislocated ? '#10b981' : undefined }}>{r.bestHome}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right' }}>{fmtCur(r.value)}</td><td style={{ padding: '3px 8px', textAlign: 'right', color: r.mislocated ? '#f59e0b' : undefined }}>{fmtCur(r.dragAnnual)}</td>
            <td style={{ padding: '3px 8px', fontSize: 10, color: '#475569' }}>{r.why}</td>
          </tr>))}</tbody>
      </table>
      {audit.mislocated.length === 0 && !showAll && <div style={{ fontSize: 11, color: '#10b981' }}>Nothing mis-located by the engine's rule.</div>}
      <div style={{ marginTop: 4, display: 'flex', gap: 6 }}><button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setShowAll(v => !v)}>{showAll ? 'Mis-located only' : `All ${audit.rows.length} in-scope holdings`}</button>
        <ExportCsv name="asset-location" rows={audit.rows.map(r => ({ holding: r.ticker || r.name, account: r.account, treatment: r.treatment, assetClass: r.assetClass, value: r.value, bestHome: r.bestHome, mislocated: r.mislocated, dragAnnual: r.dragAnnual, why: r.why }))} /></div>
      {rb && (rb.moves.length > 0 || rb.buys.length > 0) && (
        <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px', fontSize: 11 }}>
          <div className="label" style={{ marginBottom: 4 }}>Rebalance to the targets — ranked by tax cost ({fmtCur(rb.totalTaxCost)} total)</div>
          {rb.moves.map((m, k) => <div key={k} style={{ color: '#94a3b8' }}>Sell {fmtCur(m.sell)} of {m.assetClass}{m.holding ? ` (${m.holding} in ${m.account})` : ''} — <strong style={{ color: m.taxCost ? '#f59e0b' : '#10b981' }}>{m.taxCost == null ? 'unmatched' : m.taxCost ? `${fmtCur(m.taxCost)} tax` : 'no tax'}</strong> <span style={{ fontSize: 9, color: '#475569' }}>{m.why}{m.treatment === 'taxable' && !m.basisKnown ? ' (basis unknown — 40% gain assumed)' : ''}</span></div>)}
          {rb.buys.map((b, k) => <div key={k} style={{ color: '#94a3b8' }}>Buy {fmtCur(b.buy)} of {b.assetClass} — {b.where} <span style={{ fontSize: 9, color: '#475569' }}>{b.why}</span></div>)}
          <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>{rb.note}</div>
        </div>
      )}
    </div>
  );
}

// ── C5: statements against stored figures ─────────────────────────────────
function DocumentReconcileCard({ result }) {
  if (!result || (!result.rows.length && !(result.unmatched || []).length)) return null;
  const r = result;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: r.disagreements.length ? 'rgba(245,158,11,0.45)' : 'rgba(16,185,129,0.3)' }}>
      <CardHead title="Statements vs stored figures" sub={`Statements are periodic and carry the breakdown; Monarch carries the daily total. Every statement total is compared to its OWN account's Monarch balance on the statement date (else a same-month snapshot) — never to today's balance, which is growth and contributions later, and never to a total. A disagreement past ${r.thresholdPct}% is an insight, never a silent overwrite.`} kind="data" source="documents · monarch/accountBalanceHistory · derived/netWorthSnapshots · retirementConfig"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: r.disagreements.length ? '#f59e0b' : '#10b981', lineHeight: 1 }}>{r.disagreements.length}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>disagree · {r.agreements.length} agree{r.unconverted.length ? ` · ${r.unconverted.length} unconverted` : ''}{(r.notCompared || []).length ? ` · ${r.notCompared.length} not compared` : ''}</div></div>} />
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Statement</th><th>Figure</th><th style={{ textAlign: 'right' }}>Statement</th><th style={{ textAlign: 'right' }}>Stored</th><th style={{ textAlign: 'right' }}>Drift</th><th>Compared to</th></tr></thead>
        <tbody>{r.rows.map((x, k) => (
          <tr key={k} style={{ color: x.disagrees ? '#e2e8f0' : '#94a3b8' }}>
            <td style={{ padding: '3px 8px' }}>{x.kindLabel} {x.statementDate || ''}{x.institution ? <span style={{ fontSize: 9, color: '#475569' }}> · {x.institution}</span> : null}</td>
            <td style={{ padding: '3px 8px' }}>{x.field}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right' }}>{x.unit === 'pct' ? `${x.statementValue}%` : x.currency && x.currency !== 'USD' && x.unconverted ? `${x.currency} ${x.statementValue.toLocaleString()}` : fmtCur(x.statementValue)}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right' }}>{x.unit === 'pct' ? `${x.storedValue}%` : fmtCur(x.storedValue)}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right', color: x.unconverted || x.notCompared ? '#475569' : x.disagrees ? '#f59e0b' : '#10b981' }}>{x.unconverted || x.notCompared ? 'n/a' : x.unit === 'pct' ? `${x.drift > 0 ? '+' : ''}${x.drift} pts` : `${x.drift > 0 ? '+' : ''}${fmtCur(x.drift)} (${x.driftPct > 0 ? '+' : ''}${x.driftPct}%)`}</td>
            <td style={{ padding: '3px 8px', fontSize: 10, color: '#475569' }}>{x.storedLabel}{x.note ? ` — ${x.note}` : ''}</td>
          </tr>))}</tbody>
      </table>
      <div style={{ marginTop: 6 }}><ExportCsv name="statement-reconciliation" rows={r.rows.map(x => ({ statement: x.kindLabel, date: x.statementDate, field: x.field, statementValue: x.statementValue, storedValue: x.storedValue, drift: x.drift, driftPct: x.driftPct, comparedTo: x.storedLabel, basis: x.notCompared ? 'not compared' : x.basis || null, disagrees: x.disagrees }))} /></div>
      {(r.unmatched || []).length > 0 && <div style={{ fontSize: 10, color: '#94a3b8', marginTop: 6 }}>Not compared — {r.unmatched.length} statement series {r.unmatched.length === 1 ? 'is' : 'are'} not tied to one account, and a statement is never compared to a total: {r.unmatched.map(u => `${u.kindLabel}${u.institution ? ` (${u.institution})` : ''} ${fmtCur(u.statementValue)} — ${u.why}`).join(' · ')}. Link them on the Balance history card below or on each document row.</div>}
      {r.disagreements.length > 0 && <div style={{ fontSize: 10, color: '#f59e0b', marginTop: 6 }}>A disagreement is a question: re-sync Monarch if the account is stale, apply the statement's split on the row below if the statement is right, or leave the stored figure and note why. Nothing here writes.</div>}
    </div>
  );
}

// ── Statement balance history: the years before Monarch, from uploads ─────
function StatementHistoryCard({ result, accounts, onLink }) {
  const list = (result && result.series) || [];
  const [pick, setPick] = useState(null);
  if (!list.length) return null;
  const s = list.find(x => x.key === pick) || list[0];
  const ts = d => new Date(`${d}T00:00:00Z`).getTime();
  const todayIso = new Date().toISOString().slice(0, 10);
  const rows = [
    ...s.statements.map(p => ({ t: ts(p.date), date: p.date, statement: p.value })),
    ...s.tracked.map(p => ({ t: ts(p.date), date: p.date, tracked: p.value })),
    ...(s.today != null ? [{ t: ts(todayIso), date: todayIso, tracked: s.today }] : []),
  ].sort((a, b) => a.t - b.t);
  const money = v => s.currency === 'USD' ? fmtCur(v) : `${s.currency} ${Math.round(v).toLocaleString()}`;
  const yr = t => new Date(t).toISOString().slice(0, 7);
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Balance history" sub="Each uploaded statement's balance as a point, joined to the account's Monarch month-end balances and today's — the years before Monarch come only from the statements." kind="data" source="documents (extracted figures) · monarch/accountBalanceHistory · derived/netWorthSnapshots · accounts"
        right={s.change != null ? <div style={{ textAlign: 'right' }}><div style={{ fontSize: 18, fontWeight: 800, color: s.change >= 0 ? '#10b981' : '#ef4444', lineHeight: 1 }}>{s.change >= 0 ? '+' : ''}{money(s.change)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{s.first.date.slice(0, 7)} → {s.last.date.slice(0, 7)} on statements</div></div> : null} />
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
        {list.map(x => <button key={x.key} onClick={() => setPick(x.key)} className={x.key === s.key ? 'btn-primary' : 'btn-secondary'} style={{ fontSize: 10, padding: '2px 8px', borderStyle: x.linked ? 'solid' : 'dashed' }}>{x.label} · {x.statements.length}</button>)}
      </div>
      {onLink && window.DocumentReconcile && (() => {
        const cands = window.DocumentReconcile.linkCandidates(s.kind, accounts);
        if (!cands.length || s.kind === 'ukPensionStatement') return null;
        return (
          <div style={{ fontSize: 10, color: s.linked ? '#64748b' : '#f59e0b', marginBottom: 6, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
            {s.linked ? `Account: ${s.accounts.join(' + ')} (${s.why}).` : `Not linked to an account — ${s.why}. These ${s.statements.length} statements look like one account (grouped by date and value):`}
            <select value="" onChange={e => e.target.value && onLink(s.statements.map(p => p.documentId), e.target.value === '__auto' ? null : e.target.value)} style={{ width: 'auto', fontSize: 10, padding: '1px 4px' }}>
              <option value="">{s.linked ? 'change account…' : 'link all to…'}</option>
              {cands.map(a => <option key={a.id} value={a.id}>{a.displayName || a.name || a.id}</option>)}
              {s.linked && <option value="__auto">clear link (auto)</option>}
            </select>
          </div>
        );
      })()}
      <ResponsiveContainer width="100%" height={200}>
        <LineChart data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
          <CartesianGrid stroke="#161b22" />
          <XAxis dataKey="t" type="number" scale="time" domain={['dataMin', 'dataMax']} tickFormatter={yr} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
          <YAxis domain={[0, 'auto']} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => v >= 1000 ? `${Math.round(v / 1000)}k` : v} width={44} />
          <Tooltip contentStyle={TIP} labelFormatter={t => new Date(t).toISOString().slice(0, 10)} formatter={(v, n) => [v == null ? '—' : money(v), n]} />
          <Line type="monotone" dataKey="statement" name="Statement" stroke="#8b5cf6" dot={{ r: 3 }} connectNulls isAnimationActive={false} />
          <Line type="monotone" dataKey="tracked" name="Tracked (Monarch)" stroke="#10b981" strokeDasharray="4 3" dot={{ r: 2 }} connectNulls isAnimationActive={false} />
        </LineChart>
      </ResponsiveContainer>
      <div style={{ fontSize: 10, color: '#475569', marginTop: 4 }}>
        {s.statements.length} statement{s.statements.length === 1 ? '' : 's'} {s.first.date} → {s.last.date}
        {s.accounts.length ? ` · tracked line is ${s.accounts.join(' + ')}` : ' · no Monarch account matched, so statements only'}
        {s.duplicates ? ` · ${s.duplicates} duplicate upload${s.duplicates === 1 ? '' : 's'} (same date) counted once` : ''}
        {s.currency !== 'USD' ? ` · in ${s.currency} (no FX rate supplied)` : ''}
      </div>
      <div style={{ marginTop: 6 }}><ExportCsv name="statement-history" rows={list.flatMap(x => x.statements.map(p => ({ series: x.label, date: p.date, value: p.value, currency: x.currency })))} /></div>
    </div>
  );
}

// ── C6: the shared household timeline strip ───────────────────────────────
function TimelineStrip({ timeline, height, highlight }) {
  const ref = React.useRef(null);
  const [width, setWidth] = useState(0);
  useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const read = () => setWidth(el.clientWidth || 0);
    read();
    if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', read); return () => window.removeEventListener('resize', read); }
    const ro = new ResizeObserver(read);
    ro.observe(el);
    return () => ro.disconnect();
  }, [!!timeline]);
  if (!timeline || !timeline.markers) return null;
  const { from, to, markers, bands, events } = timeline;
  const span = Math.max(1, to - from);
  const x = (y) => Math.max(0, Math.min(100, (y - from) / span * 100));
  // Labels go in lanes: each marker takes the first lane its label fits in
  // without touching the previous one (IBM Plex Mono is 0.6em per glyph),
  // and a label that would run off the right edge sits left of its line.
  // Two fixed rows overprinted as soon as three markers shared a decade.
  const FONT = 9, LANE = 13, GLYPH = FONT * 0.6, GAP = 6;
  const W = width || 600;
  const laneEnds = [];
  const rows = markers.slice().sort((a, b) => a.year - b.year).map(m => {
    const px = x(m.year) / 100 * W;
    const textW = String(m.label).length * GLYPH + 4;
    const flip = px + textW > W - 2 && px - textW >= 0;
    const left = flip ? px - textW : px, right = flip ? px : px + textW;
    let lane = laneEnds.findIndex(end => end + GAP <= left);
    if (lane < 0) { lane = laneEnds.length; laneEnds.push(right); } else laneEnds[lane] = right;
    return { ...m, lane, flip };
  });
  const top = 3, dotsH = 30;
  const H = Math.max(height || 64, top + laneEnds.length * LANE + dotsH);
  return (
    <div>
      <div ref={ref} style={{ position: 'relative', height: H, background: '#0b1220', borderRadius: 6, border: '1px solid #1e2a3a', overflow: 'hidden' }}>
        {bands.map(b => <div key={b.key} title={`${b.label} (${b.from}–${b.to})${b.assumed ? ' — assumed' : ''}`} style={{ position: 'absolute', left: `${x(b.from)}%`, width: `${x(b.to) - x(b.from)}%`, top: 0, bottom: 0, background: b.color }} />)}
        {Array.from({ length: Math.floor(span / 5) + 1 }, (_, k) => from + k * 5).map(y => <div key={y} style={{ position: 'absolute', left: `${x(y)}%`, bottom: 0, height: 6, borderLeft: '1px solid #334155' }}><span style={{ position: 'absolute', bottom: 6, left: 2, fontSize: 8, color: '#475569' }}>{y}</span></div>)}
        {rows.map(m => (
          <div key={m.key} title={`${m.year} ${m.label} · ${m.source}${m.youngestAge != null ? ` · ${m.youngestName || 'youngest'} will be ${m.youngestAge}` : ''}`} style={{ position: 'absolute', left: `${x(m.year)}%`, top: top + m.lane * LANE, bottom: 12, borderLeft: `2px ${m.assumed ? 'dashed' : 'solid'} ${m.color}`, opacity: highlight && !highlight.includes(m.key.split(':')[0]) ? 0.45 : 1 }}>
            <span style={{ position: 'absolute', top: 0, ...(m.flip ? { right: 3 } : { left: 3 }), fontSize: FONT, lineHeight: `${LANE - 1}px`, color: m.color, whiteSpace: 'nowrap', background: '#0b1220', padding: '0 2px', borderRadius: 2, zIndex: 1 }}>{m.label}</span>
          </div>))}
        {(events || []).map(e => <div key={e.key} title={`${e.date} ${e.label}${e.net != null ? ` · net ${fmtCur(e.net)}` : ''}${e.assigned ? '' : ' · unassigned'}`} style={{ position: 'absolute', left: `calc(${x(Number(e.date.slice(0, 4)) + (Number(e.date.slice(5, 7)) - 1) / 12)}% - 3px)`, top: e.kind === 'vest' ? H - 30 : H - 24, width: 6, height: 6, borderRadius: 3, background: e.kind === 'vest' ? '#06b6d4' : '#f59e0b', opacity: e.projected ? 0.5 : 1, border: e.assigned ? 'none' : '1px solid #e2e8f0' }} />)}
      </div>
      <div style={{ fontSize: 9, color: '#475569', marginTop: 3 }}>{from} → {to} · one timeline for every tab: retirements, Medicare, Social Security, RMDs, mortgage payoff, term-life expiry, college windows, every projected vest (●) and bonus (●). Dashed = assumed.</div>
    </div>
  );
}

// Every tab builds the same timeline from the same stores.
function useHouseholdTimeline(extra) {
  const d = useContext(DataContext);
  return useMemo(() => {
    const HT = window.HouseholdTimeline, VB = window.VestBonusCalendar, DP = window.DebtPlan;
    if (!HT) return null;
    const cfg = d.retirementConfig || {};
    const Sens = window.Sensitivity;
    const modelCfg = { ...cfg, ...(Sens ? Sens.presetPatch(cfg.headlinePreset || 'base', cfg) : {}), currentAge: cfg.currentAge || calcAge(USER_PROFILE.birthday), wifeAge: cfg.wifeAge || calcAge(USER_PROFILE.spouseBirthday) };
    const calendar = VB ? VB.events({ equity: d.equityConfig, compensation: d.compensation, salary: cfg.salary, assignments: d.eventAssignments, horizonYears: 5 }) : null;
    let mortgagePayoffYear = null, mortgageSource = null;
    if (DP && d.debtConfig) {
      const L = DP.liabilities({ accounts: d.accounts, debtConfig: d.debtConfig, transactions: [] });
      const m = L.rows.find(r => r.kind === 'mortgage' && r.payoffDate);
      if (m) { mortgagePayoffYear = Number(m.payoffDate.slice(0, 4)); mortgageSource = `debtPlan (${m.name} amortised at the minimum)`; }
    }
    return HT.build({ cfg: modelCfg, currentYear: new Date().getFullYear(), kidsPlan: d.kidsPlan, estatePlan: d.estatePlan, calendar, mortgagePayoffYear, mortgageSource, ...(extra || {}) });
  }, [d.retirementConfig, d.equityConfig, d.compensation, d.eventAssignments, d.debtConfig, d.accounts, d.kidsPlan, d.estatePlan, extra]);
}

// ── M10: human capital on the balance sheet ───────────────────────────────
function HumanCapitalCard({ solve, setTab }) {
  const d = useContext(DataContext);
  const HC = window.HumanCapital;
  const [newSalary, setNewSalary] = useState(null);
  const cfg = d.retirementConfig || {};
  const c = useMemo(() => ({ ...cfg, currentAge: cfg.currentAge || calcAge(USER_PROFILE.birthday), wifeAge: cfg.wifeAge || calcAge(USER_PROFILE.spouseBirthday) }), [cfg]);
  const nw = useMemo(() => (d.accounts || []).reduce((s, a) => s + (Number(a.currentBalance) || 0), 0), [d.accounts]);
  const v = useMemo(() => HC ? HC.value({ cfg: c, financialNetWorth: nw, employerTicker: d.equityConfig && d.equityConfig.ticker }) : null, [HC, c, nw, d.equityConfig]);
  const change = useMemo(() => HC && newSalary != null ? HC.jobChange({ cfg: c, newSalary, solve }) : null, [HC, c, newSalary, solve]);
  if (!v) return null;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(59,130,246,0.35)' }}>
      <CardHead title="Human capital — the asset the paycheck is" sub={`PV of both earnings streams to each retirement, discounted at ${v.discountPct}% (${v.discountBasis}), after tax at ${v.effectiveTaxPct}%. ${v.employer.note || ''}`} kind="heuristic" source="retirementConfig (salary, growth, ages) · accounts"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: '#3b82f6', lineHeight: 1 }}>{fmtCur(v.afterTax)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>after-tax PV · {v.balanceSheet ? `${v.balanceSheet.humanCapitalSharePct}% of the balance sheet` : ''}</div></div>} />
      <div className="grid-3" style={{ gap: 10, fontSize: 11 }}>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 10px' }}><div className="label" style={{ marginBottom: 2 }}>You</div><div style={{ color: '#e2e8f0', fontWeight: 600 }}>{fmtCur(v.you.pv)}</div><div style={{ color: '#64748b', fontSize: 10 }}>{fmtCur(v.you.annual)} × {v.you.years} yrs at {v.you.growthPct}% growth</div></div>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 10px' }}><div className="label" style={{ marginBottom: 2 }}>Spouse</div><div style={{ color: '#e2e8f0', fontWeight: 600 }}>{fmtCur(v.spouse.pv)}</div><div style={{ color: '#64748b', fontSize: 10 }}>{fmtCur(v.spouse.annual)} × {v.spouse.years} yrs at {v.spouse.growthPct}% growth</div></div>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 10px' }}><div className="label" style={{ marginBottom: 2 }}>Balance sheet</div><div style={{ color: '#e2e8f0', fontWeight: 600 }}>{v.balanceSheet ? fmtCur(v.balanceSheet.total) : '—'}</div><div style={{ color: '#64748b', fontSize: 10 }}>financial {fmtCur(nw)} + human capital {fmtCur(v.afterTax)}</div></div>
      </div>
      <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 12px', fontSize: 11 }}>
        <div className="label" style={{ marginBottom: 4 }}>A job change — drag the package</div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          <input type="range" min={Math.round((c.salary || 300000) * 0.5 / 5000) * 5000} max={Math.round((c.salary || 300000) * 1.5 / 5000) * 5000} step={5000} value={newSalary == null ? (c.salary || 0) : newSalary} style={{ flex: 1, minWidth: 160 }} onChange={e => setNewSalary(Number(e.target.value))} />
          <span style={{ minWidth: 260 }}>
            <strong style={{ color: '#e2e8f0' }}>{fmtCur(newSalary == null ? c.salary : newSalary)}</strong>
            {change && <> → PV <strong style={{ color: change.pvDelta >= 0 ? '#10b981' : '#ef4444' }}>{change.pvDelta >= 0 ? '+' : ''}{fmtCur(change.pvDelta)}</strong> ({change.pvDeltaPct >= 0 ? '+' : ''}{change.pvDeltaPct}%){change.ratio ? <> · funded ratio <strong style={{ color: change.ratio.delta >= 0 ? '#10b981' : '#ef4444' }}>{change.ratio.before.toFixed(2)}× → {change.ratio.after.toFixed(2)}×</strong> <Tag kind="engine">retirementNeeds</Tag></> : solve ? null : <> · ratio effect: {setTab ? <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => { try { sessionStorage.setItem('retirementScreen', 'drivers'); } catch (e) {} setTab('Retirement'); }}>Retirement → Drivers</a> : 'Retirement → Drivers'}</>}</>}
          </span>
          {newSalary != null && <button className="btn-secondary" style={{ fontSize: 9, padding: '1px 6px' }} onClick={() => setNewSalary(null)}>reset</button>}
        </div>
        <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>Nothing is saved by dragging. The stored election (annualContribution) is kept as is — a smaller package with the same dollar deferral saves the same dollars.</div>
      </div>
    </div>
  );
}

// ── M9: the school → college handoff ──────────────────────────────────────
function EducationCurveCard({ curve, setTab }) {
  if (!curve || !curve.rows.length) return null;
  const c = curve;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(245,158,11,0.35)' }}>
      <CardHead title="Education cash flow — private school ends, college begins" sub={`${c.handoffs.map(h => h.note).join('; ')}. Costs inflated ${c.eduInflationPct}%/yr; 529s grow ${c.returnPct}% and are drawn first in college years; the rest is cash flow.`} kind="heuristic" source={c.source}
        right={c.peak && <div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: '#f59e0b', lineHeight: 1 }}>{fmtCur(c.peak.total)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>peak education year · {c.peak.year}</div></div>} />
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <ResponsiveContainer width="100%" height={220}>
          <ComposedChart data={c.rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
            <CartesianGrid stroke="#161b22" />
            <XAxis dataKey="year" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
            <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
            <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} />
            <Legend wrapperStyle={{ fontSize: 10 }} />
            <Bar dataKey="school" name="Private school" stackId="a" fill="#f59e0b" opacity={0.75} />
            <Bar dataKey="college" name="College" stackId="a" fill="#3b82f6" opacity={0.75} />
            <Line type="stepAfter" dataKey="from529" name="Paid by 529s" stroke="#10b981" dot={false} strokeWidth={2} />
            <Line type="stepAfter" dataKey="fromCashflow" name="From cash flow" stroke="#e2e8f0" dot={false} strokeWidth={2} strokeDasharray="4 3" />
          </ComposedChart>
        </ResponsiveContainer>
        <div style={{ fontSize: 11 }}>
          <div className="label" style={{ marginBottom: 4 }}>529 adequacy per child</div>
          <table style={{ fontSize: 11 }}>
            <thead><tr><th>Child</th><th>College</th><th style={{ textAlign: 'right' }}>Projected 529</th><th style={{ textAlign: 'right' }}>Projected cost</th><th style={{ textAlign: 'right' }}>Gap</th><th style={{ textAlign: 'right' }}>Required /mo</th></tr></thead>
            <tbody>{c.adequacy.map(a => (
              <tr key={a.id}>
                <td style={{ padding: '3px 8px', color: '#e2e8f0' }}>{a.name}</td>
                <td style={{ padding: '3px 8px', color: a.collegeStartAssumed ? '#f59e0b' : '#94a3b8' }}>{a.collegeStart}{a.collegeStartAssumed ? ' (assumed)' : ''}</td>
                <td style={{ padding: '3px 8px', textAlign: 'right' }}>{fmtCur(a.projectedBalance)}</td>
                <td style={{ padding: '3px 8px', textAlign: 'right' }}>{fmtCur(a.projectedCost)}</td>
                <td style={{ padding: '3px 8px', textAlign: 'right', color: a.funded ? '#10b981' : '#ef4444' }}>{a.funded ? `+${fmtCur(-a.gap)}` : `−${fmtCur(a.gap)}`} <span style={{ fontSize: 9, color: '#475569' }}>({a.fundedPct}% funded)</span></td>
                <td style={{ padding: '3px 8px', textAlign: 'right', color: '#e2e8f0' }}>{fmtCur(a.requiredMonthly)} <span style={{ fontSize: 9, color: '#475569' }}>vs {fmtCur(a.monthlyNow)}</span></td>
              </tr>))}</tbody>
          </table>
          <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>{c.adequacy[0] ? c.adequacy[0].source : ''}. School ends {c.schoolEndsYear}; college ends {c.collegeEndsYear}. <ExportCsv name="education-cashflow" rows={c.rows.map(r => { const o = { year: r.year, school: r.school, college: r.college, from529: r.from529, fromCashflow: r.fromCashflow, total: r.total }; r.kids.forEach(k => { o[`${k.name}_phase`] = k.phase; o[`${k.name}_school`] = k.school; o[`${k.name}_college`] = k.college; o[`${k.name}_529`] = k.balance529; }); return o; })} /></div>
        </div>
      </div>
    </div>
  );
}

// ── M8: the coverage cliff on the timeline, and the estate gaps ranked ────
function CoverageCliffCard({ cliff, timeline, coverage }) {
  if (!cliff) return null;
  const soon = cliff.cliffs.filter(p => p.yearsUntil <= cliff.horizonYears);
  const total = cliff.cliffs.reduce((s, p) => s + p.deathBenefit, 0);
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: soon.length ? 'rgba(239,68,68,0.45)' : undefined }}>
      <CardHead title="Coverage cliff" sub={cliff.cliffs.length ? `${cliff.cliffs.map(p => `${p.insured}: ${fmtCur(p.deathBenefit)} to ${p.expires} (${p.yearsUntil} yrs; ${p.youngestName || 'youngest'} will be ${p.youngestAgeAtExpiry})`).join(' · ')}.` : 'No term policies with an expiry recorded — add them below and the cliff appears here.'} kind="data" source={cliff.source}
        right={soon.length > 0 && <div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444', lineHeight: 1 }}>{fmtCur(soon.reduce((s, p) => s + p.deathBenefit, 0))}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>lapses inside {cliff.horizonYears} years</div></div>} />
      {timeline && <TimelineStrip timeline={timeline} highlight={['termLife', 'youRetire', 'spouseRetires', 'mortgage']} />}
      {coverage && !coverage.unknown && <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 8 }}>Need today {fmtCur(coverage.need)} vs {fmtCur(coverage.inForce)} in force → {coverage.gap > 0 ? <strong style={{ color: '#ef4444' }}>{fmtCur(coverage.gap)} short</strong> : <strong style={{ color: '#10b981' }}>covered</strong>}; the need falls as the mortgage amortises and the children age out, and it is the need AT expiry that decides whether the cliff matters.</div>}
    </div>
  );
}

function EstateGapsCard({ audit, setTab }) {
  if (!audit) return null;
  const ids = /^(willInPlace|guardianshipDesignated|powerOfAttorney|livingTrust|termLife:|disability|umbrella)/;
  const rows = audit.checks.filter(c => ids.test(c.id)).sort((a, b) => (b.open - a.open) || b.consequence - a.consequence);
  const go = (link) => { if (!setTab) return; try { if (link.screen) sessionStorage.setItem('retirementScreen', link.screen); } catch (e) {} setTab(link.tab); };
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Estate completeness — ranked by consequence" sub="Each gap links to the field that closes it. 5 = a court decides who raises the children." kind="data" source="kidsPlan.householdPosture · estate" />
      <table style={{ fontSize: 11 }}><tbody>{rows.map(g => (
        <tr key={g.id} style={{ color: g.open ? '#e2e8f0' : '#475569' }}>
          <td style={{ padding: '4px 8px', width: 30, color: g.open ? (g.consequence >= 4 ? '#ef4444' : g.consequence === 3 ? '#f59e0b' : '#64748b') : '#10b981', fontWeight: 700 }}>{g.open ? g.consequence : '✓'}</td>
          <td style={{ padding: '4px 8px' }}>{g.label}<div style={{ fontSize: 10, color: '#64748b' }}>{g.why}</div></td>
          <td style={{ padding: '4px 8px', textAlign: 'right', whiteSpace: 'nowrap' }}>{g.open && <a style={{ color: '#3b82f6', cursor: 'pointer', fontSize: 10 }} onClick={() => go(g.link)}>{g.link.tab} → {g.link.field}</a>}</td>
        </tr>))}</tbody></table>
    </div>
  );
}

// ── C6: the Dashboard above the fold ──────────────────────────────────────
// Net worth + attribution, months of runway, funded ratio (base preset),
// savings rate vs assumed, the top 3 insights, the next 3 calendar events.
function DashboardHero({ setTab }) {
  const d = useContext(DataContext);
  const cfg = d.retirementConfig || {};
  const currentYear = new Date().getFullYear();
  const IR = window.InputReconciliation, Sens = window.Sensitivity, SR = window.SavingsRate, IRules = window.InsightRules, NWA = window.NetWorthAttribution;
  const mapping = useMemo(() => { const RA = window.RetirementAccounts; return IR && RA ? IR.mappingFromRows(RA.liveBalances(d.accounts || [], cfg.accountMap).rows, RA.BUCKETS) : null; }, [IR, d.accounts, cfg.accountMap]);
  const recon = useMemo(() => IR ? IR.reconcile({ cfg, accounts: d.accounts, monarchAuth: d.monarchAuth, mapping }) : null, [IR, cfg, d.accounts, d.monarchAuth, mapping]);
  const zScores = useMemo(() => Sens ? ['expectedReturn', 'salaryGrowth', 'inflationRate'].map(k => Sens.zScore(k, cfg[k])).filter(Boolean) : [], [Sens, cfg]);
  const ctx = useMemo(() => buildInsightContext({ accounts: d.accounts, transactions: d.transactions, holdings: d.holdings, equityConfig: d.equityConfig, compensation: d.compensation, payslipsByYear: d.payslipsByYear, kidsPlan: d.kidsPlan, estatePlan: d.estatePlan, spendingTaxonomy: d.spendingTaxonomy, eventAssignments: d.eventAssignments, documents: d.documents, netWorthSnapshots: d.netWorthSnapshots, accountBalanceHistory: d.accountBalanceHistory, cfg, reconciliation: recon, zScores, currentYear }),
    [d.accounts, d.transactions, d.holdings, d.equityConfig, d.compensation, d.payslipsByYear, d.kidsPlan, d.estatePlan, d.spendingTaxonomy, d.eventAssignments, d.documents, d.netWorthSnapshots, d.accountBalanceHistory, cfg, recon, zScores, currentYear]);
  const insights = useMemo(() => IRules ? IRules.evaluate(ctx, { dismissed: d.dismissedInsights || {} }) : null, [IRules, ctx, d.dismissedInsights]);
  const savings = useMemo(() => SR && d.retirementConfig ? SR.monthlySeries({ cfg, compensation: d.compensation, payslipsByYear: d.payslipsByYear, spousePayslipsByYear: d.spousePayslipsByYear, kidsPlan: d.kidsPlan, months: 12 }) : null, [SR, cfg, d.retirementConfig, d.compensation, d.payslipsByYear, d.spousePayslipsByYear, d.kidsPlan]);
  const nwSeries = useMemo(() => NWA && d.netWorthSnapshots ? NWA.series(d.netWorthSnapshots, (month) => { const m = (ctx.monthlyRows || []).find(r => r.month === month); return { txContributions: NWA.txContributionsFor(d.transactions, month, d.accounts), payrollContributions: 0, income: m ? m.income : null, spending: m ? m.spending : null }; }) : [], [NWA, d.netWorthSnapshots, d.transactions, d.accounts, ctx.monthlyRows]);
  const timeline = useHouseholdTimeline();
  const frh = useMemo(() => { const m = Object.values((d.fundedRatioHistory && d.fundedRatioHistory.months) || {}).sort((a, b) => a.month.localeCompare(b.month)); return m.length ? m[m.length - 1] : null; }, [d.fundedRatioHistory]);
  const latest = nwSeries[nwSeries.length - 1] || null;
  const netWorth = (d.accounts || []).reduce((s, a) => s + (Number(a.currentBalance) || 0), 0);
  const runway = ctx.runway;
  const sync = d.monarchAuth && d.monarchAuth.lastSync;
  const next3 = timeline ? timeline.upcoming.slice(0, 3) : [];
  const tile = (label, value, sub, color, kind, source, onClick) => (
    <div className="card" style={{ padding: '12px 14px', cursor: onClick ? 'pointer' : 'default', borderColor: color ? `${color}55` : undefined }} onClick={onClick}>
      <div className="label" style={{ marginBottom: 2 }}>{label}</div>
      <div style={{ fontSize: 24, fontWeight: 800, color: color || '#e2e8f0', lineHeight: 1.1 }}>{value}</div>
      <div style={{ fontSize: 10, color: '#94a3b8', marginTop: 4, lineHeight: 1.4 }}>{sub}</div>
      <div style={{ marginTop: 4 }}><Tag kind={kind}>{source}</Tag></div>
    </div>
  );
  const go = (tab, screen) => () => { try { if (screen) sessionStorage.setItem('retirementScreen', screen); } catch (e) {} setTab && setTab(tab); };
  const attr = latest && latest.attribution ? latest.attribution : null;
  return (
    <div style={{ marginBottom: 20 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12, marginBottom: 12 }}>
        {tile('Net worth', fmtCur(netWorth),
          attr ? <>{attr.from} → {attr.to}: <strong style={{ color: attr.netChange >= 0 ? '#10b981' : '#ef4444' }}>{attr.netChange >= 0 ? '+' : ''}{fmtCur(attr.netChange)}</strong> — saved {fmtCur(attr.components.contributions)}, market {fmtCur(attr.components.market)}, debt {fmtCur(attr.components.debtPaydown)}, cash {fmtCur(attr.components.cashChange)}</> : latest ? `snapshot ${latest.date}; the first attribution lands next month — nothing can be backfilled` : 'first snapshot written on this load',
          null, 'heuristic', `accounts · synced ${sync ? timeAgo(sync) : 'never'}`, go('Insights'))}
        {tile('Months of runway', runway && runway.months != null ? `${runway.months} mo` : '—',
          runway ? `${fmtCur(runway.cash)} cash ÷ ${fmtCur(runway.monthlySpend)}/mo true spending${runway.target != null ? ` · target ${fmtCur(runway.target)}` : ' · no emergency-fund target set'}` : 'needs synced transactions',
          runway ? (runway.severity === 'critical' ? '#ef4444' : runway.severity === 'low' ? '#f59e0b' : '#10b981') : null, 'heuristic', 'transactions via spendingTaxonomy', go('Insights'))}
        {tile('Funded ratio', frh ? (frh.ratio >= 5 ? '5×+' : `${frh.ratio.toFixed(2)}×`) : '—',
          frh ? `${frh.preset} preset (${frh.inputs.expectedReturn}% / ${frh.inputs.salaryGrowth}%) against ${fmtCur(frh.inputs.needTodayDollars || frh.inputs.spendAnnual)}/yr of projected spending · ${frh.date}${frh.ratioCurrent != null ? ` · stored assumptions ${frh.ratioCurrent.toFixed(2)}× (optimistic)` : ''}${frh.inputsUnconfirmed && frh.inputsUnconfirmed.length ? ' · PROVISIONAL: ' + frh.inputsUnconfirmed.join(', ') : ''}` : 'open the Retirement tab once to write the first reading',
          frh ? (frh.ratio >= 1 ? '#10b981' : '#ef4444') : null, 'engine', 'derived/fundedRatioHistory', go('Retirement', 'answer'))}
        {tile('Savings rate', savings && savings.trailing12.ratePct != null ? `${savings.trailing12.ratePct}%` : '—',
          savings ? `trailing 12 vs ${savings.assumed.ratePct}% the projection assumes${savings.divergencePts != null ? ` (${savings.divergencePts > 0 ? '+' : ''}${savings.divergencePts} pts)` : ''} — ${savings.note || ''}` : 'needs retirementConfig',
          savings ? (savings.diverges ? '#f59e0b' : '#10b981') : null, 'heuristic', 'payslips · retirementConfig · kidsPlan', go('Insights'))}
      </div>
      <div className="grid-2" style={{ gap: 12, alignItems: 'start' }}>
        <div>
          <div className="label" style={{ marginBottom: 6 }}>Top 3 insights — by dollar consequence</div>
          {insights && insights.insights.length ? <InsightList result={insights} onDismiss={d.dismissInsight} setTab={setTab} /> : <div className="card" style={{ fontSize: 11, color: '#64748b' }}>Nothing fires on the current data.</div>}
        </div>
        <div>
          <div className="label" style={{ marginBottom: 6 }}>Next 3 on the household timeline</div>
          <div className="card" style={{ padding: '10px 14px' }}>
            {next3.length === 0 && <div style={{ fontSize: 11, color: '#64748b' }}>Nothing dated ahead — add the retirement config, kids and policies.</div>}
            {next3.map(e => <div key={e.key} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, fontSize: 11, padding: '3px 0', borderBottom: '1px solid #161b22' }}><span style={{ color: '#e2e8f0' }}>{e.date.slice(0, 10)} · {e.label}{e.assigned === false ? <span style={{ color: '#f59e0b' }}> · unassigned</span> : null}</span><span style={{ color: '#94a3b8' }}>{e.net != null ? fmtCur(e.net) + ' net' : ''}</span></div>)}
            {timeline && <div style={{ marginTop: 8 }}><TimelineStrip timeline={timeline} height={56} /></div>}
          </div>
        </div>
      </div>
      <div style={{ fontSize: 10, color: '#475569', marginTop: 8 }}>ENGINE = an engine's own output · ESTIMATE = a heuristic rollup · DATA = stored as-is. Everything above is only as fresh as the last Monarch sync ({sync ? timeAgo(sync) : 'never'}). Detail below.</div>
    </div>
  );
}

// ── A4: compensation reconciliation ───────────────────────────────────────
function CompReconciliationCard({ recon, cfg, onPatch, compact }) {
  const [g, setG] = useState(null);
  if (!recon) return null;
  const guidance = recon.guidance;
  const draft = g || (cfg && cfg.compGuidance) || { year: recon.year + 1, base: '', icLow: '', icHigh: '', rsu: '', note: '' };
  const saveGuidance = () => { onPatch({ compGuidance: { year: Number(draft.year) || recon.year + 1, base: Number(draft.base) || 0, icLow: Number(draft.icLow) || 0, icHigh: Number(draft.icHigh) || Number(draft.icLow) || 0, rsu: Number(draft.rsu) || 0, note: draft.note || null, recordedAt: new Date().toISOString() } }); setG(null); };
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: recon.anyDrift ? 'rgba(245,158,11,0.45)' : undefined }}>
      <CardHead title="Compensation reconciliation" sub="The retirement engine's salary input beside every other reading of the same number: the compensation dataset, the last complete year, the comparable TC the planner seeds from, and this year's payslips annualised. Drift past 5% is flagged; nothing changes until you click." kind="data" source="retirementConfig · compensation · payslips"
        right={<div style={{ textAlign: 'right' }}><div style={{ fontSize: 22, fontWeight: 800, color: recon.anyDrift ? '#f59e0b' : '#10b981', lineHeight: 1 }}>{recon.engine == null ? '—' : fmtCur(recon.engine)}</div><div style={{ fontSize: 9, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.08em' }}>engine input · {recon.drifted.length} source{recon.drifted.length === 1 ? '' : 's'} past {recon.thresholdPct}%</div></div>} />
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Source</th><th style={{ textAlign: 'right' }}>Total comp</th><th style={{ textAlign: 'right' }}>vs engine</th><th>Provenance</th></tr></thead>
        <tbody>{recon.rows.map(r => (
          <tr key={r.key} style={{ color: r.anchor ? '#e2e8f0' : '#94a3b8' }}>
            <td style={{ padding: '3px 8px' }}>{r.label}{r.projected && <span style={{ fontSize: 9, color: '#f59e0b', marginLeft: 6 }}>projected</span>}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right', color: '#e2e8f0' }}>{fmtCur(r.value)}{r.parts ? <div style={{ fontSize: 9, color: '#475569' }}>{fmtCur(r.parts.base)} + {fmtCur(r.parts.bonus)} + {fmtCur(r.parts.rsu)}</div> : null}</td>
            <td style={{ padding: '3px 8px', textAlign: 'right', color: r.anchor ? '#64748b' : r.flagged ? '#f59e0b' : '#10b981' }}>{r.anchor ? 'anchor' : r.drift == null ? '—' : `${r.drift > 0 ? '+' : ''}${fmtCur(r.drift)} (${r.driftPct > 0 ? '+' : ''}${r.driftPct}%)`}</td>
            <td style={{ padding: '3px 8px', fontSize: 10, color: '#475569' }}>{r.source}{r.editedAt ? ` · ${fmtDate(r.editedAt)}` : ''}</td>
          </tr>))}</tbody>
      </table>
      <div style={{ marginTop: 4 }}><ExportCsv name="comp-reconciliation" rows={recon.rows.map(r => ({ source: r.label, value: r.value, drift: r.drift, driftPct: r.driftPct, flagged: !!r.flagged, provenance: r.source }))} /></div>
      {recon.recommended && recon.recommended.patch && recon.recommended.drift !== 0 && (
        <div style={{ marginTop: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap', fontSize: 11, background: 'rgba(245,158,11,0.06)', border: '1px solid rgba(245,158,11,0.25)', borderRadius: 8, padding: '8px 12px' }}>
          <span>Recommended engine input: <strong style={{ color: '#e2e8f0' }}>{fmtCur(recon.recommended.value)}</strong> ({recon.recommended.basis}) — {recon.recommended.drift > 0 ? '+' : ''}{fmtCur(recon.recommended.drift)} from what the engine holds.</span>
          <button className="btn-primary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={() => onPatch(recon.recommended.patch)}>Set salary to {fmtCur(recon.recommended.value)}</button>
        </div>
      )}
      {!compact && (
        <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 12px', fontSize: 11 }}>
          <div className="label" style={{ marginBottom: 4 }}>Next year's guidance (what the employer said — stored data)</div>
          {guidance && !g ? (
            <div style={{ color: '#94a3b8' }}>
              {guidance.year}: base {fmtCur(guidance.base)} + IC {fmtCur(guidance.icLow)}–{fmtCur(guidance.icHigh)} + RSU {fmtCur(guidance.rsu)} = <strong style={{ color: '#e2e8f0' }}>{fmtCur(guidance.tcLow)}–{fmtCur(guidance.tcHigh)}</strong>{guidance.recordedAt ? ` (recorded ${fmtDate(guidance.recordedAt)})` : ''}.
              {guidance.projection ? <div style={{ marginTop: 2 }}>Compensation projection for {guidance.year}: {fmtCur(guidance.projection.tc)} (IC {fmtCur(guidance.projection.ic)}) — <strong style={{ color: guidance.icLightBy > 0 ? '#f59e0b' : '#10b981' }}>{guidance.icLightBy > 0 ? `${fmtCur(guidance.icLightBy)} light on IC` : 'at or above guidance'}</strong>{guidance.tcLightBy > 0 ? `, ${fmtCur(guidance.tcLightBy)} light on TC` : ''}.</div> : <div style={{ marginTop: 2, color: '#64748b' }}>No {guidance.year} row in the compensation dataset to compare.</div>}
              <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px', marginTop: 4 }} onClick={() => setG({ ...guidance, note: guidance.note || '' })}>Edit</button>
            </div>
          ) : (
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
              <input type="number" placeholder="Year" value={draft.year} style={{ width: 70, fontSize: 11 }} onChange={e => setG({ ...draft, year: e.target.value })} />
              <input type="number" placeholder="Base" value={draft.base} style={{ width: 100, fontSize: 11 }} onChange={e => setG({ ...draft, base: e.target.value })} />
              <input type="number" placeholder="IC low" value={draft.icLow} style={{ width: 90, fontSize: 11 }} onChange={e => setG({ ...draft, icLow: e.target.value })} />
              <input type="number" placeholder="IC high" value={draft.icHigh} style={{ width: 90, fontSize: 11 }} onChange={e => setG({ ...draft, icHigh: e.target.value })} />
              <input type="number" placeholder="RSU" value={draft.rsu} style={{ width: 90, fontSize: 11 }} onChange={e => setG({ ...draft, rsu: e.target.value })} />
              <input placeholder="Note" value={draft.note || ''} style={{ width: 160, fontSize: 11 }} onChange={e => setG({ ...draft, note: e.target.value })} />
              <button className="btn-primary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={saveGuidance}>Save guidance</button>
              {g && <button className="btn-secondary" style={{ fontSize: 10, padding: '3px 8px' }} onClick={() => setG(null)}>Cancel</button>}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── 3a: the taxonomy editor ────────────────────────────────────────────────
function TaxonomyEditorCard({ rollup, taxonomy, onSave, transactions }) {
  const ST = window.SpendingTaxonomy;
  const [open, setOpen] = useState(false);
  if (!ST || !rollup) return null;
  const mapping = taxonomy?.mapping || {};
  const disp = ST.effectiveDispositions(taxonomy?.dispositions);
  const sign = ST.detectOutSign(transactions);
  const setMap = (raw, key) => onSave({ mapping: { ...mapping, [raw]: key } });
  const setDisp = (key, patch) => onSave({ dispositions: { ...(taxonomy?.dispositions || {}), [key]: { ...((taxonomy?.dispositions || {})[key] || {}), ...patch } } });
  const prot = ST.effectiveProtection(taxonomy?.protection);
  const setProt = (key, patch) => onSave({ protection: { ...(taxonomy?.protection || {}), [key]: { ...((taxonomy?.protection || {})[key] || {}), ...patch } } });
  const spendingCats = ST.CANONICAL.filter(c => c.cls === 'spending');
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <CardHead title="Spending taxonomy" sub={`${rollup.rawCategories.length} raw Monarch categories → ${ST.CANONICAL.length} planning categories; ${rollup.unmapped.length} unmapped. Source transactions are never changed.`} kind="data" source="spending/taxonomy"
        right={<button className="btn-secondary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={() => setOpen(v => !v)}>{open ? 'Close editor' : 'Edit mapping & dispositions'}</button>} />
      {rollup.protectedSpending && rollup.protectedSpending.categories.length > 0 && (
        <div style={{ fontSize: 11, color: '#10b981', marginTop: 4 }}>Protected (never proposed as a cut): {rollup.protectedSpending.categories.map(c => `${c.label} ${fmtCur(c.total)}/yr`).join(' · ')} — {fmtCur(rollup.protectedSpending.total)}/yr kept by design. The coach, the strategy and insight prompts all read this list.</div>
      )}
      <div style={{ fontSize: 10, color: sign.confidence === 'high' ? '#475569' : '#f59e0b' }}>Sign convention: {sign.note} ({sign.incomeRows} income rows){taxonomy?.outSign != null ? ` · stored override ${taxonomy.outSign}` : ''}. {sign.confidence !== 'high' && <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => onSave({ outSign: sign.outSign === -1 ? 1 : -1 })}>flip</a>}</div>
      {open && (
        <div className="grid-2" style={{ gap: 16, alignItems: 'start', marginTop: 10 }}>
          <div>
            <div className="label" style={{ marginBottom: 4 }}>Raw → canonical (last 12 months, by size)</div>
            <div style={{ maxHeight: 420, overflowY: 'auto' }}>
              {rollup.rawCategories.map(r => (
                <div key={r.raw} style={{ display: 'grid', gridTemplateColumns: '1fr 90px 150px', gap: 6, alignItems: 'center', fontSize: 11, padding: '3px 0', borderBottom: '1px solid #161b22', color: r.mapped ? '#94a3b8' : '#f59e0b' }}>
                  <span>{r.raw}</span><span style={{ textAlign: 'right' }}>{fmtCur(r.total)}</span>
                  <select value={mapping[r.raw] || mapping[r.raw.toLowerCase()] || r.canonical} onChange={e => setMap(r.raw, e.target.value)} style={{ fontSize: 10, padding: '2px 4px' }}>
                    {ST.CANONICAL.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
                  </select>
                </div>))}
            </div>
          </div>
          <div>
            <div className="label" style={{ marginBottom: 4 }}>Disposition and protection per planning category</div>
            <div style={{ fontSize: 9, color: '#475569', marginBottom: 4 }}>Protected = kept by design; never proposed as a cut by the coach or any generated plan. Owner and reason ride along so the advice can say why.</div>
            {spendingCats.map(c => {
              const d = disp[c.key]; const p = prot[c.key]; const total = (rollup.categories.find(x => x.key === c.key) || {}).total || 0;
              return (
                <div key={c.key} style={{ borderBottom: '1px solid #161b22', padding: '3px 0' }}>
                  <div style={{ display: 'grid', gridTemplateColumns: '1fr 80px 150px 80px 70px', gap: 6, alignItems: 'center', fontSize: 11, color: '#94a3b8' }}>
                    <span style={{ color: p.protected ? '#10b981' : undefined }}>{c.label}</span><span style={{ textAlign: 'right' }}>{fmtCur(total)}</span>
                    <select value={d.disposition} onChange={e => setDisp(c.key, { disposition: e.target.value })} style={{ fontSize: 10, padding: '2px 4px', color: ST.DISPOSITIONS[d.disposition].color }}>
                      {Object.values(ST.DISPOSITIONS).map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
                    </select>
                    {d.disposition === 'ENDS_ON_PAYOFF' ? <input type="number" placeholder="payoff yr" value={d.payoffYear || ''} onChange={e => setDisp(c.key, { payoffYear: e.target.value === '' ? null : Number(e.target.value) })} style={{ fontSize: 10, padding: '2px 4px' }} /> : <span />}
                    <label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 10, cursor: 'pointer' }}><input type="checkbox" checked={p.protected} onChange={e => setProt(c.key, { protected: e.target.checked })} style={{ width: 'auto' }} />protect</label>
                  </div>
                  {p.protected && (
                    <div style={{ display: 'grid', gridTemplateColumns: '110px 1fr', gap: 6, marginTop: 3, marginLeft: 8 }}>
                      <select value={p.owner || 'household'} onChange={e => setProt(c.key, { owner: e.target.value })} style={{ fontSize: 10, padding: '2px 4px' }}>
                        <option value="you">Chris</option><option value="spouse">Spouse</option><option value="household">Household</option>
                      </select>
                      <input placeholder="why it is kept" defaultValue={p.reason || ''} onBlur={e => { if (e.target.value !== (p.reason || '')) setProt(c.key, { reason: e.target.value }); }} style={{ fontSize: 10, padding: '2px 6px' }} />
                    </div>
                  )}
                </div>);
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// ── The tab ────────────────────────────────────────────────────────────────
function Insights({ setTab }) {
  const d = useContext(DataContext);
  const { show, Toast } = useToast();
  const currentYear = new Date().getFullYear();
  const cfg = d.retirementConfig || {};
  const IR = window.InputReconciliation, Sens = window.Sensitivity, SR = window.SavingsRate, TCF = window.TrueCashFlow, PC = window.PlanCompleteness, TB = window.TaxBuckets, IRules = window.InsightRules, ST = window.SpendingTaxonomy;
  // Same field → account map the Retirement tab uses (lib/retirementAccounts).
  const mapping = useMemo(() => { const RA = window.RetirementAccounts; return IR && RA ? IR.mappingFromRows(RA.liveBalances(d.accounts || [], cfg.accountMap).rows, RA.BUCKETS) : null; }, [IR, d.accounts, cfg.accountMap]);
  const recon = useMemo(() => IR ? IR.reconcile({ cfg, accounts: d.accounts, monarchAuth: d.monarchAuth, mapping }) : null, [IR, cfg, d.accounts, d.monarchAuth, mapping]);
  const manual = useMemo(() => IR ? IR.manualFieldStatus({ cfg, accounts: d.accounts, mapping, docUpdatedAt: tsIso(cfg.updatedAt) }) : null, [IR, cfg, d.accounts, mapping]);
  const zScores = useMemo(() => Sens ? ['expectedReturn', 'salaryGrowth', 'inflationRate'].map(k => Sens.zScore(k, cfg[k])).filter(Boolean) : [], [Sens, cfg]);
  const ctx = useMemo(() => buildInsightContext({ accounts: d.accounts, transactions: d.transactions, holdings: d.holdings, equityConfig: d.equityConfig, compensation: d.compensation, payslipsByYear: d.payslipsByYear, kidsPlan: d.kidsPlan, estatePlan: d.estatePlan, spendingTaxonomy: d.spendingTaxonomy, eventAssignments: d.eventAssignments, documents: d.documents, netWorthSnapshots: d.netWorthSnapshots, accountBalanceHistory: d.accountBalanceHistory, cfg, reconciliation: recon, zScores, currentYear }),
    [d.accounts, d.transactions, d.holdings, d.equityConfig, d.compensation, d.payslipsByYear, d.kidsPlan, d.estatePlan, d.spendingTaxonomy, d.eventAssignments, d.documents, d.netWorthSnapshots, d.accountBalanceHistory, cfg, recon, zScores, currentYear]);
  const pacing = ctx.pacing;
  // The 13-week forecast: recurring detection over the ledger, the calendar's
  // net events, stored lumps, and the paycheck shape from contribution pacing
  // when it is available.
  const forecast = useMemo(() => {
    const CF = window.CashForecast, CP = window.ContributionPacing;
    if (!CF || !d.transactions.length) return null;
    const pacing = CP ? CP.paycheckSchedule({ cfg, compensation: d.compensation, payslipsByYear: d.payslipsByYear, year: currentYear, weeks: 13 }) : null;
    return CF.forecast({ transactions: d.transactions, accounts: d.accounts, outSign: ctx.outSign, mapping: d.spendingTaxonomy?.mapping, weeks: 13,
      calendarEvents: ctx.calendar ? ctx.calendar.events : [], lumps: d.cashLumps || [], monthlyTrueSpend: ctx.rollup ? ctx.rollup.spending.monthly : 0,
      paychecks: pacing && pacing.paychecks && pacing.paychecks.length ? pacing.paychecks : null });
  }, [d.transactions, d.accounts, d.spendingTaxonomy, d.cashLumps, d.compensation, d.payslipsByYear, cfg, ctx, currentYear]);
  const insights = useMemo(() => IRules ? IRules.evaluate(ctx, { dismissed: d.dismissedInsights || {} }) : null, [IRules, ctx, d.dismissedInsights]);
  const savings = useMemo(() => SR ? SR.monthlySeries({ cfg, compensation: d.compensation, payslipsByYear: d.payslipsByYear, spousePayslipsByYear: d.spousePayslipsByYear, kidsPlan: d.kidsPlan, months: 24 }) : null, [SR, cfg, d.compensation, d.payslipsByYear, d.spousePayslipsByYear, d.kidsPlan]);
  const cashflow = useMemo(() => TCF && d.transactions.length ? TCF.series({ transactions: d.transactions, accounts: d.accounts, months: 12, mapping: d.spendingTaxonomy?.mapping, outSign: ctx.outSign }) : null, [TCF, d.transactions, d.accounts, d.spendingTaxonomy, ctx.outSign]);
  const k401 = useMemo(() => TB ? TB.k401Split(cfg) : null, [TB, cfg]);
  const audit = useMemo(() => PC ? PC.audit({ taxConfig: d.taxConfig, budgets: d.budgets, netWorthHistory: d.netWorthHistory, netWorthSnapshots: d.netWorthSnapshots, kidsPlan: d.kidsPlan, estatePlan: d.estatePlan, retirementConfig: cfg, k401Split: k401, reconciliation: recon, manualStale: manual ? manual.stale : [], decisionCount: (d.decisions || []).length, documentCadence: ctx.documentCadence }) : null,
    [PC, d.taxConfig, d.budgets, d.netWorthHistory, d.netWorthSnapshots, d.kidsPlan, d.estatePlan, cfg, k401, recon, manual, d.decisions, ctx.documentCadence]);
  const frh = d.fundedRatioHistory;
  const latestRatio = useMemo(() => { const m = Object.values((frh && frh.months) || {}).sort((a, b) => a.month.localeCompare(b.month)); return m.length ? m[m.length - 1] : null; }, [frh]);
  const payrollMonthly = useMemo(() => TB ? Math.round(TB.contributionsToday(cfg).total / 12) : 0, [TB, cfg]);
  const monthly = useMemo(() => ST && d.transactions.length ? ST.monthly(d.transactions, { months: 24, mapping: d.spendingTaxonomy?.mapping, outSign: ctx.outSign }) : [], [ST, d.transactions, d.spendingTaxonomy, ctx.outSign]);
  const lastSync = d.monarchAuth && d.monarchAuth.lastSync;
  return (
    <div className="page">
      {Toast}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
        <div className="page-title" style={{ marginBottom: 0 }}>Insights</div>
        <div style={{ fontSize: 10, color: '#475569' }}>Every number here is only as fresh as the last Monarch sync: {lastSync ? timeAgo(lastSync) : 'never'}. ENGINE = an engine's own output · ESTIMATE = a heuristic rollup · DATA = stored as-is.</div>
      </div>
      <InsightList result={insights} onDismiss={d.dismissInsight} setTab={setTab} />
      <CompletenessCard audit={audit} setTab={setTab} />
      <FundedRatioHistoryCard history={frh} />
      <NetWorthAttributionCard store={d.netWorthSnapshots} transactions={d.transactions} accounts={d.accounts} rollupMonthly={monthly} payrollMonthly={payrollMonthly} />
      <TrueCashFlowCard series={cashflow} runway={ctx.runway} setTab={setTab} />
      <CashForecastCard forecast={forecast} lumps={d.cashLumps} onSaveLumps={(items) => d.saveCashLumps(items).then(() => show('Lumps saved')).catch(e => show(e.message, 'error'))} setTab={setTab} />
      <TransferIntegrityCard t={ctx.transferIntegrity} />
      <CompReconciliationCard recon={ctx.compReconciliation} cfg={cfg} compact onPatch={(patch) => d.saveRetirementConfig(patch).then(() => show('Retirement config updated')).catch(e => show(e.message, 'error'))} />
      <SavingsRateCard series={savings} />
      <ConcentrationCard c={ctx.concentration} coverage={ctx.positionCoverage} unvestedInHoldings={d.equityConfig && d.equityConfig.unvestedInHoldings}
        onSetUnvestedInHoldings={(v) => d.saveEquityConfig({ unvestedInHoldings: v }).then(() => show(v ? 'Unvested shares counted once' : 'Employer holding counted as held')).catch(e => show(e.message, 'error'))}
        equity={d.equityConfig} onSaveEquity={(patch) => d.saveEquityConfig(patch).then(() => show('Employer holding updated')).catch(e => show(e.message, 'error'))} />
      <VestBonusCard cal={ctx.calendar} assignments={d.eventAssignments} onSave={(m) => d.saveEventAssignments(m).then(() => show('Assignments saved')).catch(e => show(e.message, 'error'))} />
      <ContributionPacingCard pacing={pacing} cfg={cfg} onPatch={(patch) => d.saveRetirementConfig(patch).then(() => show('Election saved')).catch(e => show(e.message, 'error'))} />
      <DecisionLogCard decisions={d.decisions || []} cfg={cfg} fundedRatio={latestRatio ? latestRatio.ratio : null} preset={latestRatio ? latestRatio.preset : (cfg.headlinePreset || 'base')}
        onSave={(doc) => d.saveDecision(doc).then(() => show('Decision recorded')).catch(e => show(e.message, 'error'))} onDelete={(id) => d.deleteDecision(id).catch(e => show(e.message, 'error'))} />
      <TaxonomyEditorCard rollup={ctx.rollup} taxonomy={d.spendingTaxonomy} transactions={d.transactions} onSave={(patch) => d.saveSpendingTaxonomy(patch).then(() => show('Taxonomy saved')).catch(e => show(e.message, 'error'))} />
    </div>
  );
}

window.FinanceProducts = { buildInsightContext, coverageCliffFor, InsightList, CompletenessCard, FundedRatioHistoryCard, NetWorthAttributionCard, SavingsRateCard, TrueCashFlowCard, ConcentrationCard, PositionCoverage, VestBonusCard, MarginalRateCard, DecisionLogCard, TaxonomyEditorCard,
  TransferIntegrityCard, CashForecastCard, CompReconciliationCard, ContributionPacingCard, TaxConfigBuilderCard, AssetLocationCard, DocumentReconcileCard, StatementHistoryCard,
  TimelineStrip, useHouseholdTimeline, HumanCapitalCard, EducationCurveCard, CoverageCliffCard, EstateGapsCard, DashboardHero, TabAnswer };
window.FinanceViews = Object.assign(window.FinanceViews || {}, { Insights });
