/* official.jsx - RaDa Intelligence T1 Institutional Viewer (gov/agency portal, S0-S9).
 * radaconnect.site/official<county>  (rewrite -> official.html).
 *
 * Auth: phone + PIN (Firebase email/password; account provisioned with ext_scope /
 * ext_role / ext_tenant_id custom claims by portal_set_claims). Data: portal_api CF,
 * which reads gsg_publish_eu ONLY and scopes every row to the caller's ext_scope claim
 * (never the URL). Screens map 1:1 to the S0-S9 design skeleton; every science value
 * carries confidence + method + freshness (honesty chrome). Design owns the look; the
 * wiring + the screens are Code (per George 2026-07-18). */
const { useState, useEffect, useMemo, useRef } = React;

const FB_CONFIG = { apiKey: "AIzaSyAFQ37zvZAD4vWIE5nwuCxzHUeCY8P-m5s", authDomain: "root-range-465417-m6.firebaseapp.com", projectId: "root-range-465417-m6", storageBucket: "root-range-465417-m6.firebasestorage.app", messagingSenderId: "6353794009", appId: "1:6353794009:web:09a6a1ffa464dfea445fc8" };
const PORTAL_API = 'https://portal-api-5bsfvjejza-ew.a.run.app';
const phoneToEmail = (phone) => phone.replace(/\D/g, '') + '@officials.radaconnect.app';

/* ---- i18n (S1 copy translated; institutional chrome stays EN per design) ---- */
const STR = {
  en: { signin: 'Institutional viewer sign-in', phone: 'Phone number', pin: 'PIN', enter: 'Sign in',
    loading: 'Loading your jurisdiction…', signout: 'Sign out', viewonly: 'T1 · VIEW-ONLY',
    scoped: 'SCOPED - FROM TOKEN, NOT URL', season: 'Season driver', sowhat: 'The "so what"',
    farms: 'Farms monitored', ha: 'Area under watch', crops: 'Crops covered', maturity: 'Intelligence maturity',
    impacts: 'Active impacts', pheno: 'Dominant phenophase', rain: 'Season rainfall', fresh: 'Data freshness',
    wards: 'Ward rollup', nge5: 'n >= 5 gate', suppressed: 'Insufficient coverage - suppressed (n<5)',
    impact: 'Impact & Risk', ranked: 'Ranked', crop: 'Crop & Phenology', hypo: 'HYPOTHESIS',
    trends: 'Season trends', analog: '2023 Analog Replay', method: 'Methodology & Provenance',
    exportt: 'Export / Downloads', locked: 'MOU to unlock (T2)', follow: 'Supplementary to KMD / ICPAC - follow official warnings.',
    nopass: 'No wards clear the n>=5 gate yet - county + farm views carry the read.',
    denied: 'Signed in, but your account is not scoped to a jurisdiction.' },
  sw: { signin: 'Kuingia kwa mtazamaji wa taasisi', phone: 'Nambari ya simu', pin: 'PIN', enter: 'Ingia',
    loading: 'Inapakia eneo lako…', signout: 'Toka', viewonly: 'T1 · KUANGALIA TU',
    scoped: 'IMEFUNGWA - KUTOKA TOKENI, SI URL', season: 'Kiendeshi cha msimu', sowhat: 'Maana yake',
    farms: 'Mashamba', ha: 'Eneo linalofuatiliwa', crops: 'Mazao', maturity: 'Ukomavu wa data',
    impacts: 'Athari hai', pheno: 'Hatua kuu', rain: 'Mvua ya msimu', fresh: 'Uhalisia wa data',
    wards: 'Muhtasari wa kata', nge5: 'lango n >= 5', suppressed: 'Data haitoshi - imefichwa (n<5)',
    impact: 'Athari na Hatari', ranked: 'Zimepangwa', crop: 'Zao na Hatua', hypo: 'DHANIA',
    trends: 'Mwenendo wa msimu', analog: 'Marudio ya 2023', method: 'Mbinu na Vyanzo',
    exportt: 'Pakua', locked: 'MOU kufungua (T2)', follow: 'Nyongeza kwa KMD / ICPAC - fuata tahadhari rasmi.',
    nopass: 'Hakuna kata inayopita lango n>=5 bado - mtazamo wa kaunti + shamba unabeba usomaji.',
    denied: 'Umeingia, lakini akaunti yako haijafungwa kwa eneo.' },
};

/* ---- design tokens + honesty chrome ---- */
/* no containers — open blocks on the ground, a single hairline above */
const card = { background: 'transparent', border: 'none', borderTop: '1px solid var(--line)', borderRadius: 0, padding: '16px 2px 6px' };
const eyebrow = { fontFamily: 'var(--font-mono)', fontSize: 10.5, letterSpacing: 1.1, textTransform: 'uppercase', color: 'var(--ink-mute)', marginBottom: 8 };
const mono = { fontFamily: 'var(--font-mono)' };
const fnum = (v, d = 0) => (v == null || v !== v) ? '—' : Number(v).toLocaleString(undefined, { maximumFractionDigits: d });
const BAND = { high: 'var(--red)', med: 'var(--amber)', mod: 'var(--amber)', low: 'var(--green)', good: 'var(--green-deep)' };
const bandCol = (b) => BAND[String(b || '').toLowerCase()] || 'var(--ink-mute)';

function Chip({ conf, method }) {
  if (conf == null || conf === '') return null;
  const c = Math.max(0, Math.min(1, +conf));
  const col = c >= 0.62 ? 'var(--green)' : c >= 0.45 ? 'var(--amber)' : 'var(--red)';
  return <span title={'confidence ' + Math.round(c * 100) + '% · ' + (method || 'provisional')}
    style={{ ...mono, fontSize: 10, fontWeight: 700, color: '#fff', background: col, borderRadius: 8, padding: '1px 7px', marginLeft: 6, whiteSpace: 'nowrap' }}>CONF {Math.round(c * 100) + '%'}</span>;
}
function Tag({ children, tone }) {
  const t = { hypo: ['var(--amber-soft,#F6E9CF)', 'var(--amber-ink,#7A5A16)'], live: ['var(--green-soft,#E4F0DC)', 'var(--green-deep,#2C6B18)'], mute: ['var(--bg-cream-dim,#EDE7D8)', 'var(--ink-mute)'] }[tone] || ['var(--bg-cream-dim,#EDE7D8)', 'var(--ink-mute)'];
  return <span style={{ ...mono, fontSize: 9.5, fontWeight: 700, letterSpacing: .4, background: t[0], color: t[1], borderRadius: 99, padding: '2px 8px', textTransform: 'uppercase', whiteSpace: 'nowrap' }}>{children}</span>;
}
function Fresh({ d }) {
  if (d == null) return null;
  const stale = d > 10;
  return <span style={{ ...mono, fontSize: 9.5, fontWeight: 700, color: stale ? 'var(--amber-ink,#7A5A16)' : 'var(--green-deep,#2C6B18)', background: stale ? 'var(--amber-soft,#F6E9CF)' : 'var(--green-soft,#E4F0DC)', borderRadius: 99, padding: '2px 8px' }}>{stale ? 'STALE ' : 'FRESH '}{d}D</span>;
}

