// financial/src/retirement.jsx — the Retirement planner.
//
// Everything Retirement: the Delaware SEPP pension model (MA+30 pay grid,
// plan-era rules, eligibility, healthcare cliffs, survivor-election EV),
// the planner page itself, and the 401(k) contribution calculator.
//
// Slices run in their own Babel scope. src/shared.jsx runs first and
// publishes window.FinanceShared — the SAME DataContext object the shell's
// DataProvider renders, plus the shared formatters and profile helpers.
// Nothing here may touch firebase or FinanceShared-dependent values at
// module scope beyond the destructures below: a throw out here never
// publishes window.FinanceViews.Retirement and the shell falls back to
// MissingView.

const { useState, useEffect, useContext, useMemo, useCallback, useRef } = React;
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, AreaChart, Area,
        XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine } = window.Recharts;
const { DataContext, useToast, fmtCur, fmtMonthYear, parseLocalDate, calcAge,
        USER_PROFILE } = window.FinanceShared;

// ── Retirement ────────────────────────────────────────────────────────────────
// The Red Clay salary schedule used to live here as three hardcoded constants.
// It now lives in Firestore, one document per fiscal year, and lib/teacherScale
// owns every calculation done with it — see that file for why. What remains
// here is the bridge: the same `ma30Salary(step, opts)` call the pension math
// has always made, now answered from a stored scale instead of a literal.
//
// `scale` defaults to the newest built-in schedule so anything that hasn't
// been handed a stored year still prices correctly rather than returning zero.
const DEFAULT_WIFE_SCALE = TeacherScale.FY2027_MA30;

/** The scale to price "today" with: the most recent stored fiscal year. */
const activeWifeScale = (wifeComp) => {
  const y = TeacherScale.activeYear(wifeComp);
  return (y && wifeComp[String(y)]) || DEFAULT_WIFE_SCALE;
};

// `fromStep` + `postCapGrowthPct` project contract raises for years past the
// grid's top step (the grid is one year's dollars and doesn't model future
// contract settlements). With fromStep omitted, returns today's grid value.
const ma30Salary = (step, { nationallyCertified = false, fromStep = null, postCapGrowthPct = 0, scale = null } = {}) => {
  const sc = scale || DEFAULT_WIFE_SCALE;
  const placement = { step, natCertified: nationallyCertified };
  if (fromStep == null || postCapGrowthPct <= 0) return TeacherScale.computeSalary(sc, placement).total;
  return TeacherScale.projectSalary(sc, { ...placement, step: fromStep }, {
    yearsAhead: Math.max(0, Math.floor(step||1) - Math.floor(fromStep||1)),
    growthPct: postCapGrowthPct,
  });
};
// ── Delaware State Employees' Pension Plan (SEPP) ────────────────────────────
// Source: Delaware Office of Pensions Summary Plan Description (29 Del. C. Ch. 55).
// All rates, eligibility tests, and healthcare tiers are direct quotations from
// the SPD — change only if the underlying law changes.
const SEPP = {
  // The 1/1/2012 cutoff is the single largest driver of plan economics:
  // vesting rule, normal retirement age, and early-retirement penalty rate all
  // shift across this boundary.
  PRE_2012_CUTOFF: '2012-01-01',
  HEALTH_1991_CUTOFF: '1991-07-01',
  HEALTH_2007_CUTOFF: '2007-01-01',
  // Service multipliers (SPD §6 Benefit Computations, Figure 1)
  MULT_PRE_1997:  0.0200,  // service before 1/1/1997
  MULT_POST_1996: 0.0185,  // service on or after 1/1/1997 (the vast majority for modern hires)
  // Early-retirement reduction per month under normal threshold
  EARLY_REDUCTION_PER_MONTH_PRE_2012:  0.002,
  EARLY_REDUCTION_PER_MONTH_POST_2012: 0.004,
  // Survivor election (SPD §4): irrevocable, locked at first deposit.
  // {survivorPct} = % of member's pension paid to survivor; {pensionReductionPct} = reduction taken on member's own pension while alive.
  SURVIVOR_OPTIONS: [
    {survivorPct: 50,    pensionReductionPct: 0, label:'50% (no reduction)'},
    {survivorPct: 66.67, pensionReductionPct: 2, label:'66.67% (–2%)'},
    {survivorPct: 75,    pensionReductionPct: 3, label:'75% (–3%)'},
    {survivorPct: 100,   pensionReductionPct: 6, label:'100% (–6%)'},
  ],
  // Sick-leave buy-in (SPD §7, Table 4): 21 unused sick days = 1 month of credited service, max 12 months.
  // Cost = FAE × 5% × months purchased, payable at retirement (rollover-eligible).
  SICK_LEAVE_DAYS_PER_MONTH: 21,
  SICK_LEAVE_MAX_MONTHS: 12,
  SICK_LEAVE_COST_PCT: 0.05,
  // Burial benefit (SPD §9): $7,000 taxable, paid to named beneficiary.
  BURIAL_BENEFIT: 7000,
};

// Classify a hire date into plan era. Note: a break-in-service + rehire after
// 1/1/2012 may reclassify the member — confirm with Office of Pensions in
// writing if there was a gap > 4 months.
const seppEra = (hireIso) => {
  if (!hireIso) return 'post-2012';
  return parseLocalDate(hireIso) < parseLocalDate(SEPP.PRE_2012_CUTOFF) ? 'pre-2012' : 'post-2012';
};
// Healthcare subsidy tier schedule depends on first-hire date.
const seppHealthEra = (hireIso) => {
  if (!hireIso) return 'post-2007';
  const d = parseLocalDate(hireIso);
  if (d < parseLocalDate(SEPP.HEALTH_1991_CUTOFF)) return 'pre-1991';
  if (d < parseLocalDate(SEPP.HEALTH_2007_CUTOFF)) return '1991-2006';
  return 'post-2007';
};
// State-share % of retiree health premium for given era + years of service.
const seppHealthShare = (healthEra, yos) => {
  if (healthEra === 'pre-1991') return 100;
  if (healthEra === '1991-2006') {
    if (yos < 10) return 0;
    if (yos < 15) return 50;
    if (yos < 20) return 75;
    return 100;
  }
  // post-2007
  if (yos < 15)   return 0;
  if (yos < 17.5) return 50;
  if (yos < 20)   return 75;
  return 100;
};
// Healthcare cliff thresholds in (yos, sharePct) pairs for a given era.
const seppHealthCliffs = (healthEra) => {
  if (healthEra === 'pre-1991') return [{yos:0, share:100}];
  if (healthEra === '1991-2006') return [{yos:10, share:50}, {yos:15, share:75}, {yos:20, share:100}];
  return [{yos:15, share:50}, {yos:17.5, share:75}, {yos:20, share:100}];
};
// Vesting requirement varies by era.
const seppVested = (era, yos) => yos >= (era === 'pre-2012' ? 5 : 10);
// Eligibility for a pension at (era, yos, age). Returns the most-favorable matching rule.
// {eligible, type, reductionPct, label}
const seppEligibility = (era, yos, age) => {
  // 30 years any age is the universal "full pension" path
  if (yos >= 30) return {eligible:true, type:'service-30', reductionPct:0, label:'30 yrs any age (full)'};
  if (era === 'pre-2012') {
    if (yos >= 15 && age >= 60) return {eligible:true, type:'service-15+60', reductionPct:0, label:'15 yrs + age 60 (full)'};
    if (yos >= 5  && age >= 62) return {eligible:true, type:'service-5+62',  reductionPct:0, label:'5 yrs + age 62 (full)'};
    if (yos >= 15 && age >= 55) {
      const monthsUnder60 = Math.max(0, (60 - age) * 12);
      return {eligible:true, type:'early-15+55', reductionPct: monthsUnder60 * SEPP.EARLY_REDUCTION_PER_MONTH_PRE_2012 * 100, label:`15 yrs + age 55 (early, –${(monthsUnder60*SEPP.EARLY_REDUCTION_PER_MONTH_PRE_2012*100).toFixed(1)}%)`};
    }
    if (yos >= 25) {
      const monthsShort = Math.max(0, (30 - yos) * 12);
      return {eligible:true, type:'early-25', reductionPct: monthsShort * SEPP.EARLY_REDUCTION_PER_MONTH_PRE_2012 * 100, label:`25 yrs any age (early, –${(monthsShort*SEPP.EARLY_REDUCTION_PER_MONTH_PRE_2012*100).toFixed(1)}%)`};
    }
    return {eligible:false, type:null, reductionPct:0, label:'Not yet eligible'};
  }
  // post-2012
  if (yos >= 20 && age >= 60) return {eligible:true, type:'service-20+60', reductionPct:0, label:'20 yrs + age 60 (full)'};
  if (yos >= 10 && age >= 65) return {eligible:true, type:'service-10+65', reductionPct:0, label:'10 yrs + age 65 (full)'};
  if (yos >= 15 && age >= 55) {
    const monthsUnder60 = Math.max(0, (60 - age) * 12);
    return {eligible:true, type:'early-15+55', reductionPct: monthsUnder60 * SEPP.EARLY_REDUCTION_PER_MONTH_POST_2012 * 100, label:`15 yrs + age 55 (early, –${(monthsUnder60*SEPP.EARLY_REDUCTION_PER_MONTH_POST_2012*100).toFixed(1)}%)`};
  }
  if (yos >= 25) {
    const monthsShort = Math.max(0, (30 - yos) * 12);
    return {eligible:true, type:'early-25', reductionPct: monthsShort * SEPP.EARLY_REDUCTION_PER_MONTH_POST_2012 * 100, label:`25 yrs any age (early, –${(monthsShort*SEPP.EARLY_REDUCTION_PER_MONTH_POST_2012*100).toFixed(1)}%)`};
  }
  return {eligible:false, type:null, reductionPct:0, label:'Not yet eligible'};
};
// Gross pension formula: split between pre-1997 and post-1996 service.
// For modern hires (post-1997) preYears is 0 → reduces to FAE × 1.85% × yos.
const seppGrossPension = (fae, yos, preYears = 0) => {
  const post1996 = Math.max(0, yos - (preYears||0));
  return fae * (SEPP.MULT_PRE_1997 * (preYears||0) + SEPP.MULT_POST_1996 * post1996);
};
// Final Average Compensation: average of the 3 highest 12-month earnings periods.
// On a step grid that's effectively the last 3 service years; for flat growth
// it's the last 3 compounded years.
const seppFae = ({useMa30, fromStep, atService, nationallyCertified, postCapGrowthPct, flatSalary, flatGrowth, yearsOut, scale}) => {
  if (useMa30) {
    const opts = { nationallyCertified, fromStep, postCapGrowthPct, scale };
    const yrs = [atService-2, atService-1, atService].filter(y=>y>=1);
    if (!yrs.length) return 0;
    return Math.round(yrs.reduce((s,y)=>s+ma30Salary(y,opts),0)/yrs.length);
  }
  const compound = n => (flatSalary||0) * Math.pow(1+((flatGrowth||0)/100), Math.max(0,n));
  return Math.round((compound(yearsOut) + compound(yearsOut-1) + compound(yearsOut-2)) / 3);
};
// Convex survival curve: probability of being alive at targetAge given alive
// at currentAge, using S(t) = 1 - ((t-start)/(max-start))^k. Linear decay
// drastically overstates death probability in midlife (peak earning + first
// retirement decade) and would skew survivor-election EVs upward. Cubic-ish
// decay calibrated against SSA 2020 period life tables hits the well-known
// landmarks reasonably well — e.g., F starting age 42 alive at 75 ≈ 0.83
// (real ≈ 0.79), F at 85 ≈ 0.49 (real ≈ 0.50), M starting 45 alive at 80
// ≈ 0.61 (real ≈ 0.55).
const probAlive = (currentAge, targetAge, gender) => {
  if (targetAge <= currentAge) return 1;
  const maxAge = gender === 'F' ? 100 : 96;
  if (targetAge >= maxAge) return 0;
  const k = gender === 'F' ? 3.2 : 2.8;
  const elapsed = (targetAge - currentAge) / (maxAge - currentAge);
  return Math.max(0, 1 - Math.pow(elapsed, k));
};
// Expected lifetime payout of each SEPP survivor election option for a given
// household: member receives memberAnnual while alive; survivor receives
// survivorAnnual in years where member is dead but spouse alive. Sum over
// future years gives the household-EV; comparing options reveals the dominant
// choice given the age/gender gap.
const evSurvivorElection = ({memberCurrentAge, memberGender, spouseCurrentAge, spouseGender, memberRetireAge, memberPensionGross}) => {
  return SEPP.SURVIVOR_OPTIONS.map(opt => {
    const memberAnnual = memberPensionGross * (1 - opt.pensionReductionPct/100);
    const survivorAnnual = memberAnnual * opt.survivorPct/100;
    let memberEv = 0, survivorEv = 0;
    for (let yr = memberRetireAge; yr < 102; yr++) {
      const spouseAgeYr = spouseCurrentAge + (yr - memberCurrentAge);
      const pMember = probAlive(memberCurrentAge, yr, memberGender);
      const pSpouse = probAlive(spouseCurrentAge, spouseAgeYr, spouseGender);
      memberEv   += memberAnnual * pMember;
      survivorEv += survivorAnnual * (1 - pMember) * pSpouse;
    }
    return {
      ...opt,
      memberAnnual: Math.round(memberAnnual),
      survivorAnnual: Math.round(survivorAnnual),
      memberEv: Math.round(memberEv),
      survivorEv: Math.round(survivorEv),
      totalEv: Math.round(memberEv + survivorEv),
    };
  });
};
// Recommend the best SEPP survivor election option. Pure dollar EV treats $1
// to the member while alive as equal to $1 to the survivor when the member
// is dead. That's only true if both dollars buy the same utility — which
// breaks down when the surviving spouse has independent retirement income
// (own SS + portfolio). Then the survivor benefit is "extra" that doesn't
// substantively improve the spouse's standard of living, while the member's
// own pension reduction is a permanent tax on her standard of living during
// the (longer expected) years she's alive. Apply a utility weight to the
// survivor leg based on spouse independence, then rank.
//
// spouseReplacementPct: spouse's own retirement income as % of working income.
// 0% = totally dependent on member's pension; 70%+ = fully independent.
const recommendSurvivor = (evRows, spouseReplacementPct = 0) => {
  // Linear weight: 1.0 if spouse has 0 own income, 0.30 if spouse has ≥70%
  // own replacement. Reflects that survivor money is "icing" not "bread".
  const survUtilWeight = Math.max(0.30, 1 - (spouseReplacementPct/100) * 1.0);
  const scored = evRows.map(r => ({
    ...r,
    survUtilWeight,
    weightedEv: Math.round(r.memberEv + r.survivorEv * survUtilWeight),
  }));
  const sorted = [...scored].sort((a,b)=>b.weightedEv - a.weightedEv);
  const best = sorted[0];
  return {recommended: best, ladder: scored, survUtilWeight};
};

const RetCfgCtx = React.createContext({});
// `optional` is not cosmetic. The default path coerces an unset field to 0
// and writes 0 back, so "nobody said" and "he said zero" become the same
// stored value — which is how a placeholder core rate turned into a fact the
// Tax tab then rendered as free money. A rate that steps TO 0% is also a real
// (if unusual) plan term, so the two states have to stay distinguishable at
// the input, not just downstream.
const F=({label,k,step,prefix,suffix,hint,min,max,type,optional})=>{
  const {cfg,upd,updRaw}=React.useContext(RetCfgCtx);
  const isText = type==='date'||type==='text';
  const blank = cfg[k]==null||cfg[k]==='';
  return (
    <div style={{marginBottom:10}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:3}}>
        <span className="label" style={{marginBottom:0}}>{label}</span>
        {hint&&<span style={{fontSize:10,color:'#3b82f6',background:'rgba(59,130,246,0.12)',padding:'1px 6px',borderRadius:4}}>{hint}</span>}
      </div>
      <div style={{display:'flex',alignItems:'center',gap:6}}>
        {prefix&&<span style={{fontSize:11,color:'#64748b'}}>{prefix}</span>}
        {isText||optional ? (
          <input type={type||'number'} placeholder="not set"
            value={blank?'':cfg[k]} step={step??1} min={min} max={max}
            onChange={updRaw(k, isText)} style={{flex:1}}/>
        ) : (
          <input type="number" value={cfg[k]??0} step={step??1} min={min} max={max} onChange={upd(k)} style={{flex:1}}/>
        )}
        {suffix&&<span style={{fontSize:11,color:'#64748b'}}>{suffix}</span>}
      </div>
    </div>
  );
};

