// financial/src/retirementScreens.jsx — the Retirement tab's screens.
//
// The planner used to be a flat wall of ~50 inputs with the answer somewhere
// in the middle. It is now four screens plus the deep-dive: ANSWER (the
// funded ratio, one sentence, the three assumptions it hangs on, one fix),
// DRIVERS (sensitivity, presets, tax buckets, the spending bridge, levers),
// TIMELINE (one chart 2026→2060 with the life events on it, and the
// year-by-year ledger every number came from) and INPUTS (the full form,
// grouped, with anything at its default collapsed). Everything here is a
// presentational component fed by retirement.jsx, which owns the engine
// runs; nothing here computes a planning number.
//
// Loaded BEFORE retirement.jsx (document order) and published on
// window.RetirementScreens. Slices run in their own Babel scope, so the
// shared helpers come from window.FinanceShared and the libs from window.

const { useState, useMemo } = React;
const { ResponsiveContainer, ComposedChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
        ReferenceLine, ReferenceArea, BarChart, Bar, Cell, LabelList } = window.Recharts;
const { fmtCur, fmtDate, timeAgo } = window.FinanceShared;

const SCREENS = [
  { key: 'answer', label: 'Answer' },
  { key: 'drivers', label: 'Drivers' },
  { key: 'timeline', label: 'Timeline' },
  { key: 'inputs', label: 'Inputs' },
  { key: 'analysis', label: 'Deep dive' },
];

const TIP = { background: '#0f172a', border: '1px solid #334155', borderRadius: 8, fontSize: 11, color: '#e2e8f0' };
const iso = (v) => { try { return v && v.toDate ? v.toDate().toISOString() : v ? new Date(v).toISOString() : null; } catch (e) { return null; } };
const daysAgo = (v) => { const s = iso(v); if (!s) return null; return Math.floor((Date.now() - new Date(s).getTime()) / 86400000); };

// A small "where did this number come from" tag every card carries.
function Source({ children, kind }) {
  const color = kind === 'engine' ? '#10b981' : kind === 'heuristic' ? '#f59e0b' : '#64748b';
  return <span style={{ fontSize: 9, color, border: `1px solid ${color}55`, borderRadius: 4, padding: '1px 5px', letterSpacing: '0.04em', whiteSpace: 'nowrap' }}>{kind === 'engine' ? 'ENGINE' : kind === 'heuristic' ? 'ESTIMATE' : 'DATA'} · {children}</span>;
}

function ScreenNav({ screen, setScreen, badges }) {
  return (
    <div style={{ display: 'flex', gap: 4, marginBottom: 16, borderBottom: '1px solid #1e2a3a', flexWrap: 'wrap' }}>
      {SCREENS.map(s => (
        <button key={s.key} onClick={() => setScreen(s.key)}
          style={{ background: 'transparent', border: 'none', borderBottom: `2px solid ${screen === s.key ? '#10b981' : 'transparent'}`, borderRadius: 0,
            color: screen === s.key ? '#10b981' : '#64748b', padding: '8px 12px', fontSize: 12, fontWeight: 600 }}>
          {s.label}{badges && badges[s.key] ? <span style={{ marginLeft: 6, fontSize: 9, background: 'rgba(245,158,11,0.15)', color: '#f59e0b', borderRadius: 8, padding: '1px 6px' }}>{badges[s.key]}</span> : null}
        </button>
      ))}
    </div>
  );
}