/* ---- data hook: fetch a screen's publish views via portal_api, cache per screen ---- */
function usePortal(user) {
  const [cache, setCache] = useState({});
  const [errors, setErrors] = useState({});
  const load = (screen) => {
    if (!user || cache[screen] || errors[screen]) return;
    user.getIdToken().then(tok =>
      fetch(PORTAL_API + '?screen=' + screen, { headers: { Authorization: 'Bearer ' + tok } })
        .then(r => r.json().then(j => ({ ok: r.ok, j })))
    ).then(({ ok, j }) => {
      if (ok) setCache(c => ({ ...c, [screen]: j.data || {} }));
      else setErrors(e => ({ ...e, [screen]: j.error || 'error' }));
    }).catch(e => setErrors(er => ({ ...er, [screen]: String(e) })));
  };
  return { cache, errors, load };
}

function Login({ t }) {
  const [phone, setPhone] = useState('+254 ');
  const [pin, setPin] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const go = () => {
    setErr(''); setBusy(true);
    firebase.auth().signInWithEmailAndPassword(phoneToEmail(phone), pin)
      .catch(e => { setErr(t('phone') + ' / ' + t('pin') + ' — ' + (e.code || e.message || 'failed')); setBusy(false); });
  };
  return (
    <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-cream)', padding: 16 }}>
      <div style={{ ...card, width: 360, boxShadow: 'var(--shadow-2,0 8px 30px rgba(0,0,0,.12))' }}>
        <img src="/assets/rada_logo_cutout.png" alt="RaDa" style={{ height: 40, objectFit: 'contain', marginBottom: 14 }} onError={e => e.target.style.display = 'none'} />
        <div style={{ ...eyebrow, color: 'var(--orange-deep,#B4681C)' }}>Institutional Viewer · T1</div>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, fontWeight: 600, color: 'var(--forest,#0B1B1A)' }}>Sign in to RaDa Intelligence</div>
        <p style={{ fontSize: 13, color: 'var(--ink-soft,#4A5856)', margin: '6px 0 18px', lineHeight: 1.45 }}>Scoped to your jurisdiction only. No PII. Audit-first — every value carries its confidence, freshness and method.</p>
        <input value={phone} onChange={e => setPhone(e.target.value)} placeholder={t('phone')} style={inp} />
        <input value={pin} onChange={e => setPin(e.target.value)} type="password" inputMode="numeric" placeholder={t('pin')} style={inp} />
        <button onClick={go} disabled={busy} style={btn}>{busy ? '…' : t('enter')}</button>
        {err && <div style={{ marginTop: 12, fontSize: 12.5, color: 'var(--red)' }}>{err}</div>}
        <div style={{ ...mono, fontSize: 8.5, letterSpacing: .4, color: 'var(--ink-mute)', marginTop: 16, lineHeight: 1.7 }}>CHEMELI V.8 · FIREBASE PHONE AUTH · SCOPE FROM EXT_TENANT CLAIM</div>
      </div>
    </div>
  );
}
const inp = { width: '100%', boxSizing: 'border-box', padding: '11px 13px', margin: '6px 0', fontSize: 15, fontFamily: 'inherit', border: '1px solid var(--line)', borderRadius: 10, background: 'var(--bg-cream)', color: 'var(--ink)' };
const btn = { width: '100%', marginTop: 10, padding: 13, border: 'none', borderRadius: 10, background: 'var(--orange,#DE994C)', color: '#fff', fontWeight: 700, fontSize: 15, fontFamily: 'inherit', cursor: 'pointer' };

/* ============================ SCREENS ============================ */
/* theme: dark default, 'light' = brand cream (Chemeli). Persisted; set pre-paint in official.html */
function ThemeBtn({ style }) {
  const [th, setTh] = useState(() => document.documentElement.dataset.theme === 'light' ? 'light' : 'dark');
  const flip = () => { const nx = th === 'dark' ? 'light' : 'dark'; setTh(nx); if (nx === 'light') document.documentElement.dataset.theme = 'light'; else delete document.documentElement.dataset.theme; try { localStorage.setItem('rada_theme', nx); } catch (e) {} };
  return <button onClick={flip} title={th === 'dark' ? 'Switch to the cream daylight theme' : 'Switch to the dark theme'} style={style}>{th === 'dark' ? '☀' : '☾'}</button>;
}
/* El Niño preparedness — calm, dismissible, links to the live bulletin (banded, supplementary to KMD) */
function NinoBanner() {
  const [on, setOn] = useState(() => { try { return !localStorage.getItem('rada_nino_ack_official_v1'); } catch (e) { return true; } });
  if (!on) return null;
  return <div className="nino" style={{ marginBottom: 14 }}>
    <span className="nd"></span>
    <span className="nt"><b>El Niño preparedness.</b> Heavy rains expected this season — drainage, storage and slope wards need readiness now. The ward-level flood &amp; landslide watch updates daily.</span>
    <a href="/bulletin">Read this week's bulletin →</a>
    <span className="ntag">preparedness · banded · supplementary to KMD</span>
    <button className="nx" title="Dismiss" onClick={() => { setOn(false); try { localStorage.setItem('rada_nino_ack_official_v1', '1'); } catch (e) {} }}>×</button>
  </div>;
}
function KPI({ label, value, sub, chip, fresh }) {
  return <div style={card}>
    <div style={eyebrow}>{label}</div>
    <div style={{ fontFamily: 'var(--font-display)', fontSize: 27, fontWeight: 600, color: 'var(--ink)', lineHeight: 1 }}>{value}</div>
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6, flexWrap: 'wrap' }}>
      {sub && <span style={{ fontSize: 11.5, color: 'var(--ink-mute)' }}>{sub}</span>}
      {chip}{fresh}
    </div>
  </div>;
}

function S1({ d, t, lang }) {
  const ov = (d.v_county_overview || [])[0] || {};
  const cw = (d.v_climate_watch || [])[0] || {};
  const sb = (d.v_systemic_banner || [])[0] || {};
  const flood = (ov.n_flood_high || 0) + (ov.n_flood_med || 0) + (ov.n_flood_low || 0);
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
    {/* season driver + authority ribbon */}
    <div style={{ ...card, background: 'linear-gradient(118deg,#FBEACB 0%,#F5D9A9 45%,#E9CEA2 100%)', border: '1px solid #E7D3B0' }}>
      <div style={{ ...eyebrow, color: 'var(--orange-deep,#B4681C)' }}>{t('season')} · CLIMATE_WATCH</div>
      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 26, color: 'var(--ink)', lineHeight: 1.1 }}>{cw.enso_state || 'El Niño Advisory'}</div>
      <div style={{ fontSize: 13, color: 'var(--ink-soft)', marginTop: 6 }}>
        {cw.enso_prob_ond != null ? 'OND persistence ' + cw.enso_prob_ond + ' · ' : ''}Niño-3.4 {cw.nino34_c != null ? '+' + cw.nino34_c + '°C' : '—'}
      </div>
      <div style={{ ...mono, fontSize: 10, color: 'var(--ink-mute)', marginTop: 8 }}>⚠ {t('follow')}</div>
    </div>
    {/* systemic banner */}
    {sb.county && <div style={{ ...card, borderLeft: '4px solid ' + (sb.systemic_flag ? 'var(--green-deep,#2C6B18)' : 'var(--ink-mute)') }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 34, color: sb.systemic_flag ? 'var(--green-deep,#2C6B18)' : 'var(--ink-mute)', lineHeight: 1 }}>
          {Math.round((sb.systemic_dryness_frac || 0) * (sb.n_farms || 0))}<span style={{ fontSize: 18, color: 'var(--ink-soft)' }}> / {sb.n_farms}</span>
        </div>
        <div>
          <div style={{ fontWeight: 700, fontSize: 15, lineHeight: 1.25 }}>{sb.attribution_tag || (sb.systemic_flag ? 'Farms co-moving into stress' : 'At seasonal normal — not a county-wide signal')}</div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4 }}>
            <Chip conf={sb.systemic_confidence} method={sb.method} />
            <span style={{ ...mono, fontSize: 9, color: 'var(--ink-mute)' }}>v_systemic_banner</span>
          </div>
        </div>
      </div>
    </div>}
    {/* KPI grid */}
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(175px,1fr))', gap: 12 }}>
      <KPI label={t('farms')} value={fnum(ov.n_farms)} sub="Centroid pins · scoped" fresh={<Fresh d={ov.data_age_days} />} />
      <KPI label={t('ha')} value={fnum(ov.hectares_monitored, 0) + ' ha'} sub="Aggregate estimate" />
      <KPI label={t('crops')} value={fnum(ov.n_crops)} sub={ov.crops_covered} chip={<Tag tone="hypo">{t('hypo')} ≤0.70</Tag>} />
      <KPI label={t('impacts')} value={fnum(flood)} sub={(ov.n_flood_high || 0) + ' HIGH · ' + (ov.n_flood_med || 0) + ' MOD · ' + (ov.n_flood_low || 0) + ' LOW'} />
      <KPI label={t('pheno')} value={ov.dominant_phenophase || '—'} chip={<Tag tone="hypo">{t('hypo')}</Tag>} />
      <KPI label={t('rain')} value={ov.avg_pct_rain_vs_normal != null ? (ov.avg_pct_rain_vs_normal > 0 ? '+' : '') + fnum(ov.avg_pct_rain_vs_normal) + '%' : '—'} sub="vs normal" chip={<Chip conf={ov.rain_confidence === 'high' ? 0.8 : 0.6} method="CHIRPS anomaly" />} />
      <KPI label={t('fresh')} value={ov.data_as_of || '—'} fresh={<Fresh d={ov.data_age_days} />} />
    </div>
  </div>;
}

