// MOD-04 rate_engine — UI module
// Version: v2.5.11
// Updated: 2026-05-20 12:00 PT
// Part of: Wipomo / CCE Solar Tools
//
// Stage 2 browser UI for MOD-04 rate_engine.
// Requires: rate_engine_v2.5.3.js loaded before this file (provides window.MOD04)
//
// Accepted file formats:
//   Load (Green Button):
//     • SDG&E CSV / MOD-02b synthetic CSV  — DATE, START TIME, END TIME, USAGE (kWh), NOTES
//     • UtilityAPI CSV                     — interval_start, interval_kwh columns
//   PV production (optional):
//     • PVWatts v8 CSV / MOD-09 export     — Month, Day, Hour, AC System Output (W)

const { useState, useCallback, useRef, useMemo } = React;
const { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
        ResponsiveContainer } = Recharts;

// ─── COLORS ───────────────────────────────────────────────────────────────────
const C = {
  bg:      '#0d1117',
  surface: '#161b22',
  card:    '#1c2128',
  border:  '#30363d',
  text:    '#e6edf3',
  muted:   '#8b949e',
  faint:   '#484f58',
  blue:    '#58a6ff',
  green:   '#3fb950',
  orange:  '#d29922',
  red:     '#f85149',
  purple:  '#bc8cff',
  accent:  '#58a6ff',
};

const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

const VERSION = "2.5.11";

// recommendedNem may be a string or array (array = compatible with multiple NEM types)
const nemMatch = (rate, nemType) =>
  Array.isArray(rate.recommendedNem)
    ? rate.recommendedNem.includes(nemType)
    : rate.recommendedNem === nemType;

// ─── BATTERY LIBRARY ──────────────────────────────────────────────────────────
const BATTERY_LIBRARY = {
  "1x Enphase 10C":  { label: "1x Enphase 10C",  kwh:  10.0, kw:  7.68, fixedCost:  19600 },
  "2x Enphase 10C":  { label: "2x Enphase 10C",  kwh:  20.0, kw: 14.16, fixedCost:  28610 },
  "3x Enphase 10C":  { label: "3x Enphase 10C",  kwh:  30.0, kw: 21.24, fixedCost:  37621 },
  "4x Enphase 10C":  { label: "4x Enphase 10C",  kwh:  40.0, kw: 28.32, fixedCost:  46632 },
  "5x Enphase 10C":  { label: "5x Enphase 10C",  kwh:  50.0, kw: 35.40, fixedCost:  55643 },
  "6x Enphase 10C":  { label: "6x Enphase 10C",  kwh:  60.0, kw: 42.48, fixedCost:  64654 },
  "1x Powerwall 3":  { label: "1x Powerwall 3",  kwh:  13.5, kw:  11.5, fixedCost:  19627 },
  "2x Powerwall 3":  { label: "2x Powerwall 3",  kwh:  27.0, kw:  23.0, fixedCost:  39254 },
  "3x Powerwall 3":  { label: "3x Powerwall 3",  kwh:  40.5, kw:  34.5, fixedCost:  58881 },
  "4x Powerwall 3":  { label: "4x Powerwall 3",  kwh:  54.0, kw:  46.0, fixedCost:  78508 },
  "5x Powerwall 3":  { label: "5x Powerwall 3",  kwh:  67.5, kw:  57.5, fixedCost:  98135 },
  "6x Powerwall 3":  { label: "6x Powerwall 3",  kwh:  81.0, kw:  69.0, fixedCost: 117762 },
};

// Solar self-consumption dispatch: charge battery from solar surplus, discharge to serve load.
// Returns { intervals: [[ts, netKwh], ...], chargedKwh, dischargedKwh }
// netIntervals is the output of computeNetIntervals — negative = solar surplus, positive = grid import.
function dispatchBattery(netIntervals, batKwh, batKw) {
  const batMin = batKwh * 0.10;
  const batMax = batKwh;
  let batE = batKwh * 0.50; // start at 50% SoC
  let chargedKwh = 0, dischargedKwh = 0;
  // Determine interval duration in hours
  const dtH = netIntervals.length > 1
    ? Math.min(1.0, (netIntervals[1][0] - netIntervals[0][0]) / 3_600_000)
    : 0.25;
  const intervals = netIntervals.map(([ts, net]) => {
    if (net < 0) {
      // Solar surplus: charge battery
      const canCharge = Math.min(batKw * dtH, batMax - batE, -net);
      batE += canCharge;
      chargedKwh += canCharge;
      return [ts, net + canCharge]; // remaining surplus (export or curtail)
    } else {
      // Load demand: discharge battery
      const canDischarge = Math.min(batKw * dtH, batE - batMin, net);
      batE -= canDischarge;
      dischargedKwh += canDischarge;
      return [ts, net - canDischarge]; // remaining demand from grid
    }
  });
  return { intervals, chargedKwh, dischargedKwh };
}

// ─── FILE PARSERS ─────────────────────────────────────────────────────────────
// Normalise different Green Button CSV variants into [[Date, kWh], ...].

// All parsers return { intervals: [[Date, kWh], ...], address: string|null }
function parseGreenButtonCsv(text) {
  const sample = text.slice(0, 3000);
  if (/interval_start/i.test(sample))              return parseUtilityApiCsv(text);
  if (/Meter Number.*Date.*Start Time/i.test(sample)) return parseSdgeMyEnergyCsv(text);
  return parseSdgeMod02bCsv(text);
}

function _extractAddressFromHeader(lines, stopIdx) {
  for (let i = 0; i < stopIdx; i++) {
    const m = lines[i].match(/^Service [Aa]ddress[^,]*,\s*"?([^"\r\n]+)"?\s*$/i);
    if (m) return m[1].trim().replace(/^"|"$/g, '');
  }
  return null;
}