// ── WI1: the stale-inputs banner ───────────────────────────────────────────
function StaleInputsBanner({ recon, onSync, syncing }) {
  if (!recon || !recon.anyDrift) return null;
  const rows = recon.drifted;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: '#f59e0b', background: 'rgba(245,158,11,0.05)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
        <div>
          <div style={{ fontSize: 13, fontWeight: 700, color: '#f59e0b' }}>Inputs are stale — {rows.length} balance{rows.length === 1 ? '' : 's'} drifted more than {recon.thresholdPct}% from the synced accounts</div>
          <div style={{ fontSize: 10, color: '#94a3b8', marginTop: 2 }}>
            Accounts synced {recon.accountsSyncedAt ? timeAgo(recon.accountsSyncedAt) : 'never'} · inputs last synced {recon.lastSyncedAt ? timeAgo(recon.lastSyncedAt) : 'never'} · total drift {fmtCur(recon.totalDriftAbs)}. Nothing is overwritten until you say so.
          </div>
        </div>
        <button className="btn-primary" disabled={syncing} style={{ fontSize: 11, padding: '6px 12px' }} onClick={() => onSync(rows.map(r => r.key))}>Sync all {rows.length} from accounts</button>
      </div>
      <table style={{ fontSize: 11 }}>
        <thead><tr><th>Input</th><th style={{ textAlign: 'right' }}>Entered</th><th style={{ textAlign: 'right' }}>Accounts</th><th style={{ textAlign: 'right' }}>Drift</th><th>Mapped accounts</th><th></th></tr></thead>
        <tbody>
          {rows.map(r => (
            <tr key={r.key}>
              <td style={{ padding: '5px 8px', color: '#e2e8f0' }}>{r.label}<div style={{ fontSize: 9, color: '#475569' }}>{r.lastSyncedAt ? `synced ${fmtDate(r.lastSyncedAt)}` : r.lastEditedAt ? `edited ${fmtDate(r.lastEditedAt)}` : 'never synced'}</div></td>
              <td style={{ padding: '5px 8px', textAlign: 'right' }}>{fmtCur(r.configValue)}</td>
              <td style={{ padding: '5px 8px', textAlign: 'right', color: '#e2e8f0', fontWeight: 600 }}>{fmtCur(r.accountValue)}</td>
              <td style={{ padding: '5px 8px', textAlign: 'right', color: r.driftAbs > 0 ? '#10b981' : '#ef4444' }}>{r.driftAbs > 0 ? '+' : ''}{fmtCur(r.driftAbs)} ({r.driftPct > 0 ? '+' : ''}{r.driftPct}%)</td>
              <td style={{ padding: '5px 8px', fontSize: 10, color: '#64748b' }}>{r.accountNames.join(', ')}</td>
              <td style={{ padding: '5px 8px', textAlign: 'right' }}><button className="btn-secondary" disabled={syncing} style={{ fontSize: 10, padding: '3px 8px' }} onClick={() => onSync([r.key])}>Sync</button></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// ── WI2: the Tax Buckets panel ─────────────────────────────────────────────
function TaxBucketsPanel({ summary, clocks, contributions, k401, onSetSplit, onConfirm, taxCfgGaps, onGotoTax }) {
  const TB = window.TaxBuckets;
  const [draft, setDraft] = useState(null);
  if (!summary || !TB) return null;
  const colors = TB.BUCKET_COLORS;
  const data = summary.points.map(p => ({ name: p.label, pretax: p.pretax + (p.pension || 0), roth: p.roth, taxable: p.taxable, total: p.total, pretaxShare: p.pretaxShare, from: p.from }));
  const late = summary.points.find(p => p.key === 'late');
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(139,92,246,0.35)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 6 }}>
        <div>
          <div className="label" style={{ marginBottom: 2 }}>Tax Buckets — pre-tax / Roth / taxable</div>
          <div style={{ fontSize: 11, color: '#64748b' }}>The dashboard for conversion decisions. Today from the inputs; {summary.points.slice(1).map(p => p.label).join(' and ')} from the projection.</div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontSize: 26, fontWeight: 800, color: '#8b5cf6', lineHeight: 1 }}>{summary.headlinePretaxShareAtLate == null ? '—' : `${summary.headlinePretaxShareAtLate}%`}</div>
          <div style={{ fontSize: 9, color: '#64748b', letterSpacing: '0.08em', textTransform: 'uppercase' }}>pre-tax share at {summary.lateAge}</div>
          <div style={{ marginTop: 3 }}><Source kind={late && late.from === 'engine' ? 'engine' : 'heuristic'}>{late ? late.from : 'inputs'}</Source></div>
        </div>
      </div>
      {k401 && (k401.needsReview || k401.needsMigration || k401.needsConfirmation) && (
        <div style={{ background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.35)', borderRadius: 8, padding: '8px 12px', fontSize: 11, marginBottom: 10, lineHeight: 1.5, display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          <span>{k401.needsReview
            ? <><strong style={{ color: '#f59e0b' }}>Confirm the 401(k) split.</strong> The migration defaulted {fmtCur(k401.balance)} to <strong>100% pre-tax</strong>. If part of it is Roth 401(k), the conversion schedule and the RMD projection are wrong by that amount until you correct it — enter the statement's money sources below, or type the split here.</>
            : <><strong style={{ color: '#f59e0b' }}>Split not yet confirmed.</strong> {fmtCur(k401.pretax)} pre-tax / {fmtCur(k401.roth)} Roth, basis <em>{k401.basis}</em>{k401.seeded ? ' (seeded statement rows)' : ''}. In use by the engine; confirm once you have checked it against the statement.</>}</span>
          {onConfirm && k401.basis !== 'none' && <button className="btn-primary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={onConfirm}>{k401.needsReview ? 'Confirm 100% pre-tax' : 'Confirm'}</button>}
        </div>
      )}
      {k401 && k401.confirmed && k401.confirmedAt && <div style={{ fontSize: 9, color: '#475569', marginBottom: 6 }}>Split confirmed {fmtDate(k401.confirmedAt)} on the <em>{k401.basis}</em> basis.</div>}
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <div>
          <ResponsiveContainer width="100%" height={200}>
            <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
              <XAxis dataKey="name" tick={{ fontSize: 10, fill: '#94a3b8' }} 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="pretax" name="Pre-tax (incl. UK pension)" stackId="a" fill={colors.pretax} />
              <Bar dataKey="roth" name="Roth" stackId="a" fill={colors.roth} />
              <Bar dataKey="taxable" name="Taxable" stackId="a" fill={colors.taxable} />
            </BarChart>
          </ResponsiveContainer>
          <div style={{ display: 'flex', gap: 14, fontSize: 10, color: '#64748b', flexWrap: 'wrap', marginTop: 4 }}>
            {data.map(d => <span key={d.name}>{d.name}: pre-tax <strong style={{ color: colors.pretax }}>{d.pretaxShare == null ? '—' : `${d.pretaxShare}%`}</strong> of {fmtCur(d.total)}</span>)}
          </div>
        </div>
        <div style={{ fontSize: 11 }}>
          <div className="label" style={{ marginBottom: 6 }}>Today, by line</div>
          {summary.today.lines.map(l => (
            <div key={l.key} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '3px 0', borderBottom: '1px solid #161b22', color: l.outOfScope ? '#475569' : '#94a3b8' }}>
              <span title={l.source}><span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: colors[l.bucket], marginRight: 6 }} />{l.label}</span>
              <span style={{ color: l.outOfScope ? '#475569' : '#e2e8f0' }}>{fmtCur(l.amount)}</span>
            </div>
          ))}
          <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>Hover a line for its source field. 529s are out of scope for the glidepath and excluded from every total above.</div>
          {contributions && (
            <div style={{ marginTop: 10 }}>
              <div className="label" style={{ marginBottom: 4 }}>This year's contributions by bucket</div>
              <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', fontSize: 10 }}>
                {TB.BUCKETS.filter(b => contributions.buckets[b]).map(b => <span key={b}><span style={{ color: colors[b] }}>{TB.BUCKET_LABELS[b]}</span> {fmtCur(contributions.buckets[b])}</span>)}
              </div>
              <div style={{ fontSize: 9, color: '#475569', marginTop: 2 }}>Employer match and core are always pre-tax; the deferral follows the Roth share; after-tax dollars are Roth only via the mega backdoor.</div>
            </div>
          )}
        </div>
      </div>
      {/* The split editor */}
      <div style={{ marginTop: 12, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', fontSize: 11 }}>
        <span className="label" style={{ marginBottom: 0 }}>401(k) split</span>
        <span style={{ color: '#94a3b8' }}>pre-tax <strong style={{ color: colors.pretax }}>{fmtCur(k401.pretax)}</strong> · Roth <strong style={{ color: colors.roth }}>{fmtCur(k401.roth)}</strong> · basis <em>{k401.basis}</em>{k401.updatedAt ? ` · ${fmtDate(k401.updatedAt)}` : ''}</span>
        {draft == null
          ? <button className="btn-secondary" style={{ fontSize: 10, padding: '3px 8px' }} onClick={() => setDraft({ pretax: k401.pretax, roth: k401.roth })}>Type the split</button>
          : <span style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              <input type="number" value={draft.pretax} style={{ width: 120 }} onChange={e => setDraft(d => ({ ...d, pretax: Number(e.target.value) || 0 }))} placeholder="pre-tax $" />
              <input type="number" value={draft.roth} style={{ width: 120 }} onChange={e => setDraft(d => ({ ...d, roth: Number(e.target.value) || 0 }))} placeholder="Roth $" />
              <button className="btn-primary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={() => { onSetSplit(draft); setDraft(null); }}>Save</button>
              <button className="btn-secondary" style={{ fontSize: 10, padding: '3px 8px' }} onClick={() => setDraft(null)}>Cancel</button>
            </span>}
      </div>
      {/* Roth 5-year clocks */}
      <div style={{ marginTop: 12 }}>
        <div className="label" style={{ marginBottom: 4 }}>Roth 5-year clocks (contribution clock)</div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))', gap: 8 }}>
          {clocks.map(c => (
            <div key={c.key} style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '8px 10px', fontSize: 11 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: '#e2e8f0' }}>{c.label}</span>
                <span style={{ color: c.status === 'satisfied' ? '#10b981' : c.status === 'running' ? '#f59e0b' : '#64748b' }}>{c.status}</span></div>
              <div style={{ fontSize: 10, color: '#64748b', marginTop: 2 }}>{c.firstYear ? `first contribution ${c.firstYear} → eligible ${c.eligibleDate}` : `set "${c.label} — first contribution year" on the Inputs screen`}</div>
              <div style={{ fontSize: 10, color: '#475569' }}>{c.note}</div>
            </div>
          ))}
        </div>
      </div>
      {taxCfgGaps && taxCfgGaps.empty && (
        <div style={{ marginTop: 12, background: 'rgba(59,130,246,0.08)', border: '1px solid rgba(59,130,246,0.3)', borderRadius: 8, padding: '8px 12px', fontSize: 11, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <span><strong style={{ color: '#3b82f6' }}>Tax config is empty.</strong> {taxCfgGaps.missing.length} assumptions the tax model needs are unset: {taxCfgGaps.missing.map(m => m.label).join(', ')}.</span>
          {onGotoTax && <button className="btn-secondary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={onGotoTax}>Set up on the Tax tab</button>}
        </div>
      )}
    </div>
  );
}

// ── WI3: the spending bridge (waterfall) ───────────────────────────────────
function SpendingBridgeCard({ bridge, rollup, basis, ratioNow, ratioOnLedger, ratioOnDerived, onConfirm, onGotoTaxonomy, confirmedAt, solveAt, engineBasisAnnual }) {
  const [showExcluded, setShowExcluded] = useState(false);
  // The drag: a retirement spend in today's dollars, re-solved by the
  // engine after a short pause so the slider stays fluid.
  const [drag, setDrag] = useState(null);
  const [dragRatio, setDragRatio] = useState(null);
  React.useEffect(() => {
    if (drag == null || !solveAt) return undefined;
    const t = setTimeout(() => { try { setDragRatio(solveAt(drag)); } catch (e) { setDragRatio(null); } }, 180);
    return () => clearTimeout(t);
  }, [drag, solveAt]);
  if (!rollup) return (
    <div className="card" style={{ marginBottom: 16 }}>
      <div className="label">Projected retirement spending — from the ledger</div>
      <div style={{ fontSize: 11, color: '#64748b' }}>No transactions synced yet, so the ledger cannot produce a spending figure. The funded ratio is on the {basis} basis.</div>
    </div>
  );
  const steps = bridge ? bridge.steps : [];
  // Running totals for the waterfall bars.
  let run = 0;
  const bars = steps.map(s => {
    if (s.type === 'start') { run = s.amount; return { name: s.label, base: 0, value: s.amount, fill: '#e2e8f0', s }; }
    if (s.type === 'end') return { name: s.label, base: 0, value: s.amount, fill: '#10b981', s };
    const before = run; run += s.amount;
    return { name: s.label, base: Math.min(before, run), value: Math.abs(s.amount), fill: s.amount < 0 ? '#f59e0b' : '#3b82f6', s };
  });
  const onLedger = basis === 'ledger';
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(16,185,129,0.35)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 6 }}>
        <div>
          <div className="label" style={{ marginBottom: 2 }}>Projected retirement spending — the bridge from the ledger</div>
          <div style={{ fontSize: 11, color: '#64748b' }}>Last 12 complete months ({rollup.window.from} → {rollup.window.to}), non-spending excluded, categories merged, each tagged with what happens to it at retirement. <Source kind="heuristic">transactions</Source></div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontSize: 22, fontWeight: 800, color: '#10b981', lineHeight: 1 }}>{bridge ? fmtCur(bridge.retirementAnnualToday) : '—'}<span style={{ fontSize: 11, color: '#64748b' }}>/yr</span></div>
          <div style={{ fontSize: 9, color: '#64748b', letterSpacing: '0.08em', textTransform: 'uppercase' }}>retirement spend, today's $ ({bridge ? fmtCur(bridge.retirementMonthlyToday) : '—'}/mo)</div>
        </div>
      </div>
      <div className="grid-2" style={{ gap: 16, alignItems: 'start' }}>
        <div>
          <ResponsiveContainer width="100%" height={Math.max(180, bars.length * 26)}>
            <BarChart data={bars} layout="vertical" margin={{ top: 0, right: 60, left: 0, bottom: 0 }}>
              <XAxis type="number" tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${Math.round(v / 1000)}k`} />
              <YAxis type="category" dataKey="name" width={190} tick={{ fontSize: 9, fill: '#94a3b8' }} axisLine={false} tickLine={false} />
              <Tooltip contentStyle={TIP} formatter={(v, n, p) => [fmtCur(p.payload.s.amount), p.payload.s.source || p.payload.s.disposition || '']} />
              <Bar dataKey="base" stackId="w" fill="transparent" />
              <Bar dataKey="value" stackId="w">
                {bars.map((b, i) => <Cell key={i} fill={b.fill} />)}
                <LabelList dataKey="value" position="right" formatter={v => fmtCur(v)} style={{ fontSize: 9, fill: '#94a3b8' }} />
              </Bar>
            </BarChart>
          </ResponsiveContainer>
          {bridge && bridge.protected && bridge.protected.length > 0 && <div style={{ fontSize: 10, color: '#10b981' }}>Protected, kept by design (never proposed as a cut): {bridge.protected.map(p => `${p.label} ${fmtCur(p.amount)}/yr`).join(' · ')}.</div>}
          {bridge && bridge.stillRunning.length > 0 && <div style={{ fontSize: 10, color: '#f59e0b' }}>Still running into retirement: {bridge.stillRunning.map(s => `${s.label} until ${s.payoffYear}`).join(', ')} — kept in the spend.</div>}
          {bridge && bridge.steps.some(s => s.assumed) && <div style={{ fontSize: 10, color: '#64748b' }}>A payoff year not set on the taxonomy is assumed to fall before retirement; set it on the Insights → Spending taxonomy card.</div>}
        </div>
        <div style={{ fontSize: 11 }}>
          <div style={{ background: 'rgba(16,185,129,0.06)', border: '1px solid rgba(16,185,129,0.2)', borderRadius: 8, padding: '10px 12px', lineHeight: 1.6 }}>
            <div><strong style={{ color: '#e2e8f0' }}>True outflow:</strong> {fmtCur(rollup.spending.total)}/yr ({fmtCur(rollup.spending.monthly)}/mo) of {fmtCur(rollup.rawMoneyOut)} money out.</div>
            <div style={{ color: '#94a3b8' }}>{rollup.excluded.explanation} <button className="btn-secondary" style={{ fontSize: 9, padding: '1px 6px', marginLeft: 4 }} onClick={() => setShowExcluded(v => !v)}>{showExcluded ? 'hide' : 'show'}</button></div>
            {showExcluded && <ul style={{ margin: '4px 0 0 16px', color: '#64748b', fontSize: 10 }}>{rollup.excluded.byClass.map(c => <li key={c.key}>{c.label}: {fmtCur(c.total)}</li>)}</ul>}
            {rollup.unmapped.length > 0 && <div style={{ color: '#f59e0b', fontSize: 10, marginTop: 4 }}>{rollup.unmapped.length} raw categor{rollup.unmapped.length === 1 ? 'y' : 'ies'} unmapped ({fmtCur(rollup.unmapped.reduce((s, u) => s + u.total, 0))}, counted as "other"). {onGotoTaxonomy && <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={onGotoTaxonomy}>Map them →</a>}</div>}
          </div>
          {bridge && solveAt && (() => {
            const derived = bridge.retirementAnnualToday;
            const lo = Math.round(derived * 0.6 / 1000) * 1000, hi = Math.round(derived * 1.5 / 1000) * 1000;
            const v = drag == null ? derived : drag;
            return (
              <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' }}>
                <div className="label" style={{ marginBottom: 4 }}>Drag the number — the ratio follows</div>
                <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
                  <input type="range" min={lo} max={hi} step={1000} value={v} style={{ flex: 1, minWidth: 160 }} onChange={e => setDrag(Number(e.target.value))} />
                  <span style={{ minWidth: 200 }}><strong style={{ color: '#e2e8f0' }}>{fmtCur(v)}</strong>/yr today's $ → <strong style={{ color: dragRatio == null ? '#64748b' : dragRatio >= 1 ? '#10b981' : '#ef4444' }}>{drag == null ? (ratioOnLedger != null ? `${ratioOnLedger.toFixed(2)}×` : ratioNow != null ? `${ratioNow.toFixed(2)}×` : '—') : dragRatio == null ? 'solving…' : `${dragRatio.toFixed(2)}×`}</strong></span>
                  {drag != null && <button className="btn-secondary" style={{ fontSize: 9, padding: '1px 6px' }} onClick={() => { setDrag(null); setDragRatio(null); }}>reset</button>}
                </div>
                <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>Derived from the ledger: {fmtCur(derived)}{engineBasisAnnual != null ? ` · the engine's current basis: ${fmtCur(engineBasisAnnual)}` : ''}. Each position is the engine's own solve on the ledger basis; nothing is saved by dragging. <Source kind="engine">retirementNeeds.solveFundedRatio</Source></div>
              </div>
            );
          })()}
          <div style={{ marginTop: 10, border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' }}>
            <div className="label" style={{ marginBottom: 4 }}>Feed it into the funded ratio</div>
            {onLedger
              ? <div style={{ color: '#10b981' }}>In use — the ratio is on the <strong>ledger</strong> basis{confirmedAt ? ` (confirmed ${fmtDate(confirmedAt)})` : ''}: {ratioNow == null ? '—' : `${ratioNow.toFixed(2)}×`}{ratioOnDerived != null && <span style={{ color: '#64748b' }}> · on the derived basis it would read {ratioOnDerived.toFixed(2)}×</span>}</div>
              : <div>
                  <div style={{ color: '#94a3b8' }}>Ratio now ({basis} basis): <strong style={{ color: '#e2e8f0' }}>{ratioNow == null ? '—' : `${ratioNow.toFixed(2)}×`}</strong>
                    {ratioOnLedger != null && <> → on the ledger's figure: <strong style={{ color: ratioOnLedger >= (ratioNow || 0) ? '#10b981' : '#ef4444' }}>{ratioOnLedger.toFixed(2)}×</strong> ({ratioOnLedger - (ratioNow || 0) >= 0 ? '+' : ''}{(ratioOnLedger - (ratioNow || 0)).toFixed(2)})</>}
                  </div>
                  <button className="btn-primary" style={{ fontSize: 10, padding: '4px 10px', marginTop: 6 }} onClick={onConfirm}>Use the ledger's spending as the plan's basis</button>
                  <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>A suggestion you confirm — nothing changes until you click. The engine then takes the continuing spend from the ledger and still adds the age-ranged health and travel lines itself.</div>
                </div>}
          </div>
        </div>
      </div>
    </div>
  );
}

// ── 5a: presets + tornado ──────────────────────────────────────────────────
function TornadoCard({ presets, tornado, headlinePreset, onPreset, zScores }) {
  const rows = tornado ? tornado.rows : [];
  const maxSwing = rows.length ? Math.max(...rows.map(r => Math.max(Math.abs(r.low.delta || 0), Math.abs(r.high.delta || 0)))) || 0.01 : 0.01;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: 'rgba(59,130,246,0.35)' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
        <div>
          <div className="label" style={{ marginBottom: 2 }}>Sensitivity — what the funded ratio actually hangs on</div>
          <div style={{ fontSize: 11, color: '#64748b' }}>Each bar re-runs the full engine with ONE input moved across a plausible range. <Source kind="engine">retirementNeeds.solveFundedRatio</Source></div>
        </div>
      </div>
      {presets && (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(180px,1fr))', gap: 8, marginBottom: 12 }}>
          {presets.map(p => (
            <button key={p.key} onClick={() => onPreset(p.key)}
              style={{ textAlign: 'left', background: headlinePreset === p.key ? 'rgba(16,185,129,0.1)' : 'transparent', border: `1px solid ${headlinePreset === p.key ? '#10b981' : '#1e2a3a'}`, borderRadius: 8, padding: '8px 10px' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11 }}><span style={{ color: '#e2e8f0', fontWeight: 600 }}>{p.label}{p.key === 'current' && p.optimistic ? <span style={{ color: '#f59e0b', fontSize: 9, marginLeft: 6 }}>OPTIMISTIC</span> : null}{headlinePreset === p.key ? <span style={{ color: '#10b981', fontSize: 9, marginLeft: 6 }}>HEADLINE</span> : null}</span>
                <span style={{ fontWeight: 700, color: p.ratio == null ? '#64748b' : p.ratio >= 1 ? '#10b981' : '#ef4444' }}>{p.ratio == null ? '—' : `${p.ratio.toFixed(2)}×`}</span></div>
              <div style={{ fontSize: 10, color: '#64748b' }}>{p.expectedReturn}% return · {p.salaryGrowth}% salary growth</div>
              <div style={{ fontSize: 9, color: '#475569' }}>{p.note}</div>
            </button>
          ))}
        </div>
      )}
      {zScores && zScores.filter(z => z.beyond2sd).length > 0 && (
        <div style={{ fontSize: 10, color: '#f59e0b', marginBottom: 8 }}>
          {zScores.filter(z => z.beyond2sd).map(z => `${z.key} ${z.value}% is ${Math.abs(z.z)} sd from long-run (${z.mean}% ± ${z.sd}%, ${z.basis})`).join(' · ')}
        </div>
      )}
      {rows.length === 0 ? <div style={{ fontSize: 11, color: '#64748b' }}>Running…</div> : (
        <div>
          {rows.map(r => {
            const lo = r.low.delta || 0, hi = r.high.delta || 0;
            const left = Math.min(lo, hi, 0), right = Math.max(lo, hi, 0);
            const pct = v => 50 + (v / maxSwing) * 48;
            return (
              <div key={r.key} style={{ display: 'grid', gridTemplateColumns: '210px 1fr 90px', gap: 8, alignItems: 'center', padding: '4px 0', borderBottom: '1px solid #161b22', fontSize: 11 }}>
                <div><div style={{ color: '#e2e8f0' }}>{r.label}</div><div style={{ fontSize: 9, color: '#475569' }}>{r.low.value}{r.unit === '%' ? '%' : ''} → {r.high.value}{r.unit === '%' ? '%' : ''} (now {r.baseValue}{r.unit === '%' ? '%' : ''})</div></div>
                <div style={{ position: 'relative', height: 14, background: '#0b1220', borderRadius: 3 }}>
                  <div style={{ position: 'absolute', left: '50%', top: 0, bottom: 0, width: 1, background: '#334155' }} />
                  <div style={{ position: 'absolute', left: `${pct(left)}%`, width: `${pct(right) - pct(left)}%`, top: 2, bottom: 2, background: 'linear-gradient(90deg,#ef4444,#10b981)', borderRadius: 3, opacity: 0.85 }} />
                </div>
                <div style={{ textAlign: 'right', fontSize: 10 }}><span style={{ color: '#ef4444' }}>{Math.min(lo, hi).toFixed(2)}</span> / <span style={{ color: '#10b981' }}>+{Math.max(lo, hi).toFixed(2)}</span></div>
              </div>
            );
          })}
          <div style={{ fontSize: 9, color: '#475569', marginTop: 6 }}>Base {tornado.base.toFixed(2)}× on the headline preset · {tornado.runs} engine runs · widest bar first.</div>
        </div>
      )}
    </div>
  );
}