function S3({ d, t }) {
  const rows = d.v_ward_rollup || [];
  const pass = rows.filter(w => w.coverage_state === 'ok');
  const sup = rows.filter(w => w.coverage_state !== 'ok');
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
    {!pass.length && <div style={{ ...card, color: 'var(--ink-mute)' }}>{t('nopass')}</div>}
    {pass.map((w, i) => <div key={i} style={{ ...card, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
      <div style={{ minWidth: 150 }}><div style={{ fontWeight: 700, fontSize: 14 }}>{w.ward}</div><div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>{w.n_farms} farms</div></div>
      <div style={{ fontSize: 13 }}>{w.dominant_crop} <Tag tone="hypo">HYP</Tag></div>
      <div style={{ flex: 1 }} />
      <div style={{ ...mono, fontSize: 12, color: 'var(--ink-soft)' }}>NDMI {fnum(w.avg_ndmi, 2)} · {w.pct_stressed != null ? w.pct_stressed + '% stressed' : ''}</div>
    </div>)}
    {sup.length > 0 && <div style={{ ...card, background: 'var(--bg-cream-dim,#EDE7D8)' }}>
      <div style={eyebrow}>{t('suppressed')}</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        {sup.map((w, i) => <span key={i} style={{ ...mono, fontSize: 11, background: 'transparent', border: 'none', padding: '3px 2px', color: 'var(--ink-mute)' }}>{w.ward} · &lt;5</span>)}
      </div>
    </div>}
  </div>;
}

function S4({ d, t }) {
  const cty = (d.v_impact_county_rollup || [])[0] || {};
  const farm = d.v_farm_risk_report_scoped || [];
  // rank impact types by worst band across farms
  const byType = {};
  farm.forEach(r => { const k = r.impact_type; const s = { high: 3, med: 2, low: 1 }[String(r.value_band || '').toLowerCase()] || 0;
    if (!byType[k] || s > byType[k]._s) byType[k] = { ...r, _s: s, _n: (byType[k] ? byType[k]._n : 0) + 1 }; else byType[k]._n++; });
  const types = Object.values(byType).sort((a, b) => b._s - a._s);
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
    {types.map((im, i) => <div key={i} style={{ ...card, borderLeft: '4px solid ' + bandCol(im.value_band), display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-start' }}>
      <div style={{ minWidth: 160 }}>
        <div style={{ fontWeight: 700, fontSize: 14.5, textTransform: 'capitalize' }}>{String(im.impact_type).replace(/_/g, ' ')}</div>
        <div style={{ ...mono, fontSize: 10.5, fontWeight: 700, color: bandCol(im.value_band), textTransform: 'uppercase' }}>{im.value_band || 'state'} {im.tier === 'B' ? '· Tier B' : ''}</div>
      </div>
      <div style={{ flex: 1, minWidth: 200 }}>
        <div style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.4 }}>{im.basis}</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6, flexWrap: 'wrap' }}>
          <Chip conf={im.confidence} method={im.method} />
          <span style={{ ...mono, fontSize: 9.5, color: 'var(--ink-mute)' }}>{im.method}</span>
        </div>
      </div>
    </div>)}
    {cty.systemic_flag != null && <div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>County systemic: {cty.systemic_flag ? 'FIRING' : 'at normal'} · {cty.attribution_tag || ''}</div>}
  </div>;
}

function S5({ d, t }) {
  const rows = d.v_phenology_pub || [];
  const total = rows.reduce((s, r) => s + (r.n_farms || 0), 0) || 1;
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
    <div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>{(rows[0] || {}).honesty_tag || 'HYPOTHESIS (crop-ID conf <=0.70)'}</div>
    {rows.sort((a, b) => (b.n_farms || 0) - (a.n_farms || 0)).map((r, i) => <div key={i} style={card}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{ fontWeight: 700, fontSize: 14 }}>{r.phenophase}</div>
        <span style={{ fontSize: 12.5, color: 'var(--ink-mute)' }}>{r.crop || 'Unclassified'}</span>
        <div style={{ flex: 1 }} />
        <div style={{ ...mono, fontSize: 12 }}>{r.n_farms} farms · {Math.round(100 * (r.n_farms || 0) / total)}%</div>
        <Chip conf={r.avg_confidence} method={r.method} />
      </div>
      <div style={{ height: 6, background: 'var(--bg-cream-dim,#EDE7D8)', borderRadius: 99, marginTop: 8 }}>
        <div style={{ width: (100 * (r.n_farms || 0) / total) + '%', height: '100%', background: 'var(--green,#4A9E25)', borderRadius: 99 }} />
      </div>
    </div>)}
  </div>;
}

/* small inline NDVI line chart from {week_date, avg_ndvi} rows */
function Spark({ series, color }) {
  if (!series.length) return null;
  const vals = series.map(p => p.v);
  const mn = Math.min(...vals), mx = Math.max(...vals), r = (mx - mn) || 1;
  const W = 520, H = 90;
  const pts = series.map((p, i) => [8 + (i / ((series.length - 1) || 1)) * (W - 16), H - 8 - ((p.v - mn) / r) * (H - 20)]);
  return <svg viewBox={'0 0 ' + W + ' ' + H} style={{ width: '100%', height: 90 }}>
    <polyline points={pts.map(p => p.join(',')).join(' ')} fill="none" stroke={color || 'var(--green,#4A9E25)'} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r="2.4" fill={color || 'var(--green,#4A9E25)'} />)}
  </svg>;
}

