/* Clear Mint — Banking, Money & Credit/Debt pages
   Cash Flow · Earnings & Income · Reconciliation · Credit Cards · CC Statements · Debts
   Depends on cm-charts + cm-components + cm-app-kit (same window-shared kit). */
const { MetricCard, ChartCard, DataTable, DonutChart, TrendChart, ProgressBar, Legend, InsightCard, FinancialHealthCard } = window;
const { KpiRow, ChangeCell, StatusPill, ActivityList, Tabs, CategoryBar, VBars, CMC } = window;
const Ib = window.CMIcon;
const { G, PU, AM, BL, SL, RD, TEAL, PINK, INDIGO } = CMC;
const tb = window.tintFor;
const money = n => (n<0?'-$':'$') + Math.abs(n).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
const money0 = n => (n<0?'-$':'$') + Math.abs(n).toLocaleString('en-US');

/* ===== shared: dual income/expense bars ===== */
function FlowBars({ rows, max, height=220 }) {
  return (
    <div className="cm-vbars" style={{height}}>
      {rows.map((m,i)=>(
        <div className="cm-vbar" key={i} style={{justifyContent:'flex-end'}}>
          <div style={{display:'flex',gap:5,alignItems:'flex-end',width:'100%',justifyContent:'center',height:'100%'}}>
            <div style={{width:16,borderRadius:'6px 6px 3px 3px',background:G,height:`${m[1]/max*100}%`}}/>
            <div style={{width:16,borderRadius:'6px 6px 3px 3px',background:'#FBD0D2',height:`${m[2]/max*100}%`}}/>
          </div>
          <div className="lbl">{m[0]}</div>
        </div>
      ))}
    </div>
  );
}