function parseSdgeMyEnergyCsv(text) {
  const lines = text.split('\n');
  let headerIdx = -1;
  for (let i = 0; i < Math.min(lines.length, 30); i++) {
    if (/Meter Number.*Date.*Start Time.*Consumption/i.test(lines[i])) {
      headerIdx = i; break;
    }
  }
  if (headerIdx < 0) throw new Error(
    'SDG&E My Energy CSV: cannot find data header row.\n' +
    'Expected a row like "Meter Number,Date,Start Time,Duration,Consumption,Generation,Net".'
  );

  const header = lines[headerIdx].split(',').map(h => h.replace(/"/g,'').trim().toLowerCase());
  const dateIdx = header.findIndex(h => h === 'date');
  const timeIdx = header.findIndex(h => h === 'start time');
  let kwhIdx = header.findIndex(h => h === 'net');
  if (kwhIdx < 0) kwhIdx = header.findIndex(h => h === 'consumption');
  if (dateIdx < 0 || timeIdx < 0 || kwhIdx < 0) throw new Error(
    'SDG&E My Energy CSV: missing expected columns.\n' +
    `Found: ${header.join(', ')}`
  );

  const intervals = [];
  for (let i = headerIdx + 1; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line) continue;
    const cols = line.split(',').map(c => c.trim().replace(/^"|"$/g, ''));
    if (cols.length <= kwhIdx) continue;
    const dateStr = cols[dateIdx];
    const timeStr = cols[timeIdx];
    const kwh     = parseFloat(cols[kwhIdx]);
    if (!dateStr || !timeStr || isNaN(kwh)) continue;
    const dateParts = dateStr.split('/');
    if (dateParts.length !== 3) continue;
    const mm = dateParts[0].padStart(2, '0');
    const dd = dateParts[1].padStart(2, '0');
    const yyyy = dateParts[2];
    const timeParts = timeStr.match(/^(\d+):(\d+)\s*(AM|PM)$/i);
    if (!timeParts) continue;
    let hr  = parseInt(timeParts[1]);
    const mn  = timeParts[2];
    const ampm = timeParts[3].toUpperCase();
    if (ampm === 'AM' && hr === 12) hr = 0;
    if (ampm === 'PM' && hr !== 12) hr += 12;
    const isoStr = `${yyyy}-${mm}-${dd}T${String(hr).padStart(2,'0')}:${mn}:00`;
    const ts = new Date(isoStr);
    if (!isNaN(ts.getTime())) intervals.push([ts, kwh]);
  }
  if (intervals.length < 96) throw new Error(
    `SDG&E My Energy CSV: only ${intervals.length} valid rows found. Check file format.`
  );
  return { intervals: intervals.sort((a, b) => a[0] - b[0]),
           address:   _extractAddressFromHeader(lines, headerIdx) };
}

function parseSdgeMod02bCsv(text) {
  const lines = text.split('\n');
  let headerIdx = -1;
  for (let i = 0; i < Math.min(lines.length, 50); i++) {
    if (/DATE.*START TIME.*USAGE/i.test(lines[i])) { headerIdx = i; break; }
  }
  if (headerIdx < 0) throw new Error(
    'Green Button CSV: unrecognised format.\n\n' +
    'Accepted formats:\n' +
    '  • SDG&E My Energy Center download (columns: Meter Number, Date, Start Time, …)\n' +
    '  • MOD-02b Green Button Emulator CSV (columns: DATE, START TIME, END TIME, USAGE)\n' +
    '  • UtilityAPI CSV (columns: interval_start, interval_kwh)\n\n' +
    'First lines of your file:\n' + lines.slice(0, 5).join('\n')
  );
  const intervals = [];
  for (let i = headerIdx + 1; i < lines.length; i++) {
    const line = lines[i].trim().replace(/^﻿/, '');
    if (!line) continue;
    const cols = line.split(',');
    if (cols.length < 4) continue;
    const dateStr  = cols[0].trim().replace(/"/g, '');
    const startStr = cols[1].trim().replace(/"/g, '');
    const kwh      = parseFloat(cols[3]);
    if (!dateStr || !startStr || isNaN(kwh)) continue;
    const isoStr   = `${dateStr}T${startStr.length === 5 ? startStr + ':00' : startStr}`;
    const ts       = new Date(isoStr);
    if (!isNaN(ts.getTime())) intervals.push([ts, kwh]);
  }
  if (intervals.length < 96) throw new Error(
    `MOD-02b CSV: only ${intervals.length} valid rows found. File may be truncated.`
  );
  return { intervals: intervals.sort((a, b) => a[0] - b[0]),
           address:   _extractAddressFromHeader(lines, headerIdx) };
}

function parseCsvLine(line) {
  const cols = [];
  let cur = '';
  let inQuote = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (ch === '"') {
      inQuote = !inQuote;
    } else if (ch === ',' && !inQuote) {
      cols.push(cur.trim());
      cur = '';
    } else {
      cur += ch;
    }
  }
  cols.push(cur.trim());
  return cols;
}

function parseUtilityApiCsv(text) {
  const lines = text.trim().split('\n');
  const header = lines[0].split(',').map(h => h.replace(/"/g, '').trim().toLowerCase());
  const sIdx  = header.findIndex(h => /interval_start/i.test(h));
  const netIdx = header.findIndex(h => /^net_kwh$/i.test(h));
  const kIdx  = netIdx >= 0 ? netIdx : header.findIndex(h => /interval_kwh/i.test(h));
  if (sIdx < 0 || kIdx < 0) throw new Error(
    'UtilityAPI CSV: missing required columns.\n' +
    `Found: ${header.join(', ')}\n` +
    'Expected "interval_start" and either "net_kWh" or "interval_kWh".'
  );
  const intervals = [];
  for (let i = 1; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line) continue;
    const cols = parseCsvLine(line);
    if (!cols[sIdx]) continue;
    const raw = cols[sIdx].replace(/"/g, '').trim();
    let ts;
    const mdy = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})/);
    if (mdy) {
      ts = new Date(`${mdy[3]}-${mdy[1].padStart(2,'0')}-${mdy[2].padStart(2,'0')}T${mdy[4].padStart(2,'0')}:${mdy[5]}:00`);
    } else {
      ts = new Date(raw.replace(' ', 'T'));
    }
    const kwh = parseFloat(cols[kIdx]);
    if (!isNaN(ts.getTime()) && !isNaN(kwh)) intervals.push([ts, kwh]);
  }
  if (intervals.length < 24) throw new Error(
    `UtilityAPI CSV: only ${intervals.length} valid rows parsed. Check file format and date column.`
  );
  return { intervals: intervals.sort((a, b) => a[0] - b[0]), address: null };
}

function parsePvwattsCsv(text) {
  const lines = text.trim().split('\n');
  let systemKwDc = null;
  for (let i = 0; i < Math.min(lines.length, 30); i++) {
    const m = lines[i].match(/System DC capacity[,\t]+"?([0-9.]+)/i) ||
              lines[i].match(/system_capacity[,\t]+"?([0-9.]+)/i) ||
              lines[i].match(/DC Nameplate Capacity.*?([0-9.]+)/i) ||
              lines[i].match(/DC System Size[^,]*[,\t]+"?([0-9.]+)/i);
    if (m) { systemKwDc = parseFloat(m[1]); break; }
  }
  let headerIdx = -1;
  for (let i = 0; i < Math.min(lines.length, 50); i++) {
    if (/Month.*Day.*Hour.*AC System Output/i.test(lines[i])) { headerIdx = i; break; }
  }
  if (headerIdx < 0) throw new Error(
    'PVWatts CSV: cannot find data header row.\n' +
    'Expected a row containing "Month,Day,Hour,AC System Output (W)".'
  );
  const hourly = [];
  for (let i = headerIdx + 1; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line || line.startsWith('#')) continue;
    const cols = line.split(',').map(c => c.replace(/"/g, '').trim());
    const acW  = parseFloat(cols[cols.length - 1]);
    if (!isNaN(acW)) hourly.push(acW / 1000);
  }
  if (hourly.length < 8760) throw new Error(
    `PVWatts CSV: expected 8760 hourly rows, found ${hourly.length}.\n` +
    'Verify the file is a full-year hourly download, not a monthly summary.'
  );
  return { hourly: hourly.slice(0, 8760), systemKwDc };
}

// ─── LOAD METADATA HELPERS ────────────────────────────────────────────────────
function clipToTwelveMonths(intervals) {
  if (!intervals || intervals.length === 0) return { intervals, clipped: false };
  const ymSet = new Set();
  for (let i = 0; i < intervals.length; i++) {
    const ts = intervals[i][0];
    ymSet.add(ts.getFullYear() * 100 + ts.getMonth());
  }
  const ymSorted = Array.from(ymSet).sort((a, b) => a - b);
  if (ymSorted.length <= 12) return { intervals, clipped: false };
  const keepSet = new Set(ymSorted.slice(-12));
  const clippedIntervals = intervals.filter(([ts]) =>
    keepSet.has(ts.getFullYear() * 100 + ts.getMonth())
  );
  const dropped = ymSorted.slice(0, ymSorted.length - 12);
  const dropFirst = new Date(Math.floor(dropped[0] / 100), dropped[0] % 100, 1);
  const dropLast  = new Date(Math.floor(dropped[dropped.length-1] / 100), dropped[dropped.length-1] % 100, 1);
  const fmt = d => d.toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
  const droppedRange = dropped.length === 1
    ? fmt(dropFirst)
    : `${fmt(dropFirst)} – ${fmt(dropLast)}`;
  return { intervals: clippedIntervals, clipped: true, droppedRange, droppedMonths: dropped.length };
}

function getLoadMeta(intervals) {
  if (!intervals || intervals.length === 0) return null;
  const first    = intervals[0][0];
  const last     = intervals[intervals.length - 1][0];
  let totalImportKwh = 0, totalExportKwh = 0, peakKwh = 0;
  for (let i = 0; i < intervals.length; i++) {
    const k = intervals[i][1];
    if (k > 0) { totalImportKwh += k; if (k > peakKwh) peakKwh = k; }
    else if (k < 0) totalExportKwh += -k;
  }
  const year       = first.getFullYear();
  const yearOffset = Math.max(0, year - 2026);
  return {
    dateRange:      `${first.toLocaleDateString()} – ${last.toLocaleDateString()}`,
    totalKwh:       totalImportKwh,
    exportKwh:      totalExportKwh,
    peakKw:         peakKwh * 4,
    count:          intervals.length,
    year,
    yearOffset,
  };
}

// ─── CHART DATA HELPERS ───────────────────────────────────────────────────────
function makeBillOnlyChartData(monthly) {
  return monthly.map(m => ({
    month:          m.label,
    'Energy charge': +m.energyCharge.toFixed(2),
    'Customer charge': +m.customerCharge.toFixed(2),
  }));
}

function makeSavingsChartData(baseline, solar) {
  return baseline.monthly.map((bm, i) => ({
    month:    bm.label,
    Baseline: +bm.netBill.toFixed(2),
    Solar:    +solar.monthly[i].netBill.toFixed(2),
    Savings:  +(bm.netBill - solar.monthly[i].netBill).toFixed(2),
  }));
}

// ─── SUB-COMPONENTS ───────────────────────────────────────────────────────────

function Tile({ label, value, sub, color }) {
  return (
    <div style={{ background: C.card, border: `1px solid ${C.border}`, borderRadius: 8,
                  padding: '14px 18px', flex: '1 1 150px', minWidth: 140 }}>
      <div style={{ fontSize: 11, color: C.muted, textTransform: 'uppercase',
                    letterSpacing: '0.06em', marginBottom: 6 }}>{label}</div>
      <div style={{ fontSize: 22, fontWeight: 700, color: color || C.text }}>{value}</div>
      {sub && <div style={{ fontSize: 11, color: C.faint, marginTop: 4 }}>{sub}</div>}
    </div>
  );
}

function UploadCard({ title, accept, onFile, meta, hint, optional }) {
  const inputRef = useRef(null);
  const [dragging, setDragging] = useState(false);

  const handleDrop = useCallback(e => {
    e.preventDefault(); setDragging(false);
    const f = e.dataTransfer.files[0];
    if (f) onFile(f);
  }, [onFile]);

  return (
    <div style={{ background: C.surface, border: `1px solid ${dragging ? C.blue : C.border}`,
                  borderRadius: 8, padding: 16, flex: '1 1 280px', minWidth: 260 }}>
      <div style={{ fontSize: 13, fontWeight: 600, color: C.text, marginBottom: 4 }}>
        {title}
        {optional && <span style={{ fontSize: 11, color: C.faint, fontWeight: 400,
                                    marginLeft: 8 }}>optional</span>}
      </div>
      {hint && <div style={{ fontSize: 11, color: C.muted, marginBottom: 10 }}>{hint}</div>}
      <div
        style={{ border: `1px dashed ${dragging ? C.blue : C.faint}`, borderRadius: 6,
                 padding: '20px 12px', textAlign: 'center', cursor: 'pointer',
                 transition: 'border-color 0.15s', background: dragging ? '#1a2233' : 'transparent' }}
        onClick={() => inputRef.current?.click()}
        onDragOver={e => { e.preventDefault(); setDragging(true); }}
        onDragLeave={() => setDragging(false)}
        onDrop={handleDrop}
      >
        <div style={{ fontSize: 22, marginBottom: 4 }}>📂</div>
        <div style={{ fontSize: 12, color: C.muted }}>Click or drag file here</div>
        <div style={{ fontSize: 11, color: C.faint, marginTop: 2 }}>{accept}</div>
      </div>
      <input ref={inputRef} type="file" accept=".csv,.xml" style={{ display: 'none' }}
             onChange={e => e.target.files[0] && onFile(e.target.files[0])} />

      {meta && (
        <div style={{ marginTop: 12, padding: '10px 12px', background: C.card,
                      borderRadius: 6, border: `1px solid ${C.border}`, fontSize: 12 }}>
          <div style={{ color: C.green, fontWeight: 600, marginBottom: 4, wordBreak: 'break-all', overflowWrap: 'anywhere' }}>✓ {meta.filename}</div>
          {meta.dateRange && (
            <div style={{ color: C.muted }}>{meta.dateRange}</div>
          )}
          {meta.totalKwh != null && (
            <div style={{ color: C.muted }}>
              {(+meta.totalKwh).toLocaleString(undefined, {maximumFractionDigits:0})} kWh import
              {meta.count && ` · ${meta.count.toLocaleString()} intervals`}
            </div>
          )}
          {meta.exportKwh != null && (
            <div style={{ color: meta.exportKwh > 0 ? C.green : C.muted }}>
              {meta.exportKwh > 0
                ? `⚡ ${Math.round(meta.exportKwh).toLocaleString()} kWh export detected — set NEM type`
                : 'No export data in file (consumption-only source)'}
            </div>
          )}
          {meta.clipped && (
            <div style={{ color: '#f59e0b', marginTop: 2 }}>
              ⚠ File spans {12 + meta.droppedMonths} months — {meta.droppedRange} dropped, billing uses most recent 12
            </div>
          )}
          {meta.peakKw != null && (
            <div style={{ color: C.muted }}>Peak demand: {(+meta.peakKw).toFixed(2)} kW</div>
          )}
          {meta.systemKwDc != null && (
            <div style={{ color: C.muted }}>System DC: {meta.systemKwDc} kW</div>
          )}
          {meta.annualKwh != null && (
            <div style={{ color: C.muted }}>
              Annual generation: {(+meta.annualKwh).toLocaleString(undefined,{maximumFractionDigits:0})} kWh
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ─── EXPORT HELPERS ───────────────────────────────────────────────────────────
function downloadFile(content, filename, mimeType) {
  const blob = new Blob([content], { type: mimeType || 'text/plain' });
  const url  = URL.createObjectURL(blob);
  const a    = document.createElement('a');
  a.href = url; a.download = filename; a.click();
  URL.revokeObjectURL(url);
}

function niceMax(val) {
  if (val <= 0) return 100;
  const mag = Math.pow(10, Math.floor(Math.log10(val)));
  for (const m of [1, 1.5, 2, 2.5, 5, 10]) { if (mag * m >= val * 1.0) return mag * m; }
  return mag * 10;
}

function buildSummaryCSV(results) {
  const isBill = results.mode === 'bill';
  const dateStr = new Date().toISOString().slice(0, 10);
  const rows = [
    '# MOD-04 rate_engine -- ' + (isBill ? 'Bill Analysis' : 'Savings Analysis') + ' Summary',
    '# Tool version,v' + VERSION,
    '# Generated,' + dateStr,
    '# Rate schedule,' + (results.rateKey  || ''),
    '# Customer type,' + (results.customerType || ''),
  ];
  rows.push('# NEM type,' + (results.nemType || 'none'));
  if (results.loadFilename) rows.push('# Load file,' + results.loadFilename);
  if (results.pvFilename)   rows.push('# PV file,'   + results.pvFilename);
  if (results.address)      rows.push('# Address,' + results.address);
  if (results.lon != null)  rows.push('# Coordinates,' + results.lat?.toFixed(6) + ',' + results.lon?.toFixed(6));
  if (results.tariffZone)   rows.push('# SDG&E tariff zone,' + results.tariffZone);
  if (results.allElectric)  rows.push('# All-electric,true');
  if (results.battery)     rows.push('# Battery,' + results.battery.label + ' — ' + results.battery.kwh + ' kWh / ' + results.battery.kw + ' kW');
  if (results.noExport)    rows.push('# Zero-export,true');
  rows.push('');

  if (isBill) {
    const { bill } = results;
    const hasDemand = bill.breakdown.demandCharge > 0;
    const hasBaseline = (bill.breakdown.baselineCredit || 0) > 0;
    rows.push('--- KEY RESULTS ---');
    rows.push('Annual Bill ($),' + Math.round(bill.totalBill));
    rows.push('Annual Usage (kWh),' + Math.round(bill.annualImportKwh));
    rows.push('Effective Energy Rate ($/kWh),' + bill.effectiveRate.toFixed(4));
    rows.push('Effective Net Rate ($/kWh),'    + bill.effectiveNetRate.toFixed(4));
    rows.push('Annual Customer Charges ($),' + Math.round(bill.breakdown.customerCharge));
    if (hasBaseline)
      rows.push('Annual Baseline Credit ($),' + (bill.breakdown.baselineCredit || 0).toFixed(2));
    if (hasDemand) {
      rows.push('Annual Demand Charges ($),' + Math.round(bill.breakdown.demandCharge));
      rows.push('Annual Peak Demand (kW),' +
        Math.max(...bill.monthly.map(m => m.maxDemandKw)).toFixed(1));
    }
    rows.push('');
    const hasExp = bill.annualExportKwh > 0;
    rows.push('--- MONTHLY DETAIL ---');
    let hdrCols = hasDemand
      ? ['Month','Import kWh','Export kWh','Peak kWh','Off-Peak kWh','SOP kWh','Peak kW','Demand $','Energy $','Customer $']
      : (hasExp
          ? ['Month','Import kWh','Export kWh','Energy $','Customer $']
          : ['Month','Import kWh','Peak kWh','Off-Peak kWh','SOP kWh','Energy $','Customer $']);
    if (hasBaseline) hdrCols.push('Baseline Credit $');
    hdrCols.push('Total $');
    rows.push(hdrCols.join(','));

    let tI=0,tX=0,tPk=0,tOff=0,tSOP=0,tD=0,tE=0,tC=0,tT=0,tBac=0;
    bill.monthly.forEach(m => {
      tI+=m.importKwh; tX+=m.exportKwh; tPk+=m.peakImportKwh; tOff+=m.offpeakImportKwh;
      tSOP+=m.sopImportKwh; tD+=m.demandCharge; tE+=m.energyCharge;
      tC+=m.customerCharge; tT+=m.netBill;
      tBac += (m.baselineCredit || 0);
      let r;
      if (hasDemand) {
        r = [m.label, m.importKwh.toFixed(0), m.exportKwh.toFixed(0),
             m.peakImportKwh.toFixed(0), m.offpeakImportKwh.toFixed(0), m.sopImportKwh.toFixed(0),
             m.maxDemandKw.toFixed(1), m.demandCharge.toFixed(2),
             m.energyCharge.toFixed(2), m.customerCharge.toFixed(2)];
      } else if (hasExp) {
        r = [m.label, m.importKwh.toFixed(0), m.exportKwh.toFixed(0),
             m.energyCharge.toFixed(2), m.customerCharge.toFixed(2)];
      } else {
        r = [m.label, m.importKwh.toFixed(0), m.peakImportKwh.toFixed(0),
             m.offpeakImportKwh.toFixed(0), m.sopImportKwh.toFixed(0),
             m.energyCharge.toFixed(2), m.customerCharge.toFixed(2)];
      }
      if (hasBaseline) r.push((m.baselineCredit || 0).toFixed(2));
      r.push(m.netBill.toFixed(2));
      rows.push(r.join(','));
    });
    let tr = hasDemand
      ? ['Total', tI.toFixed(0), tX.toFixed(0), tPk.toFixed(0), tOff.toFixed(0), tSOP.toFixed(0),
         '—', tD.toFixed(2), tE.toFixed(2), tC.toFixed(2)]
      : (hasExp
          ? ['Total', tI.toFixed(0), tX.toFixed(0), tE.toFixed(2), tC.toFixed(2)]
          : ['Total', tI.toFixed(0), tPk.toFixed(0), tOff.toFixed(0), tSOP.toFixed(0),
             tE.toFixed(2), tC.toFixed(2)]);
    if (hasBaseline) tr.push(tBac.toFixed(2));
    tr.push(tT.toFixed(2));
    rows.push(tr.join(','));

  } else {
    const { baseline, solar, annualSavings, annualGenKwh, selfConsumption } = results;
    rows.push('--- KEY RESULTS ---');
    rows.push('Baseline Bill ($/yr),' + Math.round(baseline.totalBill));
    rows.push('Solar Bill ($/yr),'    + Math.round(solar.totalBill));
    rows.push('Annual Savings ($/yr),' + Math.round(annualSavings));
    rows.push('Annual Generation (kWh),' + Math.round(annualGenKwh));
    rows.push('Self-Consumption (%),' + (selfConsumption * 100).toFixed(1));
    if (results.battery) {
      rows.push('Battery,' + results.battery.label);
      rows.push('Battery Throughput (kWh/yr),' + Math.round(results.battery.dischargedKwh));
      rows.push('Battery Cycles/day,' + (results.battery.dischargedKwh / results.battery.kwh / 365).toFixed(2));
    }
    rows.push('Export Credit ($/yr),' + Math.round(solar.breakdown.exportCredit));
    if (baseline.breakdown.demandCharge > 0)
      rows.push('Demand Savings ($/yr),' + Math.round(
        baseline.breakdown.demandCharge - solar.breakdown.demandCharge));
    rows.push('');
    rows.push('--- MONTHLY DETAIL ---');
    rows.push('Month,Import kWh,Export kWh,Export Credit $,Baseline $,Solar $,Savings $');
    let tI=0,tX=0,tCr=0,tB=0,tS=0,tSv=0;
    baseline.monthly.forEach((bm, i) => {
      const sm = solar.monthly[i];
      const sv = bm.netBill - sm.netBill;
      tI+=sm.importKwh; tX+=sm.exportKwh; tCr+=sm.exportCredit;
      tB+=bm.netBill; tS+=sm.netBill; tSv+=sv;
      rows.push([bm.label, sm.importKwh.toFixed(0), sm.exportKwh.toFixed(0),
                 sm.exportCredit.toFixed(2), bm.netBill.toFixed(2),
                 sm.netBill.toFixed(2), sv.toFixed(2)].join(','));
    });
    rows.push(['Total', tI.toFixed(0), tX.toFixed(0), tCr.toFixed(2),
               tB.toFixed(2), tS.toFixed(2), tSv.toFixed(2)].join(','));
  }
  return rows.join('\n');
}

function drawPng(results) {
  const isBill = results.mode === 'bill';
  const rateKey = results.rateKey || '';
  const dateStr = new Date().toLocaleDateString('en-US',{year:'numeric',month:'short',day:'numeric'});

  let tiles = [];
  if (isBill) {
    const { bill } = results;
    const hasDemand = bill.breakdown.demandCharge > 0;
    const annualPeakKw = hasDemand ? Math.max(...bill.monthly.map(m => m.maxDemandKw)) : null;
    tiles = [
      { label: 'Annual Bill',      value: '$' + Math.round(bill.totalBill).toLocaleString(),            color: '#d29922' },
      { label: 'Annual Usage',     value: Math.round(bill.annualImportKwh).toLocaleString() + ' kWh',   color: null },
      { label: 'Energy Rate',      value: '$' + bill.effectiveRate.toFixed(4) + '/kWh',                  color: null, sub: 'energy ÷ import kWh' },
      { label: 'Effective Net',    value: '$' + bill.effectiveNetRate.toFixed(4) + '/kWh',               color: null, sub: 'total bill ÷ import kWh' },
      { label: 'Customer Charges', value: '$' + Math.round(bill.breakdown.customerCharge).toLocaleString(), color: null, sub: '12 mo × fixed charge' },
    ];
    if (hasDemand) {
      tiles.push({ label: 'Demand Charges',    value: '$' + Math.round(bill.breakdown.demandCharge).toLocaleString() + '/yr', color: '#f85149', sub: 'monthly peak kW' });
      if (annualPeakKw != null) tiles.push({ label: 'Annual Peak Demand', value: annualPeakKw.toFixed(1) + ' kW', color: null, sub: 'max 15-min demand' });
    }
  } else {
    const { baseline, solar, annualSavings, annualGenKwh, selfConsumption } = results;
    const hasDemand = baseline.breakdown.demandCharge > 0;
    tiles = [
      { label: 'Baseline Bill',     value: '$' + Math.round(baseline.totalBill).toLocaleString() + '/yr',  color: '#d29922' },
      { label: 'Solar Bill',        value: '$' + Math.round(solar.totalBill).toLocaleString() + '/yr',     color: '#58a6ff' },
      { label: 'Annual Savings',    value: '$' + Math.round(annualSavings).toLocaleString() + '/yr',       color: '#3fb950' },
      { label: 'Annual Generation', value: Math.round(annualGenKwh).toLocaleString() + ' kWh',             color: null },
      { label: 'Self-Consumption',  value: (selfConsumption * 100).toFixed(1) + '%',                       color: null, sub: 'gen consumed on-site' },
      { label: 'Export Credit',     value: '$' + Math.round(solar.breakdown.exportCredit).toLocaleString() + '/yr', color: null },
    ];
    if (hasDemand) {
      const demandSavings = baseline.breakdown.demandCharge - solar.breakdown.demandCharge;
      tiles.push({ label: 'Demand Savings', value: '$' + Math.round(demandSavings).toLocaleString() + '/yr', color: '#bc8cff', sub: 'peak kW reduction' });
    }
  }

  const W       = 900;
  const MARGIN  = 24;
  const TITLE_H = 58;
  const TILE_H  = 76;
  const TILE_GAP = 8;
  const nTiles  = tiles.length;
  const TILE_W  = Math.floor((W - MARGIN * 2 - TILE_GAP * (nTiles - 1)) / nTiles);
  const TILE_TOP = TITLE_H;
  const CHART_TOP = TILE_TOP + TILE_H + 18;
  const CHART_H_AREA = 280;
  const LEGEND_H = 44;
  const FOOTER_H = 22;
  const H = CHART_TOP + CHART_H_AREA + LEGEND_H + FOOTER_H;

  const canvas = document.createElement('canvas');
  canvas.width = W; canvas.height = H;
  const ctx = canvas.getContext('2d');

  ctx.fillStyle = '#0d1117'; ctx.fillRect(0, 0, W, H);

  ctx.fillStyle = '#58a6ff'; ctx.font = 'bold 12px Inter,system-ui,sans-serif';
  ctx.textAlign = 'left'; ctx.fillText('MOD-04 RATE ENGINE', MARGIN, 26);
  ctx.fillStyle = '#8b949e'; ctx.font = '11px Inter,system-ui,sans-serif';
  ctx.fillText(rateKey + (results.isCare ? ' (CARE)' : '') + '  ·  ' +
    (isBill ? 'Bill Analysis' : 'Savings Analysis') + '  ·  ' + dateStr, MARGIN, 44);

  function roundRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y,     x + w, y + r,     r);
    ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
    ctx.lineTo(x + r, y + h); ctx.arcTo(x,     y + h, x,     y + h - r, r);
    ctx.lineTo(x, y + r); ctx.arcTo(x,     y,     x + r,     y,         r);
    ctx.closePath();
  }

  tiles.forEach((tile, i) => {
    const tx = MARGIN + i * (TILE_W + TILE_GAP);
    const ty = TILE_TOP;
    ctx.fillStyle = '#1c2128'; roundRect(tx, ty, TILE_W, TILE_H, 6); ctx.fill();
    ctx.strokeStyle = '#30363d'; ctx.lineWidth = 1; roundRect(tx, ty, TILE_W, TILE_H, 6); ctx.stroke();
    ctx.fillStyle = '#8b949e'; ctx.font = '10px Inter,system-ui,sans-serif';
    ctx.textAlign = 'left'; ctx.fillText(tile.label, tx + 10, ty + 16);
    ctx.fillStyle = tile.color || '#e6edf3';
    let fontSize = 15;
    ctx.font = 'bold ' + fontSize + 'px Inter,system-ui,sans-serif';
    while (ctx.measureText(tile.value).width > TILE_W - 16 && fontSize > 9) {
      fontSize -= 1;
      ctx.font = 'bold ' + fontSize + 'px Inter,system-ui,sans-serif';
    }
    ctx.fillText(tile.value, tx + 10, ty + 38);
    if (tile.sub) {
      ctx.fillStyle = '#484f58'; ctx.font = '9px Inter,system-ui,sans-serif';
      ctx.fillText(tile.sub, tx + 10, ty + 56);
    }
  });

  const cL = 60, cR = W - MARGIN;
  const cT = CHART_TOP, cB = CHART_TOP + CHART_H_AREA;
  const cW = cR - cL, cH = cB - cT;

  if (isBill) {
    const { bill } = results;
    const hasDemand = bill.breakdown.demandCharge > 0;
    const maxVal = niceMax(Math.max(...bill.monthly.map(m => m.netBill)) * 1.15);
    const nTicks = 5;
    for (let t = 0; t <= nTicks; t++) {
      const v = maxVal * t / nTicks;
      const y = cB - (v / maxVal) * cH;
      ctx.strokeStyle = '#21262d'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(cL, y); ctx.lineTo(cR, y); ctx.stroke();
      ctx.fillStyle = '#8b949e'; ctx.font = '10px Inter,system-ui,sans-serif';
      ctx.textAlign = 'right';
      ctx.fillText('$' + Math.round(v).toLocaleString(), cL - 5, y + 4);
    }
    const gW = cW / 12;
    bill.monthly.forEach((m, i) => {
      const bW = gW * 0.6;
      const x  = cL + i * gW + (gW - bW) / 2;
      let yBot = cB;
      const drawSeg = (val, color) => {
        if (val <= 0) return;
        const h = Math.max(1, (val / maxVal) * cH);
        ctx.fillStyle = color; ctx.fillRect(x, yBot - h, bW, h); yBot -= h;
      };
      drawSeg(m.energyCharge,   '#58a6ff');
      if (hasDemand) drawSeg(m.demandCharge, '#f85149');
      drawSeg(m.customerCharge, '#8b949e');
      ctx.fillStyle = '#8b949e'; ctx.font = '10px Inter,system-ui,sans-serif';
      ctx.textAlign = 'center'; ctx.fillText(MONTHS[i], x + bW / 2, cB + 14);
    });
    let lx = cL;
    const legend = [['Energy','#58a6ff']];
    if (hasDemand) legend.push(['Demand','#f85149']);
    legend.push(['Customer','#8b949e']);
    ctx.textAlign = 'left';
    legend.forEach(([lbl, col]) => {
      ctx.fillStyle = col; ctx.fillRect(lx, cB + 24, 12, 10);
      ctx.fillStyle = '#8b949e'; ctx.font = '11px Inter,system-ui,sans-serif';
      ctx.fillText(lbl, lx + 16, cB + 34);
      lx += 80 + (lbl.length > 6 ? 20 : 0);
    });

  } else {
    const { baseline, solar } = results;
    const maxVal = niceMax(Math.max(...baseline.monthly.map(m => m.netBill)) * 1.15);
    const nTicks = 5;
    for (let t = 0; t <= nTicks; t++) {
      const v = maxVal * t / nTicks;
      const y = cB - (v / maxVal) * cH;
      ctx.strokeStyle = '#21262d'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(cL, y); ctx.lineTo(cR, y); ctx.stroke();
      ctx.fillStyle = '#8b949e'; ctx.font = '10px Inter,system-ui,sans-serif';
      ctx.textAlign = 'right'; ctx.fillText('$' + Math.round(v).toLocaleString(), cL - 5, y + 4);
    }
    const gW = cW / 12;
    baseline.monthly.forEach((bm, i) => {
      const sm = solar.monthly[i];
      const bW = gW * 0.28;
      const bx = cL + i * gW + gW * 0.08;
      const sx = bx + bW + gW * 0.04;
      const drawBar = (val, color, x2) => {
        const h = Math.max(1, (Math.max(0, val) / maxVal) * cH);
        ctx.fillStyle = color; ctx.fillRect(x2, cB - h, bW, h);
      };
      drawBar(bm.netBill, '#d29922', bx);
      drawBar(sm.netBill, '#58a6ff', sx);
      ctx.fillStyle = '#8b949e'; ctx.font = '10px Inter,system-ui,sans-serif';
      ctx.textAlign = 'center'; ctx.fillText(MONTHS[i], cL + i * gW + gW / 2, cB + 14);
    });
    let lx = cL;
    [['Baseline','#d29922'],['Solar','#58a6ff']].forEach(([lbl, col]) => {
      ctx.fillStyle = col; ctx.fillRect(lx, cB + 24, 12, 10);
      ctx.fillStyle = '#8b949e'; ctx.font = '11px Inter,system-ui,sans-serif';
      ctx.textAlign = 'left'; ctx.fillText(lbl, lx + 16, cB + 34);
      lx += 90;
    });
  }

  ctx.fillStyle = '#484f58'; ctx.font = '10px Inter,system-ui,sans-serif';
  ctx.textAlign = 'right';
  ctx.fillText('MOD-04 Rate Engine v' + VERSION + '  ·  tools.cc-energy.org', W - MARGIN, H - 8);

  const safeName = rateKey.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 30);
  canvas.toBlob(blob => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'rate_engine_' + safeName + '_' + new Date().toISOString().slice(0, 10) + '.png';
    a.click(); URL.revokeObjectURL(url);
  }, 'image/png');
}

// ─── USER MANUAL ──────────────────────────────────────────────────────────────
const RATE_ENGINE_MANUAL = [
  {
    heading: "OVERVIEW",
    body: "The Rate Engine applies utility TOU rate schedules to Green Button interval data to reproduce a customer's electricity bill or compute solar savings. Upload a load file to model the existing bill. Add a PV production file to compare baseline vs solar scenarios month by month. Optionally add battery storage to model self-consumption dispatch.",
  },
  {
    heading: "STEP 1 — LOAD DATA (Green Button)",
    bullets: [
      "SDG&E My Energy Center export — click Download My Data → Interval Data. Includes Consumption, Generation, and Net columns. The Net column is used (billing basis).",
      "SDG&E / MOD-02b synthetic CSV — DATE, START TIME, END TIME, USAGE (kWh), NOTES columns.",
      "UtilityAPI CSV — interval_start and interval_kwh columns.",
      "Drag the file onto the upload zone or click to browse. Date range, total kWh, and peak demand are shown after parsing.",
      "When a Green Button file is loaded, the Service Address in the file header is extracted and auto-populates the Address field (only if the field is currently blank). Zone lookup fires automatically after auto-populate.",
    ],
  },
  {
    heading: "STEP 2 — RATE SCHEDULE",
    bullets: [
      "SDG&E Bundled Residential: TOU-DR1 (standard, no solar), EV-TOU-5 (NEM 3.0 / EV owners), DR-SES (legacy NEM 2.0 solar), DR (non-TOU baseline tiers), EV-TOU-2 (EV owners without solar — year-round SOP, no CARE energy rates). Note: EV-TOU-2 is for EV owners who do not have solar NEM interconnection. Solar customers on NEM 2.0 use DR-SES; solar customers on NEM 3.0 use EV-TOU-5.",
      "SDCP CCA Residential: Select SDCP (CCA) as the service provider to see SDCP TOU-DR1 and SDCP EV-TOU-5. The total bill combines SDG&E delivery ($0.341/kWh flat) + NBC ($0.006/kWh) + PCIA (vintage-dependent, $0.028–$0.051/kWh) + SDCP generation (TOU-varying). SDCP does not have a dedicated EV-TOU-2 equivalent — EV owners on SDCP use SDCP TOU-DR1. NEM 2.0 customers on SDCP also use SDCP TOU-DR1 (same rate structure, export credited at retail).",
      "SDG&E Commercial: TOU-A (< 20 kW demand), AL-TOU (≥ 20 kW), AL-TOU-2 (≥ 20 kW, three-part demand). Demand charges are based on monthly 15-min peak kW. All values verified Apr 2026. Solar NEM 3.0 export credits are supported — select 'NEM 3.0' in the Solar Interconnection field to apply NBT export credits.",
      "APU TOU-2: Anaheim Public Utilities domestic TOU.",
      "Super Off-Peak 10am–2pm weekdays: Added April 1, 2026 for all SDG&E and SDCP residential rates. Applies all summer (Jun–Oct) and in March and April (winter).",
    ],
  },
  {
    heading: "STEP 3 — PV PRODUCTION (optional)",
    bullets: [
      "Upload a PVWatts v8 CSV or a MOD-09 Dual-Axis Tracker Analyzer export (Month, Day, Hour, AC System Output columns). When a PV file is loaded, clicking Calculate switches to Savings Mode — comparing the baseline bill against the solar bill month by month.",
      "After loading a PV file, two additional options appear: Zero-export system and Battery storage (see sections below).",
    ],
  },
  {
    heading: "BASELINE ADJUSTMENT CREDIT (v2.5.0+)",
    bullets: [
      "SDG&E residential TOU schedules (TOU-DR1, EV-TOU-5, DR-SES) include a Baseline Adjustment Credit (BAC) that offsets the loss of tiered pricing. The BAC is a flat credit applied to usage up to 130% of the customer's daily baseline allowance.",
      "To activate: enter the customer's address in the Rate Schedule card (or load a Green Button CSV — the address auto-populates and zone lookup fires automatically when you press Enter or tab out of the field). The engine geocodes the address (via Nominatim / OpenStreetMap), then runs a precise point-in-polygon check against the CEC Building Climate Zone boundaries to determine the SDG&E tariff zone (Coastal / Inland / Mountain / Desert). More accurate than ZIP-code lookup, especially for borderline locations.",
      "Standard BAC: −$0.10892/kWh on up to 130% of monthly baseline. CARE BAC: −$0.07080/kWh.",
      "Check 'All-electric premises' if the home has no gas service. This increases the baseline allowance (SDG&E recognises higher electric usage when gas is not available).",
      "Schedule DR tier boundary is also computed dynamically from the ZIP when provided, rather than using the hardcoded ~350 kWh/mo approximation.",
      "ZIP codes can also be entered as a shorthand (e.g. '92101'). A warning is shown if the address falls outside SDG&E territory. Geocoding requires an internet connection.",
    ],
  },
  {
    heading: "ZERO-EXPORT SYSTEM",
    bullets: [
      "Check 'Zero-export system' (visible after loading a PV file) to model an inverter programmed not to export to the grid — typically required by some utilities, HOAs, or commercial interconnection agreements.",
      "When checked: solar surplus that would normally be exported is curtailed instead. No NEM credits are applied regardless of the NEM selection. The bill is computed as if all excess generation is simply wasted.",
      "Battery storage composes correctly with zero-export: if a battery is also selected, surplus charges the battery first; only generation the battery cannot absorb is curtailed.",
      "An orange '⊘ Zero-export system' banner is shown in the results to confirm the mode. The Export Credit KPI tile is hidden (would be $0 and misleading). The CSV export header records that zero-export was active.",
      "Self-Consumption % reflects consumed ÷ total generated — curtailed energy counts against the denominator, showing the efficiency cost of not exporting.",
    ],
  },
  {
    heading: "BATTERY STORAGE",
    bullets: [
      "The Battery Storage section appears after a PV file is loaded. Select a battery system from the dropdown to model solar self-consumption dispatch.",
      "Available systems: Enphase IQ Battery 10C (1–6 units, 10–60 kWh) and Tesla Powerwall 3 (1–6 units, 13.5–81 kWh). Each configuration shows capacity (kWh), power rating (kW), and estimated installed cost.",
      "Dispatch model (solar self-consumption): each 15-min interval, if solar surplus exists (net < 0), the battery charges up to its kW and kWh limits. If load demand exists (net > 0), the battery discharges to serve load, down to a 10% minimum state of charge. The battery starts each calculation at 50% SoC.",
      "Results show a green '⚡' banner with battery specs, a 'Solar+Battery Bill' KPI replacing the plain 'Solar Bill', and a 'Battery Throughput' KPI showing annual kWh dispatched and estimated cycles/day.",
      "Battery configuration is recorded in the CSV export header and savings summary rows.",
    ],
  },
  {
    heading: "BATTERY OPTIMIZER",
    bullets: [
      "After loading both a Green Button file and a PV file, 'Find optimal size:' buttons appear for Enphase IQ 10C and Tesla Powerwall 3. Click either to run a sweep across all unit counts (1x–6x) for that make instantly — no Calculate click required.",
      "Sweep methodology: runs full dispatch + billing for each size. Computes solar-only savings (no battery) as a baseline. For each battery size: incremental savings = total savings − solar-only savings. Payback = installed cost ÷ annual incremental savings. Optimal = shortest payback among configurations where the battery adds at least $10/yr.",
      "If no size provides meaningful incremental savings (< $10/yr for all), the optimizer reports this rather than selecting an arbitrary size.",
      "Results table shows one row per size: Size | kWh | Cost | Total savings/yr | Battery adds/yr | Payback | Throughput/yr | Self-cons %. The optimal row is highlighted in green with a ★ prefix.",
      "The optimal size is automatically applied to the battery dropdown — clicking Calculate will use it. Any row in the table is clickable to override the selection.",
      "The sweep respects the current NEM type and zero-export setting. Click ✕ to dismiss the table. Clicking the other make's button re-runs the sweep for that family.",
    ],
  },
  {
    heading: "CUSTOMER TYPE, PROVIDER & NEM",
    bullets: [
      "Residential — standard SDG&E bundled TOU rates.",
      "Residential (CARE) — California Alternate Rates for Energy. Energy rates ~35–45% lower. Base Services Charge $0.197/day (vs $0.793/day standard). Not available for SDCP CCA rates or EV-TOU-2 (EV-TOU-2 has no separate CARE energy rates; the CARE Base Services Charge still applies).",
      "Commercial — demand-based rates (TOU-A, AL-TOU, AL-TOU-2).",
      "Service Provider: SDG&E Bundled = SDG&E handles both delivery and generation. SDCP (CCA) = SDG&E delivers, SDCP generates. Select the CCA option to see SDCP generation rates.",
      "PCIA (Power Charge Indifference Adjustment): Exit fee charged to CCA customers on all import kWh. Rate depends on your vintage year (year of enrollment into CCA). 2025 vintage: $0.04977/kWh.",
      "SDG&E TOU-ELEC — Schedule TOU-ELEC is a residential bundled rate available to customers who have at least one of: an electric vehicle, a battery storage system, or a heat pump. Check 'TOU-ELEC eligible' in the Rate Schedule card to add it to the rate list. All-electric homes qualify automatically (all-electric → heat pump) and the checkbox is checked automatically when 'All-electric premises' is selected. Subject to a 10,000-customer enrollment cap.",
      "NEM type is selected first — the rate plan list is filtered to only show rates compatible with that NEM type. Changing NEM type resets the rate to the first compatible option.",
      "NEM 1.0 — legacy grandfathered NEM. For tiered rates (Schedule DR): export months pay $0 energy — only the daily Base Services Charge applies.",
      "NEM 2.0 — export credit at full retail TOU rates. For systems interconnected before April 14, 2023.",
      "NEM 3.0 (NBT) — export credit at CPUC Net Billing Tariff rates. Default for solar interconnected after April 14, 2023.",
      "No export credit — all generation assumed self-consumed; no export credit applied.",
    ],
  },
  {
    heading: "RATE COMPATIBILITY",
    body: "Quick reference for NEM program, CARE discount, and Medical Baseline support per rate. CARE = California Alternate Rates for Energy discount. MB = Medical Baseline (modeled rates carry increased allowance flag; MB billing not yet computed).",
    table: {
      headers: ["Rate", "Service", "NEM Compatible", "CARE", "Medical Baseline"],
      rows: [
        ["SDG&E TOU-DR1",   "Residential bundled", "No solar (standard)",     "✓ Yes",               "✓ Yes (flag only)"],
        ["SDG&E EV-TOU-5",  "Residential bundled", "NEM 3.0",                 "✓ Yes",               "✓ Yes (flag only)"],
        ["SDG&E DR-SES",    "Residential bundled", "NEM 2.0",                 "✓ Yes",               "✓ Yes (flag only)"],
        ["SDG&E DR",        "Residential bundled", "NEM 1.0",                 "— not modeled",       "✓ Yes (flag only)"],
        ["SDG&E EV-TOU-2",  "Residential bundled", "EV owners (no NEM plan)", "— no CARE energy rates", "✓ Yes (flag only)"],
        ["SDG&E TOU-ELEC",  "Residential bundled", "None / NEM 3.0",          "✓ Yes",               "✓ Yes (flag only)"],
        ["SDCP TOU-DR1",    "Residential CCA",     "None / NEM 2.0",          "—",                   "—"],
        ["SDCP EV-TOU-5",   "Residential CCA",     "NEM 3.0",                 "—",                   "—"],
        ["SDCP DR",         "Residential CCA",     "NEM 1.0",                 "—",                   "—"],
        ["SDG&E TOU-A",     "Commercial",          "None / NEM 3.0",          "—",                   "—"],
        ["SDG&E AL-TOU",    "Commercial",          "None / NEM 3.0",          "—",                   "—"],
        ["SDG&E AL-TOU-2",  "Commercial",          "None / NEM 3.0",          "—",                   "—"],
        ["APU TOU-2",       "Residential APU",     "No solar",                "—",                   "—"],
      ],
    },
  },
  {
    heading: "BILL RESULTS (load file only)",
    bullets: [
      "Annual Bill — total electricity cost for the 12-month period.",
      "Annual Usage — total import kWh from the grid.",
      "Energy Rate — effective $/kWh from energy charges only (excludes demand and customer charges).",
      "Effective Net Rate — total bill ÷ import kWh (includes all charge components).",
      "Customer Charges — fixed daily charges annualized ($0.793/day standard, $0.197/day CARE).",
      "Baseline Credit — annual Baseline Adjustment Credit (shown when address is geocoded for eligible rates).",
      "Demand Charges — monthly peak kW charges (commercial rates only).",
      "The monthly table shows a full breakdown by TOU period for each month.",
    ],
  },
  {
    heading: "SAVINGS RESULTS (load + PV files)",
    bullets: [
      "Baseline Bill — annual bill without solar.",
      "Solar Bill — annual bill with solar generation applied (net of export credits). Labeled 'Solar+Battery Bill' when a battery is selected.",
      "Annual Savings — the difference between baseline and solar (or solar+battery) bill.",
      "Annual Generation — total PV AC production kWh for the year.",
      "Self-Consumption — fraction of generation consumed on-site (including battery-stored energy). When zero-export is active, curtailed energy counts against the denominator.",
      "Export Credit — credit for excess energy sent to the grid at the selected NEM rate. Hidden when zero-export is active.",
      "Battery Throughput — annual kWh dispatched from the battery and estimated cycles/day. Shown only when a battery is selected.",
      "Zero-export banner — orange '⊘ Zero-export system' notice shown when zero-export is active.",
    ],
  },
  {
    heading: "EXPORTING",
    bullets: [
      "Export Summary CSV — saves all key results and the full monthly breakdown table. Includes rate schedule, customer type, address, coordinates, tariff zone (when provided), NEM type, all KPI values, and — when applicable — battery configuration and zero-export flag.",
      "Save Chart PNG — exports the monthly bar chart with dollar values labeled on each bar. Suitable for proposals and presentations.",
    ],
  },
  {
    heading: "DISCLAIMER",
    body: "Bill calculations include energy charges, customer charges, and demand charges where applicable. Baseline Adjustment Credit (BAC) is modeled for TOU-DR1, EV-TOU-5, and DR-SES when an address is geocoded. Zone is determined by point-in-polygon against CEC Building Climate Zone boundaries (v2.5.0+). Schedule DR tier boundary is computed dynamically from address/zone when provided; otherwise uses a hardcoded ~350 kWh/month approximation. Medical Baseline allowance increase is flagged but not yet computed. For CCA (SDCP) customers, the PCIA is applied to all import kWh — verify vintage year on the customer's SDG&E bill. All AL-TOU and AL-TOU-2 demand charges are verified against SDG&E Apr 2026 tariff PDFs. NEM export rates assume full-year operation at the selected tariff. Battery dispatch uses a simplified self-consumption model (10% min SoC, 50% initial SoC, no degradation). Battery optimizer payback figures use estimated installed costs and are for comparison purposes only.",
  },
];

// ─── MAIN APP ─────────────────────────────────────────────────────────────────
function App() {
  const [loadIntervals,  setLoadIntervals]  = useState(null);
  const [loadMeta,       setLoadMeta]       = useState(null);
  const [pvHourly,       setPvHourly]       = useState(null);
  const [pvMeta,         setPvMeta]         = useState(null);
  const [rateKey,        setRateKey]        = useState('SDG&E EV-TOU-5');
  const [nemType,        setNemType]        = useState('nem3');
  const [parseError,     setParseError]     = useState(null);
  const [results,        setResults]        = useState(null);
  const [customerType,   setCustomerType]   = useState('residential');
  const [serviceProvider, setServiceProvider] = useState('bundled');
  const [pciaVintage,    setPciaVintage]    = useState(2025);
  const [showManual,     setShowManual]     = useState(false);
  const [nemAnniversaryMonth, setNemAnniversaryMonth] = useState(1);
  const [address,        setAddress]        = useState('');
  const [coordLon,       setCoordLon]       = useState(null);
  const [coordLat,       setCoordLat]       = useState(null);
  const [geoStatus,      setGeoStatus]      = useState(null); // null | 'loading' | {zone, lon, lat, display} | 'notfound' | 'error'
  const [allElectric,    setAllElectric]    = useState(false);
  const [touElecEligible, setTouElecEligible] = useState(false);
  const [noExport,       setNoExport]       = useState(false);
  const [batteryKey,     setBatteryKey]     = useState('none');
  const [batterySweep,   setBatterySweep]   = useState(null); // { make, rows, optimalKey }

  const isCare = customerType === 'residential_care';
  const baseCustomerType = isCare ? 'residential' : customerType;

  if (typeof MOD04 === 'undefined') {
    return (
      <div style={{ padding: 32, color: C.red, fontFamily: 'monospace', fontSize: 13 }}>
        Error: MOD04 not loaded. Verify that rate_engine_v2.5.3.js is included in the HTML
        wrapper before this script.
      </div>
    );
  }

  const rates = useMemo(() => {
    if (customerType === 'commercial') return MOD04.listRates('commercial');
    const all = MOD04.listRates('residential');
    let pool = customerType === 'residential_care' ? all.filter(r => r.careAvailable) : all;
    pool = serviceProvider === 'cca_sdcp'
      ? pool.filter(r => r.serviceType === 'cca')
      : pool.filter(r => !r.serviceType || r.serviceType === 'bundled');
    pool = pool.filter(r => nemMatch(r, nemType));
    // TOU-ELEC only shown when customer is confirmed eligible
    if (!touElecEligible) pool = pool.filter(r => !r.touElecRequired);
    return pool;
  }, [customerType, serviceProvider, nemType, touElecEligible]);

  const handleRateChange = useCallback(key => {
    setRateKey(key);
    setResults(null);
  }, []);

  const handleNemTypeChange = useCallback(newNem => {
    setNemType(newNem);
    setResults(null);
    if (customerType === 'commercial') return;
    const all = MOD04.listRates('residential');
    let pool = customerType === 'residential_care' ? all.filter(r => r.careAvailable) : all;
    pool = serviceProvider === 'cca_sdcp'
      ? pool.filter(r => r.serviceType === 'cca')
      : pool.filter(r => !r.serviceType || r.serviceType === 'bundled');
    let compatible = pool.filter(r => nemMatch(r, newNem));
    if (!touElecEligible) compatible = compatible.filter(r => !r.touElecRequired);
    if (compatible.length > 0) setRateKey(compatible[0].key);
  }, [customerType, serviceProvider, touElecEligible]);

  const handleServiceProviderChange = useCallback(provider => {
    setServiceProvider(provider);
    setResults(null);
    const allRes = MOD04.listRates('residential');
    const available = (provider === 'cca_sdcp'
      ? allRes.filter(r => r.serviceType === 'cca')
      : allRes.filter(r => !r.serviceType || r.serviceType === 'bundled'))
      .filter(r => nemMatch(r, nemType));
    if (available.length > 0) setRateKey(available[0].key);
  }, [nemType]);

  const handleLoadFile = useCallback(file => {
    setParseError(null); setResults(null); setLoadIntervals(null); setLoadMeta(null);
    const reader = new FileReader();
    reader.onload = e => {
      try {
        const text       = e.target.result;
        const parsed     = parseGreenButtonCsv(text);
        const { intervals, clipped, droppedRange, droppedMonths } = clipToTwelveMonths(parsed.intervals);
        const meta       = getLoadMeta(intervals);
        setLoadIntervals(intervals);
        setLoadMeta({ ...meta, filename: file.name, clipped, droppedRange, droppedMonths });
        if (parsed.address && !address.trim()) {
          setAddress(parsed.address);
          setCoordLon(null); setCoordLat(null); setGeoStatus(null); setResults(null);
          handleGeocode(parsed.address);
        }
      } catch (err) {
        setParseError({ source: 'load', message: err.message });
      }
    };
    reader.readAsText(file);
  }, [address, handleGeocode]);

  const handlePvFile = useCallback(file => {
    setParseError(null); setResults(null); setPvHourly(null); setPvMeta(null);
    const reader = new FileReader();
    reader.onload = e => {
      try {
        const text              = e.target.result;
        const { hourly, systemKwDc } = parsePvwattsCsv(text);
        const annualKwh         = hourly.reduce((s, h) => s + h, 0);
        setPvHourly(hourly);
        setPvMeta({ filename: file.name, systemKwDc, annualKwh });
      } catch (err) {
        setParseError({ source: 'pv', message: err.message });
      }
    };
    reader.readAsText(file);
  }, []);

  const handleCalculate = useCallback(() => {
    setParseError(null);
    try {
      const yearOffset = loadMeta?.yearOffset ?? 0;

      const _meta = { rateKey, customerType, isCare, nemType,
                      serviceProvider, pciaVintage, nemAnniversaryMonth,
                      loadFilename: loadMeta?.filename || '',
                      pvFilename:   pvMeta?.filename   || '',
                      address:      address || null,
                      lon:          coordLon,
                      lat:          coordLat,
                      allElectric,
                      batteryKey };
      const billOpts = { nemType, yearOffset, utilEsc: 0.04, care: isCare,
                         pciaVintage: parseInt(pciaVintage),
                         nemAnniversaryMonth: parseInt(nemAnniversaryMonth),
                         lon: coordLon,
                         lat: coordLat,
                         allElectric };
      if (pvHourly) {
        // Step 1: raw net intervals (load minus solar)
        const rawNet = MOD04.computeNetIntervals(loadIntervals, pvHourly, 1.0);

        // Step 2: apply battery dispatch (solar self-consumption)
        const bat = batteryKey !== 'none' ? BATTERY_LIBRARY[batteryKey] : null;
        let solarNet = rawNet;
        let batStats = null;
        if (bat) {
          const dispatched = dispatchBattery(rawNet, bat.kwh, bat.kw);
          solarNet = dispatched.intervals;
          batStats = { chargedKwh: dispatched.chargedKwh, dischargedKwh: dispatched.dischargedKwh };
        }

        // Step 3: apply zero-export clamp if selected
        const effectiveNemType = noExport ? 'none' : nemType;
        if (noExport) solarNet = solarNet.map(([ts, kwh]) => [ts, Math.max(0, kwh)]);

        // Step 4: compute baseline and solar bills
        const baseline = MOD04.computeBill(loadIntervals, rateKey,
                           Object.assign({ nemType: 'none' }, billOpts));
        const solar    = MOD04.computeBill(solarNet, rateKey,
                           Object.assign({ nemType: effectiveNemType }, billOpts));

        const annualSavings   = baseline.totalBill - solar.totalBill;
        const rawGenKwh       = pvMeta.annualKwh;
        const selfConsumedKwh = baseline.annualImportKwh - solar.annualImportKwh;
        const selfConsumption = rawGenKwh > 0 ? selfConsumedKwh / rawGenKwh : 0;
        setResults({
          mode: 'savings', rateKey, nemType: effectiveNemType,
          systemDcKw: pvMeta.systemKwDc,
          baseline, solar,
          annualSavings, annualGenKwh: rawGenKwh, selfConsumption,
          npv25: 0, installCost: 0,
          noExport: noExport || false,
          battery: bat ? { ...bat, key: batteryKey, ...batStats } : null,
          ..._meta,
        });
      } else {
        const billResult = MOD04.computeBill(
          loadIntervals, rateKey, billOpts
        );
        setResults({ mode: 'bill', bill: billResult, ..._meta,
                     tariffZone: billResult.tariffZone || null });
      }
    } catch (err) {
      setParseError({ source: 'compute', message: err.message });
    }
  }, [loadIntervals, pvHourly, rateKey, nemType, loadMeta, pvMeta, isCare,
      address, coordLon, coordLat, allElectric, noExport, batteryKey,
      pciaVintage, nemAnniversaryMonth, serviceProvider]);

  const handleOptimize = useCallback((make) => {
    if (!loadIntervals || !pvHourly) return;
    try {
      const yearOffset = loadMeta?.yearOffset ?? 0;
      const billOpts = { nemType, yearOffset, utilEsc: 0.04, care: isCare,
                         pciaVintage: parseInt(pciaVintage),
                         nemAnniversaryMonth: parseInt(nemAnniversaryMonth),
                         lon: coordLon, lat: coordLat, allElectric };
      const effectiveNemType = noExport ? 'none' : nemType;

      const rawNet = MOD04.computeNetIntervals(loadIntervals, pvHourly, 1.0);

      // Solar-only bill (no battery) — used to isolate battery's incremental contribution
      const solarOnlyNet = noExport ? rawNet.map(([ts, kwh]) => [ts, Math.max(0, kwh)]) : rawNet;
      const baseline  = MOD04.computeBill(loadIntervals, rateKey,
                          Object.assign({ nemType: 'none'           }, billOpts));
      const solarOnly = MOD04.computeBill(solarOnlyNet,  rateKey,
                          Object.assign({ nemType: effectiveNemType }, billOpts));
      const solarOnlySavings = baseline.totalBill - solarOnly.totalBill;

      // Sweep every size in the selected make family
      const family = Object.entries(BATTERY_LIBRARY)
        .filter(([k]) => k.toLowerCase().includes(make === 'enphase' ? 'enphase' : 'powerwall'));

      const rows = family.map(([key, bat]) => {
        const { intervals: batNet, dischargedKwh } = dispatchBattery(rawNet, bat.kwh, bat.kw);
        const effectiveNet = noExport
          ? batNet.map(([ts, kwh]) => [ts, Math.max(0, kwh)])
          : batNet;
        const solarBill    = MOD04.computeBill(effectiveNet, rateKey,
                               Object.assign({ nemType: effectiveNemType }, billOpts));
        const totalSavings = baseline.totalBill - solarBill.totalBill;
        const incremental  = totalSavings - solarOnlySavings;
        const payback      = incremental > 10 ? bat.fixedCost / incremental : Infinity;
        const selfConsumed = baseline.annualImportKwh - solarBill.annualImportKwh;
        const selfPct      = pvMeta.annualKwh > 0 ? selfConsumed / pvMeta.annualKwh : 0;
        return { key, label: bat.label, kwh: bat.kwh, kw: bat.kw,
                 cost: bat.fixedCost, totalSavings, incremental,
                 payback, dischargedKwh, selfPct };
      });

      // Best payback among configurations where battery meaningfully contributes
      const viable    = rows.filter(r => r.incremental > 10);
      const optimalKey = viable.length > 0
        ? viable.reduce((b, r) => r.payback < b.payback ? r : b).key
        : null;

      setBatterySweep({ make, rows, optimalKey, solarOnlySavings });
      if (optimalKey) { setBatteryKey(optimalKey); setResults(null); }
    } catch (err) {
      setParseError({ source: 'compute', message: 'Battery optimizer: ' + err.message });
    }
  }, [loadIntervals, pvHourly, rateKey, nemType, loadMeta, pvMeta, isCare,
      address, coordLon, coordLat, allElectric, noExport,
      pciaVintage, nemAnniversaryMonth, serviceProvider]);

  const handleDownloadManual = () => {
    const lines = [
      'RATE ENGINE — USER MANUAL',
      'Version ' + VERSION + ' | Center for Community Energy / Makello',
      'Generated: ' + new Date().toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'}),
      '',
    ];
    RATE_ENGINE_MANUAL.forEach(sec => {
      lines.push('');
      lines.push('── ' + sec.heading + ' ──');
      lines.push('');
      if (sec.body)    lines.push(sec.body);
      if (sec.bullets) sec.bullets.forEach(b => lines.push('  • ' + b));
      if (sec.table) {
        lines.push('');
        // Render table as fixed-width text
        const colWidths = sec.table.headers.map((h, ci) =>
          Math.max(h.length, ...sec.table.rows.map(r => (r[ci]||'').length)) + 2
        );
        const rowLine = row => row.map((c, ci) => c.padEnd(colWidths[ci])).join('| ').trimEnd();
        lines.push(rowLine(sec.table.headers));
        lines.push(colWidths.map(w => '-'.repeat(w)).join('|-'));
        sec.table.rows.forEach(r => lines.push(rowLine(r)));
      }
      if (sec.body2)   { lines.push(''); lines.push(sec.body2); }
    });
    lines.push('');
    lines.push('──────────────────────────────────────────────────────────────');
    lines.push('Center for Community Energy · tools.cc-energy.org');
    downloadFile(lines.join('\n'), 'rate_engine_manual_v' + VERSION + '.txt', 'text/plain');
  };

  const canCompute = !!loadIntervals;

  // Determine whether to show address/allElectric controls for the current rate
  const currentRate = MOD04.ALL_RATES ? MOD04.ALL_RATES[rateKey] : null;
  const showZipControls = (customerType === 'residential' || customerType === 'residential_care') &&
    serviceProvider === 'bundled' &&
    currentRate &&
    (currentRate.hasBaselineCredit || currentRate.billingType === 'tiered');

  const handleGeocode = useCallback(async (addrStr) => {
    if (!addrStr || !addrStr.trim()) return;
    setGeoStatus('loading');
    try {
      const q = encodeURIComponent(addrStr.trim() + ', California');
      const url = `https://nominatim.openstreetmap.org/search?q=${q}&format=json&limit=1&countrycodes=us`;
      const resp = await fetch(url, { headers: { 'Accept-Language': 'en' } });
      const data = await resp.json();
      if (!data || data.length === 0) { setGeoStatus('notfound'); return; }
      const lon = parseFloat(data[0].lon);
      const lat = parseFloat(data[0].lat);
      const zone = MOD04.pointInCecZone ? MOD04.pointInCecZone(lon, lat) : null;
      setCoordLon(lon); setCoordLat(lat); setResults(null);
      setGeoStatus({ zone, lon, lat, display: data[0].display_name });
    } catch (e) {
      setGeoStatus('error');
    }
  }, []);

  return (
    <div style={{ minHeight: '100vh', background: C.bg, color: C.text,
                  fontFamily: "'Inter', system-ui, sans-serif" }}>

      {/* Top bar */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    padding: '10px 24px', borderBottom: `1px solid ${C.border}`,
                    background: '#0d1520' }}>
        <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.12em',
                      textTransform: 'uppercase', color: C.blue }}>
          CCE / MAKELLO
          <span style={{ color: '#8ab8ff', opacity: 0.7, margin: '0 8px' }}>|</span>
          RATE ENGINE
          <span style={{ color: '#8ab8ff', opacity: 0.7, margin: '0 8px' }}>|</span>
          v{VERSION}
          <span style={{ color: '#8ab8ff', opacity: 0.7, margin: '0 8px' }}>|</span>
          MOD-04
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <button onClick={() => setShowManual(true)}
            style={{ padding: '4px 12px', fontSize: 11, fontWeight: 600, borderRadius: 5,
                     background: 'transparent', border: '1px solid #30363d',
                     color: '#8b949e', cursor: 'pointer', fontFamily: "'Inter',monospace",
                     letterSpacing: '0.07em' }}>
            ? HELP
          </button>
          <a href="index.html" style={{ fontSize: 12, color: '#6a9acc',
                                        textDecoration: 'none', fontFamily: 'monospace' }}>
            ← All Tools
          </a>
        </div>
      </div>

      <div style={{ maxWidth: 960, margin: '0 auto', padding: '24px 24px 48px' }}>
        <h1 style={{ fontSize: 20, fontWeight: 700, color: C.text, marginBottom: 4 }}>
          Rate Engine
        </h1>
        <p style={{ fontSize: 13, color: C.muted, marginBottom: 24 }}>
          Apply a utility rate schedule to Green Button interval data.
          Add a PV production file to compute solar savings.
        </p>

        {/* ── Customer type selector ── */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
          {[
            { key: 'residential',      label: 'Residential' },
            { key: 'residential_care', label: 'Residential (CARE)' },
            { key: 'commercial',       label: 'Commercial' },
          ].map(({ key, label }) => (
            <button
              key={key}
              onClick={() => {
                setCustomerType(key);
                setResults(null);
                if (key === 'commercial') {
                  setNemType('none');
                  const available = MOD04.listRates('commercial');
                  if (available.length > 0) setRateKey(available[0].key);
                } else {
                  const defaultNem = 'nem3';
                  setNemType(defaultNem);
                  const all = MOD04.listRates('residential');
                  const pool = (key === 'residential_care' ? all.filter(r => r.careAvailable) : all)
                    .filter(r => !r.serviceType || r.serviceType === 'bundled')
                    .filter(r => nemMatch(r, defaultNem));
                  if (pool.length > 0) setRateKey(pool[0].key);
                }
              }}
              style={{
                padding: '6px 18px', fontSize: 12, fontWeight: 600, borderRadius: 20,
                border: `1px solid ${customerType === key ? (key === 'residential_care' ? C.purple : C.blue) : C.border}`,
                background: customerType === key ? (key === 'residential_care' ? '#221a33' : '#1a2d4a') : C.surface,
                color: customerType === key ? (key === 'residential_care' ? C.purple : C.blue) : C.muted,
                cursor: 'pointer',
                transition: 'all 0.15s',
              }}
            >
              {label}
            </button>
          ))}
        </div>

        {/* ── Service provider selector (residential only) ── */}
        {(customerType === 'residential' || customerType === 'residential_care') && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 11, color: C.muted, letterSpacing: '0.04em',
                           textTransform: 'uppercase', marginRight: 4 }}>Provider:</span>
            {[
              { key: 'bundled', label: 'SDG&E Bundled' },
              { key: 'cca_sdcp', label: 'SDCP (CCA)' },
            ].map(({ key, label }) => (
              <button key={key}
                onClick={() => handleServiceProviderChange(key)}
                style={{
                  padding: '5px 14px', fontSize: 11, fontWeight: 600, borderRadius: 16,
                  border: `1px solid ${serviceProvider === key ? C.green : C.border}`,
                  background: serviceProvider === key ? '#0d2d1a' : C.surface,
                  color: serviceProvider === key ? C.green : C.muted,
                  cursor: 'pointer', transition: 'all 0.15s',
                }}>
                {label}
              </button>
            ))}
            {serviceProvider === 'cca_sdcp' && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: 8 }}>
                <span style={{ fontSize: 11, color: C.muted }}>PCIA vintage:</span>
                <select
                  value={pciaVintage}
                  onChange={e => { setPciaVintage(parseInt(e.target.value)); setResults(null); }}
                  style={{ background: '#0d1520', color: C.text, border: `1px solid ${C.border}`,
                           borderRadius: 5, padding: '3px 6px', fontSize: 11 }}>
                  {[2021, 2022, 2023, 2024, 2025, 2026].map(yr => (
                    <option key={yr} value={yr}>{yr}</option>
                  ))}
                </select>
                <span style={{ fontSize: 10, color: C.faint }}>
                  (year of CCA enrollment)
                </span>
              </div>
            )}
          </div>
        )}

        {/* ── CCA info banner ── */}
        {serviceProvider === 'cca_sdcp' && (customerType === 'residential' || customerType === 'residential_care') && (
          <div style={{ background: '#0d2219', border: `1px solid ${C.green}44`, borderRadius: 8,
                        padding: '10px 16px', marginBottom: 16, fontSize: 12,
                        display: 'flex', alignItems: 'flex-start', gap: 10 }}>
            <span style={{ color: C.green, fontSize: 16, lineHeight: 1 }}>🌿</span>
            <div>
              <span style={{ color: C.green, fontWeight: 600 }}>SDCP CCA billing — </span>
              <span style={{ color: '#7ee2a8' }}>
                Total bill = SDG&E delivery ($0.341/kWh flat) + NBC ($0.006/kWh) + PCIA (vintage {pciaVintage}: ${
                  ({2021:0.03557,2022:0.02999,2023:0.02823,2024:0.05045,2025:0.04977,2026:0.04977}[pciaVintage]||0).toFixed(3)
                }/kWh) + SDCP generation (TOU).
                PCIA vintage = year of account enrollment in SDCP. Shown on SDG&E bill under "Power Charge Indifference Adjustment."
                Rates: May 1, 2026 · 2021 V cohort · PowerOn plan.
              </span>
            </div>
          </div>
        )}

        {/* ── CARE mode banner ── */}
        {customerType === 'residential_care' && (
          <div style={{ background: '#1e1530', border: `1px solid ${C.purple}`, borderRadius: 8,
                        padding: '10px 16px', marginBottom: 20, fontSize: 12,
                        display: 'flex', alignItems: 'flex-start', gap: 10 }}>
            <span style={{ color: C.purple, fontSize: 16, lineHeight: 1 }}>⚡</span>
            <div>
              <span style={{ color: C.purple, fontWeight: 600 }}>CARE discount applied — </span>
              <span style={{ color: '#c9a9ff' }}>
                Energy rates ~35–45% lower than standard. Base Services Charge: $0.197/day
                (vs $0.793/day standard). Rates verified from CPUC-sanctioned SDG&E tariff PDFs.
                Schedule DR (non-TOU) and EV-TOU-2 CARE energy rates not modeled.
              </span>
            </div>
          </div>
        )}

        {/* ── Error banner ── */}
        {parseError && (
          <div style={{ background: '#2a0e0e', border: `1px solid ${C.red}`, borderRadius: 8,
                        padding: '12px 16px', marginBottom: 20, fontSize: 12 }}>
            <div style={{ color: C.red, fontWeight: 600, marginBottom: 4 }}>
              {parseError.source === 'load' ? 'Load file error' :
               parseError.source === 'pv'   ? 'PV file error' : 'Computation error'}
            </div>
            <div style={{ color: '#f87171', whiteSpace: 'pre-wrap' }}>{parseError.message}</div>
          </div>
        )}

        {/* ── Step 1 + 2 + 3: inputs row ── */}
        <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginBottom: 20 }}>

          {/* Load data */}
          <UploadCard
            title="Green Button Load Data"
            accept=".csv"
            hint="SDG&E My Energy Center download · MOD-02b synthetic CSV · UtilityAPI export"
            onFile={handleLoadFile}
            meta={loadMeta}
          />

          {/* Rate + NEM */}
          <div style={{ background: C.surface, border: `1px solid ${C.border}`,
                        borderRadius: 8, padding: 16, flex: '1 1 220px', minWidth: 200 }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: C.text, marginBottom: 12 }}>
              Rate Schedule
            </div>

            {/* ── NEM type ── */}
            {(
              <>
                <label style={{ fontSize: 11, color: C.muted, display: 'block', marginBottom: 4 }}>
                  Solar interconnection (NEM type)
                </label>
                <select
                  value={nemType}
                  onChange={e => handleNemTypeChange(e.target.value)}
                  style={{ width: '100%', background: '#0d1520', color: C.text,
                           border: `1px solid ${C.border}`, borderRadius: 6,
                           padding: '6px 8px', fontSize: 12, marginBottom: 14 }}
                >
                  <option value="none">No solar / no export credit</option>
                  <option value="nem3">NEM 3.0 — interconnected after Apr 14, 2023</option>
                  {customerType !== 'commercial' && (
                    <option value="nem2">NEM 2.0 — grandfathered, interconnected before Apr 14, 2023</option>
                  )}
                  {customerType !== 'commercial' && (
                    <option value="nem1">NEM 1.0 — legacy grandfathered (pre-2016)</option>
                  )}
                </select>

                {/* NEM 1.0 anniversary month */}
                {nemType === 'nem1' && (MOD04.ALL_RATES || MOD04.RESIDENTIAL_RATES)[rateKey]?.billingType === 'tiered' && (
                  <div style={{ marginBottom: 14 }}>
                    <label style={{ fontSize: 11, color: C.muted, display: 'block', marginBottom: 4 }}>
                      NEM 1.0 anniversary month
                    </label>
                    <select
                      value={nemAnniversaryMonth}
                      onChange={e => { setNemAnniversaryMonth(parseInt(e.target.value)); setResults(null); }}
                      style={{ width: '100%', background: '#0d1520', color: C.text,
                               border: `1px solid ${C.border}`, borderRadius: 6,
                               padding: '6px 8px', fontSize: 12 }}
                    >
                      {['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
                        .map((m, i) => (
                          <option key={i+1} value={i+1}>{m} — bank resets {m}</option>
                        ))}
                    </select>
                    <div style={{ fontSize: 11, color: C.muted, marginTop: 4 }}>
                      Month when the NEM 1.0 bank resets. Check the customer&apos;s SDG&E bill for
                      their NEM anniversary date.
                    </div>
                  </div>
                )}
              </>
            )}

            {/* ── Rate plan ── */}
            <label style={{ fontSize: 11, color: C.muted, display: 'block', marginBottom: 4 }}>
              Rate plan
            </label>
            {rates.length === 0 ? (
              <div style={{ padding: '8px 10px', borderRadius: 6, background: '#1a1a1a',
                            border: `1px solid ${C.border}`, fontSize: 11,
                            color: C.muted, lineHeight: 1.5, marginBottom: 14 }}>
                No rate modeled for this provider + NEM type combination.
              </div>
            ) : (
              <select
                value={rateKey}
                onChange={e => handleRateChange(e.target.value)}
                style={{ width: '100%', background: '#0d1520', color: C.text,
                         border: `1px solid ${C.border}`, borderRadius: 6,
                         padding: '6px 8px', fontSize: 12, marginBottom: 14 }}
              >
                {rates.map(r => (
                  <option key={r.key} value={r.key}>{r.key}</option>
                ))}
              </select>
            )}

            {/* Rate note */}
            {(MOD04.ALL_RATES || MOD04.RESIDENTIAL_RATES)[rateKey] && (
              <div style={{ fontSize: 11, color: C.muted, marginBottom: 14, lineHeight: 1.5 }}>
                {(MOD04.ALL_RATES || MOD04.RESIDENTIAL_RATES)[rateKey].rateNote}
              </div>
            )}

            {customerType === 'commercial' && (
              <div style={{ fontSize: 11, color: C.muted, marginBottom: 14, lineHeight: 1.5 }}>
                Demand charges apply to AL-TOU and AL-TOU-2. All demand charge values
                verified against SDG&E tariff PDFs (Apr 2026).
              </div>
            )}

            {/* ── Address lookup + all-electric controls ── */}
            {showZipControls && (
              <div style={{ borderTop: `1px solid ${C.border}`, paddingTop: 12, marginTop: 2 }}>
                <label style={{ fontSize: 11, color: C.muted, display: 'block', marginBottom: 4 }}>
                  Address
                  <span style={{ color: C.faint, fontWeight: 400, marginLeft: 4 }}>
                    (enables baseline credit)
                  </span>
                </label>
                <div style={{ marginBottom: 6 }}>
                  <input
                    type="text"
                    value={address}
                    onChange={e => { setAddress(e.target.value); setCoordLon(null); setCoordLat(null); setGeoStatus(null); setResults(null); }}
                    onBlur={e => handleGeocode(e.target.value)}
                    onKeyDown={e => e.key === 'Enter' && handleGeocode(e.target.value)}
                    placeholder="e.g. 92101 or 123 Main St, San Diego"
                    style={{ width: '100%', background: '#0d1520', color: C.text,
                             border: `1px solid ${geoStatus && geoStatus !== 'loading' && geoStatus !== 'notfound' && geoStatus !== 'error'
                               ? (geoStatus.zone ? C.green : C.orange)
                               : C.border}`,
                             borderRadius: 6, padding: '6px 8px', fontSize: 12,
                             fontFamily: 'monospace', boxSizing: 'border-box' }}
                  />
                  {geoStatus === 'loading' && (
                    <div style={{ fontSize: 11, color: C.muted, marginTop: 4 }}>Looking up zone…</div>
                  )}
                </div>
                {geoStatus && geoStatus !== 'loading' && geoStatus.zone && (
                  <div style={{ fontSize: 11, color: C.green, marginBottom: 6 }}>
                    ✓ {geoStatus.zone.charAt(0).toUpperCase() + geoStatus.zone.slice(1)} zone
                    {' · ' + geoStatus.lat?.toFixed(4) + ', ' + geoStatus.lon?.toFixed(4)}
                  </div>
                )}
                {geoStatus === 'notfound' && (
                  <div style={{ fontSize: 11, color: C.orange, marginBottom: 6 }}>
                    ⚠ Address not found — try a ZIP code or more specific address
                  </div>
                )}
                {geoStatus === 'error' && (
                  <div style={{ fontSize: 11, color: C.red, marginBottom: 6 }}>
                    ⚠ Geocoding failed — check your connection and try again
                  </div>
                )}
                {geoStatus && geoStatus !== 'loading' && geoStatus.zone === null && geoStatus !== 'notfound' && geoStatus !== 'error' && (
                  <div style={{ fontSize: 11, color: C.orange, marginBottom: 6 }}>
                    ⚠ Address outside SDG&E service territory — no baseline zone found
                  </div>
                )}
                <label style={{ display: 'flex', alignItems: 'center', gap: 6,
                                fontSize: 11, color: C.muted, cursor: 'pointer',
                                marginBottom: 4 }}>
                  <input
                    type="checkbox"
                    checked={allElectric}
                    onChange={e => {
                      setAllElectric(e.target.checked);
                      if (e.target.checked) setTouElecEligible(true);
                      setResults(null);
                    }}
                    style={{ accentColor: C.blue, cursor: 'pointer' }}
                  />
                  All-electric premises (no gas)
                </label>
                <label style={{ display: 'flex', alignItems: 'flex-start', gap: 6,
                                fontSize: 11, cursor: 'pointer' }}>
                  <input
                    type="checkbox"
                    checked={touElecEligible}
                    onChange={e => {
                      setTouElecEligible(e.target.checked);
                      if (!e.target.checked && rateKey === 'SDG&E TOU-ELEC') {
                        // Fall back to first NEM-compatible rate that doesn't require TOU-ELEC eligibility
                        const fallback = rates.find(r => !r.touElecRequired);
                        if (fallback) setRateKey(fallback.key);
                      }
                      setResults(null);
                    }}
                    style={{ accentColor: C.blue, cursor: 'pointer', marginTop: 1 }}
                  />
                  <span>
                    <span style={{ color: C.muted }}>TOU-ELEC eligible</span>
                    <span style={{ color: C.faint, display: 'block', marginTop: 2, lineHeight: 1.4 }}>
                      Has an EV, battery storage, or heat pump.
                      Unlocks Schedule TOU-ELEC in the rate list.
                      All-electric homes qualify automatically.
                    </span>
                  </span>
                </label>
              </div>
            )}
          </div>

          {/* PV production */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 220px', minWidth: 200 }}>
            <UploadCard
              title="PV Production"
              accept=".csv (PVWatts v8 or MOD-09 export)"
              hint="NREL PVWatts web calculator · MOD-09 Tracker Analyzer export"
              onFile={handlePvFile}
              meta={pvMeta ? {
                filename: pvMeta.filename,
                systemKwDc: pvMeta.systemKwDc,
                annualKwh: pvMeta.annualKwh,
              } : null}
              optional
            />
            {pvHourly && (
              <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer',
                              background: C.surface, border: `1px solid ${noExport ? C.orange : C.border}`,
                              borderRadius: 7, padding: '8px 12px', fontSize: 12, color: C.text,
                              transition: 'border-color 0.15s' }}>
                <input
                  type="checkbox"
                  checked={noExport}
                  onChange={e => { setNoExport(e.target.checked); setResults(null); }}
                  style={{ accentColor: C.orange, width: 14, height: 14, cursor: 'pointer' }}
                />
                <span>
                  <span style={{ fontWeight: 600, color: noExport ? C.orange : C.text }}>
                    Zero-export system
                  </span>
                  <span style={{ color: C.muted, marginLeft: 6 }}>
                    — surplus generation curtailed, no grid export
                  </span>
                </span>
              </label>
            )}

            {pvHourly && (
              <div style={{ background: C.surface,
                            border: `1px solid ${batteryKey !== 'none' ? C.green : C.border}`,
                            borderRadius: 7, padding: '10px 12px', fontSize: 12,
                            transition: 'border-color 0.15s' }}>
                <div style={{ fontWeight: 600, color: C.text, marginBottom: 8 }}>
                  Battery storage
                  <span style={{ fontWeight: 400, color: C.muted, marginLeft: 6 }}>
                    — solar self-consumption dispatch
                  </span>
                </div>
                <select
                  value={batteryKey}
                  onChange={e => { setBatteryKey(e.target.value); setResults(null); }}
                  style={{ width: '100%', background: '#0d1520', color: C.text,
                           border: `1px solid ${C.border}`, borderRadius: 6,
                           padding: '6px 8px', fontSize: 12 }}
                >
                  <option value="none">No battery</option>
                  <optgroup label="Enphase IQ Battery 10C">
                    {Object.entries(BATTERY_LIBRARY)
                      .filter(([k]) => k.includes('Enphase'))
                      .map(([k, v]) => (
                        <option key={k} value={k}>
                          {v.label} — {v.kwh} kWh / {v.kw} kW
                        </option>
                      ))}
                  </optgroup>
                  <optgroup label="Tesla Powerwall 3">
                    {Object.entries(BATTERY_LIBRARY)
                      .filter(([k]) => k.includes('Powerwall'))
                      .map(([k, v]) => (
                        <option key={k} value={k}>
                          {v.label} — {v.kwh} kWh / {v.kw} kW
                        </option>
                      ))}
                  </optgroup>
                </select>
                {batteryKey !== 'none' && (() => {
                  const b = BATTERY_LIBRARY[batteryKey];
                  return (
                    <div style={{ marginTop: 7, fontSize: 11, color: C.muted, lineHeight: 1.6 }}>
                      {b.kwh} kWh usable · {b.kw} kW max power · 10% min SoC
                      {' · '}${b.fixedCost.toLocaleString()} installed (est.)
                    </div>
                  );
                })()}

                {/* Optimizer buttons */}
                {loadIntervals && (
                  <div style={{ marginTop: 10, paddingTop: 10,
                                borderTop: `1px solid ${C.border}` }}>
                    <div style={{ fontSize: 11, color: C.muted, marginBottom: 6 }}>
                      Find optimal size for make:
                    </div>
                    <div style={{ display: 'flex', gap: 8 }}>
                      {[
                        { make: 'enphase',   label: 'Enphase IQ 10C' },
                        { make: 'powerwall', label: 'Tesla Powerwall 3' },
                      ].map(({ make, label }) => (
                        <button key={make}
                          onClick={() => handleOptimize(make)}
                          style={{ padding: '5px 12px', fontSize: 11, fontWeight: 600,
                                   borderRadius: 5, cursor: 'pointer',
                                   background: batterySweep?.make === make ? '#1a3d2a' : C.surface,
                                   border: `1px solid ${batterySweep?.make === make ? C.green : C.border}`,
                                   color: batterySweep?.make === make ? C.green : C.muted,
                                   transition: 'all 0.15s' }}>
                          {label}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
              </div>
            )}

            {/* Battery sweep results table */}
            {batterySweep && pvHourly && (() => {
              const { rows, optimalKey, solarOnlySavings } = batterySweep;
              const tdS = { padding: '5px 10px', fontSize: 11, borderBottom: `1px solid ${C.border}`,
                            color: C.text, textAlign: 'right', whiteSpace: 'nowrap' };
              const thS = { ...tdS, color: C.muted, fontWeight: 600,
                            textTransform: 'uppercase', letterSpacing: '0.04em', fontSize: 10 };
              return (
                <div style={{ background: C.surface, border: `1px solid ${C.green}`, borderRadius: 7,
                              overflow: 'hidden' }}>
                  <div style={{ padding: '8px 12px', fontSize: 12, fontWeight: 600,
                                borderBottom: `1px solid ${C.border}`,
                                display: 'flex', justifyContent: 'space-between',
                                alignItems: 'center' }}>
                    <span>
                      Battery size sweep
                      <span style={{ fontWeight: 400, color: C.muted, marginLeft: 8 }}>
                        solar-only savings: ${Math.round(solarOnlySavings).toLocaleString()}/yr
                      </span>
                    </span>
                    <button onClick={() => { setBatterySweep(null); }}
                      style={{ fontSize: 11, background: 'none', border: 'none',
                               color: C.muted, cursor: 'pointer' }}>✕</button>
                  </div>
                  <div style={{ overflowX: 'auto' }}>
                    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                      <thead>
                        <tr>
                          {['Size','kWh','Cost','Total savings/yr','Battery adds/yr',
                            'Payback','Throughput/yr','Self-cons %'].map(h => (
                            <th key={h} style={{ ...thS, textAlign: h === 'Size' ? 'left' : 'right',
                                                 paddingLeft: h === 'Size' ? 12 : undefined }}>{h}</th>
                          ))}
                        </tr>
                      </thead>
                      <tbody>
                        {rows.map(r => {
                          const isOpt = r.key === optimalKey;
                          const isSelected = r.key === batteryKey;
                          const rowBg = isOpt ? '#0e2218' : isSelected ? '#0d1a20' : 'transparent';
                          return (
                            <tr key={r.key}
                              onClick={() => { setBatteryKey(r.key); setResults(null); }}
                              style={{ background: rowBg, cursor: 'pointer',
                                       transition: 'background 0.1s' }}
                              onMouseEnter={e => e.currentTarget.style.background = isOpt ? '#132d20' : '#161b22'}
                              onMouseLeave={e => e.currentTarget.style.background = rowBg}>
                              <td style={{ ...tdS, textAlign: 'left', paddingLeft: 12,
                                           color: isOpt ? C.green : C.text, fontWeight: isOpt ? 700 : 400 }}>
                                {isOpt ? '★ ' : ''}{r.label}
                              </td>
                              <td style={tdS}>{r.kwh}</td>
                              <td style={tdS}>${r.cost.toLocaleString()}</td>
                              <td style={{ ...tdS, color: C.green }}>${Math.round(r.totalSavings).toLocaleString()}</td>
                              <td style={{ ...tdS, color: r.incremental > 10 ? C.blue : C.muted }}>
                                {r.incremental > 10 ? '+$' + Math.round(r.incremental).toLocaleString() : '—'}
                              </td>
                              <td style={{ ...tdS, color: isOpt ? C.green : C.text,
                                           fontWeight: isOpt ? 700 : 400 }}>
                                {isFinite(r.payback) ? r.payback.toFixed(1) + ' yr' : '—'}
                              </td>
                              <td style={tdS}>{Math.round(r.dischargedKwh).toLocaleString()} kWh</td>
                              <td style={tdS}>{(r.selfPct * 100).toFixed(1)}%</td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                  {optimalKey && (
                    <div style={{ padding: '7px 12px', fontSize: 11, color: C.green,
                                  borderTop: `1px solid ${C.border}` }}>
                      ★ Optimal: {BATTERY_LIBRARY[optimalKey].label} —
                      shortest payback ({rows.find(r=>r.key===optimalKey)?.payback.toFixed(1)} yr).
                      Applied to calculator. Click any row to change selection.
                    </div>
                  )}
                  {!optimalKey && (
                    <div style={{ padding: '7px 12px', fontSize: 11, color: C.muted,
                                  borderTop: `1px solid ${C.border}` }}>
                      No battery size provides meaningful incremental savings over solar-only.
                    </div>
                  )}
                </div>
              );
            })()}
          </div>
        </div>

        {/* ── Calculate button ── */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 32 }}>
          <button
            onClick={handleCalculate}
            disabled={!canCompute}
            style={{ padding: '10px 28px', fontSize: 14, fontWeight: 600, borderRadius: 8,
                     background: canCompute ? C.blue : C.faint,
                     color: canCompute ? '#0d1117' : C.muted,
                     border: 'none', cursor: canCompute ? 'pointer' : 'not-allowed',
                     transition: 'background 0.15s' }}
          >
            {pvHourly ? 'Calculate Savings' : 'Calculate Bill'}
          </button>
          {!loadIntervals && (
            <span style={{ fontSize: 12, color: C.muted }}>
              Upload a Green Button file to enable calculation.
            </span>
          )}
          {loadIntervals && !pvHourly && (
            <span style={{ fontSize: 12, color: C.muted }}>
              No PV file — will reproduce bill only (useful for rate validation).
            </span>
          )}
        </div>

        {/* ── Results ── */}
        {results && results.mode === 'bill' && <BillResults results={results} rateKey={rateKey} />}
        {results && results.mode === 'savings' && <SavingsResults results={results} />}

      {/* ── USER MANUAL MODAL ── */}
      {showManual && (
        <div style={{ position: 'fixed', inset: 0, background: 'rgba(13,17,23,0.94)',
                      zIndex: 1000, overflowY: 'auto', padding: '40px 20px' }}>
          <div style={{ maxWidth: 760, margin: '0 auto', background: '#161b22',
                        border: '1px solid #30363d', borderRadius: 10, padding: '28px 32px' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between',
                          alignItems: 'flex-start', marginBottom: 24 }}>
              <div>
                <div style={{ fontSize: 11, color: '#58a6ff', fontFamily: 'monospace',
                               letterSpacing: '0.12em', marginBottom: 6 }}>MOD-04 — USER MANUAL</div>
                <div style={{ fontSize: 20, fontWeight: 700, color: '#e6edf3',
                               letterSpacing: '-0.02em' }}>Rate Engine</div>
                <div style={{ fontSize: 11, color: '#8b949e', fontFamily: 'monospace',
                               marginTop: 4 }}>v{VERSION} · Center for Community Energy / Makello</div>
              </div>
              <div style={{ display: 'flex', gap: 10, flexShrink: 0, marginLeft: 20 }}>
                <button onClick={handleDownloadManual}
                  style={{ padding: '7px 14px', borderRadius: 6, fontSize: 11, fontWeight: 700,
                           background: '#1f6feb', border: '1px solid #388bfd',
                           color: '#fff', cursor: 'pointer', textTransform: 'uppercase',
                           letterSpacing: '0.05em' }}>↓ Download</button>
                <button onClick={() => setShowManual(false)}
                  style={{ padding: '7px 14px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                           background: 'transparent', border: '1px solid #30363d',
                           color: '#8b949e', cursor: 'pointer' }}>✕ Close</button>
              </div>
            </div>
            <div style={{ borderTop: '1px solid #30363d30', paddingTop: 20 }}>
              {RATE_ENGINE_MANUAL.map((sec, i) => (
                <div key={i} style={{ marginBottom: 24 }}>
                  <div style={{ fontSize: 11, fontFamily: 'monospace', color: '#58a6ff',
                                 letterSpacing: '0.1em', textTransform: 'uppercase',
                                 marginBottom: 8, paddingBottom: 5,
                                 borderBottom: '1px solid #30363d30' }}>{sec.heading}</div>
                  {sec.body && <p style={{ fontSize: 13, color: '#8b949e',
                                           lineHeight: 1.7, margin: '0 0 8px' }}>{sec.body}</p>}
                  {sec.bullets && (
                    <ul style={{ paddingLeft: 18, margin: '0 0 8px' }}>
                      {sec.bullets.map((b, j) => (
                        <li key={j} style={{ fontSize: 13, color: '#8b949e',
                                             lineHeight: 1.7, marginBottom: 4 }}>{b}</li>
                      ))}
                    </ul>
                  )}
                  {sec.table && (
                    <div style={{ overflowX: 'auto', marginTop: 8, marginBottom: 8 }}>
                      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
                        <thead>
                          <tr>
                            {sec.table.headers.map((h, ci) => (
                              <th key={ci} style={{ padding: '6px 10px',
                                                    borderBottom: `1px solid #30363d`,
                                                    borderRight: ci < sec.table.headers.length - 1
                                                      ? `1px solid #21262d` : 'none',
                                                    color: '#8b949e', fontWeight: 600, fontSize: 11,
                                                    textAlign: 'left', textTransform: 'uppercase',
                                                    letterSpacing: '0.04em',
                                                    background: '#0d1117',
                                                    whiteSpace: 'nowrap' }}>{h}</th>
                            ))}
                          </tr>
                        </thead>
                        <tbody>
                          {sec.table.rows.map((row, ri) => (
                            <tr key={ri} style={{ background: ri % 2 === 0 ? '#1a1f27' : 'transparent' }}>
                              {row.map((cell, ci) => (
                                <td key={ci} style={{ padding: '5px 10px',
                                                      borderBottom: `1px solid #21262d`,
                                                      borderRight: ci < row.length - 1
                                                        ? `1px solid #21262d` : 'none',
                                                      color: cell.startsWith('✓') ? '#3fb950'
                                                           : cell === '—' ? '#484f58'
                                                           : cell.startsWith('—') ? '#6e7681'
                                                           : '#8b949e',
                                                      fontSize: 12 }}>{cell}</td>
                              ))}
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    </div>
                  )}
                  {sec.body2 && <p style={{ fontSize: 13, color: '#8b949e',
                                            lineHeight: 1.7, margin: '8px 0 0' }}>{sec.body2}</p>}
                </div>
              ))}
            </div>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                          marginTop: 8, paddingTop: 16, borderTop: '1px solid #30363d30' }}>
              <div style={{ fontSize: 10, color: '#8b949e', fontFamily: 'monospace' }}>
                tools.cc-energy.org · Center for Community Energy
              </div>
              <button onClick={() => setShowManual(false)}
                style={{ padding: '7px 20px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                         background: 'transparent', border: '1px solid #30363d',
                         color: '#8b949e', cursor: 'pointer' }}>✕ Close</button>
            </div>
          </div>
        </div>
      )}

      </div>
    </div>
  );
}

// ─── BILL-ONLY RESULTS ────────────────────────────────────────────────────────
function BillResults({ results, rateKey }) {
  const { bill } = results;
  const [hovered, setHovered] = React.useState(null);

  const hasDemand   = bill.breakdown.demandCharge > 0;
  const hasBaseline = (bill.breakdown.baselineCredit || 0) > 0;
  const annualPeakKw = hasDemand
    ? Math.max(...bill.monthly.map(m => m.maxDemandKw))
    : null;

  const chartData = useMemo(() => bill.monthly.map(m => {
    const d = {
      month:            m.label,
      'Energy charge':  +m.energyCharge.toFixed(2),
      'Customer charge':+m.customerCharge.toFixed(2),
    };
    if (hasDemand) d['Demand charge'] = +m.demandCharge.toFixed(2);
    return d;
  }), [bill, hasDemand]);

  return (
    <div>
      {/* KPI tiles */}
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 20 }}>
        <Tile label="Annual Bill"     value={`$${Math.round(bill.totalBill).toLocaleString()}`} color={C.orange} />
        <Tile label="Annual Usage"    value={`${Math.round(bill.annualImportKwh).toLocaleString()} kWh`} />
        <Tile label="Energy Rate"     value={`$${bill.effectiveRate.toFixed(4)}/kWh`}
              sub="energy charges ÷ import kWh" />
        <Tile label="Effective Net"   value={`$${bill.effectiveNetRate.toFixed(4)}/kWh`}
              sub="total bill ÷ import kWh" />
        <Tile label="Customer Charges" value={`$${Math.round(bill.breakdown.customerCharge).toLocaleString()}`}
              sub="12 mo × fixed monthly charge" />
        {hasBaseline && (
          <Tile label="Baseline Credit"
                value={`-$${Math.round(bill.breakdown.baselineCredit).toLocaleString()}`}
                color={C.green}
                sub={bill.tariffZone
                  ? `BAC · ${bill.tariffZone.charAt(0).toUpperCase() + bill.tariffZone.slice(1)} zone`
                  : 'Baseline Adjustment Credit'} />
        )}
        {hasDemand && (
          <Tile label="Demand Charges" value={`$${Math.round(bill.breakdown.demandCharge).toLocaleString()}/yr`}
                color={C.red} sub="based on monthly peak kW" />
        )}
        {hasDemand && annualPeakKw != null && (
          <Tile label="Annual Peak Demand" value={`${annualPeakKw.toFixed(1)} kW`}
                sub="max 15-min demand" />
        )}
      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
        <button onClick={() => {
          const safeName = (results.rateKey||'rate').replace(/[^a-zA-Z0-9]/g,'_').slice(0,30);
          downloadFile('﻿' + buildSummaryCSV(results),
            'rate_engine_' + safeName + '_' + new Date().toISOString().slice(0,10) + '.csv',
            'text/csv;charset=utf-8');
        }} style={{ padding: '6px 16px', fontSize: 12, fontWeight: 600, borderRadius: 6,
                    background: '#161b22', border: '1px solid #39d353',
                    color: '#39d353', cursor: 'pointer' }}>↓ Export Summary CSV</button>
      </div>

      {/* Monthly bar chart */}
      <div style={{ background: C.surface, border: `1px solid ${C.border}`, borderRadius: 8,
                    padding: 16, marginBottom: 16 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between',
                         alignItems: 'center', marginBottom: 12 }}>
          <div style={{ fontSize: 14, fontWeight: 600 }}>Monthly Bill</div>
          <button onClick={() => drawPng(results)}
            style={{ padding: '5px 14px', fontSize: 11, fontWeight: 600, borderRadius: 5,
                     background: 'transparent', border: '1px solid #30363d',
                     color: '#8b949e', cursor: 'pointer' }}>💾 Save PNG</button>
        </div>
        <ResponsiveContainer width="100%" height={220}>
          <BarChart data={chartData} margin={{ top: 4, right: 16, left: 8, bottom: 0 }}
            onMouseMove={e => e && e.activePayload && setHovered({ label: e.activeLabel, payload: e.activePayload })}
            onMouseLeave={() => setHovered(null)}>
            <CartesianGrid strokeDasharray="3 3" stroke="#21262d" />
            <XAxis dataKey="month" tick={{ fill: C.muted, fontSize: 11 }} />
            <YAxis tick={{ fill: C.muted, fontSize: 11 }} tickFormatter={v => '$' + v}
                   width={56} />
            <Legend wrapperStyle={{ fontSize: 12, color: C.muted }} />
            <Bar dataKey="Energy charge"    fill={C.blue}   stackId="a" radius={[0,0,0,0]} />
            {hasDemand && <Bar dataKey="Demand charge" fill={C.red}   stackId="a" radius={[0,0,0,0]} />}
            <Bar dataKey="Customer charge"  fill={C.muted}  stackId="a" radius={[2,2,0,0]} />
          </BarChart>
        </ResponsiveContainer>
        <div style={{ minHeight: 22, marginTop: 6, fontSize: 12, color: C.muted,
                      borderTop: `1px solid ${C.border}`, paddingTop: 5 }}>
          {hovered ? (() => {
            const p = hovered.payload;
            const energy   = p.find(x => x.dataKey === 'Energy charge');
            const demand   = p.find(x => x.dataKey === 'Demand charge');
            const customer = p.find(x => x.dataKey === 'Customer charge');
            const total    = (energy?.value||0) + (demand?.value||0) + (customer?.value||0);
            return (
              <span>
                <span style={{ fontWeight: 600, color: C.text }}>{hovered.label}</span>
                {' — '}
                <span style={{ color: C.blue }}>Energy: ${(energy?.value||0).toFixed(2)}</span>
                {demand && <span style={{ color: C.red }}> · Demand: ${demand.value.toFixed(2)}</span>}
                <span style={{ color: C.muted }}> · Customer: ${(customer?.value||0).toFixed(2)}</span>
                <span style={{ color: C.text, fontWeight: 600 }}> · Total: ${total.toFixed(2)}</span>
              </span>
            );
          })() : <span style={{ color: C.muted, fontStyle: 'italic' }}>Hover a bar for details</span>}
        </div>
      </div>

      {/* Monthly table */}
      <MonthlyBillTable monthly={bill.monthly} hasDemand={hasDemand} hasBaseline={hasBaseline} />

      <div style={{ fontSize: 11, color: C.muted, marginTop: 12, lineHeight: 1.6 }}>
        Customer charges: standard $0.793/day · CARE $0.197/day · verified from CPUC tariff PDFs.
        {hasBaseline
          ? ` · Baseline Adjustment Credit applied (${(bill.breakdown.baselineCredit||0).toFixed(2)}/yr · ${bill.tariffZone || ''} zone).`
          : ' · Baseline Adjustment Credit not applied (no address/zone resolved).'}
        {' · '}{results.nemType && results.nemType !== 'none'
          ? `NEM export credits applied (${results.nemType === 'nem3' ? 'NEM 3.0 NBT rates' : 'NEM 2.0 retail rates'}).`
          : 'No NEM export credit applied.'}
        {!hasBaseline && ' · Schedule DR tier boundary: ~350 kWh/month (approximate — look up zone for exact).'}
        {' · AL-TOU and AL-TOU-2 demand charges verified SDG&E Apr 2026.'}
        {results.serviceProvider === 'cca_sdcp' ? ` · CCA: PCIA vintage ${results.pciaVintage} applied to all import kWh.` : ''}
      </div>
    </div>
  );
}

// ─── SAVINGS RESULTS ──────────────────────────────────────────────────────────
function SavingsResults({ results }) {
  const { baseline, solar, annualSavings, annualGenKwh, selfConsumption } = results;
  const chartData = useMemo(() => makeSavingsChartData(baseline, solar), [baseline, solar]);
  const [hovered, setHovered] = React.useState(null);
  const hasBaselineCredit = (baseline.breakdown.baselineCredit || 0) > 0 ||
                            (solar.breakdown.baselineCredit || 0) > 0;

  const bat = results.battery;

  return (
    <div>
      {/* Zero-export banner */}
      {results.noExport && (
        <div style={{ background: '#1e1a10', border: `1px solid ${C.orange}`, borderRadius: 8,
                      padding: '10px 16px', marginBottom: 10, fontSize: 12,
                      display: 'flex', alignItems: 'flex-start', gap: 10 }}>
          <span style={{ color: C.orange, fontSize: 16, lineHeight: 1 }}>⊘</span>
          <div>
            <span style={{ color: C.orange, fontWeight: 600 }}>Zero-export system — </span>
            <span style={{ color: '#f5d47a' }}>
              Surplus PV generation{bat ? ' not stored by battery is' : ' is'} curtailed, not exported.
              No NEM export credits applied.
            </span>
          </div>
        </div>
      )}

      {/* Battery banner */}
      {bat && (
        <div style={{ background: '#0e1e14', border: `1px solid ${C.green}`, borderRadius: 8,
                      padding: '10px 16px', marginBottom: 16, fontSize: 12,
                      display: 'flex', alignItems: 'flex-start', gap: 10 }}>
          <span style={{ color: C.green, fontSize: 16, lineHeight: 1 }}>⚡</span>
          <div>
            <span style={{ color: C.green, fontWeight: 600 }}>{bat.label} — </span>
            <span style={{ color: '#7defa1' }}>
              {bat.kwh} kWh · {bat.kw} kW · solar self-consumption dispatch.
              Battery charges from PV surplus and discharges to serve building load.
              {results.noExport ? ' Any remaining surplus is curtailed.' : ' Remaining surplus exported per NEM type.'}
            </span>
          </div>
        </div>
      )}

      {/* KPI tiles */}
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 20 }}>
        <Tile label="Baseline Bill"
              value={`$${Math.round(baseline.totalBill).toLocaleString()}/yr`}
              color={C.orange} />
        <Tile label={bat ? 'Solar+Battery Bill' : 'Solar Bill'}
              value={`$${Math.round(solar.totalBill).toLocaleString()}/yr`}
              color={C.blue} />
        <Tile label="Annual Savings"
              value={`$${Math.round(annualSavings).toLocaleString()}/yr`}
              color={C.green} />
        <Tile label="Annual Generation"
              value={`${Math.round(annualGenKwh).toLocaleString()} kWh`} />
        <Tile label="Self-Consumption"
              value={`${(selfConsumption * 100).toFixed(1)}%`}
              sub={results.noExport ? 'consumed ÷ generated' : 'generation consumed on-site'} />
        {bat && (
          <Tile label="Battery Throughput"
                value={`${Math.round(bat.dischargedKwh).toLocaleString()} kWh/yr`}
                sub={`${(bat.dischargedKwh / bat.kwh / 365 * 1000 / 1000).toFixed(1)} cycles/day · ${bat.kwh} kWh`}
                color={C.green} />
        )}
        {!results.noExport && (
          <Tile label="Export Credit"
                value={`$${Math.round(solar.breakdown.exportCredit).toLocaleString()}/yr`}
                sub={`${results.nemType === 'nem3' ? 'NBT rates' : results.nemType === 'nem2' ? 'retail net metering' : 'none'}`} />
        )}
        {baseline.breakdown.demandCharge > 0 && (
          <Tile label="Demand Savings"
                value={`$${Math.round(baseline.breakdown.demandCharge - solar.breakdown.demandCharge).toLocaleString()}/yr`}
                color={C.purple}
                sub="peak kW reduction from solar" />
        )}
      </div>

      <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
        <button onClick={() => {
          const safeName = (results.rateKey||'rate').replace(/[^a-zA-Z0-9]/g,'_').slice(0,30);
          downloadFile('﻿' + buildSummaryCSV(results),
            'rate_engine_' + safeName + '_' + new Date().toISOString().slice(0,10) + '.csv',
            'text/csv;charset=utf-8');
        }} style={{ padding: '6px 16px', fontSize: 12, fontWeight: 600, borderRadius: 6,
                    background: '#161b22', border: '1px solid #39d353',
                    color: '#39d353', cursor: 'pointer' }}>↓ Export Summary CSV</button>
      </div>

      {/* Monthly comparison chart */}
      <div style={{ background: C.surface, border: `1px solid ${C.border}`, borderRadius: 8,
                    padding: 16, marginBottom: 16 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between',
                         alignItems: 'center', marginBottom: 12 }}>
          <div style={{ fontSize: 14, fontWeight: 600 }}>
            Monthly Bill — Baseline vs {bat ? 'Solar + Battery' : 'Solar'}
          </div>
          <button onClick={() => drawPng(results)}
            style={{ padding: '5px 14px', fontSize: 11, fontWeight: 600, borderRadius: 5,
                     background: 'transparent', border: '1px solid #30363d',
                     color: '#8b949e', cursor: 'pointer' }}>💾 Save PNG</button>
        </div>
        <ResponsiveContainer width="100%" height={240}>
          <BarChart data={chartData} margin={{ top: 4, right: 16, left: 8, bottom: 0 }}
            onMouseMove={e => e && e.activePayload && setHovered({ label: e.activeLabel, payload: e.activePayload })}
            onMouseLeave={() => setHovered(null)}>
            <CartesianGrid strokeDasharray="3 3" stroke="#21262d" />
            <XAxis dataKey="month" tick={{ fill: C.muted, fontSize: 11 }} />
            <YAxis tick={{ fill: C.muted, fontSize: 11 }} tickFormatter={v => '$' + v}
                   width={60} />
            <Legend wrapperStyle={{ fontSize: 12, color: C.muted }} />
            <Bar dataKey="Baseline" fill={C.orange} radius={[2,2,0,0]} />
            <Bar dataKey="Solar"    fill={C.blue}   radius={[2,2,0,0]} />
          </BarChart>
        </ResponsiveContainer>
        <div style={{ minHeight: 22, marginTop: 6, fontSize: 12, color: C.muted,
                      borderTop: `1px solid ${C.border}`, paddingTop: 5 }}>
          {hovered ? (() => {
            const p = hovered.payload;
            const base = p.find(x => x.dataKey === 'Baseline');
            const sol  = p.find(x => x.dataKey === 'Solar');
            const savings = (base?.value||0) - (sol?.value||0);
            return (
              <span>
                <span style={{ fontWeight: 600, color: C.text }}>{hovered.label}</span>
                {' — '}
                <span style={{ color: C.orange }}>Baseline: ${(base?.value||0).toFixed(2)}</span>
                <span style={{ color: C.blue }}> · Solar: ${(sol?.value||0).toFixed(2)}</span>
                <span style={{ color: C.green, fontWeight: 600 }}> · Savings: ${savings.toFixed(2)}</span>
              </span>
            );
          })() : <span style={{ color: C.muted, fontStyle: 'italic' }}>Hover a bar for details</span>}
        </div>
      </div>

      {/* Monthly savings table */}
      <MonthlySavingsTable baseline={baseline} solar={solar} />

      <div style={{ fontSize: 11, color: C.muted, marginTop: 12, lineHeight: 1.6 }}>
        Customer charges: standard $0.793/day · CARE $0.197/day · verified CPUC tariff PDFs.
        {hasBaselineCredit
          ? ' · Baseline Adjustment Credit applied to both baseline and solar bills.'
          : ' · Baseline Adjustment Credit not applied (no address/zone resolved).'}
        {' · AL-TOU and AL-TOU-2 demand charges verified SDG&E Apr 2026.'}
        {bat ? ` · Battery: ${bat.label} (${bat.kwh} kWh) solar self-consumption dispatch; 10% min SoC, 50% initial SoC.` : ''}
        {results.noExport ? ' · Zero-export: surplus not stored by battery is curtailed; no NEM export credits.' : ''}
        {results.serviceProvider === 'cca_sdcp' ? ` · CCA: PCIA vintage ${results.pciaVintage} applied to all import kWh.` : ''}
      </div>
    </div>
  );
}

// ─── MONTHLY BILL TABLE ───────────────────────────────────────────────────────
function MonthlyBillTable({ monthly, hasDemand, hasBaseline }) {
  const tdS = { padding: '6px 10px', borderBottom: `1px solid ${C.border}`,
                fontSize: 12, color: C.text, textAlign: 'right' };
  const thS = { ...tdS, color: C.muted, fontWeight: 600, fontSize: 11,
                textTransform: 'uppercase', letterSpacing: '0.04em' };

  const hasExport = monthly.some(m => m.exportKwh > 0);

  let headers;
  if (hasDemand) {
    headers = ['Month','Import kWh','Peak kWh','Off-Peak kWh','SOP kWh','Peak kW',
               'Demand $','Energy $','Customer $'];
  } else if (hasExport) {
    headers = ['Month','Import kWh','Export kWh','Energy $','Customer $'];
  } else {
    headers = ['Month','Import kWh','Peak kWh','Off-Peak kWh','SOP kWh','Energy $','Customer $'];
  }
  if (hasBaseline) headers.push('BAC $');
  headers.push('Total $');

  const totals = monthly.reduce((acc, m) => ({
    importKwh: acc.importKwh + m.importKwh,
    peak: acc.peak + m.peakImportKwh,
    offpeak: acc.offpeak + m.offpeakImportKwh,
    sop: acc.sop + m.sopImportKwh,
    energyCharge: acc.energyCharge + m.energyCharge,
    customerCharge: acc.customerCharge + m.customerCharge,
    netBill: acc.netBill + m.netBill,
    demandCharge: acc.demandCharge + m.demandCharge,
    exportKwh: acc.exportKwh + m.exportKwh,
    exportCredit: acc.exportCredit + m.exportCredit,
    baselineCredit: acc.baselineCredit + (m.baselineCredit || 0),
  }), { importKwh:0, peak:0, offpeak:0, sop:0, energyCharge:0, customerCharge:0,
        netBill:0, demandCharge:0, exportKwh:0, exportCredit:0, baselineCredit:0 });

  return (
    <div style={{ background: C.surface, border: `1px solid ${C.border}`,
                  borderRadius: 8, overflow: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ background: C.card }}>
            {headers.map(h => (
              <th key={h} style={{ ...thS, textAlign: h === 'Month' ? 'left' : 'right',
                                   paddingLeft: h === 'Month' ? 12 : undefined }}>{h}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {monthly.map(m => (
            <tr key={m.month} style={{ background: m.month % 2 === 0 ? C.surface : 'transparent' }}>
              <td style={{ ...tdS, textAlign: 'left', paddingLeft: 12, color: C.text,
                           fontWeight: 500 }}>{m.label}</td>
              <td style={tdS}>{m.importKwh.toFixed(0)}</td>
              {hasDemand && <>
                <td style={tdS}>{m.peakImportKwh.toFixed(0)}</td>
                <td style={tdS}>{m.offpeakImportKwh.toFixed(0)}</td>
                <td style={tdS}>{m.sopImportKwh.toFixed(0)}</td>
                <td style={tdS}>{m.maxDemandKw.toFixed(1)}</td>
                <td style={tdS}>${m.demandCharge.toFixed(2)}</td>
              </>}
              {hasExport && !hasDemand && <td style={tdS}>{m.exportKwh.toFixed(0)}</td>}
              {!hasDemand && !hasExport && <>
                <td style={tdS}>{m.peakImportKwh.toFixed(0)}</td>
                <td style={tdS}>{m.offpeakImportKwh.toFixed(0)}</td>
                <td style={tdS}>{m.sopImportKwh.toFixed(0)}</td>
              </>}
              <td style={tdS}>${m.energyCharge.toFixed(2)}</td>
              <td style={tdS}>${m.customerCharge.toFixed(2)}</td>
              {hasBaseline && (
                <td style={{ ...tdS, color: C.green }}>
                  {(m.baselineCredit || 0) > 0 ? `-$${(m.baselineCredit||0).toFixed(2)}` : '—'}
                </td>
              )}
              <td style={{ ...tdS, fontWeight: 600 }}>${m.netBill.toFixed(2)}</td>
            </tr>
          ))}
          <tr style={{ background: C.card, fontWeight: 700 }}>
            <td style={{ ...tdS, textAlign: 'left', paddingLeft: 12 }}>Total</td>
            <td style={tdS}>{totals.importKwh.toFixed(0)}</td>
            {hasDemand && <>
              <td style={tdS}>{totals.peak.toFixed(0)}</td>
              <td style={tdS}>{totals.offpeak.toFixed(0)}</td>
              <td style={tdS}>{totals.sop.toFixed(0)}</td>
              <td style={tdS}>—</td>
              <td style={tdS}>${totals.demandCharge.toFixed(2)}</td>
            </>}
            {hasExport && !hasDemand && <td style={tdS}>{totals.exportKwh.toFixed(0)}</td>}
            {!hasDemand && !hasExport && <>
              <td style={tdS}>{totals.peak.toFixed(0)}</td>
              <td style={tdS}>{totals.offpeak.toFixed(0)}</td>
              <td style={tdS}>{totals.sop.toFixed(0)}</td>
            </>}
            <td style={tdS}>${totals.energyCharge.toFixed(2)}</td>
            <td style={tdS}>${totals.customerCharge.toFixed(2)}</td>
            {hasBaseline && (
              <td style={{ ...tdS, color: C.green }}>-${totals.baselineCredit.toFixed(2)}</td>
            )}
            <td style={{ ...tdS, color: C.orange }}>${totals.netBill.toFixed(2)}</td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

// ─── MONTHLY SAVINGS TABLE ────────────────────────────────────────────────────
function MonthlySavingsTable({ baseline, solar }) {
  const tdS = { padding: '6px 10px', borderBottom: `1px solid ${C.border}`,
                fontSize: 12, color: C.text, textAlign: 'right' };
  const thS = { ...tdS, color: C.muted, fontWeight: 600, fontSize: 11,
                textTransform: 'uppercase', letterSpacing: '0.04em' };

  let totImport=0, totExport=0, totBaseline=0, totSolar=0, totSavings=0, totCredit=0;
  const rows = baseline.monthly.map((bm, i) => {
    const sm     = solar.monthly[i];
    const saving = bm.netBill - sm.netBill;
    totImport   += sm.importKwh;
    totExport   += sm.exportKwh;
    totBaseline += bm.netBill;
    totSolar    += sm.netBill;
    totSavings  += saving;
    totCredit   += sm.exportCredit;
    return { label: bm.label, month: bm.month, importKwh: sm.importKwh,
             exportKwh: sm.exportKwh, exportCredit: sm.exportCredit,
             baselineBill: bm.netBill, solarBill: sm.netBill, savings: saving };
  });

  return (
    <div style={{ background: C.surface, border: `1px solid ${C.border}`,
                  borderRadius: 8, overflow: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ background: C.card }}>
            {['Month','Import kWh','Export kWh','Export Credit','Baseline $',
              'Solar $','Savings $'].map(h => (
              <th key={h} style={{ ...thS, textAlign: h === 'Month' ? 'left' : 'right',
                                   paddingLeft: h === 'Month' ? 12 : undefined }}>{h}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map(r => (
            <tr key={r.month} style={{ background: r.month % 2 === 0 ? C.surface : 'transparent' }}>
              <td style={{ ...tdS, textAlign: 'left', paddingLeft: 12, fontWeight: 500 }}>{r.label}</td>
              <td style={tdS}>{r.importKwh.toFixed(0)}</td>
              <td style={tdS}>{r.exportKwh.toFixed(0)}</td>
              <td style={{ ...tdS, color: C.green }}>${r.exportCredit.toFixed(2)}</td>
              <td style={{ ...tdS, color: C.orange }}>${r.baselineBill.toFixed(2)}</td>
              <td style={{ ...tdS, color: C.blue }}>${r.solarBill.toFixed(2)}</td>
              <td style={{ ...tdS, color: C.green, fontWeight: 600 }}>${r.savings.toFixed(2)}</td>
            </tr>
          ))}
          <tr style={{ background: C.card, fontWeight: 700 }}>
            <td style={{ ...tdS, textAlign: 'left', paddingLeft: 12 }}>Total</td>
            <td style={tdS}>{totImport.toFixed(0)}</td>
            <td style={tdS}>{totExport.toFixed(0)}</td>
            <td style={{ ...tdS, color: C.green }}>${totCredit.toFixed(2)}</td>
            <td style={{ ...tdS, color: C.orange }}>${totBaseline.toFixed(2)}</td>
            <td style={{ ...tdS, color: C.blue }}>${totSolar.toFixed(2)}</td>
            <td style={{ ...tdS, color: C.green }}>${totSavings.toFixed(2)}</td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

// ─── MOUNT ────────────────────────────────────────────────────────────────────
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