function S6({ d }) {
  const rows = (d.v_season_trends || []).slice().sort((a, b) => (a.week_date < b.week_date ? -1 : 1));
  const ndvi = rows.map(r => ({ v: r.avg_ndvi }));
  const ndmi = rows.map(r => ({ v: r.avg_ndmi }));
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
    <div style={card}><div style={eyebrow}>NDVI · vigor (weekly, this season)</div><Spark series={ndvi} color="var(--green,#4A9E25)" /></div>
    <div style={card}><div style={eyebrow}>NDMI · moisture (weekly)</div><Spark series={ndmi} color="var(--blue,#3B6F8C)" /></div>
    <div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>Multi-year satellite baseline unblocked by the OND backfill · rainfall-vs-normal 2024→ only (badge).</div>
  </div>;
}

function S7({ d, t }) {
  const rows = d.v_analog_replay || [];
  const years = {};
  rows.forEach(r => { (years[r.ond_year] = years[r.ond_year] || []).push(r); });
  const yl = Object.keys(years).sort();
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
    <div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>Your county's OND, week by week at 10 m. 2019 + 2023 = wet El Niño / +IOD analogs.</div>
    {!yl.length && <div style={{ ...card, color: 'var(--ink-mute)' }}>Analog replay loading (needs the OND-2023 backfill in gold).</div>}
    {yl.map(y => { const s = years[y].sort((a, b) => (a.week_date < b.week_date ? -1 : 1)); const wet = (years[y][0] || {}).analog_note && years[y][0].analog_note.indexOf('analog') >= 0;
      return <div key={y} style={{ ...card, borderLeft: wet ? '4px solid var(--blue,#3B6F8C)' : '1px solid var(--line)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
          <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 18 }}>OND {y}</div>
          {wet && <Tag tone="mute">wet El Niño / +IOD analog</Tag>}
          <div style={{ flex: 1 }} />
          <div style={{ ...mono, fontSize: 11, color: 'var(--ink-mute)' }}>{s.length} weeks · up to {Math.max(...s.map(r => r.n_farms || 0))} farms</div>
        </div>
        <Spark series={s.map(r => ({ v: r.avg_ndvi }))} color={wet ? 'var(--blue,#3B6F8C)' : 'var(--green,#4A9E25)'} />
      </div>; })}
  </div>;
}

function S8({ d }) {
  const rows = d.v_methodology || [];
  const src = (rows[0] || {}).sources;
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
    {src && <div style={{ ...card, fontSize: 13, lineHeight: 1.5 }}><div style={eyebrow}>Sources</div>{src}</div>}
    <div style={card}>
      <div style={eyebrow}>Per-metric method · confidence · freshness</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {rows.map((r, i) => <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', borderTop: i ? '1px solid var(--line)' : 'none', paddingTop: i ? 8 : 0 }}>
          <div style={{ minWidth: 150, fontWeight: 600, fontSize: 13, textTransform: 'capitalize' }}>{String(r.metric).replace(/_/g, ' ')}</div>
          <div style={{ flex: 1, fontSize: 12, color: 'var(--ink-soft)' }}>{r.method}</div>
          <Tag tone={r.tier === 'A' ? 'live' : r.tier === 'B' ? 'hypo' : 'mute'}>{r.tier === 'A' ? 'LIVE' : r.tier === 'B' ? 'BAND' : r.tier}</Tag>
          <Chip conf={r.avg_confidence} method={r.method} />
        </div>)}
      </div>
    </div>
    <div style={{ ...mono, fontSize: 10.5, color: 'var(--ink-mute)' }}>{(rows[0] || {}).honesty_note || 'M0–M5 ladder; measured, not asserted (P001).'}</div>
  </div>;
}

/* ============================================================================
   S2 MAP OVERLAY  (Map Overlay Protocol v2)
   Google Maps dark "night" base (labels ON) + native HeatmapLayer per-farm blobs
   (each blob = one monitored farm - never a fabricated continuous field) + per-farm
   status sample-circles above the heat + a Master Key panel. Base toggles to satellite
   (hybrid). Honest per P001: centroid pins only, no boundaries, no PII, maxZoom 14,
   ward aggregates k>=5, "indicative - monitored farms only" labelling.
   ============================================================================ */
const clamp01 = (x) => Math.max(0, Math.min(1, x));
const LG = { red: '#C0492B', gold: '#DE994C', green: '#4A9E25', greenDeep: '#2C6B18', blue: '#3B6F8C', mute: '#7A8682' };
const band01 = (b) => { const m = { high: 0.9, med: 0.5, mod: 0.5, low: 0.18 }[String(b || '').toLowerCase()]; return m == null ? null : m; };
const statusCol = (s) => ({ healthy: LG.green, fair: LG.gold, stressed: LG.red, no_data: LG.mute }[s] || LG.mute);

