// financial/src/compensation.jsx — the Compensation section.
//
// Two sub-tabs: Overview (income Sankey, comp-vs-market, year table, comp
// ladder, insight charts) and Payslips (market config, payslip image upload
// → parsePayslip, bulk import, manual entry). The comp-mix constants
// (IC_SPLITS ladder interpolation) and the Sankey helpers live here too.
//
// COMP_LADDER / HISTORICAL_COMP / generateCompProjection live in
// src/shared.jsx because the shell's DataProvider seeds and projects the
// compensation dataset with the same rules this page charts.
//
// Slices run in their own Babel scope. src/shared.jsx runs first and
// publishes window.FinanceShared; nothing here may touch firebase or
// FinanceShared at module scope beyond the destructures below.

const { useState, useEffect, useContext, useMemo, useRef, useCallback } = React;
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, AreaChart, Area,
        ComposedChart, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid,
        Tooltip, Legend, ReferenceLine, Sankey } = window.Recharts;
const { DataContext, useToast, callFn,
        fmtCur, fmtCurFull, fmtPct, fmtPctDirect, fmtDate, fmtMonthYear, timeAgo,
        ErrorBoundary, COMP_LADDER, HISTORICAL_COMP } = window.FinanceShared;

const getCurrentYear = () => new Date().getFullYear();

// ── Sankey helpers (defined at module level so Recharts cloneElement works) ──
const SANKEY_COLORS = {
  'Base Salary':'#10b981','Cash Bonus':'#f59e0b','RSU / Equity':'#8b5cf6',
  'Gross Pay':'#10b981',
  'Pre-tax Deductions':'#3b82f6',
  '401(k)':'#60a5fa','Health & Benefits':'#a78bfa','Other Pre-tax':'#475569',
  'Taxes':'#ef4444',
  'Federal Income Tax':'#ef4444','State Income Tax':'#dc2626','SS + Medicare':'#f97316',
  'Net Pay':'#06b6d4','Savings / Unspent':'#06b6d4',
};
function SankeyNodeShape({ x, y, width, height, payload, index, hasOutgoing }) {
  const fill = SANKEY_COLORS[payload?.name] || '#475569';
  const right = hasOutgoing?.has?.(index);
  return (
    <g>
      <rect x={x} y={y} width={width} height={height} fill={fill} rx={2} fillOpacity={0.9}/>
      <text x={right ? x+width+7 : x-7} y={y+height/2} textAnchor={right?'start':'end'}
        fill="#cbd5e1" fontSize={10} dominantBaseline="middle">{payload?.name}</text>
    </g>
  );
}

// ── Comp structure constants ──────────────────────────────────────────────────
// IC → Cash/Stock split rules (by IC total)
// The IC ladder moved to lib/compHistory (mirrored, tested) because the rule
// has a STEP in it and a copy of a step is a copy that gets applied on the
// wrong side of the line: the 2026 projection was recorded with a fifth of the
// award in stock on an IC of $97,251, which is below the $100k trigger, and
// overstated the equity by ~$9,700.
const IC_SPLITS = window.CompHistory.IC_SPLITS;
const splitIC = ic => window.CompHistory.icSplit(ic);

// currentYearTC pulls from the "current" calendar year first; falls back to
// previous year when this year hasn't been entered yet.
// TC = base + bonus + rsu. The rule itself lives in lib/compHistory (mirrored
// server-side) so the Compensation tab, the career snapshot's market verdict
// and the coach all read one definition; this keeps the tab's existing shape.
const getCurrentYearTC = (compensation) => {
  const yr = getCurrentYear();
  const row = compensation?.[yr] || compensation?.[yr-1] || {};
  return {
    year: compensation?.[yr] ? yr : (compensation?.[yr-1] ? yr-1 : yr),
    isCurrent: !!compensation?.[yr],
    baseSalary: row.baseSalary || 0,
    bonus: row.bonus || 0,
    rsu: row.rsu || 0,
    total: window.CompHistory.yearTC(row),
  };
};

// CPI for inflation-adjusted comp. Module-scope on purpose: as a per-render
// object literal inside Compensation it sat in insightData's dependency array
// and defeated the memo every render.
const CPI_BY_YEAR = {
  2010:218.1,2011:224.9,2012:229.6,2013:233.0,2014:236.7,2015:237.0,
  2016:240.0,2017:245.1,2018:251.1,2019:255.7,2020:258.8,2021:271.0,
  2022:292.7,2023:304.7,2024:314.2,2025:322.0,2026:329.0,
};
const CPI_LATEST = CPI_BY_YEAR[Math.max(...Object.keys(CPI_BY_YEAR).map(Number))];