// ── The Answer screen ──────────────────────────────────────────────────────
function AnswerCard({ funded, gradeInfo, modelCfg, needAtRetirement, needToday, headlinePreset, presets, drivers, fix, planRun, onScreen, provisional }) {
  const ratio = funded.ratio;
  const cur = presets ? presets.find(p => p.key === 'current') : null;
  const sentence = ratio == null ? 'No spending plan has been entered yet, so there is nothing to grade.'
    : funded.capped ? `On the ${headlinePreset} assumptions the plan is funded far beyond its spending to age ${modelCfg.planToAge || 95}.`
    : ratio >= 1 ? `On the ${headlinePreset} assumptions (${modelCfg.expectedReturn}% return, ${modelCfg.salaryGrowth}% salary growth) the money covers ${Math.round(ratio * 100)}% of projected spending to age ${modelCfg.planToAge || 95} — ${Math.round((ratio - 1) * 100)}% of headroom.`
    : `On the ${headlinePreset} assumptions (${modelCfg.expectedReturn}% return, ${modelCfg.salaryGrowth}% salary growth) the money covers ${Math.round(ratio * 100)}% of projected spending${planRun && planRun.depletionAge ? `, running out at ${planRun.depletionAge}` : ''}.`;
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: gradeInfo.color }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap' }}>
        <div style={{ textAlign: 'center', minWidth: 90 }}>
          <div style={{ fontSize: 44, fontWeight: 900, color: gradeInfo.color, lineHeight: 1 }}>{ratio == null ? '—' : funded.capped ? '5×+' : `${ratio.toFixed(2)}×`}</div>
          <div style={{ fontSize: 9, color: '#64748b', letterSpacing: '0.1em', textTransform: 'uppercase', marginTop: 4 }}>Funded ratio · {gradeInfo.grade}</div>
          <div style={{ marginTop: 4 }}><Source kind="engine">{headlinePreset} preset</Source></div>
          {provisional && <div title={provisional} style={{ marginTop: 4, fontSize: 9, color: '#ef4444', border: '1px solid rgba(239,68,68,0.5)', borderRadius: 4, padding: '1px 5px', letterSpacing: '0.06em' }}>PROVISIONAL · input unconfirmed</div>}
        </div>
        <div style={{ flex: 1, minWidth: 240 }}>
          <div style={{ fontSize: 13, color: '#e2e8f0', lineHeight: 1.5, marginBottom: 6 }}>{sentence}</div>
          <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', fontSize: 11, color: '#64748b' }}>
            <span>Spending at {modelCfg.retireAge}: <strong style={{ color: '#e2e8f0' }}>{fmtCur(needAtRetirement)}/yr</strong> ({fmtCur(needToday)} today)</span>
            {funded.supported && <span>supports {fmtCur(funded.supported)}/yr</span>}
            {cur && cur.ratio != null && headlinePreset !== 'current' && <span>on the stored {cur.expectedReturn}%/{cur.salaryGrowth}% assumptions: <strong style={{ color: '#f59e0b' }}>{cur.ratio.toFixed(2)}×</strong> (optimistic)</span>}
          </div>
        </div>
      </div>
      <div className="grid-2" style={{ gap: 12, marginTop: 14, 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>
          {drivers && drivers.length ? drivers.map((d, i) => (
            <div key={d.key} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, padding: '3px 0', borderBottom: i < drivers.length - 1 ? '1px solid #161b22' : 'none' }}>
              <span style={{ color: '#e2e8f0' }}>{i + 1}. {d.label} <span style={{ color: '#475569' }}>({d.low.value}–{d.high.value}{d.unit === '%' ? '%' : ''})</span></span>
              <span><span style={{ color: '#ef4444' }}>{Math.min(d.low.delta || 0, d.high.delta || 0).toFixed(2)}</span> / <span style={{ color: '#10b981' }}>+{Math.max(d.low.delta || 0, d.high.delta || 0).toFixed(2)}</span></span>
            </div>
          )) : <div style={{ fontSize: 11, color: '#64748b' }}>Computing…</div>}
          <a style={{ fontSize: 10, color: '#3b82f6', cursor: 'pointer' }} onClick={() => onScreen('drivers')}>Full ranking →</a>
        </div>
        <div style={{ border: '1px solid #1e2a3a', borderRadius: 8, padding: '10px 12px' }}>
          <div className="label" style={{ marginBottom: 6 }}>{ratio != null && ratio >= 1 ? 'Nothing needs fixing' : 'What would fix it'}</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>}
              <div style={{ color: '#10b981', marginTop: 4 }}>+{fix.delta.toFixed(2)} → {fix.reaches.toFixed(2)}×{fix.reaches >= 1 ? ' — funded' : ' — still short; combine with the next lever'}</div>
              <div style={{ fontSize: 9, color: '#475569', marginTop: 2 }}>{fix.kind === 'lever' ? 'a real move, priced by re-running the engine' : 'an assumption, not a decision — shown because no lever alone closes the gap'}</div>
            </div>
          ) : <div style={{ fontSize: 11, color: '#64748b' }}>{ratio != null && ratio >= 1 ? `The plan is funded on the ${headlinePreset} preset; the Drivers screen shows how much room each assumption has.` : 'Computing…'}</div>}
        </div>
      </div>
    </div>
  );
}