/* ============================ CASH FLOW ============================ */
function cmToast(msg){ try{ var t=document.createElement('div'); t.textContent=msg; t.style.cssText='position:fixed;bottom:26px;left:50%;transform:translateX(-50%);background:#0D3B2E;color:#FBFAF6;font:600 13.5px Inter,-apple-system,sans-serif;padding:11px 20px;border-radius:12px;z-index:99999;box-shadow:0 12px 30px rgba(13,59,46,.35)'; document.body.appendChild(t); setTimeout(function(){ t.style.transition='opacity .4s'; t.style.opacity='0'; setTimeout(function(){ try{document.body.removeChild(t);}catch(e){} },450); },1800);}catch(e){} }
function exportCashFlowCSV(rows, label){
  var head=['Month','Income','Expenses','Net','Savings Rate %','Change %'];
  var lines=[head.join(',')].concat((rows||[]).map(function(m){ return ['"'+m.mo+'"', Math.round(m.inc), Math.round(m.exp), Math.round(m.net), (m.sr==null?'':m.sr), (m.chg==null?'':m.chg)].join(','); }));
  try{ var blob=new Blob([lines.join('\n')],{type:'text/csv'}); var url=URL.createObjectURL(blob); var a=document.createElement('a'); a.href=url; a.download='clearmint-cashflow'+(label?('-'+String(label).replace(/\s+/g,'-')):'')+'.csv'; document.body.appendChild(a); a.click(); setTimeout(function(){ URL.revokeObjectURL(url); try{document.body.removeChild(a);}catch(e){} },300); cmToast('Cash flow exported'); }catch(e){}
}
function CashFlowCombo({ data, fmt }){
  var R=React, n=data.length, slot=Math.max(40,Math.min(80, 760/Math.max(1,n)));
  var H=300, W=Math.max(560, n*slot+70), padL=56, padR=14, padT=16, padB=36;
  var iw=W-padL-padR, ih=H-padT-padB, i;
  var maxV=1; for(i=0;i<n;i++){ maxV=Math.max(maxV,data[i].inc,data[i].exp); }
  var nets=data.map(function(d){return d.inc-d.exp;});
  var top=maxV, bot=Math.min(0, Math.min.apply(null, nets.concat([0])));
  var span=(top-bot)||1;
  var y=function(v){ return padT+(top-v)/span*ih; };
  var bw=Math.min(16, slot*0.32), zeroY=y(0);
  var els=[];
  for(var g=0;g<=4;g++){ var gy=padT+ih*g/4, gv=top-span*g/4; els.push(R.createElement('line',{key:'g'+g,x1:padL,x2:W-padR,y1:gy,y2:gy,stroke:'#EFEAE0',strokeWidth:1})); els.push(R.createElement('text',{key:'gt'+g,x:padL-8,y:gy+3,textAnchor:'end',fontSize:10,fill:'#98A39B'},fmt(Math.round(gv)))); }
  els.push(R.createElement('line',{key:'z',x1:padL,x2:W-padR,y1:zeroY,y2:zeroY,stroke:'#D9D3C6',strokeWidth:1}));
  var pts=[];
  for(i=0;i<n;i++){ var xc=padL+slot*i+slot/2, d=data[i];
    els.push(R.createElement('rect',{key:'i'+i,x:xc-bw-2,y:y(d.inc),width:bw,height:Math.max(0,zeroY-y(d.inc)),rx:3,fill:'#34B27B'},R.createElement('title',null,d.mo+' · Income '+fmt(d.inc))));
    els.push(R.createElement('rect',{key:'e'+i,x:xc+2,y:y(d.exp),width:bw,height:Math.max(0,zeroY-y(d.exp)),rx:3,fill:'#E2655F'},R.createElement('title',null,d.mo+' · Expenses '+fmt(d.exp))));
    pts.push([xc,y(d.inc-d.exp)]);
  }
  els.push(R.createElement('path',{key:'ln',d:pts.map(function(p,j){return (j?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1);}).join(' '),fill:'none',stroke:'#2A6FDB',strokeWidth:2.5,strokeLinejoin:'round',strokeLinecap:'round'}));
  pts.forEach(function(p,j){ els.push(R.createElement('circle',{key:'c'+j,cx:p[0],cy:p[1],r:3.2,fill:'#2A6FDB'},R.createElement('title',null,data[j].mo+' · Net '+fmt(nets[j])))); });
  data.forEach(function(d,j){ els.push(R.createElement('text',{key:'x'+j,x:padL+slot*j+slot/2,y:H-12,textAnchor:'middle',fontSize:10,fill:'#98A39B'},d.mo)); });
  return R.createElement('svg',{viewBox:'0 0 '+W+' '+H,width:W,height:H,style:{fontFamily:'inherit'},preserveAspectRatio:'xMinYMid meet'},els);
}
function PageCashFlow() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const fmt = CM.M0;
  const [period,setPeriod] = React.useState(function(){ try{ var v=JSON.parse(localStorage.getItem('cm_cashflow_view')); if(v) return v; }catch(e){} return 'all'; });
  const TRANSFER = {'Transfer':1,'Credit Card Payment':1,'Savings':1};
  const inPer = (d)=> !window.cmInPeriod || window.cmInPeriod(d, period);
  const ledger = (CM.S.bankTx||[]).filter(r=>r.date && inPer(r.date));
  const periodLabel = (function(){
    if(period && typeof period==='object') return 'selected range';
    if(/^\d{4}-\d{2}$/.test(period)) return new Date(period+'-01T00:00').toLocaleDateString('en-CA',{month:'long',year:'numeric'});
    var f=(window.CM_PERIODS||[]).find(p=>p[0]===period); return f?f[1].toLowerCase():'all time';
  })();
  // monthly buckets from the period-scoped ledger (chronological, last 12)
  const mkDate = (k)=>{ var p=String(k).split('-'); return new Date(+p[0], (+p[1]||1)-1, 1); };   // handles both 'YYYY-M' and 'YYYY-MM' keys
  const moMap = {};
  ledger.forEach(r=>{ if(TRANSFER[r.cat]) return; var k=CM.getMonthKey(r.date); if(!k) return; (moMap[k]=moMap[k]||{inc:0,exp:0}); if(r.cr>0) moMap[k].inc+=(+r.cr||0); if(r.dr>0) moMap[k].exp+=(+r.dr||0); });
  const cf = Object.keys(moMap).sort((a,b)=>mkDate(a)-mkDate(b)).slice(-12).map(k=>({mo:mkDate(k).toLocaleString('en-US',{month:'short'}),inc:moMap[k].inc,exp:moMap[k].exp}));
  const totalIn = cf.reduce((s,m)=>s+m.inc,0);
  const totalOut = cf.reduce((s,m)=>s+m.exp,0);
  const net = totalIn-totalOut;
  const save = totalIn?Math.round(net/totalIn*100):0;
  const bars = cf.map(m=>[m.mo, m.inc, m.exp]);
  const maxBar = Math.max.apply(null, cf.length?cf.map(m=>Math.max(m.inc,m.exp)):[1]) || 1;
  const trend = cf.map(m=>(m.inc-m.exp)/1000);
  // inflows: bank credits by category (period-scoped ledger)
  const inMap = {};
  ledger.forEach(r=>{ if(r.cr>0 && !TRANSFER[r.cat]){ inMap[r.cat||'Other']=(inMap[r.cat||'Other']||0)+r.cr; } });
  const inArr = Object.keys(inMap).map(k=>({label:k,value:inMap[k]})).sort((a,b)=>b.value-a.value).slice(0,5);
  const inMax = Math.max.apply(null, inArr.length?inArr.map(x=>x.value):[1])||1;
  // outflows: bank debits by category (period-scoped)
  const outMap = {};
  ledger.forEach(r=>{ if(r.dr>0 && !TRANSFER[r.cat]){ outMap[r.cat||'Other']=(outMap[r.cat||'Other']||0)+r.dr; } });
  const outArr = Object.keys(outMap).map(k=>({label:k,value:outMap[k]})).sort((a,b)=>b.value-a.value).slice(0,6);
  const outMax = Math.max.apply(null, outArr.length?outArr.map(x=>x.value):[1])||1;
  const inColors=[G,PU,BL,AM,TEAL], outColors=[RD,AM,BL,PU,PINK,TEAL];
  const [tab,setTab] = React.useState('Overview');
  const monthOpts = window.cmMonthOptions ? window.cmMonthOptions((CM.S.bankTx||[]).map(r=>r.date)).slice(0,6) : [];
  const nMo = window.cmPeriodMonths?window.cmPeriodMonths(ledger.map(r=>r.date)):Math.max(1,cf.length);
  const avgNet = Math.round(net/Math.max(1,nMo));
  // monthly table (period-scoped, chronological)
  const monthlyRows = Object.keys(moMap).sort((a,b)=>mkDate(a)-mkDate(b)).map(k=>({mo:mkDate(k).toLocaleDateString('en-CA',{month:'short',year:'numeric'}),inc:moMap[k].inc,exp:moMap[k].exp,net:moMap[k].inc-moMap[k].exp}));
  // by-category (period-scoped)
  const catMap={}; ledger.forEach(r=>{ if(TRANSFER[r.cat])return; var c=r.cat||'Other'; (catMap[c]=catMap[c]||{inc:0,exp:0}); if(r.cr>0)catMap[c].inc+=(+r.cr||0); if(r.dr>0)catMap[c].exp+=(+r.dr||0); });
  const catRows=Object.keys(catMap).map(c=>({cat:c,inc:catMap[c].inc,exp:catMap[c].exp,net:catMap[c].inc-catMap[c].exp})).sort((a,b)=>(b.inc+b.exp)-(a.inc+a.exp));
  // upcoming cash flow (next 30 days) — earnings + upcoming bills only, with row actions
  const t0=new Date(); t0.setHours(0,0,0,0); const t30=t0.getTime()+30*864e5;
  const isRecur=v=>!!v && !/one.?time|once|declined/i.test(String(v));
  const upB=(CM.S.bills||[]).map((b,bi)=>({b,bi})).filter(x=>!x.b.paid&&x.b.dueDate&&CM.toTs(x.b.dueDate)>=t0.getTime()&&CM.toTs(x.b.dueDate)<=t30)
    .map(x=>({kind:'bill',idx:x.bi,date:x.b.dueDate,desc:x.b.desc||x.b.name||'Bill',amt:-(+x.b.amount||0),rec:isRecur(x.b.recur)?(x.b.recur||'Recurring'):null,kept:!!x.b.kept}));
  const upE=(CM.S.earnings||[]).map((e,ei)=>({e,ei})).filter(x=>x.e.date&&CM.toTs(x.e.date)>=t0.getTime()&&CM.toTs(x.e.date)<=t30)
    .map(x=>({kind:'earning',idx:x.ei,date:x.e.date,desc:x.e.desc||x.e.source||'Income',amt:(+x.e.amount||0),rec:(x.e.recurring===true||isRecur(x.e.recur))?(x.e.recur||'Recurring'):null,kept:!!x.e.kept}));
  const upcoming=upB.concat(upE).sort((a,b)=>CM.toTs(a.date)-CM.toTs(b.date));
  const upNet=upcoming.reduce((s,x)=>s+x.amt,0);
  // row actions: keep / edit / delete / decline (stop the recurrence)
  const upArr=r=>r.kind==='bill'?(CM.S.bills||[]):(CM.S.earnings||[]);
  const upKeep=r=>CM.mutate(function(){ var it=upArr(r)[r.idx]; if(it) it.kept=!it.kept; });
  const upEdit=r=>{
    var nm=window.prompt('Description', r.desc); if(nm===null) return;
    var am=window.prompt('Amount ($)', String(Math.abs(r.amt))); if(am===null) return;
    var d0=new Date(CM.toTs(r.date));
    var dft=d0.getFullYear()+'-'+String(d0.getMonth()+1).padStart(2,'0')+'-'+String(d0.getDate()).padStart(2,'0');
    var dt=window.prompt('Date (YYYY-MM-DD)', dft); if(dt===null) return;
    CM.mutate(function(){ var x=upArr(r)[r.idx]; if(!x) return;
      if(nm){ x.desc=nm; if(x.name)x.name=nm; if(x.source)x.source=nm; }
      var v=parseFloat(am); if(!isNaN(v)&&v>0) x.amount=v;
      if(/^\d{4}-\d{2}-\d{2}$/.test(dt)){ if(r.kind==='bill'){ x.dueDate=dt; if(x.date)x.date=dt; } else { x.date=dt; } } });
  };
  const upDel=r=>{ if(!window.confirm('Delete "'+r.desc+'" scheduled '+CM.FD(new Date(CM.toTs(r.date)))+'? Only this occurrence is removed.')) return;
    CM.mutate(function(){ upArr(r).splice(r.idx,1); }); };
  const upDecline=r=>{ if(!window.confirm('Decline this recurring '+(r.kind==='bill'?'bill':'income')+'?\n\n"'+r.desc+'" will stop recurring and all future occurrences will be removed.')) return;
    var key=r.desc, tn=Date.now();
    CM.mutate(function(){
      var s=CM.S, nameOf=function(it){ return it.desc||it.name||it.source||''; };
      var dateOf=function(it){ return r.kind==='bill'?(it.dueDate||it.date):it.date; };
      (r.kind==='bill'?(s.bills||[]):(s.earnings||[])).forEach(function(it){ if(nameOf(it)===key){ it.recur='One-time'; it.recurring=false; it.declined=true; } });
      var keep=function(it){ if(nameOf(it)!==key) return true; var ts=CM.toTs(dateOf(it)); return !(it._projected || (ts&&ts>tn)); };
      if(r.kind==='bill') s.bills=(s.bills||[]).filter(keep); else s.earnings=(s.earnings||[]).filter(keep);
    });
    try{ CM.logActivity&&CM.logActivity('declined a recurring '+(r.kind==='bill'?'bill':'income'), r.desc, {cat:'recurring'}); }catch(e){}
  };
  // ----- enriched derivations for the redesigned Cash Flow -----
  const nets = cf.map(m=>m.inc-m.exp);
  let _rb=0; const runBal = cf.map(m=>{ _rb+=(m.inc-m.exp); return Math.round(_rb); });
  const monthlyRows2 = monthlyRows.map((m,i,arr)=>{ const sr=m.inc?Math.round(m.net/m.inc*100):0; const pv=arr[i-1]; const chg=(pv&&pv.net!==0)?Math.round((m.net-pv.net)/Math.abs(pv.net)*100):null; return Object.assign({},m,{sr:sr,chg:chg}); });
  const [sortBy,setSortBy] = React.useState({k:'mo',dir:1});
  const sortedMonthly = (function(){ var a=monthlyRows2.slice(); if(sortBy.k==='mo'){ if(sortBy.dir<0) a.reverse(); return a; } a.sort((x,y)=>(((x[sortBy.k]||0)-(y[sortBy.k]||0))*sortBy.dir)); return a; })();
  const spendDonut = outArr.map((x,i)=>({label:x.label,value:x.value,color:outColors[i%outColors.length]}));
  const incomeDonut = inArr.map((x,i)=>({label:x.label,value:x.value,color:inColors[i%inColors.length]}));
  const spendTotal = outArr.reduce((s,x)=>s+x.value,0);
  const incomeTotal = inArr.reduce((s,x)=>s+x.value,0);
  const billsRaw={}; (CM.S.bills||[]).forEach(b=>{ var k=b.cat||b.name||b.desc||'Bill'; billsRaw[k]=(billsRaw[k]||0)+(+b.amount||0); });
  const billsBreak=Object.keys(billsRaw).map(k=>({label:k,value:billsRaw[k]})).sort((a,b)=>b.value-a.value).slice(0,8);
  const billsMax=Math.max.apply(null, billsBreak.length?billsBreak.map(x=>x.value):[1])||1;
  const subsBreak=(CM.S.subscriptions||[]).filter(s=>s.active!==false).map(s=>({label:s.name||'Subscription',value:Math.round((+(s.price||s.amount||0))*(/year|annual/i.test(String(s.freq||s.cycle||''))?1/12:1))})).filter(x=>x.value>0).sort((a,b)=>b.value-a.value).slice(0,8);
  const subsMax=Math.max.apply(null, subsBreak.length?subsBreak.map(x=>x.value):[1])||1;
  const PERIODS=[['30d','Last 30 Days'],['60d','Last 60 Days'],['90d','Last 90 Days'],['120d','Last 120 Days'],['month','This Month'],['prevmonth','Previous Month'],['nextmonth','Next Month'],['thisquarter','This Quarter'],['prevquarter','Previous Quarter'],['nextquarter','Next Quarter'],['year','This Year'],['prevyear','Previous Year'],['nextyear','Next Year'],['all','All Periods']];
  const isRange = period&&typeof period==='object';
  const curMonthKey=()=>{ if(/^\d{4}-\d{2}$/.test(period)) return period; var nn=new Date(); return nn.getFullYear()+'-'+String(nn.getMonth()+1).padStart(2,'0'); };
  const shiftMonth=(delta)=>{ var k=curMonthKey().split('-'); var yy=+k[0], mm=+k[1]+delta; while(mm<1){mm+=12;yy--;} while(mm>12){mm-=12;yy++;} setPeriod(yy+'-'+String(mm).padStart(2,'0')); };
  const navLabel = /^\d{4}-\d{2}$/.test(period) ? new Date(period+'-01T00:00').toLocaleDateString('en-US',{month:'long',year:'numeric'}) : (isRange?'Custom Range':(PERIODS.find(p=>p[0]===period)||['','All Periods'])[1]);
  const dateInp = {width:'100%',boxSizing:'border-box',fontFamily:'inherit',fontSize:13,color:'var(--ink-700)',border:'1px solid var(--line)',borderRadius:9,padding:'8px 9px',background:'#fff',outline:'none'};
  const doExport=()=>{ exportCashFlowCSV(monthlyRows2, navLabel); };
  const doSaveView=()=>{ try{ localStorage.setItem('cm_cashflow_view', JSON.stringify(period)); cmToast('View saved'); }catch(e){} };
  const doReset=()=>{ try{ localStorage.removeItem('cm_cashflow_view'); }catch(e){} setPeriod('all'); };
  const doRefresh=()=>{ try{ window.cmRefreshData&&window.cmRefreshData(); }catch(e){} };
  return (
    <div>
      <div style={{display:'grid',gridTemplateColumns:'1fr auto 1fr',alignItems:'center',gap:12,marginBottom:14}}>
        <div/>
        <div style={{display:'inline-flex',alignItems:'center',gap:8,justifySelf:'center',background:'#fff',border:'1px solid var(--line)',borderRadius:12,padding:'6px 8px'}}>
          <button onClick={()=>shiftMonth(-1)} aria-label="Previous month" style={{border:0,background:'var(--mint-50)',width:30,height:30,borderRadius:8,cursor:'pointer',fontSize:18,lineHeight:1,color:'var(--green-600)'}}>‹</button>
          <div style={{minWidth:150,textAlign:'center',fontWeight:700,fontSize:14,color:'var(--ink-900)'}}>{navLabel}</div>
          <button onClick={()=>shiftMonth(1)} aria-label="Next month" style={{border:0,background:'var(--mint-50)',width:30,height:30,borderRadius:8,cursor:'pointer',fontSize:18,lineHeight:1,color:'var(--green-600)'}}>›</button>
        </div>
        <div style={{justifySelf:'end',display:'flex',gap:8,flexWrap:'wrap'}}>
          <button className="cm-btn cm-btn-soft" onClick={doRefresh}>Refresh</button>
          <button className="cm-btn cm-btn-soft" onClick={doExport}>Export</button>
          <button className="cm-btn cm-btn-soft" onClick={doSaveView}>Save View</button>
          <button className="cm-btn cm-btn-soft" onClick={doReset}>Reset Filters</button>
        </div>
      </div>
      <Tabs tabs={['Overview','Monthly Table','Upcoming','By Category']} value={tab} onChange={setTab}/>
      <div style={{height:16}}/>
      <div style={{display:'flex',gap:18,alignItems:'flex-start',flexWrap:'wrap'}}>
        <div className="cm-card" style={{flex:'0 0 210px',padding:'14px 12px',position:'sticky',top:16}}>
          <div style={{fontSize:11,fontWeight:700,letterSpacing:'.09em',color:'var(--ink-400)',textTransform:'uppercase',padding:'2px 10px 8px'}}>Period</div>
          {PERIODS.map(o=>(
            <div key={o[0]} onClick={()=>setPeriod(o[0])} className="cmpb-opt"
              style={{fontSize:13,fontWeight:600,padding:'8px 10px',borderRadius:9,cursor:'pointer',background:period===o[0]?'var(--mint-100)':'transparent',color:period===o[0]?'var(--green-600)':'var(--ink-700)'}}>{o[1]}</div>
          ))}
          <div style={{borderTop:'1px solid var(--line)',margin:'12px 0 10px'}}/>
          <div style={{fontSize:12.5,fontWeight:700,color:'var(--ink-500)',padding:'0 10px 10px'}}>Custom Range</div>
          <div style={{fontSize:10.5,fontWeight:700,letterSpacing:'.09em',color:'var(--ink-400)',textTransform:'uppercase',padding:'0 10px 5px'}}>From</div>
          <div style={{margin:'0 10px 12px'}}><input type="date" value={isRange?(period.from||''):''} onChange={e=>setPeriod({from:e.target.value,to:isRange?period.to:''})} style={dateInp}/></div>
          <div style={{fontSize:10.5,fontWeight:700,letterSpacing:'.09em',color:'var(--ink-400)',textTransform:'uppercase',padding:'0 10px 5px'}}>To</div>
          <div style={{margin:'0 10px 6px'}}><input type="date" value={isRange?(period.to||''):''} onChange={e=>setPeriod({from:isRange?period.from:'',to:e.target.value})} style={dateInp}/></div>
          <div style={{borderTop:'1px solid var(--line)',margin:'12px 0 10px'}}/>
          <button className="cm-btn cm-btn-soft" style={{width:'100%',justifyContent:'center'}} onClick={doExport}>Export Statement</button>
        </div>
        <div style={{flex:1,minWidth:0}}>
      <KpiRow>
        <MetricCard label="Total Inflow" labelColor="var(--green-600)" value={fmt(totalIn)} sub={periodLabel}/>
        <MetricCard label="Total Outflow" labelColor="var(--red-500)" value={fmt(totalOut)} sub={periodLabel}/>
        <MetricCard label="Net Cash Flow" labelColor={net>=0?'var(--green-600)':'var(--red-500)'} value={(net>=0?'+':'-')+fmt(Math.abs(net)).slice(1)} sub={periodLabel}
          art={<div style={{width:84}}><TrendChart data={trend.length?trend:[0]} width={84} height={42} color={G} pad={{l:2,r:2,t:6,b:6}} showDots={false}/></div>}/>
        <MetricCard label="Avg Monthly Net" labelColor={avgNet>=0?'var(--ink-700)':'var(--red-500)'} value={(avgNet>=0?'+':'-')+fmt(Math.abs(avgNet)).slice(1)} sub={nMo+' month'+(nMo===1?'':'s')}/>
      </KpiRow>

      {tab==='Overview' && (<React.Fragment>
        <ChartCard title="Cash Flow Overview" className="cm-mb-24" action={<span style={{display:'inline-flex',gap:16}}>
          <span className="t-caption" style={{display:'flex',alignItems:'center',gap:6}}><span style={{width:9,height:9,borderRadius:2,background:'#34B27B'}}/>Inflow</span>
          <span className="t-caption" style={{display:'flex',alignItems:'center',gap:6}}><span style={{width:9,height:9,borderRadius:2,background:'#E2655F'}}/>Outflow</span>
          <span className="t-caption" style={{display:'flex',alignItems:'center',gap:6}}><span style={{width:14,height:3,borderRadius:2,background:'#2A6FDB'}}/>Net</span>
        </span>}>
          {cf.length?<div style={{overflowX:'auto'}}><CashFlowCombo data={cf} fmt={fmt}/></div>:<div className="t-caption" style={{textAlign:'center',padding:'34px 0'}}>No activity in {periodLabel}.</div>}
        </ChartCard>
        <div className="cm-grid-2a cm-mb-24">
          <ChartCard title="Income Trend"><TrendChart data={cf.length?cf.map(m=>m.inc):[0]} width={360} height={200} color={G} xLabels={cf.map(m=>m.mo)}/></ChartCard>
          <ChartCard title="Expense Trend"><TrendChart data={cf.length?cf.map(m=>m.exp):[0]} width={360} height={200} color={RD} xLabels={cf.map(m=>m.mo)}/></ChartCard>
        </div>
        <div className="cm-grid-2a cm-mb-24">
          <ChartCard title="Net Cash Flow Trend"><TrendChart data={nets.length?nets:[0]} width={360} height={200} color={BL} xLabels={cf.map(m=>m.mo)}/></ChartCard>
          <ChartCard title="Running Balance Projection"><TrendChart data={runBal.length?runBal:[0]} width={360} height={200} color={G} xLabels={cf.map(m=>m.mo)}/></ChartCard>
        </div>
        <ChartCard title="Income vs Expenses" className="cm-mb-24">
          <div style={{display:'flex',gap:24,marginBottom:8}}>
            <span className="t-caption" style={{display:'flex',alignItems:'center',gap:6}}><span style={{width:9,height:9,borderRadius:2,background:G}}/>Income</span>
            <span className="t-caption" style={{display:'flex',alignItems:'center',gap:6}}><span style={{width:9,height:9,borderRadius:2,background:'#FBD0D2'}}/>Expenses</span>
          </div>
          {cf.length?<FlowBars rows={bars} max={maxBar}/>:<div className="t-caption" style={{textAlign:'center',padding:'34px 0'}}>No activity in {periodLabel}.</div>}
        </ChartCard>
        <div className="cm-grid-2">
          <ChartCard title="Where Money Comes From">
            <div style={{paddingTop:4}}>
              {inArr.length?inArr.map((r,i)=><CategoryBar key={i} icon={<Ib.Dollar size={16}/>} color={inColors[i%inColors.length]} label={r.label} value={fmt(r.value)} width={Math.round(r.value/inMax*100)}/>):<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No inflows in {periodLabel}.</div>}
            </div>
          </ChartCard>
          <ChartCard title="Where Money Goes">
            <div style={{paddingTop:4}}>
              {outArr.length?outArr.map((r,i)=><CategoryBar key={i} icon={<Ib.Wallet size={16}/>} color={outColors[i%outColors.length]} label={r.label} value={fmt(r.value)} width={Math.round(r.value/outMax*100)}/>):<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No outflows in {periodLabel}.</div>}
            </div>
          </ChartCard>
        </div>
      </React.Fragment>)}

      {tab==='Monthly Table' && (
        <ChartCard title={'Monthly Cash Flow · '+periodLabel} action={<button className="cm-btn cm-btn-soft" onClick={doExport}>Export CSV</button>}>
          {sortedMonthly.length?(
          <div style={{overflowX:'auto'}}><table className="cm-table" style={{minWidth:660}}>
            <thead><tr>
              {[['mo','Month','left'],['inc','Income','right'],['exp','Expenses','right'],['net','Net','right'],['sr','Savings Rate','right'],['chg','Change %','right']].map(col=>(
                <th key={col[0]} style={{textAlign:col[2],cursor:'pointer',userSelect:'none'}} onClick={()=>setSortBy(s=>({k:col[0],dir:s.k===col[0]?-s.dir:1}))}>{col[1]}{sortBy.k===col[0]?(sortBy.dir>0?' ▲':' ▼'):''}</th>
              ))}
            </tr></thead>
            <tbody>{sortedMonthly.map((m,i)=>(<tr key={i}>
              <td><span style={{fontWeight:700,color:'var(--ink-900)'}}>{m.mo}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{color:G,fontWeight:700}}>{fmt(m.inc)}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{color:'var(--red-500)',fontWeight:700}}>{fmt(m.exp)}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{fontWeight:800,color:m.net>=0?G:'var(--red-500)'}}>{(m.net>=0?'+':'-')+fmt(Math.abs(m.net)).slice(1)}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{fontWeight:700,color:m.sr>=0?'var(--ink-700)':'var(--red-500)'}}>{m.sr}%</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{fontWeight:700,color:m.chg==null?'var(--ink-400)':(m.chg>=0?G:'var(--red-500)')}}>{m.chg==null?'—':((m.chg>=0?'+':'')+m.chg+'%')}</span></td>
            </tr>))}</tbody>
          </table></div>
          ):<div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>No activity in {periodLabel}.</div>}
        </ChartCard>
      )}

      {tab==='Upcoming' && (
        <ChartCard title="Upcoming Cash Flow · Next 30 Days" action={<span className="t-caption">Earnings &amp; upcoming bills · Net <b style={{color:upNet>=0?G:'var(--red-500)'}}>{(upNet>=0?'+':'-')+fmt(Math.abs(upNet)).slice(1)}</b></span>}>
          {upcoming.length?(
          <div style={{overflowX:'auto'}}><table className="cm-table" style={{minWidth:740}}>
            <thead><tr><th>Date</th><th>Description</th><th>Type</th><th className="right">Amount</th><th className="right">Actions</th></tr></thead>
            <tbody>{upcoming.map((u,i)=>(<tr key={u.kind+'-'+u.idx}>
              <td><span className="num" style={{color:'var(--ink-600)',fontWeight:600,whiteSpace:'nowrap'}}>{CM.FD(new Date(CM.toTs(u.date)))}</span></td>
              <td><span style={{fontWeight:600,color:'var(--ink-900)'}}>{u.desc}</span></td>
              <td><span style={{display:'inline-flex',gap:6,alignItems:'center'}}>
                <StatusPill tone={u.amt>=0?'green':'slate'}>{u.amt>=0?'Income':'Bill'}</StatusPill>
                {u.rec?<StatusPill tone="amber">{/^recurring$/i.test(String(u.rec))?'Recurring':u.rec}</StatusPill>:null}
              </span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{fontWeight:700,color:u.amt>=0?G:'var(--ink-900)'}}>{(u.amt>=0?'+':'-')+fmt(Math.abs(u.amt)).slice(1)}</span></td>
              <td style={{textAlign:'right'}}><span style={{display:'inline-flex',gap:12,whiteSpace:'nowrap',justifyContent:'flex-end'}}>
                <a className="t-label" style={{cursor:'pointer',color:u.kept?G:'var(--ink-500)',fontWeight:u.kept?800:600}} onClick={()=>upKeep(u)} title="Confirm this stays in your cash flow">{u.kept?'✓ Kept':'Keep'}</a>
                <a className="t-label" style={{cursor:'pointer',color:'var(--ink-500)'}} onClick={()=>upEdit(u)}>Edit</a>
                <a className="t-label" style={{cursor:'pointer',color:'var(--red-500)'}} onClick={()=>upDel(u)}>Delete</a>
                {u.rec?<a className="t-label" style={{cursor:'pointer',color:'#B5740F'}} onClick={()=>upDecline(u)} title="Stop this recurring transaction">Decline</a>:null}
              </span></td>
            </tr>))}</tbody>
          </table></div>
          ):<div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>Nothing scheduled in the next 30 days.</div>}
        </ChartCard>
      )}

      {tab==='By Category' && (<React.Fragment>
        <div className="cm-grid-2a cm-mb-24">
          <ChartCard title="Spending by Category">
            {spendDonut.length?<React.Fragment><div className="cm-donut-card" style={{justifyContent:'center'}}><DonutChart size={150} thickness={22} center={{value:fmt(spendTotal),label:'Spending'}} segments={spendDonut}/></div><div style={{marginTop:14}}><Legend items={spendDonut.map(s=>({color:s.color,label:s.label,value:fmt(s.value)}))}/></div></React.Fragment>:<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No spending in {periodLabel}.</div>}
          </ChartCard>
          <ChartCard title="Income by Category">
            {incomeDonut.length?<React.Fragment><div className="cm-donut-card" style={{justifyContent:'center'}}><DonutChart size={150} thickness={22} center={{value:fmt(incomeTotal),label:'Income'}} segments={incomeDonut}/></div><div style={{marginTop:14}}><Legend items={incomeDonut.map(s=>({color:s.color,label:s.label,value:fmt(s.value)}))}/></div></React.Fragment>:<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No income in {periodLabel}.</div>}
          </ChartCard>
        </div>
        <ChartCard title="Top Spending Categories" className="cm-mb-24">
          <div style={{paddingTop:4}}>{outArr.length?outArr.map((r,i)=><CategoryBar key={i} icon={<Ib.Wallet size={16}/>} color={outColors[i%outColors.length]} label={r.label} value={fmt(r.value)} width={Math.round(r.value/outMax*100)}/>):<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No spending in {periodLabel}.</div>}</div>
        </ChartCard>
        <div className="cm-grid-2a cm-mb-24">
          <ChartCard title="Recurring Bills Breakdown">
            <div style={{paddingTop:4}}>{billsBreak.length?billsBreak.map((r,i)=><CategoryBar key={i} icon={<Ib.File size={16}/>} color={outColors[i%outColors.length]} label={r.label} value={fmt(r.value)} width={Math.round(r.value/billsMax*100)}/>):<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No recurring bills.</div>}</div>
          </ChartCard>
          <ChartCard title="Subscription Breakdown">
            <div style={{paddingTop:4}}>{subsBreak.length?subsBreak.map((r,i)=><CategoryBar key={i} icon={<Ib.Card size={16}/>} color={inColors[i%inColors.length]} label={r.label} value={fmt(r.value)+'/mo'} width={Math.round(r.value/subsMax*100)}/>):<div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>No subscriptions.</div>}</div>
          </ChartCard>
        </div>
        <ChartCard title={'Cash Flow by Category · '+periodLabel}>
          {catRows.length?(
          <div style={{overflowX:'auto'}}><table className="cm-table" style={{minWidth:560}}>
            <thead><tr><th>Category</th><th className="right">Inflow</th><th className="right">Outflow</th><th className="right">Net</th></tr></thead>
            <tbody>{catRows.map((c,i)=>(<tr key={i}>
              <td><span style={{fontWeight:700,color:'var(--ink-900)'}}>{c.cat}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{color:G,fontWeight:700}}>{c.inc?fmt(c.inc):'—'}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{color:'var(--red-500)',fontWeight:700}}>{c.exp?fmt(c.exp):'—'}</span></td>
              <td style={{textAlign:'right'}}><span className="num" style={{fontWeight:800,color:c.net>=0?G:'var(--red-500)'}}>{(c.net>=0?'+':'-')+fmt(Math.abs(c.net)).slice(1)}</span></td>
            </tr>))}</tbody>
          </table></div>
          ):<div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>No categorized activity in {periodLabel}.</div>}
        </ChartCard>
      </React.Fragment>)}
        </div>
      </div>
    </div>
  );
}
window.PageCashFlow = PageCashFlow;