function Retirement() {
  const { retirementConfig, compensation, wifeComp, accounts, cashflow, uid, saveRetirementConfig } = useContext(DataContext);
  const { show, Toast } = useToast();

  const currentYear = new Date().getFullYear();
  // Prefer the latest actual compensation row; skip _projected synthetic rows
  // so the ladder/IC interpolation doesn't feed back into the UI as "current".
  const latestComp = (() => {
    const cur = compensation[currentYear];
    if (cur && !cur._projected) return cur;
    const prev = compensation[currentYear-1];
    if (prev && !prev._projected) return prev;
    return cur || prev || {};
  })();
  const ytd = compensation[currentYear]?.ytd;
  const payslipCount = compensation[currentYear]?.payslipCount || 0;
  // Derive ages from USER_PROFILE birthdays — source of truth, not user-entered.
  const derivedAge = calcAge(USER_PROFILE.birthday);
  const derivedSpouseAge = calcAge(USER_PROFILE.spouseBirthday);
  // Current-year total comp product (explicit current year, not "latest").
  // Total comp for the retirement projection, by the shared rule in
  // lib/compHistory. It deliberately does NOT use getCurrentYearTC here:
  // that returns the CURRENT year whenever a row exists for it, and a year
  // in progress holds base salary with bonus and stock still unpaid — so it
  // reported this year's base as though it were total comp and understated
  // the projection's starting point by the whole variable component.
  const compSummary = window.CompHistory.summary(compensation);
  const compTcProduct = {
    year: compSummary.comparable.year,
    total: compSummary.comparable.total || 0,
    basis: compSummary.comparable.basis,
    inProgress: compSummary.currentYear,
  };

  const detected401kContrib = useMemo(()=>{
    if(!ytd?.retirement401k || !payslipCount) return null;
    // IRS 402(g) elective deferral limit — payslips only show the employee
    // contribution. The old $70k cap was the 415(c) combined ceiling and
    // would let anomalous payroll data (or included match) pass through.
    const IRS_402G_LIMIT = window.TaxConstants.k401ForYear(currentYear).k401.employee;
    return Math.min(Math.round((ytd.retirement401k / payslipCount) * 26), IRS_402G_LIMIT);
  },[ytd, payslipCount]);

  // Year-keyed IRS numbers for the IRA cards — one source in lib/taxConstants
  // (the old hard-coded hint said "$8,500 age 50+" when the 2026 catch-up
  // makes it $8,600).
  const iraTax = window.TaxConstants.forYear(currentYear);
  const iraMax = iraTax.ira.limit + iraTax.ira.catchUp50;
  const iraHint = `${iraTax.year} limit $${iraTax.ira.limit.toLocaleString()} ($${iraMax.toLocaleString()} age 50+) total across all IRAs`;
  const rothPhaseHint = `Roth phase-out $${Math.round(iraTax.mfj.rothPhaseOut.start/1000)}k–$${Math.round(iraTax.mfj.rothPhaseOut.end/1000)}k MFJ`;

  const detectedAccounts = useMemo(()=>{
    if(!accounts?.length) return {};
    const isRet = a => {
      const t=(a.type?.name||'').toLowerCase();
      const st=(a.subtype?.name||a.subtype?.display||'').toLowerCase();
      const n=(a.displayName||'').toLowerCase();
      return t==='retirement'||st.includes('401')||st.includes('ira')||st.includes('roth')||st.includes('403')||st.includes('pension')||n.includes('401')||n.includes('403')||n.includes('roth')||n.includes(' ira')||n.includes('pension')||n.includes('sipp')||n.includes(' isa');
    };
    const retAccts = accounts.filter(a=>a.isAsset&&isRet(a));
    const your401k = retAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return n.includes('401')&&!n.includes('403');});
    const wife403b = retAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return n.includes('403');});
    const rothAccts = retAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return n.includes('roth');});
    const tradIra   = retAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return n.includes('ira')&&!n.includes('roth');});
    // Split trad IRAs: wife's name contains "wife","spouse","sarah","joint" or account belongs to wife
    const wifeKeywords=['wife','spouse','sarah','her '];
    const yourTradIra = tradIra.filter(a=>{const n=(a.displayName||'').toLowerCase();return !wifeKeywords.some(w=>n.includes(w));});
    const wifeTradIra = tradIra.filter(a=>{const n=(a.displayName||'').toLowerCase();return wifeKeywords.some(w=>n.includes(w));});
    // If can't distinguish, attribute all trad IRA to "your" (most common scenario)
    const yourRoth  = rothAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return !wifeKeywords.some(w=>n.includes(w));});
    const wifeRoth  = rothAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return wifeKeywords.some(w=>n.includes(w));});
    const ukPension = retAccts.filter(a=>{const n=(a.displayName||'').toLowerCase();return n.includes('pension')||n.includes('sipp')||n.includes('nest');});
    // The taxable brokerage — the bridge account. It is defined by what it is
    // NOT: not a retirement wrapper, not a kid's education account. A 529 in
    // this bucket would be spent twice, once on college and once on the years
    // between the last paycheck and Social Security.
    const isEducation = a => {
      const st=(a.subtype?.name||a.subtype?.display||'').toLowerCase();
      const n=(a.displayName||'').toLowerCase();
      return /529|utma|ugma|custodial|education/.test(`${st} ${n}`);
    };
    const brokerage = accounts.filter(a=>{
      if(!a.isAsset || isRet(a) || isEducation(a)) return false;
      const t=(a.type?.name||'').toLowerCase();
      const st=(a.subtype?.name||a.subtype?.display||'').toLowerCase();
      return t==='investment'||t==='brokerage'||st.includes('brokerage')||st.includes('taxable');
    });
    const sum = arr => arr.reduce((s,a)=>s+(a.currentBalance||0),0);
    const total401k = your401k.length ? sum(your401k) : null;
    const allIra = tradIra.length ? sum(tradIra) : null;
    return {
      total401k,
      wife403b: wife403b.length ? sum(wife403b) : null,
      yourIra: yourTradIra.length ? sum(yourTradIra) : allIra,
      wifeIra: wifeTradIra.length ? sum(wifeTradIra) : null,
      yourRoth: yourRoth.length ? sum(yourRoth) : null,
      wifeRoth: wifeRoth.length ? sum(wifeRoth) : null,
      ukPension: ukPension.length ? sum(ukPension) : null,
      brokerage: brokerage.length ? sum(brokerage) : null,
      all: retAccts,
    };
  },[accounts]);

  const [cfg, setCfg] = useState({
    // Your profile
    currentAge:45, retireAge:65, salary:latestComp.baseSalary||150000, salaryGrowth:3,
    // Your 401(k)
    currentBalance:500000, annualContribution:23500,
    employerMatchPct:3, employerCorePct:5, employerCoreSalaryCap:100000,
    // Your IRA
    yourIraBalance:0, yourIraContrib:7500, yourRothBalance:0, yourRothContrib:0,
    // Wife's profile — wifeUseMa30Scale auto-derives salary from Red Clay MA+30 schedule
    wifeAge:44, wifeRetireAge:55, wifeSalary:118094, wifeSalaryGrowth:2.5,
    wifeUseMa30Scale:true, wifeNationallyCertified:true, wifeCurrentService:17,
    // Wife's 403(b)
    wife403bBalance:0, wife403bContrib:20500, wife403bEmployerContrib:0,
    // Wife's IRA
    wifeIraBalance:0, wifeIraContrib:7500, wifeRothBalance:0, wifeRothContrib:0,
    // Delaware Pension (wifeCurrentService default is set in wife's profile block above).
    // Hire date drives plan-era classification (pre-2012 vs post-2012 rules) and
    // the healthcare subsidy tier schedule. wifeServiceLossYears captures any
    // break-in-service period that was not creditable (e.g. a gap >4 months
    // pre-vesting). wifeContribsWithdrawn flags whether a refund was taken
    // during the gap — if true and pre-gap service was <5 yrs, that service
    // is forfeited until repaid + 5 consecutive yrs accrued (SPD §2).
    wifeYearsService:30,
    wifeHireDate:'2009-08-01',
    wifeServiceLossYears:1,         // ~1-yr gap, not creditable per SPD §2 (>4 mo). Overridden when CAS data present.
    wifeContribsWithdrawn:false,    // unknown; safer to assume preserved
    wifeSickLeaveDays:0,            // buy-in: 21 days = 1 month service (max 12 mo)
    wifeSurvivorElection:75,        // irrevocable at first deposit — 50/66.67/75/100
    wifeSurvivorReviewDate:'',      // ISO date this election was last consciously reviewed; empty = never
    wifeFaeOverride:0,              // optional manual FAE if known from CAS Block 3
    wifePensionRate:1.85,           // SPD §6: 1.85% × post-1996 service. Legacy field — derived from formula.
    // CAS-anchored data (from her Comprehensive Annual Statement). The Office
    // of Pensions is the authoritative source for what she HAS earned and how
    // much service she has, so these govern creditable service and the
    // retire-today FAE. They do not govern the FAE projected to retirement —
    // see the note on wifeFaeAt for why a trailing average must not be
    // compounded forward as though it were a current salary.
    wifeCasPensionId:'233082',
    wifeCasAsOfDate:'2025-12-31',
    wifeCasService:15.0528,         // Block 2: Total Pension Creditable Service
    wifeCasFae:99312,               // Block 4: 36×monthly FAE = annual FAE
    wifeCasContribBalance:40039,    // Block 3: contributions ($28,596.74) + interest ($11,442.56)
    // The three highest consecutive 12-month periods the CAS used to build
    // Block 4 — the ONLY actual earnings record for her anywhere in this app
    // (payslip upload is his employer's; Monarch sees net deposits, not gross).
    // Their mean is the CAS FAE, which is the check on this list: 105,532 +
    // 101,563 + 90,842 = 297,937, ÷3 = 99,312. Kept as data rather than the
    // prose bullet it used to be so it can be updated when a new CAS arrives.
    wifeCasPeriods:[
      { label:'Calendar 2025',      amount:105532 },
      { label:'Dec 2023 – Nov 2024', amount:101563, note:'non-calendar — possibly captures lag pay' },
      { label:'Jul 2022 – Jun 2023', amount:90842 },
    ],
    // Early-retirement + contract work scenario calculator
    earlyRetireYrs:25,              // service yrs at the early exit scenario
    contractAnnual:80000,           // expected annual contract income during the gap
    contractExpenses:5000,          // self-employment expenses (deductible)
    earlyRetireHealthMo:650,        // DSHBP retiree couple premium during contract years
    // Social Security (monthly at FRA)
    yourSsMonthly:2500, wifeSsMonthly:1800, ssStartAge:67,
    // UK Pension (optional)
    ukPensionBalance:0, ukPensionAnnualContrib:0, ukPensionEmployerContrib:0,
    // ── What retirement actually COSTS ──────────────────────────────────
    // The plan is graded against this, not against a fraction of the final
    // pay packet. Every line below is a thing that is true of this household
    // and not of the 70%-of-salary rule of thumb: the mortgage clears before
    // the last day of work, the school fees stop, the 529s stop, the kids are
    // not dependents, and health cover comes from the Delaware retiree plan
    // rather than a marketplace premium.
    planToAge:95,
    spendBasis:'derived',           // 'derived' | 'observed' | 'manual' — never blended
    manualAnnualSpend:0,
    effectiveTaxPct:33,             // household all-in rate on gross (fed + DE + FICA)
    // Costs inside today's spending that stop on a date. Ages are YOURS.
    schoolCostAnnual:60000,  schoolEndsAtAge:52,
    mortgageAnnual:42000,    mortgageEndsAtAge:55,
    plan529Annual:12000,     plan529EndsAtAge:52,
    dependentKidAnnual:12000, kidsIndependentAtAge:56,
    // Costs retirement ADDS.
    medicareHealthMo:350,           // Medicare B+D + DSHBP supplement, 65+
    travelAnnual:20000, travelUntilAge:78,
    realSpendDeclinePct:0,          // Blanchett "smile" — off unless asked for
    // The bridge account and the conversion window it pays for.
    brokerageBalance:0, brokerageContrib:0, brokerageBasisPct:60,
    conversionEndAge:74, conversionTargetRatePct:24, dividendYieldPct:1.8,
    // Redirect freed cashflow: when school / mortgage / 529 / kid costs hit
    // their end dates, route the freed money into the brokerage. A
    // commitment, not a fact — off until switched on, priced by the levers.
    redirectFreedCashflow:false,
    // IRMAA-aware conversion ceiling. 0 = off; the input hints tier 1.
    irmaaMagiCap:0,
    // Pay conversion tax from the conversion itself (59½+) when the
    // brokerage can't carry it. Second-best and explicit — off by default,
    // priced by the levers like every other commitment.
    payTaxFromConversion:false,
    // Per-person Social Security claim ages (monthly figures above are at
    // FRA 67; the actuarial factor is applied from these).
    yourSsClaimAge:67, wifeSsClaimAge:67,
    // Monte Carlo return volatility (annual stdev).
    mcStdevPct:12,
    // Assumptions
    expectedReturn:7, inflationRate:3,
  });
  const upd = k => e => setCfg(c=>({...c,[k]:Number(e.target.value)||0}));
  // Keeps an empty input EMPTY instead of turning it into 0. See F's note.
  const updRaw = (k, isText) => e => setCfg(c=>({
    ...c, [k]: e.target.value === '' ? null : (isText ? e.target.value : Number(e.target.value)),
  }));

  // Hydrate cfg from saved config ONCE on first load. Re-running on every
  // retirementConfig reference change would clobber unsaved user edits
  // (e.g. typing a new "years of service" value). Ages are always forced to
  // the birthday-derived values — never whatever was saved previously.
  const hydratedRef = useRef(false);
  useEffect(()=>{
    if(retirementConfig && !hydratedRef.current){
      hydratedRef.current = true;
      setCfg(c=>({...c,...retirementConfig, currentAge:derivedAge, wifeAge:derivedSpouseAge}));
    }
  },[retirementConfig, derivedAge, derivedSpouseAge]);
  // Keep ages in sync with derived values even on initial render (before
  // retirementConfig loads) so all downstream calculations use real ages.
  useEffect(()=>{
    setCfg(c=>(c.currentAge===derivedAge && c.wifeAge===derivedSpouseAge) ? c : ({...c, currentAge:derivedAge, wifeAge:derivedSpouseAge}));
  },[derivedAge, derivedSpouseAge]);
  // Auto-seed your salary from the current-year TC product (base + bonus + RSU).
  // Uses compensation[currentYear] as the source of truth — falls back to
  // prior year only when current year hasn't been entered yet.
  const latestCompTC = compTcProduct.total;
  useEffect(()=>{ if(latestCompTC && !retirementConfig) setCfg(c=>({...c, salary:latestCompTC})); },[latestCompTC, retirementConfig]);
  // Seeding only ever ran when no retirementConfig existed, so the moment
  // anything on this page was saved the figure froze and later Compensation
  // years never reached it. It stays an override — modelling a different
  // salary is legitimate — but a divergence is now visible and one click to
  // resolve, instead of silently going stale.
  const compTcStale = !!(latestCompTC && Number(cfg.salary) !== latestCompTC);
  useEffect(()=>{ if(!retirementConfig && detected401kContrib) setCfg(c=>({...c,annualContribution:detected401kContrib})); },[detected401kContrib, retirementConfig]);
  useEffect(()=>{
    if(retirementConfig) return;
    const upd={};
    if(detectedAccounts.total401k) upd.currentBalance=Math.round(detectedAccounts.total401k);
    if(detectedAccounts.wife403b) upd.wife403bBalance=Math.round(detectedAccounts.wife403b);
    if(detectedAccounts.yourIra) upd.yourIraBalance=Math.round(detectedAccounts.yourIra);
    if(detectedAccounts.wifeIra) upd.wifeIraBalance=Math.round(detectedAccounts.wifeIra);
    if(detectedAccounts.yourRoth) upd.yourRothBalance=Math.round(detectedAccounts.yourRoth);
    if(detectedAccounts.wifeRoth) upd.wifeRothBalance=Math.round(detectedAccounts.wifeRoth);
    if(detectedAccounts.ukPension) upd.ukPensionBalance=Math.round(detectedAccounts.ukPension);
    if(detectedAccounts.brokerage) upd.brokerageBalance=Math.round(detectedAccounts.brokerage);
    if(Object.keys(upd).length) setCfg(c=>({...c,...upd}));
  },[detectedAccounts, retirementConfig]);

  // The pay schedule every projection below is priced from — the most recent
  // year stored in the Compensation → Hers section, which is where it gets
  // edited when the district reissues the scale. Falls back to the newest
  // built-in schedule before Firestore has loaded.
  const wifeScale = useMemo(()=>activeWifeScale(wifeComp||{}), [wifeComp]);
  const wifeScaleTopStep = TeacherScale.topStep(wifeScale) || 17;
  // Her step as recorded for that year in the Hers section. Divergence from the
  // retirement input is surfaced below rather than silently reconciled — two
  // places holding a step is exactly the situation where a quiet auto-sync
  // makes the wrong one win.
  const wifeScaleStep = wifeScale && wifeScale.step != null ? Math.floor(wifeScale.step) : null;

  // Keep cfg.wifeSalary in sync with the MA+30 scale when auto-scale is enabled
  // so downstream tabs (Strategy etc.) that read cfg.wifeSalary stay consistent.
  useEffect(()=>{
    if(!cfg.wifeUseMa30Scale) return;
    const target = ma30Salary(cfg.wifeCurrentService||1, {nationallyCertified: !!cfg.wifeNationallyCertified, scale: wifeScale});
    if(target !== cfg.wifeSalary) setCfg(c=>({...c, wifeSalary:target}));
  },[cfg.wifeUseMa30Scale, cfg.wifeCurrentService, cfg.wifeNationallyCertified, cfg.wifeSalary, wifeScale]);

  // Derived values
  // wifeYearsService = TARGET total years of service at retirement (e.g. 30 for Rule of 30)
  // wifeRetireAge = drives salary growth projection; set to age when she'll reach her service target
  const wifeYearsToRetire = Math.max(0, (cfg.wifeRetireAge||55) - (cfg.wifeAge||44));
  const wifeYearsAtRetire = cfg.wifeYearsService||30; // treat as total yrs at retirement, not additive
  const wifeFinalSalary = cfg.wifeUseMa30Scale
    ? ma30Salary((cfg.wifeCurrentService||1) + wifeYearsToRetire, {
        nationallyCertified: !!cfg.wifeNationallyCertified,
        fromStep: cfg.wifeCurrentService||1,
        postCapGrowthPct: cfg.wifeSalaryGrowth||2.5,
        scale: wifeScale,
      })
    : Math.round((cfg.wifeSalary||0) * Math.pow(1 + (cfg.wifeSalaryGrowth||2.5)/100, wifeYearsToRetire));

  // ── SEPP-specific derivations (Delaware Office of Pensions SPD) ─────────────
  const seppEraVal       = seppEra(cfg.wifeHireDate);
  const seppHealthEraVal = seppHealthEra(cfg.wifeHireDate);
  // Creditable service: when an authoritative CAS number is on file, linear-
  // extrapolate forward from the CAS as-of date (assumes continuous employment
  // since the statement). Otherwise fall back to the years-on-job minus gap
  // estimate. The CAS path is preferred because it reflects the Office of
  // Pensions' own records — including all leave/gap reconciliation.
  const today = new Date();
  const wifeHasCas = (cfg.wifeCasService||0) > 0 && !!cfg.wifeCasAsOfDate;
  const wifeYrsSinceCas = wifeHasCas
    ? Math.max(0, (today - parseLocalDate(cfg.wifeCasAsOfDate)) / (365.25*24*3600*1000))
    : 0;
  const wifeCreditableNow = wifeHasCas
    ? (cfg.wifeCasService||0) + wifeYrsSinceCas
    : Math.max(0, (cfg.wifeCurrentService||0) - (cfg.wifeServiceLossYears||0));
  const wifeVestedNow     = seppVested(seppEraVal, wifeCreditableNow);
  // Creditable service at planned retirement: now + years from now to retire.
  const wifeYrsFromNowToRetire = Math.max(0, (cfg.wifeRetireAge||55) - (cfg.wifeAge||44));
  const wifeCreditableAtRetire = wifeHasCas
    ? wifeCreditableNow + wifeYrsFromNowToRetire
    : Math.max(0, wifeYearsAtRetire - (cfg.wifeServiceLossYears||0));
  // FAE = average of the 3 highest consecutive 12-month earnings periods (SPD §6).
  //
  // Which source is right depends on WHERE THOSE THREE PERIODS SIT IN TIME,
  // and getting that backwards is worth about $12k/yr of pension.
  //
  // The CAS is authoritative for what she has already earned, and it is what
  // wifeFaeNow uses below. But $99,312 is a three-year TRAILING AVERAGE
  // centred roughly two years behind the statement date, so compounding it
  // forward carries that lag all the way to retirement — it projects as though
  // she will always be paid what she averaged over 2022-2025. Her salary today
  // is a known $118,094 off the FY27 sheet, and the SPD's own definition
  // (average of the final three years) built from that is both better anchored
  // and what the statute actually describes.
  //
  // So the weight between the two sources is just HOW MUCH OF THE THREE-YEAR
  // WINDOW IS STILL IN THE FUTURE. Retiring today, all three periods are
  // history and the CAS owns the answer outright. Retiring in three years or
  // more, all three are future and the scale owns it. In between the window
  // straddles, and the blend is the literal fraction — at t=1 one of the three
  // periods is future, so the scale gets a third of the weight.
  //
  // This is also what keeps the two sources from producing a cliff: a hard
  // switch at t=3 made the projected FAE jump ~$18k between a retirement two
  // years out and one three years out, which is an artefact of the rule rather
  // than anything about her pay.
  const FAE_WINDOW_YEARS = 3;
  const wifeFaeAt = (yrsFromNow) => {
    if (cfg.wifeFaeOverride > 0) return cfg.wifeFaeOverride;
    const t = Math.max(0, yrsFromNow);
    const fromScale = seppFae({
      useMa30: !!cfg.wifeUseMa30Scale,
      fromStep: cfg.wifeCurrentService||1,
      atService: (cfg.wifeCurrentService||1) + t,
      nationallyCertified: !!cfg.wifeNationallyCertified,
      postCapGrowthPct: cfg.wifeSalaryGrowth||2.5,
      flatSalary: cfg.wifeSalary||0,
      flatGrowth: cfg.wifeSalaryGrowth||2.5,
      yearsOut: t,
      scale: wifeScale,
    });
    if (!(wifeHasCas && cfg.wifeCasFae > 0)) return fromScale;
    const futureShare = Math.min(1, t / FAE_WINDOW_YEARS);
    if (futureShare >= 1) return fromScale;
    const fromCas = cfg.wifeCasFae * Math.pow(1 + (cfg.wifeSalaryGrowth||2.5)/100, wifeYrsSinceCas + t);
    return Math.round(fromCas * (1 - futureShare) + fromScale * futureShare);
  };
  const wifeFae = wifeFaeAt(wifeYearsToRetire);
  // What the old CAS-escalation projection would have said, kept so the pension
  // card can show both instead of presenting a ~$21k swing as a bare new number.
  const wifeFaeCasProjected = wifeHasCas && cfg.wifeCasFae > 0
    ? Math.round(cfg.wifeCasFae * Math.pow(1 + (cfg.wifeSalaryGrowth||2.5)/100, wifeYrsSinceCas + wifeYearsToRetire))
    : null;
  // Current FAE — what her pension would be if she retired today. Drives the
  // refund-vs-deferred comparison and the "vested deferred pension" scenario.
  // At t=0 none of the window is future, so this is the pure CAS figure by
  // construction; going through the same helper keeps it that way instead of
  // leaving a third hand-written copy of the escalation to drift.
  const wifeFaeNow = wifeFaeAt(0);
  // Gross monthly pension before early-retirement reduction and survivor election.
  // She was hired Aug 2009 → 100% of service is post-1997 → multiplier is 1.85%.
  const wifeGrossPension = Math.round(seppGrossPension(wifeFae, wifeCreditableAtRetire, 0));
  // Eligibility at planned retirement → drives early-retirement reduction.
  const wifeEligAtRetire = seppEligibility(seppEraVal, wifeCreditableAtRetire, cfg.wifeRetireAge||55);
  // Survivor election: irrevocable. Applies a permanent reduction to the member's pension.
  const wifeSurvivorOpt = SEPP.SURVIVOR_OPTIONS.find(o => Math.abs(o.survivorPct - (cfg.wifeSurvivorElection||75)) < 0.5) || SEPP.SURVIVOR_OPTIONS[2];
  const wifePensionAfterEarly   = Math.round(wifeGrossPension * (1 - wifeEligAtRetire.reductionPct/100));
  const wifePensionAnnual       = Math.round(wifePensionAfterEarly * (1 - wifeSurvivorOpt.pensionReductionPct/100));
  const wifeSurvivorPensionAnnual = Math.round(wifePensionAnnual * wifeSurvivorOpt.survivorPct/100);
  // Survivor election recommendation: rank options by expected lifetime
  // household payout given the age/gender gap. "Spouse independent" trigger
  // checks whether your own retirement income (portfolio + SS) materially
  // covers your needs without her pension — if so, the survivor reduction
  // is paying for protection you don't need.
  const wifeSurvivorEvRows = useMemo(() => evSurvivorElection({
    memberCurrentAge: cfg.wifeAge||44,
    memberGender: 'F',
    spouseCurrentAge: cfg.currentAge||45,
    spouseGender: 'M',
    memberRetireAge: cfg.wifeRetireAge||55,
    memberPensionGross: wifePensionAfterEarly,
  }), [cfg.wifeAge, cfg.currentAge, cfg.wifeRetireAge, wifePensionAfterEarly]);
  // Your own retirement income replacement %: 4% rule on your portfolio + SS,
  // as a fraction of your current total comp. Drives the utility-weighting of
  // her pension's survivor benefit — if you're already at 70%+ replacement
  // from your own assets, the survivor pension is "extra" rather than "need".
  const yourOwnReplacementPct = useMemo(() => {
    if (!cfg.salary) return 0;
    // Portfolio at her death is hard to forecast; use today's combined retirement
    // accounts that are clearly yours (not the household pension).
    const yourPortfolioAt55 = (cfg.currentBalance||0) + (cfg.yourIraBalance||0) + (cfg.yourRothBalance||0);
    const portfolioIncome = yourPortfolioAt55 * 0.04;
    const ssIncome = (cfg.yourSsMonthly||0) * 12;
    const ownIncome = portfolioIncome + ssIncome;
    return Math.round(ownIncome / cfg.salary * 100);
  }, [cfg.salary, cfg.yourSsMonthly, cfg.currentBalance, cfg.yourIraBalance, cfg.yourRothBalance]);
  const wifeSurvivorRec = useMemo(
    () => recommendSurvivor(wifeSurvivorEvRows, yourOwnReplacementPct),
    [wifeSurvivorEvRows, yourOwnReplacementPct]
  );
  // Periodic-review tracker: prompt when last review > 12 months ago, or
  // when she's within 90 days of first pension deposit (election locks).
  const wifeReviewDaysSince = cfg.wifeSurvivorReviewDate
    ? Math.floor((today - parseLocalDate(cfg.wifeSurvivorReviewDate)) / (24*3600*1000))
    : null;
  const wifeReviewDue = wifeReviewDaysSince === null || wifeReviewDaysSince > 365;
  // Sick leave buy-in: 21 days = 1 month service; max 12 months; cost = FAE × 5% × months.
  const wifeSickLeaveMonths = Math.min(SEPP.SICK_LEAVE_MAX_MONTHS, Math.floor((cfg.wifeSickLeaveDays||0) / SEPP.SICK_LEAVE_DAYS_PER_MONTH));
  const wifeSickLeaveCost   = Math.round(wifeFae * SEPP.SICK_LEAVE_COST_PCT * wifeSickLeaveMonths);
  const wifeSickLeaveAnnualBoost = Math.round(seppGrossPension(wifeFae, wifeSickLeaveMonths/12, 0));
  // Healthcare state-share % at planned retirement.
  const wifeHealthShareAtRetire = seppHealthShare(seppHealthEraVal, wifeCreditableAtRetire);
  const yourYearsToRetire = Math.max(0, (cfg.retireAge||65) - (cfg.currentAge||45));
  const yourFinalSalary = Math.round((cfg.salary||0) * Math.pow(1 + (cfg.salaryGrowth||3)/100, yourYearsToRetire));
  const employerMatchAmt  = Math.min(cfg.annualContribution||0, (cfg.salary||0) * (cfg.employerMatchPct||0) / 100);
  const employerCoreAmt   = Math.min(cfg.salary||0, cfg.employerCoreSalaryCap||0) * (cfg.employerCorePct||0) / 100;
  const total401kAnnual   = (cfg.annualContribution||0) + employerMatchAmt + employerCoreAmt;
  const yourAgeWhenWifeRetires = (cfg.currentAge||0) + ((cfg.wifeRetireAge||55) - (cfg.wifeAge||44));

  // Accumulation only — from today to the last day of work.
  //
  // This loop used to run to 90 and apply a 4%-rule drawdown of its own,
  // which made it a SECOND retirement model sitting beside the one that
  // decides the grade. Two drawdown implementations on one page is the
  // situation where the chart and the headline quietly disagree, so the
  // withdrawal side now lives in exactly one place (lib/retirementNeeds,
  // which knows about tax, RMDs, the conversion window and the order the
  // accounts are drained) and this loop does the one thing it is good at:
  // growing the balances until they are needed. It also carries the taxable
  // brokerage, which the old version did not model at all — and which is the
  // account the whole 60-to-75 plan runs on.
  // Extracted into a plain function so the Decision Levers card can re-run
  // the SAME accumulation with a patched config — a lever that changes the
  // retire age or the brokerage contribution has to change the balances the
  // drawdown starts from, or the sensitivity it reports is fiction.
  const accumulate = useCallback((c)=>{
    const r = (c.expectedReturn||7) / 100;
    const yGrow = (c.salaryGrowth||3) / 100;
    const wGrow = (c.wifeSalaryGrowth||2.5) / 100;
    let ySalary = c.salary||0;
    let wSalary = c.wifeSalary||0;
    let b401=c.currentBalance||0, bIra=c.yourIraBalance||0;
    let b403=c.wife403bBalance||0, wIra=c.wifeIraBalance||0;
    let yRoth=c.yourRothBalance||0, wRoth=c.wifeRothBalance||0;
    let ukPen=c.ukPensionBalance||0;
    let brok=c.brokerageBalance||0;
    const data=[];
    for(let age=c.currentAge; age<=(c.retireAge||65); age++){
      const yearsElapsed = age - c.currentAge;
      const wAge=c.wifeAge+yearsElapsed;
      const yWork=age<c.retireAge, wWork=wAge<c.wifeRetireAge;
      // Grow salaries while working. Wife follows the MA+30 step scale when enabled.
      if(yWork && age>c.currentAge) ySalary *= (1+yGrow);
      if(c.wifeUseMa30Scale){
        wSalary = wWork ? ma30Salary((c.wifeCurrentService||1) + yearsElapsed, {
          nationallyCertified: !!c.wifeNationallyCertified,
          fromStep: c.wifeCurrentService||1,
          postCapGrowthPct: c.wifeSalaryGrowth||2.5,
          scale: wifeScale,
        }) : wSalary;
      } else if(wWork && age>c.currentAge){
        wSalary *= (1+wGrow);
      }
      // 401(k)/403(b) employee deferral limits historically rise ~$500/yr.
      // Escalate the user's contribution proportionally so projected savings
      // reflect the rising IRS limits rather than a flat nominal contribution.
      const limitEscalator = 1 + 500 * yearsElapsed / Math.max(1, c.annualContribution||23500);
      const yourContribY = (c.annualContribution||0) * limitEscalator;
      const wifeContribY = (c.wife403bContrib||0) * (1 + 500 * yearsElapsed / Math.max(1, c.wife403bContrib||23500));
      // Your 401(k) contributions scale with salary (employer match/core).
      // The core rate is resolved PER PROJECTED YEAR, not held flat: plans
      // that step the non-elective contribution at a service milestone were
      // being projected at the starting rate for the next forty years, which
      // quietly deletes the step from every balance after it. Same resolver
      // the Tax tab uses, so the two cannot disagree.
      const coreY = window.TaxPlanner.coreRateForYear({
        basePct: c.employerCorePct || 0,
        stepPct: c.employerCoreStepPct == null || c.employerCoreStepPct === '' ? null : c.employerCoreStepPct,
        stepYears: c.employerCoreStepYears,
        serviceStart: c.employerServiceStartDate,
        year: currentYear + yearsElapsed,
        periodsPerYear: 24,
      }).pct;
      const yourAnn = yWork ? (yourContribY
        + Math.min(yourContribY, ySalary*(c.employerMatchPct||0)/100)
        + Math.min(ySalary, c.employerCoreSalaryCap||0)*coreY/100) : 0;
      const wifeAnn = wWork ? (wifeContribY + (c.wife403bEmployerContrib||0)) : 0;
      // The redirect: school fees, the mortgage payment, 529 contributions
      // and dependent-kid costs each free their cashflow on a known date
      // years before retirement — and redirecting the freed money into the
      // brokerage is the single most powerful move this household has. Off
      // by default because it is a commitment, not a fact; the Decision
      // Levers card prices exactly what making it is worth.
      let redirected = 0;
      if(c.redirectFreedCashflow && yWork){
        if(age >= (c.schoolEndsAtAge||99)) redirected += c.schoolCostAnnual||0;
        if(age >= (c.mortgageEndsAtAge||99)) redirected += c.mortgageAnnual||0;
        if(age >= (c.plan529EndsAtAge||99)) redirected += c.plan529Annual||0;
        if(age >= (c.kidsIndependentAtAge||99)) redirected += c.dependentKidAnnual||0;
      }
      b401=b401*(1+r)+yourAnn;
      bIra=bIra*(1+r)+(yWork?c.yourIraContrib||0:0);
      b403=b403*(1+r)+wifeAnn;
      wIra=wIra*(1+r)+(wWork?c.wifeIraContrib||0:0);
      yRoth=yRoth*(1+r)+(yWork?c.yourRothContrib||0:0);
      wRoth=wRoth*(1+r)+(wWork?c.wifeRothContrib||0:0);
      ukPen=ukPen*(1+r); // UK retirement is a preserved investment account — no new contributions
      brok=brok*(1+r)+(yWork?c.brokerageContrib||0:0)+redirected;
      // The UK pension is reported apart from the US pre-tax pile because the
      // drawdown treats them differently: no RMD ever forces money out of it
      // and no conversion can touch it.
      const preTaxUs=Math.max(0,b401+bIra+b403+wIra);
      const rothBal=Math.max(0,yRoth+wRoth);
      const total=Math.max(0,preTaxUs+ukPen+rothBal+brok);
      const pension=wAge>=(c.wifeRetireAge||55)?wifePensionAnnual:0;
      const ySs=!yWork&&age>=(c.yourSsClaimAge||c.ssStartAge||67)?(c.yourSsMonthly||0)*12:0;
      const wSs=!wWork&&wAge>=(c.wifeSsClaimAge||c.ssStartAge||67)?(c.wifeSsMonthly||0)*12:0;
      data.push({age,wAge,portfolio:Math.round(total),
        taxable:Math.round(brok), preTax:Math.round(preTaxUs), ukPen:Math.round(ukPen), roth:Math.round(rothBal),
        saved:Math.round(yourAnn+wifeAnn+(yWork?(c.brokerageContrib||0)+(c.yourIraContrib||0)+(c.yourRothContrib||0):0)+(wWork?(c.wifeIraContrib||0)+(c.wifeRothContrib||0):0)+redirected),
        redirected:Math.round(redirected),
        income:Math.round(pension+ySs+wSs),ySalary:Math.round(ySalary),wSalary:Math.round(wSalary)});
    }
    return data;
  },[wifePensionAnnual, wifeScale, currentYear]);
  const projection = useMemo(()=>accumulate(cfg),[accumulate, cfg]);

  const retirePoint    = projection[projection.length-1]||{portfolio:0,income:0,taxable:0,preTax:0,ukPen:0,roth:0};
  const yearsToRetire  = Math.max(0, cfg.retireAge - cfg.currentAge);

  // Social Security estimate from salary (simplified SSA formula)
  const estimateSS = salary => {
    if(!salary) return 0;
    const aime = salary / 12;
    const bp1=1115, bp2=6721;
    let pia = 0.90*Math.min(aime,bp1);
    if(aime>bp1) pia += 0.32*Math.min(aime-bp1, bp2-bp1);
    if(aime>bp2) pia += 0.15*(aime-bp2);
    return Math.round(pia);
  };
  const estYourSs = estimateSS(cfg.salary);
  const estWifeSs = estimateSS(cfg.wifeSalary);

  // Project FAE forward. This was a second, independently written copy of the
  // precedence rule above — the two agreed, but only by hand, and the pension
  // headline and the scenario table would have silently disagreed the moment
  // one was edited. One implementation now, shared by both callers.
  const projectFae = wifeFaeAt;

  // Rule of 30: years from NOW until wife accumulates 30 CREDITABLE service years.
  const wifeYearsToRule30 = Math.max(0, 30 - wifeCreditableNow);
  const wifeAgeAtRule30 = (cfg.wifeAge||44) + wifeYearsToRule30;
  const wifeFaeAtRule30 = projectFae(wifeYearsToRule30);
  // At 30 yrs the pension is full (no early-retirement reduction). Survivor
  // election still reduces the member's own monthly amount.
  const wifePensionRule30Gross = Math.round(seppGrossPension(wifeFaeAtRule30, 30, 0));
  const wifePensionRule30 = Math.round(wifePensionRule30Gross * (1 - wifeSurvivorOpt.pensionReductionPct/100));

  // Eligibility scenarios across multiple retirement ages — the core "when
  // should she retire" question. For each candidate exit age compute the
  // service years, eligibility result, pension after early-retirement and
  // survivor reductions, healthcare share, and total annual value.
  const wifeScenarios = useMemo(() => {
    const candidates = [55, 57, 60, 62, 65];
    return candidates.map(targetAge => {
      const yrsFromNow = Math.max(0, targetAge - (cfg.wifeAge||44));
      const yos = Math.max(0, wifeCreditableNow + yrsFromNow);
      const elig = seppEligibility(seppEraVal, yos, targetAge);
      const fae = projectFae(yrsFromNow);
      const gross = seppGrossPension(fae, yos, 0);
      const afterEarly = gross * (1 - elig.reductionPct/100);
      const afterSurvivor = afterEarly * (1 - wifeSurvivorOpt.pensionReductionPct/100);
      const healthShare = seppHealthShare(seppHealthEraVal, yos);
      return {
        age: targetAge,
        year: new Date().getFullYear() + yrsFromNow,
        yos: Math.round(yos*10)/10,
        eligible: elig.eligible,
        eligLabel: elig.label,
        reductionPct: elig.reductionPct,
        fae,
        grossAnnual: Math.round(gross),
        annual: Math.round(afterSurvivor),
        monthly: Math.round(afterSurvivor/12),
        healthShare,
      };
    });
  }, [cfg, wifeCreditableNow, seppEraVal, seppHealthEraVal, wifeSurvivorOpt, wifeHasCas, wifeYrsSinceCas, wifeScale]);

  // ── Early Retirement + Contract Work calculator ──────────────────────────
  // "If she retires at 25 yrs instead of 30, how much contract income would
  // she need to earn during those 5 'gap' years to come out at least net
  // zero on lifetime household value?"
  //
  // Bookkeeping for two scenarios over the 5-yr window (gap = fullYrs - earlyYrs)
  // followed by lifetime pension collection through expected death:
  //
  //   Scenario A — Work full 30 yrs:
  //     Yrs 1..gap: salary + employer 403b contribution, employer healthcare
  //     Yrs gap+1..death: P30 (full unreduced pension, larger FAE)
  //
  //   Scenario B — Retire at 25 yrs + contract:
  //     Yrs 1..gap: contract income (minus SE tax incremental) + P25 (early)
  //                 minus DSHBP retiree health premium
  //     Yrs gap+1..death: P25 forever (smaller — fewer service yrs, lower FAE,
  //                       and the 0.2%/mo early-retirement reduction for being
  //                       short of 30 yrs)
  //
  // Break-even contract income makes total lifetime household value equal.
  const earlyRetireScenario = useMemo(() => {
    const earlyYrs = Math.max(15, Math.min(29, cfg.earlyRetireYrs || 25));
    const fullYrs  = Math.max(earlyYrs + 1, cfg.wifeYearsService || 30);
    const gap = fullYrs - earlyYrs;
    // Years from today until each milestone (creditable service rate = 1 yr/calendar yr)
    const yrsToEarly = Math.max(0, earlyYrs - wifeCreditableNow);
    const yrsToFull  = Math.max(0, fullYrs - wifeCreditableNow);
    // Age at each milestone
    const ageAtEarly = (cfg.wifeAge||44) + yrsToEarly;
    const ageAtFull  = (cfg.wifeAge||44) + yrsToFull;
    // FAE at each milestone (CAS-anchored projection)
    const faeAtEarly = projectFae(yrsToEarly);
    const faeAtFull  = projectFae(yrsToFull);
    // Eligibility & reductions
    const eligEarly = seppEligibility(seppEraVal, earlyYrs, ageAtEarly);
    const eligFull  = seppEligibility(seppEraVal, fullYrs,  ageAtFull);
    // Gross + net pensions
    const grossEarly = seppGrossPension(faeAtEarly, earlyYrs, 0);
    const grossFull  = seppGrossPension(faeAtFull,  fullYrs,  0);
    const afterEarlyReduction = grossEarly * (1 - eligEarly.reductionPct/100);
    const afterFullReduction  = grossFull  * (1 - eligFull.reductionPct/100);
    const survFactor = 1 - wifeSurvivorOpt.pensionReductionPct/100;
    const netP25 = Math.round(afterEarlyReduction * survFactor);
    const netP30 = Math.round(afterFullReduction  * survFactor);
    const pensionDelta = netP30 - netP25; // permanent loss per year if early
    // Salary in the final-5 working years (rough avg of the gap window)
    const salaryDuringGap = Math.round((projectFae(yrsToEarly) + faeAtFull) / 2);
    const employerCtrib   = (cfg.wife403bEmployerContrib || 0);
    // Health: working = employer-paid (assume $0 to household); early retired = DSHBP premium
    const dshbpAnnual = Math.round((cfg.earlyRetireHealthMo || 650) * 12);
    // Self-employment tax: contractor pays both halves of FICA. Net cost vs W-2 ≈
    // 7.65% × contract on top of normal income tax.
    const seTaxIncremental = Math.round(((cfg.contractAnnual||0) - (cfg.contractExpenses||0)) * 0.0765);
    // Annual cash to household during gap, each scenario
    const annualWorking   = salaryDuringGap + employerCtrib; // pre-tax household value
    const annualEarlyRet  = (cfg.contractAnnual || 0) + netP25 - dshbpAnnual - seTaxIncremental;
    // Years of pension collection from full-retirement age until expected death (use 87 as
    // a midpoint between female median 85 and the model's terminal). This is the period
    // over which the pensionDelta accrues as a permanent loss.
    const expectedPensionYrs = Math.max(0, 87 - ageAtFull);
    // Break-even contract income: makes total lifetime household value equal.
    // Working:   5 × annualWorking + expectedPensionYrs × netP30
    // EarlyRet:  5 × (contract + netP25 - dshbpAnnual - seTaxOf(contract)) + (gap + expectedPensionYrs) × netP25
    // For B − A = 0 over the FULL horizon:
    //   gap × contract = gap × annualWorking + expectedPensionYrs × (netP30 - netP25)
    //                    − gap × netP25 + gap × dshbpAnnual + gap × seTax(contract approx)
    // We solve for contract approximating seTax(contract) at the current contract value,
    // then iterate once for accuracy (linear in contract).
    const lifetimeDeltaIfEqualGap = expectedPensionYrs * pensionDelta - gap * netP25 + gap * dshbpAnnual;
    // First pass (ignore SE tax dependency on contract)
    let breakEvenContract = salaryDuringGap + employerCtrib + lifetimeDeltaIfEqualGap / gap;
    // SE tax correction: contract income generates extra ~7.65% SE tax which must
    // also be replaced. Inflate break-even by 1/(1-0.0765).
    breakEvenContract = breakEvenContract / (1 - 0.0765);
    breakEvenContract = Math.max(0, Math.round(breakEvenContract));
    // Lifetime household total at the current contract assumption
    const lifetimeWorking  = gap * annualWorking  + expectedPensionYrs * netP30;
    const lifetimeEarlyRet = gap * annualEarlyRet + (gap + expectedPensionYrs) * netP25;
    const lifetimeDeficit  = lifetimeWorking - lifetimeEarlyRet;

    // Build chart series:
    // 1) Annual cash flow comparison during the gap (bar chart per year)
    const gapCashFlow = [];
    for (let i = 0; i < gap; i++) {
      const yr = new Date().getFullYear() + Math.round(yrsToEarly) + i;
      gapCashFlow.push({
        yr,
        working:   Math.round(annualWorking  * Math.pow(1 + (cfg.wifeSalaryGrowth||2.5)/100, i)),
        earlyRet:  Math.round(annualEarlyRet * Math.pow(1 + (cfg.wifeSalaryGrowth||2.5)/100, i)),
      });
    }
    // 2) Cumulative lifetime household value (line chart)
    const cumulative = [];
    let cumWork = 0, cumEarly = 0;
    const totalYrs = gap + Math.max(expectedPensionYrs, 1);
    for (let i = 0; i < totalYrs; i++) {
      const age = ageAtEarly + i;
      if (i < gap) {
        cumWork  += annualWorking;
        cumEarly += annualEarlyRet;
      } else {
        cumWork  += netP30;
        cumEarly += netP25;
      }
      cumulative.push({age: Math.round(age), work: Math.round(cumWork), early: Math.round(cumEarly), delta: Math.round(cumWork - cumEarly)});
    }
    // Year at which the cumulative lines cross (i.e., when the "deficit" begins
    // to dominate the contract-period gains, if ever)
    let crossoverAge = null;
    for (let i = 1; i < cumulative.length; i++) {
      const prev = cumulative[i-1], curr = cumulative[i];
      if (prev.delta < 0 && curr.delta >= 0) { crossoverAge = curr.age; break; }
      if (prev.delta > 0 && curr.delta <= 0) { crossoverAge = curr.age; break; }
    }

    return {
      earlyYrs, fullYrs, gap,
      yrsToEarly, yrsToFull, ageAtEarly, ageAtFull,
      faeAtEarly: Math.round(faeAtEarly), faeAtFull: Math.round(faeAtFull),
      eligEarlyLabel: eligEarly.label,
      eligFullLabel:  eligFull.label,
      netP25, netP30, pensionDelta,
      salaryDuringGap, employerCtrib, dshbpAnnual, seTaxIncremental,
      annualWorking, annualEarlyRet,
      expectedPensionYrs,
      breakEvenContract,
      lifetimeWorking: Math.round(lifetimeWorking),
      lifetimeEarlyRet: Math.round(lifetimeEarlyRet),
      lifetimeDeficit: Math.round(lifetimeDeficit),
      gapCashFlow,
      cumulative,
      crossoverAge,
    };
  }, [cfg, wifeCreditableNow, seppEraVal, wifeSurvivorOpt, wifeHasCas, wifeYrsSinceCas]);

  // Healthcare cliffs: when does each (15/17.5/20-yr) threshold land on the
  // calendar? Returned as Date objects so they can be plotted on the timeline.
  const wifeHealthCliffs = useMemo(() => {
    const cliffs = seppHealthCliffs(seppHealthEraVal);
    // Years FROM NOW to hit each cliff = cliff.yos - current creditable service.
    const today = new Date();
    return cliffs.map(c => {
      const yrsAway = c.yos - wifeCreditableNow;
      const date = new Date(today.getTime() + yrsAway * 365.25 * 24 * 3600 * 1000);
      return {
        ...c,
        yrsAway,
        date,
        ageAt: (cfg.wifeAge||44) + yrsAway,
        passed: yrsAway <= 0,
      };
    });
  }, [seppHealthEraVal, wifeCreditableNow, cfg.wifeAge]);

  // ── What retirement COSTS, and whether the money covers it ────────────────
  //
  // The old grade divided retirement income by final household salary and
  // called anything under 70% a failure. On this household that denominator
  // is a fiction: the salary it measures against is paying a mortgage that
  // clears before retirement, ~$60k of after-tax school fees that stop, 529
  // contributions that stop, and the retirement saving itself — none of which
  // is retirement spending. It graded a well-funded plan an F. Everything
  // below grades the plan against what it will actually cost to live, year by
  // year, after tax. See lib/retirementNeeds for the reasoning.
  const RN = window.RetirementNeeds;
  const observedSpendRow = useMemo(()=>RN.observedSpend(cashflow,{months:12}),[cashflow]);

  // One function builds the whole chain — accumulation, spending, need,
  // engine inputs — from a config object, so the Decision Levers card can
  // price an alternative FUTURE by patching the config and re-running the
  // real model, not a linearised copy of it. The page's own memos call it
  // with `cfg`; a lever calls it with `{...cfg, patch}`.
  const scenario = useCallback((c)=>{
    const proj = accumulate(c);
    const rp = proj[proj.length-1]||{portfolio:0,taxable:0,preTax:0,ukPen:0,roth:0};
    const yrsToRet = Math.max(0, (c.retireAge||65) - (c.currentAge||45));
    const ageWhenWifeRetires = (c.currentAge||0) + ((c.wifeRetireAge||55) - (c.wifeAge||44));
    // 529 contributions are SAVING, and the derived basis has already removed
    // every dollar of saving from the spending figure. Listing them again as
    // a cost that ends would remove them twice. On the observed basis they
    // are a real outflow Monarch saw, so there they belong in the ending
    // costs.
    const treat529 = c.spendBasis === 'observed' || c.spendBasis === 'manual';
    const spend = RN.currentSpending({
      grossHousehold: (c.salary||0)+(c.wifeSalary||0),
      preTaxDeferrals: (c.annualContribution||0)+(c.wife403bContrib||0),
      effectiveTaxPct: c.effectiveTaxPct,
      afterTaxSavings: (c.yourIraContrib||0)+(c.wifeIraContrib||0)
        +(c.yourRothContrib||0)+(c.wifeRothContrib||0)+(c.brokerageContrib||0)
        +(treat529?0:(c.plan529Annual||0)),
      observed: observedSpendRow,
      basis: c.spendBasis,
      manualAnnual: c.manualAnnualSpend,
    });
    const plan = RN.spendPlan({
      currentAge: c.currentAge, retireAge: c.retireAge, planToAge: c.planToAge||95,
      currentYear, inflationPct: c.inflationRate, currentSpend: spend.annual,
      realDeclinePct: c.realSpendDeclinePct,
      endingCosts: [
        {key:'school', label:'School fees', annual:c.schoolCostAnnual, endsAtAge:c.schoolEndsAtAge,
         note:'after tax — the biggest single line that simply stops'},
        {key:'mortgage', label:'Mortgage principal + interest', annual:c.mortgageAnnual, endsAtAge:c.mortgageEndsAtAge,
         note:'property tax, insurance and upkeep stay in the plan'},
        {key:'kids', label:'Dependent kid costs', annual:c.dependentKidAnnual, endsAtAge:c.kidsIndependentAtAge,
         note:'college is funded from the 529s, never from retirement income'},
        ...(treat529 ? [{key:'529', label:'529 contributions', annual:c.plan529Annual, endsAtAge:c.plan529EndsAtAge}] : []),
      ],
      retirementCosts: [
        {key:'healthBridge', label:'DSHBP retiree premium (you as her dependent)',
         annual:(c.earlyRetireHealthMo||650)*12, fromAge:c.retireAge, toAge:64,
         note:'the state plan is a cost, not the imputed "benefit" the old score counted as income'},
        {key:'healthMedicare', label:'Medicare B+D + DSHBP supplement',
         annual:(c.medicareHealthMo||350)*12, fromAge:65, toAge:c.planToAge||95,
         note:'the STANDARD premium — IRMAA surcharges are priced separately, per year, by the engine'},
        {key:'travel', label:'Travel / discretionary', annual:c.travelAnnual,
         fromAge:c.retireAge, toAge:c.travelUntilAge},
      ],
    });
    // Everything the engine needs: when each income stream starts, what is in
    // each tax bucket on the last day of work, and the conversion window.
    const yourClaim = c.yourSsClaimAge||c.ssStartAge||67;
    const wifeClaim = c.wifeSsClaimAge||c.ssStartAge||67;
    const inputs = {
      startAge: c.retireAge, planToAge: c.planToAge||95,
      startYear: currentYear + yrsToRet,
      spouseAgeOffset: (c.wifeAge||44)-(c.currentAge||45),
      inflationPct: c.inflationRate, returnPct: c.expectedReturn,
      yearsToStart: yrsToRet,
      needByAge: plan.byAge,
      // SEPP has no automatic COLA. Holding it flat in nominal terms while
      // the need inflates is not pessimism, it is the plan document. The
      // survivor election travels with it so a first-death scenario pays the
      // right fraction.
      pension: { annual: wifePensionAnnual, startAge: ageWhenWifeRetires, colaPct: 0,
                 survivorPct: wifeSurvivorOpt.survivorPct },
      // Per-person claim ages with the statutory actuarial factors — the FRA
      // monthly figures are adjusted DOWN for an early claim and UP for a
      // delayed one, permanently. startAge is in YOUR age scale.
      socialSecurity: [
        { label:'You', owner:'you',
          annualToday:(c.yourSsMonthly||0)*12*RN.ssAdjustmentFactor(yourClaim, 67),
          startAge: yourClaim },
        { label:'Wife', owner:'spouse',
          annualToday:(c.wifeSsMonthly||0)*12*RN.ssAdjustmentFactor(wifeClaim, 67),
          startAge: wifeClaim + ((c.currentAge||45)-(c.wifeAge||44)) },
      ],
      balances: {
        taxable: rp.taxable||0,
        taxableBasis: (rp.taxable||0) * (c.brokerageBasisPct==null?60:c.brokerageBasisPct)/100,
        preTax: rp.preTax||0,
        // The UK pension: ordinary income when drawn, no RMD, no conversion.
        preTaxNoRmd: rp.ukPen||0,
        roth: rp.roth||0,
      },
      conversion: { startAge: c.retireAge, endAge: c.conversionEndAge,
                    targetRatePct: c.conversionTargetRatePct,
                    magiCap: (c.irmaaMagiCap||0) > 0 ? c.irmaaMagiCap : null,
                    payTaxFromConversion: !!c.payTaxFromConversion },
      rmdAge: RN.rmdAgeFor(currentYear - (c.currentAge||45)),
      dividendYieldPct: c.dividendYieldPct,
      // Seeds the IRMAA lookback for premium years whose MAGI was earned
      // while still working: roughly gross less pre-tax deferrals.
      preRetirementMagi: (c.salary||0)+(c.wifeSalary||0)-(c.annualContribution||0)-(c.wife403bContrib||0),
    };
    return { proj, rp, spend, plan, inputs };
  },[accumulate, observedSpendRow, wifePensionAnnual, wifeSurvivorOpt, currentYear]);

  const baseScenario = useMemo(()=>scenario(cfg),[scenario, cfg]);
  const spendToday   = baseScenario.spend;
  const spendPlanOut = baseScenario.plan;
  const planInputs   = baseScenario.inputs;

  const funded      = useMemo(()=>RN.solveFundedRatio(planInputs),[planInputs]);
  const planRun     = funded.atPlan;
  const gradeInfo   = RN.gradeForFundedRatio(funded.ratio);
  const grade       = gradeInfo.grade;
  const gradeColor  = gradeInfo.color;
  const bridgeWindowEnd = Math.max(cfg.retireAge, cfg.conversionEndAge||74);
  const bridge      = useMemo(()=>RN.bridgeAnalysis(planRun.rows,{throughAge:bridgeWindowEnd}),[planRun, bridgeWindowEnd]);
  const bridgeTarget= useMemo(()=>RN.bridgeFundingTarget(planInputs,{throughAge:bridgeWindowEnd}),[planInputs, bridgeWindowEnd]);
  const bridgeGap   = useMemo(()=>RN.requiredMonthlySaving({
    target: bridgeTarget.atRetirement, current: cfg.brokerageBalance||0,
    years: yearsToRetire, returnPct: cfg.expectedReturn,
  }),[bridgeTarget, cfg.brokerageBalance, yearsToRetire, cfg.expectedReturn]);

  // ── Decision levers: what each move you could make NOW is worth ──────────
  //
  // Each lever is the full chain re-run — accumulation with the patched
  // config, the spending plan it implies, the drawdown engine, the funded
  // ratio — so a lever's delta carries every knock-on the real decision
  // would: saving more into the brokerage also LOWERS today's spending
  // (money saved is money not spent), which lowers the need, which raises
  // the ratio twice. A linearised sensitivity would miss exactly that.
  // Deferred so typing in an input stays responsive while eight engines
  // re-run in the background.
  const deferredCfg = React.useDeferredValue(cfg);
  const irmaaTier1 = useMemo(()=>RN.irmaaFor(0, currentYear, deferredCfg.inflationRate, 'mfj').tier1Threshold,
    [currentYear, deferredCfg.inflationRate]);
  const levers = useMemo(()=>{
    const solveFor = (c)=>RN.solveFundedRatio(scenario(c).inputs,{precision:0.005});
    const baseSc = scenario(deferredCfg);
    const base = RN.solveFundedRatio(baseSc.inputs,{precision:0.005}).ratio;
    if(base==null) return null;
    const freed = (deferredCfg.schoolCostAnnual||0)+(deferredCfg.mortgageAnnual||0)
      +(deferredCfg.plan529Annual||0)+(deferredCfg.dependentKidAnnual||0);
    const defs = [
      !deferredCfg.redirectFreedCashflow && freed>0 && {key:'redirect',
        label:'Redirect freed cashflow to the brokerage as each cost ends',
        detail:`school, mortgage, 529s and kid costs free ${fmtCur(freed)}/yr between now and retirement — route each into the brokerage the year it stops`,
        patch:{redirectFreedCashflow:true}},
      {key:'brok2k', label:'Save $2,000/mo into the brokerage starting now',
        detail:'also reduces today\'s spending by the same amount — the need falls as the bridge fills',
        patch:{brokerageContrib:(deferredCfg.brokerageContrib||0)+24000}},
      {key:'retire2', label:`Retire at ${(deferredCfg.retireAge||65)+2} instead of ${deferredCfg.retireAge||65}`,
        detail:'two more years of saving, two fewer years of spending, a shorter bridge',
        patch:{retireAge:(deferredCfg.retireAge||65)+2}},
      (deferredCfg.yourSsClaimAge||67)<70 && {key:'ss70', label:'Delay your Social Security to 70',
        detail:`${(RN.ssAdjustmentFactor(70,67)*100-RN.ssAdjustmentFactor(deferredCfg.yourSsClaimAge||67,67)*100).toFixed(0)} points more of FRA benefit, inflation-indexed for life`,
        patch:{yourSsClaimAge:70}},
      {key:'spend1k', label:'Spend $1,000/mo less in retirement',
        detail:'applied to the whole plan, not just the early years',
        // Switching to the manual basis makes the 529 line an ending cost
        // again, so it is added back here — the derived figure never
        // contained it, and without this the lever would cut spending twice.
        patch:{spendBasis:'manual', manualAnnualSpend: Math.max(0,(baseSc.spend.annual||0)
          + (deferredCfg.spendBasis!=='observed'&&deferredCfg.spendBasis!=='manual' ? (deferredCfg.plan529Annual||0) : 0) - 12000)}},
      (deferredCfg.travelAnnual||0)>0 && {key:'travel0', label:'Drop the travel budget entirely',
        detail:`${fmtCur(deferredCfg.travelAnnual)}/yr to age ${deferredCfg.travelUntilAge} — the one discretionary line`,
        patch:{travelAnnual:0}},
      !(deferredCfg.irmaaMagiCap>0) && {key:'irmaaCap', label:'Cap conversions below IRMAA tier 1',
        detail:`hold MAGI under ~${fmtCur(irmaaTier1)} from age 63 — smaller Roth, no Medicare surcharge`,
        patch:{irmaaMagiCap: irmaaTier1}},
      !deferredCfg.payTaxFromConversion && (deferredCfg.conversionTargetRatePct||0)>0 &&
        {key:'convSelfPay', label:'Pay conversion tax from the conversion itself (59½+)',
        detail:'second-best to brokerage-paid, but a window the brokerage can\'t fund otherwise converts nothing and rides the whole IRA into RMD-era rates',
        patch:{payTaxFromConversion:true}},
    ].filter(Boolean);
    const rows = defs.map(d=>{
      const ratio = solveFor({...deferredCfg, ...d.patch}).ratio;
      return {...d, ratio, delta: ratio==null?null:Math.round((ratio-base)*100)/100};
    }).sort((a,b)=>(b.delta??-9)-(a.delta??-9));
    return { base, rows };
  },[scenario, deferredCfg, irmaaTier1]);

  // ── The survivor scenario: the widow's tax, priced ───────────────────────
  const survivorView = useMemo(()=>{
    const atAge = Math.min((cfg.planToAge||95)-5, Math.max(cfg.retireAge+5, 80));
    return { atAge, ...RN.survivorAnalysis(planInputs, { atAge }) };
  },[planInputs, cfg.retireAge, cfg.planToAge]);

  // ── Monte Carlo: run on demand, flag when stale ──────────────────────────
  // 300 engine runs is real work; it happens on a click, not on every
  // keystroke, and the card says when the stored result no longer matches
  // the inputs on screen.
  const [mc, setMc] = useState(null);
  const mcKey = useMemo(()=>JSON.stringify([planInputs.balances, planInputs.startAge,
    planInputs.planToAge, spendPlanOut.needTodayDollars, cfg.mcStdevPct, cfg.expectedReturn]),
    [planInputs, spendPlanOut, cfg.mcStdevPct, cfg.expectedReturn]);
  const runMonteCarlo = ()=>{
    setMc({ key: mcKey, res: RN.monteCarlo(planInputs, { runs: 300, stdevPct: cfg.mcStdevPct||12, seed: 42 }) });
  };
  const mcStale = mc && mc.key !== mcKey;

  // ── The year-by-year plan: every year from today to the last, with the
  //    milestones that change what the money is doing ──────────────────────
  const yearPlan = useMemo(()=>{
    const events = {};
    const add = (age, txt)=>{ if(age==null||age<cfg.currentAge||age>(cfg.planToAge||95)) return;
      (events[Math.round(age)] = events[Math.round(age)]||[]).push(txt); };
    add(cfg.schoolEndsAtAge, `School fees end — ${fmtCur(cfg.schoolCostAnnual)}/yr freed`);
    add(cfg.plan529EndsAtAge, `529 contributions end — ${fmtCur(cfg.plan529Annual)}/yr freed`);
    add(cfg.mortgageEndsAtAge, `Mortgage clears — ${fmtCur(cfg.mortgageAnnual)}/yr freed`);
    add(cfg.kidsIndependentAtAge, 'Kids independent');
    add(yourAgeWhenWifeRetires, `Wife retires — SEPP pension ${fmtCur(wifePensionAnnual)}/yr starts, DSHBP covers you both`);
    add(60, 'Penalty-free access to 401(k)/IRA (59½)');
    add(cfg.retireAge, `You retire — conversion window opens (${cfg.conversionTargetRatePct}% bracket)`);
    add(63, 'IRMAA lookback begins — MAGI from here prices Medicare at 65');
    add(cfg.yourSsClaimAge||67, `Your Social Security starts (${Math.round(RN.ssAdjustmentFactor(cfg.yourSsClaimAge||67,67)*100)}% of FRA)`);
    add((cfg.wifeSsClaimAge||67)+((cfg.currentAge||45)-(cfg.wifeAge||44)), `Wife's Social Security starts (${Math.round(RN.ssAdjustmentFactor(cfg.wifeSsClaimAge||67,67)*100)}% of FRA)`);
    add(65, 'Medicare — you enroll');
    add(65+((cfg.currentAge||45)-(cfg.wifeAge||44)), 'Medicare — wife enrolls');
    add((cfg.conversionEndAge||74)+1, 'RMDs begin — the conversion window is closed');
    const saveRows = projection.slice(0,-1).map(d=>({
      phase:'save', age:d.age, year:currentYear+(d.age-cfg.currentAge),
      saved:d.saved, redirected:d.redirected, total:d.portfolio,
      taxable:d.taxable, deferred:(d.preTax||0)+(d.ukPen||0), roth:d.roth,
      events:events[d.age]||[],
    }));
    const drawRows = planRun.rows.map(r=>({
      phase: r.conversion>0?'convert':(r.rmd>0?'rmd':'draw'),
      age:r.age, year:r.year, need:r.need, tax:r.tax, conversion:r.conversion,
      rmd:r.rmd, irmaa:r.irmaa, total:r.total, shortfall:r.shortfall,
      taxable:r.balances.taxable, deferred:r.balances.preTax+r.balances.preTaxNoRmd, roth:r.balances.roth,
      events:events[r.age]||[],
    }));
    return [...saveRows, ...drawRows];
  },[projection, planRun, cfg, currentYear, yourAgeWhenWifeRetires, wifePensionAnnual]);
  const [showAllYears, setShowAllYears] = useState(false);

  // One trajectory, two phases: accumulation to the last day of work, then the
  // drawdown the grade is computed from. The chart used to be drawn by its own
  // 4%-rule loop, which is how a page ends up with a picture that disagrees
  // with its own headline. Split by tax bucket so the conversion window is
  // visible as what it is — purple becoming green.
  const trajectory = useMemo(()=>[
    ...projection.map(d=>({age:d.age, portfolio:d.portfolio, taxable:d.taxable, preTax:d.preTax, ukPen:d.ukPen, roth:d.roth})),
    ...planRun.rows.slice(1).map(r=>({age:r.age, portfolio:r.total,
      taxable:r.balances.taxable, preTax:r.balances.preTax, ukPen:r.balances.preTaxNoRmd, roth:r.balances.roth})),
  ],[projection, planRun]);

  // First retirement year, for the headline cards.
  const firstRetYear = planRun.rows[0] || {};
  const firstRmdRow  = planRun.rows.find(r=>(r.rmd||0)>0) || null;
  const needAtRetirement = spendPlanOut.needAtRetirement;
  const guaranteedFirstYear = (firstRetYear.pension||0) + (firstRetYear.socialSecurity||0);

  // Healthcare bridge analysis
  const wifeRule30YourAge = cfg.currentAge + Math.max(0, 30 - (cfg.wifeCurrentService||10));
  const yourAgeWhenWifeRetires2 = cfg.currentAge + Math.max(0, (cfg.wifeRetireAge||55) - (cfg.wifeAge||44));
  const healthcareBridgeYears = Math.max(0, 65 - cfg.retireAge); // years you need non-Medicare coverage
  const DSHBP_COUPLE_MONTHLY = 650;  // est. DSHBP retiree couple premium
  const COBRA_MONTHLY = 1800;        // est. COBRA/marketplace couple premium
  const healthcareSavingsVsCobra = Math.round(healthcareBridgeYears * 12 * (COBRA_MONTHLY - DSHBP_COUPLE_MONTHLY));

  // Action timeline
  const actionTimeline = useMemo(()=>{
    const curYear = new Date().getFullYear();
    const yr = n => curYear + n;
    const events = [];
    const iraLimit = (cfg.currentAge||45) >= 50 ? 8500 : 7500;
    const empLimit = (cfg.currentAge||45) >= 60 && (cfg.currentAge||45) <= 63 ? 34750
                   : (cfg.currentAge||45) >= 50 ? 31000 : 23500;

    // NOW
    events.push({age:cfg.currentAge, year:curYear, phase:'now', icon:'📍', label:'Right Now', color:'#3b82f6', actions:[
      `Max 401(k) to ${fmtCur(empLimit)}/yr (${((empLimit/cfg.salary)*100).toFixed(1)}% of salary)`,
      `Backdoor Roth IRA: ${fmtCur(iraLimit)} each — ${fmtCur(iraLimit*2)}/yr combined via non-deductible trad → convert annually`,
      `Max wife's 403(b) to ${fmtCur((cfg.wifeAge||44)>=50?31000:23500)}/yr`,
      `Target 3–6 month emergency fund (~${fmtCur(Math.round((cfg.salary+cfg.wifeSalary)/12*4))})`,
      bridgeGap && !bridgeGap.alreadyThere
        ? `START THE BRIDGE: ~${fmtCur(bridgeGap.monthly)}/mo into a taxable brokerage toward the ${fmtCur(bridgeTarget.atRetirement)} target — it funds ${cfg.retireAge}–${bridgeWindowEnd} AND pays every Roth conversion's tax`
        : `Bridge account on track — projected ${fmtCur(retirePoint.taxable||0)} vs ${fmtCur(bridgeTarget.atRetirement)} needed at ${cfg.retireAge}`,
      // Modern glidepath: "110 minus age" is more realistic for 20+ yr horizons
      // than the 1970s-era "100 minus age" rule (which leaves too little equity).
      (()=>{
        const equity = Math.max(30, Math.min(95, 110-(cfg.currentAge||45)));
        return `Review 401(k) investment mix — at ${cfg.currentAge} target ~${equity}% equities / ${100-equity}% bonds (110-age rule; target-date fund handles this automatically)`;
      })(),
    ]});

    // ── SEPP-specific milestones (Delaware pension plan) ──
    // Use creditable service (already adjusted for gap), not raw years.
    // Healthcare subsidy cliffs are the highest-leverage retirement-timing
    // levers — surface each one as a discrete decision point.
    wifeHealthCliffs.filter(c => !c.passed && c.yrsAway < 25).forEach(c => {
      const yourAgeAtCliff = (cfg.currentAge||45) + c.yrsAway;
      events.push({
        age: yourAgeAtCliff,
        year: yr(c.yrsAway),
        phase: 'health-cliff',
        icon: c.share===100?'🏥':'🩺',
        label: `Wife hits ${c.yos} yrs service → ${c.share}% state-paid retiree health (her ${c.ageAt.toFixed(0)}, you ${yourAgeAtCliff.toFixed(0)})`,
        color: c.share===100?'#10b981':c.share>=75?'#3b82f6':'#8b5cf6',
        actions: [
          `Date: ${fmtMonthYear(c.date)} — ${c.yrsAway.toFixed(1)} yrs from today`,
          `State now pays ${c.share}% of monthly health premium for retirees who exit at/after this point`,
          c.share===100
            ? `This is the top cliff — retiring even one month earlier costs the 100% subsidy for life`
            : `Retiring before this date locks in only ${seppHealthShare(seppHealthEraVal, c.yos-0.1)}% subsidy — worth thousands/yr through age 65`,
          `Pre-Medicare savings vs. COBRA (~${fmtCur((COBRA_MONTHLY-DSHBP_COUPLE_MONTHLY)*12)}/yr) compound to age 65`,
        ],
      });
    });

    // Wife hits 30 yrs of credited service (any-age full pension)
    const wR30 = Math.max(0, 30 - wifeCreditableNow);
    if(wR30 > 0){
      const yourAgeR30 = (cfg.currentAge||45) + wR30;
      events.push({age:yourAgeR30, year:yr(wR30), phase:'wife-milestone', icon:'🎯', label:`Wife reaches 30 creditable yrs (her ${(cfg.wifeAge||44)+wR30}, you ${yourAgeR30.toFixed(0)})`, color:'#8b5cf6', actions:[
        `SEPP 30-year rule: eligible to retire at any age with full (un-reduced) pension`,
        `Net pension at 30 yrs: ${fmtCur(wifePensionRule30)}/yr (${fmtCur(Math.round(wifePensionRule30/12))}/mo) — after ${wifeSurvivorOpt.survivorPct}% survivor election`,
        `Healthcare: ${seppHealthShare(seppHealthEraVal, 30)}% state-paid premium at 30 yrs of service`,
        `DSHBP retiree coverage — covers you as a dependent (saves vs. COBRA in pre-Medicare years)`,
        `Decision: retire now vs. continue for larger FAE/service at ${cfg.wifeYearsService||30} yr target`,
        `If retiring now: ${fmtCur(wifePensionRule30)}/yr pension vs ${fmtCur(wifePensionAnnual)}/yr at planned age ${cfg.wifeRetireAge}`,
      ]});
    }

    // Wife retires (if different from Rule of 30)
    const wifeRetDelta = (cfg.wifeRetireAge||55) - (cfg.wifeAge||44);
    if(wifeRetDelta > 0 && Math.abs(wifeRetDelta - wR30) > 1){
      const yourAgeWR = (cfg.currentAge||45) + wifeRetDelta;
      events.push({age:yourAgeWR, year:yr(wifeRetDelta), phase:'wife-retire', icon:'🏛️', label:`Wife retires at ${cfg.wifeRetireAge} (you ${yourAgeWR})`, color:'#10b981', actions:[
        `SEPP pension begins: ${fmtCur(wifePensionAnnual)}/yr net (${wifeEligAtRetire.label})`,
        `DSHBP retiree health: wife + dependents — estimated ${fmtCur(DSHBP_COUPLE_MONTHLY*12)}/yr vs ${fmtCur(COBRA_MONTHLY*12)}/yr COBRA`,
        yourAgeWR < cfg.retireAge ? `You still working — household income now your salary + ${fmtCur(wifePensionAnnual)}/yr pension` : null,
        `Begin Roth conversion window: lower household income = lower bracket`,
        `Wife's 403(b) can stay invested — no withdrawal required yet (no RMDs until 73)`,
        `Review beneficiary designations on all accounts`,
      ].filter(Boolean)});
    }

    // ── The redirect moments: costs that end while still working ──
    // Each is a raise nobody has to ask for, and redirecting it is the
    // decision that builds the bridge. Dated so it lands on the calendar.
    [
      {endAge: cfg.schoolEndsAtAge, amt: cfg.schoolCostAnnual, what: 'School fees end', icon:'🎓'},
      {endAge: cfg.plan529EndsAtAge, amt: cfg.plan529Annual, what: '529 contributions end', icon:'🎓'},
      {endAge: cfg.mortgageEndsAtAge, amt: cfg.mortgageAnnual, what: 'Mortgage clears', icon:'🏠'},
      {endAge: cfg.kidsIndependentAtAge, amt: cfg.dependentKidAnnual, what: 'Kids independent', icon:'👨‍👩‍👧'},
    ].filter(e=>(e.amt||0)>0 && e.endAge>cfg.currentAge && e.endAge<cfg.retireAge).forEach(e=>{
      events.push({age:e.endAge, year:yr(e.endAge-cfg.currentAge), phase:'redirect', icon:e.icon,
        label:`${e.what} — ${fmtCur(e.amt)}/yr freed (age ${e.endAge})`, color:'#06b6d4', actions:[
        cfg.redirectFreedCashflow
          ? `Redirect plan is ON: this ${fmtCur(e.amt)}/yr flows to the brokerage automatically in the projection`
          : `Redirect ${fmtCur(Math.round(e.amt/12))}/mo into the brokerage — lifestyle inflation here is the silent alternative`,
        `${cfg.retireAge-e.endAge} years of growth before retirement turns it into ~${fmtCur(Math.round(e.amt*((Math.pow(1+(cfg.expectedReturn||7)/100,cfg.retireAge-e.endAge)-1)/((cfg.expectedReturn||7)/100))))}`,
      ]});
    });

    // 59½ — the penalty gate opens (only matters if it opens before retirement)
    if(cfg.retireAge > 60 && 60 > cfg.currentAge){
      events.push({age:60, year:yr(60-cfg.currentAge), phase:'access', icon:'🔓',
        label:'Penalty-free 401(k)/IRA access (59½)', color:'#64748b', actions:[
        'All pre-tax and Roth money is now reachable without the 10% penalty — the bridge no longer has to carry everything alone',
      ]});
    }

    // IRMAA lookback opens two years before Medicare prices the first premium
    if(63 > cfg.currentAge){
      events.push({age:63, year:yr(63-cfg.currentAge), phase:'irmaa', icon:'🩺',
        label:'IRMAA lookback begins (age 63)', color:'#f59e0b', actions:[
        `MAGI from this year forward sets Medicare premiums at 65 — tier 1 starts at ~${fmtCur(irmaaTier1)} MFJ`,
        cfg.irmaaMagiCap>0
          ? `MAGI cap is set at ${fmtCur(cfg.irmaaMagiCap)} — conversions are held under it from here`
          : 'No MAGI cap set — the Decision Levers card prices what capping conversions below tier 1 would cost/save',
        'The cliff is per-dollar: $1 over a threshold buys the whole tier for BOTH of you for a year',
      ]});
    }

    // 5 years pre-retirement
    const preRetDelta = (cfg.retireAge||65) - (cfg.currentAge||45) - 5;
    if(preRetDelta > 0){
      events.push({age:(cfg.currentAge||45)+preRetDelta, year:yr(preRetDelta), phase:'pre-retire', icon:'⚙️', label:`5 years to your retirement (age ${(cfg.retireAge||65)-5})`, color:'#f59e0b', actions:[
        `Shift 401(k) allocation: target ~40% equities / 60% fixed income by retirement`,
        `Build 2-year cash/bond ladder (~${fmtCur(needAtRetirement*2)} — two years of actual spending) for sequence-of-returns buffer`,
        `Finalize SS strategy: you at ${cfg.yourSsClaimAge||67}, wife at ${cfg.wifeSsClaimAge||67} — re-check the delay-to-70 lever before locking it`,
        cfg.retireAge < 65 ? `Plan Medicare gap: DSHBP dependent coverage through wife's pension` : `Enroll Medicare Part B 3 months before 65th birthday`,
        `Model Roth conversion amounts to fill the ${cfg.conversionTargetRatePct}% bracket before RMDs`,
        `Check pension/401(k) beneficiary designations`,
      ]});
    }

    // Your retirement
    const yourRetDelta = (cfg.retireAge||65) - (cfg.currentAge||45);
    events.push({age:cfg.retireAge, year:yr(yourRetDelta), phase:'your-retire', icon:'🎉', label:`You retire (age ${cfg.retireAge})`, color:'#10b981', actions:[
      `Portfolio ~${fmtCur(retirePoint.portfolio)} against a plan that costs ${fmtCur(needAtRetirement)}/yr — funded ratio ${(funded.ratio||0).toFixed(2)}×`,
      cfg.retireAge < 65 ? `Healthcare: enroll as dependent on wife's DSHBP — saves ~${fmtCur(healthcareSavingsVsCobra)} vs COBRA over ${healthcareBridgeYears} yrs` : `Enroll Medicare Part A (free) + Part B (~$2,100/yr)`,
      (cfg.yourSsClaimAge||67) > cfg.retireAge ? `Bridge to SS at ${cfg.yourSsClaimAge||67} using the brokerage — the ${(cfg.yourSsClaimAge||67) - cfg.retireAge} yr wait is what the bridge target is sized for` : `File for Social Security`,
      `Withdrawal order: taxable accounts first → Traditional 401k/IRA → Roth last`,
      `Continue Roth conversions up to top of your tax bracket before RMDs`,
    ]});

    // Medicare at 65 (if retiring before)
    if((cfg.retireAge||65) < 65){
      const medDelta = 65 - (cfg.currentAge||45);
      events.push({age:65, year:yr(medDelta), phase:'medicare', icon:'🏥', label:'Medicare eligible (age 65)', color:'#6366f1', actions:[
        `Enroll Medicare Part A (hospital — free if 40+ work quarters)`,
        `Enroll Medicare Part B (~$2,100/yr premium in 2026)`,
        `Choose: Medicare Supplement (Medigap) or Medicare Advantage`,
        `Part D prescription coverage`,
        `Wife enrolls when she turns 65 — DSHBP becomes secondary to Medicare`,
        `DSHBP Medicare supplement plan bridges gaps`,
      ]});
    }

    // SS claiming — one event per person, at each one's own claim age
    const yourClaim = cfg.yourSsClaimAge||cfg.ssStartAge||67;
    const wifeClaim = cfg.wifeSsClaimAge||cfg.ssStartAge||67;
    const wifeClaimYourAge = wifeClaim + ((cfg.currentAge||45)-(cfg.wifeAge||44));
    if(yourClaim > cfg.currentAge){
      const f = RN.ssAdjustmentFactor(yourClaim, 67);
      events.push({age:yourClaim, year:yr(yourClaim-(cfg.currentAge||45)), phase:'ss', icon:'💵',
        label:`Your Social Security begins (age ${yourClaim})`, color:'#f59e0b', actions:[
        `${fmtCur(Math.round((cfg.yourSsMonthly||0)*f))}/mo — ${Math.round(f*100)}% of your FRA benefit, inflation-indexed for life`,
        yourClaim<70 ? `Each further year of delay adds 8% — the Decision Levers card prices waiting to 70` : `Maximum benefit — no credit accrues past 70`,
        `This is also the check the survivor keeps: your claim age is her longevity insurance`,
      ]});
    }
    if(wifeClaimYourAge > cfg.currentAge && wifeClaimYourAge !== yourClaim){
      const f = RN.ssAdjustmentFactor(wifeClaim, 67);
      events.push({age:wifeClaimYourAge, year:yr(wifeClaimYourAge-(cfg.currentAge||45)), phase:'ss', icon:'💵',
        label:`Wife's Social Security begins (her ${wifeClaim}, you ${wifeClaimYourAge})`, color:'#f59e0b', actions:[
        `${fmtCur(Math.round((cfg.wifeSsMonthly||0)*f))}/mo — ${Math.round(f*100)}% of her FRA benefit`,
        `Delaware pays FICA, so her teaching years count fully — no WEP/GPO offset applies since repeal`,
      ]});
    }

    // RMDs at the age this birth year actually faces (75 for anyone born 1960+)
    const rmdAge = RN.rmdAgeFor(curYear - (cfg.currentAge||45));
    if(rmdAge > cfg.currentAge){
      const firstRmd = planRun.rows.find(r=>(r.rmd||0)>0);
      events.push({age:rmdAge, year:yr(rmdAge-(cfg.currentAge||45)), phase:'rmd', icon:'⚠️', label:`RMDs begin (age ${rmdAge} — SECURE 2.0)`, color:'#ef4444', actions:[
        firstRmd
          ? `Projected first RMD: ${fmtCur(firstRmd.rmd)} from ~${fmtCur(planRun.preTaxAtRmd||0)} still pre-tax — this stacks on pension + SS whether wanted or not`
          : `Projected pre-tax balance at ${rmdAge}: ${fmtCur(planRun.preTaxAtRmd||0)} — the conversion window's job is to shrink this`,
        `The UK pension is exempt — no RMD ever forces money out of it`,
        `Roth accounts: no RMDs — preserve for legacy or late-life spending`,
        `Qualified Charitable Distribution (QCD): donate RMD directly to charity tax-free`,
      ]});
    }

    return events.sort((a,b)=>a.age-b.age);
  },[cfg, retirePoint, needAtRetirement, funded, planRun, bridgeGap, bridgeTarget, bridgeWindowEnd, irmaaTier1, wifePensionAnnual, wifePensionRule30, healthcareBridgeYears, wifeHealthCliffs, wifeCreditableNow, wifeEligAtRetire, wifeSurvivorOpt, seppHealthEraVal]);

  const handleSave = async()=>{
    try {
      await saveRetirementConfig(cfg);
      show('Retirement plan saved');
    } catch(e){ show(e.message,'error'); }
  };

  return (
    <RetCfgCtx.Provider value={{cfg, upd, updRaw}}>
    <div className="page">
      {Toast}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}>
        <div className="page-title" style={{marginBottom:0}}>Retirement Planner</div>
        <button className="btn-primary" onClick={handleSave}>Save Plan</button>
      </div>

      {/* ── Funded ratio — the plan graded against what it will COST ── */}
      <div className="card" style={{marginBottom:16,borderColor:gradeColor}}>
        <div style={{display:'flex',alignItems:'center',gap:20,flexWrap:'wrap'}}>
          <div style={{textAlign:'center',minWidth:64}}>
            <div style={{fontSize:52,fontWeight:900,color:gradeColor,lineHeight:1}}>{grade}</div>
            <div style={{fontSize:9,color:'#64748b',letterSpacing:'0.1em',textTransform:'uppercase',marginTop:4}}>Funded Ratio</div>
          </div>
          <div style={{flex:1,minWidth:200}}>
            <div style={{fontSize:13,fontWeight:600,marginBottom:6}}>
              {funded.ratio == null ? 'Enter your spending below to grade the plan'
                : funded.capped ? 'Funded far beyond the plan'
                : `${funded.ratio.toFixed(2)}× the plan's spending, to age ${cfg.planToAge||95}`}
              <span style={{color:'#64748b',fontWeight:400}}> — {gradeInfo.label}</span>
            </div>
            {/* The bar is scaled so 1.00 sits at the two-thirds mark: funded to
                the dollar is the middle of the picture, not the end of it. */}
            <div style={{background:'#1e2a3a',borderRadius:6,height:8,overflow:'hidden',marginBottom:8,position:'relative'}}>
              <div style={{width:`${Math.min((funded.ratio||0)/1.5*100,100)}%`,height:'100%',background:gradeColor,borderRadius:6,transition:'width 0.3s'}}/>
              <div style={{position:'absolute',left:'66.7%',top:0,bottom:0,width:1,background:'#64748b'}}/>
            </div>
            <div style={{display:'flex',gap:16,flexWrap:'wrap',fontSize:11,color:'#64748b'}}>
              <span>Retirement need <strong style={{color:'#e2e8f0'}}>{fmtCur(needAtRetirement)}/yr</strong> at {cfg.retireAge}</span>
              <span>({fmtCur(spendPlanOut.needTodayDollars)}/yr in today's money)</span>
              {funded.supported && <span>Supports <strong style={{color:'#e2e8f0'}}>{fmtCur(funded.supported)}/yr</strong></span>}
              {planRun.depletionAge && <span style={{color:'#ef4444'}}>Money runs out at {planRun.depletionAge}</span>}
            </div>
            <div style={{fontSize:10,color:'#475569',marginTop:6,lineHeight:1.5}}>
              Graded against spending, not salary: the mortgage, the school fees, the 529s
              and every dollar of retirement saving stop before this plan starts. A straight-line
              {' '}{cfg.expectedReturn}% return is assumed — 1.00 means funded exactly, with no room
              for a bad first decade, which is why it grades a C rather than an A.
            </div>
          </div>
        </div>
      </div>

      {/* Detection banners from Monarch accounts */}
      {(detectedAccounts.total401k||detected401kContrib||detectedAccounts.wife403b||detectedAccounts.yourIra||detectedAccounts.wifeIra||detectedAccounts.yourRoth||detectedAccounts.wifeRoth||detectedAccounts.ukPension||detectedAccounts.brokerage)&&(
        <div style={{display:'flex',gap:10,flexWrap:'wrap',marginBottom:16}}>
          {detectedAccounts.total401k&&<div style={{background:'rgba(59,130,246,0.1)',border:'1px solid rgba(59,130,246,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Your 401(k): </span><span style={{fontWeight:700,color:'#3b82f6'}}>{fmtCur(detectedAccounts.total401k)}</span></div>}
          {detected401kContrib&&<div style={{background:'rgba(59,130,246,0.1)',border:'1px solid rgba(59,130,246,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">401(k) contrib/yr: </span><span style={{fontWeight:700,color:'#3b82f6'}}>{fmtCur(detected401kContrib)}</span></div>}
          {detectedAccounts.wife403b&&<div style={{background:'rgba(139,92,246,0.1)',border:'1px solid rgba(139,92,246,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Wife 403(b): </span><span style={{fontWeight:700,color:'#8b5cf6'}}>{fmtCur(detectedAccounts.wife403b)}</span></div>}
          {detectedAccounts.yourIra&&<div style={{background:'rgba(16,185,129,0.1)',border:'1px solid rgba(16,185,129,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Your Trad IRA: </span><span style={{fontWeight:700,color:'#10b981'}}>{fmtCur(detectedAccounts.yourIra)}</span></div>}
          {detectedAccounts.wifeIra&&<div style={{background:'rgba(16,185,129,0.1)',border:'1px solid rgba(16,185,129,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Wife Trad IRA: </span><span style={{fontWeight:700,color:'#10b981'}}>{fmtCur(detectedAccounts.wifeIra)}</span></div>}
          {detectedAccounts.yourRoth&&<div style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Your Roth IRA: </span><span style={{fontWeight:700,color:'#f59e0b'}}>{fmtCur(detectedAccounts.yourRoth)}</span></div>}
          {detectedAccounts.wifeRoth&&<div style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Wife Roth IRA: </span><span style={{fontWeight:700,color:'#f59e0b'}}>{fmtCur(detectedAccounts.wifeRoth)}</span></div>}
          {detectedAccounts.ukPension&&<div style={{background:'rgba(99,102,241,0.1)',border:'1px solid rgba(99,102,241,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">UK Retirement: </span><span style={{fontWeight:700,color:'#6366f1'}}>{fmtCur(detectedAccounts.ukPension)}</span></div>}
          {detectedAccounts.brokerage&&<div style={{background:'rgba(6,182,212,0.1)',border:'1px solid rgba(6,182,212,0.25)',borderRadius:8,padding:'8px 14px',fontSize:12}}><span className="muted">Taxable brokerage: </span><span style={{fontWeight:700,color:'#06b6d4'}}>{fmtCur(detectedAccounts.brokerage)}</span></div>}
        </div>
      )}

      {/* ── What retirement will cost ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10,flexWrap:'wrap',gap:8}}>
          <div className="label" style={{marginBottom:0}}>What Retirement Will Cost</div>
          <div style={{display:'flex',gap:6,alignItems:'center'}}>
            <span style={{fontSize:10,color:'#64748b'}}>spending basis</span>
            {[['derived','Derived'],['observed','Monarch'],['manual','Manual']].map(([k,lbl])=>{
              const disabled = k==='observed' && !observedSpendRow;
              return (
                <button key={k} disabled={disabled}
                  onClick={()=>setCfg(c=>({...c,spendBasis:k}))}
                  style={{fontSize:10,padding:'3px 8px',borderRadius:6,cursor:disabled?'not-allowed':'pointer',
                    border:`1px solid ${spendToday.basis===k?'#10b981':'#334155'}`,
                    background:spendToday.basis===k?'rgba(16,185,129,0.12)':'transparent',
                    color:disabled?'#334155':spendToday.basis===k?'#10b981':'#94a3b8'}}>{lbl}</button>
              );
            })}
          </div>
        </div>
        <div className="grid-2" style={{gap:16,alignItems:'start'}}>
          <div>
            {/* The waterfall: today's outgoings, everything that stops, everything
                retirement adds. This is the whole argument against grading the
                plan on a fraction of final salary. */}
            {spendPlanOut.lines.map((l,idx)=>{
              const isBase = l.kind==='base';
              const color = isBase?'#e2e8f0':l.kind==='ends'?'#10b981':'#f59e0b';
              return (
                <div key={l.key} style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:10,
                  padding:'7px 0',borderBottom:idx===spendPlanOut.lines.length-1?'none':'1px solid #161b22'}}>
                  <div style={{minWidth:0}}>
                    <div style={{fontSize:12,color:isBase?'#e2e8f0':'#94a3b8',fontWeight:isBase?600:400}}>{l.label}</div>
                    <div style={{fontSize:10,color:'#475569',lineHeight:1.4}}>
                      {l.kind==='ends'&&(l.stillRunningAtRetirement
                        ? `still running at ${cfg.retireAge} — drops out at ${l.endsAtAge}`
                        : `stops at ${l.endsAtAge==null?'—':`age ${l.endsAtAge}`}, before you retire`)}
                      {l.kind==='adds'&&`added ages ${l.fromAge}–${l.toAge}`}
                      {l.note?`${l.kind==='base'?'':' · '}${l.note}`:''}
                    </div>
                  </div>
                  <span style={{fontWeight:600,color,fontSize:12,whiteSpace:'nowrap'}}>
                    {l.delta<0?'−':l.kind==='adds'?'+':''}{fmtCur(Math.abs(l.delta))}
                  </span>
                </div>
              );
            })}
            <div style={{display:'flex',justifyContent:'space-between',padding:'10px 0 0',fontWeight:700,fontSize:13,borderTop:'1px solid #1e2a3a',marginTop:6}}>
              <span>Retirement need, today's money</span>
              <span style={{color:'#10b981'}}>{fmtCur(spendPlanOut.needTodayDollars)}/yr</span>
            </div>
            <div style={{display:'flex',justifyContent:'space-between',padding:'4px 0 0',fontSize:11,color:'#64748b'}}>
              <span>…in {currentYear+yearsToRetire} dollars at {cfg.inflationRate}% inflation</span>
              <span>{fmtCur(needAtRetirement)}/yr</span>
            </div>
          </div>
          <div style={{display:'flex',flexDirection:'column',gap:10}}>
            <div style={{background:'rgba(16,185,129,0.07)',border:'1px solid rgba(16,185,129,0.2)',borderRadius:8,padding:'10px 12px',fontSize:11,lineHeight:1.6,color:'#94a3b8'}}>
              <strong style={{color:'#10b981'}}>{Math.round(spendPlanOut.shareOfCurrentSpend*100)}% of today's spending</strong>, not
              {' '}{Math.round(spendPlanOut.needTodayDollars/Math.max(1,(cfg.salary||0)+(cfg.wifeSalary||0))*100)}% of today's income.
              The two are different questions and only the first one is a plan: {fmtCur(spendPlanOut.endingTotal)}/yr of
              what the household pays for now has an end date that falls before retirement, and the saving that
              funds the plan is not spending the plan has to replace.
            </div>
            <div style={{fontSize:11,color:'#64748b',lineHeight:1.6}}>
              <div style={{marginBottom:4}}><strong style={{color:'#e2e8f0'}}>How today's spending was measured</strong></div>
              <div>Derived — gross {fmtCur(spendToday.components.gross)} less {fmtCur(spendToday.components.preTaxDeferrals)} deferred,
                less {fmtCur(spendToday.components.tax)} tax at {cfg.effectiveTaxPct}%, less {fmtCur(spendToday.components.afterTaxSavings)} saved
                = <strong style={{color:'#e2e8f0'}}>{fmtCur(spendToday.derived)}</strong></div>
              {observedSpendRow
                ? <div style={{marginTop:3}}>Monarch — {fmtCur(observedSpendRow.annual)}/yr from {observedSpendRow.months} months
                    ({observedSpendRow.from} → {observedSpendRow.to}){spendToday.divergencePct!=null?`, ${spendToday.divergencePct>0?'+':''}${spendToday.divergencePct}% vs derived`:''}</div>
                : <div style={{marginTop:3,color:'#475569'}}>Monarch — no cashflow synced, so the observed rung is unavailable</div>}
              <div style={{marginTop:6,color:'#475569'}}>
                The two are never averaged: one is an outflow rollup that cannot see a pre-tax deduction,
                the other an identity that cannot see a category. Pick the one you trust; the other stays on screen.
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* ── Retirement Outlook — headline results ── */}
      <div className="section-header">Retirement Outlook</div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(170px,1fr))',gap:12,marginBottom:12}}>
            <div className="card-sm"><div className="label">Portfolio at Your Retire</div><div style={{fontSize:18,fontWeight:700,color:'#10b981'}}>{fmtCur(retirePoint.portfolio)}</div></div>
            <div className="card-sm"><div className="label">Monthly Need at {cfg.retireAge}</div><div style={{fontSize:18,fontWeight:700}}>{fmtCur(Math.round(needAtRetirement/12))}</div><div style={{fontSize:9,color:'#64748b',marginTop:2}}>{fmtCur(Math.round(spendPlanOut.needTodayDollars/12))}/mo in today's money</div></div>
            <div className="card-sm"><div className="label">Wife's Monthly Pension</div><div style={{fontSize:18,fontWeight:700,color:'#3b82f6'}}>{fmtCur(Math.round(wifePensionAnnual/12))}</div></div>
            <div className="card-sm"><div className="label">Your Years to Retire</div><div style={{fontSize:18,fontWeight:700,color:'#f59e0b'}}>{yearsToRetire}</div></div>
            <div className="card-sm"><div className="label">Wife's Final Salary</div><div style={{fontSize:18,fontWeight:700,color:'#8b5cf6'}}>{fmtCur(wifeFinalSalary)}</div></div>
            <div className="card-sm"><div className="label">Wife Retires In</div><div style={{fontSize:18,fontWeight:700,color:'#8b5cf6'}}>{wifeYearsToRetire} yrs</div></div>
      </div>
      <div className="grid-3" style={{gap:12,marginBottom:12,alignItems:'start'}}>
          {/* First retirement year, as cash actually arrives.
              The old version of this card added Social Security at the retire
              age whether or not it had started — on this plan it starts seven
              years later — and counted a $12,000 "state healthcare benefit" as
              income. The benefit is real, but it is not money in: it shows up
              in this plan as a health premium of ~$650/mo instead of a
              marketplace one, which is a COST line in the spending plan. */}
          <div className="card">
            <div className="label" style={{marginBottom:12}}>Year One of Retirement (age {cfg.retireAge})</div>
            {[
              {label:"Wife's Pension (SEPP)", val:firstRetYear.pension||0, color:'#3b82f6',
               note: (firstRetYear.pension||0)>0 ? 'no COLA — flat for life' : `starts when she retires (you ${yourAgeWhenWifeRetires})`},
              {label:'Social Security (both)', val:firstRetYear.socialSecurity||0, color:'#f59e0b',
               note: (firstRetYear.socialSecurity||0)>0 ? 'claimed' : `starts at ${cfg.ssStartAge||67}`},
              {label:'Taxable brokerage', val:firstRetYear.fromTaxable||0, color:'#06b6d4', note:'the bridge account'},
              {label:'Pre-tax (401k/403b/IRA)', val:firstRetYear.fromPreTax||0, color:'#8b5cf6'},
              {label:'UK pension', val:firstRetYear.fromPreTaxNoRmd||0, color:'#6366f1', note:'no RMD, drawn after the US pile'},
              {label:'Roth', val:firstRetYear.fromRoth||0, color:'#10b981'},
            ].map(item=>(
              <div key={item.label} style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'6px 0',borderBottom:'1px solid #161b22'}}>
                <div style={{display:'flex',alignItems:'center',gap:8,minWidth:0}}>
                  <div style={{width:7,height:7,borderRadius:'50%',background:item.color,flexShrink:0}}/>
                  <span style={{fontSize:12,color:'#94a3b8'}}>{item.label}
                    {item.note&&<span style={{fontSize:9,color:'#475569',marginLeft:6}}>{item.note}</span>}</span>
                </div>
                <span style={{fontWeight:600,color:item.color,fontSize:12}}>{fmtCur(item.val)}</span>
              </div>
            ))}
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'6px 0',borderBottom:'1px solid #161b22'}}>
              <span style={{fontSize:12,color:'#94a3b8'}}>Income tax (fed + DE){(firstRetYear.earlyPenalty||0)>0?' + 10% early-withdrawal penalty':''}</span>
              <span style={{fontWeight:600,color:'#ef4444',fontSize:12}}>−{fmtCur(firstRetYear.tax||0)}</span>
            </div>
            {(planRun.earlyPenaltyTotal||0)>0&&(
              <div style={{fontSize:10,color:'#f59e0b',marginTop:6,lineHeight:1.5}}>
                Retiring at {cfg.retireAge} means pre-tax withdrawals before 59½: {fmtCur(planRun.earlyPenaltyTotal)} of
                §72(t) penalty is charged across the plan. The rule-of-55 exception (the plan of the employer you
                separate from) or a 72(t) SEPP ladder avoids it — but both are things to set up, not defaults.
              </div>
            )}
            <div style={{display:'flex',justifyContent:'space-between',padding:'8px 0 0',fontWeight:700,fontSize:13}}>
              <span>Spending funded</span>
              <span style={{color:'#10b981'}}>{fmtCur(firstRetYear.need||0)}</span>
            </div>
            <div style={{fontSize:10,color:'#475569',marginTop:6,lineHeight:1.5}}>
              Guaranteed income covers {needAtRetirement>0?Math.round(guaranteedFirstYear/needAtRetirement*100):0}% of
              year one; the portfolio covers the rest. Withdrawals come taxable-first so the
              conversion window below has bracket room to work in.
            </div>
          </div>

          {/* Portfolio trajectory chart */}
          <div className="card">
            <div className="label" style={{marginBottom:8}}>Portfolio Trajectory by Tax Bucket</div>
            <ResponsiveContainer width="100%" height={240}>
              <AreaChart data={trajectory} margin={{top:10,right:8,left:0,bottom:0}}>
                <XAxis dataKey="age" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}/>
                <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={v=>fmtCur(v)}/>
                <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
                <Tooltip formatter={v=>fmtCur(v)} labelFormatter={l=>`Age ${l}`} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                {yourAgeWhenWifeRetires>=cfg.currentAge&&<ReferenceLine x={yourAgeWhenWifeRetires} stroke="#3b82f6" strokeDasharray="4 2" label={{value:'wife',position:'insideTopRight',fontSize:9,fill:'#3b82f6'}}/>}
                <ReferenceLine x={cfg.retireAge} stroke="#10b981" strokeDasharray="4 2" label={{value:'you',position:'insideTopRight',fontSize:9,fill:'#10b981'}}/>
                {(cfg.conversionEndAge||74)+1<=(cfg.planToAge||95)&&<ReferenceLine x={(cfg.conversionEndAge||74)+1} stroke="#f59e0b" strokeDasharray="4 2" label={{value:'RMD',position:'insideTopRight',fontSize:9,fill:'#f59e0b'}}/>}
                <Area type="monotone" dataKey="taxable" stackId="1" stroke="#06b6d4" fill="#06b6d4" fillOpacity={0.35} strokeWidth={1.5} dot={false} name="Taxable"/>
                <Area type="monotone" dataKey="preTax"  stackId="1" stroke="#8b5cf6" fill="#8b5cf6" fillOpacity={0.30} strokeWidth={1.5} dot={false} name="Pre-tax"/>
                <Area type="monotone" dataKey="ukPen"   stackId="1" stroke="#6366f1" fill="#6366f1" fillOpacity={0.30} strokeWidth={1.5} dot={false} name="UK pension"/>
                <Area type="monotone" dataKey="roth"    stackId="1" stroke="#10b981" fill="#10b981" fillOpacity={0.30} strokeWidth={1.5} dot={false} name="Roth"/>
                <Legend wrapperStyle={{fontSize:10}}/>
              </AreaChart>
            </ResponsiveContainer>
          </div>

          {/* Healthcare coverage timeline */}
          <div className="card">
            <div className="label" style={{marginBottom:10}}>Healthcare Coverage</div>
            <div style={{display:'flex',flexDirection:'column',gap:6,fontSize:12}}>
              {[
                {phase:'Now → wife retires', coverage:'Both working — employer plans', cost:'~$0 net (employer-paid)', color:'#3b82f6', icon:'💼'},
                {phase:`Wife retires (age ${cfg.wifeRetireAge})`, coverage:'DSHBP retiree plan — covers wife + you as dependent', cost:`~${fmtCur(DSHBP_COUPLE_MONTHLY)}/mo est.`, color:'#10b981', icon:'🏛️'},
                cfg.retireAge < 65
                  ? {phase:`You retire → age 65 (${healthcareBridgeYears} yrs)`, coverage:'Remain on DSHBP as dependent — no COBRA needed', cost:`~${fmtCur(DSHBP_COUPLE_MONTHLY)}/mo vs ${fmtCur(COBRA_MONTHLY)}/mo COBRA`, color:'#8b5cf6', icon:'✅'}
                  : null,
                {phase:'Age 65+ both', coverage:'Medicare A+B+D + DSHBP Medicare supplement', cost:'~$350/mo combined premiums', color:'#6366f1', icon:'🏥'},
              ].filter(Boolean).map((row,i)=>(
                <div key={i} style={{borderLeft:`3px solid ${row.color}`,paddingLeft:10,paddingTop:4,paddingBottom:4}}>
                  <div style={{color:'#64748b',fontSize:10,marginBottom:2}}>{row.icon} {row.phase}</div>
                  <div style={{fontWeight:600,fontSize:11,color:'#e2e8f0',marginBottom:2}}>{row.coverage}</div>
                  <div style={{fontSize:10,color:row.color}}>{row.cost}</div>
                </div>
              ))}
            </div>
            {healthcareSavingsVsCobra > 0 && (
              <div style={{marginTop:10,background:'rgba(16,185,129,0.08)',border:'1px solid rgba(16,185,129,0.2)',borderRadius:6,padding:'8px 10px',fontSize:11}}>
                <strong style={{color:'#10b981'}}>DSHBP dependent coverage saves ~{fmtCur(healthcareSavingsVsCobra)}</strong> vs COBRA over {healthcareBridgeYears} yr bridge — a major hidden benefit of the Delaware pension.
              </div>
            )}
          </div>
      </div>
      {/* ── The bridge years: brokerage + Roth conversion window ── */}
      <div className="card" style={{marginBottom:16,borderColor:'rgba(6,182,212,0.35)'}}>
        <div className="label" style={{marginBottom:10}}>
          The {cfg.retireAge}–{bridgeWindowEnd} Window — Bridge Money and Roth Conversions
        </div>
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(200px,1fr))',gap:12,marginBottom:12}}>
          <div className="card-sm">
            <div className="label">Brokerage needed at {cfg.retireAge}</div>
            <div style={{fontSize:18,fontWeight:700,color:'#06b6d4'}}>{fmtCur(bridgeTarget.atRetirement)}</div>
            <div style={{fontSize:9,color:'#64748b',marginTop:2}}>
              funds {bridgeTarget.years} yrs of spending the pension and SS don't cover
            </div>
          </div>
          <div className="card-sm">
            <div className="label">Projected brokerage</div>
            <div style={{fontSize:18,fontWeight:700,color:(retirePoint.taxable||0)>=bridgeTarget.atRetirement?'#10b981':'#f59e0b'}}>
              {fmtCur(retirePoint.taxable||0)}
            </div>
            <div style={{fontSize:9,color:'#64748b',marginTop:2}}>
              {fmtCur(cfg.brokerageBalance||0)} today + {fmtCur(cfg.brokerageContrib||0)}/yr at {cfg.expectedReturn}%
            </div>
          </div>
          <div className="card-sm">
            <div className="label">{bridgeGap&&bridgeGap.alreadyThere?'Surplus':'Save per month'}</div>
            <div style={{fontSize:18,fontWeight:700,color:bridgeGap&&bridgeGap.alreadyThere?'#10b981':'#f59e0b'}}>
              {bridgeGap ? (bridgeGap.alreadyThere ? fmtCur((retirePoint.taxable||0)-bridgeTarget.atRetirement) : `${fmtCur(bridgeGap.monthly)}/mo`) : '—'}
            </div>
            <div style={{fontSize:9,color:'#64748b',marginTop:2}}>
              {bridgeGap&&bridgeGap.alreadyThere ? 'bridge already covered' : `to close the gap over ${yearsToRetire} yrs`}
            </div>
          </div>
          <div className="card-sm">
            <div className="label">Converted in the window</div>
            <div style={{fontSize:18,fontWeight:700,color:'#10b981'}}>{fmtCur(bridge?bridge.converted:0)}</div>
            <div style={{fontSize:9,color:'#64748b',marginTop:2}}>
              tax ~{fmtCur(bridge?bridge.conversionTax:0)}, paid from the brokerage
            </div>
          </div>
          <div className="card-sm">
            <div className="label">Pre-tax left at RMD age</div>
            <div style={{fontSize:18,fontWeight:700,color:'#8b5cf6'}}>{fmtCur(planRun.preTaxAtRmd||0)}</div>
            <div style={{fontSize:9,color:'#64748b',marginTop:2}}>
              {firstRmdRow ? `first RMD ${fmtCur(firstRmdRow.rmd)} at ${firstRmdRow.age}` : 'no RMD inside the plan'}
            </div>
          </div>
        </div>
        <div style={{fontSize:11,color:'#94a3b8',lineHeight:1.65}}>
          Two jobs, one account. From {cfg.retireAge} to {bridgeWindowEnd} the brokerage pays the spending Social
          Security has not started covering <em>and</em> the tax on every conversion made in the same years — so it is
          sized against the first job only ({fmtCur(bridgeTarget.perYear)}/yr average) and the conversion pace is
          whatever is left over. That ordering is deliberate: a conversion target that eats the bridge closes the
          window early. Conversions here fill ordinary income to the top of the {cfg.conversionTargetRatePct}% federal
          bracket (plus Delaware's 6.6%, which quoting the federal rate alone would hide), and the tax is paid from
          outside the IRA — paying it from inside turns the whole exercise into a pure bet on future rates.
          {' '}Whether to convert at all — the break-even rate, not the cash flow — is the Tax tab's Roth conversion analysis.
        </div>
      </div>

      {/* ── Decision levers: how NOW changes the future ── */}
      <div className="card" style={{marginBottom:16,borderColor:'rgba(16,185,129,0.35)'}}>
        <div className="label" style={{marginBottom:4}}>Decision Levers — What Each Move Is Worth</div>
        <div style={{fontSize:11,color:'#64748b',marginBottom:10,lineHeight:1.5}}>
          Every row re-runs the whole model with that one decision changed — accumulation, spending,
          taxes, conversions, the lot — against today's plan at {levers?levers.base.toFixed(2):'—'}×.
          These are the decisions available NOW; the further from retirement they're made, the more they compound.
        </div>
        {levers ? (
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            {levers.rows.map(l=>{
              const good = (l.delta||0) > 0.005;
              const flat = Math.abs(l.delta||0) <= 0.005;
              const color = flat ? '#64748b' : good ? '#10b981' : '#ef4444';
              return (
                <div key={l.key} style={{display:'flex',alignItems:'flex-start',gap:12,padding:'8px 10px',
                  background:'rgba(148,163,184,0.04)',borderRadius:8,borderLeft:`3px solid ${color}`}}>
                  <div style={{minWidth:86,textAlign:'right'}}>
                    <div style={{fontSize:16,fontWeight:700,color}}>
                      {flat ? '±0.00' : `${l.delta>0?'+':''}${l.delta.toFixed(2)}`}×
                    </div>
                    <div style={{fontSize:9,color:'#475569'}}>→ {l.ratio==null?'—':l.ratio.toFixed(2)}×</div>
                  </div>
                  <div style={{minWidth:0}}>
                    <div style={{fontSize:12,fontWeight:600,color:'#e2e8f0'}}>{l.label}</div>
                    <div style={{fontSize:10,color:'#64748b',lineHeight:1.5}}>{l.detail}</div>
                  </div>
                </div>
              );
            })}
          </div>
        ) : <div style={{fontSize:11,color:'#475569'}}>Enter a spending plan to price the levers.</div>}
      </div>

      {/* ── Risk: Monte Carlo + the survivor scenario ── */}
      <div className="grid-2" style={{gap:12,marginBottom:16,alignItems:'start'}}>
        <div className="card">
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
            <div className="label" style={{marginBottom:0}}>Sequence Risk — Monte Carlo</div>
            <button className="btn-primary" style={{fontSize:11,padding:'4px 12px'}} onClick={runMonteCarlo}>
              {mc ? 'Re-run' : 'Run'} 300 scenarios
            </button>
          </div>
          {mc ? (
            <div>
              {mcStale && <div style={{fontSize:10,color:'#f59e0b',marginBottom:6}}>Inputs changed since this run — re-run to refresh.</div>}
              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(110px,1fr))',gap:10,marginBottom:8}}>
                <div><div className="label">Success rate</div>
                  <div style={{fontSize:20,fontWeight:700,color:mc.res.successPct>=90?'#10b981':mc.res.successPct>=75?'#f59e0b':'#ef4444'}}>{mc.res.successPct}%</div></div>
                <div><div className="label">Ending p10 / p50 / p90</div>
                  <div style={{fontSize:12,fontWeight:600,color:'#e2e8f0'}}>{fmtCur(mc.res.endingP10)} / {fmtCur(mc.res.endingP50)} / {fmtCur(mc.res.endingP90)}</div></div>
                {mc.res.failures>0 && <div><div className="label">If it fails, at age</div>
                  <div style={{fontSize:12,fontWeight:600,color:'#ef4444'}}>~{mc.res.depletionP50} ({mc.res.failures}/{mc.res.runs} paths)</div></div>}
              </div>
              <div style={{fontSize:10,color:'#475569',lineHeight:1.5}}>
                Same engine — taxes, conversions, RMDs, IRMAA — over randomized {mc.res.meanPct}%±{mc.res.stdevPct}% annual
                returns, deterministically seeded. The straight-line funded ratio is the median story; this is the spread.
              </div>
            </div>
          ) : (
            <div style={{fontSize:11,color:'#64748b',lineHeight:1.6}}>
              The funded ratio assumes {cfg.expectedReturn}% every single year. This runs the same plan through 300
              randomized return sequences (±{cfg.mcStdevPct||12}% stdev) and reports how often it survives to {cfg.planToAge||95} —
              sequence-of-returns risk, measured instead of footnoted.
            </div>
          )}
        </div>
        <div className="card">
          <div className="label" style={{marginBottom:8}}>The Survivor Scenario — first death at {survivorView.atAge}</div>
          <div style={{display:'flex',flexDirection:'column',gap:5,fontSize:11}}>
            {survivorView.scenarios.map(sc=>(
              <div key={sc.key} style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'5px 0',borderBottom:'1px solid #161b22'}}>
                <span style={{color:'#94a3b8'}}>{sc.label}</span>
                <span style={{display:'flex',gap:14}}>
                  <span style={{color:'#64748b'}}>marginal <strong style={{color:'#e2e8f0'}}>{sc.avgMarginalAfter}%</strong></span>
                  <span style={{color:'#64748b'}}>tax/yr <strong style={{color:'#e2e8f0'}}>{fmtCur(sc.avgTaxAfter)}</strong></span>
                  <span style={{color:sc.funded?'#10b981':'#ef4444',fontWeight:600}}>{sc.funded?'funded':`out at ${sc.depletionAge}`}</span>
                </span>
              </div>
            ))}
          </div>
          <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
            The widow's tax: the survivor files single — half the bracket widths, half the deduction, single IRMAA
            thresholds — on one Social Security check and {wifeSurvivorOpt.survivorPct}% of the pension. A higher marginal
            rate after a first death is the standing argument for converting MORE while both of you are alive and the
            brackets are wide. Assumes the survivor spends 75% of the couple's plan.
          </div>
        </div>
      </div>

          {/* Key insights */}
          <div className="card" style={{marginBottom:16}}>
            <div className="label" style={{marginBottom:10}}>Key Insights</div>
            <div style={{display:'flex',flexDirection:'column',gap:8,fontSize:12,lineHeight:1.6,color:'#94a3b8'}}>
              {wifePensionAnnual>0&&(
                <div style={{padding:'8px 12px',background:'rgba(59,130,246,0.07)',borderRadius:8,borderLeft:'3px solid #3b82f6'}}>
                  <strong style={{color:'#3b82f6'}}>Delaware Pension (SEPP)</strong> — pays <strong style={{color:'#e2e8f0'}}>{fmtCur(wifePensionAnnual)}/yr</strong> ({((wifeCreditableAtRetire*SEPP.MULT_POST_1996)*100).toFixed(1)}% of FAE {fmtCur(wifeFae)}) based on {wifeCreditableAtRetire.toFixed(1)} creditable yrs at retirement{wifeEligAtRetire.reductionPct>0?` with –${wifeEligAtRetire.reductionPct.toFixed(1)}% early-retirement reduction`:''}, after {wifeSurvivorOpt.survivorPct}% survivor election. Defined-benefit pensions are rare — this is a significant retirement asset. No automatic COLA.
                </div>
              )}
              <div style={{padding:'8px 12px',background:'rgba(16,185,129,0.07)',borderRadius:8,borderLeft:'3px solid #10b981'}}>
                <strong style={{color:'#10b981'}}>Employer Core</strong> — {cfg.employerCorePct}% on first {fmtCur(cfg.employerCoreSalaryCap)} = <strong style={{color:'#e2e8f0'}}>{fmtCur(employerCoreAmt)}/yr</strong> before you contribute. Combined with {cfg.employerMatchPct}% match: <strong style={{color:'#e2e8f0'}}>{fmtCur(employerCoreAmt+employerMatchAmt)}/yr</strong> total employer.
              </div>
              {((cfg.yourSsMonthly||0)+(cfg.wifeSsMonthly||0))>0&&(
                <div style={{padding:'8px 12px',background:'rgba(245,158,11,0.07)',borderRadius:8,borderLeft:'3px solid #f59e0b'}}>
                  <strong style={{color:'#f59e0b'}}>Social Security</strong> — Combined FRA benefit <strong style={{color:'#e2e8f0'}}>{fmtCur(((cfg.yourSsMonthly||0)+(cfg.wifeSsMonthly||0))*12)}/yr</strong>. Delaying from 62→70 could add ~{fmtCur(((cfg.yourSsMonthly||0)+(cfg.wifeSsMonthly||0))*12*0.30)} annually. Both eligible — Delaware pays FICA.
                </div>
              )}
              {(cfg.yourIraContrib||0)+(cfg.wifeIraContrib||0)>0&&(
                <div style={{padding:'8px 12px',background:'rgba(139,92,246,0.07)',borderRadius:8,borderLeft:'3px solid #8b5cf6'}}>
                  <strong style={{color:'#8b5cf6'}}>IRAs</strong> — {fmtCur((cfg.yourIraContrib||0)+(cfg.wifeIraContrib||0))}/yr in IRA contributions add tax-advantaged diversification outside employer plans.
                </div>
              )}
              {funded.ratio!=null&&funded.ratio<1&&(
                <div style={{padding:'8px 12px',background:'rgba(239,68,68,0.07)',borderRadius:8,borderLeft:'3px solid #ef4444'}}>
                  <strong style={{color:'#ef4444'}}>Gap</strong> — the money supports {fmtCur(funded.supported||0)}/yr against a
                  plan that costs {fmtCur(needAtRetirement)}/yr{planRun.depletionAge?`, and runs out at ${planRun.depletionAge}`:''}.
                  The levers, in the order they move the number: retire later, spend less, or move the travel
                  budget ({fmtCur(cfg.travelAnnual||0)}/yr to age {cfg.travelUntilAge}) — the last of which is
                  discretionary by construction and closes {Math.round((cfg.travelAnnual||0)/Math.max(1,spendPlanOut.needTodayDollars)*100)}% of the plan on its own.
                </div>
              )}
              {funded.ratio>=1&&funded.ratio<1.15&&(
                <div style={{padding:'8px 12px',background:'rgba(245,158,11,0.07)',borderRadius:8,borderLeft:'3px solid #f59e0b'}}>
                  <strong style={{color:'#f59e0b'}}>Funded, with no margin</strong> — {funded.ratio.toFixed(2)}× on a
                  straight-line {cfg.expectedReturn}% return. That is not the same as safe: a poor first decade is
                  the risk this model cannot see. A cash/bond ladder for the first two years of spending
                  (~{fmtCur(needAtRetirement*2)}) is the standard defence.
                </div>
              )}
              {funded.ratio>=1.15&&(
                <div style={{padding:'8px 12px',background:'rgba(16,185,129,0.07)',borderRadius:8,borderLeft:'3px solid #10b981'}}>
                  <strong style={{color:'#10b981'}}>Funded</strong> — the plan supports {fmtCur(funded.supported||0)}/yr
                  against a need of {fmtCur(needAtRetirement)}/yr ({funded.ratio.toFixed(2)}×), to age {cfg.planToAge||95}.
                  The headroom is the answer to the question the old 50%-replacement grade was answering wrongly:
                  what this household spends is not what it earns, and the difference — mortgage, school fees, 529s,
                  and the saving itself — all stops before the first day of retirement.
                </div>
              )}
              <div style={{padding:'8px 12px',background:'rgba(139,92,246,0.07)',borderRadius:8,borderLeft:'3px solid #8b5cf6'}}>
                <strong style={{color:'#8b5cf6'}}>No COLA</strong> — the SEPP pension is {fmtCur(wifePensionAnnual)}/yr for life in
                nominal terms. At {cfg.inflationRate}% inflation it buys {fmtCur(Math.round(wifePensionAnnual/Math.pow(1+(cfg.inflationRate||3)/100, (cfg.planToAge||95)-cfg.retireAge)))} of
                today's money by {cfg.planToAge||95}. It covers {needAtRetirement>0?Math.round(wifePensionAnnual/needAtRetirement*100):0}% of the need in
                year one and a third of that by the end — the portfolio is what makes up the difference, and that is
                what the funded ratio is testing.
              </div>
            </div>
          </div>

      {/* ── The year-by-year plan ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8,flexWrap:'wrap',gap:8}}>
          <div className="label" style={{marginBottom:0}}>The Plan, Year by Year</div>
          <button onClick={()=>setShowAllYears(v=>!v)}
            style={{fontSize:10,padding:'3px 10px',borderRadius:6,cursor:'pointer',border:'1px solid #334155',
              background:'transparent',color:'#94a3b8'}}>
            {showAllYears ? 'Milestones only' : 'Show every year'}
          </button>
        </div>
        <div style={{overflowX:'auto'}}>
          <table style={{width:'100%',borderCollapse:'collapse',fontSize:11,whiteSpace:'nowrap'}}>
            <thead>
              <tr style={{color:'#64748b',textAlign:'right'}}>
                <th style={{textAlign:'left',padding:'4px 8px'}}>Age · Year</th>
                <th style={{textAlign:'left',padding:'4px 8px'}}>Phase</th>
                <th style={{padding:'4px 8px'}}>Saved / Need</th>
                <th style={{padding:'4px 8px'}}>Tax</th>
                <th style={{padding:'4px 8px'}}>Convert</th>
                <th style={{padding:'4px 8px'}}>RMD</th>
                <th style={{padding:'4px 8px'}}>IRMAA</th>
                <th style={{padding:'4px 8px',color:'#06b6d4'}}>Taxable</th>
                <th style={{padding:'4px 8px',color:'#8b5cf6'}}>Deferred</th>
                <th style={{padding:'4px 8px',color:'#10b981'}}>Roth</th>
                <th style={{padding:'4px 8px'}}>Portfolio</th>
                <th style={{textAlign:'left',padding:'4px 8px'}}>Milestones</th>
              </tr>
            </thead>
            <tbody>
              {yearPlan.filter((r,idx)=>{
                if(showAllYears) return true;
                const prev = yearPlan[idx-1];
                return r.events.length>0
                  || r.shortfall>0
                  || (prev && prev.phase!==r.phase)          // a phase transition
                  || idx===yearPlan.length-1;                 // the plan's last year
              }).map(r=>{
                const phaseColor = r.phase==='save'?'#3b82f6':r.phase==='convert'?'#10b981':r.phase==='rmd'?'#f59e0b':'#8b5cf6';
                const phaseLabel = r.phase==='save'?'saving':r.phase==='convert'?'converting':r.phase==='rmd'?'RMDs':'drawing';
                return (
                  <tr key={r.age} style={{borderTop:'1px solid #161b22',color:'#94a3b8',textAlign:'right'}}>
                    <td style={{textAlign:'left',padding:'4px 8px',color:'#e2e8f0',fontWeight:600}}>{r.age} · {r.year}</td>
                    <td style={{textAlign:'left',padding:'4px 8px',color:phaseColor}}>{phaseLabel}</td>
                    <td style={{padding:'4px 8px'}}>{r.phase==='save'
                      ? <span style={{color:'#10b981'}}>+{fmtCur(r.saved)}{r.redirected>0&&<span style={{color:'#06b6d4'}}> ({fmtCur(r.redirected)} redirected)</span>}</span>
                      : fmtCur(r.need)}</td>
                    <td style={{padding:'4px 8px'}}>{r.phase==='save'?'—':fmtCur(r.tax)}</td>
                    <td style={{padding:'4px 8px',color:r.conversion>0?'#10b981':undefined}}>{r.conversion>0?fmtCur(r.conversion):'—'}</td>
                    <td style={{padding:'4px 8px',color:r.rmd>0?'#f59e0b':undefined}}>{r.rmd>0?fmtCur(r.rmd):'—'}</td>
                    <td style={{padding:'4px 8px',color:r.irmaa>0?'#ef4444':undefined}}>{r.irmaa>0?fmtCur(r.irmaa):'—'}</td>
                    <td style={{padding:'4px 8px',color:'#06b6d4'}}>{fmtCur(r.taxable||0)}</td>
                    <td style={{padding:'4px 8px',color:'#8b5cf6'}}>{fmtCur(r.deferred||0)}</td>
                    <td style={{padding:'4px 8px',color:'#10b981'}}>{fmtCur(r.roth||0)}</td>
                    <td style={{padding:'4px 8px',color:'#e2e8f0'}}>{fmtCur(r.total)}{r.shortfall>0&&<span style={{color:'#ef4444'}}> !</span>}</td>
                    <td style={{textAlign:'left',padding:'4px 8px',color:'#64748b',whiteSpace:'normal',minWidth:220}}>{r.events.join(' · ')}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.5}}>
          Milestones view shows the years where something changes; every-year view is the full ledger the funded
          ratio is computed from. A red ! marks a year the money could not cover. Taxable is the brokerage
          (the money that pays conversion tax and bridges to Social Security); Deferred is 401(k)/403(b)/IRA plus
          the UK pension (ordinary income when drawn, RMDs on the US part); Roth is tax-free. A Taxable column
          sitting near zero through the saving years is why the conversion window converts nothing.
        </div>
      </div>

      {/* ── Plan inputs ── */}
      <div className="section-header" style={{marginTop:24}}>Plan Inputs</div>
      {/* Profiles (tiled) */}
      <div className="grid-2" style={{gap:12,marginBottom:12}}>
            <div className="card">
              <div className="label" style={{marginBottom:12}}>Your Profile</div>
              <div className="grid-2">
                <div style={{marginBottom:10}}>
                  <div className="label" style={{marginBottom:3}}>Your Age</div>
                  <div style={{fontSize:14,fontWeight:600,color:'#e2e8f0'}}>{derivedAge} <span style={{fontSize:10,color:'#64748b',fontWeight:400}}>· born {parseLocalDate(USER_PROFILE.birthday).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}</span></div>
                </div>
                <F label="Retire Age" k="retireAge" min={50} max={80}/>
                <F label="Current Total Comp" k="salary" step={5000} prefix="$" hint={compTcProduct.total?`${compTcProduct.year} TC: ${fmtCur(compTcProduct.total)}`:null}/>
                <F label="Annual Raise" k="salaryGrowth" step={0.5} suffix="%"/>
              </div>
              <div style={{fontSize:10,color:'#64748b',margin:'-4px 0 6px',lineHeight:1.5}}>
                TC = base + bonus + stock from Compensation ({compTcProduct.year}
                {compTcProduct.inProgress ? ` — ${compTcProduct.inProgress.year} is still in progress, bonus and stock not yet paid` : ''}). Edit to override.
                {compTcStale && (
                  <span style={{color:'#f59e0b'}}>
                    {' '}Compensation now says {fmtCur(latestCompTC)}.{' '}
                    <button onClick={()=>setCfg(c=>({...c, salary:latestCompTC}))}
                      style={{background:'none',border:'none',padding:0,color:'#f59e0b',textDecoration:'underline',cursor:'pointer',font:'inherit'}}>
                      Use it
                    </button>
                  </span>
                )}
              </div>
              <div style={{fontSize:11,background:'#161b22',borderRadius:6,padding:'7px 10px',color:'#64748b'}}>
                Projected TC at retire ({cfg.retireAge}): <strong style={{color:'#e2e8f0'}}>{fmtCur(yourFinalSalary)}</strong>
              </div>
            </div>

            <div className="card" style={{borderColor:'rgba(139,92,246,0.3)'}}>
              <div className="label" style={{marginBottom:12,color:'#8b5cf6'}}>Wife's Profile</div>
              <div className="grid-2">
                <div style={{marginBottom:10}}>
                  <div className="label" style={{marginBottom:3,color:'#8b5cf6'}}>Wife's Age</div>
                  <div style={{fontSize:14,fontWeight:600,color:'#e2e8f0'}}>{derivedSpouseAge} <span style={{fontSize:10,color:'#64748b',fontWeight:400}}>· born {parseLocalDate(USER_PROFILE.spouseBirthday).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}</span></div>
                </div>
                <F label="Wife's Retire Age" k="wifeRetireAge" min={50} max={70}/>
                {!cfg.wifeUseMa30Scale && <F label="Current Salary" k="wifeSalary" step={1000} prefix="$"/>}
                {!cfg.wifeUseMa30Scale && <F label="Annual Raise" k="wifeSalaryGrowth" step={0.5} suffix="%"/>}
              </div>
              <label style={{display:'flex',gap:8,alignItems:'center',fontSize:11,color:'#8b5cf6',margin:'4px 0 4px'}}>
                <input type="checkbox" checked={cfg.wifeUseMa30Scale!==false}
                  onChange={e=>setCfg(c=>({...c, wifeUseMa30Scale:e.target.checked}))}/>
                <span>Use Red Clay {wifeScale.lane||'MA+30'} pay scale · FY{wifeScale.fiscalYear||'—'}{wifeScale.effectiveDate?`, eff. ${wifeScale.effectiveDate}`:''} (Step = current years of service)</span>
              </label>
              {cfg.wifeUseMa30Scale && (
                <label style={{display:'flex',gap:8,alignItems:'center',fontSize:11,color:'#8b5cf6',margin:'0 0 8px'}}>
                  <input type="checkbox" checked={!!cfg.wifeNationallyCertified}
                    onChange={e=>setCfg(c=>({...c, wifeNationallyCertified:e.target.checked}))}/>
                  <span>Nationally Board Certified (+{wifeScale.natCertPct??6}% of State salary)</span>
                </label>
              )}
              {cfg.wifeUseMa30Scale && (()=>{
                // Every component comes from the scale module, so this card and
                // the Hers tab can't disagree about what she earns.
                const calc = TeacherScale.computeSalary(wifeScale, {
                  step: cfg.wifeCurrentService||1,
                  natCertified: !!cfg.wifeNationallyCertified,
                });
                return (
                  <div style={{background:'rgba(139,92,246,0.08)',border:'1px solid rgba(139,92,246,0.25)',borderRadius:8,padding:'8px 12px',fontSize:12,marginBottom:6}}>
                    <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}>
                      <span style={{color:'#64748b',fontSize:10,letterSpacing:'0.08em',textTransform:'uppercase'}}>Current salary ({wifeScale.lane||'MA+30'} · Step {calc.gridStep}{calc.overTopStep?'+':''})</span>
                      <strong style={{color:'#8b5cf6',fontSize:15}}>{fmtCur(calc.total)}</strong>
                    </div>
                    <div style={{fontSize:10,color:'#64748b',lineHeight:1.5}}>
                      {calc.steps.map((s,i)=><React.Fragment key={s.label}>{i>0?' + ':''}{s.label.replace(/ \(.*\)$/,'')} {fmtCur(s.amount)}</React.Fragment>)}
                    </div>
                    {wifeScaleStep != null && wifeScaleStep !== Math.floor(cfg.wifeCurrentService||1) && (
                      <div style={{color:'#f59e0b',marginTop:4,fontSize:10}}>
                        ℹ FY{wifeScale.fiscalYear} in Compensation → Hers records her on step {wifeScaleStep}.
                        <button onClick={()=>setCfg(c=>({...c, wifeCurrentService:wifeScaleStep}))}
                          style={{marginLeft:6,padding:'1px 7px',fontSize:10,borderRadius:4,cursor:'pointer',background:'transparent',color:'#f59e0b',border:'1px solid rgba(245,158,11,0.4)'}}>use {wifeScaleStep}</button>
                      </div>
                    )}
                  </div>
                );
              })()}
              <div style={{fontSize:11,background:'#161b22',borderRadius:6,padding:'7px 10px',color:'#64748b'}}>
                Projected salary at retire ({cfg.wifeRetireAge}): <strong style={{color:'#8b5cf6'}}>{fmtCur(wifeFinalSalary)}</strong>
                {cfg.wifeUseMa30Scale && <span style={{marginLeft:6,fontSize:10}}>({wifeScale.lane||'MA+30'} · Step {Math.min(wifeScaleTopStep,(cfg.wifeCurrentService||1)+wifeYearsToRetire)}{((cfg.wifeCurrentService||1)+wifeYearsToRetire)>wifeScaleTopStep?'+':''})</span>}
              </div>
            </div>
      </div>
      {/* Workplace plans + IRAs (tiled) */}
      <div className="grid-4" style={{gap:12,marginBottom:12}}>
            <div className="card">
              <div className="label" style={{marginBottom:12}}>Your 401(k)</div>
              <F label="Current Balance" k="currentBalance" step={5000} prefix="$" hint={detectedAccounts.total401k?`Monarch: ${fmtCur(detectedAccounts.total401k)}`:null}/>
              <F label="Your Annual Contribution" k="annualContribution" step={500} prefix="$" hint={detected401kContrib?`payslips: ${fmtCur(detected401kContrib)}/yr`:null}/>
              <F label="Employer Match" k="employerMatchPct" step={0.5} suffix="% of salary"/>
              <F label="Employer Core Contribution" k="employerCorePct" step={0.5} suffix="% of salary"/>
              <F label="Core Salary Cap" k="employerCoreSalaryCap" step={5000} prefix="$"/>
              {/* A core rate that steps with service. Held flat unless BOTH
                  the milestone and the rate it becomes are known — a
                  milestone on its own is a fact about the future, not a
                  number to project with. */}
              <F label="Service Start Date" k="employerServiceStartDate" type="date"/>
              <F label="Core Steps After" k="employerCoreStepYears" step={1} suffix="years' service" optional/>
              <F label="…And Becomes" k="employerCoreStepPct" step={0.5} suffix="% of salary" optional/>
              <div style={{fontSize:11,background:'#161b22',borderRadius:6,padding:'7px 10px',marginTop:2,color:'#64748b'}}>
                Total/yr <strong style={{color:'#10b981'}}>{fmtCur(total401kAnnual)}</strong>
                <span style={{marginLeft:8}}>({fmtCur(cfg.annualContribution)} you + {fmtCur(employerMatchAmt)} match + {fmtCur(employerCoreAmt)} core)</span>
              </div>
            </div>

            <div className="card">
              <div className="label" style={{marginBottom:12}}>Wife's 403(b)</div>
              <F label="Current Balance" k="wife403bBalance" step={5000} prefix="$"/>
              <F label="Annual Contribution" k="wife403bContrib" step={500} prefix="$"/>
              <F label="Employer Contribution" k="wife403bEmployerContrib" step={500} prefix="$"/>
            </div>

            <div className="card">
              <div className="label" style={{marginBottom:12}}>Your IRA</div>
              <div style={{fontSize:11,color:'#64748b',marginBottom:8}}>Traditional IRA</div>
              <F label="Current Balance" k="yourIraBalance" step={5000} prefix="$" hint={detectedAccounts.yourIra?`Monarch: ${fmtCur(detectedAccounts.yourIra)}`:null}/>
              <F label="Annual Contribution" k="yourIraContrib" step={500} prefix="$" max={iraMax}/>
              <div style={{fontSize:11,color:'#f59e0b',marginTop:8,marginBottom:4}}>Roth IRA</div>
              <F label="Roth Balance" k="yourRothBalance" step={5000} prefix="$" hint={detectedAccounts.yourRoth?`Monarch: ${fmtCur(detectedAccounts.yourRoth)}`:null}/>
              <F label="Roth Annual Contribution" k="yourRothContrib" step={500} prefix="$" max={iraMax}/>
              <div style={{fontSize:10,color:'#475569',marginTop:4}}>{iraHint}</div>
            </div>

            <div className="card">
              <div className="label" style={{marginBottom:12}}>Wife's IRA</div>
              <div style={{fontSize:11,color:'#64748b',marginBottom:8}}>Traditional IRA</div>
              <F label="Current Balance" k="wifeIraBalance" step={5000} prefix="$" hint={detectedAccounts.wifeIra?`Monarch: ${fmtCur(detectedAccounts.wifeIra)}`:null}/>
              <F label="Annual Contribution" k="wifeIraContrib" step={500} prefix="$" max={iraMax}/>
              <div style={{fontSize:11,color:'#f59e0b',marginTop:8,marginBottom:4}}>Roth IRA</div>
              <F label="Roth Balance" k="wifeRothBalance" step={5000} prefix="$" hint={detectedAccounts.wifeRoth?`Monarch: ${fmtCur(detectedAccounts.wifeRoth)}`:null}/>
              <F label="Roth Annual Contribution" k="wifeRothContrib" step={500} prefix="$" max={iraMax}/>
              <div style={{fontSize:10,color:'#475569',marginTop:4}}>{iraHint} · {rothPhaseHint}</div>
            </div>
      </div>
      {/* Social Security, UK account, assumptions */}
      <div className="grid-3" style={{gap:12,marginBottom:16,alignItems:'start'}}>
          <div className="card">
            <div className="label" style={{marginBottom:12}}>Social Security</div>
            <div style={{background:'rgba(245,158,11,0.08)',border:'1px solid rgba(245,158,11,0.2)',borderRadius:8,padding:'8px 12px',marginBottom:10,fontSize:11}}>
              <div style={{color:'#64748b',marginBottom:4}}>Estimated FRA benefit from salary (simplified SSA formula)</div>
              <div style={{display:'flex',gap:16}}>
                <span>You: <strong style={{color:'#f59e0b'}}>{fmtCur(estYourSs)}/mo</strong></span>
                <span>Wife: <strong style={{color:'#f59e0b'}}>{fmtCur(estWifeSs)}/mo</strong></span>
              </div>
              <div style={{fontSize:10,color:'#64748b',marginTop:3}}>Override below with your SSA statement estimate for precision.</div>
            </div>
            <div className="grid-2">
              <F label="Your Monthly (at FRA)" k="yourSsMonthly" step={50} prefix="$"/>
              <F label="Wife's Monthly (at FRA)" k="wifeSsMonthly" step={50} prefix="$"/>
            </div>
            <div className="grid-2">
              <F label="Your Claim Age" k="yourSsClaimAge" min={62} max={70}
                 hint={`${Math.round(RN.ssAdjustmentFactor(cfg.yourSsClaimAge||67,67)*100)}% of FRA`}/>
              <F label="Wife's Claim Age" k="wifeSsClaimAge" min={62} max={70}
                 hint={`${Math.round(RN.ssAdjustmentFactor(cfg.wifeSsClaimAge||67,67)*100)}% of FRA`}/>
            </div>
            {((cfg.yourSsClaimAge||67)!==67||(cfg.wifeSsClaimAge||67)!==67)&&(
              <div style={{fontSize:11,background:'#161b22',borderRadius:6,padding:'6px 10px',marginTop:4,color:'#64748b'}}>
                {(()=>{
                  const y=Math.round((cfg.yourSsMonthly||0)*RN.ssAdjustmentFactor(cfg.yourSsClaimAge||67,67));
                  const w=Math.round((cfg.wifeSsMonthly||0)*RN.ssAdjustmentFactor(cfg.wifeSsClaimAge||67,67));
                  return <>At those claim ages: you <strong style={{color:'#e2e8f0'}}>{fmtCur(y)}/mo</strong>, wife <strong style={{color:'#e2e8f0'}}>{fmtCur(w)}/mo</strong> — statutory factors (5/9% and 5/12% per early month, 2/3% per delayed month), permanent for life. The classic couples' play is delaying the higher earner to 70: it also raises the check the survivor keeps.</>;
                })()}
              </div>
            )}
            <div style={{fontSize:10,color:'#475569',marginTop:4,lineHeight:1.6}}>Delaware state employees pay FICA and retain full SS benefits. FRA = 67 born after 1960. Claiming at 62 reduces ~30%; delaying to 70 increases ~24% above FRA. Get exact estimates at ssa.gov/myaccount.</div>
          </div>

            {(cfg.ukPensionBalance>0||detectedAccounts.ukPension>0)&&(
              <div className="card" style={{borderColor:'rgba(99,102,241,0.3)'}}>
                <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:12}}>
                  <span>🇬🇧</span>
                  <span style={{fontWeight:600,color:'#6366f1',fontSize:12}}>UK Retirement Account</span>
                  <span style={{fontSize:10,color:'#64748b',marginLeft:'auto'}}>no longer contributing</span>
                </div>
                <F label="Current Balance" k="ukPensionBalance" step={5000} prefix="$" hint={detectedAccounts.ukPension?`Monarch: ${fmtCur(detectedAccounts.ukPension)}`:null}/>
                <div style={{fontSize:10,color:'#475569',marginTop:4,lineHeight:1.6}}>Preserved UK pension pot (balance tracked in USD) — treated as an investment account that continues to grow at the assumed rate of return. No new contributions. Projected value included in portfolio total.</div>
              </div>
            )}

            {/* ── Spending today, and the costs with an end date ────────────
                These are the inputs the funded ratio is actually built on.
                Ages are YOURS throughout, so "school fees stop at 52" and
                "mortgage clears at 55" line up against a retirement age on the
                same scale. A cost whose end age falls after the retirement age
                is not an error — it stays in the plan until that age and then
                drops out, which the spending card shows explicitly. */}
            <div className="card" style={{borderColor:'rgba(16,185,129,0.3)'}}>
              <div className="label" style={{marginBottom:12}}>Spending Today &amp; Costs That End</div>
              <div className="grid-2">
                <F label="Household tax rate (all-in)" k="effectiveTaxPct" step={0.5} suffix="%" min={0} max={60}
                   hint={`derived spend ${fmtCur(spendToday.derived)}`}/>
                <F label="Manual spend override" k="manualAnnualSpend" step={5000} prefix="$" optional
                   hint={spendToday.basis==='manual'?'in use':null}/>
                <F label="School fees /yr (after tax)" k="schoolCostAnnual" step={2500} prefix="$"/>
                <F label="…stop at your age" k="schoolEndsAtAge" min={40} max={80}/>
                <F label="Mortgage P&amp;I /yr" k="mortgageAnnual" step={1000} prefix="$"/>
                <F label="…clears at your age" k="mortgageEndsAtAge" min={40} max={90}/>
                <F label="529 contributions /yr" k="plan529Annual" step={1000} prefix="$"/>
                <F label="…stop at your age" k="plan529EndsAtAge" min={40} max={80}/>
                <F label="Dependent kid costs /yr" k="dependentKidAnnual" step={1000} prefix="$"/>
                <F label="…independent at your age" k="kidsIndependentAtAge" min={40} max={80}/>
              </div>
              <div style={{fontSize:10,color:'#475569',marginTop:4,lineHeight:1.6}}>
                College is deliberately absent: it is paid from the 529s, before this plan starts, and a
                retirement plan that budgets for it is charging itself twice. Property tax, insurance and
                upkeep are NOT in the mortgage line — they outlive the loan and stay in the need.
              </div>
            </div>

            {/* ── What retirement adds, and the years it is funded from ───── */}
            <div className="card" style={{borderColor:'rgba(6,182,212,0.3)'}}>
              <div className="label" style={{marginBottom:12}}>Retirement Costs &amp; the Bridge Account</div>
              <div className="grid-2">
                <F label="DSHBP premium /mo (pre-65)" k="earlyRetireHealthMo" step={25} prefix="$"/>
                <F label="Medicare + supplement /mo" k="medicareHealthMo" step={25} prefix="$"/>
                <F label="Travel budget /yr" k="travelAnnual" step={2500} prefix="$"/>
                <F label="…through your age" k="travelUntilAge" min={60} max={95}/>
                <F label="Brokerage balance" k="brokerageBalance" step={10000} prefix="$"
                   hint={detectedAccounts.brokerage?`Monarch: ${fmtCur(detectedAccounts.brokerage)}`:null}/>
                <F label="Brokerage contrib /yr" k="brokerageContrib" step={5000} prefix="$"
                   hint={bridgeGap&&!bridgeGap.alreadyThere?`need ${fmtCur(bridgeGap.annual)}/yr`:null}/>
                <F label="Brokerage cost basis" k="brokerageBasisPct" step={5} suffix="%" min={0} max={100}/>
                <F label="Dividend yield" k="dividendYieldPct" step={0.1} suffix="%" min={0} max={8}/>
                <F label="Convert through age" k="conversionEndAge" min={59} max={80}/>
                <F label="Fill bracket to" k="conversionTargetRatePct" step={1} suffix="%" min={0} max={37}/>
                <F label="IRMAA MAGI cap" k="irmaaMagiCap" step={5000} prefix="$" optional
                   hint={`tier 1 ≈ ${fmtCur(irmaaTier1)}`}/>
                <F label="Return volatility (MC)" k="mcStdevPct" step={1} suffix="%" min={0} max={30}/>
                <F label="Plan to age" k="planToAge" min={80} max={105}/>
                <F label="Real spending decline" k="realSpendDeclinePct" step={0.25} suffix="%/yr" min={0} max={3}/>
              </div>
              <label style={{display:'flex',alignItems:'center',gap:8,fontSize:12,color:'#94a3b8',margin:'6px 0',cursor:'pointer'}}>
                <input type="checkbox" checked={!!cfg.redirectFreedCashflow}
                  onChange={e=>setCfg(c=>({...c,redirectFreedCashflow:e.target.checked}))}/>
                <span>
                  <strong style={{color:'#06b6d4'}}>Redirect freed cashflow</strong> — as school fees, the mortgage,
                  529s and kid costs each end, route the freed money into the brokerage until retirement
                </span>
              </label>
              <label style={{display:'flex',alignItems:'center',gap:8,fontSize:12,color:'#94a3b8',margin:'6px 0',cursor:'pointer'}}>
                <input type="checkbox" checked={!!cfg.payTaxFromConversion}
                  onChange={e=>setCfg(c=>({...c,payTaxFromConversion:e.target.checked}))}/>
                <span>
                  <strong style={{color:'#10b981'}}>Pay conversion tax from the conversion itself</strong> — from 59½,
                  let Roth conversions fund their own tax out of pre-tax money when the brokerage can't carry it.
                  Second-best to brokerage-paid, but a window with no brokerage otherwise converts nothing
                </span>
              </label>
              <div style={{fontSize:10,color:'#475569',marginTop:4,lineHeight:1.6}}>
                Cost basis matters because the bridge is funded by SELLING: a low basis means a bigger
                capital gain on every dollar of spending, which is tax the plan has to fund as well.
                Real spending decline is the "retirement smile" and is off at 0 by default — a plan that
                assumes it spends less every year is a plan that funds itself by assumption.
              </div>
            </div>

            <div className="card">
              <div className="label" style={{marginBottom:12}}>Assumptions</div>
              <div className="grid-2">
                <F label="Expected Return" k="expectedReturn" step={0.5} suffix="%" min={1} max={15}/>
                <F label="Inflation Rate" k="inflationRate" step={0.1} suffix="%" min={1} max={8}/>
              </div>
              <div style={{fontSize:10,color:'#475569',marginTop:4,lineHeight:1.6}}>
                One return, applied every year, with no variance — so the funded ratio is a scenario, not a
                probability. Sequence-of-returns risk is the thing it cannot see, which is why the headroom
                above 1.00 is the part worth looking at.
              </div>
            </div>
      </div>

      {/* ── Delaware pension deep-dive ── */}
      <div className="section-header" style={{marginTop:24}}>Delaware State Pension (SEPP)</div>
      <div className="grid-2" style={{gap:12,marginBottom:12,alignItems:'start'}}>
          {/* ── Delaware Pension (SEPP) ── */}
          <div className="card" style={{borderColor:'rgba(59,130,246,0.4)'}}>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:12,flexWrap:'wrap'}}>
              <span>🏛️</span>
              <span style={{fontWeight:600,color:'#3b82f6',fontSize:12}}>Wife's Delaware State Pension (SEPP)</span>
              <span style={{fontSize:9,color:'#64748b',padding:'2px 6px',background:'rgba(59,130,246,0.12)',border:'1px solid rgba(59,130,246,0.25)',borderRadius:4,letterSpacing:'0.05em',textTransform:'uppercase'}}>
                {seppEraVal==='pre-2012'?'Pre-2012 rules':'Post-2012 rules'}
              </span>
              <span style={{fontSize:9,color:'#64748b',padding:'2px 6px',background:'rgba(139,92,246,0.12)',border:'1px solid rgba(139,92,246,0.25)',borderRadius:4,letterSpacing:'0.05em',textTransform:'uppercase'}}>
                Health: {seppHealthEraVal}
              </span>
              {wifeVestedNow
                ? <span style={{fontSize:9,color:'#10b981',padding:'2px 6px',background:'rgba(16,185,129,0.12)',border:'1px solid rgba(16,185,129,0.25)',borderRadius:4,letterSpacing:'0.05em',textTransform:'uppercase'}}>✓ Vested</span>
                : <span style={{fontSize:9,color:'#f59e0b',padding:'2px 6px',background:'rgba(245,158,11,0.12)',border:'1px solid rgba(245,158,11,0.25)',borderRadius:4,letterSpacing:'0.05em',textTransform:'uppercase'}}>Not vested</span>}
              {wifeHasCas && (
                <span style={{fontSize:9,color:'#10b981',padding:'2px 6px',background:'rgba(16,185,129,0.12)',border:'1px solid rgba(16,185,129,0.25)',borderRadius:4,letterSpacing:'0.05em',textTransform:'uppercase'}}>
                  ✓ CAS-verified
                </span>
              )}
            </div>

            {/* CAS-anchored data block — the Office of Pensions' own numbers
                drive everything downstream when present. */}
            <div style={{background:'rgba(16,185,129,0.06)',border:'1px solid rgba(16,185,129,0.2)',borderRadius:8,padding:'10px 12px',marginBottom:10}}>
              <div style={{display:'flex',alignItems:'center',gap:6,marginBottom:8}}>
                <span style={{fontSize:9,color:'#10b981',fontWeight:700,letterSpacing:'0.08em',textTransform:'uppercase'}}>📑 From Comprehensive Annual Statement</span>
                {cfg.wifeCasPensionId && <span style={{fontSize:9,color:'#64748b',marginLeft:'auto'}}>Pension ID: {cfg.wifeCasPensionId}</span>}
              </div>
              <div className="grid-2" style={{gap:10}}>
                <div>
                  <div className="label" style={{marginBottom:3}}>CAS as-of date</div>
                  <input type="date" value={cfg.wifeCasAsOfDate||''}
                    onChange={e=>setCfg(c=>({...c, wifeCasAsOfDate:e.target.value}))}
                    style={{width:'100%'}}/>
                </div>
                <F label="Creditable svc on CAS (Block 2)" k="wifeCasService" step={0.01} suffix=" yrs"/>
                <F label="FAE on CAS (Block 4 × 36)" k="wifeCasFae" step={1000} prefix="$"/>
                <F label="Contrib + interest balance" k="wifeCasContribBalance" step={500} prefix="$" hint="Block 3"/>
              </div>
              {wifeHasCas && (
                <div style={{fontSize:10,color:'#64748b',marginTop:6,lineHeight:1.5}}>
                  Today: <strong style={{color:'#10b981'}}>{wifeCreditableNow.toFixed(2)} yrs</strong> creditable
                  ({(cfg.wifeCasService||0).toFixed(2)} on CAS + {wifeYrsSinceCas.toFixed(2)} since).
                  Current FAE estimate: <strong style={{color:'#10b981'}}>{fmtCur(wifeFaeNow)}</strong>
                  (CAS {fmtCur(cfg.wifeCasFae)} × {((1+(cfg.wifeSalaryGrowth||2.5)/100)**wifeYrsSinceCas).toFixed(3)}).
                </div>
              )}
            </div>

            {/* Plan-classification inputs — used for fallback when no CAS data
                and to drive era badges / healthcare tier. */}
            <div className="grid-2" style={{gap:10,marginBottom:8}}>
              <div>
                <div className="label" style={{marginBottom:3}}>Hire Date</div>
                <input type="date" value={cfg.wifeHireDate||''}
                  onChange={e=>setCfg(c=>({...c, wifeHireDate:e.target.value}))}
                  style={{width:'100%'}}/>
                <div style={{fontSize:10,color:'#64748b',marginTop:3}}>Drives era (pre/post 2012) and healthcare tier.</div>
              </div>
              <F label="Service-loss yrs (gap)" k="wifeServiceLossYears" step={0.25} min={0} max={20} hint={wifeHasCas?'ignored when CAS svc set':null}/>
            </div>
            <div className="grid-2" style={{gap:10,marginBottom:8}}>
              <F label="MA+30 pay step" k="wifeCurrentService" step={0.5} min={0} max={40} hint="drives salary projection"/>
              <F label="Target years at retirement" k="wifeYearsService" step={1} min={1} max={40}/>
            </div>
            <div className="grid-2" style={{gap:10,marginBottom:8}}>
              <F label="FAE override" k="wifeFaeOverride" step={1000} prefix="$" hint="0 = use CAS or MA+30"/>
              <F label="Unused sick leave (days)" k="wifeSickLeaveDays" step={1} min={0} max={400}/>
            </div>
            <label style={{display:'flex',gap:8,alignItems:'center',fontSize:11,color:'#64748b',margin:'4px 0 10px'}}>
              <input type="checkbox" checked={!!cfg.wifeContribsWithdrawn}
                onChange={e=>setCfg(c=>({...c, wifeContribsWithdrawn:e.target.checked}))}/>
              <span>Contributions withdrawn during the {cfg.wifeServiceLossYears||0}-yr gap (forfeits pre-gap service unless repaid)</span>
            </label>

            {/* Service status banner */}
            <div style={{background:'#0d1117',border:'1px solid #1e2a3a',borderRadius:8,padding:'8px 12px',marginBottom:10,fontSize:11,lineHeight:1.5}}>
              <div style={{color:'#94a3b8'}}>
                {wifeHasCas
                  ? <>CAS-anchored: <strong style={{color:'#10b981'}}>{(cfg.wifeCasService||0).toFixed(2)} yrs</strong> on {parseLocalDate(cfg.wifeCasAsOfDate).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})} + <strong style={{color:'#e2e8f0'}}>{wifeYrsSinceCas.toFixed(2)} yrs</strong> since = creditable <strong style={{color:'#3b82f6'}}>{wifeCreditableNow.toFixed(2)} yrs</strong></>
                  : <>Reported service <strong style={{color:'#e2e8f0'}}>{cfg.wifeCurrentService||0} yrs</strong>
                      {cfg.wifeServiceLossYears>0 && <> − gap <strong style={{color:'#f59e0b'}}>{cfg.wifeServiceLossYears} yr{cfg.wifeServiceLossYears===1?'':'s'}</strong></>}
                      {' '}= creditable <strong style={{color:'#3b82f6'}}>{wifeCreditableNow.toFixed(1)} yrs</strong></>}
              </div>
              {wifeHasCas && (cfg.wifeCurrentService - (cfg.wifeServiceLossYears||0)) - cfg.wifeCasService > 0.5 && (
                <div style={{color:'#f59e0b',marginTop:4,fontSize:10}}>
                  ℹ CAS service ({cfg.wifeCasService.toFixed(2)} yrs) differs from MA+30 step − gap by {((cfg.wifeCurrentService - (cfg.wifeServiceLossYears||0)) - cfg.wifeCasService).toFixed(2)} yrs.
                  The gap appears closer to {(cfg.wifeCurrentService - cfg.wifeCasService).toFixed(2)} yrs in pension records.
                </div>
              )}
              {cfg.wifeContribsWithdrawn && wifeCreditableNow < 5 && (
                <div style={{color:'#ef4444',marginTop:4}}>
                  ⚠ Pre-gap service may be forfeited (contributions withdrawn, &lt;5 consecutive yrs at gap). Per SPD §2 — repay refund + 5 consecutive yrs to restore.
                </div>
              )}
              {!wifeVestedNow && (
                <div style={{color:'#f59e0b',marginTop:4}}>
                  Not yet vested ({seppEraVal==='pre-2012'?'need 5 consecutive yrs':'need 10 yrs, 5 consecutive'}). Termination before vesting forfeits all future pension.
                </div>
              )}
            </div>

            {/* Pension outputs */}
            <div style={{background:'rgba(59,130,246,0.08)',border:'1px solid rgba(59,130,246,0.2)',borderRadius:8,padding:12,marginTop:4}}>
              <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,fontSize:12}}>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Creditable yrs at retire</div><strong style={{color:'#3b82f6',fontSize:15}}>{wifeCreditableAtRetire.toFixed(1)}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>FAE (3-yr avg)</div><strong style={{color:'#3b82f6',fontSize:15}}>{fmtCur(wifeFae)}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Gross annual pension</div><strong style={{color:'#3b82f6',fontSize:15}}>{fmtCur(wifeGrossPension)}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>After early-retire reduction</div><strong style={{color:wifeEligAtRetire.reductionPct>0?'#f59e0b':'#3b82f6',fontSize:15}}>{fmtCur(wifePensionAfterEarly)}{wifeEligAtRetire.reductionPct>0?` (–${wifeEligAtRetire.reductionPct.toFixed(1)}%)`:''}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Net annual ({wifeSurvivorOpt.survivorPct}% survivor)</div><strong style={{color:'#10b981',fontSize:15}}>{fmtCur(wifePensionAnnual)}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Monthly net</div><strong style={{color:'#10b981',fontSize:15}}>{fmtCur(Math.round(wifePensionAnnual/12))}</strong></div>
              </div>
              <div style={{marginTop:10,borderTop:'1px solid rgba(59,130,246,0.2)',paddingTop:8,fontSize:11,color:'#94a3b8'}}>
                Eligibility at retire ({cfg.wifeRetireAge}): <strong style={{color:wifeEligAtRetire.eligible?(wifeEligAtRetire.reductionPct>0?'#f59e0b':'#10b981'):'#ef4444'}}>{wifeEligAtRetire.label}</strong>
              </div>
              {/* Where the FAE came from. This drives the whole pension number,
                  the two available sources disagree by a lot, and the reason
                  one wins is a modelling judgement — so it is stated, not
                  buried. */}
              {wifeFaeCasProjected != null && cfg.wifeFaeOverride <= 0 && (
                <div style={{marginTop:8,fontSize:10,color:'#64748b',lineHeight:1.6}}>
                  FAE built from the {wifeScale.lane||'MA+30'} scale: her final 3 years priced from
                  today's known {fmtCur(cfg.wifeSalary||0)} at {cfg.wifeSalaryGrowth||2.5}%/yr past the top step.
                  {' '}Escalating the CAS FAE ({fmtCur(cfg.wifeCasFae)}, as of {cfg.wifeCasAsOfDate}) instead would say{' '}
                  <strong style={{color:'#94a3b8'}}>{fmtCur(wifeFaeCasProjected)}</strong>
                  {' '}({wifeFae>=wifeFaeCasProjected?'−':'+'}{fmtCur(Math.abs(wifeFae-wifeFaeCasProjected))}) —
                  but that figure is a 3-year trailing average, so compounding it forward keeps her
                  permanently ~2 years behind her own pay. The CAS still governs the retire-today
                  figures below.
                </div>
              )}
              {wifeYearsToRule30>0&&(
                <div style={{marginTop:10,borderTop:'1px solid rgba(59,130,246,0.2)',paddingTop:8}}>
                  <div style={{fontSize:10,color:'#64748b',marginBottom:4}}>Rule of 30 — {wifeYearsToRule30.toFixed(1)} yrs from now → 30 creditable yrs at age {wifeAgeAtRule30.toFixed(0)}</div>
                  <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,fontSize:12}}>
                    <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Net pension at 30 yrs</div><strong style={{color:'#10b981',fontSize:15}}>{fmtCur(wifePensionRule30)}/yr</strong></div>
                    <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>vs. plan ({wifeYearsAtRetire} yrs target)</div><strong style={{color:wifePensionRule30>=wifePensionAnnual?'#10b981':'#f59e0b',fontSize:15}}>{wifePensionRule30>=wifePensionAnnual?'+':'–'}{fmtCur(Math.abs(wifePensionRule30 - wifePensionAnnual))}/yr</strong></div>
                  </div>
                </div>
              )}
              <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
                <strong>SEPP formula (SPD §6):</strong> 1.85% × creditable yrs × FAE (avg of 3 highest 12-mo periods). She was hired post-1997, so all service uses the 1.85% multiplier — there is no 2% portion and no 80% cap. <strong>No automatic COLA</strong> — increases require Delaware legislative action. Survivor election is irrevocable at first deposit.
              </div>
            </div>
          </div>
        <div style={{display:'flex',flexDirection:'column',gap:12}}>
          {/* ── Eligibility Scenarios — when can she retire and at what cost ── */}
          <div className="card" style={{borderColor:'rgba(16,185,129,0.3)'}}>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
              <span>🎯</span>
              <span style={{fontWeight:600,color:'#10b981',fontSize:12}}>Retirement Eligibility Scenarios</span>
              <span style={{marginLeft:'auto',fontSize:10,color:'#64748b'}}>net of {wifeSurvivorOpt.survivorPct}% survivor election</span>
            </div>
            <div style={{overflowX:'auto'}}>
              <table style={{width:'100%',fontSize:11,borderCollapse:'collapse'}}>
                <thead>
                  <tr style={{color:'#64748b',fontSize:10,letterSpacing:'0.05em',textTransform:'uppercase',borderBottom:'1px solid #1e2a3a'}}>
                    <th style={{textAlign:'left',padding:'6px 4px'}}>At age</th>
                    <th style={{textAlign:'right',padding:'6px 4px'}}>Yr</th>
                    <th style={{textAlign:'right',padding:'6px 4px'}}>Svc</th>
                    <th style={{textAlign:'left',padding:'6px 4px'}}>Eligibility</th>
                    <th style={{textAlign:'right',padding:'6px 4px'}}>FAE</th>
                    <th style={{textAlign:'right',padding:'6px 4px'}}>Pension/yr</th>
                    <th style={{textAlign:'right',padding:'6px 4px'}}>Health</th>
                  </tr>
                </thead>
                <tbody>
                  {wifeScenarios.map(s => {
                    const isPlanned = s.age === (cfg.wifeRetireAge||55);
                    return (
                      <tr key={s.age} style={{borderBottom:'1px solid #161b22', background:isPlanned?'rgba(16,185,129,0.06)':'transparent'}}>
                        <td style={{padding:'6px 4px',fontWeight:isPlanned?700:500,color:isPlanned?'#10b981':'#e2e8f0'}}>{s.age}{isPlanned?' ★':''}</td>
                        <td style={{textAlign:'right',padding:'6px 4px',color:'#64748b'}}>{s.year}</td>
                        <td style={{textAlign:'right',padding:'6px 4px'}}>{s.yos.toFixed(1)}</td>
                        <td style={{textAlign:'left',padding:'6px 4px',color:s.eligible?(s.reductionPct>0?'#f59e0b':'#10b981'):'#ef4444',fontSize:10}}>{s.eligLabel}</td>
                        <td style={{textAlign:'right',padding:'6px 4px',color:'#94a3b8'}}>{fmtCur(s.fae)}</td>
                        <td style={{textAlign:'right',padding:'6px 4px',fontWeight:600,color:s.eligible?'#3b82f6':'#475569'}}>{s.eligible?fmtCur(s.annual):'—'}</td>
                        <td style={{textAlign:'right',padding:'6px 4px',color:s.healthShare===100?'#10b981':s.healthShare>=75?'#3b82f6':s.healthShare>=50?'#f59e0b':'#ef4444',fontWeight:600}}>{s.healthShare}%</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
            <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
              ★ = your currently planned retirement age. Healthcare = state-paid % of premium ({seppHealthEraVal} schedule: {seppHealthCliffs(seppHealthEraVal).map(c=>`${c.yos}yr→${c.share}%`).join(', ')}). Working past a cliff is often worth more than the marginal pension growth.
            </div>
          </div>

          {/* ── Termination scenarios — vested-deferred vs refund ── */}
          {wifeHasCas && wifeVestedNow && (() => {
            // What she'd get if she left state employment TODAY, vested but deferred.
            // Pension formula uses CAS FAE escalated to retirement age (frozen at
            // termination — no further salary growth in real terms).
            const todayPensionGross = Math.round(seppGrossPension(wifeFaeNow, wifeCreditableNow, 0));
            const todayPensionNet = Math.round(todayPensionGross * (1 - wifeSurvivorOpt.pensionReductionPct/100));
            // Vested deferred: she can collect at age 62 (pre-2012 rules) once she
            // would have hit that age, but the FAE is frozen at her last working
            // year — only available as 50% survivor on death per SPD §4.
            const refund = (cfg.wifeCasContribBalance||0);
            // Break-even: how many months of pension to recover the refund value.
            const breakEvenMonths = todayPensionNet > 0 ? Math.round(refund / (todayPensionNet/12) * 10) / 10 : 0;
            return (
              <div className="card" style={{borderColor:'rgba(245,158,11,0.3)'}}>
                <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
                  <span>⚖️</span>
                  <span style={{fontWeight:600,color:'#f59e0b',fontSize:12}}>If She Left State Employment Today (vested)</span>
                </div>
                <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,fontSize:12,marginBottom:8}}>
                  <div style={{padding:'10px 12px',background:'rgba(16,185,129,0.07)',border:'1px solid rgba(16,185,129,0.2)',borderRadius:6}}>
                    <div style={{fontSize:10,color:'#64748b',marginBottom:3,textTransform:'uppercase',letterSpacing:'0.05em'}}>Option A · Vested deferred</div>
                    <div style={{fontSize:18,fontWeight:800,color:'#10b981'}}>{fmtCur(todayPensionNet)}/yr</div>
                    <div style={{fontSize:10,color:'#94a3b8',marginTop:4,lineHeight:1.5}}>
                      Frozen pension starts at age 62 (current rules). FAE locked at {fmtCur(wifeFaeNow)}, svc locked at {wifeCreditableNow.toFixed(2)} yrs. No healthcare bridge — has to buy own coverage to 65.
                    </div>
                  </div>
                  <div style={{padding:'10px 12px',background:'rgba(239,68,68,0.07)',border:'1px solid rgba(239,68,68,0.2)',borderRadius:6}}>
                    <div style={{fontSize:10,color:'#64748b',marginBottom:3,textTransform:'uppercase',letterSpacing:'0.05em'}}>Option B · Refund contributions</div>
                    <div style={{fontSize:18,fontWeight:800,color:'#ef4444'}}>{fmtCur(refund)} one-time</div>
                    <div style={{fontSize:10,color:'#94a3b8',marginTop:4,lineHeight:1.5}}>
                      Cash payout taxed 20% federal (or rollover to IRA/401k). Voids ALL future pension rights including the survivor and healthcare benefits.
                    </div>
                  </div>
                </div>
                <div style={{padding:'8px 12px',background:'#0d1117',border:'1px solid #1e2a3a',borderRadius:6,fontSize:11,color:'#94a3b8',lineHeight:1.6}}>
                  <strong style={{color:'#f59e0b'}}>Refund break-even:</strong> deferred pension recovers the {fmtCur(refund)} refund in <strong style={{color:'#e2e8f0'}}>{breakEvenMonths} months</strong> ({(breakEvenMonths/12).toFixed(1)} yrs) once it starts. The deferred pension is also a lifetime annuity with survivor protection — refund is one-time and gone. Refund is almost never the right choice for a vested member.
                </div>
              </div>
            );
          })()}

          {/* ── Healthcare Cliff Timeline ── */}
          <div className="card" style={{borderColor:'rgba(139,92,246,0.3)'}}>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
              <span>🏥</span>
              <span style={{fontWeight:600,color:'#8b5cf6',fontSize:12}}>Retiree Healthcare Subsidy Cliffs</span>
              <span style={{marginLeft:'auto',fontSize:10,color:'#64748b'}}>{seppHealthEraVal} hire schedule</span>
            </div>
            <div style={{display:'flex',flexDirection:'column',gap:6}}>
              {wifeHealthCliffs.map((c,i) => (
                <div key={i} style={{
                  display:'flex',justifyContent:'space-between',alignItems:'center',
                  padding:'8px 12px',
                  background: c.passed?'rgba(16,185,129,0.08)':'rgba(139,92,246,0.06)',
                  border:`1px solid ${c.passed?'rgba(16,185,129,0.25)':'rgba(139,92,246,0.2)'}`,
                  borderRadius:6,fontSize:12
                }}>
                  <div>
                    <div style={{fontWeight:700,color:c.passed?'#10b981':'#8b5cf6'}}>
                      {c.passed?'✓ ':''}{c.yos} years → state pays {c.share}% of health premium
                    </div>
                    <div style={{fontSize:10,color:'#64748b',marginTop:2}}>
                      {c.passed
                        ? `Already past (${Math.abs(c.yrsAway).toFixed(1)} yrs ago)`
                        : `${c.yrsAway.toFixed(1)} yrs away · ${fmtMonthYear(c.date)} · wife age ${c.ageAt.toFixed(0)}`}
                    </div>
                  </div>
                  <div style={{fontSize:18,fontWeight:800,color:c.passed?'#10b981':'#8b5cf6'}}>{c.share}%</div>
                </div>
              ))}
            </div>
            <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
              Pre-Medicare retiree health coverage is often worth $10–20k/yr in premium savings vs. COBRA or the marketplace. Hitting the next cliff before retiring is frequently the single highest-ROI calendar adjustment available.
            </div>
          </div>
        </div>
      </div>
      {/* Survivor election | sick-leave buy-in + plan confirmations */}
      <div className="grid-2" style={{gap:12,marginBottom:16,alignItems:'start'}}>
            <div className="card" style={{borderColor:'rgba(245,158,11,0.3)'}}>
              <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10,flexWrap:'wrap'}}>
                <span>👥</span>
                <span style={{fontWeight:600,color:'#f59e0b',fontSize:12}}>Survivor Election</span>
                <span style={{marginLeft:'auto',fontSize:9,color:'#ef4444',padding:'1px 6px',background:'rgba(239,68,68,0.1)',border:'1px solid rgba(239,68,68,0.25)',borderRadius:4}}>IRREVOCABLE AT 1ST DEPOSIT</span>
              </div>
              <div style={{fontSize:10,color:'#64748b',marginBottom:8,lineHeight:1.5}}>
                Sets what % of her pension you receive if she predeceases you, and what permanent reduction she takes on her own monthly amount while alive.
              </div>

              {/* Recommendation banner — uses utility-weighted EV (survivor
                  dollars weighted down by spouse's own income independence). */}
              {wifeSurvivorRec.recommended && (() => {
                const rec = wifeSurvivorRec.recommended;
                const current = wifeSurvivorRec.ladder.find(r => Math.abs(r.survivorPct - (cfg.wifeSurvivorElection||75)) < 0.5);
                const matchesCurrent = current && Math.abs(current.survivorPct - rec.survivorPct) < 0.5;
                const evDelta = current ? rec.weightedEv - current.weightedEv : 0;
                const ageGap = (cfg.currentAge||45) - (cfg.wifeAge||44);
                return (
                  <div style={{background: matchesCurrent?'rgba(16,185,129,0.08)':'rgba(245,158,11,0.08)', border:`1px solid ${matchesCurrent?'rgba(16,185,129,0.3)':'rgba(245,158,11,0.3)'}`, borderRadius:6, padding:'8px 10px', marginBottom:8, fontSize:11, lineHeight:1.5}}>
                    <div style={{fontSize:9,color:'#64748b',letterSpacing:'0.08em',textTransform:'uppercase',marginBottom:3}}>Recommendation</div>
                    <div style={{fontWeight:700,color:matchesCurrent?'#10b981':'#f59e0b',fontSize:13,marginBottom:4}}>
                      {matchesCurrent ? '✓ ' : '→ '}Choose <strong>{rec.survivorPct}% survivor</strong> ({rec.pensionReductionPct?'–'+rec.pensionReductionPct+'%':'no reduction'} to her monthly)
                    </div>
                    <div style={{color:'#94a3b8'}}>
                      Age + gender: she's <strong style={{color:'#e2e8f0'}}>{ageGap>0?ageGap+' yrs younger':ageGap<0?Math.abs(ageGap)+' yrs older':'same age'}</strong> and female, so under standard mortality she's expected to outlive you — survivor benefit has a low payout probability.
                      {' '}Your own assets project to <strong style={{color:'#e2e8f0'}}>~{yourOwnReplacementPct}% income replacement</strong> independent of her pension, so survivor dollars are weighted at <strong style={{color:'#e2e8f0'}}>{(wifeSurvivorRec.survUtilWeight*100).toFixed(0)}%</strong> utility.
                      {' '}A {rec.pensionReductionPct}% permanent cut to her pension {rec.survivorPct===50?'is avoided entirely':'buys protection you likely won\'t need'}.
                    </div>
                    {!matchesCurrent && evDelta !== 0 && (
                      <div style={{marginTop:4,color:'#f59e0b'}}>
                        Switching to {rec.survivorPct}% adds <strong>{fmtCur(Math.abs(evDelta))}</strong> in utility-weighted expected lifetime value vs. your current {(cfg.wifeSurvivorElection||75)}% election.
                      </div>
                    )}
                  </div>
                );
              })()}

              <div style={{display:'flex',flexDirection:'column',gap:6}}>
                {wifeSurvivorRec.ladder.map(opt => {
                  const sel = Math.abs(opt.survivorPct - (cfg.wifeSurvivorElection||75)) < 0.5;
                  const isRec = wifeSurvivorRec.recommended && Math.abs(opt.survivorPct - wifeSurvivorRec.recommended.survivorPct) < 0.5;
                  return (
                    <label key={opt.survivorPct} style={{
                      display:'flex',justifyContent:'space-between',alignItems:'center',
                      padding:'8px 10px',cursor:'pointer',
                      background: sel?'rgba(245,158,11,0.1)':'transparent',
                      border:`1px solid ${sel?'rgba(245,158,11,0.4)':isRec?'rgba(16,185,129,0.3)':'#1e2a3a'}`,
                      borderRadius:6
                    }}>
                      <div style={{display:'flex',alignItems:'center',gap:8,flex:1}}>
                        <input type="radio" name="survivorElect" checked={sel}
                          onChange={()=>setCfg(c=>({...c, wifeSurvivorElection:opt.survivorPct}))}/>
                        <div style={{flex:1}}>
                          <div style={{fontSize:12,fontWeight:600,color:sel?'#f59e0b':'#e2e8f0',display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
                            {opt.label}
                            {isRec && <span style={{fontSize:8,color:'#10b981',padding:'1px 5px',background:'rgba(16,185,129,0.15)',border:'1px solid rgba(16,185,129,0.3)',borderRadius:3,letterSpacing:'0.05em',textTransform:'uppercase'}}>Recommended</span>}
                          </div>
                          <div style={{fontSize:10,color:'#64748b'}}>Her: {fmtCur(opt.memberAnnual)} · You: {fmtCur(opt.survivorAnnual)}</div>
                        </div>
                      </div>
                      <div style={{textAlign:'right',minWidth:100}}>
                        <div style={{fontSize:9,color:'#64748b'}}>Util-wtd EV</div>
                        <div style={{fontSize:12,fontWeight:700,color:isRec?'#10b981':'#94a3b8'}}>{fmtCur(opt.weightedEv)}</div>
                        <div style={{fontSize:9,color:'#475569'}}>raw: {fmtCur(opt.totalEv)}</div>
                      </div>
                    </label>
                  );
                })}
              </div>

              {/* Periodic review tracker */}
              <div style={{marginTop:10,padding:'8px 10px',background: wifeReviewDue?'rgba(239,68,68,0.06)':'rgba(16,185,129,0.06)',border:`1px solid ${wifeReviewDue?'rgba(239,68,68,0.25)':'rgba(16,185,129,0.25)'}`,borderRadius:6,fontSize:11}}>
                <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:8}}>
                  <div>
                    <div style={{fontSize:9,color:'#64748b',letterSpacing:'0.08em',textTransform:'uppercase',marginBottom:2}}>Periodic review</div>
                    {wifeReviewDaysSince === null
                      ? <div style={{color:'#ef4444',fontWeight:600}}>Never reviewed</div>
                      : wifeReviewDue
                        ? <div style={{color:'#ef4444',fontWeight:600}}>Last reviewed {wifeReviewDaysSince} days ago — overdue</div>
                        : <div style={{color:'#10b981',fontWeight:600}}>Last reviewed {wifeReviewDaysSince} days ago</div>}
                  </div>
                  <button className="btn-secondary" style={{fontSize:10,padding:'4px 10px'}}
                    onClick={()=>setCfg(c=>({...c, wifeSurvivorReviewDate: new Date().toISOString().slice(0,10)}))}>
                    Mark reviewed
                  </button>
                </div>
                <div style={{fontSize:10,color:'#64748b',marginTop:6,lineHeight:1.5}}>
                  Re-review when any of these change: your health, her health, your own retirement income (portfolio + SS), age gap, or as the irrevocable deadline (first pension deposit) approaches. Annual cadence at minimum.
                </div>
              </div>

              <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
                EV uses simple linear survival curves (F terminal ~92, M ~88). Real life expectancy depends on health, family history, lifestyle. The ranking is robust to ±5 yr terminal adjustments; the absolute EVs are illustrative.
              </div>
            </div>
        <div style={{display:'flex',flexDirection:'column',gap:12}}>
            <div className="card" style={{borderColor:'rgba(16,185,129,0.3)'}}>
              <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
                <span>🏖️</span>
                <span style={{fontWeight:600,color:'#10b981',fontSize:12}}>Sick Leave Buy-In</span>
              </div>
              <F label="Unused sick days at retire" k="wifeSickLeaveDays" step={1} min={0} max={400}/>
              <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,marginTop:8,fontSize:12}}>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Months purchasable</div><strong style={{color:'#10b981',fontSize:15}}>{wifeSickLeaveMonths}{wifeSickLeaveMonths===SEPP.SICK_LEAVE_MAX_MONTHS?' (max)':''}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Cost (5% × FAE × mo)</div><strong style={{color:'#f59e0b',fontSize:15}}>{fmtCur(wifeSickLeaveCost)}</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Pension boost</div><strong style={{color:'#10b981',fontSize:15}}>{fmtCur(wifeSickLeaveAnnualBoost)}/yr</strong></div>
                <div><div style={{color:'#64748b',fontSize:10,marginBottom:2}}>Payback period</div><strong style={{color:'#e2e8f0',fontSize:15}}>{wifeSickLeaveAnnualBoost>0?`${(wifeSickLeaveCost/wifeSickLeaveAnnualBoost).toFixed(1)} yrs`:'—'}</strong></div>
              </div>
              <div style={{fontSize:10,color:'#475569',marginTop:8,lineHeight:1.6}}>
                21 unused sick days = 1 month of service (max 12 mo). Cost is rollover-eligible from 401(a)/457(b)/403(b)/IRA. Don't burn sick leave in final years — it's deferred pension value.
              </div>
            </div>

          {/* ── Plan Confirmations & Remaining Questions ── */}
          <div className="card" style={{borderColor: wifeHasCas?'rgba(16,185,129,0.3)':'rgba(239,68,68,0.3)', background: wifeHasCas?'rgba(16,185,129,0.04)':'rgba(239,68,68,0.04)'}}>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10}}>
              <span>{wifeHasCas?'📋':'❓'}</span>
              <span style={{fontWeight:600,color:wifeHasCas?'#10b981':'#ef4444',fontSize:12}}>{wifeHasCas?'Plan Confirmations from CAS':'Confirm with Office of Pensions'}</span>
              <span style={{marginLeft:'auto',fontSize:10,color:'#64748b'}}>(302) 739-4208 · pensionoffice@delaware.gov</span>
            </div>
            <div style={{fontSize:11,color:'#94a3b8',lineHeight:1.6,display:'flex',flexDirection:'column',gap:6}}>
              {wifeHasCas && (
                <>
                  <div>✓ <strong style={{color:'#10b981'}}>Confirmed by CAS:</strong> Plan = State Employees' Pension Plan (SEPP). Creditable service {cfg.wifeCasService.toFixed(2)} yrs as of {parseLocalDate(cfg.wifeCasAsOfDate).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}. FAE = {fmtCur(cfg.wifeCasFae)}. Gap reconciliation already baked in.</div>
                  <div>✓ <strong style={{color:'#10b981'}}>Vested:</strong> {cfg.wifeCasService.toFixed(2)} yrs &gt; 5-yr pre-2012 threshold. Termination today still preserves the deferred pension at age 62.</div>
                  <div>✓ <strong style={{color:'#10b981'}}>Healthcare tier:</strong> post-2007 schedule confirmed by hire date. Just crossed the 15-yr threshold ({cfg.wifeCasService.toFixed(2)}≥15) → eligible for 50% state-paid premium today.</div>
                </>
              )}
              <div style={{borderTop:'1px solid #1e2a3a',marginTop:6,paddingTop:6,fontSize:10,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em'}}>Still to verify</div>
              <div>› <strong style={{color:'#e2e8f0'}}>Beneficiary review:</strong> Block 6 lists you (primary) and Norah + Emerson (secondary). The order-of-priority for survivor pension defaults to spouse first per SPD §4 — confirm this still reflects your wishes.</div>
              <div>› <strong style={{color:'#e2e8f0'}}>Sick leave balance:</strong> ask Red Clay HR for her current unused sick leave count. Every 21 days = 1 month of service (max 12) at retirement, costed at 5% × FAE.</div>
              <div>› <strong style={{color:'#e2e8f0'}}>FAE optimization:</strong> her three highest periods on the CAS were {(cfg.wifeCasPeriods||[]).map(p=>`${p.label} (${fmtCur(p.amount)})`).join(', ')}{(cfg.wifeCasPeriods||[]).some(p=>p.note)?` — ${(cfg.wifeCasPeriods||[]).filter(p=>p.note).map(p=>`${p.label} is ${p.note}`).join('; ')}`:''}. They are listed with their arithmetic under Compensation → Hers. Final-3-year salaries dominate FAE — pay-step jumps and any late-career stipends compound for life.</div>
              <div>› <strong style={{color:'#e2e8f0'}}>Survivor election timing:</strong> the irrevocable 50/66.67/75/100% choice locks at first deposit. Pre-Retirement Workshop attendance recommended to model both lives' actuarial expectations.</div>
            </div>
          </div>
        </div>
      </div>

      {/* ── Early retirement scenario ── */}
      <div className="section-header" style={{marginTop:24}}>Early Retirement Scenario</div>
          {/* ── Early Retirement + Contract Work calculator ── */}
          <div className="card" style={{borderColor:'rgba(99,102,241,0.4)',marginBottom:16}}>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:10,flexWrap:'wrap'}}>
              <span>💼</span>
              <span style={{fontWeight:600,color:'#6366f1',fontSize:12}}>Early Retire + Contract Work Calculator</span>
              <span style={{marginLeft:'auto',fontSize:10,color:'#64748b'}}>retire at {earlyRetireScenario.earlyYrs} vs {earlyRetireScenario.fullYrs} yrs of service</span>
            </div>
            <div style={{fontSize:10,color:'#64748b',marginBottom:10,lineHeight:1.5}}>
              Models the lifetime trade-off: retire early (with the SEPP early-retirement reduction + smaller FAE + fewer service yrs) and pick up contract work for the {earlyRetireScenario.gap}-yr gap, vs. work the full {earlyRetireScenario.fullYrs} years. Tells you the break-even contract income for net-zero lifetime household value.
            </div>
        <div className="grid-2" style={{gap:14,alignItems:'start'}}>
          <div>
            {/* Inputs */}
            <div className="grid-2" style={{gap:10,marginBottom:10}}>
              <F label="Early retirement svc yrs" k="earlyRetireYrs" min={15} max={29} step={1}/>
              <F label="Full retirement svc yrs" k="wifeYearsService" min={20} max={40} step={1}/>
              <F label="Contract income / yr" k="contractAnnual" step={5000} prefix="$"/>
              <F label="DSHBP retiree health /mo" k="earlyRetireHealthMo" step={50} prefix="$"/>
            </div>

            {/* Headline numbers grid */}
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:10}}>
              <div style={{padding:'10px 12px',background:'rgba(16,185,129,0.07)',border:'1px solid rgba(16,185,129,0.25)',borderRadius:6}}>
                <div style={{fontSize:9,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:3}}>Scenario A · Work full {earlyRetireScenario.fullYrs} yrs</div>
                <div style={{fontSize:18,fontWeight:800,color:'#10b981',marginBottom:4}}>{fmtCur(earlyRetireScenario.netP30)}/yr</div>
                <div style={{fontSize:10,color:'#94a3b8',lineHeight:1.5}}>
                  Net pension forever (no reduction). FAE {fmtCur(earlyRetireScenario.faeAtFull)}. Retires at age {earlyRetireScenario.ageAtFull.toFixed(0)} in {Math.round(earlyRetireScenario.yrsToFull)} yrs.
                </div>
              </div>
              <div style={{padding:'10px 12px',background:'rgba(245,158,11,0.07)',border:'1px solid rgba(245,158,11,0.25)',borderRadius:6}}>
                <div style={{fontSize:9,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:3}}>Scenario B · Retire at {earlyRetireScenario.earlyYrs} + contract</div>
                <div style={{fontSize:18,fontWeight:800,color:'#f59e0b',marginBottom:4}}>{fmtCur(earlyRetireScenario.netP25)}/yr</div>
                <div style={{fontSize:10,color:'#94a3b8',lineHeight:1.5}}>
                  Net pension forever ({earlyRetireScenario.eligEarlyLabel}). FAE {fmtCur(earlyRetireScenario.faeAtEarly)}. Retires at age {earlyRetireScenario.ageAtEarly.toFixed(0)} in {Math.round(earlyRetireScenario.yrsToEarly)} yrs.
                </div>
              </div>
            </div>

            {/* Break-even callout */}
            <div style={{padding:'12px 14px',background:'rgba(99,102,241,0.08)',border:'1px solid rgba(99,102,241,0.3)',borderRadius:8,marginBottom:10}}>
              <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:10,flexWrap:'wrap'}}>
                <div>
                  <div style={{fontSize:9,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:3}}>Break-even contract income</div>
                  <div style={{fontSize:24,fontWeight:800,color:'#6366f1'}}>{fmtCur(earlyRetireScenario.breakEvenContract)}/yr</div>
                  <div style={{fontSize:10,color:'#94a3b8',marginTop:2}}>required for {earlyRetireScenario.gap} yrs to offset lifetime pension loss</div>
                </div>
                <div style={{textAlign:'right'}}>
                  <div style={{fontSize:9,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:3}}>Permanent pension hit</div>
                  <div style={{fontSize:20,fontWeight:700,color:'#ef4444'}}>–{fmtCur(earlyRetireScenario.pensionDelta)}/yr</div>
                  <div style={{fontSize:10,color:'#94a3b8',marginTop:2}}>× ~{earlyRetireScenario.expectedPensionYrs} expected yrs = –{fmtCur(earlyRetireScenario.pensionDelta * earlyRetireScenario.expectedPensionYrs)} lifetime</div>
                </div>
              </div>
              <div style={{marginTop:8,paddingTop:8,borderTop:'1px solid rgba(99,102,241,0.2)',fontSize:11,color:'#94a3b8',lineHeight:1.5}}>
                At your current contract assumption of <strong style={{color:'#e2e8f0'}}>{fmtCur(cfg.contractAnnual)}/yr</strong>: lifetime household value is{' '}
                <strong style={{color: earlyRetireScenario.lifetimeDeficit > 0 ? '#ef4444' : '#10b981'}}>
                  {earlyRetireScenario.lifetimeDeficit > 0
                    ? `${fmtCur(earlyRetireScenario.lifetimeDeficit)} short`
                    : `${fmtCur(-earlyRetireScenario.lifetimeDeficit)} better`} vs working through {earlyRetireScenario.fullYrs} yrs
                </strong>.
                {earlyRetireScenario.crossoverAge && ` Cumulative lines cross at her age ${earlyRetireScenario.crossoverAge}.`}
              </div>
            </div>

            {/* Component breakdown — what's actually being traded */}
            <div style={{padding:'8px 12px',background:'#0d1117',border:'1px solid #1e2a3a',borderRadius:6,fontSize:11,lineHeight:1.6,color:'#94a3b8'}}>
              <div style={{fontSize:9,color:'#64748b',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:6}}>What's being traded (per year during the {earlyRetireScenario.gap}-yr gap)</div>
              <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8}}>
                <div>
                  <div style={{color:'#10b981',fontWeight:600,marginBottom:3}}>Working scenario gives up:</div>
                  <div>• Salary <strong style={{color:'#e2e8f0'}}>{fmtCur(earlyRetireScenario.salaryDuringGap)}</strong></div>
                  <div>• Employer 403(b) <strong style={{color:'#e2e8f0'}}>{fmtCur(earlyRetireScenario.employerCtrib)}</strong></div>
                  <div style={{color:'#64748b',marginTop:3}}>= total <strong style={{color:'#10b981'}}>{fmtCur(earlyRetireScenario.annualWorking)}/yr</strong></div>
                </div>
                <div>
                  <div style={{color:'#f59e0b',fontWeight:600,marginBottom:3}}>Early + contract receives:</div>
                  <div>• Contract <strong style={{color:'#e2e8f0'}}>{fmtCur(cfg.contractAnnual)}</strong></div>
                  <div>• Early pension <strong style={{color:'#e2e8f0'}}>{fmtCur(earlyRetireScenario.netP25)}</strong></div>
                  <div>• Less DSHBP <strong style={{color:'#ef4444'}}>–{fmtCur(earlyRetireScenario.dshbpAnnual)}</strong></div>
                  <div>• Less SE tax <strong style={{color:'#ef4444'}}>–{fmtCur(earlyRetireScenario.seTaxIncremental)}</strong></div>
                  <div style={{color:'#64748b',marginTop:3}}>= total <strong style={{color:'#f59e0b'}}>{fmtCur(earlyRetireScenario.annualEarlyRet)}/yr</strong></div>
                </div>
              </div>
              <div style={{marginTop:8,paddingTop:8,borderTop:'1px solid #1e2a3a',fontSize:10,color:'#64748b'}}>
                Not modeled: ordinary income tax (similar in both), Social Security PIA reduction from stopping W-2 earnings (contract still contributes), 403(b) compounding growth in the working scenario, any reduction in survivor benefit from a smaller pension base. Lifetime horizon ends at age 87.
              </div>
            </div>
          </div>
          <div>
            {/* Gap-year cash-flow comparison */}
            <div style={{marginBottom:10}}>
              <div style={{fontSize:10,color:'#64748b',marginBottom:6,textTransform:'uppercase',letterSpacing:'0.05em'}}>Annual cash flow during the {earlyRetireScenario.gap}-yr gap</div>
              <ResponsiveContainer width="100%" height={220}>
                <BarChart data={earlyRetireScenario.gapCashFlow} margin={{top:6,right:8,left:0,bottom:0}}>
                  <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
                  <XAxis dataKey="yr" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}/>
                  <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={v=>fmtCur(v)}/>
                  <Tooltip formatter={v=>fmtCur(v)} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                  <Legend wrapperStyle={{fontSize:10}} iconSize={10}/>
                  <Bar dataKey="working" fill="#10b981" name={`Work to ${earlyRetireScenario.fullYrs}`} radius={[3,3,0,0]}/>
                  <Bar dataKey="earlyRet" fill="#f59e0b" name={`Retire ${earlyRetireScenario.earlyYrs} + contract`} radius={[3,3,0,0]}/>
                </BarChart>
              </ResponsiveContainer>
            </div>

            {/* Cumulative lifetime household value */}
            <div style={{marginBottom:10}}>
              <div style={{fontSize:10,color:'#64748b',marginBottom:6,textTransform:'uppercase',letterSpacing:'0.05em'}}>Cumulative household value · gap + lifetime pension</div>
              <ResponsiveContainer width="100%" height={260}>
                <LineChart data={earlyRetireScenario.cumulative} margin={{top:6,right:8,left:0,bottom:0}}>
                  <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
                  <XAxis dataKey="age" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} label={{value:`Wife's age`,position:'insideBottom',offset:-2,fontSize:10,fill:'#475569'}}/>
                  <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={v=>fmtCur(v)}/>
                  <Tooltip formatter={v=>fmtCur(v)} labelFormatter={l=>`Age ${l}`} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                  <Legend wrapperStyle={{fontSize:10}} iconSize={10}/>
                  <ReferenceLine x={earlyRetireScenario.ageAtFull} stroke="#10b981" strokeDasharray="4 2" label={{value:`${earlyRetireScenario.fullYrs}-yr retire`,position:'top',fontSize:9,fill:'#10b981'}}/>
                  {earlyRetireScenario.crossoverAge && <ReferenceLine x={earlyRetireScenario.crossoverAge} stroke="#ef4444" strokeDasharray="2 2" label={{value:'crossover',position:'top',fontSize:9,fill:'#ef4444'}}/>}
                  <Line type="monotone" dataKey="work"  stroke="#10b981" strokeWidth={2} dot={false} name={`Work to ${earlyRetireScenario.fullYrs} yrs`}/>
                  <Line type="monotone" dataKey="early" stroke="#f59e0b" strokeWidth={2} dot={false} name={`Retire ${earlyRetireScenario.earlyYrs} + contract`}/>
                </LineChart>
              </ResponsiveContainer>
            </div>
          </div>
        </div>
          </div>{/* /Early Retire card */}

      {/* ── Action Timeline ── */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:16}}>
          <div className="page-title" style={{marginBottom:0,fontSize:15}}>Retirement Action Timeline</div>
          <div style={{fontSize:11,color:'#64748b'}}>Key decisions and moves from now to age 90</div>
        </div>
        <div style={{position:'relative',paddingLeft:28}}>
          {/* vertical line */}
          <div style={{position:'absolute',left:10,top:0,bottom:0,width:2,background:'#1e2a3a',borderRadius:2}}/>
          <div style={{display:'flex',flexDirection:'column',gap:20}}>
            {actionTimeline.map((ev,i)=>(
              <div key={i} style={{position:'relative'}}>
                {/* dot */}
                <div style={{position:'absolute',left:-23,top:3,width:14,height:14,borderRadius:'50%',background:ev.color,border:'2px solid #0f172a',display:'flex',alignItems:'center',justifyContent:'center',fontSize:8}}>
                  {ev.phase==='now'&&<div style={{width:6,height:6,borderRadius:'50%',background:'#fff'}}/>}
                </div>
                <div style={{background:'#0d1117',border:`1px solid ${ev.color}30`,borderRadius:10,padding:'10px 14px'}}>
                  <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:8,flexWrap:'wrap'}}>
                    <span style={{fontSize:14}}>{ev.icon}</span>
                    <span style={{fontWeight:700,fontSize:13,color:ev.color}}>{ev.label}</span>
                    <span style={{fontSize:10,color:'#475569',marginLeft:'auto'}}>{ev.year}</span>
                  </div>
                  <div style={{display:'flex',flexDirection:'column',gap:4}}>
                    {ev.actions.map((a,j)=>(
                      <div key={j} style={{display:'flex',gap:8,fontSize:11,color:'#94a3b8',lineHeight:1.5}}>
                        <span style={{color:ev.color,flexShrink:0,marginTop:1}}>›</span>
                        <span>{a}</span>
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* 401k Contribution Calculator */}
      <Contrib401kCalc ytd={ytd} payslipCount={payslipCount} currentAge={cfg.currentAge} salary={cfg.salary}/>
    </div>
    </RetCfgCtx.Provider>
  );
}

// ── 401k Contribution Calculator ─────────────────────────────────────────────
function Contrib401kCalc({ ytd, payslipCount, currentAge, salary }) {
  const currentYear = new Date().getFullYear();
  const [selYear, setSelYear] = useState(currentYear);
  const [payFreq, setPayFreq] = useState(26);   // 26=biweekly, 24=semi-monthly, 12=monthly
  const [overrideYtd, setOverrideYtd] = useState('');

  // Year-keyed limits from lib/taxConstants — stale means the selected year
  // has no entry yet and we're showing the latest table we have.
  const yearTax = window.TaxConstants.k401ForYear(selYear);
  const limits = yearTax.k401;
  const catchUpEligible = currentAge >= 50;
  // Special super catch-up for ages 60-63 starting 2025
  const superCatchUp = selYear >= 2025 && currentAge >= 60 && currentAge <= 63;
  const catchUpAmt = superCatchUp ? (limits.superCatchUp60to63||limits.catchUp50) : limits.catchUp50;
  const totalLimit = limits.employee + (catchUpEligible ? catchUpAmt : 0);

  // YTD from payslips or manual override
  const ytdAmt = overrideYtd !== '' ? (Number(overrideYtd)||0) : (ytd?.retirement401k||0);

  // Pay periods: estimate remaining based on payslip count if current year
  const periodsElapsed = selYear === currentYear ? (payslipCount||0) : payFreq;
  const periodsRemaining = Math.max(0, payFreq - periodsElapsed);
  const remaining = Math.max(0, totalLimit - ytdAmt);
  const perPaycheck = periodsRemaining > 0 ? remaining / periodsRemaining : 0;
  const pct = totalLimit > 0 ? Math.min(ytdAmt / totalLimit * 100, 100) : 0;
  const onTrack = periodsElapsed > 0 && periodsRemaining > 0
    ? (ytdAmt / periodsElapsed) * payFreq >= totalLimit * 0.95
    : null;

  const [overrideSalary, setOverrideSalary] = useState('');
  const effectiveSalary = overrideSalary !== '' ? (Number(overrideSalary)||0) : (salary||0);
  const icPctNeeded = effectiveSalary > 0 ? totalLimit / effectiveSalary * 100 : 0;
  const icPctCurrentYtd = effectiveSalary > 0 && periodsElapsed > 0
    ? (ytdAmt / periodsElapsed * payFreq) / effectiveSalary * 100 : 0;
  const perPaycheckNeeded = effectiveSalary > 0 ? totalLimit / payFreq : 0;
  const icPctPerPaycheck = effectiveSalary > 0 ? perPaycheckNeeded / (effectiveSalary / payFreq) * 100 : 0;

  const years = window.TaxConstants.availableYears().slice().sort((a,b)=>b-a);

  return (
    <div className="card" style={{marginTop:16}}>
      <div className="label" style={{marginBottom:16}}>401(k) Contribution Calculator
        {yearTax.stale && <span style={{fontSize:10,color:'#f59e0b',marginLeft:8,padding:'2px 6px',background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',borderRadius:4,textTransform:'none',letterSpacing:'normal'}}>no {yearTax.requestedYear} limits yet — showing {yearTax.year}</span>}
      </div>

      <div className="grid-4" style={{marginBottom:16}}>
        <div>
          <div className="label">Year</div>
          <select value={selYear} onChange={e=>setSelYear(Number(e.target.value))}>
            {years.map(y=><option key={y} value={y}>{y}</option>)}
          </select>
        </div>
        <div>
          <div className="label">Pay Frequency</div>
          <select value={payFreq} onChange={e=>setPayFreq(Number(e.target.value))}>
            <option value={26}>Biweekly (26/yr)</option>
            <option value={24}>Semi-monthly (24/yr)</option>
            <option value={12}>Monthly (12/yr)</option>
            <option value={52}>Weekly (52/yr)</option>
          </select>
        </div>
        <div>
          <div className="label">YTD Contributed</div>
          <input type="number" value={overrideYtd} onChange={e=>setOverrideYtd(e.target.value)}
            placeholder={ytdAmt ? String(Math.round(ytdAmt)) : '0'}
            style={{width:'100%'}}/>
          {ytd?.retirement401k>0 && overrideYtd==='' && <div style={{fontSize:10,color:'#3b82f6',marginTop:3}}>from payslips</div>}
        </div>
        <div>
          <div className="label">Periods Remaining</div>
          <input type="number" value={periodsRemaining} readOnly style={{width:'100%',background:'#0d1117',color:'#64748b'}}/>
          <div style={{fontSize:10,color:'#475569',marginTop:3}}>{periodsElapsed} of {payFreq} elapsed</div>
        </div>
      </div>

      {/* Progress bar */}
      <div style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',marginBottom:6,fontSize:12}}>
          <span style={{color:'#94a3b8'}}>Progress to {selYear} limit</span>
          <span style={{fontWeight:700,color:pct>=100?'#10b981':pct>=75?'#f59e0b':'#94a3b8'}}>{pct.toFixed(1)}%</span>
        </div>
        <div style={{background:'#1e2a3a',borderRadius:6,height:10,overflow:'hidden'}}>
          <div style={{width:`${pct}%`,height:'100%',background:pct>=100?'#10b981':pct>=75?'#f59e0b':'#3b82f6',borderRadius:6,transition:'width 0.3s'}}/>
        </div>
      </div>

      {/* Key numbers */}
      <div className="grid-4" style={{marginBottom:16}}>
        <div className="card-sm">
          <div className="label">IRS Limit</div>
          <div style={{fontSize:16,fontWeight:700,color:'#3b82f6'}}>{fmtCur(totalLimit)}</div>
          {catchUpEligible && <div style={{fontSize:10,color:'#64748b',marginTop:2}}>{superCatchUp?'super ':''}catch-up +{fmtCur(catchUpAmt)}</div>}
        </div>
        <div className="card-sm">
          <div className="label">YTD Contributed</div>
          <div style={{fontSize:16,fontWeight:700,color:'#10b981'}}>{fmtCur(ytdAmt)}</div>
        </div>
        <div className="card-sm">
          <div className="label">Remaining</div>
          <div style={{fontSize:16,fontWeight:700,color:remaining===0?'#10b981':'#f59e0b'}}>{fmtCur(remaining)}</div>
        </div>
        <div className="card-sm">
          <div className="label">Per Paycheck</div>
          <div style={{fontSize:16,fontWeight:700,color:perPaycheck>0?'#e2e8f0':'#475569'}}>
            {periodsRemaining>0 ? fmtCur(perPaycheck) : remaining===0 ? '✓ Maxed' : '—'}
          </div>
          {onTrack!==null && <div style={{fontSize:10,color:onTrack?'#10b981':'#ef4444',marginTop:2}}>{onTrack?'on track':'behind pace'}</div>}
        </div>
      </div>

      {/* IC% of Salary to Max */}
      <div style={{background:'rgba(16,185,129,0.07)',border:'1px solid rgba(16,185,129,0.15)',borderRadius:8,padding:12,marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
          <div style={{fontWeight:600,fontSize:12}}>% of Salary to Max 401(k)</div>
          <div style={{display:'flex',alignItems:'center',gap:6}}>
            <span style={{fontSize:11,color:'#64748b'}}>$</span>
            <input type="number" value={overrideSalary} onChange={e=>setOverrideSalary(e.target.value)}
              placeholder={effectiveSalary?String(Math.round(effectiveSalary)):'salary'}
              style={{width:110,fontSize:12}}/>
          </div>
        </div>
        <div style={{display:'grid',gridTemplateColumns:'repeat(3,1fr)',gap:10}}>
          <div style={{textAlign:'center'}}>
            <div style={{fontSize:11,color:'#64748b',marginBottom:4}}>IC% to max limit</div>
            <div style={{fontSize:22,fontWeight:800,color:'#10b981'}}>{effectiveSalary>0?icPctNeeded.toFixed(1)+'%':'—'}</div>
            <div style={{fontSize:10,color:'#475569'}}>{fmtCur(totalLimit)}/yr ÷ salary</div>
          </div>
          <div style={{textAlign:'center'}}>
            <div style={{fontSize:11,color:'#64748b',marginBottom:4}}>Per paycheck needed</div>
            <div style={{fontSize:22,fontWeight:800,color:'#3b82f6'}}>{effectiveSalary>0?fmtCur(Math.round(perPaycheckNeeded)):'—'}</div>
            <div style={{fontSize:10,color:'#475569'}}>{effectiveSalary>0?icPctPerPaycheck.toFixed(1)+'% of gross check':''}</div>
          </div>
          <div style={{textAlign:'center'}}>
            <div style={{fontSize:11,color:'#64748b',marginBottom:4}}>Current pace</div>
            <div style={{fontSize:22,fontWeight:800,color:icPctCurrentYtd>0&&icPctCurrentYtd>=icPctNeeded*0.95?'#10b981':'#f59e0b'}}>
              {icPctCurrentYtd>0?icPctCurrentYtd.toFixed(1)+'%':'—'}
            </div>
            <div style={{fontSize:10,color:'#475569'}}>annualized from YTD</div>
          </div>
        </div>
      </div>

      {/* Historical limits table */}
      <div className="label" style={{marginBottom:8}}>IRS Limits by Year</div>
      <table>
        <thead>
          <tr>
            <th>Year</th>
            <th style={{textAlign:'right'}}>Employee Limit</th>
            <th style={{textAlign:'right'}}>Catch-up (50+)</th>
            <th style={{textAlign:'right'}}>Max w/ Catch-up</th>
          </tr>
        </thead>
        <tbody>
          {years.map(y=>{
            const l=window.TaxConstants.YEARS[y].k401;
            const isCurrentYear = y===currentYear;
            return (
              <tr key={y} style={isCurrentYear?{background:'rgba(59,130,246,0.06)'}:{}}>
                <td style={{fontWeight:isCurrentYear?700:400,color:isCurrentYear?'#3b82f6':'inherit'}}>{y}{isCurrentYear?' ★':''}</td>
                <td style={{textAlign:'right'}}>{fmtCur(l.employee)}</td>
                <td style={{textAlign:'right',color:'#64748b'}}>{fmtCur(l.catchUp50)}</td>
                <td style={{textAlign:'right',fontWeight:600}}>{fmtCur(l.employee+l.catchUp50)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

window.FinanceViews = Object.assign(window.FinanceViews || {}, { Retirement });