// ── Projection config panel ───────────────────────────────────────────────────
function ProjectionConfigPanel({ config, onSave }) {
  const defaults = { salaryGrowthPct: 5, yearsOut: 10, enabled: true };
  const [draft, setDraft] = useState({ ...defaults, ...(config||{}) });
  useEffect(() => { setDraft({ ...defaults, ...(config||{}) }); }, [config]);
  const [saving, setSaving] = useState(false);
  const handleSave = async () => {
    setSaving(true);
    await onSave(draft);
    setSaving(false);
  };
  const dirty = JSON.stringify(draft) !== JSON.stringify({ ...defaults, ...(config||{}) });
  return (
    <div className="card" style={{marginBottom:16}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
        <div className="label" style={{marginBottom:0}}>Projections</div>
        <div style={{fontSize:11,color:'#64748b'}}>IC is interpolated from the Comp Ladder as salary grows</div>
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(160px,1fr))',gap:12,alignItems:'end'}}>
        <div>
          <div className="label" style={{marginBottom:3,fontSize:11}}>Salary Growth</div>
          <div style={{display:'flex',alignItems:'center',gap:4}}>
            <input type="number" step="0.5" min="0" max="20" value={draft.salaryGrowthPct} onChange={e=>setDraft({...draft,salaryGrowthPct:Number(e.target.value)||0})} style={{flex:1}}/>
            <span style={{fontSize:11,color:'#64748b'}}>% / yr</span>
          </div>
        </div>
        <div>
          <div className="label" style={{marginBottom:3,fontSize:11}}>Horizon</div>
          <div style={{display:'flex',alignItems:'center',gap:4}}>
            <input type="number" step="1" min="0" max="30" value={draft.yearsOut} onChange={e=>setDraft({...draft,yearsOut:Number(e.target.value)||0})} style={{flex:1}}/>
            <span style={{fontSize:11,color:'#64748b'}}>years</span>
          </div>
        </div>
        <div>
          <div className="label" style={{marginBottom:3,fontSize:11}}>Enabled</div>
          <label style={{display:'flex',alignItems:'center',gap:6,fontSize:12,color:'#94a3b8'}}>
            <input type="checkbox" checked={draft.enabled!==false} onChange={e=>setDraft({...draft,enabled:e.target.checked})}/>
            <span>Show projected years</span>
          </label>
        </div>
        <div style={{textAlign:'right'}}>
          <button className="btn-primary" onClick={handleSave} disabled={!dirty||saving} style={{fontSize:11,padding:'6px 14px'}}>
            {saving?<span className="spinner"/>:'Save Projection'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Her compensation ─────────────────────────────────────────────────────────
// The spouse side of the Compensation tab. Her pay isn't negotiated, it's
// looked up: the district publishes a schedule, she sits on a step, and the
// salary follows. So this section edits the SCHEDULE and the PLACEMENT, and
// never lets anyone type a salary and call it a fact — the total is always
// produced by lib/teacherScale from parts you can point at on the PDF.
//
// One document per fiscal year, kept forever. When the district reissues the
// scale you clone last year's, retype the cells that moved, and the prior year
// still says what it always said. That's the whole reason this replaced three
// hardcoded constants: the constants could only ever describe one year, and
// updating them erased the year before.

// ── Her payslips ──────────────────────────────────────────────────────────
// The same pipeline as his, pointed at a sibling collection. Everything
// downstream — the column repair, the YTD totals, the Tax section's
// projection — is shared code, so her side gains every guard his side has
// rather than a second, thinner implementation that drifts.
function SpousePayslipUpload() {
  const { compensation, spousePayslipsByYear, reload } = useContext(DataContext);
  const { show, Toast } = useToast();
  const [busy, setBusy] = useState(false);
  const [queue, setQueue] = useState([]);
  const year = new Date().getFullYear();
  const rows = (spousePayslipsByYear && spousePayslipsByYear[String(year)]) || [];
  const ytd = (compensation[String(year)] || {}).spouseYtd || null;

  const process = async (files) => {
    const arr = [...files];
    if (!arr.length) return;
    setBusy(true);
    setQueue(arr.map(f => ({ name: f.name, status: 'pending' })));
    for (let i = 0; i < arr.length; i++) {
      setQueue(q => q.map((x, j) => j === i ? { ...x, status: 'parsing' } : x));
      try {
        const b64 = await new Promise((res, rej) => {
          const r = new FileReader();
          r.onload = ev => res(ev.target.result.split(',')[1]);
          r.onerror = rej;
          r.readAsDataURL(arr[i]);
        });
        await callFn('parsePayslip', {
          imageBase64: b64, mimeType: arr[i].type || 'image/png',
          fallbackYear: year, person: 'spouse',
        });
        setQueue(q => q.map((x, j) => j === i ? { ...x, status: 'done' } : x));
      } catch (e) {
        setQueue(q => q.map((x, j) => j === i ? { ...x, status: 'error', error: e.message } : x));
      }
    }
    await reload();
    show(`${arr.length} payslip${arr.length !== 1 ? 's' : ''} processed`);
    setBusy(false);
  };

  return (
    <div className="card" style={{marginBottom:16}}>
      {Toast}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:10,marginBottom:6}}>
        <div className="label" style={{marginBottom:0}}>Her payslips</div>
        <label className="btn-primary" style={{padding:'6px 14px',fontSize:11,cursor:busy?'default':'pointer',opacity:busy?0.6:1}}>
          {busy ? 'Reading…' : 'Upload payslips'}
          <input type="file" accept="image/*,application/pdf" multiple disabled={busy}
            style={{display:'none'}} onChange={e => { process(e.target.files); e.target.value = ''; }}/>
        </label>
      </div>
      <div style={{fontSize:11,color:'#64748b',marginBottom:12,lineHeight:1.6,maxWidth:'70ch'}}>
        Her statements run through the same parser, the same wrong-column repair and the same totals as yours.
        With them on file the Tax section measures her withholding instead of modelling it from a naive W-4 —
        which is the last estimated figure in the April balance.
      </div>

      {queue.length > 0 && (
        <div style={{display:'grid',gap:3,marginBottom:12}}>
          {queue.map((q,i)=>(
            <div key={i} style={{display:'flex',gap:10,fontSize:11,alignItems:'center'}}>
              <span style={{color: q.status==='error' ? '#ef4444' : q.status==='done' ? '#10b981' : '#64748b'}}>
                {q.status==='done' ? '✓' : q.status==='error' ? '✕' : '…'}
              </span>
              <span style={{color:'#94a3b8'}}>{q.name}</span>
              {q.error && <span style={{color:'#ef4444'}}>{q.error}</span>}
            </div>
          ))}
        </div>
      )}

      {rows.length === 0 ? (
        <div style={{fontSize:11,color:'#f59e0b'}}>
          None on file for {year}. Her income and withholding are currently the stated salary plus a model.
        </div>
      ) : (
        <>
          <div style={{display:'flex',gap:18,flexWrap:'wrap',fontSize:11,color:'#64748b',marginBottom:10}}>
            <span><strong style={{color:'#e2e8f0'}}>{rows.length}</strong> statements in {year}</span>
            {ytd && <span>gross <strong style={{color:'#e2e8f0'}}>{fmtCur(ytd.grossPay)}</strong></span>}
            {ytd && <span>federal withheld <strong style={{color:'#e2e8f0'}}>{fmtCur(ytd.federalTax)}</strong></span>}
            {ytd && <span>403(b) <strong style={{color:'#e2e8f0'}}>{fmtCur(ytd.retirement401k)}</strong></span>}
          </div>
          <div style={{overflowX:'auto'}}>
            <table>
              <thead><tr>
                <th>Pay date</th><th style={{textAlign:'right'}}>Gross</th>
                <th style={{textAlign:'right'}}>Federal</th><th style={{textAlign:'right'}}>State</th>
                <th style={{textAlign:'right'}}>403(b)</th><th style={{textAlign:'right'}}>Net</th>
              </tr></thead>
              <tbody>
                {[...rows].sort((a,b)=>String(b.payDate||'').localeCompare(String(a.payDate||''))).map((r,i)=>(
                  <tr key={i}>
                    <td style={{color:'#e2e8f0'}}>{r.payDate || '—'}</td>
                    <td style={{textAlign:'right'}}>{fmtCur(r.grossPay)}</td>
                    <td style={{textAlign:'right',color:'#94a3b8'}}>{fmtCur(r.federalTax)}</td>
                    <td style={{textAlign:'right',color:'#94a3b8'}}>{fmtCur(r.stateTax)}</td>
                    <td style={{textAlign:'right',color:'#94a3b8'}}>{fmtCur(r.retirement401k)}</td>
                    <td style={{textAlign:'right',color:'#94a3b8'}}>{fmtCur(r.netPay)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </>
      )}
    </div>
  );
}

function WifeCompSection() {
  const { wifeComp, retirementConfig, saveWifeCompYear, deleteWifeCompYear } = useContext(DataContext);
  const { show, Toast } = useToast();
  const [editYear, setEditYear] = useState(null);
  const [draft, setDraft] = useState(null);
  const [saving, setSaving] = useState(false);
  const [pasteText, setPasteText] = useState('');
  const [pasteResult, setPasteResult] = useState(null);

  const activeFy = TeacherScale.activeYear(wifeComp||{});
  const retStep = Math.floor(retirementConfig?.wifeCurrentService || 0) || null;
  const retCert = !!retirementConfig?.wifeNationallyCertified;

  // The seed schedules ship without a placement — a published pay scale says
  // nothing about which step she's on. Rather than showing the newest year as
  // salary-less, borrow the step the Retirement tab already holds, but ONLY
  // for the current year and clearly flagged. Older years are left blank on
  // purpose: applying today's step backwards would invent a salary history.
  const effectiveYears = useMemo(()=>{
    const out = {};
    for (const [y, doc] of Object.entries(wifeComp||{})) {
      const blank = doc.step == null || doc.step === '';
      out[y] = (blank && Number(y) === activeFy && retStep)
        ? { ...doc, step: retStep, natCertified: retCert, _stepFromRetirement: true }
        : doc;
    }
    return out;
  }, [wifeComp, activeFy, retStep, retCert]);

  // The CAS earnings periods, and the arithmetic that checks them: their mean
  // IS the CAS FAE, so a mismatch means one of the two was mistyped.
  const casPeriods = (retirementConfig?.wifeCasPeriods||[]).filter(p=>p && Number(p.amount)>0);
  const casPeriodMean = casPeriods.length
    ? Math.round(casPeriods.reduce((s,p)=>s+Number(p.amount),0)/casPeriods.length) : 0;
  const casFaeMatches = Math.abs(casPeriodMean - (retirementConfig?.wifeCasFae||0)) <= 1;

  const history = useMemo(()=>TeacherScale.buildHistory(effectiveYears), [effectiveYears]);
  const current = history.length ? history[history.length-1] : null;
  const paid = history.filter(r=>r.placed && r.total>0);
  const firstPaid = paid.length ? paid[0] : null;

  const set = (patch) => setDraft(d=>({ ...d, ...patch }));
  const setStepCell = (step, field, value) => setDraft(d=>({
    ...d, steps: { ...(d.steps||{}), [step]: { ...((d.steps||{})[step]||{}), [field]: value } },
  }));

  const openEditor = (fy) => {
    // Open on the EFFECTIVE year, not the raw stored document. The card above
    // prices the current year using the step inherited from the Retirement tab,
    // and an editor that ignored that inheritance would open showing a
    // different, lower salary for the year you just clicked — and saving it
    // would silently drop her certification. Prefilling means Save is what
    // pins the inherited placement, which is what the hint promises.
    // Deep-copy so an abandoned edit leaves the loaded data untouched.
    const doc = effectiveYears[String(fy)] || {};
    setDraft(JSON.parse(JSON.stringify({
      fiscalYear: fy, schoolYear:'', effectiveDate:'', lane:'MA+30', natCertPct:6,
      longevityTiers: [], steps: {}, step:'', natCertified:false, stipends:0,
      stipendNote:'', salaryOverride:0, notes:'', source:'', ...doc,
    })));
    setEditYear(fy);
    setPasteText(''); setPasteResult(null);
  };

  const addYear = () => {
    const base = effectiveYears[String(activeFy)] || TeacherScale.FY2027_MA30;
    const fy = (activeFy || new Date().getFullYear()) + 1;
    if ((wifeComp||{})[String(fy)]) { show(`FY${fy} already exists`, 'error'); return; }
    const clone = JSON.parse(JSON.stringify(base));
    // Clone the grid so only the cells that moved need retyping; clear the
    // things that are specific to the year it came from.
    setDraft({
      ...clone,
      fiscalYear: fy,
      schoolYear: `${fy-1}-${fy}`,
      effectiveDate: '',
      source: '',
      seeded: false,
      step: base.step != null && base.step !== '' ? Math.floor(base.step)+1 : (retStep ? retStep+1 : ''),
      natCertified: base.natCertified != null ? !!base.natCertified : retCert,
    });
    setEditYear(fy);
    setPasteText(''); setPasteResult(null);
  };

  const applyPaste = () => {
    const res = TeacherScale.parseScalePaste(pasteText);
    setPasteResult(res);
    if (!res.count) return;
    // Replace the whole grid rather than merging: a pasted schedule is the
    // schedule, and leftover rows from the year it was cloned from would
    // survive as cells nobody checked.
    set({ steps: res.steps });
  };

  const saveDraft = async () => {
    if (!draft) return;
    const calc = TeacherScale.computeSalary(draft, draft);
    if (!calc.hasGrid) { show('That year has no State/Local figures — nothing to save', 'error'); return; }
    setSaving(true);
    try {
      await saveWifeCompYear(draft.fiscalYear, TeacherScale.normalizeScale(draft));
      show(`FY${draft.fiscalYear} saved`);
      setEditYear(null); setDraft(null);
    } catch(e){ show(e.message, 'error'); }
    setSaving(false);
  };

  const removeYear = async (fy) => {
    const seeded = TeacherScale.SEED_SCALES.some(s=>s.fiscalYear===fy);
    if (!window.confirm(seeded
      ? `FY${fy} is a built-in schedule. Deleting removes your edits and restores the built-in version. Continue?`
      : `Delete FY${fy} entirely?`)) return;
    try {
      await deleteWifeCompYear(fy);
      show(seeded ? `FY${fy} reset to the built-in schedule` : `FY${fy} deleted`);
      if (editYear === fy) { setEditYear(null); setDraft(null); }
    } catch(e){ show(e.message, 'error'); }
  };

  const draftCalc = draft ? TeacherScale.computeSalary(draft, draft) : null;
  const draftSteps = draft ? Object.keys(draft.steps||{}).map(Number).filter(n=>!isNaN(n)).sort((a,b)=>a-b) : [];

  return (
    <div>
      {Toast}

      {/* Current salary — the same computeSalary the Retirement tab prices from */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',flexWrap:'wrap',gap:12}}>
          <div>
            <div className="label">Current salary{current?` · FY${current.fiscalYear}`:''}</div>
            <div className="val-lg" style={{color:'#8b5cf6'}}>{current && current.placed ? fmtCur(current.total) : '—'}</div>
            <div style={{fontSize:11,color:'#64748b',marginTop:4}}>
              {current
                ? <>{current.lane||'—'} · Step {current.placed?current.step:'not set'}
                    {current.natCertified?' · Nat\'l Board certified':''}
                    {current.schoolYear?` · SY ${current.schoolYear}`:''}
                    {current.effectiveDate?` · eff. ${current.effectiveDate}`:''}</>
                : 'No pay scale on file'}
            </div>
            {current?.inheritedStep && (
              <div style={{fontSize:10,color:'#f59e0b',marginTop:4}}>
                ℹ Step borrowed from the Retirement tab — record it on FY{current.fiscalYear} to pin it here.
              </div>
            )}
          </div>
          {current && current.delta != null && (
            <div style={{textAlign:'right'}}>
              <div className="label">vs FY{current.fiscalYear-1}</div>
              <div className="val-md" style={{color:current.delta>=0?'#10b981':'#ef4444'}}>
                {current.delta>=0?'+':''}{fmtCur(current.delta)}
              </div>
              <div style={{fontSize:11,color:'#64748b'}}>{current.deltaPct>=0?'+':''}{current.deltaPct.toFixed(1)}%</div>
            </div>
          )}
        </div>
        {current?.placed && (
          <div style={{display:'flex',flexWrap:'wrap',gap:6,marginTop:14}}>
            {current.breakdown.steps.map(s=>(
              <div key={s.label} style={{background:'#161b22',border:'1px solid #1e2a3a',borderRadius:6,padding:'5px 10px',fontSize:11}}>
                <span style={{color:'#64748b'}}>{s.label}</span>{' '}
                <strong style={{color:'#e2e8f0'}}>{fmtCur(s.amount)}</strong>
              </div>
            ))}
          </div>
        )}
        {paid.length>1 && firstPaid && (
          <div style={{fontSize:11,color:'#64748b',marginTop:12,borderTop:'1px solid #1e2a3a',paddingTop:10}}>
            {fmtCur(firstPaid.total)} in FY{firstPaid.fiscalYear} → {fmtCur(current.total)} in FY{current.fiscalYear}
            {' · '}{(((current.total/firstPaid.total)-1)*100).toFixed(1)}% over {current.fiscalYear-firstPaid.fiscalYear} year{current.fiscalYear-firstPaid.fiscalYear===1?'':'s'}
          </div>
        )}
      </div>

      {/* Recorded earnings. Everything else on this page is DERIVED — a
          schedule plus a step. These three figures are the only actual record
          of what she was paid that exists anywhere in the app, and they come
          from the Office of Pensions rather than from us. Shown separately
          from the fiscal-year history below because they are what they are:
          three overlapping 12-month windows, one of them non-calendar, not a
          year-by-year series. */}
      {casPeriods.length > 0 && (
        <div className="card" style={{marginBottom:16}}>
          <div className="section-header">Recorded earnings · Delaware CAS as of {retirementConfig?.wifeCasAsOfDate||'—'}</div>
          <div style={{fontSize:11,color:'#64748b',marginBottom:12,lineHeight:1.6}}>
            The three highest consecutive 12-month periods the Office of Pensions used to build
            Block 4. Authoritative, but not a complete history — they are the top three, not every year.
          </div>
          <div style={{overflowX:'auto'}}>
            <table>
              <thead><tr><th>Period</th><th style={{textAlign:'right'}}>Earnings</th></tr></thead>
              <tbody>
                {casPeriods.map((p,i)=>(
                  <tr key={i}>
                    <td>
                      <span style={{color:'#e2e8f0'}}>{p.label}</span>
                      {p.note && <div style={{fontSize:10,color:'#f59e0b',marginTop:2}}>{p.note}</div>}
                    </td>
                    <td style={{textAlign:'right',color:'#e2e8f0',fontWeight:600}}>{fmtCur(p.amount)}</td>
                  </tr>
                ))}
                <tr>
                  <td style={{color:'#64748b'}}>Mean — the CAS FAE (Block 4)</td>
                  <td style={{textAlign:'right',color:casFaeMatches?'#10b981':'#f59e0b',fontWeight:700}}>
                    {fmtCur(casPeriodMean)}
                    {!casFaeMatches && <div style={{fontSize:10,fontWeight:400}}>CAS states {fmtCur(retirementConfig?.wifeCasFae||0)}</div>}
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* History — one row per fiscal year, kept as the scale is reissued */}
      <div className="card" style={{marginBottom:16}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12,gap:8,flexWrap:'wrap'}}>
          <div className="section-header" style={{marginBottom:0}}>Salary history</div>
          <button className="btn-secondary" onClick={addYear} style={{padding:'5px 12px',fontSize:12}}>
            + Add FY{(activeFy||new Date().getFullYear())+1} (clone FY{activeFy||'—'})
          </button>
        </div>
        <div style={{overflowX:'auto'}}>
          <table>
            <thead>
              <tr>
                <th>Fiscal yr</th><th>School yr</th><th>Lane</th><th>Step</th>
                <th style={{textAlign:'right'}}>Salary</th><th style={{textAlign:'right'}}>Change</th><th></th>
              </tr>
            </thead>
            <tbody>
              {history.slice().reverse().map(r=>(
                <tr key={r.fiscalYear}>
                  <td>
                    <strong style={{color:'#e2e8f0'}}>FY{r.fiscalYear}</strong>
                    {r.seeded && <span className="tag tag-blue" style={{marginLeft:6}}>built-in</span>}
                    {r.overridden && <span className="tag tag-red" style={{marginLeft:6}}>override</span>}
                  </td>
                  <td className="muted">{r.schoolYear||'—'}</td>
                  <td className="muted">{r.lane||'—'}</td>
                  <td>
                    {r.placed ? r.step : <span style={{color:'#f59e0b'}}>not set</span>}
                    {r.natCertified && <span style={{color:'#8b5cf6',marginLeft:6,fontSize:10}}>NBCT</span>}
                    {r.inheritedStep && <span style={{color:'#f59e0b',marginLeft:4,fontSize:10}}>·inherited</span>}
                  </td>
                  <td style={{textAlign:'right',color:'#e2e8f0',fontWeight:600}}>{r.placed?fmtCur(r.total):'—'}</td>
                  <td style={{textAlign:'right',color:r.delta==null?'#64748b':(r.delta>=0?'#10b981':'#ef4444')}}>
                    {r.delta==null ? '—' : `${r.delta>=0?'+':''}${fmtCur(r.delta)} (${r.deltaPct>=0?'+':''}${r.deltaPct.toFixed(1)}%)`}
                  </td>
                  <td style={{textAlign:'right',whiteSpace:'nowrap'}}>
                    <button className="btn-secondary" onClick={()=>openEditor(r.fiscalYear)} style={{padding:'3px 10px',fontSize:11}}>Edit</button>
                    <button className="btn-danger" onClick={()=>removeYear(r.fiscalYear)} style={{padding:'3px 10px',fontSize:11,marginLeft:6}}>
                      {r.seeded?'Reset':'Delete'}
                    </button>
                  </td>
                </tr>
              ))}
              {!history.length && (
                <tr><td colSpan={7} className="muted" style={{textAlign:'center',padding:24}}>No pay scale years on file.</td></tr>
              )}
            </tbody>
          </table>
        </div>
      </div>

      {/* Year editor */}
      {draft && (
        <div className="card" style={{marginBottom:16,borderColor:'#8b5cf6'}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16,flexWrap:'wrap',gap:8}}>
            <div className="section-header" style={{marginBottom:0,color:'#8b5cf6'}}>Editing FY{draft.fiscalYear}</div>
            <div style={{display:'flex',gap:8}}>
              <button className="btn-secondary" onClick={()=>{setEditYear(null);setDraft(null);}} style={{padding:'6px 14px',fontSize:12}}>Cancel</button>
              <button className="btn-primary" onClick={saveDraft} disabled={saving} style={{padding:'6px 14px',fontSize:12}}>
                {saving?'Saving…':`Save FY${draft.fiscalYear}`}
              </button>
            </div>
          </div>

          {/* Live total, always shown as its parts */}
          <div style={{background:'rgba(139,92,246,0.08)',border:'1px solid rgba(139,92,246,0.25)',borderRadius:8,padding:'10px 14px',marginBottom:16}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
              <span style={{color:'#64748b',fontSize:10,letterSpacing:'0.08em',textTransform:'uppercase'}}>
                Salary at step {draftCalc.placed?draftCalc.gridStep:'—'}{draftCalc.overTopStep?'+':''}
              </span>
              <strong style={{color:'#8b5cf6',fontSize:18}}>{draftCalc.placed?fmtCur(draftCalc.total):'—'}</strong>
            </div>
            <div style={{fontSize:10,color:'#64748b',marginTop:4,lineHeight:1.6}}>
              {draftCalc.placed
                ? draftCalc.steps.map((s,i)=><React.Fragment key={s.label}>{i>0?' + ':''}{s.label} {fmtCur(s.amount)}</React.Fragment>)
                : 'Set her step for this year to price it.'}
            </div>
          </div>

          <div className="grid-4" style={{gap:12,marginBottom:12}}>
            <div>
              <div className="label">School year</div>
              <input value={draft.schoolYear||''} placeholder="2026-2027" onChange={e=>set({schoolYear:e.target.value})}/>
            </div>
            <div>
              <div className="label">Effective date</div>
              <input value={draft.effectiveDate||''} placeholder="2026-08-23" onChange={e=>set({effectiveDate:e.target.value})}/>
            </div>
            <div>
              <div className="label">Lane</div>
              {/* The app has always called her lane "MA+30" while the district
                  sheet prints "Master's + 30". Keep whatever the year already
                  says in the list so opening an old year can't silently
                  relabel it as a different lane. */}
              <select value={draft.lane||'MA+30'} onChange={e=>set({lane:e.target.value})}>
                {[...new Set([draft.lane||'MA+30', ...TeacherScale.LANES])].map(l=><option key={l} value={l}>{l}</option>)}
              </select>
            </div>
            <div>
              <div className="label">Her step this year</div>
              <input type="number" min={1} max={TeacherScale.MAX_STEP} value={draft.step??''} placeholder="not set"
                onChange={e=>set({step:e.target.value})}/>
            </div>
          </div>

          <div className="grid-4" style={{gap:12,marginBottom:12}}>
            <div>
              <div className="label">Nat'l Board cert %</div>
              <input type="number" step={0.5} value={draft.natCertPct??0} onChange={e=>set({natCertPct:e.target.value})}/>
            </div>
            <div>
              <div className="label">Stipends</div>
              <input type="number" step={100} value={draft.stipends??0} onChange={e=>set({stipends:e.target.value})}/>
            </div>
            <div>
              <div className="label">Stipend note</div>
              <input value={draft.stipendNote||''} placeholder="coaching, dept head…" onChange={e=>set({stipendNote:e.target.value})}/>
            </div>
            <div>
              <div className="label">Off-schedule override</div>
              <input type="number" step={500} value={draft.salaryOverride??0} onChange={e=>set({salaryOverride:e.target.value})}/>
            </div>
          </div>
          <label style={{display:'flex',gap:8,alignItems:'center',fontSize:12,color:'#8b5cf6',marginBottom:16}}>
            <input type="checkbox" style={{width:'auto'}} checked={!!draft.natCertified} onChange={e=>set({natCertified:e.target.checked})}/>
            <span>Nationally Board Certified this year</span>
          </label>

          {/* Longevity — entered exactly as the sheet prints it */}
          <div className="section-header">Longevity tiers</div>
          <div style={{fontSize:11,color:'#64748b',marginBottom:10,lineHeight:1.6}}>
            Enter the increments the district sheet prints, not the running totals — what's paid at a
            step is the sum of every tier reached. The FY27 sheet's
            "6-14 200 / 15-20 +300 / 21-25 +250 / 26+ +750" is four rows here, and its
            "Total Longevity $1,500" is the check: it should equal the sum of the Add column.
          </div>
          {(draft.longevityTiers||[]).map((t,i)=>(
            <div key={i} style={{display:'flex',gap:10,alignItems:'center',marginBottom:8}}>
              <span style={{fontSize:11,color:'#64748b',width:70}}>From step</span>
              <input type="number" min={1} value={t.minStep??''} style={{width:90}}
                onChange={e=>set({longevityTiers:(draft.longevityTiers||[]).map((x,j)=>j===i?{...x,minStep:e.target.value}:x)})}/>
              <span style={{fontSize:11,color:'#64748b',width:34}}>Add $</span>
              <input type="number" step={50} value={t.add??''} style={{width:110}}
                onChange={e=>set({longevityTiers:(draft.longevityTiers||[]).map((x,j)=>j===i?{...x,add:e.target.value}:x)})}/>
              <button className="btn-danger" style={{padding:'3px 10px',fontSize:11}}
                onClick={()=>set({longevityTiers:(draft.longevityTiers||[]).filter((_,j)=>j!==i)})}>Remove</button>
            </div>
          ))}
          <div style={{display:'flex',gap:10,alignItems:'center',marginBottom:16}}>
            <button className="btn-secondary" style={{padding:'4px 12px',fontSize:11}}
              onClick={()=>set({longevityTiers:[...(draft.longevityTiers||[]),{minStep:'',add:''}]})}>+ Tier</button>
            <span style={{fontSize:11,color:'#64748b'}}>
              Total longevity at the top tier: <strong style={{color:'#e2e8f0'}}>
                {fmtCur((draft.longevityTiers||[]).reduce((s,t)=>s+(Number(t.add)||0),0))}
              </strong>
            </span>
          </div>

          {/* The grid */}
          <div className="section-header">Step grid — State + Local</div>
          <div style={{fontSize:11,color:'#64748b',marginBottom:10}}>
            Paste the lane's column straight out of the district PDF — one row per line as
            <span style={{color:'#94a3b8'}}> step, state, local, total</span>. The Total column is checked
            against State + Local and rejected if it doesn't reconcile, so a mis-scraped row can't land silently.
          </div>
          <textarea rows={4} value={pasteText} placeholder={'1  47,666  20,202  67,868\n2  48,044  21,250  69,294'}
            onChange={e=>setPasteText(e.target.value)} style={{marginBottom:8,fontSize:12}}/>
          <div style={{display:'flex',gap:10,alignItems:'center',marginBottom:14,flexWrap:'wrap'}}>
            <button className="btn-secondary" onClick={applyPaste} style={{padding:'5px 12px',fontSize:11}}>Replace grid from paste</button>
            {pasteResult && (
              <span style={{fontSize:11,color:pasteResult.errors.length?'#f59e0b':'#10b981'}}>
                {pasteResult.count} step{pasteResult.count===1?'':'s'} parsed
                {pasteResult.errors.length?` · ${pasteResult.errors.length} row(s) rejected`:''}
              </span>
            )}
          </div>
          {pasteResult?.errors?.length>0 && (
            <div style={{background:'rgba(245,158,11,0.08)',border:'1px solid rgba(245,158,11,0.3)',borderRadius:8,padding:'8px 12px',marginBottom:14,fontSize:11,color:'#f59e0b',lineHeight:1.6}}>
              {pasteResult.errors.map((e,i)=><div key={i}>{e}</div>)}
            </div>
          )}
          <div style={{overflowX:'auto'}}>
            <table>
              <thead><tr><th>Step</th><th>State</th><th>Local</th><th style={{textAlign:'right'}}>Total</th><th></th></tr></thead>
              <tbody>
                {draftSteps.map(s=>{
                  const cell = draft.steps[s]||{};
                  const total = (Number(cell.state)||0)+(Number(cell.local)||0);
                  const here = Math.floor(Number(draft.step))===s;
                  return (
                    <tr key={s} style={here?{background:'rgba(139,92,246,0.08)'}:undefined}>
                      <td style={{width:70,color:here?'#8b5cf6':'#e2e8f0',fontWeight:here?700:400}}>{s}{here?' ◂':''}</td>
                      <td><input type="number" value={cell.state??''} onChange={e=>setStepCell(s,'state',e.target.value)} style={{padding:'5px 8px',fontSize:12}}/></td>
                      <td><input type="number" value={cell.local??''} onChange={e=>setStepCell(s,'local',e.target.value)} style={{padding:'5px 8px',fontSize:12}}/></td>
                      <td style={{textAlign:'right',color:'#64748b'}}>{fmtCur(total)}</td>
                      <td style={{textAlign:'right'}}>
                        <button className="btn-danger" style={{padding:'2px 9px',fontSize:11}}
                          onClick={()=>setDraft(d=>{const st={...d.steps}; delete st[s]; return {...d, steps:st};})}>×</button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          <button className="btn-secondary" style={{padding:'4px 12px',fontSize:11,marginTop:10}}
            onClick={()=>setDraft(d=>{
              const next = draftSteps.length ? Math.max(...draftSteps)+1 : 1;
              return {...d, steps:{...(d.steps||{}), [next]:{state:'',local:''}}};
            })}>+ Step</button>

          <div style={{marginTop:16}}>
            <div className="label">Source</div>
            <input value={draft.source||''} placeholder="FY27_TeacherScale_withlongevity — eff. 08/23/26"
              onChange={e=>set({source:e.target.value})} style={{marginBottom:10}}/>
            <div className="label">Notes</div>
            <textarea rows={2} value={draft.notes||''} onChange={e=>set({notes:e.target.value})} style={{fontSize:12}}/>
          </div>
        </div>
      )}
    </div>
  );
}

// ── RSU vesting ──────────────────────────────────────────────────────────────
// The forward half of equity comp: one grant a year, 50% at two years and 50%
// at three, so three grants are always in flight and the calendar runs three
// years out.
//
// The rest of this tab reports the GRANT value — what an award was worth the
// day it was made, which is that year's comp and what the RSU bar in the
// breakdown chart shows. This section reports the VEST: shares, on a date, at
// a price, taxed in the year they release. A 2026 grant is 2026 comp and
// 2028/2029 income, and the two must never be added together.
//
// Everything numeric comes from lib/rsuVesting so the schedule here, the
// overhang, and the figure the Tax section plans next year's withholding
// against cannot disagree. Nothing about the calendar is stored — a saved
// schedule would outlive an edited grant.
const VestTile = ({label, value, sub, color}) => (
  <div style={{background:'#161b22',border:'1px solid #1e2a3a',borderRadius:8,padding:'10px 12px'}}>
    <div style={{fontSize:10,color:'#64748b',letterSpacing:'0.08em',textTransform:'uppercase',marginBottom:5}}>{label}</div>
    <div style={{fontSize:18,fontWeight:700,color:color||'#e2e8f0',lineHeight:1.2}}>{value}</div>
    <div style={{fontSize:10,color:'#64748b',marginTop:4,lineHeight:1.5}}>{sub}</div>
  </div>
);
const fmtShares = n => `${Number(n||0).toLocaleString('en-US',{maximumFractionDigits:1})} sh`;

function RsuVestingSection({ show }) {
  const { equityConfig, compensation, saveEquityConfig } = useContext(DataContext);
  const V = window.RsuVesting;
  const grants = (equityConfig && equityConfig.grants) || [];
  const today = V.toISO(new Date());

  const [priceDraft, setPriceDraft] = useState({ ticker:'', sharePrice:'', priceAsOf:'' });
  const [savingPrice, setSavingPrice] = useState(false);
  const [draft, setDraft] = useState(null);        // grant being added/edited
  const [savingGrant, setSavingGrant] = useState(false);
  const [armedDelete, setArmedDelete] = useState(null);
  const [showProjected, setShowProjected] = useState(false);
  const [showReleased, setShowReleased] = useState(false);

  useEffect(()=>{
    setPriceDraft({
      ticker: equityConfig?.ticker || '',
      sharePrice: equityConfig?.sharePrice != null ? String(equityConfig.sharePrice) : '',
      priceAsOf: equityConfig?.priceAsOf || '',
    });
  },[equityConfig?.ticker, equityConfig?.sharePrice, equityConfig?.priceAsOf]);

  // Projected grants extend the calendar past the horizon on request only, and
  // stay flagged the whole way through so no chart bar can quietly imply an
  // award that hasn't been made.
  const withProjected = useMemo(()=>{
    if(!showProjected) return grants;
    const horizon = V.horizonYear(grants);
    if(!horizon) return grants;
    return [...grants, ...V.projectFutureGrants(grants, { throughYear: horizon })];
  },[grants, showProjected]);

  const summary = useMemo(()=>V.summary(withProjected, {
    asOf: today,
    sharePrice: equityConfig?.sharePrice,
    priceAsOf: equityConfig?.priceAsOf,
    ticker: equityConfig?.ticker,
    compensation,
  }),[withProjected, today, equityConfig?.sharePrice, equityConfig?.priceAsOf, equityConfig?.ticker, compensation]);

  const priced = summary.sharePrice != null;
  const nextYear = new Date().getFullYear() + 1;
  const nextYearVest = V.vestForYear(grants, nextYear, {
    asOf: today, sharePrice: equityConfig?.sharePrice,
  });

  // Chart: value when there's a price to apply, shares when there isn't —
  // never a value axis of zeros standing in for an unpriced schedule. Three
  // series, not one, because a year can hold more than one kind: 2026 released
  // two half-grants, and 2029 vests a real grant beside a hypothetical one.
  // Money already delivered must not look like money still coming.
  const chartData = summary.years.map(y => ({
    year: String(y.year),
    released: priced ? (y.releasedValue||0) : y.releasedShares,
    scheduled: priced ? (y.scheduledValue||0) : y.scheduledShares,
    projected: priced ? (y.projectedValue||0) : y.projectedShares,
  }));

  // The statement's own totals, checked rather than trusted: the seeds record
  // what was GRANTED and let the rules produce the schedule, so a rule that
  // drifts or a grant edited by hand moves totals nobody is watching.
  const check = useMemo(
    () => V.reconcile(summary, equityConfig?.statement),
    [summary, equityConfig?.statement]);

  // The one write path. Always the whole list, through RsuVesting.toStored:
  // arrays are replaced rather than merged (so a removal is a removal), and the
  // library handles the tombstones a built-in grant needs to stay deleted.
  const saveGrants = async (list, msg) => {
    await saveEquityConfig({ grants: V.toStored(list) });
    show(msg);
  };

  const handleSavePrice = async () => {
    setSavingPrice(true);
    try {
      const p = Number(priceDraft.sharePrice);
      await saveEquityConfig({
        ticker: priceDraft.ticker || null,
        sharePrice: p > 0 ? p : null,
        // A price with no date can't be aged, so it gets today's — the date it
        // was entered is the date it was true.
        priceAsOf: p > 0 ? (priceDraft.priceAsOf || today) : null,
      });
      show('Share price saved');
    } catch(e){ show(e.message,'error'); }
    setSavingPrice(false);
  };

  const handleSaveGrant = async () => {
    if(!draft?.id || !draft.grantDate){ show('A grant needs an ID and a grant date','error'); return; }
    setSavingGrant(true);
    try {
      const others = grants.filter(g => g.id !== draft.id);
      await saveGrants([...others, { ...draft, trancheSource: draft.trancheSource || 'derived' }],
        `Grant ${draft.id} saved`);
      setDraft(null);
    } catch(e){ show(e.message,'error'); }
    setSavingGrant(false);
  };

  // Two clicks. Removing a grant is not destructive of anything but the record
  // of it, but the record is the whole section.
  const handleDelete = async (id) => {
    if(armedDelete !== id){ setArmedDelete(id); return; }
    try {
      await saveGrants(grants.filter(g => g.id !== id), `Grant ${id} removed`);
      setArmedDelete(null);
    } catch(e){ show(e.message,'error'); }
  };

  const priceNote = summary.priceState === 'missing'
    ? 'Set a share price to value the schedule'
    : summary.priceState === 'stale'
      ? `Price quoted ${fmtDate(summary.priceAsOf)} — ${V.daysBetween(summary.priceAsOf, today)}d old`
      : `At ${fmtCurFull(summary.sharePrice)}/share, quoted ${fmtDate(summary.priceAsOf)}`;

  return (
    <div className="card" style={{marginBottom:16}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:10,marginBottom:4}}>
        <div className="label" style={{marginBottom:0}}>RSU Vesting</div>
        <div style={{fontSize:11,color:'#64748b'}}>
          50% at 2 years · 50% at 3 years{summary.ticker?` · ${summary.ticker}`:''}
        </div>
      </div>
      <div style={{fontSize:11,color:'#64748b',marginBottom:14,lineHeight:1.6}}>
        Shares owed, when they release, and what they'd be worth at one stated price.
        Separate from the RSU column above, which is what each grant was worth the day it was
        made — a grant is comp in its grant year and income in its vest year.
      </div>

      {/* Price basis. Every dollar on this card is denominated in it, so it is
          stated on the card rather than buried in a settings panel. */}
      <div style={{display:'flex',flexWrap:'wrap',gap:10,alignItems:'flex-end',background:'#0b1220',border:'1px solid #1e2a3a',borderRadius:8,padding:'10px 12px',marginBottom:14}}>
        <div style={{width:80}}>
          <div className="label" style={{fontSize:10,marginBottom:3}}>Ticker</div>
          <input value={priceDraft.ticker} placeholder="JPM" onChange={e=>setPriceDraft(d=>({...d,ticker:e.target.value.toUpperCase()}))}/>
        </div>
        <div style={{width:120}}>
          <div className="label" style={{fontSize:10,marginBottom:3}}>Share price</div>
          <input type="number" step="0.01" min="0" value={priceDraft.sharePrice} placeholder="—"
            onChange={e=>setPriceDraft(d=>({...d,sharePrice:e.target.value}))}/>
        </div>
        <div style={{width:130}}>
          <div className="label" style={{fontSize:10,marginBottom:3}}>Quoted</div>
          <input value={priceDraft.priceAsOf} placeholder={today} onChange={e=>setPriceDraft(d=>({...d,priceAsOf:e.target.value}))}/>
        </div>
        <button className="btn-secondary" onClick={handleSavePrice} disabled={savingPrice} style={{fontSize:11,padding:'6px 12px'}}>
          {savingPrice?'Saving…':'Save price'}
        </button>
        <div style={{flex:1,minWidth:180,textAlign:'right',fontSize:11,
          color: summary.priceState==='fresh' ? '#64748b' : '#f59e0b'}}>
          {priceNote}
          {summary.priceState!=='fresh' && summary.priceState!=='missing' &&
            <span className="tag tag-red" style={{marginLeft:6}}>stale</span>}
        </div>
      </div>

      <div className="grid-4" style={{gap:10,marginBottom:16}}>
        <VestTile
          label="Unvested"
          value={priced ? fmtCur(summary.unvested.value) : fmtShares(summary.unvested.shares)}
          color="#8b5cf6"
          sub={`${fmtShares(summary.unvested.shares)} scheduled${priced?'':' — no price set'} · what leaving forfeits`}
        />
        <VestTile
          label="Next vest"
          value={summary.nextVest ? (priced ? fmtCur(summary.nextVest.value) : fmtShares(summary.nextVest.shares)) : '—'}
          sub={summary.nextVest
            ? `${fmtDate(summary.nextVest.vestDate)} · ${fmtShares(summary.nextVest.shares)} · in ${summary.nextVest.daysAway}d`
            : 'Nothing scheduled'}
        />
        <VestTile
          label={`${nextYear} vest`}
          value={nextYearVest.covered ? (priced ? fmtCur(nextYearVest.value) : fmtShares(nextYearVest.shares)) : '—'}
          sub={nextYearVest.covered
            ? `${fmtShares(nextYearVest.shares)} · supplemental wages, 22% withheld`
            : `Past the ${summary.horizonYear||'—'} horizon — that grant isn't made yet`}
        />
        <VestTile
          label="Released to date"
          value={priced ? fmtCur(summary.vested.value) : fmtShares(summary.vested.shares)}
          sub={summary.vested.shares
            ? `${fmtShares(summary.vested.shares)} delivered since ${summary.tranches[0]?.year} · at today's price, not what they were worth on the day`
            : 'Nothing released yet'}
        />
      </div>

      {check && (
        <div style={{display:'flex',alignItems:'center',flexWrap:'wrap',gap:10,marginBottom:16,padding:'8px 12px',borderRadius:8,
          background: check.ok ? 'rgba(16,185,129,0.07)' : 'rgba(239,68,68,0.08)',
          border: `1px solid ${check.ok ? 'rgba(16,185,129,0.25)' : 'rgba(239,68,68,0.3)'}`}}>
          <span style={{fontSize:11,fontWeight:600,color:check.ok?'#10b981':'#ef4444'}}>
            {check.ok ? '✓ Agrees with your statement' : '✕ Does not match your statement'}
          </span>
          <span style={{fontSize:10,color:'#64748b',flex:1,minWidth:200}}>
            {check.checks.map(c=>`${c.label} ${c.unit==='$'?fmtCur(c.ours||0):fmtShares(c.ours||0)}${c.ok?'':` vs ${c.unit==='$'?fmtCur(c.theirs):fmtShares(c.theirs)}`}`).join(' · ')}
          </span>
          <span style={{fontSize:10,color:'#475569'}}>{check.source} · {fmtDate(check.asOf)}</span>
        </div>
      )}

      {chartData.length > 0 && (
        <>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
            <div style={{fontSize:11,color:'#64748b'}}>
              {priced?'Vest value':'Shares vesting'} by year · schedule runs to {summary.horizonYear}
              {summary.steadyState && <> · steady state {priced
                ? `${fmtCur(summary.steadyState.value)}/yr`
                : `${fmtShares(summary.steadyState.sharesPerYear)}/yr`}, the average grant {summary.steadyState.basisYears[0]}–{summary.steadyState.basisYears[summary.steadyState.basisYears.length-1]} — every grant vests in full within 3 years, so a year vests what a year is granted</>}
            </div>
            <label style={{display:'flex',alignItems:'center',gap:6,fontSize:11,color:'#94a3b8'}}>
              <input type="checkbox" checked={showProjected} onChange={e=>setShowProjected(e.target.checked)}/>
              <span>Extend with projected grants</span>
            </label>
          </div>
          <ResponsiveContainer width="100%" height={200}>
            <BarChart data={chartData} margin={{top:4,right:16,left:0,bottom:0}}>
              <XAxis dataKey="year" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}/>
              <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}
                tickFormatter={v=>priced?fmtCur(v):v}/>
              <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
              <Tooltip formatter={(v,n)=>[priced?fmtCur(v):`${v} sh`, n]}
                contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
              <Bar dataKey="released" stackId="v" fill="#3f3a63" name="Released" radius={[0,0,0,0]}/>
              <Bar dataKey="scheduled" stackId="v" fill="#8b5cf6" name="Scheduled" radius={[4,4,0,0]}/>
              <Bar dataKey="projected" stackId="v" fill="#334155" name="Projected (not granted)" radius={[4,4,0,0]}/>
            </BarChart>
          </ResponsiveContainer>
          {/* The chart stopping is the absence of a record, not a forecast of a
              loss — the grants that would vest later haven't been awarded. */}
          <div style={{fontSize:10,color:'#64748b',marginTop:6,lineHeight:1.6,marginBottom:16}}>
            Nothing appears after {summary.horizonYear} because the grants that would vest then have not
            been made — the schedule is silent past its horizon, not empty.
            {showProjected && ' Grey bars assume the average grant repeats; nothing there has been awarded.'}
          </div>
        </>
      )}

      {/* The schedule, listed the way the brokerage lists it. Released tranches
          are folded away by default — they are history and already in the
          payslips, and the question this card answers is what is still owed —
          but they are one click away rather than dropped from the record. */}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:8,marginBottom:8}}>
        <div className="section-header" style={{marginBottom:0}}>Vest schedule</div>
        {summary.vested.shares > 0 && (
          <label style={{display:'flex',alignItems:'center',gap:6,fontSize:11,color:'#94a3b8'}}>
            <input type="checkbox" checked={showReleased} onChange={e=>setShowReleased(e.target.checked)}/>
            <span>Show {fmtShares(summary.vested.shares)} already released</span>
          </label>
        )}
      </div>
      <div style={{overflowX:'auto',marginBottom:16}}>
        <table>
          <thead><tr>
            <th>Vest date</th><th>Grant</th><th style={{textAlign:'right'}}>Shares</th>
            <th style={{textAlign:'right'}}>Value</th><th></th>
          </tr></thead>
          <tbody>
            {summary.tranches.filter(t=>showReleased||!t.vested).map((t,i)=>(
              <tr key={`${t.grantId}-${t.vestDate}-${i}`} style={t.projected?{opacity:0.6}:undefined}>
                <td style={{color:'#e2e8f0',whiteSpace:'nowrap'}}>{fmtDate(t.vestDate)}</td>
                <td className="muted" style={{fontSize:11}}>{t.grantId}</td>
                <td style={{textAlign:'right',color:'#e2e8f0'}}>{fmtShares(t.shares)}</td>
                <td style={{textAlign:'right',color:priced?'#8b5cf6':'#64748b'}}>{t.value!=null?fmtCur(t.value):'—'}</td>
                <td style={{textAlign:'right',whiteSpace:'nowrap'}}>
                  {t.vested && <span className="tag tag-green">released</span>}
                  {!t.vested && !t.projected && <span className="muted" style={{fontSize:11}}>in {t.daysAway}d</span>}
                  {t.projected && <span className="tag tag-blue">projected</span>}
                </td>
              </tr>
            ))}
            {!summary.tranches.length && (
              <tr><td colSpan={5} className="muted" style={{textAlign:'center',padding:20}}>No grants on file.</td></tr>
            )}
          </tbody>
        </table>
      </div>

      {/* Grants. A grant entered as (date, shares) has its schedule derived by
          the terms; one copied off the brokerage carries its own tranches and
          those always win. */}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:8,marginBottom:8}}>
        <div className="section-header" style={{marginBottom:0}}>Grants</div>
        <button className="btn-secondary" style={{fontSize:11,padding:'5px 12px'}}
          onClick={()=>setDraft({ id:'', grantDate:`${new Date().getFullYear()}-01-13`, shares:'' })}>
          + Add grant
        </button>
      </div>
      <div style={{overflowX:'auto'}}>
        <table>
          <thead><tr>
            <th>Grant</th><th>Granted</th><th style={{textAlign:'right'}}>Shares</th>
            <th style={{textAlign:'right'}}>Implied grant price</th><th>Schedule</th><th>Plan</th><th></th>

          </tr></thead>
          <tbody>
            {summary.grants.filter(g=>!String(g.id||'').startsWith('projected-')).map(g=>{
              const stored = grants.find(x=>x.id===g.id) || {};
              return (
                <tr key={g.id}>
                  <td>
                    <strong style={{color:'#e2e8f0'}}>{g.id}</strong>
                    {stored.seeded && <span className="tag tag-blue" style={{marginLeft:6}}>built-in</span>}
                  </td>
                  <td className="muted">{g.grantDate?fmtDate(g.grantDate):'—'}</td>
                  <td style={{textAlign:'right',color:'#e2e8f0'}}>
                    {g.shares!=null ? fmtShares(g.shares)
                      : <span style={{color:'#f59e0b'}} title="Only the unvested tranches are on record — earlier ones already released">part on record</span>}
                  </td>
                  {/* The dollars come from the performance year the grant
                      SETTLES — a January grant is the prior year's award — and
                      only when that year's IC is final. Anything else names the
                      reason rather than showing a dash that reads as "no data". */}
                  <td style={{textAlign:'right'}} className="muted">
                    {g.priceBasis.price!=null
                      ? <>
                          {fmtCurFull(g.priceBasis.price)}
                          <div style={{fontSize:9,color:'#475569'}}>
                            {g.priceBasis.state==='stated' ? 'stated on the grant' : `FY${g.priceBasis.performanceYear} IC`}
                          </div>
                        </>
                      : <span style={{fontSize:10,color:'#475569'}}>
                          {g.priceBasis.state==='projected' ? `FY${g.priceBasis.performanceYear} IC still projected`
                            : g.priceBasis.state==='unsettled' ? `FY${g.priceBasis.performanceYear} IC not announced yet`
                            : g.priceBasis.state==='partial-grant' ? 'award only part on record'
                            : '—'}
                        </span>}
                  </td>
                  <td className="muted" style={{fontSize:11}}>
                    {g.tranches.map(t=>`${t.shares} on ${t.vestDate}`).join(' · ')||'—'}
                    <span style={{marginLeft:6,color:'#475569'}}>
                      ({g.trancheSource==='derived'?'from terms':g.trancheSource==='future-only'?'future only':'stated'})
                    </span>
                  </td>
                  {/* The plan changed (21-A01S60 -> 24-B01S60) while the terms
                      did not. A future plan whose terms DO differ is what would
                      silently break every derived date beside it. */}
                  <td className="muted" style={{fontSize:10}}>{g.plan||'—'}</td>
                  <td style={{textAlign:'right',whiteSpace:'nowrap'}}>
                    <button className="btn-secondary" style={{fontSize:11,padding:'3px 10px'}}
                      onClick={()=>setDraft({ ...stored, id:g.id, grantDate:g.grantDate||'',
                        shares: stored.shares!=null?stored.shares:'' })}>Edit</button>
                    <button className="btn-danger" style={{fontSize:11,padding:'3px 10px',marginLeft:6}}
                      onClick={()=>handleDelete(g.id)}>
                      {armedDelete===g.id?'Confirm':'Remove'}
                    </button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {draft && (
        <div style={{marginTop:14,border:'1px solid rgba(139,92,246,0.35)',borderRadius:8,padding:'12px 14px',background:'rgba(139,92,246,0.06)'}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10,flexWrap:'wrap',gap:8}}>
            <div className="section-header" style={{marginBottom:0,color:'#8b5cf6'}}>
              {grants.some(g=>g.id===draft.id)?`Editing ${draft.id}`:'New grant'}
            </div>
            <div style={{display:'flex',gap:8}}>
              <button className="btn-secondary" style={{fontSize:11,padding:'5px 12px'}} onClick={()=>setDraft(null)}>Cancel</button>
              <button className="btn-primary" style={{fontSize:11,padding:'5px 12px'}} disabled={savingGrant} onClick={handleSaveGrant}>
                {savingGrant?'Saving…':'Save grant'}
              </button>
            </div>
          </div>
          <div className="grid-4" style={{gap:12}}>
            <div>
              <div className="label" style={{fontSize:10,marginBottom:3}}>Grant ID</div>
              <input value={draft.id} placeholder="JPM630017" onChange={e=>setDraft(d=>({...d,id:e.target.value.trim()}))}/>
            </div>
            <div>
              <div className="label" style={{fontSize:10,marginBottom:3}}>Grant date</div>
              <input value={draft.grantDate||''} placeholder="2027-01-13" onChange={e=>setDraft(d=>({...d,grantDate:e.target.value.trim()}))}/>
            </div>
            <div>
              <div className="label" style={{fontSize:10,marginBottom:3}}>Shares awarded</div>
              <input type="number" step="1" min="0" value={draft.shares??''} placeholder="30"
                onChange={e=>setDraft(d=>({...d,shares:e.target.value}))}/>
            </div>
            <div>
              <div className="label" style={{fontSize:10,marginBottom:3}}>Grant value (optional)</div>
              <input type="number" step="1" min="0" value={draft.grantValue??''} placeholder="from comp year"
                onChange={e=>setDraft(d=>({...d,grantValue:e.target.value}))}/>
            </div>
          </div>
          <div style={{fontSize:10,color:'#64748b',marginTop:8,lineHeight:1.6}}>
            {Array.isArray(draft.tranches) && draft.tranches.length
              ? <>
                  This grant carries a schedule copied from the brokerage ({draft.tranches.map(t=>`${t.shares} on ${t.vestDate}`).join(' · ')})
                  and it will be used as-is — the statement is the record, so the share count above changes nothing while it stands.
                  <button className="btn-secondary" style={{fontSize:10,padding:'2px 8px',marginLeft:8}}
                    onClick={()=>setDraft(d=>({...d,tranches:null,trancheSource:'derived'}))}>
                    Derive it from the terms instead
                  </button>
                </>
              : Number(draft.shares) > 0
                ? <>The schedule is derived from the terms: {V.splitShares(Number(draft.shares)).join(' then ')} shares, vesting {V.grantTranches({id:'x',grantDate:draft.grantDate,shares:Number(draft.shares)}).map(t=>fmtDate(t.vestDate)).join(' and ')||'—'}. Odd grants round down first — the brokerage schedules 33 shares as 16 then 17 — and a January grant vests on the award anchor (Jan 13), not on its own date.</>
                : <>Enter the shares awarded and the terms will split them 50/50 across the two-year and three-year anniversaries.</>}
          </div>
        </div>
      )}

      <div style={{fontSize:10,color:'#64748b',marginTop:14,paddingTop:12,borderTop:'1px solid #1e2a3a',lineHeight:1.7}}>
        A grant is the stock share of one year's IC — a tenth of the award, a fifth once IC crosses
        {' '}{fmtCur(100000)} — settled the January after the performance year it was earned in. So the grant
        dated Jan {new Date().getFullYear()} is last year's award, and this year's is not known until next January.
        Values are gross — a vest triggers sell-to-cover withholding, so fewer shares reach the account
        than the schedule lists. Future vests are valued at one stated price with no growth assumed:
        &quot;at today&apos;s price&quot; is a basis you can check, and a projected price on your own employer&apos;s
        stock is not.
      </div>
    </div>
  );
}

// ── Compensation ──────────────────────────────────────────────────────────────
function Compensation() {
  const { marketRates, marketConfig, compensation, payslipsByYear, transactions, uid, projectionConfig, saveProjectionConfig, reload } = useContext(DataContext);
  const { show, Toast } = useToast();
  const [cfg, setCfg] = useState({defaultRole:'',defaultLocation:'',defaultFirmType:'Corporate',yearsExperience:'',seniorityLevel:''});
  const [saving, setSaving] = useState(false);
  const [refreshing, setRefreshing] = useState(false);
  const [uploadStatus, setUploadStatus] = useState('');
  const [parseResult, setParseResult] = useState(null);
  const fileRef = useRef();
  const [manualComp, setManualComp] = useState({year:new Date().getFullYear(),baseSalary:'',ic:'',bonus:'',rsu:'',otherComp:''});
  const [bulkMode, setBulkMode] = useState(false);
  const [bulkText, setBulkText] = useState('');
  const [bulkStatus, setBulkStatus] = useState('');
  const [compTab, setCompTab] = useState('overview'); // 'overview' | 'payslips'
  const currentYear = new Date().getFullYear();
  const [expandedYears, setExpandedYears] = useState(new Set([currentYear]));
  const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const [sankeyPeriod, setSankeyPeriod] = useState('ytd'); // 'ytd' | 'month' | 'year'
  const [sankeyMonth, setSankeyMonth] = useState(new Date().getMonth() + 1);
  // All years that have any comp or market data
  const compYears = [...new Set([
    ...Object.keys(marketRates),
    ...Object.keys(compensation),
  ])].filter(y=>y.match(/^\d{4}$/)).sort();

  useEffect(()=>{ if(marketConfig) setCfg({defaultRole:marketConfig.defaultRole||'',defaultLocation:marketConfig.defaultLocation||'',defaultFirmType:marketConfig.defaultFirmType||'Corporate',yearsExperience:marketConfig.yearsExperience??'',seniorityLevel:marketConfig.seniorityLevel||''}); },[marketConfig]);

  // Payslips come from the provider, already fetched and already bucketed by
  // the year printed on them. This component used to re-fetch every payslip
  // and re-bucket them with its own slightly different rule, which is how the
  // Overview and the Payslips tab ended up reporting different gross pay for
  // the same year. Rendering what everything else computes from is the point.
  const payslipsByActualYear = useMemo(()=>{
    const out = {};
    for(const [y,list] of Object.entries(payslipsByYear||{})){
      out[y] = PayslipColumns.sortByPayDate(list).map(p=>{
        const mNum = Number(p.month);
        let month = null;
        if(p.payDate){ const d=new Date(p.payDate); if(!isNaN(d.getTime())) month=d.getMonth()+1; }
        if(!month && mNum>=1 && mNum<=12) month = mNum;
        return {...p, actualYear:Number(y), actualMonth:month};
      });
    }
    return out;
  },[payslipsByYear]);

  // Per-year aggregates — read, not recomputed. The provider already summed
  // the REPAIRED series (a statement that captured the payslip's year-to-date
  // column is otherwise added to the very total it already contains; three
  // 2026 rows did exactly that and pushed the year's gross to $612k against a
  // $297k package). Reading its answer is what stops this tab and the
  // Retirement tab from disagreeing again.
  const yearSummaries = useMemo(()=>{
    const out = {};
    for(const y of Object.keys(payslipsByActualYear)){
      if(compensation[y]?.ytd) out[y] = compensation[y].ytd;
    }
    return out;
  },[payslipsByActualYear, compensation]);

  // Which rows were repaired, keyed for the per-row badge, plus the totals as
  // they were stored so the banner can show what changed.
  const columnIssues = useMemo(()=>{
    const out = {};
    for(const [y,list] of Object.entries(payslipsByActualYear)){
      const bad = PayslipColumns.findYtdRows(list);
      if(!bad.length) continue;
      const storedGross = list.reduce((s,p)=>s+(p.grossPay||0),0);
      out[y] = {
        rows: bad,
        keys: new Set(bad.map(b=>`${b.row.payDate||''}|${b.row._docId||''}`)),
        storedGross,
        repairedGross: PayslipColumns.repairedTotals(list).grossPay,
      };
    }
    return out;
  },[payslipsByActualYear]);

  const payslipYears = useMemo(()=>{
    const ys = new Set(Object.keys(payslipsByActualYear).map(Number));
    ys.add(currentYear);
    return [...ys].sort((a,b)=>b-a);
  },[payslipsByActualYear, currentYear]);

  const payslips = payslipsByActualYear[currentYear] || [];
  const ytd = yearSummaries[currentYear];
  const payslipCount = ytd?.count || 0;

  const toggleYearExpand = (year) => {
    const next = new Set(expandedYears);
    if(next.has(year)) next.delete(year); else next.add(year);
    setExpandedYears(next);
  };

  // Derive the data slice for the selected Sankey period
  const sankeySource = useMemo(()=>{
    if(sankeyPeriod==='month'){
      // Sum every payslip whose payDate lands in the selected month — bi-weekly
      // schedules produce multiple payslips per calendar month.
      const inMonth = payslips.filter(p=>p.actualMonth===sankeyMonth);
      if(!inMonth.length) return null;
      const keys = ['grossPay','baseSalary','bonus','rsu','federalTax','stateTax','socialSecurity','medicare','retirement401k','healthInsurance','otherDeductions','netPay'];
      const agg = {};
      for(const k of keys) agg[k] = inMonth.reduce((s,p)=>s+(p[k]||0), 0);
      if(!agg.grossPay) return null;
      const monthsElapsed = new Date().getMonth()+1;
      const isProjected = currentYear===new Date().getFullYear() && sankeyMonth>monthsElapsed;
      return {
        data: agg,
        label: `${MONTHS[sankeyMonth-1]} ${currentYear}${inMonth.length>1?` (${inMonth.length} pay periods)`:''}${isProjected?' — projected':''}`,
        txFilter: t=>{ const d=new Date(t.date); return d.getMonth()+1===sankeyMonth && d.getFullYear()===currentYear; }
      };
    }
    if(!ytd || !ytd.grossPay) return null;
    if(sankeyPeriod==='year'){
      // Estimate remaining months using months covered by the payslips, not raw payslip count.
      const monthsCovered = new Set(payslips.map(p=>p.actualMonth).filter(Boolean)).size || 1;
      const r = 12/monthsCovered;
      const proj = {};
      for(const [k,v] of Object.entries(ytd)) proj[k]=Math.round((v||0)*r);
      return {
        data: proj,
        label: `${currentYear} Full Year${monthsCovered===12?' (Actual)':` (Est. from ${monthsCovered} mo.)`}`,
        txFilter: t=>new Date(t.date).getFullYear()===currentYear
      };
    }
    return {
      data: ytd,
      label: `YTD ${currentYear} — ${payslipCount} payslip${payslipCount!==1?'s':''}`,
      txFilter: t=>new Date(t.date)>=new Date(`${currentYear}-01-01`)
    };
  },[sankeyPeriod, sankeyMonth, ytd, payslips, payslipCount, currentYear, MONTHS]);

  // Flat Sankey: Gross Pay → every deduction line + Net Pay → spending. One level,
  // so Recharts places each flow without the sibling crossings the aggregator layout produced.
  const sankeyData = useMemo(()=>{
    if(!sankeySource) return null;
    const src = sankeySource.data;
    if(!src?.grossPay) return null;
    const N=[], L=[];
    const node = name => { N.push({name}); return N.length-1; };
    const link = (s,t,v) => { if(v>=50) L.push({source:s,target:t,value:Math.round(v)}); };

    const GROSS = node('Gross Pay');

    // Every payslip deduction as an individual sibling off Gross Pay.
    // Order here controls vertical order in the Sankey — taxes first, then pre-tax,
    // then Net Pay last so spending children sit at the bottom without crossing.
    const rows = [
      ['Federal Income Tax', src.federalTax      || 0],
      ['State Income Tax',   src.stateTax        || 0],
      ['SS + Medicare',      (src.socialSecurity || 0) + (src.medicare || 0)],
      ['401(k)',             src.retirement401k  || 0],
      ['Health & Benefits',  src.healthInsurance || 0],
      ['Other Pre-tax',      src.otherDeductions || 0],
    ];
    let deducted = 0;
    for(const [label, amt] of rows){
      if(amt>50){ const n=node(label); link(GROSS, n, amt); deducted += amt; }
    }

    const netVal = Math.round((src.netPay && src.netPay>0) ? src.netPay : Math.max(src.grossPay-deducted, 0));
    if(netVal>50){
      const NET = node('Net Pay');
      link(GROSS, NET, netVal);

      // Optional spending breakdown out of Net Pay.
      if(transactions?.length>0){
        const filtTx = transactions.filter(t=>!t.hideFromReports && t.amount>0 && sankeySource.txFilter(t));
        const cats = {};
        filtTx.forEach(t=>{ const c=t.category?.name||'Other'; cats[c]=(cats[c]||0)+t.amount; });
        const top = Object.entries(cats).sort((a,b)=>b[1]-a[1]).slice(0,6);
        const spent = top.reduce((s,[,v])=>s+v,0);
        top.forEach(([cat,amt])=>{ if(amt>=100){ const C=node(cat); link(NET, C, Math.round(amt)); }});
        const savings = netVal - spent;
        if(savings>200){ const SAV=node('Savings / Unspent'); link(NET, SAV, Math.round(savings)); }
      }
    }
    return {nodes:N, links:L};
  },[sankeySource, transactions]);

  const nodeHasOutgoing=useMemo(()=>sankeyData?new Set(sankeyData.links.map(l=>l.source)):new Set(),[sankeyData]);

  const CPI_NOW = CPI_BY_YEAR[currentYear] || CPI_LATEST;

  // Include projected years in the selector so charts/tables extend past last actual
  const projectedYears = Object.keys(compensation).filter(k=>compensation[k]?._projected);
  const allCompYears = [...new Set([...compYears, ...projectedYears])].filter(y=>y.match(/^\d{4}$/)).sort();
  const compChartData = allCompYears.map(y=>({
    year: y,
    yourBase: compensation[y]?.baseSalary||0,
    yourBonus: compensation[y]?.bonus||0,
    yourRSU: compensation[y]?.rsu||0,
    yourTC: (compensation[y]?.baseSalary||0)+(compensation[y]?.bonus||0)+(compensation[y]?.rsu||0),
    projected: !!compensation[y]?._projected,
    // The year in progress: base measured, IC modelled until January. The whole
    // row isn't a projection — the base is real — so it can't be marked with
    // `projected`, and leaving it unmarked showed two estimates as measured.
    icProjected: !!compensation[y]?._icProjected,
    p25: marketRates[y]?.tcP25||0,
    p50: marketRates[y]?.tcP50||0,
    p75: marketRates[y]?.tcP75||0,
  })).filter(d=>d.p50>0||d.yourTC>0);

  // Derived insight series
  const insightData = useMemo(()=>{
    const rows = compChartData.filter(d=>d.yourTC>0);
    return rows.map((d,i)=>{
      const prev = i>0?rows[i-1]:null;
      const yoy = prev&&prev.yourTC>0 ? ((d.yourTC-prev.yourTC)/prev.yourTC*100) : null;
      // CAGR since 2010
      const base = rows[0];
      const yrs = Number(d.year)-Number(rows[0].year);
      const cagr = yrs>0 ? (Math.pow(d.yourTC/base.yourTC,1/yrs)-1)*100 : null;
      const realTC = Math.round(d.yourTC*(CPI_NOW/(CPI_BY_YEAR[d.year]||CPI_NOW)));
      const icPct = d.yourTC>0 ? Math.round(((d.yourBonus+d.yourRSU)/d.yourTC)*100) : 0;
      const equityPct = d.yourTC>0 ? Math.round((d.yourRSU/d.yourTC)*100) : 0;
      return {...d, yoy:yoy?Math.round(yoy*10)/10:null, cagr:cagr?Math.round(cagr*10)/10:null, realTC, icPct, equityPct};
    });
  },[compChartData, CPI_NOW]);

  const handleSaveCfg = async () => {
    setSaving(true);
    try { await callFn('saveMarketConfig', cfg); show('Market config saved'); }
    catch(e){ show(e.message,'error'); }
    setSaving(false);
  };
  const handleRefresh = async () => {
    setRefreshing(true);
    try { await callFn('refreshMarketRateNow', {}); await reload(); show('Market rates refreshed'); }
    catch(e){ show(e.message,'error'); }
    setRefreshing(false);
  };
  const [uploadYear, setUploadYear] = useState(new Date().getFullYear());
  const [uploadQueue, setUploadQueue] = useState([]); // {name, status, result}
  const [dragOver, setDragOver] = useState(false);

  const processFiles = async (files) => {
    const fileArr = [...files];
    if(!fileArr.length) return;
    setParseResult(null);
    const queue = fileArr.map(f=>({name:f.name, status:'pending', result:null}));
    setUploadQueue(queue);
    setUploadStatus('');
    for(let i=0;i<fileArr.length;i++){
      const file = fileArr[i];
      setUploadQueue(q=>q.map((x,j)=>j===i?{...x,status:'parsing'}:x));
      try {
        const b64 = await new Promise((res,rej)=>{
          const reader = new FileReader();
          reader.onload = ev => res(ev.target.result.split(',')[1]);
          reader.onerror = rej;
          reader.readAsDataURL(file);
        });
        const mimeType = file.type || 'image/png';
        // Pass uploadYear as a fallback only — the server will prefer the parsed
        // payDate so payslips land in the year they were actually issued.
        const result = await callFn('parsePayslip',{imageBase64:b64,mimeType,fallbackYear:uploadYear});
        setUploadQueue(q=>q.map((x,j)=>j===i?{...x,status:'done',result}:x));
        if(fileArr.length===1){ setParseResult(result); }
      } catch(e){
        setUploadQueue(q=>q.map((x,j)=>j===i?{...x,status:'error',error:e.message}:x));
      }
    }
    const allDone = fileArr.length;
    await reload();
    show(`${allDone} payslip${allDone!==1?'s':''} processed`);
  };

  const handleFileUpload = async e => {
    await processFiles(e.target.files);
    e.target.value = '';
  };

  const handleDrop = async e => {
    e.preventDefault(); setDragOver(false);
    await processFiles(e.dataTransfer.files);
  };
  // Deleting a payslip is destructive and unrecoverable — the scan is gone and
  // the statement has to be re-uploaded — so it takes two clicks: the first
  // arms the row, the second commits. Nothing here decides on its own that a
  // row should go; the flags are evidence, the choice is the athlete's.
  const [armedDelete, setArmedDelete] = useState(null);
  const [deleting, setDeleting] = useState(false);
  const handleDeletePayslip = async (row) => {
    const key = PayslipQuality.docKey(row);
    if (armedDelete !== key) { setArmedDelete(key); return; }
    if (!row._docId || row._storedYear == null) { show('This row has no stored document to delete','error'); return; }
    setDeleting(true);
    try {
      await db.collection('users').doc(uid)
        .collection('compensation').doc(String(row._storedYear))
        .collection('payslips').doc(String(row._docId)).delete();
      show(`Deleted payslip ${row.payDate || row._docId}`);
      setArmedDelete(null);
      await reload();
    } catch(e){ show(e.message,'error'); }
    finally { setDeleting(false); }
  };

  const handleSaveManual = async () => {
    const {year,baseSalary,ic,bonus,rsu,otherComp}=manualComp;
    // Auto-split IC into cash/stock if IC entered but not cash/rsu
    let cashBonus = Number(bonus)||0, stockRsu = Number(rsu)||0;
    const icAmt = Number(ic)||0;
    if(icAmt && !cashBonus && !stockRsu){ const s=splitIC(icAmt); cashBonus=s.cash; stockRsu=s.stock; }
    try {
      await db.collection('users').doc(uid).collection('compensation').doc(String(year)).set({
        baseSalary:Number(baseSalary)||0, bonus:cashBonus, rsu:stockRsu,
        otherComp:Number(otherComp)||0, updatedAt:firebase.firestore.FieldValue.serverTimestamp()
      },{merge:true});
      show('Compensation saved');
      setManualComp(c=>({...c,baseSalary:'',ic:'',bonus:'',rsu:'',otherComp:'',year:c.year-1}));
    } catch(e){ show(e.message,'error'); }
  };

  const saveCompRecords = async (records) => {
    let saved=0, failed=0;
    for(const r of records){
      try{
        await db.collection('users').doc(uid).collection('compensation').doc(String(r.year)).set({
          baseSalary:r.baseSalary||0, bonus:r.bonus||0, rsu:r.rsu||0, otherComp:r.otherComp||0,
          updatedAt:firebase.firestore.FieldValue.serverTimestamp()
        },{merge:true});
        saved++;
      } catch{ failed++; }
    }
    return {saved, failed};
  };

  const handleBulkSave = async () => {
    const lines = bulkText.trim().split('\n').filter(l=>l.trim());
    if(!lines.length){ show('Nothing to import','error'); return; }
    setBulkStatus('Saving…');
    const records = lines.flatMap(line=>{
      const parts = line.split(/[\t,]/).map(p=>p.trim().replace(/[$,]/g,''));
      const [yr,base,icOrBonus,rsuOrStock] = parts;
      const year=parseInt(yr); const baseSalary=parseFloat(base)||0;
      if(!year||year<2000||year>2035) return [];
      // If 3rd column looks like total IC (no separate cash/stock), split it
      const ic=parseFloat(icOrBonus)||0, rsuIn=parseFloat(rsuOrStock)||0;
      const {cash,stock} = rsuIn===0 ? splitIC(ic) : {cash:ic,stock:rsuIn};
      return [{year,baseSalary,bonus:cash,rsu:stock,otherComp:0}];
    });
    const {saved,failed} = await saveCompRecords(records);
    setBulkStatus(`Saved ${saved} years${failed?`, ${failed} failed`:''}`);
    if(saved>0) setTimeout(()=>{ setBulkMode(false); setBulkText(''); setBulkStatus(''); },2000);
  };

  const handleImportHistorical = async () => {
    setBulkStatus('Importing 2010–2026…');
    const {saved,failed} = await saveCompRecords(HISTORICAL_COMP);
    setBulkStatus(`Imported ${saved} years${failed?`, ${failed} failed`:' ✓'}`);
    setTimeout(()=>setBulkStatus(''),3000);
  };

  return (
    <div className="page">
      {Toast}
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16,flexWrap:'wrap',gap:8}}>
        <div className="page-title" style={{marginBottom:0}}>Compensation</div>
        <div style={{display:'flex',gap:4}}>
          {[['overview','Overview'],['payslips','Payslips'],['wife','Hers']].map(([t,lbl])=>(
            <button key={t} onClick={()=>setCompTab(t)}
              style={{padding:'5px 14px',fontSize:12,borderRadius:6,cursor:'pointer',
                background:compTab===t?'#10b981':'transparent',
                color:compTab===t?'#fff':'#64748b',
                border:`1px solid ${compTab===t?'#10b981':'#334155'}`}}>
              {lbl}
            </button>
          ))}
        </div>
      </div>

      {/* ── Hers tab ── the district pay scale, its history, and the editor */}
      {compTab==='wife' && <SpousePayslipUpload/>}
      {compTab==='wife' && <WifeCompSection/>}

      {/* ── Overview tab ── */}
      {/* Income Flow — period-selectable Sankey */}
      {compTab==='overview' && (ytd?.grossPay > 0 || payslips.length > 0) && (
        <div className="card" style={{marginBottom:16}}>
          {/* Header + period controls */}
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:8,marginBottom:12}}>
            <div className="label" style={{margin:0}}>Where Your Income Goes</div>
            <div style={{display:'flex',gap:4,alignItems:'center',flexWrap:'wrap'}}>
              {[['ytd','YTD'],['month','Monthly'],['year','Full Year']].map(([p,lbl])=>(
                <button key={p} onClick={()=>setSankeyPeriod(p)}
                  style={{padding:'3px 10px',fontSize:11,borderRadius:6,cursor:'pointer',
                    background:sankeyPeriod===p?'#10b981':'transparent',
                    color:sankeyPeriod===p?'#fff':'#94a3b8',
                    border:sankeyPeriod===p?'1px solid #10b981':'1px solid #334155'}}>
                  {lbl}
                </button>
              ))}
              {sankeyPeriod==='month' && (
                <select value={sankeyMonth} onChange={e=>setSankeyMonth(Number(e.target.value))}
                  style={{padding:'3px 8px',fontSize:11,marginLeft:4}}>
                  {MONTHS.map((m,i)=>payslips.some(p=>p.actualMonth===i+1) ? (
                    <option key={i+1} value={i+1}>{m} {currentYear}</option>
                  ) : null)}
                </select>
              )}
            </div>
          </div>
          {/* Stats row for selected period */}
          {sankeySource && (()=>{
            const src=sankeySource.data;
            const taxes = (src.federalTax||0)+(src.stateTax||0)+(src.socialSecurity||0)+(src.medicare||0);
            const preTax = (src.retirement401k||0)+(src.healthInsurance||0)+(src.otherDeductions||0);
            const net = (src.netPay && src.netPay>0) ? src.netPay : Math.max((src.grossPay||0) - taxes - preTax, 0);
            return (
              <div className="grid-4" style={{marginBottom:12}}>
                <div className="card-sm"><div className="label">Gross Pay</div><div className="val-md">{fmtCur(src.grossPay)}</div></div>
                <div className="card-sm"><div className="label">Total Taxes</div><div className="val-md red">{fmtCur(taxes)}</div></div>
                <div className="card-sm"><div className="label">401(k)</div><div className="val-md" style={{color:'#3b82f6'}}>{fmtCur(src.retirement401k)}</div></div>
                <div className="card-sm"><div className="label">Net Pay</div><div className="val-md green">{fmtCur(net)}</div></div>
              </div>
            );
          })()}
          {sankeySource && <div style={{fontSize:10,color:'#64748b',marginBottom:8}}>{sankeySource.label}</div>}
          {/* Backstop for the case the repair arithmetic cannot reach: if every
              statement in a year captured the wrong column there is no clean
              period to measure against, so nothing gets flagged as YTD and the
              totals just come out wrong. These two checks are physical limits
              rather than judgements — elective deferrals are capped by statute,
              and a year cannot gross far more than the whole package — so an
              impossible total says so instead of being displayed with
              confidence. */}
          {(()=>{
            const limits = PayslipColumns.implausibilities(yearSummaries[currentYear]||{}, {
              deferralLimit: 24500,
              expectedAnnualComp: (()=>{ const c=compensation[currentYear]||{};
                return (c.baseSalary||0)+(c.bonus||0)+(c.rsu||0)+(c.otherComp||0); })(),
            });
            if(!limits.length) return null;
            return (
              <div style={{background:'rgba(239,68,68,0.08)',border:'1px solid rgba(239,68,68,0.35)',
                borderRadius:8,padding:'8px 12px',marginBottom:10,fontSize:11,color:'#fca5a5',lineHeight:1.6}}>
                <strong>These totals cannot be right.</strong>
                <div style={{marginTop:3}}>{limits.map(l=><div key={l.field}>› {l.message}</div>)}</div>
                <div style={{marginTop:4,color:'#94a3b8'}}>Check the Payslips tab against the paper statements — most often a statement's year-to-date column was captured instead of its pay-period column.</div>
              </div>
            );
          })()}
          {/* Sankey chart */}
          {sankeyData && Sankey && (
            <ErrorBoundary>
              <ResponsiveContainer width="100%" height={340}>
                <Sankey
                  data={sankeyData}
                  nodeWidth={12}
                  nodePadding={16}
                  margin={{top:10,right:170,bottom:10,left:120}}
                  link={{stroke:'#334155',strokeOpacity:0.45}}
                  node={<SankeyNodeShape hasOutgoing={nodeHasOutgoing}/>}
                >
                  <Tooltip
                    formatter={(v,n)=>[fmtCur(v),n]}
                    contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:12,color:'#e2e8f0'}}
                    labelStyle={{color:'#94a3b8',marginBottom:2}}
                    itemStyle={{color:'#e2e8f0'}}
                  />
                </Sankey>
              </ResponsiveContainer>
            </ErrorBoundary>
          )}
          {!sankeySource && sankeyPeriod==='month' && (
            <div style={{color:'#64748b',fontSize:12,textAlign:'center',padding:'40px 0'}}>
              No payslip data for {MONTHS[sankeyMonth-1]} {currentYear}. Upload a payslip for that month.
            </div>
          )}
          {/* Fallback bar chart if Recharts Sankey unavailable */}
          {sankeyData && !Sankey && (
            <div style={{display:'flex',flexDirection:'column',gap:4}}>
              {sankeyData.links.filter(l=>l.source===sankeyData.nodes.findIndex(n=>n.name==='Gross Pay')).map((l,i)=>{
                const tgt=sankeyData.nodes[l.target];
                const total=sankeyData.links.filter(x=>x.source===l.source).reduce((s,x)=>s+x.value,0);
                const pct=Math.round(l.value/total*100);
                return (
                  <div key={i} style={{display:'flex',alignItems:'center',gap:8}}>
                    <div style={{fontSize:11,width:150,color:'#94a3b8',flexShrink:0}}>{tgt.name}</div>
                    <div style={{flex:1,background:'#1e2a3a',borderRadius:3,height:16,overflow:'hidden'}}>
                      <div style={{width:`${pct}%`,height:'100%',background:SANKEY_COLORS[tgt.name]||'#475569',borderRadius:3}}/>
                    </div>
                    <div style={{fontSize:11,color:'#e2e8f0',width:80,textAlign:'right'}}>{fmtCur(l.value)}</div>
                  </div>
                );
              })}
            </div>
          )}
        </div>
      )}

      {/* Payslip History */}
      {/* ── Payslips tab ── */}
      {compTab==='payslips' && payslipYears.length > 0 && (
        <div className="card" style={{marginBottom:16}}>
          <div className="label" style={{marginBottom:12}}>Payslip History</div>
          {payslipYears.map(year=>{
            const yd = yearSummaries[year] || {};
            const pc = yd.count || 0;
            const isExp = expandedYears.has(year);
            const rows = payslipsByActualYear[year] || [];
            const issue = columnIssues[year];
            const audit = PayslipQuality.auditYear(rows);
            const taxes = (yd.federalTax||0)+(yd.stateTax||0)+(yd.socialSecurity||0)+(yd.medicare||0);
            return (
              <div key={year} style={{marginBottom:6}}>
                {/* A repaired total must never appear as a bare number — say
                    which rows were corrected and what the stored figures said,
                    so the year can be checked against the paper statements. */}
                {issue && (
                  <div style={{background:'rgba(245,158,11,0.08)',border:'1px solid rgba(245,158,11,0.3)',
                    borderRadius:8,padding:'8px 12px',marginBottom:6,fontSize:11,color:'#fbbf24',lineHeight:1.6}}>
                    <strong>{issue.rows.length} payslip{issue.rows.length!==1?'s':''} in {year} captured the year-to-date column instead of the pay-period column.</strong>
                    {' '}Each one equals everything banked before it plus one ordinary period, so adding it to the year double-counts the whole year up to that date.
                    {' '}Totals below are shown repaired: <strong>{fmtCur(issue.repairedGross)}</strong> gross, not the {fmtCur(issue.storedGross)} the stored rows sum to.
                    <div style={{marginTop:4,color:'#94a3b8'}}>
                      {issue.rows.map(b=>`${b.row.payDate||'?'}: stored ${fmtCur(b.evidence.reportedGross)} = ${fmtCur(b.evidence.bankedBefore)} banked + ${fmtCur(b.evidence.periodGross)} period`).join(' · ')}
                    </div>
                    <div style={{marginTop:4,color:'#94a3b8'}}>Re-upload those statements to correct the stored data.</div>
                  </div>
                )}
                {/* Duplicates are reported, never auto-removed: a year can
                    legitimately contain two statements paid on the same day,
                    and silently dropping one to tidy a total destroys real
                    data. Identical-gross pairs are called out as certain;
                    same-day different-amount pairs are shown far more weakly
                    because on this athlete's own data that pattern is two
                    genuine statements. */}
                {audit.duplicateGroups.length > 0 && (
                  <div style={{background:'rgba(239,68,68,0.07)',border:'1px solid rgba(239,68,68,0.3)',
                    borderRadius:8,padding:'8px 12px',marginBottom:6,fontSize:11,color:'#fca5a5',lineHeight:1.6}}>
                    <strong>{audit.duplicateGroups.filter(g=>g.confidence==='certain').length > 0
                      ? 'Duplicate payslips in ' + year
                      : 'Same-day payslips in ' + year + ' — check these are two real statements'}</strong>
                    {audit.duplicateGroups.map((g,gi)=>(
                      <div key={gi} style={{marginTop:5,paddingTop:5,borderTop:gi?'1px solid rgba(239,68,68,0.15)':'none'}}>
                        <div style={{color:g.confidence==='certain'?'#fca5a5':'#94a3b8'}}>{g.reason}</div>
                        <div style={{marginTop:3,display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'}}>
                          {g.rows.map(r=>{
                            const k = PayslipQuality.docKey(r);
                            const isKeep = g.keep && PayslipQuality.docKey(g.keep)===k;
                            return (
                              <span key={k} style={{fontSize:10,color:'#94a3b8',background:'rgba(255,255,255,0.04)',
                                border:'1px solid #334155',borderRadius:5,padding:'2px 7px'}}>
                                doc {r._docId||'?'} · {fmtCur(r.grossPay||0)}
                                {isKeep && <strong style={{color:'#10b981'}}> · keep</strong>}
                              </span>
                            );
                          })}
                        </div>
                        {g.confidence==='certain' && g.keep && (
                          <div style={{fontSize:10,color:'#64748b',marginTop:3}}>
                            Suggested keep: doc {g.keep._docId} — it captured the most fields. Delete the other with the ✕ on its row.
                          </div>
                        )}
                      </div>
                    ))}
                  </div>
                )}
                {/* Year summary row — clickable */}
                <div onClick={()=>toggleYearExpand(year)}
                  style={{display:'flex',alignItems:'center',gap:10,cursor:'pointer',
                    padding:'9px 12px',borderRadius:8,
                    background: isExp ? 'rgba(16,185,129,0.06)' : 'rgba(255,255,255,0.02)',
                    border:`1px solid ${isExp?'rgba(16,185,129,0.25)':'#1e2a3a'}`,
                    userSelect:'none',transition:'background 0.15s'}}>
                  <span style={{fontSize:9,color:isExp?'#10b981':'#475569',
                    display:'inline-block',transform:isExp?'rotate(90deg)':'none',
                    transition:'transform 0.15s',lineHeight:1}}>▶</span>
                  <span style={{fontWeight:700,color:'#e2e8f0',fontSize:13,minWidth:42}}>{year}</span>
                  <span style={{fontSize:10,color:'#475569',minWidth:70}}>{pc} payslip{pc!==1?'s':''}</span>
                  <div style={{flex:1,display:'flex',gap:20,flexWrap:'wrap'}}>
                    {yd.grossPay>0 && (
                      <div><span style={{fontSize:10,color:'#64748b'}}>Gross </span>
                        <span style={{fontSize:12,color:'#e2e8f0'}}>{fmtCur(yd.grossPay)}</span></div>
                    )}
                    {taxes>0 && (
                      <div><span style={{fontSize:10,color:'#64748b'}}>Taxes </span>
                        <span style={{fontSize:12,color:'#ef4444'}}>{fmtCur(taxes)}</span></div>
                    )}
                    {(yd.retirement401k||0)>0 && (
                      <div><span style={{fontSize:10,color:'#64748b'}}>401(k) </span>
                        <span style={{fontSize:12,color:'#3b82f6'}}>{fmtCur(yd.retirement401k)}</span></div>
                    )}
                    {yd.netPay>0 && (
                      <div><span style={{fontSize:10,color:'#64748b'}}>Net </span>
                        <span style={{fontSize:12,color:'#10b981',fontWeight:600}}>{fmtCur(yd.netPay)}</span></div>
                    )}
                  </div>
                </div>
                {/* Monthly breakdown */}
                {isExp && (
                  <div style={{marginTop:4,marginLeft:8,marginBottom:4}}>
                    {rows.length === 0 ? (
                      <div style={{color:'#475569',fontSize:11,padding:'8px 12px'}}>No payslip records for {year}.</div>
                    ) : (
                      <table style={{marginBottom:0}}>
                        <thead><tr>
                          <th>Pay Date</th><th>Employer</th><th>Gross</th>
                          <th>Federal</th><th>SS+Med</th><th>401(k)</th><th>Net Pay</th>
                          <th style={{textAlign:'right'}}>Scan</th><th></th>
                        </tr></thead>
                        <tbody>
                          {/* Rows render REPAIRED, in the chronological order the
                              repair depends on, with the stored figure kept
                              visible underneath — the correction is an
                              inference, so the number it replaced stays on
                              screen to be checked against the statement. */}
                          {audit.rows.map((entry,idx)=>{
                            const p = entry.repaired, raw = entry.row;
                            const bad = entry.column === 'ytd';
                            const errs = entry.flags.filter(f=>f.severity==='error');
                            const warns = entry.flags.filter(f=>f.severity!=='error');
                            const armed = armedDelete === entry.key;
                            const fica=(p.socialSecurity||0)+(p.medicare||0);
                            const when = raw.payDate
                              || (raw.actualMonth ? `${MONTHS[raw.actualMonth-1]} ${year}` : `—`);
                            return (
                              <tr key={`${year}-${raw._docId}-${idx}`}
                                style={errs.length?{background:'rgba(239,68,68,0.07)'}:bad?{background:'rgba(245,158,11,0.06)'}:undefined}>
                                <td>
                                  {when}
                                  {bad && <div style={{fontSize:9,color:'#fbbf24',marginTop:2}}>YTD column — repaired</div>}
                                </td>
                                <td className="muted">{raw.employer||'—'}</td>
                                <td>
                                  {p.grossPay?fmtCur(p.grossPay):'—'}
                                  {bad && <div style={{fontSize:9,color:'#64748b',marginTop:2}}>stored {fmtCur(entry.evidence.reportedGross)}</div>}
                                </td>
                                <td style={{color:'#ef4444'}}>{p.federalTax?fmtCur(p.federalTax):'—'}</td>
                                <td style={{color:'#f59e0b'}}>{fica>0?fmtCur(fica):'—'}</td>
                                <td style={{color:'#3b82f6'}}>{p.retirement401k?fmtCur(p.retirement401k):'—'}</td>
                                <td style={{fontWeight:600,color:'#10b981'}}>{p.netPay?fmtCur(p.netPay):'—'}</td>
                                {/* Every flag carries the reason it fired, so a
                                    row marked bad can be judged rather than
                                    taken on trust. */}
                                <td style={{textAlign:'right',maxWidth:260}}>
                                  {entry.flags.length === 0
                                    ? <span style={{fontSize:10,color:'#10b981'}}>ok</span>
                                    : (
                                      <div style={{display:'flex',flexDirection:'column',gap:2,alignItems:'flex-end'}}>
                                        {errs.concat(warns).map((f,fi)=>(
                                          <span key={fi} title={f.message}
                                            style={{fontSize:9,lineHeight:1.4,textAlign:'right',
                                              color:f.severity==='error'?'#fca5a5':'#fbbf24'}}>
                                            {f.message}
                                          </span>
                                        ))}
                                      </div>
                                    )}
                                </td>
                                <td style={{textAlign:'right',whiteSpace:'nowrap'}}>
                                  <button onClick={()=>handleDeletePayslip(raw)} disabled={deleting}
                                    title={armed?'Click again to delete permanently':'Delete this payslip'}
                                    style={{padding:'2px 8px',fontSize:10,borderRadius:5,cursor:'pointer',
                                      background:armed?'#ef4444':'transparent',
                                      color:armed?'#fff':'#64748b',
                                      border:`1px solid ${armed?'#ef4444':'#334155'}`}}>
                                    {armed?'Delete?':'✕'}
                                  </button>
                                </td>
                              </tr>
                            );
                          })}
                          {/* Totals row — from the same repaired series as the
                              year header, so the footer can't contradict it. */}
                          {rows.length > 1 && (()=>{
                            const rt = PayslipColumns.repairedTotals(rows);
                            const tot = {
                              gross: rt.grossPay,
                              fed:   rt.federalTax,
                              fica:  rt.socialSecurity + rt.medicare,
                              k401:  rt.retirement401k,
                              net:   rt.netPay,
                            };
                            return (
                              <tr style={{borderTop:'1px solid #334155',fontWeight:600}}>
                                <td style={{color:'#64748b'}}>Total</td>
                                <td></td>
                                <td>{fmtCur(tot.gross)}</td>
                                <td style={{color:'#ef4444'}}>{tot.fed>0?fmtCur(tot.fed):'—'}</td>
                                <td style={{color:'#f59e0b'}}>{tot.fica>0?fmtCur(tot.fica):'—'}</td>
                                <td style={{color:'#3b82f6'}}>{tot.k401>0?fmtCur(tot.k401):'—'}</td>
                                <td style={{color:'#10b981'}}>{fmtCur(tot.net)}</td>
                                <td style={{textAlign:'right',fontSize:10,color:'#64748b',fontWeight:400}}>
                                  {audit.errorCount>0 && <span style={{color:'#fca5a5'}}>{audit.errorCount} row{audit.errorCount!==1?'s':''} to review</span>}
                                  {audit.errorCount===0 && audit.warnCount>0 && <span style={{color:'#fbbf24'}}>{audit.warnCount} with warnings</span>}
                                  {audit.errorCount===0 && audit.warnCount===0 && <span style={{color:'#10b981'}}>all scans clean</span>}
                                </td>
                                <td></td>
                              </tr>
                            );
                          })()}
                        </tbody>
                      </table>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}

      {/* TC Breakdown: Base / Cash IC / RSU stacked bar */}
      {compTab==='overview' && compChartData.some(d=>d.yourBase||d.yourBonus||d.yourRSU) && (
        <div className="card" style={{marginBottom:16}}>
          <div className="label" style={{marginBottom:12}}>Compensation Breakdown</div>
          <ResponsiveContainer width="100%" height={220}>
            <BarChart data={compChartData} margin={{top:4,right:16,left:0,bottom:0}}>
              <XAxis dataKey="year" 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)} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
              <Legend iconType="circle" iconSize={8} formatter={v=><span style={{fontSize:11,color:'#94a3b8'}}>{v}</span>}/>
              <Bar dataKey="yourBase" stackId="tc" fill="#10b981" name="Base Salary" radius={[0,0,0,0]}/>
              <Bar dataKey="yourBonus" stackId="tc" fill="#f59e0b" name="Cash IC" radius={[0,0,0,0]}/>
              <Bar dataKey="yourRSU" stackId="tc" fill="#8b5cf6" name="RSU" radius={[4,4,0,0]}/>
            </BarChart>
          </ResponsiveContainer>
        </div>
      )}

      {/* Vesting sits directly under the breakdown, where the RSU bar was just
          read as grant-year dollars — the next question is when those become
          income, and the answer is a different set of years. */}
      {compTab==='overview' && <RsuVestingSection show={show}/>}

      {/* Market Comparison + Year by Year table */}
      {compTab==='overview' && (compChartData.length > 0 ? (<>
        <div className="card" style={{marginBottom:16}}>
          <div className="label" style={{marginBottom:12}}>Total Comp vs Market ({marketConfig?.defaultRole||'your role'})</div>
          <ResponsiveContainer width="100%" height={220}>
            <LineChart data={compChartData} margin={{top:4,right:16,left:0,bottom:0}}>
              <XAxis dataKey="year" 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)} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
              <Line type="monotone" dataKey="p75" stroke="#1e2a3a" strokeWidth={1} dot={false} name="P75 Market"/>
              <Line type="monotone" dataKey="p50" stroke="#64748b" strokeWidth={1} strokeDasharray="4 2" dot={false} name="P50 Market"/>
              <Line type="monotone" dataKey="p25" stroke="#1e2a3a" strokeWidth={1} dot={false} name="P25 Market"/>
              {compChartData.some(d=>d.yourTC>0) && <Line type="monotone" dataKey="yourTC" stroke="#10b981" strokeWidth={2.5} dot={{fill:'#10b981',r:4}} name="Your TC"/>}
            </LineChart>
          </ResponsiveContainer>
        </div>

        <ProjectionConfigPanel
          config={projectionConfig}
          onSave={async (cfg) => { try { await saveProjectionConfig(cfg); show('Projection saved — reload to refresh'); } catch(e){ show(e.message,'error'); } }}
        />

        <div className="card" style={{marginBottom:16}}>
          <div className="label" style={{marginBottom:12}}>Year by Year</div>
          <table>
            <thead><tr><th>Year</th><th>Base</th><th>Bonus</th><th>RSU</th><th>Your TC</th><th>Market P50</th><th>vs Market</th></tr></thead>
            <tbody>
              {compChartData.map(r=>{
                const diff = r.yourTC - r.p50;
                const proj = r.projected;
                const cellStyle = proj ? {fontStyle:'italic',color:'#64748b'} : undefined;
                // A row whose base is measured but whose IC is not: only the
                // two IC cells and the total they feed are marked.
                const icStyle = (proj || r.icProjected) ? {fontStyle:'italic',color:'#64748b'} : undefined;
                const TAG = {fontSize:9,marginLeft:6,padding:'1px 5px',background:'rgba(100,116,139,0.2)',color:'#94a3b8',borderRadius:3,letterSpacing:'0.05em'};
                return (
                  <tr key={r.year} style={proj?{opacity:0.85}:undefined}>
                    <td style={cellStyle}>{r.year}
                      {proj && <span style={TAG}>PROJ</span>}
                      {!proj && r.icProjected && <span style={TAG} title="Base is known; the IC that completes the package is decided for this year and announced next January">IC PROJ</span>}
                    </td>
                    <td style={cellStyle}>{r.yourBase?fmtCur(r.yourBase):'—'}</td>
                    <td style={icStyle}>{r.yourBonus?fmtCur(r.yourBonus):'—'}</td>
                    <td style={icStyle}>{r.yourRSU?fmtCur(r.yourRSU):'—'}</td>
                    <td style={{fontWeight:600,...(icStyle||{})}}>{r.yourTC?fmtCur(r.yourTC):'—'}</td>
                    <td style={cellStyle}>{fmtCur(r.p50)}</td>
                    <td style={{color:(proj||r.icProjected)?'#64748b':(diff>=0?'#10b981':'#ef4444'),...((proj||r.icProjected)?{fontStyle:'italic'}:{})}}>{r.yourTC?(diff>=0?'+':'')+fmtCur(diff):'—'}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        {/* ── Comp Ladder ── */}
        {(()=>{
          // Current TC = most recent actual year, NOT the last projected row.
          // The chart includes 10 years of forward projections; using the tail
          // was showing a 5%-compounded future year as "current".
          const actualRows = compChartData.filter(d=>!d.projected&&d.yourTC>0);
          const latestActual = actualRows.length ? actualRows[actualRows.length-1] : null;
          const latestTC = latestActual?.yourTC || 0;
          if(!latestTC) return null;
          const currentRung = COMP_LADDER.findIndex(r=>r.tc>latestTC);
          const prev = currentRung>0 ? COMP_LADDER[currentRung-1] : COMP_LADDER[0];
          const next = currentRung>=0 ? COMP_LADDER[currentRung] : COMP_LADDER[COMP_LADDER.length-1];
          const pct = currentRung>0 ? Math.min(100,Math.round((latestTC-prev.tc)/(next.tc-prev.tc)*100)) : 0;
          return (
            <div className="card" style={{marginBottom:16}}>
              <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
                <div className="label" style={{marginBottom:0}}>Comp Ladder</div>
                <div style={{fontSize:11,color:'#64748b'}}>
                  {latestActual?.year} TC: <strong style={{color:'#10b981'}}>{fmtCur(latestTC)}</strong>
                </div>
              </div>
              <div style={{marginBottom:12}}>
                <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'#64748b',marginBottom:4}}>
                  <span>{fmtCur(prev.tc)}</span>
                  <span style={{color:'#f59e0b'}}>{pct}% to next level</span>
                  <span>{fmtCur(next.tc)}</span>
                </div>
                <div style={{background:'#1e2a3a',borderRadius:6,height:8,overflow:'hidden'}}>
                  <div style={{width:`${pct}%`,height:'100%',background:'#10b981',borderRadius:6,transition:'width 0.3s'}}/>
                </div>
              </div>
              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(140px,1fr))',gap:8}}>
                {COMP_LADDER.map((r,i)=>{
                  const isCurrentRange = i===currentRung;
                  const isPast = i<currentRung;
                  return (
                    <div key={r.tc} style={{background:isCurrentRange?'rgba(16,185,129,0.08)':isPast?'rgba(100,116,139,0.05)':'#161b22',border:`1px solid ${isCurrentRange?'rgba(16,185,129,0.3)':'#1e2a3a'}`,borderRadius:8,padding:'8px 10px',opacity:isPast?0.55:1}}>
                      <div style={{fontWeight:700,fontSize:13,color:isCurrentRange?'#10b981':'#e2e8f0',marginBottom:4}}>{fmtCur(r.tc)}</div>
                      <div style={{fontSize:10,color:'#64748b',lineHeight:1.6}}>
                        <div>Base: <span style={{color:'#94a3b8'}}>{fmtCur(r.salary)}</span></div>
                        <div>IC: <span style={{color:'#f59e0b'}}>{fmtCur(r.cash)}</span> + <span style={{color:'#8b5cf6'}}>{fmtCur(r.stock)}</span></div>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })()}

        {/* ── Insight Charts ── */}
        {insightData.length>1 && (<>

          {/* YoY Growth Rate */}
          <div className="card" style={{marginBottom:16}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
              <div className="label" style={{marginBottom:0}}>Year-over-Year TC Growth</div>
              {insightData.length>1&&<div style={{fontSize:11,color:'#64748b'}}>Career CAGR: <strong style={{color:'#10b981'}}>{insightData[insightData.length-1].cagr?.toFixed(1)}%</strong></div>}
            </div>
            <ResponsiveContainer width="100%" height={200}>
              <ComposedChart data={insightData.filter(d=>d.yoy!==null)} margin={{top:4,right:16,left:0,bottom:0}}>
                <XAxis dataKey="year" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}/>
                <YAxis tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={v=>`${v}%`}/>
                <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
                <Tooltip formatter={(v,n)=>[typeof v==='number'?`${v.toFixed(1)}%`:v,n]} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                <ReferenceLine y={0} stroke="#1e2a3a"/>
                <Bar dataKey="yoy" name="YoY Growth %" radius={[3,3,0,0]} fill="#10b981">
                  {insightData.filter(d=>d.yoy!==null).map((d,i)=><Cell key={i} fill={d.yoy>=0?'#10b981':'#ef4444'}/>)}
                </Bar>
                <Line type="monotone" dataKey="cagr" name="Running CAGR %" stroke="#f59e0b" strokeWidth={2} dot={false} strokeDasharray="4 2"/>
              </ComposedChart>
            </ResponsiveContainer>
          </div>

          {/* Comp mix: Base vs Cash IC vs Equity over time */}
          <div className="card" style={{marginBottom:16}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
              <div className="label" style={{marginBottom:0}}>Comp Mix Evolution — Base / Cash / Equity</div>
              <div style={{fontSize:11,color:'#64748b'}}>IC as % TC in {insightData[insightData.length-1]?.year}: <strong style={{color:'#f59e0b'}}>{insightData[insightData.length-1]?.icPct}%</strong></div>
            </div>
            <ResponsiveContainer width="100%" height={200}>
              <AreaChart data={insightData} margin={{top:4,right:16,left:0,bottom:0}}>
                <defs>
                  <linearGradient id="gBase" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#10b981" stopOpacity={0.4}/><stop offset="95%" stopColor="#10b981" stopOpacity={0.1}/></linearGradient>
                  <linearGradient id="gCash" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#f59e0b" stopOpacity={0.4}/><stop offset="95%" stopColor="#f59e0b" stopOpacity={0.1}/></linearGradient>
                  <linearGradient id="gRSU" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.5}/><stop offset="95%" stopColor="#8b5cf6" stopOpacity={0.1}/></linearGradient>
                </defs>
                <XAxis dataKey="year" 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)} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                <Legend iconType="circle" iconSize={8} formatter={v=><span style={{fontSize:11,color:'#94a3b8'}}>{v}</span>}/>
                <Area type="monotone" dataKey="yourBase" stackId="1" stroke="#10b981" fill="url(#gBase)" name="Base Salary"/>
                <Area type="monotone" dataKey="yourBonus" stackId="1" stroke="#f59e0b" fill="url(#gCash)" name="Cash IC"/>
                <Area type="monotone" dataKey="yourRSU" stackId="1" stroke="#8b5cf6" fill="url(#gRSU)" name="RSU / Equity"/>
              </AreaChart>
            </ResponsiveContainer>
          </div>

          {/* Real vs Nominal TC (inflation-adjusted) */}
          <div className="card" style={{marginBottom:16}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
              <div className="label" style={{marginBottom:0}}>Real vs Nominal Total Comp</div>
              <div style={{fontSize:11,color:'#64748b'}}>Inflation-adjusted to {currentYear} dollars</div>
            </div>
            <ResponsiveContainer width="100%" height={200}>
              <ComposedChart data={insightData} margin={{top:4,right:16,left:0,bottom:0}}>
                <XAxis dataKey="year" 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)} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                <Legend iconType="circle" iconSize={8} formatter={v=><span style={{fontSize:11,color:'#94a3b8'}}>{v}</span>}/>
                <Area type="monotone" dataKey="yourTC" stroke="#10b981" fill="rgba(16,185,129,0.1)" strokeWidth={2} dot={false} name="Nominal TC"/>
                <Line type="monotone" dataKey="realTC" stroke="#3b82f6" strokeWidth={2.5} dot={{fill:'#3b82f6',r:3}} name={`Real TC (${currentYear}$)`}/>
              </ComposedChart>
            </ResponsiveContainer>
          </div>

          {/* IC Leverage + Equity % */}
          <div className="card" style={{marginBottom:16}}>
            <div className="label" style={{marginBottom:12}}>IC Leverage &amp; Equity Mix Over Time</div>
            <ResponsiveContainer width="100%" height={180}>
              <ComposedChart data={insightData} margin={{top:4,right:16,left:0,bottom:0}}>
                <XAxis dataKey="year" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false}/>
                <YAxis yAxisId="l" tick={{fontSize:10,fill:'#64748b'}} axisLine={false} tickLine={false} tickFormatter={v=>`${v}%`}/>
                <CartesianGrid strokeDasharray="3 3" stroke="#1e2a3a"/>
                <Tooltip formatter={(v,n)=>[`${v}%`,n]} contentStyle={{background:'#0f172a',border:'1px solid #334155',borderRadius:8,fontSize:11,color:'#e2e8f0'}}/>
                <Legend iconType="circle" iconSize={8} formatter={v=><span style={{fontSize:11,color:'#94a3b8'}}>{v}</span>}/>
                <Bar yAxisId="l" dataKey="icPct" name="IC as % of TC" fill="#f59e0b" opacity={0.8} radius={[3,3,0,0]}/>
                <Line yAxisId="l" type="monotone" dataKey="equityPct" name="Equity % of TC" stroke="#8b5cf6" strokeWidth={2.5} dot={{fill:'#8b5cf6',r:3}}/>
              </ComposedChart>
            </ResponsiveContainer>
          </div>

          {/* Summary stat row */}
          {(()=>{
            const first = insightData[0], last = insightData[insightData.length-1];
            if(!first||!last||!first.yourTC||!last.yourTC) return null;
            const totalEarned = insightData.reduce((s,d)=>s+(d.yourTC||0),0);
            const realGain = last.realTC - first.realTC;
            return (
              <div className="grid-4" style={{marginBottom:16}}>
                <div className="card-sm"><div className="label">Career CAGR</div><div style={{fontSize:18,fontWeight:700,color:'#10b981'}}>{last.cagr?.toFixed(1)}%</div></div>
                <div className="card-sm"><div className="label">TC Since {first.year}</div><div style={{fontSize:18,fontWeight:700,color:'#10b981'}}>{fmtCur(totalEarned)}</div></div>
                <div className="card-sm"><div className="label">Real Purchasing Power Gain</div><div style={{fontSize:18,fontWeight:700,color:'#3b82f6'}}>{fmtCur(realGain)}</div></div>
                <div className="card-sm"><div className="label">TC Multiplier ({first.year}→{last.year})</div><div style={{fontSize:18,fontWeight:700,color:'#f59e0b'}}>{(last.yourTC/first.yourTC).toFixed(1)}×</div></div>
              </div>
            );
          })()}
        </>)}

      </>) : (
        <div className="card" style={{marginBottom:16}}>
          <div className="empty-state">
            <h3>No compensation data yet</h3>
            <p style={{fontSize:12,marginBottom:16}}>Enter your comp data manually below, or upload payslips. Once you have market config saved, click Refresh Market Data to fetch benchmark rates.</p>
            {marketConfig && <button className="btn-primary" onClick={handleRefresh} disabled={refreshing} style={{marginBottom:8}}>{refreshing?'Refreshing…':'Refresh Market Data Now'}</button>}
          </div>
        </div>
      ))}

      {compTab==='payslips' && <div className="grid-2">
        <div className="card">
          <div className="label" style={{marginBottom:12}}>Market Config</div>
          <div style={{display:'flex',flexDirection:'column',gap:10}}>
            <div><div className="label">Your Role / Title</div><input value={cfg.defaultRole} onChange={e=>setCfg(c=>({...c,defaultRole:e.target.value}))} placeholder="e.g. Senior Software Engineer"/></div>
            <div><div className="label">Seniority Level</div>
              <select value={cfg.seniorityLevel} onChange={e=>setCfg(c=>({...c,seniorityLevel:e.target.value}))}>
                <option value="">— select —</option>
                {['Junior','Mid','Senior','Staff','Principal','Distinguished','Manager','Senior Manager','Director','Senior Director','VP','SVP','C-Level'].map(l=><option key={l} value={l}>{l}</option>)}
              </select>
            </div>
            <div><div className="label">Years of Experience</div><input type="number" min="0" max="50" value={cfg.yearsExperience} onChange={e=>setCfg(c=>({...c,yearsExperience:e.target.value}))} placeholder="e.g. 15"/></div>
            <div><div className="label">Location</div><input value={cfg.defaultLocation} onChange={e=>setCfg(c=>({...c,defaultLocation:e.target.value}))} placeholder="e.g. New York, NY"/></div>
            <div><div className="label">Firm Type</div>
              <select value={cfg.defaultFirmType} onChange={e=>setCfg(c=>({...c,defaultFirmType:e.target.value}))}>
                {['Corporate','Startup','Big Tech','Finance','Consulting','Government'].map(f=><option key={f}>{f}</option>)}
              </select>
            </div>
            <div style={{display:'flex',gap:8,marginTop:4}}>
              <button className="btn-primary" onClick={handleSaveCfg} disabled={saving}>{saving?'Saving…':'Save'}</button>
              <button className="btn-secondary" onClick={handleRefresh} disabled={refreshing}>{refreshing?'Refreshing…':'Refresh Market Data'}</button>
            </div>
          </div>
        </div>

        <div className="card">
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
            <div className="label" style={{margin:0}}>Add Compensation</div>
            <button className="btn-secondary" style={{padding:'3px 10px',fontSize:11}} onClick={()=>{setBulkMode(b=>!b);setBulkStatus('');}}>{bulkMode?'Single Entry':'Bulk Import'}</button>
          </div>
          <div style={{display:'flex',flexDirection:'column',gap:10}}>
          {bulkMode ? (<>
            <div style={{background:'rgba(16,185,129,0.07)',border:'1px solid rgba(16,185,129,0.2)',borderRadius:8,padding:'10px 14px',fontSize:12}}>
              <div style={{fontWeight:600,color:'#10b981',marginBottom:4}}>Your historical data (2010–2026) is ready to import</div>
              <div style={{color:'#64748b',fontSize:11,marginBottom:8}}>Salary, cash bonus, and RSU already split per your IC rules.</div>
              <div style={{display:'flex',gap:8,alignItems:'center'}}>
                <button className="btn-primary" onClick={handleImportHistorical}>Import 2010–2026 Now</button>
                {bulkStatus&&<span style={{fontSize:12,color:'#10b981'}}>{bulkStatus}</span>}
              </div>
            </div>
            <div style={{fontSize:11,color:'#64748b'}}>Or paste custom data below (year, base, total IC or base, cash, rsu):</div>
            <textarea value={bulkText} onChange={e=>setBulkText(e.target.value)} rows={6} style={{fontFamily:'monospace',fontSize:11,resize:'vertical'}} placeholder="2024, 185000, 84678&#10;2023, 162800, 62200"/>
            <div style={{display:'flex',gap:8,alignItems:'center'}}>
              <button className="btn-secondary" onClick={handleBulkSave} disabled={!bulkText.trim()}>Import Custom</button>
            </div>
          </>) : (<>
            <div className="grid-2">
              <div><div className="label">Year</div><input type="number" value={manualComp.year} onChange={e=>setManualComp(c=>({...c,year:e.target.value}))}/></div>
              <div><div className="label">Base Salary</div><input type="number" value={manualComp.baseSalary} onChange={e=>setManualComp(c=>({...c,baseSalary:e.target.value}))} placeholder="0"/></div>
            </div>
            <div>
              <div className="label">Total IC (auto-splits cash/stock)</div>
              <input type="number" value={manualComp.ic} onChange={e=>{const ic=Number(e.target.value)||0; const s=splitIC(ic); setManualComp(c=>({...c,ic:e.target.value,bonus:s.cash||'',rsu:s.stock||''}));}} placeholder="0"/>
            </div>
            <div className="grid-2">
              <div><div className="label">Cash Bonus</div><input type="number" value={manualComp.bonus} onChange={e=>setManualComp(c=>({...c,bonus:e.target.value}))} placeholder="auto"/></div>
              <div><div className="label">RSU / Stock</div><input type="number" value={manualComp.rsu} onChange={e=>setManualComp(c=>({...c,rsu:e.target.value}))} placeholder="auto"/></div>
            </div>
            {(Number(manualComp.ic)||0)>0&&<div style={{fontSize:11,color:'#64748b',background:'#161b22',borderRadius:6,padding:'5px 10px'}}>
              IC {fmtCur(manualComp.ic)} → Cash {fmtCur(manualComp.bonus)} + Stock {fmtCur(manualComp.rsu)} ({IC_SPLITS.find(t=>Number(manualComp.ic)>=t.lo&&Number(manualComp.ic)<t.hi)?.cash||70}%/{IC_SPLITS.find(t=>Number(manualComp.ic)>=t.lo&&Number(manualComp.ic)<t.hi)?.stock||30}%)
            </div>}
            <button className="btn-primary" onClick={handleSaveManual}>Save &amp; Go to Previous Year</button>
          </>)}
            <hr className="divider"/>
            <div className="label">Upload Payslips (AI Parsing)</div>
            <div style={{display:'flex',gap:8,marginBottom:8,alignItems:'center'}}>
              <div className="label" style={{margin:0,whiteSpace:'nowrap'}}>Year:</div>
              <input type="number" value={uploadYear} onChange={e=>setUploadYear(Number(e.target.value))} style={{width:80}}/>
              <div style={{fontSize:11,color:'#64748b'}}>Select year these payslips belong to</div>
            </div>
            <div
              style={{border:`1px dashed ${dragOver?'#10b981':'#1e2a3a'}`,borderRadius:8,padding:'18px 16px',textAlign:'center',cursor:'pointer',background:dragOver?'rgba(16,185,129,0.05)':'transparent',transition:'all 0.2s'}}
              onClick={()=>fileRef.current?.click()}
              onDragOver={e=>{e.preventDefault();setDragOver(true);}}
              onDragLeave={()=>setDragOver(false)}
              onDrop={handleDrop}
            >
              <div style={{fontSize:24,marginBottom:6}}>📄</div>
              <div style={{fontSize:12,color:'#64748b'}}>Drop payslips here or click to browse</div>
              <div style={{fontSize:11,color:'#475569',marginTop:4}}>Images or PDFs — select multiple at once</div>
              <input ref={fileRef} type="file" accept="image/*,.pdf" multiple style={{display:'none'}} onChange={handleFileUpload}/>
            </div>
            {uploadQueue.length > 0 && (
              <div style={{fontSize:11,background:'#161b22',borderRadius:6,padding:8,display:'flex',flexDirection:'column',gap:4}}>
                {uploadQueue.map((f,i)=>(
                  <div key={i} style={{display:'flex',gap:8,alignItems:'center'}}>
                    <span style={{color:f.status==='done'?'#10b981':f.status==='error'?'#ef4444':f.status==='parsing'?'#f59e0b':'#64748b'}}>{f.status==='done'?'✓':f.status==='error'?'✗':f.status==='parsing'?'…':'○'}</span>
                    <span style={{flex:1,color:'#94a3b8',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{f.name}</span>
                    {f.status==='done'&&f.result&&<span style={{color:'#10b981'}}>{fmtCur(f.result.grossPay)} gross</span>}
                    {f.status==='error'&&<span style={{color:'#ef4444',fontSize:10}}>{f.error}</span>}
                  </div>
                ))}
              </div>
            )}
            {parseResult && uploadQueue.length===1 && (
              <div style={{fontSize:11,background:'#161b22',borderRadius:8,padding:12}}>
                <div style={{fontWeight:600,marginBottom:8}}>Parsed: {parseResult.employer||'Payslip'}</div>
                <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:4}}>
                  <div>Gross <span style={{color:'#e2e8f0',float:'right'}}>{fmtCur(parseResult.grossPay)}</span></div>
                  <div>Net <span style={{color:'#10b981',float:'right'}}>{fmtCur(parseResult.netPay)}</span></div>
                  <div>Federal <span style={{color:'#ef4444',float:'right'}}>{fmtCur(parseResult.federalTax)}</span></div>
                  <div>State <span style={{color:'#ef4444',float:'right'}}>{fmtCur(parseResult.stateTax)}</span></div>
                  {parseResult.retirement401k>0 && <div>401(k) <span style={{color:'#3b82f6',float:'right'}}>{fmtCur(parseResult.retirement401k)}</span></div>}
                  {parseResult.healthInsurance>0 && <div>Health <span style={{color:'#a78bfa',float:'right'}}>{fmtCur(parseResult.healthInsurance)}</span></div>}
                </div>
              </div>
            )}
          </div>
        </div>
      </div>}
    </div>
  );
}

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