// The 5 El-Nino risk categories (George's framework) x the layers we have REAL data for.
// i(p) -> 0..1 intensity (null = no data); grad = heat gradient; legend = ramp key; tier = confidence.
const LAYERS = {
  rain:      { cat: 'climate', label: 'Rainfall anomaly', tier: 'LIVE', i: p => p.rain_pct == null ? null : clamp01((p.rain_pct + 50) / 150), grad: { 0.0: LG.red, 0.4: LG.gold, 0.6: LG.green, 1.0: LG.blue }, legend: [[LG.red, 'Drought <-30%'], [LG.gold, 'Below normal'], [LG.green, 'Near normal'], [LG.blue, 'Wet >+50%']] },
  soil:      { cat: 'hydro', label: 'Soil saturation', tier: 'BAND', i: p => p.soil_sat == null ? null : clamp01(p.soil_sat / 0.5), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Dry'], [LG.gold, 'Moist'], [LG.red, 'Saturated']] },
  flood:     { cat: 'hydro', label: 'Flood potential', tier: 'BAND', i: p => band01(p.flood_band), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Low'], [LG.gold, 'Moderate'], [LG.red, 'High']] },
  slope:     { cat: 'terrain', label: 'Slope', tier: 'LIVE', i: p => p.slope_deg == null ? null : clamp01(p.slope_deg / 15), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Flat <5deg'], [LG.gold, 'Moderate'], [LG.red, 'Steep >12deg']] },
  erosion:   { cat: 'terrain', label: 'Erosion', tier: 'BAND', i: p => band01(p.erosion_band), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Low'], [LG.gold, 'Moderate'], [LG.red, 'High']] },
  landslide: { cat: 'terrain', label: 'Landslide (rain x slope)', tier: 'v0', i: p => p.slope_deg == null ? null : clamp01(0.5 * clamp01((p.rain_pct || 0) / 80) + 0.5 * clamp01(p.slope_deg / 15)), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Low'], [LG.gold, 'Watch'], [LG.red, 'High (steep + wet)']] },
  ndvi:      { cat: 'veg', label: 'NDVI vigor', tier: 'LIVE', i: p => p.latest_ndvi == null ? null : clamp01((p.latest_ndvi - 0.15) / 0.70), grad: { 0.0: LG.red, 0.30: LG.gold, 0.58: LG.green, 0.82: LG.blue }, legend: [[LG.red, 'Stressed'], [LG.gold, 'Fair'], [LG.green, 'Healthy'], [LG.blue, 'High vigor']] },
  ndvi_anom: { cat: 'veg', label: 'Crop stress (NDVI anomaly)', tier: 'LIVE', i: p => p.ndvi_anomaly == null ? null : clamp01((p.ndvi_anomaly + 0.2) / 0.4), grad: { 0.0: LG.red, 0.5: LG.gold, 1.0: LG.green }, legend: [[LG.red, 'Browner than norm'], [LG.gold, 'At norm'], [LG.green, 'Greener than norm']] },
  ndmi:      { cat: 'veg', label: 'NDMI moisture', tier: 'LIVE', i: p => p.latest_ndmi == null ? null : clamp01((p.latest_ndmi - 0.10) / 0.50), grad: { 0.0: LG.gold, 0.5: LG.green, 1.0: LG.blue }, legend: [[LG.gold, 'Dry'], [LG.green, 'Moist'], [LG.blue, 'Wet']] },
  waterlog:  { cat: 'veg', label: 'Waterlogging', tier: 'BAND-B', i: p => band01(p.waterlog_band), grad: { 0.0: LG.green, 0.5: LG.gold, 1.0: LG.red }, legend: [[LG.green, 'Low'], [LG.gold, 'Watch'], [LG.red, 'High']] },
  status:    { cat: 'veg', label: 'Farm status', tier: 'DERIVED', heat: false, i: p => ({ stressed: 0.1, fair: 0.5, healthy: 0.9 }[p.status] != null ? { stressed: 0.1, fair: 0.5, healthy: 0.9 }[p.status] : null), grad: { 0.0: LG.red, 0.5: LG.gold, 1.0: LG.green }, legend: [[LG.red, 'Stressed'], [LG.gold, 'Fair'], [LG.green, 'Healthy']] },
};
const CATS = [
  { id: 'climate', n: '1', label: 'Climate anomaly', road: ['SPI', 'Temp anomaly'] },
  { id: 'hydro', n: '2', label: 'Hydrology & soil', road: ['Groundwater'] },
  { id: 'terrain', n: '3', label: 'Terrain & geo-risk', road: [] },
  { id: 'veg', n: '4', label: 'Vegetation & crop', road: ['Wildfire fuel'] },
  { id: 'socio', n: '5', label: 'People & assets', road: ['Critical infra', 'Population', 'Evac routes'] },
];
const gradColour = (cfg, v) => { if (v == null) return LG.mute; const ks = Object.keys(cfg.grad).map(Number).sort((a, b) => a - b); let c = cfg.grad[ks[0]]; for (const k of ks) if (v >= k) c = cfg.grad[k]; return c; };
const dotCol = (p, cfg) => gradColour(cfg, cfg.i(p));

// Dark "night" Google Maps style - authored for the Map Overlay Protocol v2.
// Land/water dark, roads muted, LABELS ON (towns/roads/admin) - fixes "no labels".
const GMAP_DARK = [
  { elementType: 'geometry', stylers: [{ color: '#1b2127' }] },
  { elementType: 'labels.text.stroke', stylers: [{ color: '#0b0d11' }] },
  { elementType: 'labels.text.fill', stylers: [{ color: '#b9c4c1' }] },
  { featureType: 'administrative', elementType: 'geometry', stylers: [{ color: '#3a464d' }] },
  { featureType: 'administrative.country', elementType: 'labels.text.fill', stylers: [{ color: '#dbe3e0' }] },
  { featureType: 'administrative.province', elementType: 'geometry.stroke', stylers: [{ color: '#54636b' }, { weight: 1.1 }] },
  { featureType: 'administrative.locality', elementType: 'labels.text.fill', stylers: [{ color: '#e9cfa4' }] },
  { featureType: 'administrative.neighborhood', elementType: 'labels.text.fill', stylers: [{ color: '#9aa6a3' }] },
  { featureType: 'poi', stylers: [{ visibility: 'off' }] },
  { featureType: 'road', elementType: 'geometry', stylers: [{ color: '#2b343b' }] },
  { featureType: 'road', elementType: 'labels.text.fill', stylers: [{ color: '#94a09d' }] },
  { featureType: 'road.highway', elementType: 'geometry', stylers: [{ color: '#3e4a53' }] },
  { featureType: 'road.highway', elementType: 'labels.text.fill', stylers: [{ color: '#c7cfcc' }] },
  { featureType: 'transit', stylers: [{ visibility: 'off' }] },
  { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#0e1a24' }] },
  { featureType: 'water', elementType: 'labels.text.fill', stylers: [{ color: '#51707f' }] },
  { featureType: 'landscape.natural', elementType: 'geometry', stylers: [{ color: '#18211f' }] },
];
const hexA = (hex, a) => { const n = String(hex).replace('#', ''); const r = parseInt(n.slice(0, 2), 16), g = parseInt(n.slice(2, 4), 16), b = parseInt(n.slice(4, 6), 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; };
// Own canvas heat overlay - HeatmapLayer was removed from Maps JS API v3.65. Each blob is one
// monitored farm (radial gradient, coloured by that farm's value on the active layer) so it stays
// honest per P001: it is NOT a fabricated continuous field, it is per-farm signal rendered soft.
function makeHeatOverlay(google) {
  class Heat extends google.maps.OverlayView {
    constructor() { super(); this.canvas = null; this.points = []; this.opts = {}; }
    setData(points, opts) { this.points = points || []; if (opts) this.opts = opts; this.draw(); }
    onAdd() {
      const c = document.createElement('canvas');
      c.style.position = 'absolute'; c.style.left = '0'; c.style.top = '0'; c.style.pointerEvents = 'none';
      this.canvas = c; this.getPanes().overlayLayer.appendChild(c);
    }
    onRemove() { if (this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); this.canvas = null; }
    draw() {
      const proj = this.getProjection(), map = this.getMap(), c = this.canvas;
      if (!proj || !map || !c) return;
      const b = map.getBounds(); if (!b) return;
      const ne = b.getNorthEast(), sw = b.getSouthWest();
      const tl = proj.fromLatLngToDivPixel(new google.maps.LatLng(ne.lat(), sw.lng()));
      const br = proj.fromLatLngToDivPixel(new google.maps.LatLng(sw.lat(), ne.lng()));
      const pad = 80;
      const w = Math.max(1, Math.round(Math.abs(br.x - tl.x)) + pad * 2), h = Math.max(1, Math.round(Math.abs(br.y - tl.y)) + pad * 2);
      const ox = Math.min(tl.x, br.x) - pad, oy = Math.min(tl.y, br.y) - pad;
      c.width = w; c.height = h; c.style.left = ox + 'px'; c.style.top = oy + 'px';
      const ctx = c.getContext('2d'); ctx.clearRect(0, 0, w, h);
      const R = this.opts.radius || 44, op = this.opts.opacity != null ? this.opts.opacity : 0.6, colorFor = this.opts.colorFor || (() => '#DE994C');
      for (const p of this.points) {
        const px = proj.fromLatLngToDivPixel(new google.maps.LatLng(p.lat, p.lng));
        const x = px.x - ox, y = px.y - oy;
        if (x < -R || y < -R || x > w + R || y > h + R) continue;
        const col = colorFor(p.weight);
        const g = ctx.createRadialGradient(x, y, 0, x, y, R);
        g.addColorStop(0, hexA(col, op)); g.addColorStop(0.55, hexA(col, op * 0.5)); g.addColorStop(1, hexA(col, 0));
        ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.fill();
      }
    }
  }
  return new Heat();
}
const pinPopup = (p) => '<div style="font:12px/1.45 system-ui;min-width:172px;color:#15181F">'
  + '<b>' + (p.ward || '—') + '</b><br>'
  + 'Crop: ' + (p.crop_hypothesis || '—') + ' <span style="color:#B4681C;font-weight:700">HYP</span><br>'
  + (p.milestone ? ('Stage: ' + p.milestone + '<br>') : '')
  + 'Impact: ' + (p.top_impact || 'none') + ' (' + (p.top_band || 'low') + ')<br>'
  + 'NDVI ' + (p.latest_ndvi != null ? Number(p.latest_ndvi).toFixed(2) : '—') + ' · NDMI ' + (p.latest_ndmi != null ? Number(p.latest_ndmi).toFixed(2) : '—') + '<br>'
  + '<span style="color:#7A8682">obs ' + (p.last_obs || '—') + '</span> · <span style="color:' + statusCol(p.status) + ';font-weight:700;text-transform:capitalize">' + (p.status || '') + '</span></div>';

function S2({ d, ov, theme, onToggleTheme }) {
  const [layer, setLayer] = useState('ndvi');
  const [base, setBase] = useState('dark');   // dark | satellite
  const [mapReady, setMapReady] = useState(false);   // tiles actually painted
  const [mapSlow, setMapSlow] = useState(false);      // tiles never arrived (old browser / no GPU)
  const mapDiv = useRef(null);
  const mapObj = useRef(null);
  const heatRef = useRef(null);
  const mkRef = useRef([]);
  const infoRef = useRef(null);
  const fitRef = useRef(false);
  const ov2 = ov || {};
  const pins = (d.v_farm_pins_scoped || []).filter(p => p.centroid_lat != null && p.centroid_lng != null);
  const cho = d.v_map_choropleth || [];
  const latestWeek = cho.reduce((m, r) => (r.week_date > m ? r.week_date : m), '');
  const wards = cho.filter(r => r.week_date === latestWeek).sort((a, b) => (b.n_farms || 0) - (a.n_farms || 0));
  const cfg = LAYERS[layer] || LAYERS.ndvi;
  const asOf = ov2.data_as_of || ov2.as_of || latestWeek || '';

  // paint the active layer: canvas heat overlay (vibrant on dark) + per-farm sample circles above it
  const paint = () => {
    const g = window.google, map = mapObj.current;
    if (!g || !map) return;
    mkRef.current.forEach(m => m.setMap(null)); mkRef.current = [];
    if (!pins.length) { if (heatRef.current) heatRef.current.setData([], {}); return; }
    if (heatRef.current) {
      const hp = cfg.heat === false ? [] : pins.map(p => { const v = cfg.i(p); return v == null ? null : { lat: p.centroid_lat, lng: p.centroid_lng, weight: v }; }).filter(Boolean);
      heatRef.current.setData(hp, { radius: 44, opacity: 0.6, colorFor: v => gradColour(cfg, v) });
    }
    // sample circles = one honest discrete signal per monitored farm, drawn above the heat
    pins.forEach(p => {
      const mk = new g.maps.Marker({ position: { lat: p.centroid_lat, lng: p.centroid_lng }, map,
        icon: { path: g.maps.SymbolPath.CIRCLE, scale: 6, fillColor: dotCol(p, cfg), fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 1.5 }, zIndex: 900, optimized: false });
      mk.addListener('click', () => { if (infoRef.current) { infoRef.current.setContent(pinPopup(p)); infoRef.current.open(map, mk); } });
      mkRef.current.push(mk);
    });
    if (!fitRef.current) {
      try { const b = new g.maps.LatLngBounds(); pins.forEach(p => b.extend({ lat: p.centroid_lat, lng: p.centroid_lng })); map.fitBounds(b, 46); fitRef.current = true; } catch (e) {}
    }
  };

  // init the map once (poll for the async Google loader - house pattern from Map.jsx)
  useEffect(() => {
    let dead = false, n = 0;
    const init = () => {
      if (dead) return;
      if (!window.google || !window.google.maps) { if (n++ < 120) setTimeout(init, 150); return; }
      if (mapObj.current || !mapDiv.current) return;
      const g = window.google;
      // maxZoom 14 (village/ward scale) - re-identification guardrail per the honesty review.
      const mapOpts = {
        center: { lat: 0.52, lng: 35.27 }, zoom: 9, minZoom: 6, maxZoom: 14,
        mapTypeId: 'roadmap', styles: GMAP_DARK, backgroundColor: '#0B0D11',
        disableDefaultUI: true, zoomControl: true, gestureHandling: 'greedy', clickableIcons: false,
      };
      // Force raster rendering: JSON dark styles require it, and it renders on machines without
      // WebGL2 (older gov hardware / embedded browsers) where the default vector renderer stalls.
      try { if (g.maps.RenderingType && g.maps.RenderingType.RASTER) mapOpts.renderingType = g.maps.RenderingType.RASTER; } catch (e) {}
      mapObj.current = new g.maps.Map(mapDiv.current, mapOpts);
      infoRef.current = new g.maps.InfoWindow();
      heatRef.current = makeHeatOverlay(g);
      heatRef.current.setMap(mapObj.current);
      g.maps.event.addListenerOnce(mapObj.current, 'tilesloaded', () => { if (!dead) setMapReady(true); });
      paint();
      // The card is a flex child - the map div can size a frame after construction; nudge Google
      // to load tiles and re-fit bounds once the container has its real height.
      [90, 350, 800].forEach(ms => setTimeout(() => {
        if (dead || !mapObj.current) return;
        try { g.maps.event.trigger(mapObj.current, 'resize'); } catch (e) {}
        fitRef.current = false; paint();
      }, ms));
    };
    init();
    const slowT = setTimeout(() => { if (!dead) setMapSlow(true); }, 8000);
    return () => { dead = true; clearTimeout(slowT); try { mkRef.current.forEach(m => m.setMap(null)); if (heatRef.current) heatRef.current.setMap(null); } catch (e) {} heatRef.current = null; mapObj.current = null; fitRef.current = false; };
  }, []);

  // base toggle: dark styled roadmap <-> satellite hybrid (labels carried on both)
  useEffect(() => {
    const g = window.google, map = mapObj.current;
    if (!g || !map) return;
    if (base === 'satellite') { map.setMapTypeId('hybrid'); map.setOptions({ styles: null }); }
    else { map.setMapTypeId('roadmap'); map.setOptions({ styles: GMAP_DARK }); }
  }, [base]);

  // repaint on layer / data change
  useEffect(() => { paint(); }, [layer, d]);

  const gmReady = !!(window.google && window.google.maps);

  return <div style={{ display: 'flex', gap: 14, alignItems: 'flex-start', flexWrap: 'wrap' }}>
    <div style={{ flex: '1 1 440px', minWidth: 290 }}>
      <div style={{ ...card, padding: 0, overflow: 'hidden', position: 'relative' }}>
        <div ref={mapDiv} style={{ height: 500, width: '100%', background: '#0B0D11' }} />
        {!gmReady && !mapSlow && <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#8A9490' }}>Map engine loading…</div>}
        {gmReady && !mapReady && mapSlow && <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: 24, color: '#C7D0CD', background: 'rgba(11,13,17,.92)' }}>
          <div style={{ fontSize: 22 }}>🗺️</div>
          <div style={{ fontWeight: 600, fontSize: 13, color: '#E8EEEC' }}>Interactive map couldn't render in this browser</div>
          <div style={{ ...mono, fontSize: 11, color: '#8A9490', maxWidth: 320, lineHeight: 1.5 }}>The layer readings on the right are live. For the map view, open this portal in a current Chrome, Edge, or Firefox.</div>
        </div>}
        {!pins.length && gmReady && !mapSlow && <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', background: 'rgba(0,0,0,.35)', pointerEvents: 'none' }}>No scoped farm pins.</div>}
        <div style={{ position: 'absolute', left: 10, top: 10, zIndex: 5, ...mono, fontSize: 9.5, fontWeight: 700, color: '#fff', background: 'rgba(11,13,17,.80)', padding: '4px 9px', borderRadius: 6, pointerEvents: 'none', border: '1px solid rgba(255,255,255,.14)' }}>🛡 {pins.length} MONITORED FARMS · CENTROID PINS · NO FIELD BOUNDARIES</div>
        {asOf && <div style={{ position: 'absolute', right: 10, top: 10, zIndex: 5, ...mono, fontSize: 10, fontWeight: 700, letterSpacing: .4, color: '#0B0D11', background: 'var(--orange,#DE994C)', padding: '4px 10px', borderRadius: 6, pointerEvents: 'none' }}>AS OF {asOf}</div>}
        <div style={{ position: 'absolute', left: 10, bottom: 10, zIndex: 5, display: 'flex', gap: 6 }}>
          <div style={{ display: 'flex', border: '1px solid rgba(255,255,255,.18)', borderRadius: 8, overflow: 'hidden', background: 'rgba(11,13,17,.72)' }}>
            {[['dark', 'Dark'], ['satellite', 'Satellite']].map(([k, l]) => <button key={k} onClick={() => setBase(k)}
              style={{ ...mono, fontSize: 10, fontWeight: 700, padding: '5px 10px', border: 'none', cursor: 'pointer', background: base === k ? 'var(--orange,#DE994C)' : 'transparent', color: base === k ? '#0B0D11' : '#cfd8d5' }}>{l}</button>)}
          </div>
          {onToggleTheme && <button onClick={onToggleTheme} title="Toggle page theme"
            style={{ ...mono, fontSize: 12, fontWeight: 700, padding: '4px 10px', border: '1px solid rgba(255,255,255,.18)', borderRadius: 8, cursor: 'pointer', background: 'rgba(11,13,17,.72)', color: '#fff' }}>{theme === 'dark' ? '☀' : '🌙'}</button>}
        </div>
      </div>
    </div>
    <aside style={{ flex: '0 0 244px', ...card, display: 'flex', flexDirection: 'column', gap: 13 }}>
      <div>
        <div style={{ ...eyebrow, marginBottom: 3 }}>Master Key</div>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 19, lineHeight: 1.05 }}>Total area {fnum(ov2.hectares_monitored, 0)} ha</div>
        <div style={{ ...mono, fontSize: 10, color: 'var(--ink-mute)', marginTop: 3 }}>{pins.length} monitored farms · scoped</div>
      </div>
      <div>
        <div style={{ ...mono, fontSize: 9.5, letterSpacing: .5, color: 'var(--ink-mute)', marginBottom: 7 }}>RISK OVERLAYS · El Niño</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
          {CATS.map(cat => {
            const catLayers = Object.entries(LAYERS).filter(([, c]) => c.cat === cat.id);
            return <div key={cat.id}>
              <div style={{ ...mono, fontSize: 8.5, letterSpacing: .3, color: 'var(--ink-mute)', marginBottom: 4 }}>{cat.n} · {cat.label.toUpperCase()}</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                {catLayers.map(([k, c]) => { const on = k === layer; return <button key={k} onClick={() => setLayer(k)} title={c.label}
                  style={{ ...mono, fontSize: 10, fontWeight: 700, padding: '4px 7px', borderRadius: 7, cursor: 'pointer', border: '1px solid ' + (on ? 'var(--orange,#DE994C)' : 'var(--line)'), background: on ? 'var(--orange,#DE994C)' : 'transparent', color: on ? '#fff' : 'var(--ink-soft)' }}>{c.label}</button>; })}
                {cat.road.map((r, i) => <span key={i} title="needs data ingestion — roadmap"
                  style={{ ...mono, fontSize: 9.5, fontWeight: 600, padding: '4px 7px', borderRadius: 7, border: '1px dashed var(--line)', color: 'var(--ink-mute)', opacity: .55 }}>{r} · soon</span>)}
              </div>
            </div>;
          })}
        </div>
      </div>
      <div style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
          <span style={{ ...mono, fontSize: 10, fontWeight: 700, color: 'var(--ink)' }}>{cfg.label}</span>
          <Tag tone={/LIVE/.test(cfg.tier) ? 'live' : /HYP|BAND/.test(cfg.tier) ? 'hypo' : 'mute'}>{cfg.tier}</Tag>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {cfg.legend.map(([c, l], i) => <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ width: 16, height: 12, borderRadius: 3, background: c, border: '1px solid rgba(0,0,0,.15)' }} />
            <span style={{ fontSize: 12, color: 'var(--ink-soft)' }}>{l}</span></div>)}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 9, fontSize: 11, color: 'var(--ink-mute)' }}>
          <span style={{ width: 11, height: 11, borderRadius: '50%', background: '#7A8682', border: '2px solid #fff', boxShadow: '0 0 0 1px #7A8682', flex: 'none' }} /> circle = one farm (this layer)
        </div>
      </div>
      {wards.length > 0 && <div>
        <div style={{ ...mono, fontSize: 9.5, letterSpacing: .5, color: 'var(--ink-mute)', marginBottom: 6 }}>LOCATION LIST · wards n≥5 · {latestWeek}</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 5, maxHeight: 148, overflowY: 'auto' }}>
          {wards.map((w, i) => <div key={i} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, fontSize: 12 }}>
            <span style={{ fontWeight: 600 }}>{w.ward}</span>
            <span style={{ ...mono, fontSize: 10.5, color: 'var(--ink-mute)' }}>{w.n_farms}f · NDVI {fnum(w.avg_ndvi, 2)}</span></div>)}
        </div>
      </div>}
      <div style={{ borderTop: '1px solid var(--line)', paddingTop: 10, display: 'flex', flexDirection: 'column', gap: 7 }}>
        <Fresh d={ov2.data_age_days} />
        <div style={{ fontSize: 10.5, color: 'var(--ink-mute)', lineHeight: 1.4 }}>Indicative — monitored farms only, not a continuous survey. Ward aggregates suppressed below n=5.</div>
      </div>
    </aside>
  </div>;
}