/* ============================ EARNINGS & INCOME ============================ */
const EARN_CATS=['Salary','Bonus','Commission','Freelance','Benefits','Child Benefit','Pension','Rental Income','Investment','Dividend','Interest','Refund','Government','Other'];
const RECUR_OPTS=['One-time','Weekly','Biweekly','Semi-monthly','Monthly','Quarterly','Yearly'];
// Government / CRA income (Child Benefit, GST, Trillium, EI, OAS/CPP, Canada FED/PRO…) → Canada logo.
function isGovEarn(e){
  var t = String((e&&e.cat)||'').toLowerCase()+' '+String((e&&(e.desc||e.source))||'').toLowerCase();
  return /\b(cra|canada revenue|child[\w ]{0,14}benefit|ccb|cctb|canada child|gst|trillium|carbon|climate action|ei|employment insurance|service canada|government|canada fed|canada pro|canada txd|old age|oas|cpp)\b/.test(t);
}
function earnLogoName(e){ return isGovEarn(e) ? 'Canada Revenue Agency' : (e.desc||e.source||'Income'); }
// Regenerate the 24-month forward projections after a recurring earning/bill changes.
function reproject(){ try{ window.CMrecurring && window.CMrecurring.generateProjections && window.CMrecurring.generateProjections(); }catch(e){} }
function EarningModal({ CM, idx, onClose }){
  const adding = idx==null||idx<0;
  const e = adding?{}:((CM.S.earnings||[])[idx]||{});
  const toISO=d=>{ var t=CM.toTs(d); if(!t) return ''; var x=new Date(t); return x.getFullYear()+'-'+String(x.getMonth()+1).padStart(2,'0')+'-'+String(x.getDate()).padStart(2,'0'); };
  const [f,setF]=React.useState(()=>({ source:e.desc||e.source||'', cat:e.cat||'Salary', amount:e.amount!=null?String(e.amount):'', date:toISO(e.date), recur:e.recur||'Monthly', account:e.account||'', notes:e.notes||'' }));
  const isRecurEdit = !adding && !/one.?time/i.test(String(f.recur));   // only recurring edits fan out to future months
  const isRecurDel = !adding && (e.recurring===true || (e.recur && !/one.?time/i.test(String(e.recur))));
  const [applyFuture,setApplyFuture]=React.useState(true);
  const [override,setOverride]=React.useState(true);
  // when adding, detect an existing earning with the same name so we can override instead of duplicating
  const dupIdx = React.useMemo(()=>{
    if(!adding) return -1; const key=(f.source||'').trim().toLowerCase(); if(!key) return -1;
    const arr=CM.S.earnings||[];
    for(let i=0;i<arr.length;i++){ const x=arr[i]; if(x._projected) continue;
      if(((x.desc||x.source||'').trim().toLowerCase())===key) return i; }
    return -1;
  },[adding,f.source]);
  const dupName = dupIdx>=0 ? ((CM.S.earnings[dupIdx].desc||CM.S.earnings[dupIdx].source||'this earning')) : '';
  const set=k=>ev=>{ const v=ev.target.value; setF(s=>({...s,[k]:v})); };
  const inp={width:'100%',fontFamily:'inherit',fontSize:13.5,color:'var(--ink-900)',border:'1px solid var(--line)',borderRadius:10,padding:'9px 11px',background:'#fff',outline:'none',boxSizing:'border-box'};
  const save=()=>{ const rec={ desc:f.source||'Income', source:f.source||'Income', cat:f.cat, amount:parseFloat(f.amount)||0, recur:f.recur, recurring:!/one.?time/i.test(f.recur), account:f.account||'', notes:f.notes||'' }; rec.date=f.date?new Date(f.date+'T00:00'):(adding?new Date():e.date);
    const changes={ desc:rec.desc, source:rec.source, cat:rec.cat, amount:rec.amount, recur:rec.recur, recurring:rec.recurring, account:rec.account, notes:rec.notes, date:rec.date };
    if(adding && dupIdx>=0 && override && window.cmSeriesEdit){
      // override the matching earning instead of creating a duplicate
      window.cmSeriesEdit('earnings', dupIdx, changes, rec.recurring===true);
    } else if(adding){ CM.mutate(function(){ (CM.S.earnings=CM.S.earnings||[]).push(rec); }); reproject(); }
    else if(window.cmSeriesEdit){
      window.cmSeriesEdit('earnings', idx, changes, isRecurEdit && applyFuture);
    } else { CM.mutate(function(){ Object.assign(CM.S.earnings[idx], rec); }); reproject(); }
    try{ CM.logActivity&&CM.logActivity(adding?(dupIdx>=0&&override?'overrode an income source':'added an income source'):'updated an income source', rec.desc, {cat:'earnings'}); }catch(e2){}
    onClose(); };
  const del=()=>{ if(adding){ onClose(); return; }
    if(isRecurDel){
      if(!window.confirm('Delete the entire recurring earning "'+(e.desc||e.source||'this income')+'"?\n\nThis removes every occurrence — past entries and all scheduled future months.')) return;
      if(window.cmSeriesDelete){ window.cmSeriesDelete('earnings', idx); }
      else { const k=(e.desc||e.source||'').toLowerCase(); CM.mutate(function(){ CM.S.earnings=(CM.S.earnings||[]).filter(function(x){return (x.desc||x.source||'').toLowerCase()!==k;}); }); reproject(); }
    } else {
      if(!window.confirm('Delete "'+(e.desc||'this income')+'"?')) return;
      CM.mutate(function(){ CM.S.earnings.splice(idx,1); });
    }
    onClose(); };
  return (<div onClick={onClose} style={{position:'fixed',inset:0,zIndex:1000,background:'rgba(13,33,26,.45)',display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
    <div onClick={ev=>ev.stopPropagation()} className="cm-card" style={{width:560,maxWidth:'100%',maxHeight:'92vh',overflowY:'auto',borderRadius:18,padding:'22px 24px',boxShadow:'0 24px 64px rgba(13,42,33,.28)'}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:4}}><div className="t-h2" style={{fontSize:18}}>{adding?'Add Earning':'Edit Earning'}</div><button onClick={onClose} aria-label="Close" style={{border:0,background:'none',cursor:'pointer',color:'var(--ink-400)',fontSize:20,lineHeight:1,padding:4}}>×</button></div>
      <div className="t-caption" style={{marginBottom:16}}>{adding?'Record a new income source or one-time earning.':'Update this income entry — changes apply everywhere.'}</div>
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:14}}>
        <CardField label="Company / Source" full={true}><input style={inp} value={f.source} onChange={set('source')} placeholder="e.g. County of Simcoe Payroll"/></CardField>
        <CardField label="Nature / Category"><select style={inp} value={f.cat} onChange={set('cat')}>{EARN_CATS.map(c=><option key={c} value={c}>{c}</option>)}</select></CardField>
        <CardField label="Amount ($)"><input style={inp} type="number" value={f.amount} onChange={set('amount')}/></CardField>
        <CardField label="Date"><input style={inp} type="date" value={f.date} onChange={set('date')}/></CardField>
        <CardField label="Is this a one-time payment or a recurring earning?" full={true}>
          <div className="cm-seg" style={{display:'flex',width:'100%'}}>
            <button type="button" style={{flex:1}} className={/one.?time/i.test(f.recur)?'active':''} onClick={()=>setF(s=>({...s,recur:'One-time'}))}>One-time payment</button>
            <button type="button" style={{flex:1}} className={!/one.?time/i.test(f.recur)?'active':''} onClick={()=>setF(s=>({...s,recur:/one.?time/i.test(f.recur)?'Monthly':f.recur}))}>Recurring earning</button>
          </div>
        </CardField>
        {!/one.?time/i.test(f.recur) && <CardField label="How often"><select style={inp} value={f.recur} onChange={set('recur')}>{RECUR_OPTS.filter(o=>!/one.?time/i.test(o)).map(c=><option key={c} value={c}>{c}</option>)}</select></CardField>}
        <CardField label="Account"><input style={inp} value={f.account} onChange={set('account')} placeholder="e.g. TD Chequing"/></CardField>
        <CardField label="Notes" full={true}><textarea style={{...inp,minHeight:56,resize:'vertical'}} value={f.notes} onChange={set('notes')}/></CardField>
      </div>
      {isRecurEdit && <label style={{display:'flex',gap:9,alignItems:'flex-start',marginTop:16,padding:'11px 13px',background:'var(--mint-50)',border:'1px solid var(--line)',borderRadius:11,cursor:'pointer'}}>
        <input type="checkbox" checked={applyFuture} onChange={e=>setApplyFuture(e.target.checked)} style={{width:16,height:16,marginTop:1}}/>
        <span style={{fontSize:13,color:'var(--ink-700)',lineHeight:1.4}}><b style={{color:'var(--ink-900)'}}>Apply to all future occurrences</b><br/>Update every upcoming month of this earning — amount, name, category and more. Leave unchecked to change only this month.</span>
      </label>}
      {adding && dupIdx>=0 && <label style={{display:'flex',gap:9,alignItems:'flex-start',marginTop:16,padding:'11px 13px',background:'#FFF8EC',border:'1px solid #F0D9AE',borderRadius:11,cursor:'pointer'}}>
        <input type="checkbox" checked={override} onChange={e=>setOverride(e.target.checked)} style={{width:16,height:16,marginTop:1}}/>
        <span style={{fontSize:13,color:'var(--ink-700)',lineHeight:1.4}}><b style={{color:'var(--ink-900)'}}>Override the existing &ldquo;{dupName}&rdquo; earning</b><br/>An earning with this name already exists. Overriding updates it{!/one.?time/i.test(f.recur)?' and its future months':''} instead of creating a duplicate. Uncheck to add it as a separate earning.</span>
      </label>}
      <div style={{display:'flex',gap:10,marginTop:20,alignItems:'center'}}>{!adding&&<button className="cm-btn cm-btn-ghost" style={{color:'var(--red-500)'}} onClick={del}>{isRecurDel?'Delete recurring':'Delete'}</button>}<div style={{flex:1}}></div><button className="cm-btn cm-btn-soft" onClick={onClose}>Cancel</button><button className="cm-btn cm-btn-primary" onClick={save}>{adding?(dupIdx>=0&&override?'Override Earning':'Add Earning'):'Save Changes'}</button></div>
    </div></div>);
}
function PageIncome() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const fmt = CM.M0, fmt2 = CM.M;
  const [tab, setTab] = React.useState('All Earnings');
  const [period, setPeriod] = React.useState(function(){ var n=new Date(); return n.getFullYear()+'-'+String(n.getMonth()+1).padStart(2,'0'); }); // start in the current month, not All Periods
  const [eModal, setEModal] = React.useState(null);
  React.useEffect(()=>{ const h=()=>setEModal({idx:-1}); window.addEventListener('cm-add-earning',h); return ()=>window.removeEventListener('cm-add-earning',h); },[]);
  const incCurKey=()=>{ if(/^\d{4}-\d{2}$/.test(period)) return period; var n=new Date(); return n.getFullYear()+'-'+String(n.getMonth()+1).padStart(2,'0'); };
  const shiftIncMonth=(d)=>{ var k=incCurKey().split('-'); var y=+k[0],m=+k[1]+d; while(m<1){m+=12;y--;} while(m>12){m-=12;y++;} setPeriod(y+'-'+String(m).padStart(2,'0')); };
  const incNavLabel = /^\d{4}-\d{2}$/.test(period)? new Date(period+'-01T00:00').toLocaleDateString('en-US',{month:'long',year:'numeric'}) : 'All Periods';
  const earn = CM.S.earnings||[];
  const earnP = (CM.S.earnings||[]).filter(e=>!window.cmInPeriod||window.cmInPeriod(e.date,period));
  const periodLabel = window.cmPeriodLabel?window.cmPeriodLabel(period):'';
  const now = new Date(), tm = CM.getMonthKey(now), yr = now.getFullYear();
  const incomeP = earnP.reduce((s,e)=>s+(+e.amount||0),0);
  const nMonths = window.cmPeriodMonths?window.cmPeriodMonths(earnP.map(e=>e.date)):1;
  const avg = Math.round(incomeP/nMonths);
  const ytd = earn.filter(e=>e.date&&e.date.getFullYear&&e.date.getFullYear()===yr).reduce((s,e)=>s+(+e.amount||0),0);
  const future = earn.filter(e=>CM.toTs(e.date)>CM.toTs(now)).sort((a,b)=>CM.toTs(a.date)-CM.toTs(b.date))[0];
  // group by source (desc), period-scoped
  const byName = {};
  earnP.forEach(e=>{ const k=e.desc||'Income'; (byName[k]=byName[k]||{name:k,type:e.cat||'Income',freq:e.recur||'—',per:0,ytd:0,_n:0}); var b=byName[k];
    b.per+=(+e.amount||0); if(e.date&&e.date.getFullYear&&e.date.getFullYear()===yr)b.ytd+=(+e.amount||0); });
  let sources = Object.keys(byName).map(k=>byName[k]).sort((a,b)=>b.per-a.per);
  const typeIcon = {Salary:<Ib.Briefcase size={18}/>,Benefits:<Ib.Heart size={18}/>,Investment:<Ib.Bars size={18}/>,Income:<Ib.Dollar size={18}/>};
  const typeColor = {Salary:G,Benefits:PINK,Investment:TEAL,Income:BL};
  const types = ['All Earnings','By Month'].concat(Array.from(new Set(sources.map(s=>s.type))));
  const rows = (tab==='All Earnings'||tab==='By Month') ? sources : sources.filter(s=>s.type===tab);
  // every dated income entry (incl. each month of a recurring earning), newest first
  const entries = earnP.slice().sort((a,b)=>(CM.toTs(b.date)||0)-(CM.toTs(a.date)||0));
  const entryEdit = (e)=>{ var gi=(CM.S.earnings||[]).indexOf(e); if(gi<0) return; setEModal({idx:gi}); };
  const entryDel = (e)=>{ var gi=(CM.S.earnings||[]).indexOf(e); if(gi<0) return;
    var ts=CM.toTs(e.date);
    if(!window.confirm('Delete this entry — "'+(e.desc||'Income')+'" on '+(ts?CM.FD(new Date(ts)):'—')+'? Only this occurrence is removed.')) return;
    CM.mutate(function(){ CM.S.earnings.splice(gi,1); }); };
  const entryCols = [
    {header:'Date', cell:r=>{ const ts=CM.toTs(r.date); return <span className="num" style={{fontSize:13,fontWeight:600,color:'var(--ink-700)',whiteSpace:'nowrap'}}>{ts?CM.FD(new Date(ts)):'—'}</span>;}},
    {header:'Description', cell:r=><DataTable.Lead thumb={window.BrandIcon ? <window.BrandIcon name={earnLogoName(r)} size={34} radius={9}/> : <span style={{color:G}}><Ib.Dollar size={18}/></span>} primary={r.desc||r.source||'Income'} sub={r.account||''}/>},
    {header:'Category', cell:r=><StatusPill tone="slate">{r.cat||'Income'}</StatusPill>},
    {header:'Amount', align:'right', cell:r=><span className="cm-cell-strong num" style={{color:G}}>{'+'+fmt2(r.amount||0)}</span>},
    {header:'Recurrence', cell:r=>{ const rec=r.recurring===true||(r.recur&&!/one.?time/i.test(String(r.recur))); return <span style={{display:'inline-flex',gap:6}}>
      <StatusPill tone={rec?'amber':'slate'}>{r.recur||(rec?'Recurring':'One-time')}</StatusPill>
      {(r.scheduled||r._projected)?<StatusPill tone="green">Scheduled</StatusPill>:null}</span>;}},
    {header:'Notes', cell:r=><span className="t-caption" style={{maxWidth:150,display:'inline-block',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',verticalAlign:'bottom'}}>{r.notes||''}</span>},
    {header:'', align:'right', cell:r=><span style={{display:'inline-flex',gap:12,whiteSpace:'nowrap'}}>
      <a className="t-label" style={{cursor:'pointer',color:'var(--ink-500)'}} onClick={()=>entryEdit(r)}>Edit</a>
      <a className="t-label" style={{cursor:'pointer',color:'var(--red-500)'}} onClick={()=>entryDel(r)}>Delete</a></span>},
  ];
  // monthly income series (last 12)
  const series=[]; for(let i=11;i>=0;i--){ const k=CM.getMonthKey(new Date(yr,now.getMonth()-i,1)); series.push(earn.filter(e=>CM.getMonthKey(e.date)===k).reduce((s,e)=>s+(+e.amount||0),0)/1000); }
  // donut by source (period-scoped)
  const pal=[G,BL,AM,PU,TEAL,INDIGO];
  const donutArr = sources.filter(s=>s.per>0).map((s,i)=>({label:s.name,value:s.per,color:pal[i%pal.length]}));
  const editSource = (name)=>{ var arr=(CM.S.earnings||[]).map((x,i)=>({x,i})).filter(o=>(o.x.desc||o.x.source||'Income')===name).sort((a,b)=>(CM.toTs(b.x.date)||0)-(CM.toTs(a.x.date)||0)); if(arr[0]) setEModal({idx:arr[0].i}); };
  const delSource = (name)=>{ var n=(CM.S.earnings||[]).filter(function(e){return (e.desc||'Income')===name;}).length;
    if(!window.confirm('Delete "'+name+'" and its '+n+' entr'+(n===1?'y':'ies')+'? This removes every occurrence, including scheduled future months.')) return;
    CM.mutate(function(){ CM.S.earnings=(CM.S.earnings||[]).filter(function(e){return (e.desc||'Income')!==name;}); }); reproject(); };
  const cols = [
    {header:'Source', cell:r=><DataTable.Lead thumb={window.BrandIcon ? <window.BrandIcon name={isGovEarn({cat:r.type,desc:r.name}) ? 'Canada Revenue Agency' : r.name} size={36} radius={9}/> : <span style={{color:typeColor[r.type]||G}}>{typeIcon[r.type]||<Ib.Dollar size={18}/>}</span>} primary={r.name} sub={r.type}/>},
    {header:'Type', cell:r=><span className="cm-cell-meta" style={{fontSize:13,color:'var(--ink-500)',fontWeight:600}}>{r.type}</span>},
    {header:'Frequency', cell:r=><span className="cm-cell-meta" style={{fontSize:13,color:'var(--ink-500)',fontWeight:600}}>{r.freq}</span>},
    {header:'In period', cell:r=><span className="cm-cell-strong num">{fmt2(r.per)}</span>},
    {header:'YTD', align:'right', cell:r=><span className="cm-cell-strong num">{fmt2(r.ytd)}</span>},
    {header:'', align:'right', cell:r=><span style={{display:'inline-flex',gap:12,whiteSpace:'nowrap'}}>
      <a className="t-label" style={{cursor:'pointer',color:'var(--ink-500)'}} onClick={()=>editSource(r.name)}>Edit</a>
      <a className="t-label" style={{cursor:'pointer',color:'var(--red-500)'}} onClick={()=>delSource(r.name)}>Delete</a></span>},
  ];
  return (
    <div>
      <div style={{display:'grid',gridTemplateColumns:'1fr auto 1fr',alignItems:'center',gap:12,marginBottom:14}}>
        <div/>
        <div style={{display:'inline-flex',alignItems:'center',gap:8,justifySelf:'center',background:'#fff',border:'1px solid var(--line)',borderRadius:12,padding:'6px 8px'}}>
          <button onClick={()=>shiftIncMonth(-1)} aria-label="Previous month" style={{border:0,background:'var(--mint-50)',width:30,height:30,borderRadius:8,cursor:'pointer',fontSize:18,lineHeight:1,color:'var(--green-600)'}}>‹</button>
          <div style={{minWidth:140,textAlign:'center',fontWeight:700,fontSize:14,color:'var(--ink-900)'}}>{incNavLabel}</div>
          <button onClick={()=>shiftIncMonth(1)} aria-label="Next month" style={{border:0,background:'var(--mint-50)',width:30,height:30,borderRadius:8,cursor:'pointer',fontSize:18,lineHeight:1,color:'var(--green-600)'}}>›</button>
        </div>
        <div style={{justifySelf:'end',display:'flex',gap:8,alignItems:'center'}}>{window.PeriodBar ? <window.PeriodBar value={period} onChange={setPeriod}/> : null}<button className="cm-btn cm-btn-primary" onClick={()=>setEModal({idx:-1})}><Ib.Plus size={15}/>Add Earning</button></div>
      </div>
      {/* KPI tiles at the top, then the income table, then the charts. */}
      <KpiRow>
        <MetricCard label="Income" labelColor="var(--green-600)" value={fmt(incomeP)} sub={periodLabel}/>
        <MetricCard label="Income YTD" value={fmt(ytd)} sub="this year"/>
        <MetricCard label="Avg / Month" value={fmt(avg)} sub={periodLabel}/>
        <MetricCard label="Next Payment" labelColor="var(--green-600)" value={future?CM.FDshort(future.date):'—'} sub={future?(fmt2(future.amount)+' · '+(future.desc||'')):'no scheduled income'}/>
      </KpiRow>
      <div style={{height:20}}/>
      <ChartCard title="Income Sources" className="cm-mb-24" action={<button className="cm-btn cm-btn-ghost" onClick={()=>setEModal({idx:-1})}><Ib.Plus size={16}/>Add Earning</button>}>
        <Tabs tabs={types} value={tab} onChange={setTab}/>
        <div style={{height:18}}/>
        {(tab==='All Earnings')
          ? (entries.length
              ? <React.Fragment>
                  <div className="t-caption" style={{marginBottom:12}}>Every income entry — company, nature, amount &amp; recurrence · {periodLabel}</div>
                  <DataTable columns={entryCols} rows={entries.slice(0,200)} minWidth={840}/>
                  {entries.length>200 && <div className="t-caption" style={{textAlign:'center',marginTop:12}}>Showing the latest 200 of {entries.length.toLocaleString()} — narrow with the month filter.</div>}
                </React.Fragment>
              : <div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>No income entries in {periodLabel}. Use “Add Earning” to record one.</div>)
          : (rows.length?<DataTable columns={cols} rows={rows} minWidth={760}/>:<div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>No income recorded.</div>)}
      </ChartCard>
      <div className="cm-grid-2a cm-mb-24">
        <ChartCard title="Income Over Time"
          action={<div className="cm-seg">{['6M','1Y','All'].map(t=><button key={t} className={t==='1Y'?'active':''}>{t}</button>)}</div>}>
          <TrendChart data={series} width={760} height={280} color={G}
            yLabels={[fmt(Math.max.apply(null,series)*1000),'',fmt(0)]} xLabels={['','','','','']}/>
        </ChartCard>
        <ChartCard title="By Source">
          <div className="cm-donut-card" style={{justifyContent:'center'}}>
            <DonutChart size={160} thickness={22} center={{value:fmt(incomeP),label:periodLabel}} segments={donutArr.length?donutArr:[{value:1,color:'var(--mint-100)'}]}/>
          </div>
          <div style={{marginTop:16}}><Legend items={donutArr.map(s=>({color:s.color,label:s.label,pct:Math.round(s.value/(incomeP||1)*100)+'%',value:fmt2(s.value)}))}/></div>
        </ChartCard>
      </div>
      {eModal!=null && <EarningModal CM={CM} idx={eModal.idx} onClose={()=>setEModal(null)}/>}
    </div>
  );
}
window.PageIncome = PageIncome;

/* Add income source — wired to the header “+ Add Source” action; supports one-time or recurring */
window.addSourcePrompt = function(){ try{ window.dispatchEvent(new CustomEvent('cm-add-earning')); }catch(e){} };

/* ============================ RECONCILIATION ============================ */
/* Live, two-sided matching:
   LEFT  = bank transactions imported from OFX/CSV/PDF  (CM.S.bankTx — what actually occurred)
   RIGHT = your books: earnings + bills/expenses        (CM.S.earnings + CM.S.bills)
   We predict matches on the keys common to both — direction, amount, date proximity and a
   normalized vendor name — and, when no shared identifier exists, mint a shared `reconKey`
   that links the two records going forward. No fake/seed data. */
function _reconNorm(s){ return String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim(); }
function _reconTokens(s){ return _reconNorm(s).split(' ').filter(t=>t.length>2); }
function _reconNameSim(a,b){ const A=_reconTokens(a); if(!A.length) return 0; const B=new Set(_reconTokens(b)); let h=0; A.forEach(t=>{ if(B.has(t)) h++; }); return h/A.length; }
function _reconVendor(s){ try { return (window.cleanVendorName ? window.cleanVendorName(s) : s) || s || ''; } catch(e){ return s||''; } }
function _reconScore(bk, bn){
  if (bk.dir !== bn.dir) return -1;                                   // income↔credit, bill↔debit
  const amtDiff = Math.abs(bk.amount - bn.amount);
  const tolAmt = Math.max(0.02, bk.amount * 0.04);                    // exact, or within 4%
  if (amtDiff > tolAmt) return -1;
  let dDays = 60; if (bk.ts!=null && bn.ts!=null) dDays = Math.abs(bk.ts-bn.ts)/86400000;
  if (dDays > 45) return -1;                                          // must be in the same window
  const sim = _reconNameSim(bk.vendor, bn.vendor);
  return (amtDiff<0.02?60:42) + Math.max(0,32-dDays*1.1) + sim*26;    // 0–118
}
const _rid = () => 'r'+Math.random().toString(36).slice(2,9);

function PageReconciliation() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const fmt = CM.M;
  const FD = CM.FDshort || CM.FD || (d=>String(d||''));
  const [sel, setSel] = React.useState({ bank:null, book:null });
  const today = CM.today ? CM.today() : new Date();
  const cutoff = new Date(today.getFullYear(),today.getMonth(),today.getDate(),23,59,59,999).getTime();

  /* ---- build both sides from live state ---- */
  const bankList = (CM.S.bankTx||[]).map((r,i)=>{
    const inflow = (+r.cr||0) > 0;
    return { _ref:r, key:'B'+i, side:'bank', date:r.date, ts:CM.toTs(r.date), name:r.desc||'Transaction',
      amount:+(r.cr||r.dr||0), dir:inflow?'in':'out', cat:r.cat||'Other', vendor:_reconVendor(r.desc),
      reconciled:!!r.reconciled, with:r.reconciledWith };
  }).filter(x=> x.ts==null || x.ts<=cutoff);
  const earnList = (CM.S.earnings||[]).map((r,i)=>({ _ref:r, key:'E'+i, side:'book', kind:'earning', icon:<Ib.Dollar size={15}/>,
    date:r.date, ts:CM.toTs(r.date), name:r.desc||'Income', amount:+(r.amount||0), dir:'in', cat:r.cat||'Income',
    vendor:_reconVendor(r.desc), reconciled:!!r.reconciled, with:r.reconciledWith }));
  const billList = (CM.S.bills||[]).map((r,i)=>({ _ref:r, key:'K'+i, side:'book', kind:'bill', icon:<Ib.File size={15}/>,
    date:r.date||r.dueDate, ts:CM.toTs(r.date||r.dueDate), name:r.desc||r.name||'Bill', amount:+(r.amount||0), dir:'out',
    cat:r.cat||'Other', vendor:_reconVendor(r.desc||r.name), reconciled:!!r.reconciled, with:r.reconciledWith }));
  const bookList = earnList.concat(billList).filter(x=> x.ts==null || x.ts<=cutoff);

  const byRid = {};
  [].concat(CM.S.bankTx||[],CM.S.earnings||[],CM.S.bills||[]).forEach(r=>{ if(r&&r._rid) byRid[r._rid]=r; });

  /* ---- matched pairs (bank ⟷ book) ---- */
  const bookByRid = {}; bookList.forEach(b=>{ if(b._ref._rid) bookByRid[b._ref._rid]=b; });
  const pairs = [];
  bankList.forEach(bn=>{ if(bn.reconciled && bn.with && bookByRid[bn.with]) pairs.push({ bank:bn, book:bookByRid[bn.with] }); });
  const unBank = bankList.filter(x=>!x.reconciled);
  const unBook = bookList.filter(x=>!x.reconciled);

  /* ---- best suggestion for each open row ---- */
  const sugg = {};               // row.key -> { partner, score }
  unBook.forEach(bk=>{ let best=null; unBank.forEach(bn=>{ const s=_reconScore(bk,bn); if(s>46 && (!best||s>best.score)) best={partner:bn,score:s}; }); if(best) sugg[bk.key]=best; });
  unBank.forEach(bn=>{ let best=null; unBook.forEach(bk=>{ const s=_reconScore(bk,bn); if(s>46 && (!best||s>best.score)) best={partner:bk,score:s}; }); if(best) sugg[bn.key]=best; });

  /* ---- actions (these are user gestures → safe to mutate state) ---- */
  function link(bankRow, bookRow){
    CM.mutate(function(){
      const a=bankRow._ref, b=bookRow._ref, k='rk'+Math.random().toString(36).slice(2,9);
      if(!a._rid)a._rid=_rid(); if(!b._rid)b._rid=_rid();
      a.reconciled=true; a.reconciledWith=b._rid; a.reconKey=k; a.reconciledAt=new Date().toISOString();
      b.reconciled=true; b.reconciledWith=a._rid; b.reconKey=k; b.reconciledAt=new Date().toISOString();
    });
    setSel({bank:null,book:null});
  }
  function unlink(row){
    CM.mutate(function(){
      const a=row._ref, pid=a.reconciledWith, partner=pid?byRid[pid]:null;
      [a,partner].forEach(r=>{ if(!r)return; delete r.reconciled; delete r.reconciledWith; delete r.reconKey; delete r.reconciledAt; });
    });
  }
  function autoMatch(){
    const cand=[]; unBook.forEach(bk=>unBank.forEach(bn=>{ const s=_reconScore(bk,bn); if(s>56) cand.push({bk,bn,s}); }));
    cand.sort((x,y)=>y.s-x.s);
    const usedBank={}, usedBook={};
    CM.mutate(function(){
      cand.forEach(p=>{
        if(usedBank[p.bn.key]||usedBook[p.bk.key]) return;
        usedBank[p.bn.key]=usedBook[p.bk.key]=true;
        const a=p.bn._ref, b=p.bk._ref, k='rk'+Math.random().toString(36).slice(2,9);
        if(!a._rid)a._rid=_rid(); if(!b._rid)b._rid=_rid();
        a.reconciled=true; a.reconciledWith=b._rid; a.reconKey=k; a.reconciledAt=new Date().toISOString();
        b.reconciled=true; b.reconciledWith=a._rid; b.reconKey=k; b.reconciledAt=new Date().toISOString();
      });
    });
  }
  function unmatchAll(){ CM.mutate(function(){ [].concat(CM.S.bankTx||[],CM.S.earnings||[],CM.S.bills||[]).forEach(r=>{ if(r&&r.reconciled){ delete r.reconciled; delete r.reconciledWith; delete r.reconKey; delete r.reconciledAt; } }); }); setSel({bank:null,book:null}); }

  const amtEl = (row)=> <span className="num" style={{fontWeight:700,color:row.dir==='in'?G:'var(--ink-900)'}}>{(row.dir==='in'?'+':'-')+fmt(row.amount).slice(1)}</span>;
  const total = bankList.length + bookList.length;
  const matchPct = total ? Math.round(pairs.length*2/total*100) : 0;
  const hasData = total>0;

  /* ---- a single row chip ---- */
  function Row({row, picked, onClick, suggestion, onConfirm}){
    return (
      <div onClick={onClick} style={{display:'flex',alignItems:'center',gap:12,padding:'11px 12px',borderRadius:'var(--r-ctrl)',cursor:onClick?'pointer':'default',
        border:'1px solid '+(picked?G:'var(--line)'), background:picked?'var(--mint-50)':'#fff', marginBottom:8, transition:'.12s'}}>
        <span className="cm-act-ic" style={{width:34,height:34,flex:'0 0 34px',background:row.dir==='in'?tb(G):'var(--mint-50)',color:row.dir==='in'?G:'var(--ink-500)'}}>
          {row.side==='bank' ? (row.dir==='in'?<Ib.ArrowDown size={15}/>:<Ib.ArrowUp size={15}/>) : (row.icon||<Ib.File size={15}/>)}
        </span>
        <div style={{flex:1,minWidth:0}}>
          <div className="cm-act-title" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{row.name}</div>
          <div className="cm-act-sub">{row.cat} · {FD(row.date)}{row.side==='book'?(' · '+(row.kind==='earning'?'Income':'Bill')):''}</div>
          {suggestion && <div onClick={(e)=>{e.stopPropagation();onConfirm(suggestion.partner);}}
            style={{marginTop:6,display:'inline-flex',alignItems:'center',gap:5,cursor:'pointer',fontSize:11,fontWeight:800,color:'#1F7A63',
              background:'var(--mint-100)',borderRadius:8,padding:'3px 9px'}}>
            <Ib.Bolt size={12}/>Match “{suggestion.partner.name.slice(0,22)}” · {Math.min(99,Math.round(suggestion.score))}%
          </div>}
        </div>
        {amtEl(row)}
      </div>
    );
  }

  return (
    <div>
      <KpiRow>
        <MetricCard label="Bank Transactions" value={String(bankList.length)} sub="imported from statements"/>
        <MetricCard label="Book Entries" value={String(bookList.length)} sub={earnList.length+' earnings · '+billList.length+' bills'}/>
        <MetricCard label="Matched" labelColor="var(--green-600)" value={String(pairs.length)} sub={matchPct+'% reconciled'}/>
        <div className="cm-card cm-card-pad">
          <div className="m-label" style={{fontWeight:700,fontSize:13,marginBottom:10,color:(unBank.length||unBook.length)?'#B5740F':'var(--green-600)'}}>Status</div>
          <div style={{display:'flex',alignItems:'center',gap:12}}>
            <span className="cm-tile-ic" style={{width:44,height:44,flex:'0 0 44px',background:(unBank.length||unBook.length)?'#FEF1DD':'#fff',color:(unBank.length||unBook.length)?AM:G}}>
              {(unBank.length||unBook.length)?<Ib.Clock size={22}/>:<Ib.Check size={22}/>}
            </span>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontWeight:800,fontSize:18,lineHeight:1.1,color:(unBank.length||unBook.length)?'var(--ink-900)':G}}>{hasData?((unBank.length||unBook.length)?'In Progress':'Reconciled'):'No data'}</div>
              <div className="t-caption" style={{marginTop:2}}>{hasData?(unBank.length+' bank · '+unBook.length+' book open'):'import a statement to begin'}</div>
            </div>
          </div>
        </div>
      </KpiRow>

      {!hasData ? (
        <ChartCard title="Reconciliation">
          <div style={{textAlign:'center',padding:'52px 0'}}>
            <div style={{color:G,marginBottom:12,display:'flex',justifyContent:'center'}}><Ib.Check size={40}/></div>
            <div className="t-h3" style={{fontSize:17}}>Nothing to reconcile yet</div>
            <div className="t-caption" style={{marginTop:6,maxWidth:440,marginLeft:'auto',marginRight:'auto',lineHeight:1.55}}>
              Import a bank statement (OFX, CSV or PDF) on the Transactions page, and add your earnings &amp; bills.
              Clear Mint will line the two up side by side and predict the matches for you.
            </div>
            <a href="#/transactions" className="cm-btn cm-btn-primary" style={{marginTop:18,textDecoration:'none',display:'inline-flex'}}><Ib.Plus size={16}/>Import transactions</a>
          </div>
        </ChartCard>
      ) : (
      <ChartCard title="Match bank statement to your books"
        action={
          <div style={{display:'flex',gap:10,flexWrap:'wrap'}}>
            {pairs.length>0 && <button className="cm-btn cm-btn-soft" onClick={unmatchAll}>Reset</button>}
            <button className="cm-btn cm-btn-primary" onClick={autoMatch}><Ib.Bolt size={16}/>Auto-match</button>
          </div>
        }>
        <div style={{display:'flex',alignItems:'center',gap:10,padding:'12px 16px',marginBottom:18,borderRadius:'var(--r-ctrl)',
          background:(unBank.length||unBook.length)?'#FEF1DD':'var(--mint-100)',color:(unBank.length||unBook.length)?'#B5740F':'var(--green-600)',fontWeight:700,fontSize:13.5}}>
          {(unBank.length||unBook.length)?<Ib.Info size={18}/>:<Ib.Check size={18}/>}
          {(unBank.length||unBook.length)
            ? <span>Pick one bank line and one book line to match them, or hit <b>Auto-match</b> to link every confident pair by amount, date &amp; vendor.</span>
            : <span>Every transaction is reconciled against your books. 🎉</span>}
        </div>

        {/* matched pairs, side by side */}
        {pairs.length>0 && <div style={{marginBottom:22}}>
          <div className="st-eyebrow" style={{marginBottom:10}}>Matched · {pairs.length}</div>
          {pairs.map((p,i)=>(
            <div key={i} style={{display:'grid',gridTemplateColumns:'1fr auto 1fr auto',alignItems:'center',gap:12,
              padding:'10px 12px',borderRadius:'var(--r-ctrl)',background:'var(--mint-50)',border:'1px solid var(--mint-100)',marginBottom:8}}>
              <div style={{display:'flex',alignItems:'center',gap:10,minWidth:0}}>
                <span className="cm-act-ic" style={{width:32,height:32,flex:'0 0 32px',background:p.bank.dir==='in'?tb(G):'#fff',color:p.bank.dir==='in'?G:'var(--ink-500)'}}>{p.bank.dir==='in'?<Ib.ArrowDown size={15}/>:<Ib.ArrowUp size={15}/>}</span>
                <div style={{minWidth:0}}><div className="cm-act-title" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{p.bank.name}</div><div className="cm-act-sub">Bank · {FD(p.bank.date)} · {amtEl(p.bank)}</div></div>
              </div>
              <span style={{color:G,display:'flex',alignItems:'center'}}><Ib.Check size={18}/></span>
              <div style={{display:'flex',alignItems:'center',gap:10,minWidth:0}}>
                <span className="cm-act-ic" style={{width:32,height:32,flex:'0 0 32px',background:p.book.dir==='in'?tb(G):'var(--mint-50)',color:p.book.dir==='in'?G:'var(--ink-500)'}}>{p.book.icon}</span>
                <div style={{minWidth:0}}><div className="cm-act-title" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{p.book.name}</div><div className="cm-act-sub">{p.book.kind==='earning'?'Income':'Bill'} · {FD(p.book.date)} · {amtEl(p.book)}</div></div>
              </div>
              <button className="cm-btn cm-btn-ghost" style={{padding:'6px 10px',fontSize:12}} onClick={()=>unlink(p.bank)}>Unmatch</button>
            </div>
          ))}
        </div>}

        {/* open items, two columns */}
        <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:20}}>
          <div>
            <div className="st-eyebrow" style={{marginBottom:10,display:'flex',justifyContent:'space-between'}}><span>Bank statement · {unBank.length} open</span></div>
            {unBank.length ? unBank.map(row=>(
              <Row key={row.key} row={row} picked={sel.bank===row.key} suggestion={sugg[row.key]}
                onClick={()=>setSel(s=>({...s, bank:s.bank===row.key?null:row.key}))}
                onConfirm={(partner)=>link(row, partner)}/>
            )) : <div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>All bank lines matched.</div>}
          </div>
          <div>
            <div className="st-eyebrow" style={{marginBottom:10}}>Your books · {unBook.length} open</div>
            {unBook.length ? unBook.map(row=>(
              <Row key={row.key} row={row} picked={sel.book===row.key} suggestion={sugg[row.key]}
                onClick={()=>setSel(s=>({...s, book:s.book===row.key?null:row.key}))}
                onConfirm={(partner)=>link(partner, row)}/>
            )) : <div className="t-caption" style={{textAlign:'center',padding:'24px 0'}}>All book entries matched.</div>}
          </div>
        </div>

        {(sel.bank && sel.book) && (()=>{ const bn=unBank.find(x=>x.key===sel.bank), bk=unBook.find(x=>x.key===sel.book);
          if(!bn||!bk) return null;
          const sc=_reconScore(bk,bn); const ok=sc>0;
          return (
            <div style={{position:'sticky',bottom:0,marginTop:18,display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,flexWrap:'wrap',
              padding:'13px 16px',borderRadius:'var(--r-ctrl)',background:'var(--ink-900)',color:'#fff'}}>
              <div style={{fontSize:13,fontWeight:600}}>
                Match <b>{bn.name}</b> ({amtEl(bn)}) with <b>{bk.name}</b> ({amtEl(bk)})
                {' '}· {ok?<span style={{color:'#8FE0B0'}}>{Math.min(99,Math.round(sc))}% confidence</span>:<span style={{color:'#F4B6B6'}}>amounts/direction differ</span>}
              </div>
              <div style={{display:'flex',gap:8}}>
                <button className="cm-btn cm-btn-soft" onClick={()=>setSel({bank:null,book:null})}>Cancel</button>
                <button className="cm-btn cm-btn-primary" onClick={()=>link(bn,bk)}><Ib.Check size={16}/>Match</button>
              </div>
            </div>
          ); })()}
      </ChartCard>
      )}
    </div>
  );
}
window.PageReconciliation = PageReconciliation;