// ── The Timeline chart ─────────────────────────────────────────────────────
function TimelineChart({ timeline, calendar }) {
  if (!timeline) return null;
  const { rows, markers, bands, from, to } = timeline;
  const vests = calendar ? calendar.events.filter(e => e.kind === 'vest').slice(0, 12) : [];
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <div className="label" style={{ marginBottom: 2 }}>Timeline {from} → {to}</div>
      <div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Portfolio by tax bucket on the headline preset, with every date that changes what the money is doing. <Source kind="engine">accumulation + retirementNeeds.project</Source></div>
      <ResponsiveContainer width="100%" height={300}>
        <ComposedChart data={rows} margin={{ top: 20, right: 16, left: 0, bottom: 0 }}>
          <CartesianGrid stroke="#161b22" />
          <XAxis dataKey="year" type="number" domain={[from, to]} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickCount={10} />
          <YAxis tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} tickFormatter={v => `$${(v / 1e6).toFixed(1)}M`} />
          <Tooltip contentStyle={TIP} formatter={(v, n) => [fmtCur(v), n]} labelFormatter={y => { const r = rows.find(x => x.year === y); return `${y}${r ? ` · age ${r.age}` : ''}`; }} />
          <Legend wrapperStyle={{ fontSize: 10 }} />
          {bands.map(b => <ReferenceArea key={b.key} x1={b.from} x2={b.to} fill={b.color} stroke="none" label={{ value: b.label, position: 'insideTop', fontSize: 9, fill: '#94a3b8' }} />)}
          <Area type="monotone" dataKey="taxable" name="Taxable" stackId="1" stroke="#06b6d4" fill="#06b6d4" fillOpacity={0.35} />
          <Area type="monotone" dataKey="preTax" name="Pre-tax" stackId="1" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.35} />
          <Area type="monotone" dataKey="ukPen" name="UK pension" stackId="1" stroke="#6366f1" fill="#6366f1" fillOpacity={0.35} />
          <Area type="monotone" dataKey="roth" name="Roth" stackId="1" stroke="#10b981" fill="#10b981" fillOpacity={0.35} />
          {markers.map(m => <ReferenceLine key={m.key} x={m.year} stroke={m.color} strokeDasharray="4 3" label={{ value: m.label, position: 'top', fontSize: 9, fill: m.color }} />)}
        </ComposedChart>
      </ResponsiveContainer>
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 10, color: '#64748b', marginTop: 6 }}>
        {markers.map(m => <span key={m.key}><span style={{ color: m.color }}>▍</span>{m.year} {m.label}</span>)}
        {bands.map(b => <span key={b.key}>▒ {b.from}–{b.to} {b.label}</span>)}
      </div>
      {vests.length > 0 && <div style={{ fontSize: 10, color: '#64748b', marginTop: 4 }}>Vests ahead: {vests.map(v => `${v.date.slice(0, 7)} ${v.shares} sh${v.net != null ? ` (${fmtCur(v.net)} net)` : ''}`).join(' · ')} — future cash, not current holdings.</div>}
    </div>
  );
}