function S9({ t }) {
  return <div style={{ ...card, textAlign: 'center', padding: 40 }}>
    <div style={{ fontSize: 34, marginBottom: 8 }}>🔒</div>
    <div style={{ fontWeight: 700, fontSize: 16 }}>{t('exportt')}</div>
    <div style={{ fontSize: 13, color: 'var(--ink-mute)', margin: '6px 0 14px' }}>Ward-or-coarser CSV / GeoTIFF, watermarked per tenant. {t('locked')}</div>
    <span style={{ ...mono, fontSize: 11, background: 'var(--amber-soft,#F6E9CF)', color: 'var(--amber-ink,#7A5A16)', borderRadius: 99, padding: '5px 12px', fontWeight: 700 }}>{t('locked')}</span>
  </div>;
}

/* ============================ SHELL ============================ */
const NAV = [
  ['S1', 'Situation Overview', 'Landing'], ['S2', 'Map', 'County-bounded'], ['S3', 'Ward Rollup', 'n>=5'],
  ['S4', 'Impact & Risk', 'Ranked'], ['S5', 'Crop & Phenology', 'Hypothesis'], ['S6', 'Season Trends', 'vs normal'],
  ['S7', '2023 Analog Replay', '★'], ['S8', 'Methodology', 'Provenance'], ['S9', 'Export', 'T2 · locked'],
];