/* ============================ CREDIT CARDS ============================ */
const CARD_GRADS = ['linear-gradient(135deg,#11422F,#0A2E22)','linear-gradient(135deg,#1B5E4A,#11422F)','linear-gradient(135deg,#3A2E12,#1F1808)','linear-gradient(135deg,#0D3B2E,#11422F)'];
/* ── issuer / network detection ──────────────────────────────────────────
   Imported statements often land with a junk issuer ("Credit Card", a product
   name, or a merchant). cmCardBrand() normalizes issuer + network from the best
   available text and flags anything it can't confidently resolve as Needs Review
   so the user can fix it in the card editor. No values are invented. */
var CM_ISSUERS=[
  [/\bmbna\b/,'MBNA','mbna.ca','Mastercard'],
  [/\btd\b|td canada|toronto.?dominion/,'TD','td.com',''],
  [/\brbc\b|royal bank/,'RBC','rbc.com',''],
  [/\bcibc\b|simplii/,'CIBC','cibc.com',''],
  [/scotia/,'Scotiabank','scotiabank.com',''],
  [/\bbmo\b|bank of montreal/,'BMO','bmo.com',''],
  [/national bank|banque nationale|\bnbc\b/,'National Bank','nbc.ca',''],
  [/capital one/,'Capital One','capitalone.ca','Mastercard'],
  [/canadian tire|triangle|\bctfs\b/,'Canadian Tire Bank','canadiantire.ca','Mastercard'],
  [/pc financial|president.?s choice|\bpcf\b/,'PC Financial','pcfinancial.ca','Mastercard'],
  [/tangerine/,'Tangerine','tangerine.ca','Mastercard'],
  [/rogers/,'Rogers Bank','rogersbank.com','Mastercard'],
  [/walmart/,'Walmart Rewards','walmart.ca','Mastercard'],
  [/home ?trust/,'Home Trust','hometrust.ca','Visa'],
  [/\bneo\b/,'Neo Financial','neofinancial.com','Mastercard'],
  [/\bbrim\b/,'Brim Financial','brimfinancial.com','Mastercard'],
  [/desjardins/,'Desjardins','desjardins.com',''],
  [/amex|american express/,'American Express','americanexpress.com','American Express']
];
/* explicit user-confirmed corrections, keyed by last 4 (from the audit) */
var CM_CARD_CORRECTIONS={ '5161':{issuer:'MBNA',domain:'mbna.ca',network:'Mastercard'} };
function cmCardBrand(card){
  card=card||{};
  var hay=((card.issuer||'')+' '+(card.name||'')+' '+(card.bank||'')).toLowerCase();
  var network=card.network||'';
  if(!network){ if(/master|\bmc\b/.test(hay))network='Mastercard'; else if(/amex|american express/.test(hay))network='American Express'; else if(/discover/.test(hay))network='Discover'; else if(/visa/.test(hay))network='Visa'; }
  var found=null;
  for(var i=0;i<CM_ISSUERS.length;i++){ if(CM_ISSUERS[i][0].test(hay)){ found=CM_ISSUERS[i]; break; } }
  var corr=card.last4&&CM_CARD_CORRECTIONS[card.last4];
  var issuerName, domain;
  if(corr){ issuerName=corr.issuer; domain=corr.domain; if(corr.network) network=network||corr.network; }
  else if(found){ issuerName=found[1]; domain=found[2]; if(!network&&found[3]) network=found[3]; }
  else { var raw=(card.issuer||'').replace(/[‡†*]/g,'').trim(); var ph=/^(credit card|card|unknown|n\/?a|)$/i.test(raw); issuerName=ph?'':raw; domain=null; }
  var detected=!!(corr||found||issuerName);
  return { issuerName:issuerName||'Card', domain:domain||null, network:network||'', detected:detected, needsReview:(!detected || !network) };
}
window.cmCardBrand=cmCardBrand;
var CM_NET_DOMAIN={'Visa':'visa.com','Mastercard':'mastercard.com','American Express':'americanexpress.com','Amex':'americanexpress.com','Discover':'discover.com'};
function CardField({ label, children, full }){
  return <div style={full?{gridColumn:'1 / -1'}:undefined}><div style={{fontSize:11,fontWeight:700,letterSpacing:'.06em',textTransform:'uppercase',color:'var(--ink-400)',margin:'0 0 5px'}}>{label}</div>{children}</div>;
}
function IssuerLogo({ brand, size }){
  size=size||34; const [bad,setBad]=React.useState(false);
  const dom=brand&&brand.domain;
  if(dom && !bad && window.primaryLogoUrl){
    return <img src={window.primaryLogoUrl(dom,size)} alt={brand.issuerName} onError={()=>setBad(true)}
      style={{width:size,height:size,borderRadius:8,objectFit:'contain',background:'#fff',border:'1px solid #E2E8F0',padding:2,flexShrink:0,boxSizing:'border-box'}}/>;
  }
  if(window.BrandIcon) return React.createElement(window.BrandIcon,{name:(brand&&brand.issuerName)||'Card',size:size,radius:8});
  return null;
}
function NetworkLogo({ network }){
  const [bad,setBad]=React.useState(false);
  const dom=CM_NET_DOMAIN[network];
  if(dom && !bad && window.primaryLogoUrl){
    return <span style={{flex:'0 0 auto',background:'#fff',borderRadius:6,padding:'4px 7px',display:'inline-flex',alignItems:'center'}}>
      <img src={window.primaryLogoUrl(dom,44)} alt={network} onError={()=>setBad(true)} style={{height:18,width:'auto',maxWidth:46,objectFit:'contain',display:'block'}}/></span>;
  }
  return <span style={{fontWeight:900,fontSize:12,letterSpacing:'.06em',color:'rgba(255,255,255,.92)'}}>{network?network.toUpperCase().slice(0,4):'CARD'}</span>;
}
/* ---- edit-card modal: every card field in one popup ---- */
function CardModal({ CM, idx, onClose }){
  const adding = idx==null || idx<0;
  const c = adding ? {} : ((CM.S.cards||[])[idx]||{});
  const dval = v => (typeof v==='string'&&/^\d{4}-\d{2}-\d{2}/.test(v))?v.slice(0,10):'';
  const num = v => v!=null&&v!==''?String(v):'';
  const [f,setF]=React.useState(()=>({
    name:c.name||'', issuer:c.issuer||'', network:c.network||'', last4:c.last4||'', cardholder:c.cardholder||'',
    limit:num(c.limit), balance:num(c.balance), available:num(c.available), statementBalance:num(c.statementBalance),
    minPayment:num(c.minPayment), apr:num(c.apr), dueDate:dval(c.dueDate), statementDate:dval(c.statementDate),
    rewards:c.rewards||'', paymentAccount:c.paymentAccount||'', autopay:!!c.autopay, status:c.status||'Open', notes:c.notes||''
  }));
  const [availTouched,setAvailTouched]=React.useState(c.available!=null);
  React.useEffect(()=>{ if(availTouched) return; const l=parseFloat(f.limit); if(isNaN(l)){ return; } const b=parseFloat(f.balance)||0; setF(s=>({...s, available:String(Math.max(0,Math.round((l-b)*100)/100))})); }, [f.limit,f.balance]);
  const set=k=>e=>{ const v=e.target.type==='checkbox'?e.target.checked:e.target.value; if(k==='available') setAvailTouched(true); setF(s=>({...s,[k]:v})); };
  const inp={width:'100%',fontFamily:'inherit',fontSize:13.5,color:'var(--ink-900)',border:'1px solid var(--line)',borderRadius:10,padding:'9px 11px',background:'#fff',outline:'none',boxSizing:'border-box'};
  const lab={fontSize:11,fontWeight:700,letterSpacing:'.06em',textTransform:'uppercase',color:'var(--ink-400)',margin:'0 0 5px'};
  const preview = cmCardBrand({issuer:f.issuer, name:f.name, network:f.network, last4:(f.last4||'').replace(/\D/g,'').slice(-4)});
  const ISSUERS=['MBNA','TD','RBC','CIBC','Scotiabank','BMO','National Bank','Capital One','Canadian Tire Bank','PC Financial','Tangerine','Rogers Bank','Walmart Rewards','Home Trust','Neo Financial','Brim Financial','Desjardins','American Express'];
  const save=()=>{
    const last4=(f.last4||'').replace(/\D/g,'').slice(-4);
    const limit=parseFloat(f.limit)||0, balance=parseFloat(f.balance)||0;
    const rec={
      name:f.name||((f.issuer||'Card')+(last4?(' ····'+last4):'')), issuer:f.issuer||'', network:f.network||'', last4:last4, cardholder:f.cardholder||'',
      limit:limit, balance:balance,
      available:f.available===''?Math.max(0,limit-balance):(parseFloat(f.available)||0),
      statementBalance:f.statementBalance===''?null:(parseFloat(f.statementBalance)||0),
      minPayment:f.minPayment===''?null:(parseFloat(f.minPayment)||null), apr:f.apr===''?null:(parseFloat(f.apr)||null),
      dueDate:f.dueDate||null, statementDate:f.statementDate||null, rewards:f.rewards||'', paymentAccount:f.paymentAccount||'',
      autopay:!!f.autopay, status:f.status||'Open', notes:f.notes||''
    };
    const b=cmCardBrand(rec); if(!rec.network&&b.network) rec.network=b.network; rec.needsReview=b.needsReview;
    if(adding && window.CMAccess && !CMAccess.requireWithinLimit('creditCards',(CM.S.cards||[]).length,'More credit cards')){ return; }
    CM.mutate(function(){ if(adding){ (CM.S.cards=CM.S.cards||[]).push(rec); } else { Object.assign(CM.S.cards[idx], rec); } });
    try{ CM.logActivity&&CM.logActivity(adding?'added a credit card':'updated a credit card', rec.name, {cat:'cards'}); }catch(e){}
    onClose();
  };
  const del=()=>{ if(adding){ onClose(); return; } if(!window.confirm('Delete "'+(c.name||'this card')+'"? This removes the card from Clear Mint.')) return;
    CM.mutate(function(){ CM.S.cards.splice(idx,1); }); onClose(); };
  const Field = CardField;
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:1000,background:'rgba(13,33,26,.45)',display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
      <div onClick={e=>e.stopPropagation()} className="cm-card" style={{width:620,maxWidth:'100%',maxHeight:'92vh',overflowY:'auto',borderRadius:18,padding:'22px 24px',boxShadow:'0 24px 64px rgba(13,42,33,.28)'}}>
        <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:4}}>
          <div className="t-h2" style={{fontSize:18}}>{adding?'Add Credit Card':'Edit Card'}</div>
          <button onClick={onClose} aria-label="Close" style={{border:0,background:'none',cursor:'pointer',color:'var(--ink-400)',fontSize:20,lineHeight:1,padding:4}}>×</button>
        </div>
        <div className="t-caption" style={{marginBottom:16}}>{adding?'Enter the card details — fields left blank are saved as “Needs Review”.':('Update any detail of '+(c.name||'this card')+' — changes apply everywhere in Clear Mint.')}</div>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:16,padding:'10px 12px',background:'var(--mint-50)',border:'1px solid var(--line)',borderRadius:12}}>
          <IssuerLogo brand={preview} size={34}/>
          <div style={{flex:1,minWidth:0}}><div style={{fontWeight:700,fontSize:14,color:'var(--ink-900)'}}>{preview.issuerName}{f.last4?(' ····'+f.last4.replace(/\D/g,'').slice(-4)):''}</div><div className="t-caption">{preview.network||'Network not set'}</div></div>
          {preview.needsReview ? <span style={{background:'#FBE7CF',color:'#B5740F',fontWeight:700,fontSize:11,padding:'3px 9px',borderRadius:20}}>Needs Review</span> : <span style={{background:'var(--mint-100)',color:'var(--green-600)',fontWeight:700,fontSize:11,padding:'3px 9px',borderRadius:20}}>Detected</span>}
        </div>
        <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:14}}>
          <Field label="Card Nickname" full={true}><input style={inp} value={f.name} onChange={set('name')} placeholder="e.g. TD Cash Back Visa"/></Field>
          <Field label="Issuer / Bank"><input style={inp} list="cm-issuer-list" value={f.issuer} onChange={set('issuer')} placeholder="e.g. MBNA"/>
            <datalist id="cm-issuer-list">{ISSUERS.map(n=><option key={n} value={n}/>)}</datalist></Field>
          <Field label="Card Network"><select style={inp} value={f.network} onChange={set('network')}>
            <option value="">— Select —</option>{['Visa','Mastercard','American Express','Discover','Other'].map(n=><option key={n} value={n}>{n}</option>)}</select></Field>
          <Field label="Last 4 Digits"><input style={inp} value={f.last4} onChange={set('last4')} maxLength={4} placeholder="1234"/></Field>
          <Field label="Cardholder Name"><input style={inp} value={f.cardholder} onChange={set('cardholder')} placeholder="Name on card"/></Field>
          <Field label="Credit Limit ($)"><input style={inp} type="number" value={f.limit} onChange={set('limit')}/></Field>
          <Field label="Current Balance ($)"><input style={inp} type="number" value={f.balance} onChange={set('balance')}/></Field>
          <Field label="Available Credit ($)"><input style={inp} type="number" value={f.available} onChange={set('available')} placeholder="auto = limit − balance"/></Field>
          <Field label="Statement Balance ($)"><input style={inp} type="number" value={f.statementBalance} onChange={set('statementBalance')}/></Field>
          <Field label="Minimum Payment ($)"><input style={inp} type="number" value={f.minPayment} onChange={set('minPayment')}/></Field>
          <Field label="Purchase APR (%)"><input style={inp} type="number" value={f.apr} onChange={set('apr')} placeholder="e.g. 20.99"/></Field>
          <Field label="Payment Due Date"><input style={inp} type="date" value={f.dueDate} onChange={set('dueDate')}/></Field>
          <Field label="Statement Closing Date"><input style={inp} type="date" value={f.statementDate} onChange={set('statementDate')}/></Field>
          <Field label="Rewards Type"><input style={inp} value={f.rewards} onChange={set('rewards')} placeholder="e.g. Cash Back, Aeroplan"/></Field>
          <Field label="Payment Account"><input style={inp} value={f.paymentAccount} onChange={set('paymentAccount')} placeholder="e.g. TD Chequing"/></Field>
          <Field label="Statement Status"><select style={inp} value={f.status} onChange={set('status')}>{['Open','Paid','Past Due','Closed','Needs Review'].map(n=><option key={n} value={n}>{n}</option>)}</select></Field>
          <Field label="Auto-pay Enabled"><label style={{display:'flex',alignItems:'center',gap:8,fontSize:13.5,color:'var(--ink-700)',padding:'9px 0'}}><input type="checkbox" checked={f.autopay} onChange={set('autopay')} style={{width:16,height:16}}/>Auto-pay is set up for this card</label></Field>
          <Field label="Notes" full={true}><textarea style={{...inp,minHeight:60,resize:'vertical'}} value={f.notes} onChange={set('notes')} placeholder="Optional notes"/></Field>
        </div>
        <div style={{display:'flex',gap:10,marginTop:20,alignItems:'center'}}>
          {!adding && <button className="cm-btn cm-btn-ghost" style={{color:'var(--red-500)'}} onClick={del}>Delete Card</button>}
          <div style={{flex:1}}></div>
          <button className="cm-btn cm-btn-soft" onClick={onClose}>Cancel</button>
          <button className="cm-btn cm-btn-primary" onClick={save}>{adding?'Add Card':'Save Changes'}</button>
        </div>
      </div>
    </div>
  );
}
/* ---- CC sub-tab views ---- */
function CCTransactions({ CM, cards }){
  const BrandIcon=window.BrandIcon, fmt=CM.M;
  const [q,setQ]=React.useState('');
  const all=(CM.S.ccTx||[]).slice().sort((a,b)=>(CM.toTs(b.date)||0)-(CM.toTs(a.date)||0));   // most recent first
  const rows=q?all.filter(r=>((r.desc||'')+(r.card||'')).toLowerCase().includes(q.toLowerCase())):all;
  const cols=[
    {header:'Transaction', cell:r=><div className="cm-cell-lead"><BrandIcon name={r.desc} size={34} radius={8}/><div style={{minWidth:0}}><div className="cm-cell-primary" style={{whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',maxWidth:300}}>{r.desc||'Transaction'}</div><div className="cm-cell-sub">{r.card||(cards[r.cardIdx]&&cards[r.cardIdx].name)||'Card'}</div></div></div>},
    {header:'Date', cell:r=><span className="num" style={{fontSize:13,color:'var(--ink-500)'}}>{CM.FD(r.date)}</span>},
    {header:'Category', cell:r=><StatusPill tone={r.cr>0?'green':'slate'}>{r.cat||'Other'}</StatusPill>},
    {header:'Amount', align:'right', cell:r=>{const isD=(+r.dr||0)>0;return <span className="num" style={{fontWeight:700,color:isD?'var(--ink-900)':G}}>{(isD?'-':'+')+fmt(r.dr||r.cr||0).slice(1)}</span>;}},
  ];
  return (<ChartCard className="cm-mb-24">
    <div className="st-searchbox" style={{maxWidth:300,marginBottom:14}}><Ib.Search size={16}/><input placeholder="Search card transactions…" value={q} onChange={e=>setQ(e.target.value)}/></div>
    {rows.length?<DataTable columns={cols} rows={rows.slice(0,80)} minWidth={680}/>:<div className="t-caption" style={{textAlign:'center',padding:'40px 0'}}>No card transactions yet. Upload a card statement (PDF / OFX / CSV).</div>}
  </ChartCard>);
}
function CCAnalytics({ CM, cards }){
  const fmt=CM.M, PAL=['#10915F','#3E7CC4','#8A6E9E','#C8742E','#0E9F8E','#D8688F'];
  const segs=cards.map((c,i)=>({label:c.name,value:Math.max(+c.balance||0,0.01),color:PAL[i%PAL.length]}));
  const totalBal=cards.reduce((a,c)=>a+(+c.balance||0),0);
  return (<div className="cm-grid-2a cm-mb-24">
    <ChartCard title="Balance by Card">
      <div className="cm-donut-card" style={{justifyContent:'center'}}><DonutChart size={150} thickness={22} center={{value:CM.M0(totalBal),label:'Total'}} segments={segs}/></div>
      <div style={{marginTop:14}}><Legend items={segs.map(s=>({color:s.color,label:s.label,value:fmt(s.value)}))}/></div>
    </ChartCard>
    <ChartCard title="Utilization by Card">
      <div style={{paddingTop:4}}>{cards.map((c,i)=>{const u=c.limit?Math.round((+c.balance||0)/c.limit*100):0;return <CategoryBar key={i} color={u<30?G:AM} label={c.name} value={u+'%'} pct={Math.min(u,100)}/>;})}</div>
    </ChartCard>
  </div>);
}
function PageCreditCards() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const BrandIcon = window.BrandIcon;
  const ImportControls = window.ImportControls;
  const fmt = CM.M0, fmt2 = CM.M;
  const cards = CM.S.cards||[];
  const [sel, setSel] = React.useState(0);
  const [tab, setTab] = React.useState('Overview');
  const [editIdx, setEditIdx] = React.useState(null);
  const [addOpen, setAddOpen] = React.useState(false);
  /* one-time backfill: normalize issuer/network + flag Needs Review on imported cards */
  React.useEffect(()=>{
    var list=CM.S.cards||[], changed=false;
    list.forEach(function(c){ var b=cmCardBrand(c);
      if(!c.network&&b.network) changed=true;
      if(c._issuerName!==b.issuerName) changed=true;
      if((c._issuerDomain||null)!==(b.domain||null)) changed=true;
      if(c.needsReview!==b.needsReview) changed=true;
      if(c.available==null&&c.limit!=null) changed=true;
    });
    if(!changed) return;
    CM.mutate(function(){ (CM.S.cards||[]).forEach(function(c){ var b=cmCardBrand(c);
      if(!c.network&&b.network) c.network=b.network;
      c._issuerName=b.issuerName; c._issuerDomain=b.domain||null; c.needsReview=b.needsReview;
      if(c.available==null&&c.limit!=null) c.available=Math.max(0,(+c.limit||0)-(+c.balance||0));
    }); });
  }, []);
  const hasCards = cards.length>0;
  const idx = hasCards ? Math.min(sel, cards.length-1) : 0;
  const card = hasCards ? cards[idx] : { name:'No card yet', last4:'', balance:0, limit:0, apr:0, network:'', issuer:'' };
  const totalBal = cards.reduce((a,c)=>a+(+c.balance||0),0);
  const totalLimit = cards.reduce((a,c)=>a+(+c.limit||0),0);
  const util = totalLimit?Math.round(totalBal/totalLimit*100):0;
  const cardUtil = card.limit?Math.round(card.balance/card.limit*100):0;
  const minDue = hasCards ? (card.minPayment!=null?+card.minPayment:null) : 0;
  const totalMin = cards.reduce((a,c)=>a+(+c.minPayment||0),0);
  const reviewCount = cards.filter(c=>cmCardBrand(c).needsReview).length;
  const activeCount = cards.filter(c=>(c.status||'Open')!=='Closed').length;
  const dueTimes = cards.map(c=>c.dueDate?CM.toTs(c.dueDate):0).filter(t=>t&&t>=Date.now()-864e5).sort((a,b)=>a-b);
  const net = (window.CC_NETWORKS&&window.CC_NETWORKS[card.network])||{label:card.network||'CARD'};
  const statements = (CM.S.ccStatements||[]).filter(s=>s.card===card.name || (card.last4&&s.last4===card.last4));
  const ccActivity = (CM.S.ccTx||[]).filter(t=>t.cardIdx===idx || t.card===card.name).sort((a,b)=>(CM.toTs(b.date)||0)-(CM.toTs(a.date)||0)).slice(0,6);
  return (
    <div>
      <KpiRow>
        <MetricCard label="Total Credit Limit" value={fmt(totalLimit)} sub={'across '+cards.length+' card'+(cards.length!==1?'s':'')}/>
        <MetricCard label="Total Used" labelColor="var(--red-500)" value={fmt(totalBal)} sub={util+'% of limit'}/>
        <MetricCard label="Total Available" labelColor="var(--green-600)" value={fmt(Math.max(0,totalLimit-totalBal))} sub="limit minus used"/>
        <div className="cm-card cm-card-pad">
          <div className="m-label" style={{color:util<30?'var(--green-600)':'#B5740F',fontWeight:700,fontSize:13,marginBottom:10}}>Utilization</div>
          <div className="cm-gauge-wrap">
            <DonutChart size={72} thickness={10} segments={[{value:util,color:util<30?G:AM},{value:Math.max(0,100-util),color:'var(--mint-100)'}]} center={{value:util+'%'}}/>
            <div><div style={{fontWeight:800,color:util<30?G:'#B5740F'}}>{util<30?'Healthy':'Watch'}</div><div className="t-caption">Target below 30%</div></div>
          </div>
        </div>
      </KpiRow>

      {hasCards && (
      <div style={{display:'grid',gridTemplateColumns:'repeat(4,1fr)',gap:12,margin:'0 0 18px'}}>
        {[['Total Minimum Due',fmt2(totalMin)],['Next Payment Due',dueTimes[0]?CM.FD(new Date(dueTimes[0])):'—'],['Active Cards',String(activeCount)],['Needs Review',String(reviewCount)]].map((k,i)=>{
          const warn=k[0]==='Needs Review'&&reviewCount>0;
          return (<div key={i} className="cm-card" style={{padding:'12px 16px'}}>
            <div style={{fontSize:11.5,fontWeight:600,color:warn?'#B5740F':'var(--ink-400)',marginBottom:4}}>{k[0]}</div>
            <div className="num" style={{fontSize:18,fontWeight:800,color:warn?'#B5740F':'var(--ink-900)'}}>{k[1]}</div>
          </div>);
        })}
      </div>)}

      <div style={{margin:'4px 0 18px'}}><Tabs tabs={['Overview','Transactions','Analytics','Statements']} value={tab} onChange={setTab}/></div>

      {tab==='Transactions' && <CCTransactions CM={CM} cards={cards}/>}
      {tab==='Analytics' && <CCAnalytics CM={CM} cards={cards}/>}
      {tab==='Statements' && (window.PageStatements ? <window.PageStatements/> : null)}

      {tab==='Overview' && (
      <div className="cm-grid-2b">
        <ChartCard title="Your Cards" action={<button className="cm-btn cm-btn-ghost" onClick={()=>setAddOpen(true)}><Ib.Plus size={16}/>Add Card</button>}>
          <div style={{display:'flex',flexDirection:'column',gap:14}}>
            {cards.map((c,i)=>{
              const active = i===idx;
              const b = cmCardBrand(c);
              const avail = c.available!=null?c.available:Math.max(0,(+c.limit||0)-(+c.balance||0));
              return (
                <div key={i} onClick={()=>{ setSel(i); setEditIdx(i); }}
                  style={{borderRadius:16,padding:'18px 20px',cursor:'pointer',background:CARD_GRADS[i%CARD_GRADS.length],color:'#EAF0EA',position:'relative',overflow:'hidden',
                    border:active?'2px solid var(--gold)':'2px solid transparent',boxShadow:'0 10px 26px rgba(13,59,46,.18)'}}>
                  <div style={{position:'absolute',right:-30,top:-24,width:120,height:120,background:'radial-gradient(closest-side,rgba(212,169,93,.18),transparent 70%)'}}/>
                  <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',position:'relative',gap:10}}>
                    <div style={{display:'flex',alignItems:'center',gap:10,minWidth:0}}>
                      <span style={{flex:'0 0 auto'}}><IssuerLogo brand={b} size={34}/></span>
                      <div style={{minWidth:0}}>
                        <div style={{fontSize:12,color:'rgba(234,240,234,.72)',fontWeight:600,display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>{b.issuerName}{b.needsReview&&<span style={{background:'rgba(212,169,93,.24)',color:'#F2DCAC',fontWeight:700,fontSize:10,padding:'1px 6px',borderRadius:20}}>Needs Review</span>}</div>
                        <div style={{fontFamily:'var(--font-serif)',fontSize:18,fontWeight:700,color:'#fff',marginTop:2,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{c.name}</div>
                      </div>
                    </div>
                    <NetworkLogo network={b.network}/>
                  </div>
                  <div className="num" style={{letterSpacing:'.14em',fontSize:14,marginTop:18,color:'rgba(234,240,234,.85)'}}>•••• •••• •••• {c.last4||'••••'}</div>
                  <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-end',marginTop:12,position:'relative',gap:8}}>
                    <div><div style={{fontSize:11,color:'rgba(234,240,234,.6)'}}>Balance</div><div className="num" style={{fontFamily:'var(--font-serif)',fontSize:21,fontWeight:700,color:'#fff'}}>{fmt(c.balance)}</div></div>
                    <div style={{textAlign:'center'}}><div style={{fontSize:11,color:'rgba(234,240,234,.6)'}}>Available</div><div className="num" style={{fontWeight:700,color:'#CFE8D6'}}>{fmt(avail)}</div></div>
                    <div style={{textAlign:'right'}}><div style={{fontSize:11,color:'rgba(234,240,234,.6)'}}>Limit</div><div className="num" style={{fontWeight:700,color:'var(--gold)'}}>{fmt(c.limit)}</div></div>
                  </div>
                  {(c.dueDate||c.minPayment!=null)&&<div style={{display:'flex',justifyContent:'space-between',marginTop:10,fontSize:11,color:'rgba(234,240,234,.72)'}}><span>{c.dueDate?('Due '+CM.FD(new Date(CM.toTs(c.dueDate)))):''}</span><span>{c.minPayment!=null?('Min '+fmt(c.minPayment)):''}</span></div>}
                </div>
              );
            })}
          </div>
        </ChartCard>

        <div className="cm-stack">
          <ChartCard title={`${card.name} · ••${card.last4||''}`} action={<span style={{display:'inline-flex',gap:8,alignItems:'center'}}><button className="cm-btn cm-btn-ghost" onClick={()=>hasCards&&setEditIdx(idx)}>Edit Card</button><ImportControls type="cc" idx={idx}/></span>}>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:14,marginBottom:18}}>
              <div className="cm-ministat"><div className="v num">{fmt2(card.balance)}</div><div className="k">Current Balance</div></div>
              <div className="cm-ministat"><div className="v num">{fmt2(card.available!=null?card.available:Math.max(0,(+card.limit||0)-(+card.balance||0)))}</div><div className="k">Available Credit</div></div>
              <div className="cm-ministat"><div className="v num">{fmt2(card.limit)}</div><div className="k">Credit Limit</div></div>
              <div className="cm-ministat"><div className="v num">{card.apr!=null?card.apr+'%':'—'}</div><div className="k">Purchase APR</div></div>
              <div className="cm-ministat"><div className="v num">{minDue!=null?fmt2(minDue):'—'}</div><div className="k">{minDue!=null?'Minimum Due':'Min. Due · needs review'}</div></div>
              <div className="cm-ministat"><div className="v num" style={{fontSize:15}}>{card.dueDate?CM.FD(new Date(CM.toTs(card.dueDate))):'—'}</div><div className="k">Payment Due</div></div>
            </div>
            <div style={{marginBottom:6,display:'flex',justifyContent:'space-between'}}>
              <span className="t-caption">Card Utilization</span>
              <span className="num" style={{fontWeight:700,color:cardUtil<30?G:AM}}>{cardUtil}%</span>
            </div>
            <ProgressBar pct={cardUtil} showPct={false} color={cardUtil<30?G:AM}/>
            <div style={{display:'flex',gap:10,marginTop:16}}>
              <button className="cm-btn cm-btn-primary" style={{flex:1,justifyContent:'center'}} onClick={()=>{ if(!hasCards) return; var d=window.prompt('Payment amount ($)', String(Math.max(25,Math.round((card.balance||0)*0.02)))); if(d===null) return; var amt=parseFloat(d); if(isNaN(amt)||amt<=0) return; CM.mutate(function(){ var c=CM.S.cards[idx]; if(c) c.balance=Math.max(0,Math.round(((+c.balance||0)-amt)*100)/100); }); try{CM.logActivity&&CM.logActivity('made a credit-card payment', CM.M(amt)+' · '+card.name, {cat:'payment'});}catch(e){} }}>Make a Payment</button>
              <a href="#/statements" className="cm-btn cm-btn-soft" style={{flex:1,justifyContent:'center',textDecoration:'none'}}>View Statements</a>
            </div>
          </ChartCard>
          <ChartCard title="Statements">
            {statements.length ? statements.map((s,i)=>(
              <div className="cm-act" key={i}>
                <div className="cm-act-ic" style={{background:'var(--red-100)',color:RD}}><Ib.File size={16}/></div>
                <div className="cm-act-body"><div className="cm-act-title">{CM.FD(s.date?new Date(s.date):null)}</div><div className="cm-act-sub">Min {fmt2(s.minPayment||0)}</div></div>
                <div className="cm-act-right"><div className="cm-act-amt num">{fmt2(s.balance)}</div>
                  <div>{s.paid?<StatusPill tone="green" icon={<Ib.Check size={12}/>}>Paid</StatusPill>:<StatusPill tone="amber" icon={<Ib.Clock size={12}/>}>Due</StatusPill>}</div></div>
              </div>
            )) : <div className="t-caption" style={{textAlign:'center',padding:'18px 0'}}>No statements yet — use “Import statement” above to upload a PDF.</div>}
          </ChartCard>
          <ChartCard title="Recent Activity">
            {ccActivity.length ? <ActivityList items={ccActivity.map(t=>{ const tb=cmCardBrand(card); return {icon:<BrandIcon name={t.desc} size={36} radius={9}/>,color:t.cr>0?G:RD,title:t.desc,sub:(t.cat?t.cat+' · ':'')+CM.FDshort(t.date)+' · '+tb.issuerName+(card.network?(' '+card.network):'')+(card.last4?(' ••'+card.last4):''),amt:(t.dr>0?'-':'+')+fmt2(t.dr||t.cr||0).slice(1),dir:t.dr>0?'neg':'pos',amtColor:t.dr>0?'var(--ink-900)':undefined};})}/>
              : <div className="t-caption" style={{textAlign:'center',padding:'18px 0'}}>No card transactions imported yet.</div>}
          </ChartCard>
        </div>
      </div>)}
      {editIdx!=null && (CM.S.cards||[])[editIdx] ? <CardModal CM={CM} idx={editIdx} onClose={()=>setEditIdx(null)}/> : null}
      {addOpen ? <CardModal CM={CM} idx={-1} onClose={()=>setAddOpen(false)}/> : null}
    </div>
  );
}
window.PageCreditCards = PageCreditCards;

/* ============================ CC STATEMENTS (fallback) ============================ */
/* The full Statements "command center" lives in cm-pages-statements.jsx and
   overrides this. This real-data fallback never shows sample/placeholder data. */
function PageStatements() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const fmt = CM.M;
  const stmts = (CM.S.ccStatements||[]).slice().sort((a,b)=>(CM.toTs(b.date)||0)-(CM.toTs(a.date)||0));
  const open = stmts.filter(s=>!s.paid);
  const totalBal = open.reduce((a,s)=>a+(+s.balance||0),0);
  const totalMin = open.reduce((a,s)=>a+(+s.minPayment||0),0);
  const cols = [
    {header:'Statement', cell:r=><DataTable.Lead thumb={<span style={{color:G}}><Ib.File size={18}/></span>} primary={r.card||'Card'} sub={r.last4?('•• '+r.last4):''}/>},
    {header:'Closing Date', cell:r=><span className="cm-cell-meta num" style={{fontSize:13,color:'var(--ink-500)'}}>{CM.FD(r.date?new Date(r.date):null)}</span>},
    {header:'Statement Balance', cell:r=><span className="cm-cell-strong num">{fmt(+r.balance||0)}</span>},
    {header:'Minimum Due', cell:r=><span className="cm-cell-meta num" style={{fontSize:13,color:'var(--ink-500)'}}>{fmt(+r.minPayment||0)}</span>},
    {header:'Status', cell:r=> r.paid
      ? <StatusPill tone="green" icon={<Ib.Check size={12}/>}>Paid</StatusPill>
      : <StatusPill tone="amber" icon={<Ib.Clock size={12}/>}>Due</StatusPill>},
  ];
  return (
    <div>
      <KpiRow>
        <MetricCard label="Current Balance" labelColor="var(--red-500)" value={fmt(totalBal)} sub={open.length+' open statement'+(open.length===1?'':'s')}/>
        <MetricCard label="Total Minimum Due" value={fmt(totalMin)} sub="this cycle"/>
        <MetricCard label="Cards" value={String((CM.S.cards||[]).length)} sub="on file"/>
        <MetricCard label="Statements" value={String(stmts.length)} sub="tracked"/>
      </KpiRow>
      <ChartCard title="Statements">
        {stmts.length
          ? <DataTable columns={cols} rows={stmts} minWidth={760}/>
          : <div className="t-caption" style={{textAlign:'center',padding:'36px 0'}}>No statements yet — upload one from the Credit Cards page.</div>}
      </ChartCard>
    </div>
  );
}
window.PageStatements = PageStatements;

/* ============================ DEBTS (payoff planner) ============================ */
function buildDebts(CM){
  const ic = { 'Credit Card':<Ib.Card size={18}/>, Mortgage:<Ib.House size={18}/>, 'Auto Loan':<Ib.Car size={18}/>, 'Student Loan':<Ib.Reports size={18}/>, LOC:<Ib.Card size={18}/>, 'Line of Credit':<Ib.Card size={18}/>, 'Personal Loan':<Ib.User size={18}/> };
  const cl = { 'Credit Card':BL, Mortgage:RD, 'Auto Loan':AM, 'Student Loan':TEAL, LOC:PU, 'Personal Loan':PINK };
  const d = [].concat(
    (CM.S.liabilities||[]).map(l=>({name:l.name, type:l.type||'Loan', bal:+l.balance||0, apr:+l.rate||0, min:+l.payment||0})),
    (CM.S.cards||[]).map(c=>({name:c.name, type:'Credit Card', bal:+c.balance||0, apr:+c.apr||0, min:Math.max(25,Math.round((+c.balance||0)*0.02))}))
  ).filter(x=>x.bal>0);
  d.forEach(x=>{ x.ic=ic[x.type]||<Ib.Bank size={18}/>; x.c=cl[x.type]||SL; });
  return d;
}
// `extra` = your monthly free cash flow directed at debt on top of the minimums. `rollover` rolls a
// cleared debt's payment into the next focus debt. Returns the payoff timeline + this-month plan so the
// page can show what to pay each debt and when each clears. Re-derived from live cash flow → auto-adjusts.
function simulate(debts, strategy, rollover, extra){
  extra = +extra || 0;
  let list = debts.map((d,idx)=>({idx, name:d.name, bal:d.bal, apr:d.apr, min:d.min, payoff:null, firstPay:0}));
  list.sort(strategy==='Snowball' ? (a,b)=>a.bal-b.bal : (a,b)=>b.apr-a.apr);
  const pool = list.reduce((s,d)=>s+d.min,0) + (rollover?extra:0);
  let months=0, interest=0; const curve=[]; const start=list.reduce((s,d)=>s+d.bal,0)||1;
  while(list.some(d=>d.bal>0.5) && months<600){
    let budget=pool; const firstMonth = months===0;
    list.forEach(d=>{ if(d.bal>0){ const i=d.bal*d.apr/100/12; d.bal+=i; interest+=i; } });
    // pay minimums
    list.forEach(d=>{ if(d.bal>0){ const pay=Math.min(d.bal, d.min); d.bal-=pay; budget-=pay; if(firstMonth)d.firstPay+=pay; } });
    if(rollover){ // focus the remaining budget (rolled minimums + extra) on the first unpaid
      for(let k=0;k<list.length && budget>0.5;k++){ if(list[k].bal>0){ const pay=Math.min(list[k].bal,budget); list[k].bal-=pay; budget-=pay; if(firstMonth)list[k].firstPay+=pay; } }
    }
    months++;
    list.forEach(d=>{ if(d.payoff==null && d.bal<=0.5) d.payoff=months; });
    curve.push(list.reduce((s,d)=>s+Math.max(0,d.bal),0));
  }
  // sample curve to ~12 points as % of start
  const pts=[]; const step=Math.max(1,Math.floor(curve.length/11));
  for(let i=0;i<curve.length;i+=step) pts.push(Math.round(curve[i]/start*100));
  pts.push(0);
  const plan = list.map(d=>({ name:d.name, idx:d.idx, thisMonth:Math.round(d.firstPay), payoffMonth:d.payoff||months }));
  return { months, interest, pts, plan };
}
function PageDebts() {
  const CM = window.useStore ? window.useStore() : window.CM;
  const [strategy, setStrategy] = React.useState('Avalanche');
  const [aiBusy, setAiBusy] = React.useState(false);
  const [aiText, setAiText] = React.useState('');
  const fmt = CM.M0;
  const DEBTS = buildDebts(CM);
  const totalDebt = DEBTS.reduce((a,d)=>a+d.bal,0);
  const totalMin = DEBTS.reduce((a,d)=>a+d.min,0);
  const avgApr = totalDebt?(DEBTS.reduce((a,d)=>a+d.apr*d.bal,0)/totalDebt).toFixed(2):'0';
  const ordered = [...DEBTS].sort(strategy==='Snowball'?(a,b)=>a.bal-b.bal:(a,b)=>b.apr-a.apr);
  // Extra payment = your average monthly surplus (income − expenses) from the last 6 months. Because the
  // plan is recomputed from live cash flow on every render, it auto-adjusts as your cash flow changes.
  const cf = CM.last6CashFlow?CM.last6CashFlow():[];
  const surplus = cf.length ? Math.max(0, Math.round(cf.reduce((s,m)=>s+((+m.inc||0)-(+m.exp||0)),0)/cf.length)) : 0;
  const sim = simulate(DEBTS, strategy, true, surplus);
  const baseline = simulate(DEBTS, strategy, false, 0);
  const saved = Math.max(0, baseline.interest - sim.interest);
  const monthsSaved = Math.max(0, baseline.months - sim.months);
  const freeDate = new Date(); freeDate.setMonth(freeDate.getMonth()+sim.months);
  const freeStr = freeDate.toLocaleDateString('en-CA',{month:'short',year:'numeric'});
  const tagline = strategy==='Avalanche'?'Highest interest rate first — saves the most money.':'Smallest balance first — fastest psychological wins.';
  // This-month plan: what to pay each debt now, and when each clears, given your surplus.
  const planMonthDate = (n)=>{ const d=new Date(); d.setMonth(d.getMonth()+Math.max(0,(n||sim.months)-1)); return d.toLocaleDateString('en-CA',{month:'short',year:'numeric'}); };
  const planRows = sim.plan.map((p,i)=>{ const dd=DEBTS.find(x=>x.name===p.name)||{}; return { name:p.name, type:dd.type||'Debt', ic:dd.ic, c:dd.c, thisMonth:p.thisMonth, payoffStr:planMonthDate(p.payoffMonth), _i:i }; });
  const planCols = [
    {header:'Debt', cell:r=><DataTable.Lead thumb={<span style={{color:r.c}}>{r.ic}</span>} primary={r.name} sub={r.type}/>},
    {header:'Pay this month', cell:r=><span className="cm-cell-strong num">{fmt(r.thisMonth)}</span>},
    {header:'Paid off by', cell:r=><span className="cm-cell-meta num" style={{color:'var(--ink-500)'}}>{r.payoffStr}</span>},
    {header:'', align:'right', cell:r=> r._i===0 && surplus>0
      ? <StatusPill tone="amber" icon={<Ib.Bolt size={12}/>}>Extra {fmt(surplus)} goes here</StatusPill>
      : <StatusPill tone="slate">Minimum</StatusPill>},
  ];
  const rows = ordered.map((d,i)=>({...d,_i:i}));
  const cols = [
    {header:'#', width:'48px', cell:r=><span className="num" style={{display:'inline-flex',width:26,height:26,borderRadius:8,background:r._i===0?'var(--gold)':'var(--mint-100)',color:r._i===0?'#3A2E12':'var(--green-600)',alignItems:'center',justifyContent:'center',fontWeight:800,fontSize:13}}>{r._i+1}</span>},
    {header:'Debt', cell:r=><DataTable.Lead thumb={<span style={{color:r.c}}>{r.ic}</span>} primary={r.name} sub={r.type}/>},
    {header:'Balance', cell:r=><span className="cm-cell-strong num">{fmt(r.bal)}</span>},
    {header:'APR', cell:r=><DataTable.Stack strong={r.apr+'%'} meta={r.type==='Credit Card'?'Variable':'Fixed'}/>},
    {header:'Min Payment', cell:r=><span className="cm-cell-strong num">{fmt(r.min)}</span>},
    {header:'Priority', cell:r=> r._i===0
      ? <StatusPill tone="amber" icon={<Ib.Bolt size={12}/>}>Focus next</StatusPill>
      : <StatusPill tone="slate">In queue</StatusPill>},
  ];
  // Ask Mint (AI router) for a payoff plan, constrained to the same avalanche/snowball options + numbers.
  const askMint = ()=>{
    if(!DEBTS.length){ setAiText('You have no debts on file — nothing to plan. 🎉'); return; }
    setAiBusy(true); setAiText('');
    const list = DEBTS.map(d=>`${d.name} (${d.type}): ${fmt(d.bal)} balance at ${d.apr}% APR, minimum ${fmt(d.min)}/mo`).join('; ');
    const system = `You are Mint, a Canadian debt-payoff coach. The app already supports two methods: Avalanche (highest APR first) and Snowball (smallest balance first). `
      + `Given the user's debts and the monthly surplus they can add on top of minimums, recommend a concrete plan in 3–5 short sentences: which ONE debt to attack first with the extra, the order after that, and the rough payoff timeline. `
      + `Use ONLY the numbers provided — never invent balances, rates or dates. Plain language, encouraging, no markdown.`;
    const q = `My debts: ${list}. Extra I can pay monthly on top of minimums: ${fmt(surplus)}. Preferred method: ${strategy}. What's my best payoff plan?`;
    fetch((window.CM_API_BASE||'/api')+'/ai/route',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskClass:'financial',question:q,system:system})})
      .then(r=>r.json()).then(d=>setAiText(d&&d.ok&&d.text ? d.text : 'Mint is unavailable right now — the calculated plan above still applies.'))
      .catch(()=>setAiText('Mint is unavailable right now — the calculated plan above still applies.'))
      .finally(()=>setAiBusy(false));
  };
  const byType={}; DEBTS.forEach(d=>{ byType[d.type]=(byType[d.type]||0)+d.bal; });
  const pal={'Student Loan':TEAL,'Auto Loan':AM,'Personal Loan':PINK,'Credit Card':BL,Mortgage:RD,LOC:PU};
  const typeArr=Object.keys(byType).map(t=>({label:t,value:byType[t],color:pal[t]||SL})).sort((a,b)=>b.value-a.value);
  return (
    <div>
      <KpiRow>
        <MetricCard label="Total Debt" labelColor="var(--red-500)" value={fmt(totalDebt)}
          art={<span style={{color:'var(--red-100)'}}><Ib.Bolt size={42}/></span>}/>
        <MetricCard label="Monthly Payment" value={fmt(totalMin+surplus)} sub={surplus>0?(fmt(totalMin)+' min + '+fmt(surplus)+' from cash flow'):('minimum across '+DEBTS.length+' debts')}/>
        <MetricCard label="Avg Interest Rate" value={avgApr+'%'} sub="weighted by balance"/>
        <MetricCard label="Debt-Free Date" labelColor="var(--green-600)" value={freeStr} sub={surplus>0&&monthsSaved>0?(monthsSaved+' mo sooner with '+strategy):('with '+strategy)}/>
      </KpiRow>

      <ChartCard className="cm-mb-24" title="Payoff Strategy"
        action={<div className="cm-seg">{['Avalanche','Snowball'].map(t=><button key={t} className={t===strategy?'active':''} onClick={()=>setStrategy(t)}>{t}</button>)}</div>}>
        <div style={{display:'flex',alignItems:'center',gap:10,padding:'12px 16px',marginBottom:18,borderRadius:'var(--r-ctrl)',background:'var(--mint-50)',color:'var(--green-600)',fontWeight:700,fontSize:13.5}}>
          <Ib.Spark size={18}/>{tagline}
        </div>
        <div className="cm-grid-3" style={{marginBottom:20}}>
          <div className="cm-ministat"><div className="v num" style={{color:G}}>{fmt(saved)}</div><div className="k">Interest saved vs no-rollover</div></div>
          <div className="cm-ministat"><div className="v num">{freeStr}</div><div className="k">Projected debt-free ({sim.months} mo)</div></div>
          <div className="cm-ministat"><div className="v num">{ordered[0]?ordered[0].name:'—'}</div><div className="k">Focus this debt next</div></div>
        </div>
        {rows.length?<DataTable columns={cols} rows={rows} minWidth={820}/>:<div className="t-caption" style={{textAlign:'center',padding:'30px 0'}}>No debts — you're debt-free! 🎉</div>}
      </ChartCard>

      {DEBTS.length>0 && <ChartCard className="cm-mb-24" title="Your Monthly Payment Plan"
        action={<button className="cm-btn cm-btn-soft" disabled={aiBusy} onClick={askMint} style={{display:'inline-flex',alignItems:'center',gap:7}}><Ib.Spark size={15}/>{aiBusy?'Asking Mint…':'Ask Mint for a plan'}</button>}>
        <div style={{display:'flex',alignItems:'center',gap:10,padding:'12px 16px',marginBottom:18,borderRadius:'var(--r-ctrl)',background:'var(--mint-50)',color:'var(--green-600)',fontWeight:700,fontSize:13.5}}>
          <Ib.Spark size={18}/>
          {surplus>0
            ? `Pay every minimum, then put your ${fmt(surplus)}/mo surplus on the focus debt. Clears all debt by ${freeStr}${monthsSaved>0?` — ${monthsSaved} months sooner`:''}.`
            : `Your last 6 months show no surplus to accelerate debt. This plan pays the minimums; freeing up cash flow shortens it automatically.`}
        </div>
        <DataTable columns={planCols} rows={planRows} minWidth={760}/>
        {aiText && <div style={{marginTop:16,padding:'14px 16px',borderRadius:'var(--r-ctrl)',border:'1px solid var(--line)',background:'#fff'}}>
          <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:6,fontWeight:800,fontSize:12.5,color:'var(--green-600)'}}><Ib.Spark size={15}/>Mint's recommendation</div>
          <div style={{fontSize:13.5,lineHeight:1.55,color:'var(--ink-800)',whiteSpace:'pre-wrap'}}>{aiText}</div>
        </div>}
      </ChartCard>}

      <div className="cm-grid-2a">
        <ChartCard title="Projected Balance Over Time">
          <TrendChart data={sim.pts.length>1?sim.pts:[100,0]} width={760} height={260} color={G}
            yLabels={[fmt(totalDebt),'',fmt(0)]} xLabels={['','','','','']}/>
        </ChartCard>
        <ChartCard title="Debt by Type">
          <div className="cm-donut-card" style={{justifyContent:'center'}}>
            <DonutChart size={150} thickness={22} center={{value:fmt(totalDebt),label:'Total Debt'}} segments={typeArr}/>
          </div>
          <div style={{marginTop:16}}><Legend items={typeArr.map(t=>({color:t.color,label:t.label,pct:Math.round(t.value/(totalDebt||1)*100)+'%',value:fmt(t.value)}))}/></div>
        </ChartCard>
      </div>
    </div>
  );
}
window.PageDebts = PageDebts;