// ── Every derived intermediate, exportable ─────────────────────────────────
// One CSV writer for every projection table (src/shared.jsx).
const downloadCsv = window.FinanceShared.downloadCsv;
function IntermediatesTable({ rows }) {
  const [open, setOpen] = useState(false);
  if (!rows || !rows.length) return null;
  const cols = [];
  rows.forEach(r => Object.keys(r).forEach(k => { if (!cols.includes(k)) cols.push(k); }));
  return (
    <div className="card" style={{ marginBottom: 16 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <div>
          <div className="label" style={{ marginBottom: 2 }}>Every derived intermediate, every year</div>
          <div style={{ fontSize: 11, color: '#64748b' }}>Income, taxes, withdrawals by bucket, conversions, RMDs, IRMAA and balances — the engine's own rows, nothing summarised. <Source kind="engine">retirementNeeds.project</Source></div>
        </div>
        <div style={{ display: 'flex', gap: 6 }}>
          <button className="btn-secondary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={() => setOpen(v => !v)}>{open ? 'Hide table' : `Show table (${rows.length} rows × ${cols.length} columns)`}</button>
          <button className="btn-primary" style={{ fontSize: 10, padding: '3px 10px' }} onClick={() => downloadCsv(rows, `retirement-projection-${new Date().toISOString().slice(0, 10)}.csv`)}>Export CSV</button>
        </div>
      </div>
      {open && (
        <div style={{ overflowX: 'auto', marginTop: 10 }}>
          <table style={{ fontSize: 10, whiteSpace: 'nowrap' }}>
            <thead><tr>{cols.map(c => <th key={c} style={{ padding: '3px 6px' }}>{c}</th>)}</tr></thead>
            <tbody>{rows.map((r, i) => <tr key={i}>{cols.map(c => <td key={c} style={{ padding: '2px 6px', textAlign: typeof r[c] === 'number' ? 'right' : 'left', color: '#94a3b8' }}>{typeof r[c] === 'number' ? (Math.abs(r[c]) >= 1000 ? fmtCur(r[c]) : r[c]) : (r[c] == null ? '' : String(r[c]))}</td>)}</tr>)}</tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── The grouped, progressively-disclosed inputs ────────────────────────────
const CHOICES = {
  spendBasis: [['derived', 'Derived (gross − tax − saving)'], ['observed', 'Monarch cashflow'], ['ledger', 'Ledger (taxonomy, confirmed)'], ['manual', 'Manual']],
  headlinePreset: [['base', 'Base 7% / 3%'], ['conservative', 'Conservative 5% / 2%'], ['current', 'Current (stored)']],
  balanceSource: [['live', 'Live from Monarch (typing a balance pins it)'], ['manual', 'Manual — the typed balances']],
};
function fieldProps(f) {
  switch (f.kind) {
    case 'rate': return { step: f.key === 'inflationRate' || f.key === 'dividendYieldPct' ? 0.1 : 0.5, suffix: '%' };
    case 'age': return { step: 1, min: 40, max: 105 };
    case 'balance': case 'amount': return { step: 1000, prefix: '$' };
    case 'contribution': return { step: 500, prefix: '$' };
    case 'year': return { step: 1, min: 1998, max: 2100 };
    default: return {};
  }
}
// A known value in the field's own units.
function fmtKnown(f, v) {
  if (f.kind === 'flag') return v ? 'yes' : 'no';
  if (f.kind === 'rate') return `${v}%`;
  if (f.kind === 'balance' || f.kind === 'amount' || f.kind === 'contribution') return fmtCur(v);
  return String(v);
}
function RegistryField({ f, cfg, setCfg, status, mapped, hint, known, onAdopt, live }) {
  const v = cfg[f.key];
  const set = (val) => setCfg(c => ({ ...c, [f.key]: val }));
  const meta = (cfg.fieldMeta || {})[f.key] || {};
  const IA = window.InputAutofill;
  const matches = known && IA ? IA.same(v, known.value) : false;
  const isLive = live && live.on && live.keys && live.keys[f.key] != null;
  const pinned = isLive && live.pinned && live.pinned[f.key];
  // A live balance IS the account (lib/retirementAccounts lays it over the
  // input on every render), so it reports the feed, not a sync that the
  // live mode never needs.
  const badge = isLive ? (pinned
      ? { text: `pinned · Monarch ${fmtCur(live.keys[f.key])}`, color: '#f59e0b', onClick: live.unpin ? () => live.unpin(f.key) : null, title: 'Typed value in use — click to follow Monarch again' }
      : { text: `live · Monarch${live.syncedAt ? ` ${fmtDate(live.syncedAt)}` : live.asOf ? ` ${fmtDate(live.asOf)}` : ''}`, color: '#10b981' })
    : meta.source === 'autofill' && matches ? { text: `auto · ${meta.from || known.from}`, color: '#10b981', title: known && known.asOf ? `as of ${known.asOf}` : undefined }
    : mapped ? { text: mapped.lastSyncedAt ? `synced ${fmtDate(mapped.lastSyncedAt)}` : 'mapped · never synced', color: '#3b82f6' }
    : status ? { text: status.lastEditedAt ? `edited ${status.ageDays}d ago${status.lastEditedBasis === 'document' ? ' (doc)' : ''}` : 'never recorded', color: status.stale && !status.atDefault ? '#f59e0b' : '#475569' } : null;
  // The known value beside a typed one that disagrees — one click to adopt.
  const offer = known && !matches && !isLive ? known : null;
  const p = fieldProps(f);
  let control;
  if (f.kind === 'flag') control = <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#94a3b8', cursor: 'pointer' }}><input type="checkbox" checked={!!v} onChange={e => set(e.target.checked)} style={{ width: 'auto' }} />{v ? 'yes' : 'no'}</label>;
  else if (f.kind === 'choice') control = <select value={v == null ? f.default : v} onChange={e => set(e.target.value)}>{(CHOICES[f.key] || []).map(([k, l]) => <option key={k} value={k}>{l}</option>)}</select>;
  else if (f.kind === 'date') control = <input type="date" value={v || ''} onChange={e => set(e.target.value || null)} />;
  else control = (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
      {p.prefix && <span style={{ fontSize: 11, color: '#64748b' }}>{p.prefix}</span>}
      <input type="number" value={v == null || v === '' ? '' : v} placeholder={f.default == null ? 'not set' : String(f.default)} step={p.step} min={p.min} max={p.max}
        onChange={e => set(e.target.value === '' ? null : Number(e.target.value))} style={{ flex: 1 }} />
      {p.suffix && <span style={{ fontSize: 11, color: '#64748b' }}>{p.suffix}</span>}
    </div>
  );
  return (
    <div style={{ marginBottom: 10 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3, gap: 6 }}>
        <span className="label" style={{ marginBottom: 0 }}>{f.label}</span>
        <span style={{ display: 'flex', gap: 4 }}>
          {hint && <span style={{ fontSize: 9, color: '#3b82f6', background: 'rgba(59,130,246,0.12)', padding: '1px 6px', borderRadius: 4 }}>{hint}</span>}
          {offer && <span title={`${offer.from}${offer.asOf ? ` · as of ${offer.asOf}` : ''} — click to use`} onClick={onAdopt ? () => onAdopt(f.key) : undefined}
            style={{ fontSize: 9, color: '#3b82f6', background: 'rgba(59,130,246,0.12)', padding: '1px 6px', borderRadius: 4, cursor: onAdopt ? 'pointer' : 'default' }}>
            {offer.suggestOnly ? 'likely' : 'known'}: {fmtKnown(f, offer.value)}{onAdopt ? ' · use' : ''}</span>}
          {badge && <span title={badge.title} onClick={badge.onClick || undefined} style={{ fontSize: 9, color: badge.color, cursor: badge.onClick ? 'pointer' : 'default' }}>{badge.text}</span>}
        </span>
      </div>
      {control}
    </div>
  );
}
function InputGroup({ group, fields, cfg, setCfg, statusByKey, reconByKey, hints, known, onAdopt, live, children }) {
  const [showDefaults, setShowDefaults] = useState(false);
  const isDefault = (f) => { const s = statusByKey[f.key]; return s ? s.atDefault : (cfg[f.key] == null ? f.default == null : cfg[f.key] === f.default); };
  // A field with a known value it does not hold stays open, so the offer is seen.
  const open = (f) => !isDefault(f) || reconByKey[f.key] || (known && known[f.key] && !(window.InputAutofill && window.InputAutofill.same(cfg[f.key], known[f.key].value)));
  const shown = fields.filter(open);
  const atDefault = fields.filter(f => !open(f));
  const stale = fields.filter(f => statusByKey[f.key] && statusByKey[f.key].stale && !statusByKey[f.key].atDefault).length;
  return (
    <div className="card" style={{ marginBottom: 12 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10, gap: 8, flexWrap: 'wrap' }}>
        <div className="label" style={{ marginBottom: 0 }}>{group.label}</div>
        <div style={{ fontSize: 10, color: '#64748b' }}>{stale > 0 && <span style={{ color: '#f59e0b', marginRight: 8 }}>{stale} untouched &gt; 12 months</span>}{atDefault.length > 0 && <a style={{ color: '#3b82f6', cursor: 'pointer' }} onClick={() => setShowDefaults(v => !v)}>{showDefaults ? 'hide' : 'show'} {atDefault.length} at default</a>}</div>
      </div>
      <div className="grid-2">
        {[...shown, ...(showDefaults ? atDefault : [])].map(f => <RegistryField key={f.key} f={f} cfg={cfg} setCfg={setCfg} status={statusByKey[f.key]} mapped={reconByKey[f.key]} hint={hints && hints[f.key]}
          known={known && known[f.key]} onAdopt={onAdopt} live={live} />)}
      </div>
      {children}
    </div>
  );
}
function GroupedInputs({ cfg, setCfg, manualStatus, recon, hints, known, onAdopt, live, extras }) {
  const IR = window.InputReconciliation;
  if (!IR) return null;
  const statusByKey = Object.fromEntries(((manualStatus && manualStatus.rows) || []).map(r => [r.key, r]));
  const reconByKey = Object.fromEntries(((recon && recon.rows) || []).filter(r => !r.unmapped).map(r => [r.key, r]));
  return (
    <div>
      {IR.GROUPS.map(g => (
        <InputGroup key={g.key} group={g} fields={IR.FIELD_REGISTRY.filter(f => f.group === g.key)} cfg={cfg} setCfg={setCfg} statusByKey={statusByKey} reconByKey={reconByKey} hints={hints} known={known} onAdopt={onAdopt} live={live}>
          {extras && extras[g.key]}
        </InputGroup>
      ))}
    </div>
  );
}

window.RetirementScreens = { SCREENS, ScreenNav, Source, StaleInputsBanner, TaxBucketsPanel, SpendingBridgeCard, TornadoCard, AnswerCard, TimelineChart, IntermediatesTable, GroupedInputs, downloadCsv };