function Shell({ user, lang, setLang, t }) {
  const [screen, setScreen] = useState('S1');
  // Map tab auto-flips the WHOLE page to dark (heatmaps read better); user can toggle back.
  const [mapTheme, setMapTheme] = useState(() => { try { return localStorage.getItem('rada-portal-map-theme') || 'dark'; } catch (e) { return 'dark'; } });
  const darkOn = screen === 'S2' && mapTheme === 'dark';
  useEffect(() => {
    const el = document.documentElement;
    if (darkOn) el.setAttribute('data-theme', 'dark'); else el.removeAttribute('data-theme');
    return () => { el.removeAttribute('data-theme'); };
  }, [darkOn]);
  const toggleTheme = () => setMapTheme(m => { const n = m === 'dark' ? 'light' : 'dark'; try { localStorage.setItem('rada-portal-map-theme', n); } catch (e) {} return n; });
  const { cache, errors, load } = usePortal(user);
  useEffect(() => { load(screen); }, [screen, user]);
  const d = cache[screen] || {};
  const ov = (cache.S1 && cache.S1.v_county_overview && cache.S1.v_county_overview[0]) || {};
  const county = ov.county || (d.v_county_overview && d.v_county_overview[0] && d.v_county_overview[0].county) || 'Your county';
  useEffect(() => { load('S1'); }, [user]);
  const SCREENS = { S1, S2, S3, S4, S5, S6, S7, S8, S9 };
  const Cur = SCREENS[screen];
  const err = errors[screen];
  const loaded = !!cache[screen];
  return <div style={{ minHeight: '100vh', background: 'var(--bg-cream)', color: 'var(--ink)', display: 'flex', flexDirection: 'column' }}>
    <header style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '11px 18px', background: 'transparent', borderBottom: '1px solid var(--line)' }}>
      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 15 }}>RaDa Intelligence</div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', background: 'var(--bg-cream)', border: '1px solid var(--line)', borderRadius: 99 }}>
        <span style={{ color: 'var(--orange-deep,#B4681C)' }}>◉</span>
        <div style={{ lineHeight: 1.1 }}><div style={{ fontWeight: 600, fontSize: 13 }}>{county} County</div><div style={{ ...mono, fontSize: 8, color: 'var(--ink-mute)' }}>{t('scoped')}</div></div>
      </div>
      <div style={{ flex: 1 }} />
      <span style={{ ...mono, fontSize: 10, fontWeight: 600, color: 'var(--ink-soft)', display: 'flex', gap: 5, alignItems: 'center' }}>🔒 {t('viewonly')}</span>
      <div style={{ display: 'flex', border: '1px solid var(--line)', borderRadius: 99, overflow: 'hidden' }}>
        {['en', 'sw'].map(l => <button key={l} onClick={() => setLang(l)} style={{ ...mono, fontSize: 11, fontWeight: 700, padding: '5px 11px', border: 'none', cursor: 'pointer', background: lang === l ? 'var(--orange,#DE994C)' : 'transparent', color: lang === l ? '#fff' : 'var(--ink-soft)' }}>{l.toUpperCase()}</button>)}
      </div>
      <ThemeBtn style={miniBtn} />
      <button onClick={() => firebase.auth().signOut()} style={miniBtn}>{t('signout')}</button>
    </header>
    <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
      <nav style={{ flex: '0 0 232px', background: 'var(--bg-cream-dim,#EDE7D8)', borderRight: '1px solid var(--line)', padding: '14px 10px', overflowY: 'auto' }}>
        <div style={{ ...mono, fontSize: 9, letterSpacing: 1.3, color: 'var(--ink-mute)', padding: '4px 10px 10px' }}>/OFFICIAL/{String(county).toUpperCase().replace(/\s+/g, '-')}</div>
        {NAV.map(([k, label, sub]) => { const on = k === screen; return <button key={k} onClick={() => setScreen(k)}
          style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', width: '100%', textAlign: 'left', gap: 1, padding: '9px 12px', borderRadius: 10, border: 'none', cursor: 'pointer', marginBottom: 2, background: on ? 'var(--orange-soft,#F6E1CB)' : 'transparent', color: on ? 'var(--orange-deep,#B4681C)' : 'var(--ink)' }}>
          <span style={{ fontSize: 13, fontWeight: 600 }}>{k} · {label}</span><span style={{ ...mono, fontSize: 9, color: 'var(--ink-mute)' }}>{sub}</span>
        </button>; })}
      </nav>
      <main id="t1-main" style={{ flex: 1, overflowY: 'auto', padding: '20px 26px' }}>
        <div style={{ maxWidth: 900, margin: '0 auto' }}>
          <NinoBanner />
          {err ? <div style={{ ...card, color: 'var(--red)' }}>Screen error: {err}</div>
            : !loaded ? <div style={{ color: 'var(--ink-mute)', padding: 40, textAlign: 'center' }}>{t('loading')}</div>
              : <Cur d={d} t={t} lang={lang} ov={ov} theme={mapTheme} onToggleTheme={toggleTheme} />}
        </div>
      </main>
    </div>
  </div>;
}
const miniBtn = { background: 'var(--bg-cream)', color: 'var(--ink-soft)', border: '1px solid var(--line)', borderRadius: 8, padding: '5px 11px', fontSize: 12, fontFamily: 'inherit', cursor: 'pointer' };

// Dark palette override, injected once. chemeli.css ships dark ANCHOR tokens but does not
// wire them to the consumed tokens; here we remap the consumed vars under data-theme="dark"
// so flipping one attribute on <html> cascades the whole portal. Brand anchors
// (--orange/--red/--green/--blue) are theme-invariant and reused as-is.
const DARK_CSS = 'html[data-theme="dark"]{'
  + '--bg-cream:#0B0D11;--bg-paper:#15181F;--bg-cream-dim:#12151B;'
  + '--ink:#F5EFE3;--ink-soft:#B7C2BF;--ink-mute:#7E8C89;'
  + '--line:#2A3138;--orange-soft:#33240F;--orange-deep:#E8A85C;'
  + '--amber-soft:#2A2210;--amber-ink:#E8C77A;}'
  + 'html[data-theme="dark"] body{background:#0B0D11;}';

function Portal() {
  const [lang, setLang] = useState('en');
  const [user, setUser] = useState(undefined);
  const [scopeErr, setScopeErr] = useState('');
  const t = (k) => (STR[lang] && STR[lang][k]) || STR.en[k] || k;
  useEffect(() => {
    if (!document.getElementById('rada-portal-dark')) {
      const s = document.createElement('style'); s.id = 'rada-portal-dark'; s.textContent = DARK_CSS;
      document.head.appendChild(s);
    }
  }, []);
  useEffect(() => {
    try { if (!firebase.apps.length) firebase.initializeApp(FB_CONFIG); } catch (e) {}
    return firebase.auth().onAuthStateChanged(u => {
      setUser(u || null); setScopeErr('');
      if (u) u.getIdToken().then(tok => fetch(PORTAL_API + '?screen=S1', { headers: { Authorization: 'Bearer ' + tok } })
        .then(r => { if (r.status === 401) setScopeErr(t('denied')); }));
    });
  }, []);
  const wrap = (m) => <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-mute)', background: 'var(--bg-cream)', textAlign: 'center', padding: 20 }}>{m}</div>;
  if (user === undefined) return wrap(t('loading'));
  if (!user) return <Login t={t} />;
  if (scopeErr) return wrap(<span>{scopeErr}{' '}<span style={{ textDecoration: 'underline', cursor: 'pointer' }} onClick={() => firebase.auth().signOut()}>{t('signout')}</span></span>);
  return <Shell user={user} lang={lang} setLang={setLang} t={t} />;
}

ReactDOM.createRoot(document.getElementById('root')).render(<Portal />);
