// Personal Dashboard — owner-only (#personal). Mirrors the Markets Dashboard
// structure but tracks personal life instead of market data.
//
// Step I — section 00 · FINANCIAL:
//   · Spending          — card CSV ingest (Amex UK / Amex US / Chase), Claude
//                          categorization + GBP→USD FX, dynamic table with
//                          notes, personal/work tags, fund/GP split, trip
//                          allocation, filters, and a Claude savings read.
//   · Trips & Reimb.    — work trips; one click emails the trip's expense
//                          summary (grouped Fund / GP) for forwarding into
//                          Xero. Plus a who-owes-me-what IOU tracker.
//   · Net Worth         — assets (auto-priced tickers + manual values, vesting
//                          badges) and liabilities with due dates; Claude read.
//   · Portfolio Overview — the look-through portfolio: statement-ingested
//                          holdings across Morgan Stanley / J.P. Morgan / Schwab
//                          (taxable + IRAs + 401k) plus the VAC and FBC fund
//                          stakes expanded to underlying positions; exposure
//                          views, consolidated position table, and a Claude PM
//                          review (personal-portfolio Edge Function). Fund NAVs
//                          come from Net Worth; ingests sync totals back to it.
//   · Options Strategies — interactive option trees (CBOE delayed data, every
//                          expiration incl. LEAPS, OI + IV + covered-call
//                          yields) for JOBY/AUR plus any prospect ticker; a
//                          self-building IV history (Bloomberg-backfillable);
//                          and a Claude strategy review that respects the
//                          vesting triggers and UK-tax timeline
//                          (options-intel Edge Function).
// Sections 01–04 (Real Estate, Health, CRM, Travel) arrive in later steps.
//
// Data: personal_* tables (owner-only RLS) — see the personal_dashboard
// migration. AI/FX/prices/email run through the `personal-finance` Edge
// Function. See PERSONAL-DASHBOARD-SETUP.md.

// ── Shared bits ───────────────────────────────────────────────────────────────

const PD_CATEGORIES = [
  'Dining', 'Groceries', 'Travel — Flights', 'Travel — Hotels', 'Travel — Other',
  'Transport', 'Subscriptions', 'Shopping', 'Entertainment', 'Health & Fitness',
  'Utilities & Bills', 'Services', 'Fees & Charges', 'Cash & Transfers', 'Other',
];

const PD_ACCOUNTS = {
  amex_uk: { label: 'Amex UK · BA',          currency: 'GBP', dateFmt: 'DMY', sign: 1,  color: '#1A4FB5' },
  amex_us: { label: 'Amex US · MS Platinum', currency: 'USD', dateFmt: 'MDY', sign: 1,  color: '#0E7C3A' },
  chase:   { label: 'Chase Sapphire',        currency: 'USD', dateFmt: 'MDY', sign: -1, color: '#7A2D8E' },
};

const pdLbl = { fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.12em', color: tokens.inkMute, display: 'block', marginBottom: '6px', textTransform: 'uppercase' };
const pdInp = { width: '100%', padding: '8px 10px', border: `1px solid ${tokens.inkLine}`, background: '#FDFBF7', fontFamily: 'inherit', fontSize: '12.5px', color: tokens.ink, boxSizing: 'border-box', outline: 'none' };
const pdBtn = (primary) => ({ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.1em', padding: '8px 14px', cursor: 'pointer', background: primary ? tokens.ink : 'none', color: primary ? tokens.paper : tokens.inkMute, border: `1px solid ${primary ? tokens.ink : tokens.inkLine}` });
const pdTh = { textAlign: 'left', fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.1em', color: tokens.inkMute, fontWeight: 400, padding: '6px 8px', borderBottom: `1px solid ${tokens.inkLine}`, whiteSpace: 'nowrap' };
const pdTd = { padding: '7px 8px', borderBottom: `1px solid ${tokens.inkLineSoft}`, fontSize: '12.5px', verticalAlign: 'top' };

const pdMoney = (v, ccy) => v == null || isNaN(v) ? '—'
  : `${v < 0 ? '−' : ''}${ccy === 'GBP' ? '£' : '$'}${Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const pdUsd0 = (v) => v == null || isNaN(v) ? '—' : `${v < 0 ? '−' : ''}$${Math.abs(v).toLocaleString('en-US', { maximumFractionDigits: 0 })}`;

// USD value actually being submitted for an expense — the partial reimbursable
// amount when set (converted at the txn's stored FX rate), else the full USD.
const pdSubmitUsd = (t) => {
  if (t.reimb_amount == null) return t.amount_usd;
  const rate = t.fx_rate != null ? t.fx_rate : (t.currency === 'USD' ? 1 : null);
  return rate != null ? +(t.reimb_amount * rate).toFixed(2) : null;
};

const pdCollapsed = (key, def = false) => {
  const K = `pd.collapse.${key}`;
  const [c, setC] = React.useState(() => { try { const v = localStorage.getItem(K); return v == null ? def : v === '1'; } catch { return def; } });
  const toggle = () => setC((x) => { try { localStorage.setItem(K, x ? '0' : '1'); } catch {} return !x; });
  return [c, toggle];
};

const PdChevron = ({ collapsed, onClick }) => (
  <span onClick={onClick} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, padding: '0 6px', userSelect: 'none' }}>{collapsed ? '▸' : '▾'}</span>
);

// Collapsible panel shell — same silhouette as the Markets Dashboard panels.
const PdShell = ({ title, right, actions, collapseKey, defaultCollapsed, children }) => {
  const [collapsed, toggle] = pdCollapsed(collapseKey || title, !!defaultCollapsed);
  return (
    <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper, marginTop: '16px', borderRadius: '6px', overflow: 'hidden' }}>
      <div style={{ padding: '12px 20px', borderBottom: collapsed ? 'none' : `1px solid ${tokens.inkLineSoft}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0 }}>
          <PdChevron collapsed={collapsed} onClick={toggle} />
          <span style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.18em', color: tokens.inkMute, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</span>
        </div>
        <div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
          {right && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>{right}</span>}
          {(actions || []).map((a, i) => (
            <button key={i} onClick={a.onClick} disabled={a.running}
              style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.1em', padding: '6px 12px', whiteSpace: 'nowrap', cursor: a.running ? 'default' : 'pointer',
                background: a.running ? 'none' : (a.primary === false ? 'none' : tokens.ink),
                color: a.running ? tokens.inkMute : (a.primary === false ? tokens.inkMute : tokens.paper),
                border: `1px solid ${a.running ? tokens.inkLine : (a.primary === false ? tokens.inkLine : tokens.ink)}` }}>
              {a.running ? 'RUNNING…' : a.label}
            </button>
          ))}
        </div>
      </div>
      {!collapsed && children}
    </div>
  );
};

// Edge function invoke with the error-unwrap pattern used across the site.
const pfInvoke = async (body) => {
  const client = window._supabaseClient;
  if (!client) throw new Error('No backend connected.');
  const { data, error } = await client.functions.invoke('personal-finance', { body });
  let payload = data;
  if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
  if (payload && payload.error) throw new Error(payload.error);
  return payload;
};

// ── Section lock — re-verification gate for sensitive sections ────────────────
// A reusable privacy lock: sensitive sections stay collapsed behind a padlock
// until the owner re-verifies (account password, or an enrolled device passkey /
// Face ID / Touch ID). Unlock state is in-memory only — it clears on reload, on
// leaving the page, and after the tab has been hidden a while. NOTE: the data
// itself is already protected server-side by Supabase Auth + owner-only RLS; this
// gate adds privacy against a glance at an already-signed-in screen, and a real
// biometric/password ceremony on top — it is not a substitute for that RLS.

const pdLocks = (() => {
  const unlocked = new Set();
  const subs = new Set();
  const emit = () => subs.forEach((f) => f());
  return {
    isUnlocked: (id) => unlocked.has(id),
    unlock: (id) => { unlocked.add(id); emit(); },
    lock: (id) => { unlocked.delete(id); emit(); },
    lockAll: () => { if (unlocked.size) { unlocked.clear(); emit(); } },
    subscribe: (f) => { subs.add(f); return () => subs.delete(f); },
  };
})();
const usePdLock = (id) => {
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => pdLocks.subscribe(force), []);
  return pdLocks.isUnlocked(id);
};

// Re-verify the current user's password without disturbing their session data.
const pdVerifyPassword = async (password) => {
  const client = window._supabaseClient;
  if (!client) return { ok: false, error: 'No backend connected.' };
  const { data: { user } } = await client.auth.getUser();
  const email = user && user.email;
  if (!email) return { ok: false, error: 'Not signed in.' };
  const { error } = await client.auth.signInWithPassword({ email, password });
  return error ? { ok: false, error: 'Incorrect password.' } : { ok: true };
};

// ── Device passkey (WebAuthn platform authenticator — Face ID / Touch ID) ─────
const b64uEnc = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const b64uDec = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); const bin = atob(s); const u = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i); return u.buffer; };
const pdPkKey = (email) => `pd.passkey.${(email || '').toLowerCase()}`;
const pdPkEnrolled = (email) => { try { return !!localStorage.getItem(pdPkKey(email)); } catch { return false; } };
const pdPasskeyUsable = () => typeof window !== 'undefined' && !!(window.PublicKeyCredential && navigator.credentials && navigator.credentials.create);
const pdPlatformAvailable = async () => {
  try { return pdPasskeyUsable() && await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); }
  catch { return false; }
};
const pdPasskeyRegister = async (email) => {
  const cred = await navigator.credentials.create({ publicKey: {
    challenge: crypto.getRandomValues(new Uint8Array(32)),
    rp: { name: 'Personal Dashboard', id: location.hostname },
    user: { id: new TextEncoder().encode(email).slice(0, 64), name: email, displayName: email },
    pubKeyCredParams: [{ type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }],
    authenticatorSelection: { authenticatorAttachment: 'platform', userVerification: 'required', residentKey: 'preferred' },
    timeout: 60000, attestation: 'none',
  } });
  if (!cred) throw new Error('Passkey setup was cancelled.');
  localStorage.setItem(pdPkKey(email), b64uEnc(cred.rawId));
};
const pdPasskeyUnlock = async (email) => {
  const id = localStorage.getItem(pdPkKey(email));
  if (!id) throw new Error('No passkey on this device.');
  const assertion = await navigator.credentials.get({ publicKey: {
    challenge: crypto.getRandomValues(new Uint8Array(32)),
    allowCredentials: [{ type: 'public-key', id: b64uDec(id) }],
    userVerification: 'required',
    timeout: 60000,
  } });
  return !!assertion;   // ceremony completed → Face ID / Touch ID passed
};

const PdUnlockModal = ({ label, onUnlock, onCancel }) => {
  const isMobile = useIsMobile();
  const [pw, setPw] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [email, setEmail] = React.useState('');
  const [platformOk, setPlatformOk] = React.useState(false);

  React.useEffect(() => {
    let live = true;
    (async () => {
      const client = window._supabaseClient;
      const { data: { user } } = client ? await client.auth.getUser() : { data: {} };
      const ok = await pdPlatformAvailable();
      if (live) { setEmail((user && user.email) || ''); setPlatformOk(ok); }
    })();
    return () => { live = false; };
  }, []);
  const enrolled = pdPkEnrolled(email);

  const byPassword = async (e) => {
    e.preventDefault();
    if (!pw) return;
    setBusy(true); setErr('');
    const r = await pdVerifyPassword(pw);
    setBusy(false);
    if (r.ok) onUnlock(); else setErr(r.error || 'Verification failed.');
  };
  const byPasskey = async () => {
    setBusy(true); setErr('');
    try { await pdPasskeyUnlock(email); onUnlock(); }
    catch (e) { setErr(e.name === 'NotAllowedError' ? 'Face ID / Touch ID cancelled.' : (e.message || 'Passkey failed.')); }
    finally { setBusy(false); }
  };

  return (
    <div onClick={busy ? undefined : onCancel} style={{ position: 'fixed', inset: 0, background: 'rgba(14,14,12,0.5)', zIndex: 200, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: isMobile ? '18px' : '64px 24px', overflowY: 'auto' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: '420px', background: tokens.bg, border: `1px solid ${tokens.ink}`, borderTop: `4px solid ${tokens.ochre}`, borderRadius: '4px', padding: isMobile ? '22px' : '28px 30px' }}>
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, marginBottom: '6px' }}>🔒 LOCKED</div>
        <div style={{ fontSize: '19px', letterSpacing: '-0.02em', marginBottom: '16px' }}>Verify to view {label}.</div>

        {enrolled && platformOk && (
          <React.Fragment>
            <button onClick={byPasskey} disabled={busy} style={{ ...pdBtn(true), width: '100%', padding: '11px', fontSize: '11px', marginBottom: '12px' }}>
              {busy ? 'VERIFYING…' : '☑ USE FACE ID / TOUCH ID'}
            </button>
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', margin: '4px 0 12px' }}>
              <span style={{ flex: 1, height: '1px', background: tokens.inkLine }} />
              <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>OR PASSWORD</span>
              <span style={{ flex: 1, height: '1px', background: tokens.inkLine }} />
            </div>
          </React.Fragment>
        )}

        <form onSubmit={byPassword}>
          <input type="password" value={pw} onChange={(e) => setPw(e.target.value)} autoFocus={!(enrolled && platformOk)}
            placeholder="Your account password" autoComplete="current-password"
            style={{ ...pdInp, marginBottom: '10px' }} />
          {err && <div style={{ color: tokens.red, fontSize: '12px', marginBottom: '10px' }}>{err}</div>}
          <div style={{ display: 'flex', gap: '8px' }}>
            <button type="submit" disabled={busy || !pw} style={{ ...pdBtn(true), opacity: (busy || !pw) ? 0.5 : 1 }}>{busy ? 'VERIFYING…' : 'UNLOCK'}</button>
            <button type="button" onClick={onCancel} disabled={busy} style={pdBtn(false)}>CANCEL</button>
          </div>
        </form>
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.04em', color: tokens.inkMute, marginTop: '16px', lineHeight: 1.6 }}>
          RE-VERIFICATION FOR PRIVACY · DATA IS ALSO PROTECTED SERVER-SIDE BY YOUR LOGIN
        </div>
      </div>
    </div>
  );
};

// Wrap any sensitive section. Sections sharing an `id` unlock together.
const PdLockGate = ({ id, label, children }) => {
  const unlocked = usePdLock(id);
  const [showModal, setShowModal] = React.useState(false);
  const [email, setEmail] = React.useState('');
  const [platformOk, setPlatformOk] = React.useState(false);
  const [enrollMsg, setEnrollMsg] = React.useState('');
  const [, force] = React.useReducer((x) => x + 1, 0);

  React.useEffect(() => {
    let live = true;
    (async () => {
      const client = window._supabaseClient;
      const { data: { user } } = client ? await client.auth.getUser() : { data: {} };
      const ok = await pdPlatformAvailable();
      if (live) { setEmail((user && user.email) || ''); setPlatformOk(ok); }
    })();
    return () => { live = false; };
  }, []);

  const enroll = async () => {
    setEnrollMsg('');
    try { await pdPasskeyRegister(email); force(); setEnrollMsg('✓ FACE ID ENABLED ON THIS DEVICE'); setTimeout(() => setEnrollMsg(''), 6000); }
    catch (e) { setEnrollMsg(e.name === 'NotAllowedError' ? 'CANCELLED' : (e.message || 'FAILED').toUpperCase()); }
  };

  if (!unlocked) {
    return (
      <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper, marginTop: '16px', borderRadius: '6px', overflow: 'hidden' }}>
        <div style={{ padding: '14px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
          <span style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute }}>🔒 {label.toUpperCase()} · LOCKED</span>
          <button onClick={() => setShowModal(true)} style={pdBtn(true)}>VERIFY TO VIEW</button>
        </div>
        <div style={{ padding: '10px 20px 20px', fontSize: '12.5px', color: tokens.inkSoft }}>
          Hidden for privacy. Verify with your password{platformOk && pdPkEnrolled(email) ? ' or Face ID / Touch ID' : ''} to view {label}.
        </div>
        {showModal && <PdUnlockModal label={label} onCancel={() => setShowModal(false)} onUnlock={() => { pdLocks.unlock(id); setShowModal(false); }} />}
      </div>
    );
  }

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '10px', marginTop: '16px', flexWrap: 'wrap' }}>
        {enrollMsg && <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', color: enrollMsg[0] === '✓' ? tokens.green : tokens.red }}>{enrollMsg}</span>}
        {platformOk && !pdPkEnrolled(email) && (
          <button onClick={enroll} title="Register a device passkey so you can unlock with Face ID / Touch ID next time"
            style={{ ...pdBtn(false), padding: '4px 10px', fontSize: '9px' }}>＋ ENABLE FACE ID</button>
        )}
        <button onClick={() => pdLocks.lock(id)} style={{ ...pdBtn(false), padding: '4px 10px', fontSize: '9px' }}>🔓 {label.toUpperCase()} · LOCK</button>
      </div>
      {children}
    </div>
  );
};

// ── CSV parsing (client-side; the edge fn does dedup + categorize + FX) ───────

const pdParseCsv = (text) => {
  const rows = []; let row = [], cell = '', q = false;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (q) { if (ch === '"') { if (text[i + 1] === '"') { cell += '"'; i++; } else q = false; } else cell += ch; }
    else if (ch === '"') q = true;
    else if (ch === ',') { row.push(cell); cell = ''; }
    else if (ch === '\n' || ch === '\r') { if (ch === '\r' && text[i + 1] === '\n') i++; row.push(cell); rows.push(row); row = []; cell = ''; }
    else cell += ch;
  }
  if (cell !== '' || row.length) { row.push(cell); rows.push(row); }
  return rows.filter((r) => r.some((c) => c.trim() !== ''));
};

const pdParseDate = (s, fmt) => {
  s = (s || '').trim();
  let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (m) return `${m[1]}-${m[2]}-${m[3]}`;
  m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/);
  if (m) {
    let a = +m[1], b = +m[2], y = +m[3]; if (y < 100) y += 2000;
    let day, mon;
    if (fmt === 'DMY') { day = a; mon = b; } else { mon = a; day = b; }
    if (mon > 12 && day <= 12) { const t = mon; mon = day; day = t; }
    if (mon < 1 || mon > 12 || day < 1 || day > 31) return null;
    return `${y}-${String(mon).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  }
  m = s.match(/^(\d{1,2})\s+([A-Za-z]{3})[A-Za-z]*\s+(\d{4})$/);
  if (m) {
    const mon = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'].indexOf(m[2].slice(0, 3).toLowerCase()) + 1;
    if (!mon) return null;
    return `${m[3]}-${String(mon).padStart(2, '0')}-${m[1].padStart(2, '0')}`;
  }
  return null;
};

const pdHash = (str) => { let h = 5381; for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0; return h.toString(36); };

const pdDecodeEntities = (s) => String(s || '')
  .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");

const pdTitleCase = (s) => String(s || '').toLowerCase().replace(/\b[a-z]/g, (c) => c.toUpperCase());

// SheetJS, lazy-loaded from its official CDN only when an .xlsx is picked.
let _pdSheetJs = null;
const pdLoadSheetJs = () => {
  if (window.XLSX) return Promise.resolve(window.XLSX);
  if (_pdSheetJs) return _pdSheetJs;
  _pdSheetJs = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js';
    s.onload = () => resolve(window.XLSX);
    s.onerror = () => { _pdSheetJs = null; reject(new Error('Could not load the spreadsheet library — check the network and retry.')); };
    document.head.appendChild(s);
  });
  return _pdSheetJs;
};

// Which card did this file come from? Amex workbooks name the product in the
// preamble; a Chase CSV is recognizable by its Post Date + Type columns.
const pdSniffAccount = (grid) => {
  const head = grid.slice(0, 10).map((r) => r.map((c) => String(c ?? '')).join(' ')).join('\n').toLowerCase();
  if (/british airways/.test(head)) return 'amex_uk';
  if (/morgan stanley platinum|platinum card/.test(head)) return 'amex_us';
  if (/post date/.test(head) && /\btype\b/.test(head)) return 'chase';
  return null;
};

// Excel stores dates either as text or as day serials (epoch 1899-12-30).
const pdFromSerial = (n) => {
  const d = new Date(Date.UTC(1899, 11, 30) + n * 86400000);
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
};

// statement (CSV text or XLSX grid) → normalized rows for the ingest call.
// Header-driven so column order doesn't matter; the richer Amex XLSX columns
// (Address / City / Country / Reference / Category) are used when present.
const pdExtractRows = (input, accountId) => {
  const acct = PD_ACCOUNTS[accountId];
  const grid = typeof input === 'string' ? pdParseCsv(input) : input;
  const find = (low, names, starts) => {
    let i = low.findIndex((c) => names.includes(c));
    if (i < 0 && starts) i = low.findIndex((c) => starts.some((p) => c.startsWith(p)));
    return i;
  };
  let hdr = null;
  for (let i = 0; i < Math.min(grid.length, 20); i++) {
    const low = grid[i].map((c) => String(c ?? '').trim().toLowerCase());
    const di = find(low, ['transaction date', 'date', 'transaction_date', 'converted on']);
    const ai = find(low, ['billing amount'], ['amount']);
    if (di >= 0 && ai >= 0) {
      const desc = find(low, ['description', 'merchant', 'details', 'narrative']);
      hdr = {
        row: i, di, ai,
        desc: desc >= 0 ? desc : (di === 0 ? 1 : 0),
        type: low.indexOf('type'),
        ref: low.indexOf('reference'),
        city: find(low, ['city/state', 'town/city', 'city']),
        country: low.indexOf('country'),
        cat: low.indexOf('category'),
        ext: low.indexOf('extended details'),
      };
      break;
    }
  }
  if (!hdr) throw new Error("Couldn't find the header row — expected columns like Date / Description / Amount.");

  const out = []; const seen = {}; let skippedPayments = 0;
  for (let i = hdr.row + 1; i < grid.length; i++) {
    const r = grid[i];
    const cell = (j) => j >= 0 && r[j] != null ? String(r[j]) : '';
    const rawDate = r[hdr.di];
    const date = typeof rawDate === 'number' && rawDate > 20000 && rawDate < 60000
      ? pdFromSerial(rawDate) : pdParseDate(cell(hdr.di), acct.dateFmt);
    const desc = pdDecodeEntities(cell(hdr.desc)).replace(/\s+/g, ' ').trim();
    const rawAmt = typeof r[hdr.ai] === 'number' ? r[hdr.ai] : parseFloat(cell(hdr.ai).replace(/[£$,\s]/g, ''));
    if (!date || !desc || !isFinite(rawAmt)) continue;
    const amount = +(rawAmt * acct.sign).toFixed(2);   // spend positive, refunds negative
    const isPayment = (hdr.type >= 0 && /^payment$/i.test(cell(hdr.type).trim()))
      || (/payment received|thank you|autopay|automatic payment|direct debit|online payment/i.test(desc) && amount < 0);
    if (isPayment) { skippedPayments++; continue; }

    // Location straight from the Amex address columns when present.
    let location = '';
    if (hdr.city >= 0) {
      const parts = cell(hdr.city).split('\n').map((x) => x.trim()).filter(Boolean);
      const city = pdTitleCase(parts[0] || '');
      const region = (parts[1] || '').trim();
      const country = cell(hdr.country).trim();
      const suffix = /UNITED KINGDOM/i.test(country) ? 'UK'
        : /UNITED STATES/i.test(country) ? (/^[A-Z]{2}$/.test(region) ? region : 'US')
        : pdTitleCase(country);
      if (city) location = suffix ? `${city}, ${suffix}` : city;
    }

    // Amex's Reference column is a stable per-transaction id → best dedup key.
    const ref = cell(hdr.ref).trim();
    let external_id;
    if (ref) external_id = `${accountId}|ref|${ref}`;
    else {
      const key = `${accountId}|${date}|${amount.toFixed(2)}|${pdHash(desc.toUpperCase())}`;
      seen[key] = (seen[key] || 0) + 1;                // same coffee twice in one day ≠ duplicate
      external_id = `${key}|${seen[key]}`;
    }

    out.push({
      external_id, txn_date: date, description: desc.slice(0, 400), amount,
      ...(location ? { location: location.slice(0, 120) } : {}),
      ...(hdr.cat >= 0 && cell(hdr.cat).trim() ? { hint: cell(hdr.cat).trim().slice(0, 60) } : {}),
      ...(hdr.ext >= 0 && cell(hdr.ext).trim() ? { details: cell(hdr.ext).replace(/\s+/g, ' ').trim().slice(0, 160) } : {}),
    });
  }
  return { rows: out, skippedPayments };
};

// ── CSV upload modal ──────────────────────────────────────────────────────────

const PdUploadModal = ({ onDone, onCancel }) => {
  const isMobile = useIsMobile();
  const [account, setAccount] = React.useState('amex_uk');
  const [autoDetected, setAutoDetected] = React.useState(false);
  const [raw, setRaw] = React.useState(null);         // CSV text or XLSX grid, kept for re-parse on card switch
  const [parsed, setParsed] = React.useState(null);   // {rows, skippedPayments, filename}
  const [busy, setBusy] = React.useState(false);
  const [progress, setProgress] = React.useState('');
  const [err, setErr] = React.useState('');
  const fileRef = React.useRef(null);

  const parseWith = (input, acctId, filename) => {
    const p = pdExtractRows(input, acctId);
    if (!p.rows.length) throw new Error('No transactions found in that file.');
    setParsed({ ...p, filename });
  };

  const onFile = async (f) => {
    if (!f) return;
    setErr(''); setAutoDetected(false);
    try {
      let input;
      if (/\.xlsx?$/i.test(f.name)) {
        const XLSX = await pdLoadSheetJs();
        const wb = XLSX.read(await f.arrayBuffer(), { type: 'array' });
        input = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, raw: true, defval: '' });
      } else {
        input = await f.text();
      }
      const grid = typeof input === 'string' ? pdParseCsv(input) : input;
      const sniffed = pdSniffAccount(grid);
      const acctId = sniffed || account;
      if (sniffed) { setAccount(sniffed); setAutoDetected(true); }
      setRaw({ input, filename: f.name });
      parseWith(input, acctId, f.name);
    } catch (e) { setErr(e.message); setParsed(null); setRaw(null); }
    finally { if (fileRef.current) fileRef.current.value = ''; }
  };

  const switchAccount = (id) => {
    setAccount(id); setAutoDetected(false); setErr('');
    if (raw) { try { parseWith(raw.input, id, raw.filename); } catch (e) { setErr(e.message); setParsed(null); } }
  };

  const ingest = async () => {
    setBusy(true); setErr('');
    try {
      const acct = PD_ACCOUNTS[account];
      let inserted = 0, duplicates = 0;
      for (let i = 0; i < parsed.rows.length; i += 400) {
        setProgress(`Categorizing ${Math.min(i + 400, parsed.rows.length)} / ${parsed.rows.length}…`);
        const res = await pfInvoke({ action: 'categorize', account, currency: acct.currency, rows: parsed.rows.slice(i, i + 400) });
        inserted += res.inserted || 0; duplicates += res.duplicates || 0;
      }
      onDone({ inserted, duplicates });
    } catch (e) { setErr(e.message); setBusy(false); setProgress(''); }
  };

  const dates = parsed ? parsed.rows.map((r) => r.txn_date).sort() : [];
  const total = parsed ? parsed.rows.reduce((a, r) => a + (r.amount > 0 ? r.amount : 0), 0) : 0;

  return (
    <div onClick={busy ? undefined : onCancel} style={{ position: 'fixed', inset: 0, background: 'rgba(14,14,12,0.45)', zIndex: 200, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: isMobile ? '14px' : '48px 24px', overflowY: 'auto' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: '560px', background: tokens.bg, border: `1px solid ${tokens.ink}`, borderTop: `4px solid ${tokens.ochre}`, borderRadius: '4px', padding: isMobile ? '18px' : '26px 30px' }}>
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, marginBottom: '18px' }}>UPLOAD CARD STATEMENT · XLSX OR CSV</div>
        <div style={{ marginBottom: '14px' }}>
          <label style={pdLbl}>CARD {autoDetected && <span style={{ color: tokens.green, letterSpacing: '0.06em' }}>· AUTO-DETECTED FROM FILE</span>}</label>
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
            {Object.entries(PD_ACCOUNTS).map(([id, a]) => (
              <span key={id} onClick={() => switchAccount(id)}
                style={{ fontFamily: fontMono, fontSize: '11px', padding: '7px 13px', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '7px',
                  border: `1px solid ${account === id ? tokens.ink : tokens.inkLine}`, background: account === id ? tokens.ink : 'none', color: account === id ? tokens.paper : tokens.inkMute }}>
                <span style={{ width: '7px', height: '7px', borderRadius: '50%', background: a.color }} />{a.label} · {a.currency}
              </span>
            ))}
          </div>
        </div>
        <input ref={fileRef} type="file" accept=".csv,.xlsx,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" style={{ display: 'none' }} onChange={(e) => onFile(e.target.files[0])} />
        <button onClick={() => fileRef.current && fileRef.current.click()} style={{ ...pdBtn(false), marginBottom: '14px' }}>⬆ CHOOSE FILE — AMEX XLSX / CHASE CSV</button>

        {parsed && (
          <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper, padding: '14px 16px', marginBottom: '14px', fontSize: '13px', lineHeight: 1.7 }}>
            <div style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.1em', color: tokens.inkMute, marginBottom: '6px' }}>{parsed.filename}</div>
            <div><span style={{ color: tokens.inkMute }}>Transactions</span> {parsed.rows.length} · <span style={{ color: tokens.inkMute }}>Span</span> {dates[0]} → {dates[dates.length - 1]}</div>
            <div><span style={{ color: tokens.inkMute }}>Spend total</span> {pdMoney(total, PD_ACCOUNTS[account].currency)}{parsed.skippedPayments ? <span style={{ color: tokens.inkMute }}> · {parsed.skippedPayments} card-payment line{parsed.skippedPayments === 1 ? '' : 's'} skipped</span> : null}</div>
            <div style={{ fontSize: '12px', color: tokens.inkSoft, marginTop: '6px' }}>On confirm: duplicates are skipped automatically, Claude categorizes each merchant{PD_ACCOUNTS[account].currency === 'GBP' ? ', and GBP converts to USD at each date’s ECB rate' : ''}.</div>
          </div>
        )}
        {err && <div style={{ color: tokens.red, fontSize: '12px', marginBottom: '12px' }}>{err}</div>}
        {progress && <div style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.08em', color: tokens.inkMute, marginBottom: '12px' }}>{progress.toUpperCase()}</div>}
        <div style={{ display: 'flex', gap: '8px' }}>
          <button onClick={ingest} disabled={!parsed || busy} style={{ ...pdBtn(true), opacity: !parsed || busy ? 0.5 : 1 }}>{busy ? 'INGESTING…' : 'CONFIRM & INGEST'}</button>
          <button onClick={onCancel} disabled={busy} style={pdBtn(false)}>CANCEL</button>
        </div>
      </div>
    </div>
  );
};

// ── Claude insights block (spending / networth) ───────────────────────────────

const PdInsights = ({ scope, emptyHint }) => {
  const [row, setRow] = React.useState(undefined);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  const load = React.useCallback(async () => {
    const client = window._supabaseClient;
    if (!client) { setRow(null); return; }
    const { data } = await client.from('personal_insights').select('data, generated_at').eq('scope', scope).maybeSingle();
    setRow(data || null);
  }, [scope]);
  React.useEffect(() => { load(); }, [load]);

  const run = async () => {
    setBusy(true); setErr('');
    try { await pfInvoke({ action: 'insights', scope }); await load(); }
    catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const [folded, toggleFold] = pdCollapsed(`fold.insights.${scope}`, false);
  if (row === undefined) return null;
  const d = (row && row.data) || null;
  const secLbl = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '14px 0 6px' };

  return (
    <div style={{ borderTop: `1px solid ${tokens.inkLine}`, marginTop: '18px', paddingTop: '14px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
        <span onClick={toggleFold} title={folded ? 'Show the Claude output' : 'Hide the Claude output'} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.16em', color: tokens.inkMute, cursor: 'pointer', userSelect: 'none' }}>
          {folded ? '▸' : '▾'} CLAUDE READ{row && row.generated_at ? ` · ${new Date(row.generated_at).toLocaleDateString()}` : ''}{folded && d ? ' · HIDDEN' : ''}
        </span>
        <button onClick={run} disabled={busy} style={pdBtn(false)}>{busy ? 'ANALYZING…' : (d ? '↻ RE-RUN' : '⚡ ANALYZE')}</button>
      </div>
      {err && <div style={{ color: tokens.red, fontSize: '12px', marginTop: '8px' }}>{err}</div>}
      {!d && !err && !folded && <div style={{ fontSize: '12.5px', color: tokens.inkSoft, marginTop: '8px' }}>{emptyHint}</div>}
      {d && !folded && (
        <div style={{ marginTop: '10px' }}>
          {d.headline && <div style={{ fontSize: '14.5px', lineHeight: 1.55, letterSpacing: '-0.005em' }}>{d.headline}</div>}
          {(d.monthly_read || d.allocation_read) && <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6, marginTop: '8px' }}>{d.monthly_read || d.allocation_read}</div>}
          {d.liquidity_read && <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6, marginTop: '6px' }}>{d.liquidity_read}</div>}
          {Array.isArray(d.savings_ideas) && d.savings_ideas.length > 0 && (<div>
            <div style={secLbl}>SAVINGS MOVES</div>
            {d.savings_ideas.map((x, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '22px 1fr auto', gap: '8px', padding: '5px 0', fontSize: '13px', lineHeight: 1.5, alignItems: 'baseline' }}>
                <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre }}>{String(i + 1).padStart(2, '0')}</span>
                <span><span style={{ fontWeight: 500 }}>{x.title}</span> — <span style={{ color: tokens.inkSoft }}>{x.detail}</span></span>
                {x.est_monthly_usd ? <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.green, whiteSpace: 'nowrap' }}>~{pdUsd0(x.est_monthly_usd)}/mo</span> : <span />}
              </div>
            ))}
          </div>)}
          {Array.isArray(d.subscriptions) && d.subscriptions.length > 0 && (<div>
            <div style={secLbl}>SUBSCRIPTIONS</div>
            {d.subscriptions.map((s, i) => (
              <div key={i} style={{ display: 'flex', gap: '10px', alignItems: 'baseline', padding: '4px 0', fontSize: '12.5px', flexWrap: 'wrap' }}>
                <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', border: `1px solid ${s.verdict === 'cut' ? tokens.red : s.verdict === 'keep' ? tokens.green : tokens.ochre}`, color: s.verdict === 'cut' ? tokens.red : s.verdict === 'keep' ? tokens.green : tokens.ochre }}>{(s.verdict || 'review').toUpperCase()}</span>
                <span style={{ fontWeight: 500 }}>{s.merchant}</span>
                {s.est_monthly_usd ? <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>~{pdUsd0(s.est_monthly_usd)}/mo</span> : null}
                <span style={{ color: tokens.inkSoft }}>{s.why}</span>
              </div>
            ))}
          </div>)}
          {Array.isArray(d.concentration_risks) && d.concentration_risks.length > 0 && (<div>
            <div style={secLbl}>CONCENTRATION RISKS</div>
            {d.concentration_risks.map((r, i) => <div key={i} style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.55, padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {r}</div>)}
          </div>)}
          {Array.isArray(d.actions) && d.actions.length > 0 && (<div>
            <div style={secLbl}>ACTIONS</div>
            {d.actions.map((x, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '22px 1fr', gap: '8px', padding: '4px 0', fontSize: '13px', lineHeight: 1.5 }}>
                <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre }}>{String(i + 1).padStart(2, '0')}</span>
                <span><span style={{ fontWeight: 500 }}>{x.title}</span>{x.detail ? <span style={{ color: tokens.inkSoft }}> — {x.detail}</span> : null}</span>
              </div>
            ))}
          </div>)}
          {Array.isArray(d.watch_items) && d.watch_items.length > 0 && (<div>
            <div style={secLbl}>WATCH</div>
            {d.watch_items.map((w, i) => <div key={i} style={{ fontSize: '12.5px', color: tokens.inkSoft, lineHeight: 1.5, padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}
          </div>)}
        </div>
      )}
    </div>
  );
};

// ── Spending panel ────────────────────────────────────────────────────────────

// Inline note shared by the spend table and the trip expense editor. Shows the
// saved note as plain text with a ✎; the input only appears on click and saves
// on blur / Enter (Escape cancels).
const PdNoteInput = ({ value, onSave, width = 150, placeholder = 'Note…' }) => {
  const [editing, setEditing] = React.useState(false);
  const [v, setV] = React.useState(value || '');
  const cancelled = React.useRef(false);
  React.useEffect(() => { setV(value || ''); }, [value]);

  if (!editing) {
    return (
      <span onClick={() => { setV(value || ''); setEditing(true); }} title={value ? `${value} — click ✎ to edit` : 'Add a note'}
        style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'baseline', gap: '6px', maxWidth: `${width + 20}px` }}>
        {value && <span style={{ fontSize: '11.5px', color: tokens.inkSoft, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: `${width}px`, display: 'inline-block', verticalAlign: 'bottom' }}>{value}</span>}
        <span style={{ fontFamily: fontMono, fontSize: '11px', color: value ? tokens.inkMute : tokens.inkLine, flexShrink: 0 }}>✎</span>
      </span>
    );
  }
  const done = () => {
    if (!cancelled.current && v !== (value || '')) onSave(v);
    cancelled.current = false;
    setEditing(false);
  };
  return (
    <input value={v} onChange={(e) => setV(e.target.value)} onBlur={done} autoFocus
      onKeyDown={(e) => {
        if (e.key === 'Enter') e.target.blur();
        if (e.key === 'Escape') { cancelled.current = true; e.target.blur(); }
      }}
      placeholder={placeholder}
      style={{ ...pdInp, width: `${width}px`, padding: '4px 8px', fontSize: '12px' }} />
  );
};

// Inline amount editor — shows formatted money with a ✎; click to edit the raw
// number (blur / Enter saves, Escape cancels). Mirrors PdNoteInput.
const PdAmountInput = ({ amount, currency, onSave, width = 96 }) => {
  const [editing, setEditing] = React.useState(false);
  const [v, setV] = React.useState('');
  const cancelled = React.useRef(false);
  if (!editing) {
    return (
      <span onClick={() => { setV(amount != null ? String(amount) : ''); setEditing(true); }} title="Click to edit the amount"
        style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'baseline', gap: '6px', justifyContent: 'flex-end' }}>
        <span style={{ fontFamily: fontMono, fontSize: '11.5px' }}>{pdMoney(amount, currency)}</span>
        <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>✎</span>
      </span>
    );
  }
  const done = () => {
    if (!cancelled.current) {
      const n = parseFloat(String(v).replace(/[£$,\s]/g, ''));
      if (isFinite(n) && n >= 0 && n !== amount) onSave(n);
    }
    cancelled.current = false; setEditing(false);
  };
  return (
    <input value={v} onChange={(e) => setV(e.target.value)} onBlur={done} autoFocus
      onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') { cancelled.current = true; e.target.blur(); } }}
      style={{ ...pdInp, width: `${width}px`, padding: '4px 8px', fontSize: '12px', fontFamily: fontMono, textAlign: 'right' }} />
  );
};

const PdTxnRow = ({ t, trips, onPatch, onDelete }) => {
  const acct = PD_ACCOUNTS[t.account] || { label: t.account, color: tokens.inkMute };
  const tagBtn = (id, label, color) => (
    <span onClick={() => onPatch(t.id, { tag: t.tag === id ? null : id, ...(id !== 'work' && t.tag !== id ? { work_type: null, trip_id: null } : {}) })}
      title={id === 'work' ? 'Work reimbursable' : 'Personal reimbursement'}
      style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 7px', cursor: 'pointer',
        border: `1px solid ${t.tag === id ? color : tokens.inkLine}`, background: t.tag === id ? color : 'none', color: t.tag === id ? tokens.paper : tokens.inkMute }}>
      {label}
    </span>
  );
  return (
    <React.Fragment>
      <tr>
        <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10.5px', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{t.txn_date}</td>
        <td style={pdTd}><span title={acct.label} style={{ width: '8px', height: '8px', borderRadius: '50%', background: acct.color, display: 'inline-block' }} /></td>
        <td style={{ ...pdTd, minWidth: '160px' }}>
          <div style={{ fontWeight: 500, letterSpacing: '-0.01em' }}>{t.merchant || t.description}{t.is_subscription && <span title="Subscription" style={{ fontFamily: fontMono, fontSize: '8.5px', color: tokens.ochre, marginLeft: '6px' }}>⟳ SUB</span>}</div>
          {t.merchant && t.description && t.merchant !== t.description && <div style={{ fontSize: '10.5px', color: tokens.inkMute, marginTop: '1px', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '260px', whiteSpace: 'nowrap' }}>{t.description}</div>}
        </td>
        <td style={pdTd}>
          <select value={t.category} onChange={(e) => onPatch(t.id, { category: e.target.value })}
            style={{ ...pdInp, width: 'auto', padding: '3px 6px', fontSize: '11px', background: 'none', border: `1px solid ${tokens.inkLineSoft}` }}>
            {[...new Set([t.category, ...PD_CATEGORIES])].map((c) => <option key={c} value={c}>{c}</option>)}
          </select>
        </td>
        <td style={{ ...pdTd, fontSize: '11.5px', color: tokens.inkSoft, whiteSpace: 'nowrap' }}>{t.location}</td>
        <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap' }}>{pdMoney(t.amount, t.currency)}</td>
        <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{t.currency === 'USD' ? '' : pdMoney(t.amount_usd, 'USD')}</td>
        <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
          <span style={{ display: 'inline-flex', gap: '4px' }}>
            {tagBtn('work', 'W', tokens.green)}
            {tagBtn('personal', 'P', '#3D5AA8')}
          </span>
        </td>
        <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
          {t.tag === 'work' && (
            <span style={{ display: 'inline-flex', gap: '4px' }}>
              {['fund', 'gp'].map((w) => (
                <span key={w} onClick={() => onPatch(t.id, { work_type: t.work_type === w ? null : w })}
                  style={{ fontFamily: fontMono, fontSize: '9px', padding: '2px 7px', cursor: 'pointer', letterSpacing: '0.06em',
                    border: `1px solid ${t.work_type === w ? tokens.ochre : tokens.inkLine}`, background: t.work_type === w ? tokens.ochre : 'none', color: t.work_type === w ? tokens.paper : tokens.inkMute }}>
                  {w.toUpperCase()}
                </span>
              ))}
            </span>
          )}
        </td>
        <td style={pdTd}>
          {t.tag && (
            <select value={t.trip_id || ''} onChange={(e) => onPatch(t.id, { trip_id: e.target.value || null })}
              style={{ ...pdInp, width: 'auto', maxWidth: '130px', padding: '3px 6px', fontSize: '10.5px', background: 'none', border: `1px solid ${tokens.inkLineSoft}` }}>
              <option value="">{t.tag === 'work' ? '— trip —' : '— event —'}</option>
              {trips.filter((tr) => (t.tag === 'work' ? (tr.kind || 'work') === 'work' : tr.kind === 'personal')).map((tr) => <option key={tr.id} value={tr.id}>{tr.name}</option>)}
            </select>
          )}
        </td>
        <td style={pdTd}>
          <PdNoteInput value={t.notes} onSave={(v) => onPatch(t.id, { notes: v })} width={150} />
        </td>
        <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
          <span onClick={() => { if (window.confirm('Delete this transaction?')) onDelete(t.id); }} title="Delete" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
        </td>
      </tr>
    </React.Fragment>
  );
};

const PdSpendingPanel = ({ txns, trips, reload, patchTxn, removeTxn }) => {
  const isMobile = useIsMobile();
  const [showUpload, setShowUpload] = React.useState(false);
  const [flash, setFlash] = React.useState('');
  const [fAccount, setFAccount] = React.useState('all');
  const [fCategory, setFCategory] = React.useState('all');
  const [fTag, setFTag] = React.useState('all');
  const [fMonth, setFMonth] = React.useState('all');   // 'all' | 'YYYY-MM'
  const [fFrom, setFFrom] = React.useState('');         // custom range overrides
  const [fTo, setFTo] = React.useState('');
  const [q, setQ] = React.useState('');
  const [page, setPage] = React.useState(0);
  const PAGE_SIZE = 100;
  // Whenever the filters change, jump back to the first page so you're never
  // stranded on an empty page.
  React.useEffect(() => { setPage(0); }, [fAccount, fCategory, fTag, fMonth, fFrom, fTo, q]);

  const patch = patchTxn;   // in-place: tagging never reorders or flickers the table
  const del = removeTxn;

  const list = (txns || []).filter((t) => {
    if (fAccount !== 'all' && t.account !== fAccount) return false;
    if (fCategory !== 'all' && t.category !== fCategory) return false;
    if (fTag === 'work' && t.tag !== 'work') return false;
    if (fTag === 'personal' && t.tag !== 'personal') return false;
    if (fTag === 'untagged' && t.tag) return false;
    if (fTag === 'subs' && !t.is_subscription) return false;
    if (fFrom || fTo) {                                   // custom range wins over month
      if (fFrom && t.txn_date < fFrom) return false;
      if (fTo && t.txn_date > fTo) return false;
    } else if (fMonth !== 'all' && t.txn_date.slice(0, 7) !== fMonth) return false;
    if (q.trim()) {
      const hay = `${t.merchant} ${t.description} ${t.category} ${t.location} ${t.notes}`.toLowerCase();
      if (!q.toLowerCase().split(/\s+/).every((w) => hay.includes(w))) return false;
    }
    return true;
  });

  const months = [...new Set((txns || []).map((t) => t.txn_date.slice(0, 7)))].sort().reverse();
  const monthLabel = (m) => new Date(`${m}-15T00:00:00`).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
  const dateActive = fMonth !== 'all' || fFrom || fTo;

  // Client-side paging over the filtered list (all data is already in memory, so
  // paging never refetches and never touches in-progress edits).
  const pageCount = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
  const curPage = Math.min(page, pageCount - 1);
  const pageStart = curPage * PAGE_SIZE;
  const visible = list.slice(pageStart, pageStart + PAGE_SIZE);

  // Summary strip off the loaded window.
  const ym = (d) => d.slice(0, 7);
  const now = new Date();
  const thisM = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  const lastM = ym(new Date(now.getFullYear(), now.getMonth() - 1, 15).toISOString());
  const sum = (pred) => (txns || []).reduce((a, t) => a + (pred(t) && t.amount_usd != null && t.amount_usd > 0 ? t.amount_usd : 0), 0);
  const tiles = [
    ['THIS MONTH', sum((t) => ym(t.txn_date) === thisM)],
    ['LAST MONTH', sum((t) => ym(t.txn_date) === lastM)],
    ['YTD', sum((t) => t.txn_date.slice(0, 4) === String(now.getFullYear()))],
    ['FILTERED', list.reduce((a, t) => a + (t.amount_usd || 0), 0)],
  ];

  const chip = (on, label, cb, color) => (
    <span key={label} onClick={cb} style={{ fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.06em', padding: '5px 10px', cursor: 'pointer',
      display: 'inline-flex', alignItems: 'center', gap: '6px',
      border: `1px solid ${on ? tokens.ink : tokens.inkLine}`, background: on ? tokens.ink : 'none', color: on ? tokens.paper : tokens.inkSoft }}>
      {color && <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: color }} />}{label}
    </span>
  );

  return (
    <PdShell collapseKey="pd.spending" title="SPENDING · ALL CARDS"
      right={txns ? `${txns.length} TXNS LOADED` : ''}
      actions={[{ label: '⬆ UPLOAD CSV', onClick: () => setShowUpload(true) }]}>
      <div style={{ padding: '16px 20px' }}>
        {flash && <div style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.08em', color: tokens.green, marginBottom: '10px' }}>{flash}</div>}

        {/* summary tiles */}
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(4, 1fr)', gap: '10px', marginBottom: '16px' }}>
          {tiles.map(([k, v]) => (
            <div key={k} style={{ border: `1px solid ${tokens.inkLine}`, padding: '10px 14px', background: tokens.bg }}>
              <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '4px' }}>{k}</div>
              <div style={{ fontSize: '19px', letterSpacing: '-0.02em', fontFamily: fontDisplay }}>{pdUsd0(v)}</div>
            </div>
          ))}
        </div>

        {/* filters */}
        <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '12px' }}>
          {chip(fAccount === 'all', 'ALL CARDS', () => setFAccount('all'))}
          {Object.entries(PD_ACCOUNTS).map(([id, a]) => chip(fAccount === id, a.label.toUpperCase(), () => setFAccount(fAccount === id ? 'all' : id), a.color))}
          <span style={{ width: '10px' }} />
          {chip(fTag === 'work', 'WORK', () => setFTag(fTag === 'work' ? 'all' : 'work'))}
          {chip(fTag === 'personal', 'PERSONAL', () => setFTag(fTag === 'personal' ? 'all' : 'personal'))}
          {chip(fTag === 'untagged', 'UNTAGGED', () => setFTag(fTag === 'untagged' ? 'all' : 'untagged'))}
          {chip(fTag === 'subs', '⟳ SUBS', () => setFTag(fTag === 'subs' ? 'all' : 'subs'))}
          <select value={fCategory} onChange={(e) => setFCategory(e.target.value)} style={{ ...pdInp, width: 'auto', padding: '5px 8px', fontSize: '11px' }}>
            <option value="all">All categories</option>
            {PD_CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}
          </select>
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search…" style={{ ...pdInp, flex: '1 1 140px', minWidth: '120px', padding: '5px 9px', fontSize: '12px' }} />
        </div>

        {/* date filters — month chips-by-select plus an overriding custom range */}
        <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '12px' }}>
          <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute }}>DATES</span>
          <select value={fMonth} onChange={(e) => { setFMonth(e.target.value); setFFrom(''); setFTo(''); }}
            disabled={!!(fFrom || fTo)} style={{ ...pdInp, width: 'auto', padding: '5px 8px', fontSize: '11px', opacity: (fFrom || fTo) ? 0.5 : 1 }}>
            <option value="all">All months</option>
            {months.map((m) => <option key={m} value={m}>{monthLabel(m)}</option>)}
          </select>
          <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>OR</span>
          <input type="date" value={fFrom} onChange={(e) => setFFrom(e.target.value)} title="From"
            style={{ ...pdInp, width: 'auto', padding: '4px 8px', fontSize: '11px', fontFamily: fontMono }} />
          <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>→</span>
          <input type="date" value={fTo} onChange={(e) => setFTo(e.target.value)} title="To"
            style={{ ...pdInp, width: 'auto', padding: '4px 8px', fontSize: '11px', fontFamily: fontMono }} />
          {dateActive && (
            <span onClick={() => { setFMonth('all'); setFFrom(''); setFTo(''); }}
              style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.08em', color: tokens.red, cursor: 'pointer', border: `1px solid ${tokens.inkLine}`, padding: '4px 10px' }}>
              ✕ CLEAR DATES
            </span>
          )}
        </div>

        {/* table */}
        {(!txns || txns.length === 0) ? (
          <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.7, padding: '16px 0' }}>
            No spend ingested yet. Click <span style={{ color: tokens.ink }}>⬆ UPLOAD CSV</span> and drop in a statement export from Amex UK, Amex US or Chase —
            duplicates are skipped, Claude categorizes every merchant, and GBP converts to USD at each transaction date's rate. Needs the <span style={{ color: tokens.ink }}>personal-finance</span> function deployed.
          </div>
        ) : (
          <div style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '1020px' }}>
              <thead><tr>
                {['DATE', '', 'MERCHANT', 'CATEGORY', 'LOCATION'].map((h) => <th key={h} style={pdTh}>{h}</th>)}
                {['AMOUNT', 'USD'].map((h) => <th key={h} style={{ ...pdTh, textAlign: 'right' }}>{h}</th>)}
                {['TAG', 'FUND/GP', 'TRIP', 'NOTE', ''].map((h, i) => <th key={i} style={pdTh}>{h}</th>)}
              </tr></thead>
              <tbody>
                {visible.map((t) => <PdTxnRow key={t.id} t={t} trips={trips || []} onPatch={patch} onDelete={del} />)}
              </tbody>
            </table>
            {list.length === 0 && <div style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, letterSpacing: '0.08em', padding: '18px 0' }}>NO MATCHES</div>}
            {list.length > 0 && (
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', flexWrap: 'wrap', marginTop: '12px' }}>
                <span style={{ fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.06em', color: tokens.inkMute }}>
                  {pageStart + 1}–{Math.min(pageStart + PAGE_SIZE, list.length)} OF {list.length}
                </span>
                {pageCount > 1 && (
                  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
                    <button onClick={() => setPage(0)} disabled={curPage === 0} style={{ ...pdBtn(false), padding: '5px 9px', fontSize: '9px', opacity: curPage === 0 ? 0.4 : 1 }}>« FIRST</button>
                    <button onClick={() => setPage(curPage - 1)} disabled={curPage === 0} style={{ ...pdBtn(false), padding: '5px 10px', fontSize: '9px', opacity: curPage === 0 ? 0.4 : 1 }}>‹ PREV</button>
                    <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ink, padding: '0 6px' }}>PAGE {curPage + 1} / {pageCount}</span>
                    <button onClick={() => setPage(curPage + 1)} disabled={curPage >= pageCount - 1} style={{ ...pdBtn(false), padding: '5px 10px', fontSize: '9px', opacity: curPage >= pageCount - 1 ? 0.4 : 1 }}>NEXT ›</button>
                    <button onClick={() => setPage(pageCount - 1)} disabled={curPage >= pageCount - 1} style={{ ...pdBtn(false), padding: '5px 9px', fontSize: '9px', opacity: curPage >= pageCount - 1 ? 0.4 : 1 }}>LAST »</button>
                  </div>
                )}
              </div>
            )}
          </div>
        )}

        <PdInsights scope="spending" emptyHint="Run the analyzer for a Claude read over the last 120 days — savings moves ranked by impact and which subscriptions to cut." />
      </div>
      {showUpload && <PdUploadModal onCancel={() => setShowUpload(false)} onDone={async (r) => { setShowUpload(false); setFlash(`✓ INGESTED ${r.inserted} — ${r.duplicates} DUPLICATE${r.duplicates === 1 ? '' : 'S'} SKIPPED`); await reload(); setTimeout(() => setFlash(''), 8000); }} />}
    </PdShell>
  );
};

// ── Trips & reimbursements panel ──────────────────────────────────────────────

// One standalone-IOU row: inline-editable email (saved on blur / Enter) plus the
// ✉ REMIND button that fires the "Urds' AI bot" payment reminder.
const PdIouRow = ({ r, statusChip, onCycle, onSaveEmail, onSaveDesc, onSaveAmount, onRemind, reminding, onDelete }) => {
  const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((r.email || '').trim());
  return (
    <tr>
      <td style={{ ...pdTd, fontWeight: 500 }}>{r.counterparty}</td>
      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10.5px', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{r.created_at ? new Date(r.created_at).toLocaleDateString() : ''}</td>
      <td style={{ ...pdTd, color: tokens.inkSoft }}>
        <PdNoteInput value={r.description} onSave={(v) => onSaveDesc(r.id, v)} width={180} placeholder="What it's for…" />
      </td>
      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10px' }}>{r.kind.toUpperCase()}</td>
      <td style={{ ...pdTd, textAlign: 'right', whiteSpace: 'nowrap' }}><PdAmountInput amount={r.amount} currency={r.currency} onSave={(n) => onSaveAmount(r, n)} /></td>
      <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
        <PdNoteInput value={r.email} onSave={(v) => onSaveEmail(r.id, v)} width={150} placeholder="them@email.com" />
      </td>
      <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
        {statusChip(r.status, onCycle)}
        {r.reminded_at && <div style={{ fontFamily: fontMono, fontSize: '8.5px', color: tokens.inkMute, marginTop: '3px' }}>REMINDED {new Date(r.reminded_at).toLocaleDateString()}</div>}
      </td>
      <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
        <button onClick={onRemind} disabled={reminding || !emailOk || r.status === 'paid'}
          title={r.status === 'paid' ? 'Already paid' : !emailOk ? 'Add an email address first (✎ in the EMAIL column)' : 'Send the payment reminder'}
          style={{ ...pdBtn(false), padding: '4px 10px', fontSize: '9px', opacity: (!emailOk || r.status === 'paid') ? 0.4 : 1 }}>
          {reminding ? 'SENDING…' : '✉ REMIND'}
        </button>
        <span onClick={onDelete} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine, marginLeft: '8px' }}>✕</span>
      </td>
    </tr>
  );
};

// Numeric input for the partial reimbursable amount — blank means "reimburse the
// full expense". Saves on blur / Enter; amber while unsaved.
const PdReimbInput = ({ t, onPatch }) => {
  const cur = t.reimb_amount != null ? String(t.reimb_amount) : '';
  const [v, setV] = React.useState(cur);
  React.useEffect(() => { setV(t.reimb_amount != null ? String(t.reimb_amount) : ''); }, [t.reimb_amount]);
  const dirty = v.trim() !== cur;
  const save = () => {
    if (!dirty) return;
    const s = v.trim().replace(/[£$,]/g, '');
    if (s === '') { onPatch(t.id, { reimb_amount: null }); return; }
    const n = parseFloat(s);
    if (isFinite(n) && n >= 0) onPatch(t.id, { reimb_amount: n });
    else setV(cur);   // invalid → revert
  };
  return (
    <input value={v} onChange={(e) => setV(e.target.value)} onBlur={save}
      onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
      placeholder={String(t.amount)} title="Amount to submit — blank = the full expense"
      style={{ ...pdInp, width: '78px', padding: '3px 7px', fontSize: '11px', fontFamily: fontMono, textAlign: 'right',
        background: 'none', border: `1px solid ${dirty ? tokens.ochre : tokens.inkLineSoft}` }} />
  );
};

// One expense allocated to a trip: fund/GP, full vs reimbursable amount, and the
// note (same field as the spend table — edit in either place).
const PdTripExpRow = ({ t, onPatch, onToXero }) => {
  const partial = t.reimb_amount != null && t.reimb_amount !== t.amount;
  return (
    <tr>
      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10.5px', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{t.txn_date}</td>
      <td style={pdTd}>
        <div style={{ fontWeight: 500 }}>{t.merchant || t.description}</div>
        <div style={{ fontSize: '10px', color: tokens.inkMute }}>{t.category}{t.location ? ` · ${t.location}` : ''}</div>
      </td>
      <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
        {['fund', 'gp'].map((w) => (
          <span key={w} onClick={() => onPatch(t.id, { work_type: t.work_type === w ? null : w })}
            style={{ fontFamily: fontMono, fontSize: '9px', padding: '2px 7px', cursor: 'pointer', letterSpacing: '0.06em', marginRight: '4px',
              border: `1px solid ${t.work_type === w ? tokens.ochre : tokens.inkLine}`, background: t.work_type === w ? tokens.ochre : 'none', color: t.work_type === w ? tokens.paper : tokens.inkMute }}>
            {w.toUpperCase()}
          </span>
        ))}
      </td>
      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap', color: partial ? tokens.inkMute : tokens.ink, textDecoration: partial ? 'line-through' : 'none' }}>{pdMoney(t.amount, t.currency)}</td>
      <td style={{ ...pdTd, textAlign: 'right' }}><PdReimbInput t={t} onPatch={onPatch} /></td>
      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap' }}>
        {pdMoney(pdSubmitUsd(t), 'USD')}{partial && <span title="Partial — only part of this expense is being submitted" style={{ color: tokens.ochre }}> ◐</span>}
      </td>
      <td style={pdTd}><PdNoteInput value={t.notes} onSave={(v) => onPatch(t.id, { notes: v })} width={170} placeholder="Note for the summary…" /></td>
      <td style={{ ...pdTd, whiteSpace: 'nowrap' }}>
        {onToXero && <span onClick={() => onToXero(t)} title="Mark as submitted in Xero — moves it to the 'Already in Xero' bucket and out of the amount to submit"
          style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.04em', color: tokens.green, border: `1px solid ${tokens.green}`, padding: '2px 7px', marginRight: '8px' }}>→ XERO</span>}
        <span onClick={() => onPatch(t.id, { trip_id: null })} title="Remove from this trip" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
      </td>
    </tr>
  );
};

// Split one covered expense across people — each share carries a name, email
// (✎ to edit), amount in the expense's currency, an OPEN/PAID toggle, and the
// same Urds'-AI-bot ✉ REMIND as the IOUs.
const PdSplitEditor = ({ txn, splits, onChanged }) => {
  const [draft, setDraft] = React.useState({ person: '', email: '', amount: '' });
  const [remindingId, setRemindingId] = React.useState(null);
  const [err, setErr] = React.useState('');
  const [msg, setMsg] = React.useState('');
  const client = () => window._supabaseClient;

  const sum = splits.reduce((a, s) => a + (s.amount || 0), 0);
  const remaining = +(txn.amount - sum).toFixed(2);

  const add = async (e) => {
    e.preventDefault();
    const amt = parseFloat(String(draft.amount).replace(/[£$,]/g, ''));
    if (!draft.person.trim() || !isFinite(amt) || amt <= 0) return;
    await client().from('personal_splits').insert({ txn_id: txn.id, person: draft.person.trim(), email: draft.email.trim() || null, amount: amt, currency: txn.currency });
    setDraft({ person: '', email: '', amount: '' });
    await onChanged();
  };
  const patch = async (id, p) => { await client().from('personal_splits').update(p).eq('id', id); await onChanged(); };
  const del = async (id) => { await client().from('personal_splits').delete().eq('id', id); await onChanged(); };
  const remind = async (s) => {
    if (!window.confirm(`Send the payment reminder to ${s.person} (${s.email})?\n\n${pdMoney(s.amount, s.currency)} — their share of ${txn.notes || txn.merchant}. From "Urds' AI Bot", replies to daniel@urdaneta.io.`)) return;
    setRemindingId(s.id); setErr(''); setMsg('');
    try { const r = await pfInvoke({ action: 'send_reminder', split_id: s.id }); setMsg(`✓ SENT TO ${String(r.to).toUpperCase()}`); await onChanged(); }
    catch (e) { setErr(e.message); }
    finally { setRemindingId(null); }
  };

  return (
    <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bgAlt, padding: '12px 14px', margin: '4px 0 10px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', gap: '10px', flexWrap: 'wrap', marginBottom: '8px' }}>
        <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.12em', color: tokens.inkMute }}>
          SPLIT — {pdMoney(sum, txn.currency)} OF {pdMoney(txn.amount, txn.currency)} ALLOCATED
        </span>
        <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', color: remaining === 0 ? tokens.green : remaining < 0 ? tokens.red : tokens.ochre }}>
          {remaining === 0 ? '✓ FULLY ALLOCATED' : remaining < 0 ? `OVER BY ${pdMoney(-remaining, txn.currency)}` : `${pdMoney(remaining, txn.currency)} UNASSIGNED`}
        </span>
      </div>
      {msg && <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.green, marginBottom: '6px' }}>{msg}</div>}
      {err && <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.red, marginBottom: '6px' }}>{err}</div>}

      {splits.map((s) => {
        const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((s.email || '').trim());
        return (
          <div key={s.id} style={{ display: 'flex', gap: '12px', alignItems: 'baseline', flexWrap: 'wrap', padding: '5px 0', borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
            <span style={{ fontSize: '13px', fontWeight: 500, flex: '0 0 110px' }}>{s.person}</span>
            <PdNoteInput value={s.email} onSave={(v) => patch(s.id, { email: v.trim() || null })} width={140} placeholder="their@email.com" />
            <span style={{ fontFamily: fontMono, fontSize: '11.5px', flex: '0 0 76px', textAlign: 'right' }}>{pdMoney(s.amount, s.currency)}</span>
            <span onClick={() => patch(s.id, { status: s.status === 'paid' ? 'open' : 'paid' })}
              style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', cursor: 'pointer',
                border: `1px solid ${s.status === 'paid' ? tokens.green : tokens.inkLine}`, color: s.status === 'paid' ? tokens.green : tokens.inkMute }}>
              {s.status === 'paid' ? '✓ PAID' : 'OPEN'}
            </span>
            {s.reminded_at && <span style={{ fontFamily: fontMono, fontSize: '8.5px', color: tokens.inkMute }}>REMINDED {new Date(s.reminded_at).toLocaleDateString()}</span>}
            <span style={{ flex: 1 }} />
            <button onClick={() => remind(s)} disabled={remindingId === s.id || !emailOk || s.status === 'paid'}
              title={s.status === 'paid' ? 'Already paid' : !emailOk ? 'Add an email (✎) first' : 'Send the payment reminder'}
              style={{ ...pdBtn(false), padding: '3px 9px', fontSize: '9px', opacity: (!emailOk || s.status === 'paid') ? 0.4 : 1 }}>
              {remindingId === s.id ? 'SENDING…' : '✉ REMIND'}
            </button>
            <span onClick={() => { if (window.confirm(`Remove ${s.person}'s share?`)) del(s.id); }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
          </div>
        );
      })}

      <form onSubmit={add} style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '8px', alignItems: 'center' }}>
        <input style={{ ...pdInp, width: '120px', padding: '5px 8px', fontSize: '12px' }} value={draft.person} onChange={(e) => setDraft({ ...draft, person: e.target.value })} placeholder="Name" />
        <input style={{ ...pdInp, width: '160px', padding: '5px 8px', fontSize: '12px' }} value={draft.email} onChange={(e) => setDraft({ ...draft, email: e.target.value })} placeholder="their@email.com (optional)" />
        <input style={{ ...pdInp, width: '90px', padding: '5px 8px', fontSize: '12px', fontFamily: fontMono, textAlign: 'right' }} value={draft.amount} onChange={(e) => setDraft({ ...draft, amount: e.target.value })} placeholder={remaining > 0 ? String(remaining) : '0.00'} />
        <button type="submit" style={{ ...pdBtn(true), padding: '6px 12px', fontSize: '9px' }}>+ ADD PERSON</button>
      </form>
    </div>
  );
};

const PdTripsPanel = ({ txns, trips, reload, patchTxn }) => {
  const isMobile = useIsMobile();
  const [showAdd, setShowAdd] = React.useState(false);
  const [openTripId, setOpenTripId] = React.useState(null);
  const [openSplitTxn, setOpenSplitTxn] = React.useState(null);   // txn id with the split editor open
  const [splits, setSplits] = React.useState([]);
  const [draft, setDraft] = React.useState({ name: '', description: '', start_date: '', end_date: '', kind: 'work' });
  const [busyId, setBusyId] = React.useState(null);
  const [msg, setMsg] = React.useState('');
  const [err, setErr] = React.useState('');
  const [showManualAdd, setShowManualAdd] = React.useState(false);
  const [iou, setIou] = React.useState({ kind: 'personal', counterparty: '', email: '', description: '', amount: '', currency: 'USD' });
  const [ious, setIous] = React.useState(null);
  const [remindingId, setRemindingId] = React.useState(null);

  const [xeros, setXeros] = React.useState([]);
  const [gbpUsd, setGbpUsd] = React.useState(1.25);
  const [xeroDraft, setXeroDraft] = React.useState(null);   // {trip_id, description, merchant, spent_date, amount} | null

  const client = () => window._supabaseClient;
  const loadIous = React.useCallback(async () => {
    if (!client()) { setIous([]); return; }
    const { data } = await client().from('personal_reimbursements').select('*').order('created_at', { ascending: false });
    setIous(data || []);
  }, []);
  const loadSplits = React.useCallback(async () => {
    if (!client()) { setSplits([]); return; }
    const { data } = await client().from('personal_splits').select('*').order('created_at');
    setSplits(data || []);
  }, []);
  const loadXeros = React.useCallback(async () => {
    if (!client()) return;
    const [{ data: x }, { data: p }] = await Promise.all([
      client().from('personal_xero_entries').select('*').order('spent_date'),
      client().from('personal_prices').select('price').eq('ticker', 'GBPUSD').maybeSingle(),
    ]);
    setXeros(x || []);
    if (p && p.price) setGbpUsd(Number(p.price));
  }, []);
  React.useEffect(() => { loadIous(); loadSplits(); loadXeros(); }, [loadIous, loadSplits, loadXeros]);
  const splitsFor = (txnId) => splits.filter((s) => s.txn_id === txnId);
  const xerosFor = (tripId) => xeros.filter((x) => x.trip_id === tripId);
  const xeroUsd = (x) => x.currency === 'GBP' ? x.amount * gbpUsd : x.amount;
  const unassignedXeros = xeros.filter((x) => !x.trip_id);

  const saveXero = async (e) => {
    e.preventDefault();
    const amt = parseFloat(String(xeroDraft.amount).replace(/,/g, ''));
    if (!xeroDraft.description.trim() || !isFinite(amt)) return;
    await client().from('personal_xero_entries').insert({
      trip_id: xeroDraft.trip_id, description: xeroDraft.description.trim(), merchant: xeroDraft.merchant.trim(),
      spent_date: xeroDraft.spent_date || null, amount: amt, currency: 'GBP',
    });
    setXeroDraft(null);
    await loadXeros();
  };
  const patchXero = async (id, p) => {
    setXeros((xs) => xs.map((x) => (x.id === id ? { ...x, ...p } : x)));   // optimistic
    await client().from('personal_xero_entries').update(p).eq('id', id);
  };
  const delXero = async (x) => {
    if (!window.confirm(`Delete the Xero entry "${x.description}"?`)) return;
    setXeros((xs) => xs.filter((y) => y.id !== x.id));
    await client().from('personal_xero_entries').delete().eq('id', x.id);
  };
  // Un-flag a card expense entirely — it stops being reimbursable.
  const untagTxn = async (t) => {
    if (!window.confirm(`Un-flag "${t.merchant || t.description}"? It stops being a reimbursable expense (the transaction itself stays in the spend table).`)) return;
    await patchTxn(t.id, { tag: null, work_type: null, trip_id: null });
  };
  // Mark a trip expense as submitted in Xero → derived entry in the Xero
  // bucket (linked by txn_id); the expense leaves the to-submit table.
  const markToXero = async (t) => {
    if (!window.confirm(`Mark "${t.merchant || t.description}" as submitted in Xero?\n\nIt moves to this trip's "Already in Xero" bucket and out of the amount to submit. Restore it from there if needed.`)) return;
    await client().from('personal_xero_entries').insert({
      trip_id: t.trip_id, txn_id: t.id,
      description: t.notes || t.merchant || t.description,
      merchant: t.merchant || '', spent_date: t.txn_date,
      amount: t.reimb_amount != null ? t.reimb_amount : t.amount, currency: t.currency,
    });
    await patchTxn(t.id, { reimb_status: 'submitted' });
    await loadXeros();
  };
  // Restore a linked entry: delete it and reopen the source expense.
  const restoreFromXero = async (x) => {
    if (!window.confirm(`Move "${x.description}" back to the to-submit list?`)) return;
    setXeros((xs) => xs.filter((y) => y.id !== x.id));
    await client().from('personal_xero_entries').delete().eq('id', x.id);
    await patchTxn(x.txn_id, { reimb_status: 'open' });
  };

  const addTrip = async (e) => {
    e.preventDefault();
    if (!draft.name.trim()) return;
    await client().from('personal_trips').insert({ name: draft.name.trim(), description: draft.description, start_date: draft.start_date || null, end_date: draft.end_date || null, kind: draft.kind });
    setDraft({ name: '', description: '', start_date: '', end_date: '', kind: 'work' }); setShowAdd(false);
    await reload();
  };
  const [editTrip, setEditTrip] = React.useState(null);   // {id, name, description, start_date, end_date}
  const saveTripEdit = async (e) => {
    e.preventDefault();
    if (!editTrip.name.trim()) return;
    await client().from('personal_trips').update({
      name: editTrip.name.trim(), description: editTrip.description,
      start_date: editTrip.start_date || null, end_date: editTrip.end_date || null,
    }).eq('id', editTrip.id);
    setEditTrip(null);
    await reload();
  };
  // Inline edit form shared by work trips and events.
  const tripEditForm = (tr) => editTrip && editTrip.id === tr.id && (
    <form onSubmit={saveTripEdit} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '12px 14px', margin: '2px 0 10px 22px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1.2fr 2fr auto auto auto auto', gap: '10px', alignItems: 'end' }}>
      <div><label style={pdLbl}>NAME</label><input style={pdInp} value={editTrip.name} onChange={(e) => setEditTrip({ ...editTrip, name: e.target.value })} autoFocus /></div>
      <div><label style={pdLbl}>DESCRIPTION</label><input style={pdInp} value={editTrip.description} onChange={(e) => setEditTrip({ ...editTrip, description: e.target.value })} /></div>
      <div><label style={pdLbl}>START</label><input type="date" style={pdInp} value={editTrip.start_date} onChange={(e) => setEditTrip({ ...editTrip, start_date: e.target.value })} /></div>
      <div><label style={pdLbl}>END</label><input type="date" style={pdInp} value={editTrip.end_date} onChange={(e) => setEditTrip({ ...editTrip, end_date: e.target.value })} /></div>
      <button type="submit" style={pdBtn(true)}>SAVE</button>
      <button type="button" onClick={() => setEditTrip(null)} style={pdBtn(false)}>CANCEL</button>
    </form>
  );
  const editBtn = (tr) => (
    <span onClick={() => setEditTrip(editTrip && editTrip.id === tr.id ? null : { id: tr.id, name: tr.name, description: tr.description || '', start_date: tr.start_date || '', end_date: tr.end_date || '' })}
      title="Edit name / description / dates" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>✎</span>
  );
  const delTrip = async (id) => {
    if (!window.confirm('Delete this trip/event? Its expenses stay, just unallocated.')) return;
    await client().from('personal_transactions').update({ trip_id: null }).eq('trip_id', id);
    await client().from('personal_trips').delete().eq('id', id);
    await reload();
  };
  const emailTrip = async (id) => {
    setBusyId(id); setErr(''); setMsg('');
    try {
      const r = await pfInvoke({ action: 'email_reimbursement', trip_id: id });
      setMsg(`✓ SUMMARY EMAILED TO ${String(r.to).toUpperCase()}${r.via ? ` VIA ${String(r.via).toUpperCase()}` : ''} — ${r.count} EXPENSES · ${pdUsd0(r.total_usd)}${r.net_usd != null && r.net_usd !== r.total_usd ? ` · NET TO SUBMIT ${pdUsd0(r.net_usd)}` : ''}`);
      await reload();
    } catch (e) { setErr(e.message); }
    finally { setBusyId(null); }
  };

  const addIou = async (e) => {
    e.preventDefault();
    const amt = parseFloat(iou.amount);
    if (!iou.counterparty.trim() || !isFinite(amt)) return;
    await client().from('personal_reimbursements').insert({ ...iou, counterparty: iou.counterparty.trim(), email: iou.email.trim() || null, amount: amt, amount_usd: iou.currency === 'USD' ? amt : null });
    setIou({ kind: 'personal', counterparty: '', email: '', description: '', amount: '', currency: 'USD' }); setShowManualAdd(false);
    await loadIous();
  };
  const saveIouEmail = async (id, email) => {
    await client().from('personal_reimbursements').update({ email: email.trim() || null }).eq('id', id);
    await loadIous();
  };
  const saveIouDesc = async (id, description) => {
    await client().from('personal_reimbursements').update({ description: description.trim() }).eq('id', id);
    await loadIous();
  };
  const saveIouAmount = async (r, amount) => {
    await client().from('personal_reimbursements').update({ amount, amount_usd: r.currency === 'USD' ? amount : null }).eq('id', r.id);
    await loadIous();
  };
  const remind = async (r) => {
    if (!window.confirm(`Send the payment reminder to ${r.email}?\n\nFrom reimbursements@urdaneta.io ("Urds' AI Bot"), with all your payment options included. Replies go to daniel@urdaneta.io.`)) return;
    setRemindingId(r.id); setErr(''); setMsg('');
    try {
      const res = await pfInvoke({ action: 'send_reminder', id: r.id });
      setMsg(`✓ REMINDER SENT TO ${String(res.to).toUpperCase()}`);
      await loadIous();
    } catch (e) { setErr(e.message); }
    finally { setRemindingId(null); }
  };
  const cycleIou = async (r) => {
    const next = r.status === 'open' ? 'submitted' : r.status === 'submitted' ? 'paid' : 'open';
    await client().from('personal_reimbursements').update({ status: next }).eq('id', r.id);
    await loadIous();
  };
  const cycleTxn = async (t) => {
    const next = t.reimb_status === 'open' ? 'submitted' : t.reimb_status === 'submitted' ? 'paid' : 'open';
    await patchTxn(t.id, { reimb_status: next });   // in-place — no reorder
  };

  // Trip/event-allocated expenses live under their trip; the generic list is
  // only what's flagged but not yet allocated. Chronological (earliest first;
  // undated trips sink to the bottom).
  const byStart = (a, b) => String(a.start_date || '9999-99').localeCompare(String(b.start_date || '9999-99')) || String(a.created_at).localeCompare(String(b.created_at));
  const workTrips = (trips || []).filter((tr) => (tr.kind || 'work') === 'work').sort(byStart);
  const events = (trips || []).filter((tr) => tr.kind === 'personal').sort(byStart);
  const unallocated = (txns || []).filter((t) => t.tag && !t.trip_id);
  // Expenses already pushed to the Xero bucket leave the to-submit table.
  const linkedTxnIds = new Set(xeros.map((x) => x.txn_id).filter(Boolean));
  const tripRows = (id) => (txns || []).filter((t) => t.trip_id === id && t.tag === 'work' && !linkedTxnIds.has(t.id));
  const eventRows = (id) => (txns || []).filter((t) => t.trip_id === id && t.tag === 'personal');
  const tripStats = (id) => {
    const rows = tripRows(id);
    // `all` includes expenses already mapped into Xero — the email summary can
    // always be re-sent while the trip has ANY work expense (it shows the Xero
    // reconciliation and the remaining net), so the button keys off `all`, not
    // just the unsubmitted count.
    const all = (txns || []).filter((t) => t.trip_id === id && t.tag === 'work').length;
    return { n: rows.length, all, usd: rows.reduce((a, t) => a + (pdSubmitUsd(t) || 0), 0), untyped: rows.filter((t) => !t.work_type).length };
  };
  const allTagged = (txns || []).filter((t) => t.tag);
  // A split share's USD value, via its transaction's stored FX rate.
  const txnById = {}; (txns || []).forEach((t) => { txnById[t.id] = t; });
  const splitUsd = (s) => {
    const t = txnById[s.txn_id];
    const rate = t ? (t.fx_rate != null ? t.fx_rate : (t.currency === 'USD' ? 1 : null)) : (s.currency === 'USD' ? 1 : null);
    return rate != null ? s.amount * rate : null;
  };
  const statusChip = (s, cb) => (
    <span onClick={cb} title="Click to cycle open → submitted → paid" style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', cursor: 'pointer',
      border: `1px solid ${s === 'paid' ? tokens.green : s === 'submitted' ? tokens.ochre : tokens.inkLine}`,
      color: s === 'paid' ? tokens.green : s === 'submitted' ? tokens.ochre : tokens.inkMute }}>{s.toUpperCase()}</span>
  );
  const openUsd = allTagged.filter((t) => t.reimb_status !== 'paid').reduce((a, t) => a + (pdSubmitUsd(t) || 0), 0)
    + (ious || []).filter((r) => r.status !== 'paid').reduce((a, r) => a + (r.amount_usd ?? r.amount), 0)
    + splits.filter((s) => s.status !== 'paid').reduce((a, s) => a + (splitUsd(s) || 0), 0);

  return (
    <PdShell collapseKey="pd.trips" title="TRIPS & REIMBURSEMENTS"
      right={`OUTSTANDING ${pdUsd0(openUsd)}`}
      actions={[{ label: '+ ADD TRIP / EVENT', onClick: () => setShowAdd((s) => !s), primary: false }, { label: '+ ADD IOU', onClick: () => setShowManualAdd((s) => !s), primary: false }]}>
      <div style={{ padding: '16px 20px' }}>
        {msg && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.green, marginBottom: '10px' }}>{msg}</div>}
        {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginBottom: '10px' }}>{err}</div>}

        {showAdd && (
          <form onSubmit={addTrip} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '14px 16px', marginBottom: '14px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'auto 1.2fr 2fr auto auto auto', gap: '10px', alignItems: 'end' }}>
            <div><label style={pdLbl}>KIND</label>
              <div style={{ display: 'flex', gap: '6px' }}>
                {[['work', 'Work trip'], ['personal', 'Event']].map(([id, label]) => (
                  <span key={id} onClick={() => setDraft({ ...draft, kind: id })}
                    style={{ fontFamily: fontMono, fontSize: '10px', padding: '8px 12px', cursor: 'pointer', whiteSpace: 'nowrap', letterSpacing: '0.04em',
                      border: `1px solid ${draft.kind === id ? tokens.ink : tokens.inkLine}`, background: draft.kind === id ? tokens.ink : 'none', color: draft.kind === id ? tokens.paper : tokens.inkMute }}>
                    {label.toUpperCase()}
                  </span>
                ))}
              </div>
            </div>
            <div><label style={pdLbl}>{draft.kind === 'work' ? 'TRIP NAME' : 'EVENT NAME'}</label><input style={pdInp} value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} placeholder={draft.kind === 'work' ? 'NYC — LP meetings' : '4th of July'} autoFocus /></div>
            <div><label style={pdLbl}>DESCRIPTION</label><input style={pdInp} value={draft.description} onChange={(e) => setDraft({ ...draft, description: e.target.value })} /></div>
            <div><label style={pdLbl}>START</label><input type="date" style={pdInp} value={draft.start_date} onChange={(e) => setDraft({ ...draft, start_date: e.target.value })} /></div>
            <div><label style={pdLbl}>END</label><input type="date" style={pdInp} value={draft.end_date} onChange={(e) => setDraft({ ...draft, end_date: e.target.value })} /></div>
            <button type="submit" style={pdBtn(true)}>ADD</button>
          </form>
        )}

        {/* work trips */}
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '8px' }}>WORK TRIPS · XERO</div>
        {workTrips.length === 0 ? (
          <div style={{ fontSize: '13px', color: tokens.inkSoft, marginBottom: '16px' }}>No trips yet. Add one, tag expenses <span style={{ fontFamily: fontMono, fontSize: '10px' }}>W</span> in the spend table, allocate them to the trip, then email the summary for Xero.</div>
        ) : workTrips.map((tr) => {
          const st = tripStats(tr.id);
          const period = [tr.start_date, tr.end_date].filter(Boolean).join(' → ');
          const open = openTripId === tr.id;
          const rows = open ? tripRows(tr.id) : [];
          return (
            <div key={tr.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', padding: '9px 0', flexWrap: 'wrap' }}>
                <div onClick={() => setOpenTripId(open ? null : tr.id)} style={{ minWidth: 0, cursor: 'pointer', flex: '1 1 220px' }}>
                  <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, marginRight: '8px' }}>{open ? '▾' : '▸'}</span>
                  <span style={{ fontWeight: 500, letterSpacing: '-0.01em', fontSize: '13px' }}>{tr.name}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.inkMute, marginLeft: '10px' }}>{period}</span>
                  {tr.description && <div style={{ fontSize: '11.5px', color: tokens.inkSoft, marginLeft: '22px' }}>{tr.description}</div>}
                </div>
                <div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexShrink: 0, flexWrap: 'wrap' }}>
                  <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkSoft }}>{st.n} EXP · {pdUsd0(st.usd)}</span>
                  {xerosFor(tr.id).length > 0 && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.green }} title="Already submitted in Xero — netted off the email summary">✓ {pdMoney(xerosFor(tr.id).reduce((a, x) => a + x.amount, 0), 'GBP')} IN XERO</span>}
                  {st.untyped > 0 && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.red }}>{st.untyped} MISSING FUND/GP</span>}
                  <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', border: `1px solid ${tr.status === 'submitted' ? tokens.ochre : tr.status === 'reimbursed' ? tokens.green : tokens.inkLine}`, color: tr.status === 'submitted' ? tokens.ochre : tr.status === 'reimbursed' ? tokens.green : tokens.inkMute }}>{tr.status.toUpperCase()}</span>
                  <button onClick={() => emailTrip(tr.id)} disabled={busyId === tr.id || !st.all} style={{ ...pdBtn(false), opacity: st.all ? 1 : 0.45 }}>{busyId === tr.id ? 'SENDING…' : (tr.status === 'submitted' || tr.status === 'reimbursed') ? '✉ RE-EMAIL SUMMARY' : '✉ EMAIL SUMMARY'}</button>
                  {editBtn(tr)}
                  <span onClick={() => delTrip(tr.id)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                </div>
              </div>
              {tripEditForm(tr)}
              {open && (
                <div style={{ margin: '0 0 12px 22px', overflowX: 'auto' }}>
                  {rows.length === 0 ? (
                    <div style={{ fontSize: '12px', color: tokens.inkSoft, padding: '4px 0 8px' }}>No expenses allocated yet — assign them from the list below or the spend table.</div>
                  ) : (
                    <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '860px' }}>
                      <thead><tr>
                        {['DATE', 'EXPENSE', 'FUND/GP'].map((h) => <th key={h} style={pdTh}>{h}</th>)}
                        {['FULL', 'REIMBURSABLE', 'SUBMIT USD'].map((h) => <th key={h} style={{ ...pdTh, textAlign: 'right' }}>{h}</th>)}
                        {['NOTE', ''].map((h, i) => <th key={i} style={pdTh}>{h}</th>)}
                      </tr></thead>
                      <tbody>{rows.map((t) => <PdTripExpRow key={t.id} t={t} onPatch={patchTxn} onToXero={markToXero} />)}</tbody>
                    </table>
                  )}

                  {/* already submitted in Xero — shown + netted off the email summary */}
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', margin: '12px 0 4px' }}>
                    <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.green }}>✓ ALREADY IN XERO · {xerosFor(tr.id).length}</span>
                    <span style={{ flex: 1, height: '1px', background: tokens.inkLineSoft }} />
                    <span onClick={() => setXeroDraft(xeroDraft && xeroDraft.trip_id === tr.id ? null : { trip_id: tr.id, description: '', merchant: '', spent_date: '', amount: '' })}
                      style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', color: tokens.inkMute, cursor: 'pointer', border: `1px solid ${tokens.inkLine}`, padding: '3px 9px' }}>+ ADD XERO ENTRY</span>
                  </div>
                  {xeroDraft && xeroDraft.trip_id === tr.id && (
                    <form onSubmit={saveXero} style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', padding: '6px 0' }}>
                      <input style={{ ...pdInp, width: '200px', padding: '5px 8px', fontSize: '12px' }} value={xeroDraft.description} onChange={(e) => setXeroDraft({ ...xeroDraft, description: e.target.value })} placeholder="Xero claim label" autoFocus />
                      <input style={{ ...pdInp, width: '130px', padding: '5px 8px', fontSize: '12px' }} value={xeroDraft.merchant} onChange={(e) => setXeroDraft({ ...xeroDraft, merchant: e.target.value })} placeholder="Merchant" />
                      <input type="date" style={{ ...pdInp, width: '135px', padding: '5px 8px', fontSize: '12px' }} value={xeroDraft.spent_date} onChange={(e) => setXeroDraft({ ...xeroDraft, spent_date: e.target.value })} />
                      <input style={{ ...pdInp, width: '90px', padding: '5px 8px', fontSize: '12px', fontFamily: fontMono, textAlign: 'right' }} value={xeroDraft.amount} onChange={(e) => setXeroDraft({ ...xeroDraft, amount: e.target.value })} placeholder="£ 0.00" />
                      <button type="submit" style={{ ...pdBtn(true), padding: '6px 12px', fontSize: '9px' }}>ADD (GBP)</button>
                    </form>
                  )}
                  {xerosFor(tr.id).map((x) => (
                    <div key={x.id} style={{ display: 'flex', gap: '12px', alignItems: 'baseline', flexWrap: 'wrap', padding: '5px 0', borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
                      <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, whiteSpace: 'nowrap' }}>{x.spent_date || '—'}</span>
                      <span style={{ fontSize: '12.5px', fontWeight: 500 }}>{x.description}</span>
                      {x.merchant && <span style={{ fontSize: '11.5px', color: tokens.inkMute }}>{x.merchant}</span>}
                      {x.txn_id && <span title="Derived from a card expense on this trip" style={{ fontFamily: fontMono, fontSize: '8px', letterSpacing: '0.08em', color: tokens.green, border: `1px solid ${tokens.green}`, padding: '1px 5px' }}>FROM CARD</span>}
                      <span style={{ flex: 1 }} />
                      <span style={{ fontFamily: fontMono, fontSize: '11px', whiteSpace: 'nowrap' }}>{pdMoney(x.amount, x.currency)} <span style={{ color: tokens.inkMute, fontSize: '9.5px' }}>≈{pdUsd0(xeroUsd(x))}</span></span>
                      {x.txn_id
                        ? <span onClick={() => restoreFromXero(x)} title="Move back to the to-submit list" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute, borderBottom: `1px solid ${tokens.inkLine}` }}>↩ RESTORE</span>
                        : <React.Fragment>
                            <span onClick={() => patchXero(x.id, { trip_id: null })} title="Unassign from this trip" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute, borderBottom: `1px solid ${tokens.inkLine}` }}>UNASSIGN</span>
                            <span onClick={() => delXero(x)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                          </React.Fragment>}
                    </div>
                  ))}

                  {/* the net math the email summary will show */}
                  <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '18px', padding: '10px 0 2px', fontFamily: fontMono, fontSize: '10px', flexWrap: 'wrap' }}>
                    {xerosFor(tr.id).length > 0 && (
                      <React.Fragment>
                        <span style={{ color: tokens.inkMute }}>TRIP TOTAL <span style={{ color: tokens.ink }}>{pdUsd0(st.usd + xerosFor(tr.id).reduce((a, x) => a + xeroUsd(x), 0))}</span></span>
                        <span style={{ color: tokens.inkMute }}>IN XERO <span style={{ color: tokens.green }}>−{pdUsd0(xerosFor(tr.id).reduce((a, x) => a + xeroUsd(x), 0))}</span></span>
                      </React.Fragment>
                    )}
                    <span style={{ color: tokens.ink, fontWeight: 700 }}>NET TO SUBMIT {pdUsd0(st.usd)}</span>
                  </div>
                </div>
              )}
            </div>
          );
        })}

        {/* personal events — umbrella groupings for personal-tagged spend, with per-expense splits */}
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '20px 0 8px' }}>PERSONAL EVENTS · SPLIT & COLLECT</div>
        {events.length === 0 ? (
          <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>No events yet. Add one (kind: <span style={{ fontFamily: fontMono, fontSize: '10px' }}>EVENT</span>) — e.g. “4th of July” — tag expenses <span style={{ fontFamily: fontMono, fontSize: '10px' }}>P</span> in the spend table, allocate them here, then split any bill across the people who owe you.</div>
        ) : events.map((ev) => {
          const rows = eventRows(ev.id);
          const evUsd = rows.reduce((a, t) => a + (t.amount_usd || 0), 0);
          const evOpen = openTripId === ev.id;
          const evSplits = rows.flatMap((t) => splitsFor(t.id));
          const owedOpen = evSplits.filter((s) => s.status !== 'paid').reduce((a, s) => a + (splitUsd(s) || 0), 0);
          const period = [ev.start_date, ev.end_date].filter(Boolean).join(' → ');
          return (
            <div key={ev.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', padding: '9px 0', flexWrap: 'wrap' }}>
                <div onClick={() => setOpenTripId(evOpen ? null : ev.id)} style={{ minWidth: 0, cursor: 'pointer', flex: '1 1 220px' }}>
                  <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, marginRight: '8px' }}>{evOpen ? '▾' : '▸'}</span>
                  <span style={{ fontWeight: 500, letterSpacing: '-0.01em', fontSize: '13px' }}>{ev.name}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.inkMute, marginLeft: '10px' }}>{period}</span>
                  {ev.description && <div style={{ fontSize: '11.5px', color: tokens.inkSoft, marginLeft: '22px' }}>{ev.description}</div>}
                </div>
                <div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexShrink: 0, flexWrap: 'wrap' }}>
                  <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkSoft }}>{rows.length} EXP · {pdUsd0(evUsd)}</span>
                  {owedOpen > 0 && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.ochre }}>OWED TO ME {pdUsd0(owedOpen)}</span>}
                  {editBtn(ev)}
                  <span onClick={() => delTrip(ev.id)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                </div>
              </div>
              {tripEditForm(ev)}
              {evOpen && (
                <div style={{ margin: '0 0 12px 22px' }}>
                  {rows.length === 0 ? (
                    <div style={{ fontSize: '12px', color: tokens.inkSoft, padding: '4px 0 8px' }}>No expenses allocated yet — tag rows P in the spend table and pick this event, or assign from the list below.</div>
                  ) : rows.map((t) => {
                    const tSplits = splitsFor(t.id);
                    const tSum = tSplits.reduce((a, s) => a + (s.amount || 0), 0);
                    const splitOpen = openSplitTxn === t.id;
                    return (
                      <div key={t.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
                        <div style={{ display: 'flex', gap: '12px', alignItems: 'baseline', flexWrap: 'wrap', padding: '7px 0' }}>
                          <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkSoft, whiteSpace: 'nowrap' }}>{t.txn_date}</span>
                          <span style={{ fontSize: '13px', fontWeight: 500 }}>{t.merchant || t.description}</span>
                          <PdNoteInput value={t.notes} onSave={(v) => patchTxn(t.id, { notes: v })} width={140} placeholder="Label — e.g. Dinner…" />
                          <span style={{ flex: 1 }} />
                          <span style={{ fontFamily: fontMono, fontSize: '11.5px', whiteSpace: 'nowrap' }}>{pdMoney(t.amount, t.currency)}</span>
                          <span onClick={() => setOpenSplitTxn(splitOpen ? null : t.id)}
                            style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '3px 9px', cursor: 'pointer',
                              border: `1px solid ${tSplits.length ? tokens.ochre : tokens.inkLine}`, color: tSplits.length ? tokens.ochre : tokens.inkMute }}>
                            {splitOpen ? '▴ ' : '⤹ '}SPLIT{tSplits.length ? ` · ${tSplits.length} · ${pdMoney(tSum, t.currency)}` : ''}
                          </span>
                          <span onClick={() => patchTxn(t.id, { trip_id: null })} title="Remove from this event" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                        </div>
                        {splitOpen && <PdSplitEditor txn={t} splits={tSplits} onChanged={async () => { await loadSplits(); await reload(); }} />}
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          );
        })}

        {/* flagged card expenses not yet allocated to a trip */}
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '20px 0 8px' }}>FLAGGED CARD EXPENSES · NOT ON A TRIP · {unallocated.length}</div>
        {unallocated.length === 0 ? (
          <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>
            {allTagged.length ? 'Everything flagged is allocated to a trip — see the trips above.' : 'Nothing flagged yet — tag rows W (work) or P (personal) in the spend table above.'}
          </div>
        ) : (
          <div style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '720px' }}>
              <thead><tr>{['DATE', 'MERCHANT', 'KIND', 'ASSIGN TO TRIP / EVENT', 'USD', 'STATUS', ''].map((h, i) => <th key={i} style={{ ...pdTh, textAlign: i === 4 ? 'right' : 'left' }}>{h}</th>)}</tr></thead>
              <tbody>
                {unallocated.slice(0, 120).map((t) => (
                  <tr key={t.id}>
                    <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10.5px', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{t.txn_date}</td>
                    <td style={pdTd}>{t.merchant || t.description}{t.notes && <span style={{ fontSize: '11px', color: tokens.inkMute }}> — {t.notes}</span>}</td>
                    <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10px', whiteSpace: 'nowrap' }}>{t.tag === 'work' ? `WORK${t.work_type ? ` · ${t.work_type.toUpperCase()}` : ''}` : 'PERSONAL'}</td>
                    <td style={pdTd}>
                      <select value="" onChange={(e) => { if (e.target.value) patchTxn(t.id, { trip_id: e.target.value }); }}
                        style={{ ...pdInp, width: 'auto', maxWidth: '160px', padding: '3px 6px', fontSize: '10.5px', background: 'none', border: `1px solid ${tokens.inkLineSoft}` }}>
                        <option value="">{t.tag === 'work' ? '— pick a trip —' : '— pick an event —'}</option>
                        {(t.tag === 'work' ? workTrips : events).map((x) => <option key={x.id} value={x.id}>{x.name}</option>)}
                      </select>
                    </td>
                    <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap' }}>{pdMoney(t.amount_usd, 'USD')}</td>
                    <td style={pdTd}>{statusChip(t.reimb_status || 'open', () => cycleTxn(t))}</td>
                    <td style={pdTd}><span onClick={() => untagTxn(t)} title="Un-flag — no longer a reimbursable expense" style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        {/* Xero entries not yet mapped to a trip */}
        {unassignedXeros.length > 0 && (
          <React.Fragment>
            <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '20px 0 8px' }}>XERO ENTRIES · NOT MAPPED TO A TRIP · {unassignedXeros.length}</div>
            <div style={{ overflowX: 'auto' }}>
              <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '680px' }}>
                <thead><tr>{['DATE', 'XERO CLAIM', 'MERCHANT', 'MAP TO TRIP', 'GBP', ''].map((h, i) => <th key={i} style={{ ...pdTh, textAlign: i === 4 ? 'right' : 'left' }}>{h}</th>)}</tr></thead>
                <tbody>
                  {unassignedXeros.map((x) => (
                    <tr key={x.id}>
                      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10.5px', whiteSpace: 'nowrap', color: tokens.inkSoft }}>{x.spent_date || '—'}</td>
                      <td style={{ ...pdTd, fontWeight: 500 }}>{x.description}</td>
                      <td style={{ ...pdTd, fontSize: '11.5px', color: tokens.inkMute }}>{x.merchant}</td>
                      <td style={pdTd}>
                        <select value="" onChange={(e) => { if (e.target.value) patchXero(x.id, { trip_id: e.target.value }); }}
                          style={{ ...pdInp, width: 'auto', maxWidth: '180px', padding: '3px 6px', fontSize: '10.5px', background: 'none', border: `1px solid ${tokens.inkLineSoft}` }}>
                          <option value="">— pick a trip —</option>
                          {workTrips.map((tr) => <option key={tr.id} value={tr.id}>{tr.name}</option>)}
                        </select>
                      </td>
                      <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11.5px', textAlign: 'right', whiteSpace: 'nowrap' }}>{pdMoney(x.amount, x.currency)}</td>
                      <td style={pdTd}><span onClick={() => delXero(x)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '4px' }}>
              WHAT'S ALREADY IN XERO (GBP) — MAP EACH TO ITS TRIP SO THE EMAIL SUMMARY SHOWS IT AND NETS IT OFF THE AMOUNT TO SUBMIT
            </div>
          </React.Fragment>
        )}

        {/* standalone IOUs */}
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '20px 0 8px' }}>OTHER REIMBURSEMENTS · WHO OWES ME WHAT</div>
        {showManualAdd && (
          <form onSubmit={addIou} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '14px 16px', marginBottom: '12px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'auto 1.2fr 1.4fr 1.6fr auto auto auto', gap: '10px', alignItems: 'end' }}>
            <div><label style={pdLbl}>KIND</label>
              <select style={pdInp} value={iou.kind} onChange={(e) => setIou({ ...iou, kind: e.target.value })}><option value="personal">Personal</option><option value="work">Work</option></select></div>
            <div><label style={pdLbl}>WHO</label><input style={pdInp} value={iou.counterparty} onChange={(e) => setIou({ ...iou, counterparty: e.target.value })} placeholder="Name / firm" autoFocus /></div>
            <div><label style={pdLbl}>EMAIL <span style={{ opacity: 0.6 }}>(for reminders)</span></label><input style={pdInp} value={iou.email} onChange={(e) => setIou({ ...iou, email: e.target.value })} placeholder="them@email.com" /></div>
            <div><label style={pdLbl}>FOR</label><input style={pdInp} value={iou.description} onChange={(e) => setIou({ ...iou, description: e.target.value })} /></div>
            <div><label style={pdLbl}>AMOUNT</label><input style={pdInp} value={iou.amount} onChange={(e) => setIou({ ...iou, amount: e.target.value })} placeholder="120.00" /></div>
            <div><label style={pdLbl}>CCY</label>
              <select style={pdInp} value={iou.currency} onChange={(e) => setIou({ ...iou, currency: e.target.value })}><option>USD</option><option>GBP</option></select></div>
            <button type="submit" style={pdBtn(true)}>ADD</button>
          </form>
        )}
        {ious === null ? null : ious.length === 0 ? (
          <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>Nothing tracked outside the card data. <span onClick={() => setShowManualAdd(true)} style={{ color: tokens.ink, cursor: 'pointer', borderBottom: `1px solid ${tokens.inkLine}` }}>Add an IOU</span> to track who owes you what.</div>
        ) : (
          <div style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '760px' }}>
              <thead><tr>{['WHO', 'ADDED', 'FOR', 'KIND', 'AMOUNT', 'EMAIL', 'STATUS', ''].map((h, i) => <th key={i} style={{ ...pdTh, textAlign: i === 4 ? 'right' : 'left' }}>{h}</th>)}</tr></thead>
              <tbody>
                {ious.map((r) => <PdIouRow key={r.id} r={r} statusChip={statusChip} onCycle={() => cycleIou(r)} onSaveEmail={saveIouEmail} onSaveDesc={saveIouDesc} onSaveAmount={saveIouAmount}
                  onRemind={() => remind(r)} reminding={remindingId === r.id}
                  onDelete={async () => { if (window.confirm('Delete?')) { await client().from('personal_reimbursements').delete().eq('id', r.id); await loadIous(); } }} />)}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </PdShell>
  );
};

// ── Net worth panel ───────────────────────────────────────────────────────────

const PD_ASSET_CATS = {
  brokerage: { label: 'Brokerage accounts', color: '#1A4FB5' },
  cash:      { label: 'Cash & checking',    color: '#0E7C3A' },
  fund:      { label: 'Fund investments',   color: '#7A2D8E' },
  carry:     { label: 'Carry & distribution interests', color: '#9A6A1A' },
  sponsor:   { label: 'Sponsor economics',  color: '#C9622E' },
  misc:      { label: 'Miscellaneous',      color: '#5A7A3E' },
};

const PdAssetEditor = ({ initial, onSave, onCancel }) => {
  const isMobile = useIsMobile();
  const [a, setA] = React.useState(() => ({
    category: initial.category || 'misc', name: initial.name || '', ticker: initial.ticker || '',
    quantity: initial.quantity != null ? String(initial.quantity) : '', manual_value: initial.manual_value != null ? String(initial.manual_value) : '',
    currency: initial.currency || 'USD', vesting: initial.vesting || '', unvested: !!initial.unvested, note: initial.note || '',
  }));
  const set = (k, v) => setA((p) => ({ ...p, [k]: v }));
  const submit = (e) => {
    e.preventDefault();
    if (!a.name.trim()) return;
    onSave({
      category: a.category, name: a.name.trim(), ticker: a.ticker.trim().toUpperCase() || null,
      quantity: a.quantity !== '' && isFinite(+a.quantity.replace(/,/g, '')) ? +a.quantity.replace(/,/g, '') : null,
      manual_value: a.manual_value !== '' && isFinite(+a.manual_value.replace(/,/g, '')) ? +a.manual_value.replace(/,/g, '') : null,
      currency: a.currency, vesting: a.vesting, unvested: a.unvested, note: a.note,
    });
  };
  const span = (n) => ({ gridColumn: isMobile ? 'auto' : `span ${n}` });
  return (
    <form onSubmit={submit} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '14px 16px', margin: '10px 0', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(4, minmax(0, 1fr))', gap: '10px' }}>
      <div><label style={pdLbl}>NAME</label><input style={pdInp} value={a.name} onChange={(e) => set('name', e.target.value)} autoFocus /></div>
      <div><label style={pdLbl}>CATEGORY</label>
        <select style={pdInp} value={a.category} onChange={(e) => set('category', e.target.value)}>{Object.entries(PD_ASSET_CATS).map(([id, c]) => <option key={id} value={id}>{c.label}</option>)}</select></div>
      <div><label style={pdLbl}>TICKER <span style={{ opacity: 0.6 }}>(auto-price)</span></label><input style={pdInp} value={a.ticker} onChange={(e) => set('ticker', e.target.value)} placeholder="JOBY" /></div>
      <div><label style={pdLbl}>QUANTITY</label><input style={pdInp} value={a.quantity} onChange={(e) => set('quantity', e.target.value)} placeholder="86000" /></div>
      <div><label style={pdLbl}>MANUAL VALUE</label><input style={pdInp} value={a.manual_value} onChange={(e) => set('manual_value', e.target.value)} placeholder="220000" /></div>
      <div><label style={pdLbl}>CCY</label><select style={pdInp} value={a.currency} onChange={(e) => set('currency', e.target.value)}><option>USD</option><option>GBP</option></select></div>
      <div style={span(2)}><label style={pdLbl}>VESTING CONDITION</label><input style={pdInp} value={a.vesting} onChange={(e) => set('vesting', e.target.value)} placeholder="Vests when 30-day VWAP > $18" /></div>
      <div style={span(3)}><label style={pdLbl}>NOTE</label><input style={pdInp} value={a.note} onChange={(e) => set('note', e.target.value)} /></div>
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px' }}>
        <label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, cursor: 'pointer', paddingBottom: '9px' }}>
          <input type="checkbox" checked={a.unvested} onChange={(e) => set('unvested', e.target.checked)} /> UNVESTED
        </label>
        <button type="submit" style={pdBtn(true)}>SAVE</button>
        <button type="button" onClick={onCancel} style={pdBtn(false)}>CANCEL</button>
      </div>
    </form>
  );
};

const PdNetWorthPanel = () => {
  const isMobile = useIsMobile();
  const [assets, setAssets] = React.useState(null);
  const [liabs, setLiabs] = React.useState(null);
  const [prices, setPrices] = React.useState({});
  const [editing, setEditing] = React.useState(null);   // asset id | 'new' | null
  const [liabEditing, setLiabEditing] = React.useState(false);
  const [liabDraft, setLiabDraft] = React.useState({ name: '', category: 'tax', amount: '', currency: 'USD', due_date: '', note: '' });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  const client = () => window._supabaseClient;
  const load = React.useCallback(async () => {
    if (!client()) { setAssets([]); setLiabs([]); return; }
    const [{ data: a }, { data: l }, { data: p }] = await Promise.all([
      client().from('personal_assets').select('*').order('sort_order').order('created_at'),
      client().from('personal_liabilities').select('*').order('due_date', { ascending: true, nullsFirst: false }),
      client().from('personal_prices').select('*'),
    ]);
    setAssets(a || []); setLiabs(l || []);
    const map = {}; for (const r of p || []) if (r.price != null) map[r.ticker] = r;
    setPrices(map);
  }, []);
  React.useEffect(() => { load(); }, [load]);

  const refreshPrices = async () => {
    setBusy(true); setErr('');
    try {
      const tickers = [...new Set((assets || []).map((a) => a.ticker).filter(Boolean))];
      await pfInvoke({ action: 'prices', tickers });
      await load();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const gbp = prices.GBPUSD ? prices.GBPUSD.price : 1.25;
  const usdVal = (a) => {
    if (a.ticker && a.quantity != null && prices[a.ticker]) return a.quantity * prices[a.ticker].price;
    if (a.manual_value != null) return a.currency === 'GBP' ? a.manual_value * gbp : a.manual_value;
    return null;
  };
  const liabUsd = (l) => l.currency === 'GBP' ? l.amount * gbp : l.amount;

  const totAssets = (assets || []).reduce((s, a) => s + (usdVal(a) || 0), 0);
  const totUnvested = (assets || []).filter((a) => a.unvested).reduce((s, a) => s + (usdVal(a) || 0), 0);
  const totLiabs = (liabs || []).filter((l) => l.status === 'open').reduce((s, l) => s + liabUsd(l), 0);
  const missing = (assets || []).filter((a) => usdVal(a) == null).length;

  const saveAsset = async (id, patch) => {
    if (id === 'new') await client().from('personal_assets').insert({ ...patch, sort_order: (assets || []).length * 10 });
    else await client().from('personal_assets').update(patch).eq('id', id);
    setEditing(null);
    await load();
  };
  const delAsset = async (a) => {
    if (!window.confirm(`Remove ${a.name}?`)) return;
    await client().from('personal_assets').delete().eq('id', a.id);
    await load();
  };
  const addLiab = async (e) => {
    e.preventDefault();
    const amt = parseFloat(String(liabDraft.amount).replace(/,/g, ''));
    if (!liabDraft.name.trim() || !isFinite(amt)) return;
    await client().from('personal_liabilities').insert({ ...liabDraft, name: liabDraft.name.trim(), amount: amt, due_date: liabDraft.due_date || null });
    setLiabDraft({ name: '', category: 'tax', amount: '', currency: 'USD', due_date: '', note: '' }); setLiabEditing(false);
    await load();
  };

  if (assets === null) return null;
  const byCat = {};
  for (const a of assets) (byCat[a.category] = byCat[a.category] || []).push(a);
  const catTotals = Object.entries(PD_ASSET_CATS).map(([id, c]) => ({ id, ...c, usd: (byCat[id] || []).reduce((s, a) => s + (usdVal(a) || 0), 0) })).filter((c) => c.usd > 0);
  const today = new Date().toISOString().slice(0, 10);
  const soon = new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10);

  return (
    <PdShell collapseKey="pd.networth" title="NET WORTH · ASSETS & LIABILITIES"
      right={missing ? `${missing} VALUE${missing === 1 ? '' : 'S'} TO FILL IN` : ''}
      actions={[{ label: '⟳ REFRESH PRICES', onClick: refreshPrices, running: busy, primary: false }, { label: '+ ADD ASSET', onClick: () => setEditing('new') }]}>
      <div style={{ padding: '16px 20px' }}>
        {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginBottom: '10px' }}>{err}</div>}

        {/* headline tiles */}
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(4, 1fr)', gap: '10px', marginBottom: '14px' }}>
          {[['NET WORTH', totAssets - totLiabs, tokens.ink], ['TOTAL ASSETS', totAssets, tokens.ink], ['O/W UNVESTED *', totUnvested, tokens.ochre], ['LIABILITIES (OPEN)', -totLiabs, tokens.red]].map(([k, v, c]) => (
            <div key={k} style={{ border: `1px solid ${tokens.inkLine}`, padding: '10px 14px', background: tokens.bg }}>
              <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '4px' }}>{k}</div>
              <div style={{ fontSize: '20px', letterSpacing: '-0.02em', fontFamily: fontDisplay, color: c }}>{pdUsd0(v)}</div>
            </div>
          ))}
        </div>

        {/* allocation bar */}
        {totAssets > 0 && (
          <div style={{ marginBottom: '18px' }}>
            <div style={{ display: 'flex', height: '10px', borderRadius: '5px', overflow: 'hidden', border: `1px solid ${tokens.inkLine}` }}>
              {catTotals.map((c) => <div key={c.id} title={`${c.label} — ${pdUsd0(c.usd)}`} style={{ width: `${(c.usd / totAssets) * 100}%`, background: c.color }} />)}
            </div>
            <div style={{ display: 'flex', gap: '14px', flexWrap: 'wrap', marginTop: '7px' }}>
              {catTotals.map((c) => (
                <span key={c.id} style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.04em', color: tokens.inkSoft, display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
                  <span style={{ width: '7px', height: '7px', borderRadius: '50%', background: c.color }} />{c.label.toUpperCase()} {((c.usd / totAssets) * 100).toFixed(0)}%
                </span>
              ))}
            </div>
          </div>
        )}

        {editing === 'new' && <PdAssetEditor initial={{}} onSave={(p) => saveAsset('new', p)} onCancel={() => setEditing(null)} />}

        {/* assets by category */}
        {Object.entries(PD_ASSET_CATS).filter(([id]) => (byCat[id] || []).length).map(([id, cat]) => (
          <div key={id} style={{ marginBottom: '16px' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', margin: '4px 0 2px' }}>
              <span style={{ width: '8px', height: '8px', borderRadius: '50%', background: cat.color, flexShrink: 0 }} />
              <span style={{ fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.14em', color: tokens.inkMute }}>{cat.label.toUpperCase()}</span>
              <span style={{ flex: 1, height: '1px', background: tokens.inkLineSoft }} />
              <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkSoft }}>{pdUsd0((byCat[id] || []).reduce((s, a) => s + (usdVal(a) || 0), 0))}</span>
            </div>
            {(byCat[id] || []).map((a) => {
              const v = usdVal(a);
              const auto = a.ticker && a.quantity != null && prices[a.ticker];
              return editing === a.id
                ? <PdAssetEditor key={a.id} initial={a} onSave={(p) => saveAsset(a.id, p)} onCancel={() => setEditing(null)} />
                : (
                  <div key={a.id} style={{ display: 'flex', justifyContent: 'space-between', gap: '12px', padding: '7px 0 7px 18px', borderBottom: `1px solid ${tokens.inkLineSoft}`, alignItems: 'baseline', flexWrap: 'wrap' }}>
                    <div style={{ minWidth: 0, flex: '1 1 260px' }}>
                      <span style={{ fontSize: '13.5px', letterSpacing: '-0.01em' }}>{a.name}</span>
                      {a.unvested && <span title={a.vesting} style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.08em', color: tokens.ochre, border: `1px solid ${tokens.ochre}`, padding: '1px 6px', marginLeft: '8px', whiteSpace: 'nowrap' }}>UNVESTED *</span>}
                      {(a.vesting || a.note) && <div style={{ fontSize: '11px', color: tokens.inkMute, marginTop: '1px' }}>{a.vesting || a.note}</div>}
                    </div>
                    <div style={{ display: 'flex', gap: '14px', alignItems: 'baseline', flexShrink: 0 }}>
                      {auto
                        ? <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>{Number(a.quantity).toLocaleString()} × {a.ticker} @ ${prices[a.ticker].price}</span>
                        : a.manual_value != null && a.currency === 'GBP'
                          ? <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>{pdMoney(a.manual_value, 'GBP')}</span>
                          : null}
                      <span style={{ fontFamily: fontMono, fontSize: '13px', minWidth: '90px', textAlign: 'right', color: v == null ? tokens.inkLine : tokens.ink }}>
                        {v == null ? 'SET VALUE' : `${pdUsd0(v)}${a.unvested ? ' *' : ''}`}
                      </span>
                      <span onClick={() => setEditing(a.id)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>✎</span>
                      <span onClick={() => delAsset(a)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                    </div>
                  </div>
                );
            })}
          </div>
        ))}
        <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '4px' }}>
          * UNVESTED = INDICATIVE — QUANTITY × CURRENT PRICE WITH VESTING HURDLES UNMET · GBP CONVERTED AT {gbp.toFixed(3)}
        </div>

        {/* liabilities */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', margin: '22px 0 8px' }}>
          <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute }}>LIABILITIES · TAXES, DEBTS & PAYMENTS DUE</span>
          <button onClick={() => setLiabEditing((s) => !s)} style={pdBtn(false)}>+ ADD</button>
        </div>
        {liabEditing && (
          <form onSubmit={addLiab} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '14px 16px', marginBottom: '12px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1.6fr auto auto auto auto 2fr auto', gap: '10px', alignItems: 'end' }}>
            <div><label style={pdLbl}>NAME</label><input style={pdInp} value={liabDraft.name} onChange={(e) => setLiabDraft({ ...liabDraft, name: e.target.value })} placeholder="US federal estimated tax Q3" autoFocus /></div>
            <div><label style={pdLbl}>TYPE</label><select style={pdInp} value={liabDraft.category} onChange={(e) => setLiabDraft({ ...liabDraft, category: e.target.value })}><option value="tax">Tax</option><option value="loan">Loan</option><option value="personal">Personal</option><option value="other">Other</option></select></div>
            <div><label style={pdLbl}>AMOUNT</label><input style={pdInp} value={liabDraft.amount} onChange={(e) => setLiabDraft({ ...liabDraft, amount: e.target.value })} placeholder="45000" /></div>
            <div><label style={pdLbl}>CCY</label><select style={pdInp} value={liabDraft.currency} onChange={(e) => setLiabDraft({ ...liabDraft, currency: e.target.value })}><option>USD</option><option>GBP</option></select></div>
            <div><label style={pdLbl}>DUE</label><input type="date" style={pdInp} value={liabDraft.due_date} onChange={(e) => setLiabDraft({ ...liabDraft, due_date: e.target.value })} /></div>
            <div><label style={pdLbl}>NOTE</label><input style={pdInp} value={liabDraft.note} onChange={(e) => setLiabDraft({ ...liabDraft, note: e.target.value })} /></div>
            <button type="submit" style={pdBtn(true)}>ADD</button>
          </form>
        )}
        {(liabs || []).length === 0 ? (
          <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>No liabilities tracked. Add tax payments with due dates, loans, or personal debts you owe.</div>
        ) : (liabs || []).map((l) => {
          const overdue = l.status === 'open' && l.due_date && l.due_date < today;
          const dueSoon = l.status === 'open' && l.due_date && l.due_date >= today && l.due_date <= soon;
          return (
            <div key={l.id} style={{ display: 'flex', justifyContent: 'space-between', gap: '12px', padding: '7px 0', borderBottom: `1px solid ${tokens.inkLineSoft}`, alignItems: 'baseline', flexWrap: 'wrap', opacity: l.status === 'paid' ? 0.55 : 1 }}>
              <div style={{ minWidth: 0 }}>
                <span style={{ fontSize: '13.5px' }}>{l.name}</span>
                <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute, marginLeft: '8px' }}>{l.category.toUpperCase()}</span>
                {l.note && <div style={{ fontSize: '11px', color: tokens.inkMute }}>{l.note}</div>}
              </div>
              <div style={{ display: 'flex', gap: '12px', alignItems: 'baseline', flexShrink: 0 }}>
                {l.due_date && <span style={{ fontFamily: fontMono, fontSize: '10px', color: overdue ? tokens.red : dueSoon ? tokens.ochre : tokens.inkMute }}>{overdue ? 'OVERDUE ' : 'DUE '}{l.due_date}</span>}
                <span style={{ fontFamily: fontMono, fontSize: '13px' }}>{pdMoney(l.amount, l.currency)}</span>
                <span onClick={async () => { await client().from('personal_liabilities').update({ status: l.status === 'open' ? 'paid' : 'open' }).eq('id', l.id); await load(); }}
                  style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', cursor: 'pointer', border: `1px solid ${l.status === 'paid' ? tokens.green : tokens.inkLine}`, color: l.status === 'paid' ? tokens.green : tokens.inkMute }}>
                  {l.status === 'paid' ? '✓ PAID' : 'OPEN'}
                </span>
                <span onClick={async () => { if (window.confirm('Delete?')) { await client().from('personal_liabilities').delete().eq('id', l.id); await load(); } }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
              </div>
            </div>
          );
        })}

        <PdInsights scope="networth" emptyHint="Run the analyzer for a Claude read on allocation, concentration, liquidity, and what to do next — flags any values still missing." />
      </div>
    </PdShell>
  );
};

// ── Portfolio Overview ────────────────────────────────────────────────────────
// The look-through portfolio: direct brokerage/retirement holdings (statement-
// ingested via Claude) + the VAC and FBC fund stakes expanded to their
// underlying positions. Fund NAVs come from the Net Worth section; brokerage
// ingests push totals back into Net Worth — the two sections always agree.

const PP_KIND = {
  taxable:         { label: 'TAXABLE',  color: '#1A4FB5' },
  traditional_ira: { label: 'TRAD IRA', color: '#0E7C3A' },
  roth_ira:        { label: 'ROTH IRA', color: '#5A7A3E' },
  '401k':          { label: '401(K)',   color: '#7A2D8E' },
  fund:            { label: 'FUND',     color: '#C9622E' },
};
const PP_RETIREMENT = ['traditional_ira', 'roth_ira', '401k'];
const PP_DIMS = [['sector', 'SECTOR'], ['geography', 'GEOGRAPHY'], ['asset_class', 'ASSET CLASS'], ['source', 'ACCOUNT']];
const PP_COLORS = ['#39557E', '#5A7A3E', '#C9A24A', '#7A2D8E', '#A04A3E', '#0E7C3A', '#1A4FB5', '#8A6D3B', '#4E6E81', '#6B4E71', '#3E7A72', '#B5651A', '#556B2F', '#8B4A62'];

const ppInvoke = async (body) => {
  const client = window._supabaseClient;
  if (!client) throw new Error('No backend connected.');
  const { data, error } = await client.functions.invoke('personal-portfolio', { body });
  let payload = data;
  if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
  if (payload && payload.error) throw new Error(payload.error);
  return payload;
};

const ppPct = (v) => `${(v * 100).toFixed(1)}%`;

// Upload a statement PDF / broker CSV / fund workbook → Claude parses it
// server-side into accounts + holdings.
const PpUploadModal = ({ onDone, onCancel }) => {
  const isMobile = useIsMobile();
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [result, setResult] = React.useState(null);
  const fileRef = React.useRef(null);

  const onFile = async (f) => {
    if (!f) return;
    setErr(''); setBusy(true); setResult(null);
    try {
      if (f.size > 15 * 1024 * 1024) throw new Error('File too large (15MB max).');
      const buf = new Uint8Array(await f.arrayBuffer());
      let bin = '';
      for (let i = 0; i < buf.length; i += 0x8000) bin += String.fromCharCode.apply(null, buf.subarray(i, i + 0x8000));
      const res = await ppInvoke({ action: 'ingest', file_b64: btoa(bin), filename: f.name });
      setResult(res);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); if (fileRef.current) fileRef.current.value = ''; }
  };

  return (
    <div onClick={busy ? undefined : () => (result ? onDone() : onCancel())} style={{ position: 'fixed', inset: 0, background: 'rgba(14,14,12,0.45)', zIndex: 200, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: isMobile ? '14px' : '48px 24px', overflowY: 'auto' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: '560px', background: tokens.bg, border: `1px solid ${tokens.ink}`, borderTop: `4px solid ${tokens.ochre}`, borderRadius: '4px', padding: isMobile ? '18px' : '26px 30px' }}>
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.16em', color: tokens.inkMute, marginBottom: '14px' }}>INGEST STATEMENT · PDF / CSV / XLSX</div>
        <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6, marginBottom: '14px' }}>
          Drop in a monthly brokerage statement (Morgan Stanley, J.P. Morgan, Schwab — the PDF itself works), a broker CSV, or the monthly ValueAct Public Position Summary xlsx.
          Claude reads it, extracts every account and position with basis, and updates the portfolio for that statement date. The Net Worth section's brokerage values sync automatically.
        </div>
        <input ref={fileRef} type="file" accept=".pdf,.csv,.xlsx,.xls,application/pdf" style={{ display: 'none' }} onChange={(e) => onFile(e.target.files[0])} />
        <button onClick={() => fileRef.current && fileRef.current.click()} disabled={busy} style={{ ...pdBtn(true), opacity: busy ? 0.5 : 1, marginBottom: '14px' }}>{busy ? 'PARSING WITH CLAUDE… (CAN TAKE A MINUTE)' : '⬆ CHOOSE FILE'}</button>
        {err && <div style={{ color: tokens.red, fontSize: '12px', marginBottom: '12px' }}>{err}</div>}
        {result && (
          <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper, padding: '14px 16px', marginBottom: '14px', fontSize: '13px', lineHeight: 1.7 }}>
            <div style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.1em', color: tokens.green, marginBottom: '6px' }}>✓ INGESTED · AS OF {result.as_of}</div>
            {(result.accounts || []).map((a, i) => (
              <div key={i}><span style={{ color: tokens.inkMute }}>{a.account_id}</span> — {a.holdings} position{a.holdings === 1 ? '' : 's'}{a.total_value != null ? ` · ${pdUsd0(a.total_value)}` : ''}</div>
            ))}
          </div>
        )}
        <div style={{ display: 'flex', gap: '8px' }}>
          {result && <button onClick={onDone} style={pdBtn(true)}>DONE</button>}
          <button onClick={result ? onDone : onCancel} disabled={busy} style={pdBtn(false)}>{result ? 'CLOSE' : 'CANCEL'}</button>
        </div>
      </div>
    </div>
  );
};

// The Claude PM/analyst review — renders pd_portfolio_analyses.analysis.
const PpAnalysis = ({ row, running, onRun, err }) => {
  const d = row && row.analysis;
  const [folded, toggleFold] = pdCollapsed('fold.pm.review', false);
  const secLbl = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '16px 0 6px' };
  const verdictColor = (v) => v === 'add' ? tokens.green : v === 'trim' ? tokens.ochre : v === 'exit' ? tokens.red : tokens.inkMute;
  const flagColor = (f) => f === 'overexposed' ? tokens.red : f === 'underexposed' ? tokens.ochre : tokens.green;
  const chip = (text, color) => (
    <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', border: `1px solid ${color}`, color, whiteSpace: 'nowrap' }}>{text}</span>
  );
  return (
    <div style={{ borderTop: `1px solid ${tokens.inkLine}`, marginTop: '18px', paddingTop: '14px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
        <span onClick={toggleFold} title={folded ? 'Show the Claude output' : 'Hide the Claude output'} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.16em', color: tokens.inkMute, cursor: 'pointer', userSelect: 'none' }}>
          {folded ? '▸' : '▾'} CLAUDE PM REVIEW{row && row.generated_at ? ` · ${new Date(row.generated_at).toLocaleDateString()}` : ''}{running ? ' · RUNNING…' : ''}{folded && d ? ' · HIDDEN' : ''}
        </span>
        <button onClick={onRun} disabled={running} style={pdBtn(false)}>{running ? 'ANALYZING…' : (d ? '↻ RE-RUN' : '⚡ RUN ANALYSIS')}</button>
      </div>
      {err && <div style={{ color: tokens.red, fontSize: '12px', marginTop: '8px' }}>{err}</div>}
      {!d && !running && !err && !folded && (
        <div style={{ fontSize: '12.5px', color: tokens.inkSoft, marginTop: '8px' }}>
          Run the review for a PM-grade read: allocation and exposure flags, fundamental assessments of the top positions (with web-searched current data), ETF-vs-single-names verdict, diversification ideas, tax moves off the actual basis data, and a leverage read. Tune its judgment in Admin → Claude feedback.
        </div>
      )}
      {d && !folded && (
        <div style={{ marginTop: '10px' }}>
          {d.headline && <div style={{ fontSize: '14.5px', lineHeight: 1.55 }}>{d.headline}</div>}
          {d.allocation_read && <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6, marginTop: '8px' }}>{d.allocation_read}</div>}

          {Array.isArray(d.exposures) && d.exposures.length > 0 && (<div>
            <div style={secLbl}>EXPOSURE FLAGS</div>
            {d.exposures.map((x, i) => (
              <div key={i} style={{ display: 'flex', gap: '10px', alignItems: 'baseline', padding: '4px 0', fontSize: '13px', flexWrap: 'wrap' }}>
                {chip((x.flag || 'balanced').toUpperCase(), flagColor(x.flag))}
                <span style={{ fontWeight: 500 }}>{x.exposure}</span>
                {x.pct_of_portfolio != null && <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>{Number(x.pct_of_portfolio).toFixed(1)}%</span>}
                <span style={{ color: tokens.inkSoft, flex: '1 1 300px' }}>{x.read}</span>
              </div>
            ))}
          </div>)}

          {Array.isArray(d.top_positions) && d.top_positions.length > 0 && (<div>
            <div style={secLbl}>TOP POSITIONS · FUNDAMENTAL READ</div>
            {d.top_positions.map((p, i) => (
              <div key={i} style={{ border: `1px solid ${tokens.inkLineSoft}`, background: tokens.bg, padding: '10px 14px', marginBottom: '8px' }}>
                <div style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap' }}>
                  <span style={{ fontWeight: 600, fontSize: '13.5px' }}>{p.ticker} · {p.name}</span>
                  {p.weight_pct != null && <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute }}>{Number(p.weight_pct).toFixed(1)}% of portfolio</span>}
                  {p.where_held && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>{p.where_held}</span>}
                  <span style={{ flexGrow: 1 }} />
                  {chip((p.verdict || 'hold').toUpperCase(), verdictColor(p.verdict))}
                  {p.conviction && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>{p.conviction.toUpperCase()} CONVICTION</span>}
                </div>
                <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6, marginTop: '6px' }}>{p.assessment}</div>
                {p.tax_note && <div style={{ fontSize: '12px', color: tokens.ochre, lineHeight: 1.5, marginTop: '5px' }}>TAX · {p.tax_note}</div>}
              </div>
            ))}
          </div>)}

          {d.etf_vs_stocks && (<div>
            <div style={secLbl}>ETF VS SINGLE NAMES</div>
            <div style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap', fontSize: '13px' }}>
              {chip((d.etf_vs_stocks.verdict || '').replace(/_/g, ' ').toUpperCase(), tokens.ink)}
              <span style={{ color: tokens.inkSoft, lineHeight: 1.6, flex: '1 1 300px' }}>{d.etf_vs_stocks.read}</span>
            </div>
          </div>)}

          {Array.isArray(d.diversification_ideas) && d.diversification_ideas.length > 0 && (<div>
            <div style={secLbl}>DIVERSIFICATION IDEAS · LONG-DURATION COMPOUNDERS</div>
            {d.diversification_ideas.map((x, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '22px 1fr', gap: '8px', padding: '5px 0', fontSize: '13px', lineHeight: 1.55 }}>
                <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre }}>{String(i + 1).padStart(2, '0')}</span>
                <span>
                  <span style={{ fontWeight: 500 }}>{x.ticker} · {x.name}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute, marginLeft: '8px' }}>{(x.type || '').toUpperCase()}{x.theme ? ` · ${x.theme.toUpperCase()}` : ''}</span>
                  <span style={{ color: tokens.inkSoft, display: 'block' }}>{x.why}</span>
                </span>
              </div>
            ))}
          </div>)}

          {Array.isArray(d.tax_moves) && d.tax_moves.length > 0 && (<div>
            <div style={secLbl}>TAX MOVES</div>
            {d.tax_moves.map((x, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '22px 1fr auto', gap: '8px', padding: '5px 0', fontSize: '13px', lineHeight: 1.55, alignItems: 'baseline' }}>
                <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre }}>{String(i + 1).padStart(2, '0')}</span>
                <span><span style={{ fontWeight: 500 }}>{x.move}</span> — <span style={{ color: tokens.inkSoft }}>{x.detail}</span></span>
                {x.est_benefit_usd ? <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.green, whiteSpace: 'nowrap' }}>~{pdUsd0(x.est_benefit_usd)}</span> : <span />}
              </div>
            ))}
          </div>)}

          {d.leverage_read && (<div>
            <div style={secLbl}>LEVERAGE</div>
            <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6 }}>
              {d.leverage_read.current_borrow_usd != null && <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.red }}>BORROWING {pdUsd0(d.leverage_read.current_borrow_usd)}{d.leverage_read.rates ? ` · ${d.leverage_read.rates}` : ''} — </span>}
              {d.leverage_read.read}
              {d.leverage_read.recommendation && <div style={{ color: tokens.ink, marginTop: '4px', fontWeight: 500 }}>{d.leverage_read.recommendation}</div>}
            </div>
          </div>)}

          {Array.isArray(d.watch_items) && d.watch_items.length > 0 && (<div>
            <div style={secLbl}>NEXT</div>
            {d.watch_items.map((w, i) => <div key={i} style={{ fontSize: '12.5px', color: tokens.inkSoft, lineHeight: 1.5, padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}
          </div>)}

          <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '14px' }}>
            GUIDED BY THE PERSONAL-PORTFOLIO PHILOSOPHY (BUFFETT / HOHN / THRIVE, LONG-HORIZON, TAX-FIRST) + YOUR STANDING DIRECTIVES — TUNE IN ADMIN → CLAUDE FEEDBACK
          </div>
        </div>
      )}
    </div>
  );
};

const PdPortfolioPanel = () => {
  const isMobile = useIsMobile();
  const [accounts, setAccounts] = React.useState(null);
  const [snaps, setSnaps] = React.useState([]);
  const [holdings, setHoldings] = React.useState([]);
  const [navs, setNavs] = React.useState({ vac: 0, fbc: 0 });
  const [fbcLongs, setFbcLongs] = React.useState([]);
  const [analysisRow, setAnalysisRow] = React.useState(null);
  const [scope, setScope] = React.useState('all');           // all | taxable | retirement | <account id>
  const [lookthrough, setLookthrough] = React.useState(true);
  const [dim, setDim] = React.useState('sector');
  const [showAll, setShowAll] = React.useState(false);
  const [uploading, setUploading] = React.useState(false);
  const [running, setRunning] = React.useState(false);
  const [err, setErr] = React.useState('');
  const pollRef = React.useRef(null);

  const client = () => window._supabaseClient;

  const load = React.useCallback(async () => {
    if (!client()) { setAccounts([]); return; }
    const [{ data: a }, { data: s }, { data: h }, { data: an }, { data: funds }] = await Promise.all([
      client().from('pd_portfolio_accounts').select('*').eq('active', true).order('sort'),
      client().from('pd_portfolio_snapshots').select('*').order('as_of', { ascending: false }),
      client().from('pd_portfolio_holdings').select('*'),
      client().from('pd_portfolio_analyses').select('*').order('as_of', { ascending: false }).limit(1),
      client().from('personal_assets').select('name, manual_value').eq('category', 'fund'),
    ]);
    setAccounts(a || []); setSnaps(s || []); setHoldings(h || []);
    setAnalysisRow((an || [])[0] || null);
    const nav = (pat) => Number(((funds || []).find((f) => f.name.toLowerCase().includes(pat)) || {}).manual_value || 0);
    setNavs({ vac: nav('valueact'), fbc: nav('finch bay') });
    // FBC look-through — the Markets Dashboard's position table (owner-only RLS).
    try {
      const { data: day } = await client().from('fbc_days').select('report_date').order('report_date', { ascending: false }).limit(1).maybeSingle();
      if (day) {
        const { data: longs } = await client().from('fbc_positions').select('description, ticker, sector, delta_pct').eq('report_date', day.report_date).eq('side', 'long');
        setFbcLongs((longs || []).map((l) => ({ ...l, report_date: day.report_date })));
      }
    } catch { setFbcLongs([]); }
  }, []);
  React.useEffect(() => { load(); return () => clearInterval(pollRef.current); }, [load]);

  // Poll the analysis row while a run is in flight (it lands in the background).
  const runAnalysis = async () => {
    setRunning(true); setErr('');
    try {
      await ppInvoke({ action: 'analyze' });
      clearInterval(pollRef.current);
      pollRef.current = setInterval(async () => {
        const { data } = await client().from('pd_portfolio_analyses').select('*').order('as_of', { ascending: false }).limit(1);
        const row = (data || [])[0];
        if (row && row.status !== 'running') {
          clearInterval(pollRef.current);
          setAnalysisRow(row); setRunning(false);
          if (row.status === 'error') setErr(row.error || 'Analysis failed.');
        }
      }, 5000);
    } catch (e) { setErr(e.message); setRunning(false); }
  };

  if (accounts === null) return null;

  // ── Assemble position rows for the current scope ────────────────────────────
  const latestAsOf = {};
  for (const s of snaps) if (!latestAsOf[s.account_id] || s.as_of > latestAsOf[s.account_id]) latestAsOf[s.account_id] = s.as_of;
  for (const h of holdings) if (!latestAsOf[h.account_id] || h.as_of > latestAsOf[h.account_id]) latestAsOf[h.account_id] = h.as_of;

  const direct = accounts.filter((a) => a.kind !== 'fund');
  const inScope = (a) => scope === 'all' ? true : scope === 'taxable' ? a.kind === 'taxable' : scope === 'retirement' ? PP_RETIREMENT.includes(a.kind) : a.id === scope;
  const scopeAccounts = direct.filter(inScope);
  const includeFunds = lookthrough && (scope === 'all' || scope === 'taxable');

  const rows = [];   // {key, ticker, name, value, basis, term, sector, geography, asset_class, source, accounts:[]}
  for (const a of scopeAccounts) {
    for (const h of holdings.filter((x) => x.account_id === a.id && x.as_of === latestAsOf[a.id])) {
      if (h.market_value == null) continue;
      rows.push({ ticker: h.ticker, name: h.name, value: Number(h.market_value), basis: h.cost_basis != null ? Number(h.cost_basis) : null,
        term: h.term, sector: h.sector || 'Unclassified', geography: h.geography || 'Unclassified', asset_class: h.asset_class || 'equity', source: a.label, note: h.note });
    }
  }
  if (includeFunds) {
    if (navs.vac > 0) {
      const vacAsOf = latestAsOf.vac_fund;
      for (const h of holdings.filter((x) => x.account_id === 'vac_fund' && x.as_of === vacAsOf && x.fund_pct != null)) {
        rows.push({ ticker: h.ticker, name: h.name, value: (Number(h.fund_pct) / 100) * navs.vac, basis: null, term: null,
          sector: h.sector || 'Undisclosed', geography: h.geography || 'Global', asset_class: h.asset_class || 'equity', source: 'ValueAct (look-through)', lt: 'VAC' });
      }
    }
    if (navs.fbc > 0 && fbcLongs.length) {
      for (const p of fbcLongs) {
        rows.push({ ticker: p.ticker, name: p.description, value: (Number(p.delta_pct || 0) / 100) * navs.fbc, basis: null, term: null,
          sector: p.sector || 'Unclassified', geography: 'Global', asset_class: 'equity', source: 'Finch Bay (look-through)', lt: 'FBC' });
      }
    }
  }

  // Consolidate by ticker/name across accounts.
  const byKey = {};
  for (const r of rows) {
    const k = (r.ticker || r.name).toUpperCase();
    const g = (byKey[k] = byKey[k] || { key: k, ticker: r.ticker, name: r.name, value: 0, basis: 0, hasBasis: false, sources: new Set(), lts: new Set(), sector: r.sector, geography: r.geography, asset_class: r.asset_class, terms: new Set() });
    g.value += r.value;
    if (r.basis != null) { g.basis += r.basis; g.hasBasis = true; }
    g.sources.add(r.source);
    if (r.lt) g.lts.add(r.lt);
    if (r.term) g.terms.add(r.term);
  }
  const positions = Object.values(byKey).filter((p) => p.value !== 0).sort((x, y) => y.value - x.value);
  const posTotal = positions.reduce((s, p) => s + Math.max(0, p.value), 0);

  // Headline numbers (account totals net of margin; funds at NAV).
  const sumKind = (pred) => direct.filter(pred).reduce((s, a) => s + Number((snaps.find((x) => x.account_id === a.id && x.as_of === latestAsOf[a.id]) || {}).total_value || 0), 0);
  const taxableTotal = sumKind((a) => a.kind === 'taxable');
  const retirementTotal = sumKind((a) => PP_RETIREMENT.includes(a.kind));
  const fundsTotal = navs.vac + navs.fbc;
  const marginDebt = direct.reduce((s, a) => { const c = Number((snaps.find((x) => x.account_id === a.id && x.as_of === latestAsOf[a.id]) || {}).cash || 0); return s + (c < 0 ? c : 0); }, 0);
  const grandTotal = taxableTotal + retirementTotal + fundsTotal;
  const asOfLabel = Object.values(latestAsOf).sort().pop() || '—';

  // Exposure breakdown for the selected dimension.
  const dimTotals = {};
  for (const p of positions) {
    const k = dim === 'source' ? [...p.sources][0] : p[dim] || 'Unclassified';
    dimTotals[k] = (dimTotals[k] || 0) + Math.max(0, p.value);
  }
  const dimRows = Object.entries(dimTotals).sort((a, b) => b[1] - a[1]);

  const chipStyle = (on) => ({ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.06em', padding: '6px 12px', cursor: 'pointer', whiteSpace: 'nowrap',
    border: `1px solid ${on ? tokens.ink : tokens.inkLine}`, background: on ? tokens.ink : 'none', color: on ? tokens.paper : tokens.inkMute });

  const shown = showAll ? positions : positions.slice(0, 20);

  return (
    <PdShell collapseKey="pd.portfolio" title="PORTFOLIO OVERVIEW · LOOK-THROUGH"
      right={`AS OF ${asOfLabel}`}
      actions={[{ label: '⬆ INGEST STATEMENT', onClick: () => setUploading(true), primary: false }, { label: running ? 'ANALYZING…' : '⚡ PM REVIEW', onClick: runAnalysis, running }]}>
      <div style={{ padding: '16px 20px' }}>
        {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginBottom: '10px' }}>{err}</div>}

        {/* headline tiles */}
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(4, 1fr)', gap: '10px', marginBottom: '14px' }}>
          {[['TOTAL PORTFOLIO', grandTotal, tokens.ink], ['TAXABLE', taxableTotal, PP_KIND.taxable.color], ['RETIREMENT', retirementTotal, PP_KIND['401k'].color], ['MARGIN DEBT', marginDebt, tokens.red]].map(([k, v, c]) => (
            <div key={k} style={{ border: `1px solid ${tokens.inkLine}`, padding: '10px 14px', background: tokens.bg }}>
              <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '4px' }}>{k}</div>
              <div style={{ fontSize: '20px', letterSpacing: '-0.02em', fontFamily: fontDisplay, color: c }}>{pdUsd0(v)}</div>
              {k === 'TOTAL PORTFOLIO' && <div style={{ fontFamily: fontMono, fontSize: '8px', color: tokens.inkMute, marginTop: '2px' }}>INCL. VAC {pdUsd0(navs.vac)} + FBC {pdUsd0(navs.fbc)} LP STAKES</div>}
              {k === 'MARGIN DEBT' && marginDebt < 0 && <div style={{ fontFamily: fontMono, fontSize: '8px', color: tokens.inkMute, marginTop: '2px' }}>MS 10.70% · JPM 10.75%</div>}
            </div>
          ))}
        </div>

        {/* scope + look-through */}
        <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '14px' }}>
          {[['all', 'ALL'], ['taxable', 'TAXABLE'], ['retirement', 'RETIREMENT']].map(([id, lbl]) => (
            <span key={id} onClick={() => setScope(id)} style={chipStyle(scope === id)}>{lbl}</span>
          ))}
          {direct.map((a) => (
            <span key={a.id} onClick={() => setScope(a.id)} style={chipStyle(scope === a.id)} title={a.label}>
              {a.label.replace(/ ·.*$/, '')} {a.account_number ? `·${a.account_number.slice(-4)}` : ''}
            </span>
          ))}
          <span style={{ flexGrow: 1 }} />
          <span onClick={() => setLookthrough((x) => !x)} style={{ ...chipStyle(lookthrough), borderColor: lookthrough ? tokens.ochre : tokens.inkLine, background: lookthrough ? tokens.ochre : 'none' }}>
            {lookthrough ? '✓ ' : ''}VAC + FBC LOOK-THROUGH
          </span>
        </div>

        {(navs.vac === 0 || navs.fbc === 0) && lookthrough && (
          <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.ochre, marginBottom: '12px' }}>
            {navs.vac === 0 ? 'VALUEACT ' : ''}{navs.vac === 0 && navs.fbc === 0 ? '+ ' : ''}{navs.fbc === 0 ? 'FINCH BAY ' : ''}NAV NOT SET — ADD IT TO THE FUND ROW IN NET WORTH ABOVE TO ACTIVATE LOOK-THROUGH.
          </div>
        )}

        {/* exposure bar */}
        <div style={{ marginBottom: '18px' }}>
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '8px' }}>
            {PP_DIMS.map(([id, lbl]) => <span key={id} onClick={() => setDim(id)} style={chipStyle(dim === id)}>{lbl}</span>)}
          </div>
          {posTotal > 0 && (
            <React.Fragment>
              <div style={{ display: 'flex', height: '10px', borderRadius: '5px', overflow: 'hidden', border: `1px solid ${tokens.inkLine}` }}>
                {dimRows.map(([k, v], i) => <div key={k} title={`${k} — ${pdUsd0(v)}`} style={{ width: `${(v / posTotal) * 100}%`, background: PP_COLORS[i % PP_COLORS.length] }} />)}
              </div>
              <div style={{ display: 'flex', gap: '14px', flexWrap: 'wrap', marginTop: '7px' }}>
                {dimRows.map(([k, v], i) => (
                  <span key={k} style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.04em', color: tokens.inkSoft, display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
                    <span style={{ width: '7px', height: '7px', borderRadius: '50%', background: PP_COLORS[i % PP_COLORS.length] }} />
                    {k.toUpperCase()} {ppPct(v / posTotal)} · {pdUsd0(v)}
                  </span>
                ))}
              </div>
            </React.Fragment>
          )}
        </div>

        {/* consolidated positions */}
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '640px' }}>
            <thead><tr>
              <th style={pdTh}>POSITION</th><th style={pdTh}>SECTOR</th><th style={pdTh}>HELD IN</th>
              <th style={{ ...pdTh, textAlign: 'right' }}>VALUE</th><th style={{ ...pdTh, textAlign: 'right' }}>WEIGHT</th>
              <th style={{ ...pdTh, textAlign: 'right' }}>BASIS</th><th style={{ ...pdTh, textAlign: 'right' }}>UNREAL G/L</th><th style={pdTh}>TERM</th>
            </tr></thead>
            <tbody>
              {shown.map((p) => {
                const gl = p.hasBasis ? p.value - p.basis : null;
                return (
                  <tr key={p.key}>
                    <td style={pdTd}>
                      <span style={{ fontWeight: 500 }}>{p.ticker || '—'}</span>
                      <span style={{ color: tokens.inkSoft, marginLeft: '8px', fontSize: '12px' }}>{p.name}</span>
                      {[...p.lts].map((t) => <span key={t} style={{ fontFamily: fontMono, fontSize: '8px', letterSpacing: '0.08em', color: tokens.ochre, border: `1px solid ${tokens.ochre}`, padding: '1px 5px', marginLeft: '6px' }}>{t}</span>)}
                    </td>
                    <td style={{ ...pdTd, fontSize: '11.5px', color: tokens.inkSoft, whiteSpace: 'nowrap' }}>{p.sector}</td>
                    <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '9.5px', color: tokens.inkMute }}>{[...p.sources].map((s) => s.replace(/ ·.*$/, '').replace(' (look-through)', '')).join(' · ')}</td>
                    <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '12px', whiteSpace: 'nowrap' }}>{pdUsd0(p.value)}</td>
                    <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '11px', color: tokens.inkSoft }}>{posTotal ? ppPct(p.value / posTotal) : '—'}</td>
                    <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, whiteSpace: 'nowrap' }}>{p.hasBasis ? pdUsd0(p.basis) : '—'}</td>
                    <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '11px', whiteSpace: 'nowrap', color: gl == null ? tokens.inkMute : gl >= 0 ? tokens.green : tokens.red }}>{gl == null ? '—' : `${gl >= 0 ? '+' : ''}${pdUsd0(gl)}`}</td>
                    <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '9.5px', color: tokens.inkMute }}>{[...p.terms].join('/') || '—'}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        {positions.length > 20 && (
          <div onClick={() => setShowAll((x) => !x)} style={{ fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.08em', color: tokens.inkMute, cursor: 'pointer', padding: '8px 0' }}>
            {showAll ? '▴ SHOW TOP 20' : `▾ SHOW ALL ${positions.length} POSITIONS`}
          </div>
        )}
        <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '6px', lineHeight: 1.8 }}>
          VAC / FBC BADGES = LOOK-THROUGH FROM YOUR LP STAKES (FUND WEIGHT × NAV FROM NET WORTH{fbcLongs[0] ? ` · FBC LONG BOOK AS OF ${fbcLongs[0].report_date}` : ''}) ·
          FUND NAVS ARE EDITED IN THE NET WORTH SECTION · INGESTING A STATEMENT UPDATES NET WORTH AUTOMATICALLY
        </div>

        <PpAnalysis row={analysisRow} running={running} onRun={runAnalysis} err={err} />
      </div>
      {uploading && <PpUploadModal onDone={() => { setUploading(false); load(); }} onCancel={() => setUploading(false)} />}
    </PdShell>
  );
};

// ── Options Strategies ────────────────────────────────────────────────────────
// Interactive option trees (CBOE delayed data via the options-intel Edge
// Function) for the big JOBY / AUR positions plus any prospect ticker, with IV
// history context and a Claude strategy review that knows the positions, the
// vesting triggers (embedded calls), and the UK-tax situation.

const PO_PINNED = ['JOBY', 'AUR'];

const poInvoke = async (body) => {
  const client = window._supabaseClient;
  if (!client) throw new Error('No backend connected.');
  const { data, error } = await client.functions.invoke('options-intel', { body });
  let payload = data;
  if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
  if (payload && payload.error) throw new Error(payload.error);
  return payload;
};

const poFmt = (v, d = 2) => v == null || isNaN(v) ? '—' : Number(v).toFixed(d);
// Vol figures can come back as a decimal fraction (0.85) or a percent (85) —
// normalize to percent before display.
const poIvPct = (v) => v == null || isNaN(v) ? null : (Math.abs(v) <= 3 ? v * 100 : Number(v));
const poAnnYield = (bid, spot, days) => bid != null && spot > 0 && days > 0 ? (bid / spot) * (365 / days) * 100 : null;
const poProse = { fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.65, maxWidth: '105ch' };

// One expiration's T-chart: calls | strike | puts, ATM highlighted, covered-
// call annualized yield on the call side.
const PoChainTable = ({ exp, spot }) => {
  const strikes = [...new Set([...(exp.calls || []).map((c) => c.strike), ...(exp.puts || []).map((p) => p.strike)])].sort((a, b) => a - b);
  const callBy = {}; for (const c of exp.calls || []) callBy[c.strike] = c;
  const putBy = {}; for (const p of exp.puts || []) putBy[p.strike] = p;
  const atm = strikes.reduce((best, s) => Math.abs(s - spot) < Math.abs(best - spot) ? s : best, strikes[0] || 0);
  const cell = { ...pdTd, fontFamily: fontMono, fontSize: '10.5px', textAlign: 'right', whiteSpace: 'nowrap', padding: '4px 7px' };
  return (
    <div style={{ overflowX: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '780px' }}>
        <thead>
          <tr>
            <th colSpan={6} style={{ ...pdTh, textAlign: 'center', color: tokens.green, borderRight: `1px solid ${tokens.inkLine}` }}>CALLS</th>
            <th style={{ ...pdTh, textAlign: 'center' }}>STRIKE</th>
            <th colSpan={4} style={{ ...pdTh, textAlign: 'center', borderLeft: `1px solid ${tokens.inkLine}`, color: tokens.red }}>PUTS</th>
          </tr>
          <tr>
            {['OI', 'VOL', 'IV', 'Δ', 'BID', 'ASK'].map((h, i) => <th key={h} style={{ ...pdTh, textAlign: 'right', ...(i === 5 ? { borderRight: `1px solid ${tokens.inkLine}` } : {}) }}>{h}{h === 'BID' ? ' / CC YLD*' : ''}</th>)}
            <th style={{ ...pdTh, textAlign: 'center' }} />
            {['BID', 'ASK', 'IV', 'OI'].map((h, i) => <th key={h} style={{ ...pdTh, textAlign: 'right', ...(i === 0 ? { borderLeft: `1px solid ${tokens.inkLine}` } : {}) }}>{h}</th>)}
          </tr>
        </thead>
        <tbody>
          {strikes.map((s) => {
            const c = callBy[s], p = putBy[s];
            const isAtm = s === atm;
            const yld = c && s >= spot ? poAnnYield(c.bid, spot, exp.days) : null;
            const rowBg = isAtm ? 'rgba(201,162,74,0.14)' : undefined;
            return (
              <tr key={s} style={{ background: rowBg }}>
                <td style={{ ...cell, color: tokens.inkMute, background: s < spot ? 'rgba(90,122,62,0.06)' : undefined }}>{c ? (c.oi ?? 0).toLocaleString() : ''}</td>
                <td style={{ ...cell, color: tokens.inkMute }}>{c && c.vol != null ? c.vol.toLocaleString() : ''}</td>
                <td style={cell}>{c && c.iv != null ? `${poFmt(c.iv, 0)}%` : ''}</td>
                <td style={{ ...cell, color: tokens.inkMute }}>{c && c.delta != null ? poFmt(c.delta, 2) : ''}</td>
                <td style={cell}>
                  {c ? poFmt(c.bid) : ''}
                  {yld != null && yld > 0.5 && <span style={{ color: tokens.green, marginLeft: '5px', fontSize: '9px' }}>{poFmt(yld, 1)}%</span>}
                </td>
                <td style={{ ...cell, borderRight: `1px solid ${tokens.inkLine}` }}>{c ? poFmt(c.ask) : ''}</td>
                <td style={{ ...cell, textAlign: 'center', fontWeight: isAtm ? 700 : 500, color: tokens.ink }}>{s}{isAtm ? ' ◂' : ''}</td>
                <td style={{ ...cell, borderLeft: `1px solid ${tokens.inkLine}` }}>{p ? poFmt(p.bid) : ''}</td>
                <td style={cell}>{p ? poFmt(p.ask) : ''}</td>
                <td style={cell}>{p && p.iv != null ? `${poFmt(p.iv, 0)}%` : ''}</td>
                <td style={{ ...cell, color: tokens.inkMute, background: s > spot ? 'rgba(160,74,62,0.05)' : undefined }}>{p ? (p.oi ?? 0).toLocaleString() : ''}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

// The Claude options strategy review renderer.
const PoAnalysis = ({ row, running, onRun, err }) => {
  const d = row && row.analysis;
  const [folded, toggleFold] = pdCollapsed('fold.options.review', false);
  const secLbl = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '16px 0 6px' };
  const chip = (text, color) => <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', border: `1px solid ${color}`, color, whiteSpace: 'nowrap' }}>{text}</span>;
  const ivColor = (v) => v === 'rich' ? tokens.green : v === 'cheap' ? tokens.red : tokens.ochre;
  return (
    <div style={{ borderTop: `1px solid ${tokens.inkLine}`, marginTop: '16px', paddingTop: '14px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
        <span onClick={toggleFold} title={folded ? 'Show the Claude output' : 'Hide the Claude output'} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.16em', color: tokens.inkMute, cursor: 'pointer', userSelect: 'none' }}>
          {folded ? '▸' : '▾'} CLAUDE STRATEGY REVIEW{row && row.generated_at ? ` · ${new Date(row.generated_at).toLocaleDateString()}` : ''}{running ? ' · RUNNING…' : ''}{folded && d ? ' · HIDDEN' : ''}
        </span>
        <button onClick={onRun} disabled={running} style={pdBtn(false)}>{running ? 'ANALYZING…' : (d ? '↻ RE-RUN' : '⚡ STRATEGY REVIEW')}</button>
      </div>
      {err && <div style={{ color: tokens.red, fontSize: '12px', marginTop: '8px' }}>{err}</div>}
      {!d && !running && !err && !folded && (
        <div style={{ fontSize: '12.5px', color: tokens.inkSoft, marginTop: '8px' }}>
          Runs a position-aware review: IV rich/cheap vs history and realized, the catalyst calendar, and specific trades (strike · expiry · premium · annualized yield · assignment risk) that respect your vesting triggers and UK-tax timeline. Tune it in Admin → Claude feedback.
        </div>
      )}
      {d && !folded && (
        <div style={{ marginTop: '10px' }}>
          {d.position_read && <div style={{ fontSize: '13.5px', lineHeight: 1.65, maxWidth: '105ch' }}>{d.position_read}</div>}
          {d.iv_read && (
            <div style={{ marginTop: '12px' }}>
              <div style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap' }}>
                {chip(`IV ${(d.iv_read.verdict || '').toUpperCase()}`, ivColor(d.iv_read.verdict))}
                {poIvPct(d.iv_read.atm_iv30) != null && <span style={{ fontFamily: fontMono, fontSize: '10.5px', letterSpacing: '0.04em', color: tokens.inkSoft }}>ATM 30D {poFmt(poIvPct(d.iv_read.atm_iv30), 0)}% · REALIZED {poFmt(poIvPct(d.iv_read.realized30), 0)}%</span>}
              </div>
              <div style={{ ...poProse, marginTop: '6px' }}>{d.iv_read.read}{d.iv_read.vs_history ? ` ${d.iv_read.vs_history}` : ''}</div>
            </div>
          )}
          {Array.isArray(d.catalysts) && d.catalysts.length > 0 && (<div>
            <div style={secLbl}>CATALYST CALENDAR</div>
            {d.catalysts.map((c, i) => (
              <div key={i} style={{ padding: '7px 0', borderBottom: i < d.catalysts.length - 1 ? `1px solid ${tokens.inkLineSoft}` : 'none' }}>
                <div style={{ fontSize: '13px', lineHeight: 1.5 }}>
                  <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre, marginRight: '10px' }}>{c.window}</span>
                  <span style={{ fontWeight: 500 }}>{c.event}</span>
                </div>
                <div style={{ ...poProse, fontSize: '12.5px', marginTop: '3px' }}>{c.read}</div>
              </div>
            ))}
          </div>)}
          {Array.isArray(d.strategies) && d.strategies.length > 0 && (<div>
            <div style={secLbl}>STRATEGIES</div>
            {d.strategies.map((s, i) => (
              <div key={i} style={{ border: `1px solid ${tokens.inkLineSoft}`, background: tokens.bg, padding: '10px 14px', marginBottom: '8px' }}>
                <div style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap' }}>
                  {chip((s.type || 'strategy').replace(/_/g, ' ').toUpperCase(), s.type === 'covered_call' ? tokens.green : s.type === 'protective_put' || s.type === 'collar' ? tokens.ochre : tokens.ink)}
                  <span style={{ fontWeight: 600, fontSize: '13.5px' }}>{s.name}</span>
                  {s.horizon && <span style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.inkMute }}>{s.horizon.toUpperCase()}</span>}
                  <span style={{ flexGrow: 1 }} />
                  {s.premium_usd != null && s.premium_usd > 0 && <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.green }}>{pdUsd0(s.premium_usd)}{s.annualized_yield_pct != null ? ` · ${poFmt(s.annualized_yield_pct, 1)}%/YR` : ''}</span>}
                  {s.conviction && <span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>{s.conviction.toUpperCase()}</span>}
                </div>
                {s.structure && <div style={{ fontFamily: fontMono, fontSize: '11.5px', color: tokens.ink, marginTop: '7px', lineHeight: 1.6, maxWidth: '110ch', whiteSpace: 'pre-wrap', background: 'rgba(14,14,12,0.035)', padding: '6px 10px', borderLeft: `2px solid ${tokens.inkLine}` }}>{s.structure}</div>}
                <div style={{ ...poProse, marginTop: '7px' }}>{s.why}</div>
                {s.assignment_risk && <div style={{ ...poProse, fontSize: '12px', marginTop: '5px' }}><span style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>ASSIGNMENT · </span>{s.assignment_risk}</div>}
                {s.vesting_interaction && <div style={{ ...poProse, fontSize: '12px', color: tokens.ochre, marginTop: '5px' }}><span style={{ fontFamily: fontMono, fontSize: '9px' }}>VESTING · </span>{s.vesting_interaction}</div>}
                {s.tax_note && <div style={{ ...poProse, fontSize: '12px', color: '#9A6A1A', marginTop: '5px' }}><span style={{ fontFamily: fontMono, fontSize: '9px' }}>TAX · </span>{s.tax_note}</div>}
              </div>
            ))}
          </div>)}
          {d.income_summary && <div style={{ fontSize: '13px', lineHeight: 1.65, marginTop: '8px', maxWidth: '105ch' }}><span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.1em', color: tokens.green }}>INCOME · </span>{d.income_summary}</div>}
          {Array.isArray(d.risks) && d.risks.length > 0 && (<div>
            <div style={secLbl}>RISKS</div>
            {d.risks.map((r, i) => <div key={i} style={{ ...poProse, fontSize: '12.5px', padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {r}</div>)}
          </div>)}
          {Array.isArray(d.watch_items) && d.watch_items.length > 0 && (<div>
            <div style={secLbl}>NEXT</div>
            {d.watch_items.map((w, i) => <div key={i} style={{ ...poProse, fontSize: '12.5px', padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}
          </div>)}
        </div>
      )}
    </div>
  );
};

const PdOptionsPanel = () => {
  const isMobile = useIsMobile();
  const [chains, setChains] = React.useState(null);        // {ticker: chainRow}
  const [analyses, setAnalyses] = React.useState({});      // {ticker: analysisRow}
  const [ivHist, setIvHist] = React.useState({});          // {ticker: rows[]}
  const [ticker, setTicker] = React.useState('JOBY');
  const [expIdx, setExpIdx] = React.useState(0);
  const [custom, setCustom] = React.useState('');
  const [busy, setBusy] = React.useState('');               // '' | 'chain' | 'analyze'
  const [err, setErr] = React.useState('');
  const pollRef = React.useRef(null);

  const client = () => window._supabaseClient;

  const load = React.useCallback(async () => {
    if (!client()) { setChains({}); return; }
    const [{ data: ch }, { data: an }, { data: iv }] = await Promise.all([
      client().from('pd_option_chains').select('*'),
      client().from('pd_option_analyses').select('*'),
      client().from('pd_option_iv_history').select('*').order('as_of', { ascending: true }),
    ]);
    const cm = {}; for (const r of ch || []) cm[r.ticker] = r;
    const am = {}; for (const r of an || []) am[r.ticker] = r;
    const im = {}; for (const r of iv || []) (im[r.ticker] = im[r.ticker] || []).push(r);
    setChains(cm); setAnalyses(am); setIvHist(im);
  }, []);
  React.useEffect(() => { load(); return () => clearInterval(pollRef.current); }, [load]);

  const refreshChain = async (t) => {
    setBusy('chain'); setErr('');
    try { await poInvoke({ action: 'chain', ticker: t }); await load(); setExpIdx(0); }
    catch (e) { setErr(e.message); }
    finally { setBusy(''); }
  };

  const runAnalysis = async (t) => {
    setBusy('analyze'); setErr('');
    try {
      await poInvoke({ action: 'analyze', ticker: t });
      clearInterval(pollRef.current);
      pollRef.current = setInterval(async () => {
        const { data } = await client().from('pd_option_analyses').select('*').eq('ticker', t).maybeSingle();
        if (data && data.status !== 'running') {
          clearInterval(pollRef.current);
          setAnalyses((m) => ({ ...m, [t]: data })); setBusy('');
          if (data.status === 'error') setErr(data.error || 'Analysis failed.');
          load();
        }
      }, 5000);
    } catch (e) { setErr(e.message); setBusy(''); }
  };

  const addCustom = async (e) => {
    e.preventDefault();
    const t = custom.trim().toUpperCase().replace(/[^A-Z.]/g, '');
    if (!t) return;
    setCustom(''); setTicker(t); setExpIdx(0);
    if (!(chains || {})[t]) await refreshChain(t);
  };

  if (chains === null) return null;

  const tickers = [...new Set([...PO_PINNED, ...Object.keys(chains)])];
  const row = chains[ticker];
  const chain = row ? { spot: Number(row.spot), expirations: (row.data && row.data.expirations) || [], fetchedAt: row.fetched_at, source: row.source } : null;
  const exps = chain ? chain.expirations : [];
  const exp = exps[Math.min(expIdx, Math.max(0, exps.length - 1))];
  const hist = ivHist[ticker] || [];
  const lastIv = hist.length ? hist[hist.length - 1] : null;

  const chipStyle = (on) => ({ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.06em', padding: '6px 12px', cursor: 'pointer', whiteSpace: 'nowrap',
    border: `1px solid ${on ? tokens.ink : tokens.inkLine}`, background: on ? tokens.ink : 'none', color: on ? tokens.paper : tokens.inkMute });

  return (
    <PdShell collapseKey="pd.options" title="OPTIONS STRATEGIES · INCOME, HEDGES & PROSPECTS"
      right={chain ? `${ticker} $${poFmt(chain.spot)} · ${new Date(chain.fetchedAt).toLocaleDateString()}` : ''}
      actions={[{ label: '⟳ REFRESH CHAIN', onClick: () => refreshChain(ticker), running: busy === 'chain', primary: false },
                { label: busy === 'analyze' ? 'ANALYZING…' : '⚡ STRATEGY REVIEW', onClick: () => runAnalysis(ticker), running: busy === 'analyze' }]}>
      <div style={{ padding: '16px 20px' }}>
        {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginBottom: '10px' }}>{err}</div>}

        {/* ticker tabs + prospect input */}
        <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '14px' }}>
          {tickers.map((t) => (
            <span key={t} onClick={() => { setTicker(t); setExpIdx(0); }} style={{ ...chipStyle(ticker === t), ...(PO_PINNED.includes(t) ? { borderColor: ticker === t ? tokens.ink : tokens.ochre } : {}) }}>
              {t}{PO_PINNED.includes(t) ? ' ◆' : ''}
            </span>
          ))}
          <form onSubmit={addCustom} style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
            <input style={{ ...pdInp, width: '110px', padding: '6px 10px', fontSize: '11px', fontFamily: fontMono }} value={custom}
              onChange={(e) => setCustom(e.target.value)} placeholder="ADD TICKER…" />
            <button type="submit" style={pdBtn(false)}>LOAD TREE</button>
          </form>
        </div>

        {!chain ? (
          <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.7 }}>
            No chain loaded for <span style={{ fontFamily: fontMono }}>{ticker}</span> yet — hit <span style={{ color: tokens.ink }}>⟳ REFRESH CHAIN</span> to pull the full option tree (every expiration through the LEAPS, with open interest and implied vol) from CBOE delayed data.
          </div>
        ) : (
          <React.Fragment>
            {/* IV context strip */}
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(5, 1fr)', gap: '10px', marginBottom: '14px' }}>
              {[['SPOT', `$${poFmt(chain.spot)}`, tokens.ink],
                ['ATM IV 30D', lastIv && lastIv.iv30 != null ? `${poFmt(lastIv.iv30, 0)}%` : '—', tokens.ink],
                ['ATM IV 1YR', lastIv && lastIv.iv365 != null ? `${poFmt(lastIv.iv365, 0)}%` : '—', tokens.ink],
                ['REALIZED 30D', lastIv && lastIv.realized30 != null ? `${poFmt(lastIv.realized30, 0)}%` : '—', tokens.inkSoft],
                ['IV PREMIUM', lastIv && lastIv.iv30 != null && lastIv.realized30 != null ? `${poFmt(lastIv.iv30 - lastIv.realized30, 0)}pts` : '—',
                  lastIv && lastIv.iv30 != null && lastIv.realized30 != null ? (lastIv.iv30 > lastIv.realized30 ? tokens.green : tokens.red) : tokens.inkMute]].map(([k, v, c]) => (
                <div key={k} style={{ border: `1px solid ${tokens.inkLine}`, padding: '8px 12px', background: tokens.bg }}>
                  <div style={{ fontFamily: fontMono, fontSize: '8px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '3px' }}>{k}</div>
                  <div style={{ fontSize: '17px', letterSpacing: '-0.02em', fontFamily: fontDisplay, color: c }}>{v}</div>
                </div>
              ))}
            </div>

            {/* IV history bars (30d ATM per snapshot; deepens with every refresh + Bloomberg backfill) */}
            {hist.filter((h) => h.iv30 != null).length > 1 && (
              <div style={{ marginBottom: '14px' }}>
                <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.inkMute, marginBottom: '5px' }}>ATM 30D IMPLIED VOL HISTORY</div>
                <div style={{ display: 'flex', alignItems: 'flex-end', gap: '2px', height: '44px' }}>
                  {hist.filter((h) => h.iv30 != null).slice(-60).map((h, i, arr) => {
                    const max = Math.max(...arr.map((x) => Number(x.iv30)));
                    const isLast = i === arr.length - 1;
                    return <div key={`${h.as_of}-${h.source}`} title={`${h.as_of} [${h.source}] — ${poFmt(h.iv30, 0)}%`}
                      style={{ flex: 1, maxWidth: '14px', height: `${Math.max(6, (Number(h.iv30) / max) * 44)}px`, background: isLast ? tokens.ochre : 'rgba(57,85,126,0.45)' }} />;
                  })}
                </div>
              </div>
            )}

            {/* expiration chips */}
            <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '10px' }}>
              {exps.map((e, i) => (
                <span key={e.date} onClick={() => setExpIdx(i)} style={{ ...chipStyle(exp && e.date === exp.date), padding: '5px 10px', fontSize: '9.5px' }}>
                  {e.date.slice(2)} · {e.days}D{e.days > 365 ? ' ★' : ''}
                </span>
              ))}
            </div>

            {exp && <PoChainTable exp={exp} spot={chain.spot} />}
            <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '6px', lineHeight: 1.8 }}>
              * CC YLD = ANNUALIZED COVERED-CALL YIELD (BID ÷ SPOT × 365/DTE), SHOWN FOR STRIKES ≥ SPOT · ◂ = AT-THE-MONEY · ★ = LEAPS ·
              CBOE DELAYED DATA{chain.source === 'yahoo' ? ' (YAHOO FALLBACK)' : ''} · EACH REFRESH APPENDS TO THE IV HISTORY (BLOOMBERG BACKFILL: scripts/bloomberg_options_iv.py)
            </div>
          </React.Fragment>
        )}

        <PoAnalysis row={analyses[ticker]} running={busy === 'analyze'} onRun={() => runAnalysis(ticker)} err={busy === 'analyze' ? '' : err} />
      </div>
    </PdShell>
  );
};

// ── Calendar (daniel@urdaneta.io Outlook, via calendar-hub / MS Graph) ───────
// Sits at the top of the page, outside the privacy lock. Site modules push
// events through calendar-hub's upsert_events (tagged source badges below).

const PdCalendarPanel = () => {
  const isMobile = useIsMobile();
  const tz = (Intl.DateTimeFormat().resolvedOptions().timeZone) || 'Europe/London';
  const HOUR = 44;   // px per hour in the day/week time grids

  // ── date helpers (all local wall-time) ──────────────────────────────────────
  const pad2 = (n) => String(n).padStart(2, '0');
  const dstr = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
  const addDays = (d, n) => { const x = new Date(d); x.setDate(x.getDate() + n); return x; };
  const mondayOf = (d) => addDays(d, -((d.getDay() + 6) % 7));
  const todayStr = dstr(new Date());

  const [mode, setMode] = React.useState(() => { try { return localStorage.getItem('pd.cal.mode') || 'month'; } catch { return 'month'; } });
  const pickMode = (m) => { setMode(m); try { localStorage.setItem('pd.cal.mode', m); } catch {} };
  const [anchor, setAnchor] = React.useState(new Date());
  const [state, setState] = React.useState({ loading: true, events: null, authorizeUrl: null, err: '' });
  const [busy, setBusy] = React.useState(false);
  const [digestBusy, setDigestBusy] = React.useState(false);
  const [msg, setMsg] = React.useState('');
  const [showAdd, setShowAdd] = React.useState(false);
  const [editingId, setEditingId] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [detail, setDetail] = React.useState(null);
  const gridRef = React.useRef(null);
  const blankDraft = () => ({ title: '', date: todayStr, time: '', end_date: '', end_time: '', location: '', attendees: '', notes: '', teams: false, repeat: 'none', until: '' });
  const [draft, setDraft] = React.useState(blankDraft());

  const chInvoke = async (body) => {
    const { data, error } = await window._supabaseClient.functions.invoke('calendar-hub', { body });
    let payload = data;
    if (error) { try { payload = await error.context.json(); } catch {} }
    if (payload && payload.error && payload.error !== 'not_connected') throw new Error(payload.error);
    return payload;
  };

  // The fetch window for the current mode.
  const rangeFor = (m, a) => {
    if (m === 'day') return [addDays(a, -1), addDays(a, 2)];
    if (m === 'week') { const mon = mondayOf(a); return [addDays(mon, -1), addDays(mon, 8)]; }
    const first = new Date(a.getFullYear(), a.getMonth(), 1);
    const gs = mondayOf(first);
    return [addDays(gs, -1), addDays(gs, 43)];
  };

  const load = React.useCallback(async (m, a) => {
    const client = window._supabaseClient;
    if (!client) { setState({ loading: false, events: null, authorizeUrl: null, err: 'No backend connected.' }); return; }
    try {
      const [gs, ge] = rangeFor(m, a);
      const payload = await chInvoke({ action: 'list_events', time_min: gs.toISOString(), time_max: ge.toISOString(), tz });
      if (payload && payload.error === 'not_connected') { setState({ loading: false, events: null, authorizeUrl: payload.authorize_url, err: '' }); return; }
      setState({ loading: false, events: (payload && payload.events) || [], authorizeUrl: null, err: '' });
    } catch (e) { setState({ loading: false, events: null, authorizeUrl: null, err: e.message }); }
  }, [tz]);
  React.useEffect(() => { load(mode, anchor); }, [load, mode, dstr(anchor)]);

  // Scroll the time grid to 07:30 when it appears.
  React.useEffect(() => { if (gridRef.current) gridRef.current.scrollTop = 7.5 * HOUR; }, [mode, state.loading]);

  const nav = (dir) => setAnchor((a) => {
    if (mode === 'day') return addDays(a, dir);
    if (mode === 'week') return addDays(a, dir * 7);
    return new Date(a.getFullYear(), a.getMonth() + dir, 1);
  });
  const goToday = () => setAnchor(new Date());
  const refresh = async () => { setBusy(true); await load(mode, anchor); setBusy(false); };

  const digest = async () => {
    setDigestBusy(true); setMsg('');
    try {
      const payload = await chInvoke({ action: 'digest', days: 7 });
      setMsg(payload.sent ? '✓ WEEK-AHEAD DIGEST EMAILED' : 'DIGEST BUILT — NO EMAIL PROVIDER CONFIGURED');
    } catch (e) { setMsg(`DIGEST FAILED — ${String(e.message).toUpperCase()}`); }
    finally { setDigestBusy(false); }
  };

  // Duration is an OUTPUT of start/end. Returns {label, valid}.
  const spanOf = (d) => {
    const endDate = d.end_date || d.date;
    if (!d.time) {
      if (endDate < d.date) return { label: 'ENDS BEFORE START', valid: false };
      const days = Math.round((new Date(endDate) - new Date(d.date)) / 86400000) + 1;
      return { label: `${days} DAY${days === 1 ? '' : 'S'} · ALL-DAY`, valid: true };
    }
    if (!d.end_time) return { label: '1H DEFAULT — SET AN END TIME', valid: true };
    const mins = Math.round((new Date(`${endDate}T${d.end_time}:00`) - new Date(`${d.date}T${d.time}:00`)) / 60000);
    if (mins <= 0) return { label: 'ENDS BEFORE START', valid: false };
    const dd = Math.floor(mins / 1440), hh = Math.floor((mins % 1440) / 60), mm = mins % 60;
    return { label: `= ${dd ? dd + 'D ' : ''}${hh ? hh + 'H ' : ''}${mm ? mm + 'M' : ''}`.trim() || '= 0M', valid: true };
  };
  const setStart = (patch) => setDraft((p) => {
    const n = { ...p, ...patch };
    if (patch.date && (!n.end_date || n.end_date < patch.date)) n.end_date = patch.date;
    if (patch.time && !n.end_time) {
      const [h, m] = patch.time.split(':').map(Number);
      n.end_time = `${String((h + 1) % 24).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
    }
    return n;
  });
  const openAddAt = (date, time) => {
    setEditingId(null);
    const d = { ...blankDraft(), date };
    if (time) { d.time = time; const [h, m] = time.split(':').map(Number); d.end_time = `${pad2((h + 1) % 24)}:${pad2(m)}`; d.end_date = date; }
    setDraft(d); setShowAdd(true);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const saveEvent = async (e) => {
    e.preventDefault();
    if (!draft.title.trim() || !draft.date) return;
    setSaving(true); setMsg('');
    try {
      if (!spanOf(draft).valid) throw new Error('End is before the start');
      const body = { action: editingId ? 'update_event' : 'create_event', id: editingId || undefined, tz,
        title: draft.title.trim(), date: draft.date, time: draft.time,
        end_date: draft.end_date || draft.date, end_time: draft.end_time,
        location: draft.location, description: draft.notes, attendees: draft.attendees, teams: draft.teams,
        repeat: editingId ? 'none' : draft.repeat, until: draft.until };
      await chInvoke(body);
      const invited = draft.attendees.trim() ? ' · INVITES SENT' : '';
      setMsg(`✓ ${editingId ? 'UPDATED' : 'ADDED'} "${draft.title.trim().toUpperCase()}"${!editingId && draft.repeat !== 'none' ? ` · REPEATS ${draft.repeat.toUpperCase()}` : ''}${invited}`);
      setDraft(blankDraft()); setShowAdd(false); setEditingId(null); setDetail(null);
      await load(mode, anchor);
    } catch (e2) { setMsg(`${editingId ? 'UPDATE' : 'ADD'} FAILED — ${String(e2.message).toUpperCase()}`); }
    finally { setSaving(false); }
  };

  const openDetail = async (ev) => {
    setDetail({ loading: true, ev: null });
    try {
      const payload = await chInvoke({ action: 'get_event', id: ev.id, tz });
      setDetail({ loading: false, ev: { ...ev, ...payload.event } });
    } catch (e) { setDetail(null); setMsg(`COULDN'T LOAD EVENT — ${String(e.message).toUpperCase()}`); }
  };

  const startEdit = (ev) => {
    const timed = ev.start.includes('T');
    let endDate = (ev.end || ev.start).slice(0, 10);
    if (!timed && ev.end) endDate = new Date(new Date(ev.end.slice(0, 10) + 'T00:00:00Z').getTime() - 86400000).toISOString().slice(0, 10);
    setDraft({
      title: ev.title, date: ev.start.slice(0, 10), time: timed ? ev.start.slice(11, 16) : '',
      end_date: endDate, end_time: timed && ev.end ? ev.end.slice(11, 16) : '',
      location: ev.location || '',
      attendees: (ev.attendees || []).map((a) => a.email).join(', '),
      notes: ev.notes || '', teams: !!ev.join_url, repeat: 'none', until: '',
    });
    setEditingId(ev.id); setShowAdd(true); setDetail(null);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const deleteEvent = async (id, label) => {
    if (!window.confirm(`Delete ${label}? Attendees get a normal Outlook cancellation.`)) return;
    setMsg('');
    try {
      await chInvoke({ action: 'delete_event', id });
      setMsg('✓ DELETED'); setDetail(null);
      await load(mode, anchor);
    } catch (e) { setMsg(`DELETE FAILED — ${String(e.message).toUpperCase()}`); }
  };

  // ── coverage: which days an event touches, and its timed segment per day ────
  const { loading, events, authorizeUrl, err } = state;
  const coverDays = (ev) => {
    const s = ev.start.slice(0, 10);
    let e = (ev.end || ev.start).slice(0, 10);
    if (ev.all_day && ev.end) {   // all-day ends are exclusive
      e = dstr(addDays(new Date(e + 'T12:00:00'), -1));
    } else if (!ev.all_day && ev.end && ev.end.slice(11, 16) === '00:00' && e > s) {
      e = dstr(addDays(new Date(e + 'T12:00:00'), -1));   // ends exactly at midnight
    }
    const out = [];
    let d = new Date(s + 'T12:00:00');
    for (let i = 0; i < 60 && dstr(d) <= e; i++) { out.push(dstr(d)); d = addDays(d, 1); }
    return out;
  };
  const byDay = {};   // day → {banner: [ev], timed: [ev]}  (banner = all-day or multi-day)
  for (const ev of events || []) {
    const days = coverDays(ev);
    const banner = ev.all_day || days.length > 1;
    for (const day of days) {
      const slot = (byDay[day] = byDay[day] || { banner: [], timed: [] });
      (banner ? slot.banner : slot.timed).push(ev);
    }
  }
  for (const k of Object.keys(byDay)) byDay[k].timed.sort((a, b) => a.start.localeCompare(b.start));

  // Timed segment (minutes within `day`) for the time grids.
  const segmentFor = (ev, day) => {
    const sDay = ev.start.slice(0, 10), eDay = (ev.end || ev.start).slice(0, 10);
    const startMin = sDay === day ? Number(ev.start.slice(11, 13)) * 60 + Number(ev.start.slice(14, 16)) : 0;
    let endMin = 24 * 60;
    if (ev.end && eDay === day) endMin = Number(ev.end.slice(11, 13)) * 60 + Number(ev.end.slice(14, 16));
    return { startMin, endMin: Math.max(endMin, startMin + 20) };
  };

  // ── shared bits ─────────────────────────────────────────────────────────────
  const navBtn = { fontFamily: fontMono, fontSize: '12px', padding: '4px 12px', cursor: 'pointer', border: `1px solid ${tokens.inkLine}`, background: 'none', color: tokens.inkSoft, userSelect: 'none' };
  const chip = (on) => ({ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.08em', padding: '5px 12px', cursor: 'pointer',
    border: `1px solid ${on ? tokens.ink : tokens.inkLine}`, background: on ? tokens.ink : 'none', color: on ? tokens.paper : tokens.inkMute, userSelect: 'none' });
  const rsvpChip = (r) => {
    const map = { accepted: ['✓', tokens.green], declined: ['✗', tokens.red], tentativelyAccepted: ['~', tokens.ochre] };
    const [glyph, color] = map[r] || ['·', tokens.inkMute];
    return <span style={{ fontFamily: fontMono, fontSize: '10px', color, marginRight: '5px' }}>{glyph}</span>;
  };

  // A banner (all-day / multi-day) chip inside a day cell; continuation days dim.
  const bannerChip = (ev, day, compact) => {
    const isStart = ev.start.slice(0, 10) === day;
    return (
      <div key={`${ev.id}-${day}`} onClick={() => openDetail(ev)}
        title={`${ev.title}${ev.location ? ' · ' + ev.location : ''}`}
        style={{ fontSize: compact ? '10.5px' : '12px', lineHeight: 1.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer',
          background: ev.source ? 'rgba(90,122,62,0.13)' : 'rgba(201,162,74,0.16)', color: tokens.ink,
          padding: '1px 5px', marginBottom: '1px',
          borderLeft: isStart ? `2px solid ${ev.source ? tokens.green : tokens.ochre}` : '2px solid transparent',
          opacity: isStart ? 1 : 0.75 }}>
        {isStart ? '' : '‹ '}{ev.title}
      </div>
    );
  };
  const timedLine = (ev, day, compact) => (
    <div key={`${ev.id}-${day}`} onClick={() => openDetail(ev)}
      title={`${ev.start.slice(11, 16)} — ${ev.title}${ev.location ? ' · ' + ev.location : ''}`}
      style={{ fontSize: compact ? '10.5px' : '12.5px', lineHeight: 1.45, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer',
        color: tokens.ink, padding: compact ? '1px 3px' : '2px 0', borderLeft: '2px solid transparent', paddingLeft: '5px' }}>
      <span style={{ fontFamily: fontMono, fontSize: compact ? '8.5px' : '9.5px', color: tokens.ochre, marginRight: '4px' }}>{ev.start.slice(11, 16)}</span>
      {ev.title}
    </div>
  );

  // ── views ───────────────────────────────────────────────────────────────────
  const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
  const monthGridDays = Array.from({ length: 42 }, (_, i) => addDays(mondayOf(first), i));
  const weekDays = Array.from({ length: 7 }, (_, i) => addDays(mondayOf(anchor), i));
  const viewDays = mode === 'day' ? [anchor] : weekDays;

  const headerLabel = mode === 'day'
    ? anchor.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
    : mode === 'week'
      ? `${weekDays[0].getDate()} ${weekDays[0].toLocaleDateString('en-GB', { month: 'short' })} – ${weekDays[6].getDate()} ${weekDays[6].toLocaleDateString('en-GB', { month: 'short', year: 'numeric' })}`
      : first.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });

  const monthView = (
    <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', borderBottom: `1px solid ${tokens.inkLine}` }}>
        {['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'].map((d) => (
          <div key={d} style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, textAlign: 'center', padding: '6px 0' }}>{d}</div>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))' }}>
        {monthGridDays.map((d, i) => {
          const ds = dstr(d);
          const inMonth = d.getMonth() === anchor.getMonth();
          const isToday = ds === todayStr;
          const slot = byDay[ds] || { banner: [], timed: [] };
          const shown = [...slot.banner.map((ev) => ['b', ev]), ...slot.timed.map((ev) => ['t', ev])];
          return (
            <div key={ds} style={{ minHeight: '86px', padding: '4px 4px 6px', borderRight: (i % 7) < 6 ? `1px solid ${tokens.inkLineSoft}` : 'none', borderBottom: i < 35 ? `1px solid ${tokens.inkLineSoft}` : 'none',
              background: isToday ? 'rgba(201,162,74,0.08)' : inMonth ? 'none' : 'rgba(14,14,12,0.02)', minWidth: 0 }}>
              <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: isToday ? tokens.ochre : inMonth ? tokens.inkSoft : tokens.inkLine, fontWeight: isToday ? 700 : 400, marginBottom: '2px', cursor: 'pointer' }}
                title="Add an event on this day" onClick={() => openAddAt(ds)}>
                {d.getDate()}{isToday ? ' ●' : ''}
              </div>
              {shown.slice(0, 3).map(([kind, ev]) => kind === 'b' ? bannerChip(ev, ds, true) : timedLine(ev, ds, true))}
              {shown.length > 3 && <div title={shown.slice(3).map(([, ev]) => ev.title).join('\n')}
                style={{ fontFamily: fontMono, fontSize: '8.5px', color: tokens.inkMute, paddingLeft: '5px', cursor: 'default' }}>+{shown.length - 3} MORE</div>}
            </div>
          );
        })}
      </div>
    </div>
  );

  const nowMin = new Date().getHours() * 60 + new Date().getMinutes();
  const timeGrid = (
    <div style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.paper, overflowX: 'auto' }}>
      <div style={{ minWidth: mode === 'week' ? '720px' : '320px' }}>
        {/* day headers */}
        <div style={{ display: 'flex', borderBottom: `1px solid ${tokens.inkLine}` }}>
          <div style={{ width: '46px', flexShrink: 0 }} />
          {viewDays.map((d) => {
            const ds = dstr(d);
            return (
              <div key={ds} style={{ flex: 1, textAlign: 'center', padding: '7px 0 5px', minWidth: 0, background: ds === todayStr ? 'rgba(201,162,74,0.08)' : 'none' }}>
                <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: ds === todayStr ? tokens.ochre : tokens.inkMute }}>
                  {d.toLocaleDateString('en-GB', { weekday: 'short' }).toUpperCase()} {d.getDate()}{ds === todayStr ? ' ●' : ''}
                </span>
              </div>
            );
          })}
        </div>
        {/* all-day / multi-day lane */}
        <div style={{ display: 'flex', borderBottom: `1px solid ${tokens.inkLine}` }}>
          <div style={{ width: '46px', flexShrink: 0, fontFamily: fontMono, fontSize: '7.5px', letterSpacing: '0.08em', color: tokens.inkMute, textAlign: 'right', padding: '4px 6px 4px 0' }}>ALL DAY</div>
          {viewDays.map((d) => {
            const ds = dstr(d);
            const banners = (byDay[ds] || { banner: [] }).banner;
            return (
              <div key={ds} style={{ flex: 1, minWidth: 0, padding: '3px 3px', borderLeft: `1px solid ${tokens.inkLineSoft}`, minHeight: '24px', background: ds === todayStr ? 'rgba(201,162,74,0.04)' : 'none' }}>
                {banners.map((ev) => bannerChip(ev, ds, mode === 'week'))}
              </div>
            );
          })}
        </div>
        {/* hour grid */}
        <div ref={gridRef} style={{ maxHeight: '560px', overflowY: 'auto' }}>
          <div style={{ display: 'flex', position: 'relative', height: `${24 * HOUR}px` }}>
            <div style={{ width: '46px', flexShrink: 0, position: 'relative' }}>
              {Array.from({ length: 24 }, (_, h) => (
                <div key={h} style={{ position: 'absolute', top: `${h * HOUR - 6}px`, right: '6px', fontFamily: fontMono, fontSize: '8.5px', color: tokens.inkMute }}>{h > 0 ? `${pad2(h)}:00` : ''}</div>
              ))}
            </div>
            {viewDays.map((d) => {
              const ds = dstr(d);
              const timed = (byDay[ds] || { timed: [] }).timed;
              return (
                <div key={ds} onClick={(e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const h = Math.max(0, Math.min(23, Math.floor((e.clientY - rect.top) / HOUR)));
                    openAddAt(ds, `${pad2(h)}:00`);
                  }}
                  style={{ flex: 1, minWidth: 0, position: 'relative', borderLeft: `1px solid ${tokens.inkLineSoft}`, cursor: 'copy',
                    background: `repeating-linear-gradient(to bottom, ${tokens.inkLineSoft} 0 1px, transparent 1px ${HOUR}px)${ds === todayStr ? ', rgba(201,162,74,0.05)' : ''}` }}
                  title="Click a slot to add an event there">
                  {ds === todayStr && <div style={{ position: 'absolute', top: `${(nowMin / 60) * HOUR}px`, left: 0, right: 0, height: '2px', background: tokens.red, opacity: 0.7, zIndex: 3, pointerEvents: 'none' }} />}
                  {timed.map((ev, i) => {
                    const seg = segmentFor(ev, ds);
                    const tileH = ((seg.endMin - seg.startMin) / 60) * HOUR - 2;
                    const timeStr = `${ev.start.slice(11, 16)}${ev.end ? `–${ev.end.slice(11, 16)}` : ''}`;
                    // Title leads so squeezed tiles still say WHAT the thing is;
                    // the time drops to the last line (pinned to the bottom on
                    // tall tiles), and sub-26px tiles collapse to one line.
                    const short = tileH < 26;
                    return (
                      <div key={`${ev.id}-${ds}`} onClick={(e) => { e.stopPropagation(); openDetail(ev); }}
                        title={`${timeStr} ${ev.title}${ev.location ? ' · ' + ev.location : ''}`}
                        style={{ position: 'absolute', top: `${(seg.startMin / 60) * HOUR}px`, height: `${tileH}px`,
                          left: `${2 + Math.min(i, 3) * 8}px`, right: '3px', zIndex: 2, cursor: 'pointer', overflow: 'hidden',
                          background: '#FDFBF7', border: `1px solid ${tokens.inkLine}`, borderLeft: `3px solid ${ev.source ? tokens.green : tokens.ochre}`,
                          padding: short ? '1px 5px' : '2px 5px', boxShadow: '0 1px 2px rgba(14,14,12,0.06)',
                          display: 'flex', flexDirection: short ? 'row' : 'column', alignItems: short ? 'baseline' : 'stretch', gap: short ? '6px' : 0 }}>
                        {short ? (
                          <React.Fragment>
                            <span style={{ fontSize: '10.5px', lineHeight: 1.4, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>{ev.title}</span>
                            <span style={{ fontFamily: fontMono, fontSize: '8px', color: tokens.ochre, whiteSpace: 'nowrap', flexShrink: 0 }}>{ev.start.slice(11, 16)}</span>
                          </React.Fragment>
                        ) : (
                          <React.Fragment>
                            <div style={{ fontSize: '11px', lineHeight: 1.3, fontWeight: 500, overflow: 'hidden' }}>{ev.title}</div>
                            {ev.location && tileH >= 56 && <div style={{ fontSize: '9.5px', color: tokens.inkMute, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.location}</div>}
                            <div style={{ fontFamily: fontMono, fontSize: '8.5px', color: tokens.ochre, marginTop: 'auto', paddingTop: '1px' }}>{timeStr}</div>
                          </React.Fragment>
                        )}
                      </div>
                    );
                  })}
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );

  return (
    <PdShell collapseKey="pd.calendar" title="CALENDAR · DANIEL@URDANETA.IO"
      right={events ? `${events.length} EVENTS IN VIEW` : ''}
      actions={[{ label: '+ ADD EVENT', onClick: () => { setEditingId(null); setDraft(blankDraft()); setShowAdd((s) => !s); } },
                { label: '✉ WEEK DIGEST', onClick: digest, running: digestBusy, primary: false },
                { label: '⟳ REFRESH', onClick: refresh, running: busy, primary: false }]}>
      <div style={{ padding: '14px 20px 16px' }}>
        {msg && <div style={{ fontFamily: fontMono, fontSize: '10px', color: msg.startsWith('✓') ? tokens.green : tokens.ochre, marginBottom: '10px' }}>{msg}</div>}

        {showAdd && (
          <form onSubmit={saveEvent} style={{ border: `1px solid ${tokens.inkLine}`, background: tokens.bg, padding: '14px 16px', marginBottom: '14px' }}>
            <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '10px' }}>{editingId ? '✎ EDIT EVENT' : '+ NEW EVENT'}</div>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '2fr 1fr 0.8fr 1fr 0.8fr 1.2fr', gap: '10px', alignItems: 'end' }}>
              <div><label style={pdLbl}>EVENT</label><input style={pdInp} value={draft.title} onChange={(e) => setDraft({ ...draft, title: e.target.value })} placeholder="Lunch with…" autoFocus /></div>
              <div><label style={pdLbl}>STARTS</label><input type="date" style={pdInp} value={draft.date} onChange={(e) => setStart({ date: e.target.value })} /></div>
              <div><label style={pdLbl}>&nbsp;<span style={{ opacity: 0.6 }}>(blank = all-day)</span></label><input type="time" style={pdInp} value={draft.time} onChange={(e) => setStart({ time: e.target.value })} /></div>
              <div><label style={pdLbl}>ENDS <span style={{ fontFamily: fontMono, color: spanOf(draft).valid ? tokens.green : tokens.red, letterSpacing: '0.02em' }}>{spanOf(draft).label}</span></label>
                <input type="date" style={pdInp} value={draft.end_date || draft.date} onChange={(e) => setDraft({ ...draft, end_date: e.target.value })} /></div>
              <div><label style={pdLbl}>&nbsp;</label><input type="time" style={pdInp} value={draft.end_time} onChange={(e) => setDraft({ ...draft, end_time: e.target.value })} disabled={!draft.time} /></div>
              <div><label style={pdLbl}>LOCATION</label><input style={pdInp} value={draft.location} onChange={(e) => setDraft({ ...draft, location: e.target.value })} /></div>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '2fr 2fr', gap: '10px', alignItems: 'end', marginTop: '10px' }}>
              <div><label style={pdLbl}>INVITE <span style={{ opacity: 0.6 }}>(emails, comma-separated — they get a normal Outlook invitation)</span></label>
                <input style={pdInp} value={draft.attendees} onChange={(e) => setDraft({ ...draft, attendees: e.target.value })} placeholder="tim@example.com, michelle@finchbay.com" /></div>
              <div><label style={pdLbl}>NOTES</label><input style={pdInp} value={draft.notes} onChange={(e) => setDraft({ ...draft, notes: e.target.value })} placeholder="Agenda, address details…" /></div>
            </div>
            <div style={{ display: 'flex', gap: '14px', alignItems: 'flex-end', marginTop: '10px', flexWrap: 'wrap' }}>
              {!editingId && (
                <React.Fragment>
                  <div><label style={pdLbl}>REPEAT</label>
                    <select style={pdInp} value={draft.repeat} onChange={(e) => setDraft({ ...draft, repeat: e.target.value })}>
                      <option value="none">One-time</option><option value="daily">Daily</option><option value="weekly">Weekly</option><option value="monthly">Monthly</option><option value="yearly">Yearly</option>
                    </select></div>
                  <div><label style={pdLbl}>UNTIL <span style={{ opacity: 0.6 }}>(optional)</span></label><input type="date" style={pdInp} value={draft.until} onChange={(e) => setDraft({ ...draft, until: e.target.value })} disabled={draft.repeat === 'none'} /></div>
                </React.Fragment>
              )}
              <label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, cursor: 'pointer', paddingBottom: '9px' }}>
                <input type="checkbox" checked={draft.teams} onChange={(e) => setDraft({ ...draft, teams: e.target.checked })} /> TEAMS CALL
              </label>
              <span style={{ flexGrow: 1 }} />
              <button type="submit" disabled={saving} style={{ ...pdBtn(true), opacity: saving ? 0.5 : 1 }}>{saving ? 'SAVING…' : (editingId ? 'SAVE CHANGES' : 'ADD EVENT')}</button>
              <button type="button" onClick={() => { setShowAdd(false); setEditingId(null); setDraft(blankDraft()); }} style={pdBtn(false)}>CANCEL</button>
            </div>
          </form>
        )}

        {loading ? (
          <div style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, letterSpacing: '0.1em' }}>LOADING…</div>
        ) : authorizeUrl ? (
          <div style={{ fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.7 }}>
            Your Outlook calendar isn't connected yet. <a href={authorizeUrl} target="_blank" rel="noopener noreferrer" style={{ color: tokens.ink, borderBottom: `1px solid ${tokens.inkLine}`, cursor: 'pointer' }}>Connect it once here</a> (sign in as daniel@urdaneta.io) and this panel goes live — see CALENDAR-SETUP.md for the two Azure app values to set first.
          </div>
        ) : err ? (
          <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red }}>{err}</div>
        ) : (
          <React.Fragment>
            {/* navigation + view modes */}
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px', flexWrap: 'wrap' }}>
              <span onClick={() => nav(-1)} style={navBtn}>‹</span>
              <span onClick={goToday} style={{ ...navBtn, fontSize: '9px', letterSpacing: '0.1em' }}>TODAY</span>
              <span onClick={() => nav(1)} style={navBtn}>›</span>
              <span style={{ fontSize: '17px', fontFamily: fontDisplay, letterSpacing: '-0.01em', marginLeft: '6px' }}>{headerLabel}</span>
              <span style={{ flexGrow: 1 }} />
              {[['day', 'DAY'], ['week', 'WEEK'], ['month', 'MONTH']].map(([id, lbl]) => (
                <span key={id} onClick={() => pickMode(id)} style={chip(mode === id)}>{lbl}</span>
              ))}
            </div>
            {mode === 'month' ? monthView : timeGrid}
          </React.Fragment>
        )}
        <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '10px', lineHeight: 1.8 }}>
          OUTLOOK VIA calendar-hub · TIMES IN {tz.toUpperCase()} · ‹ › STEPS BY {mode.toUpperCase()} · CLICK AN EVENT FOR DETAILS / EDIT / DELETE ·
          CLICK A DAY NUMBER OR AN HOUR SLOT TO ADD THERE · MULTI-DAY EVENTS SPAN THEIR DAYS (‹ = CONTINUED) ·
          INVITES, UPDATES & CANCELLATIONS GO OUT AS NORMAL OUTLOOK MAIL · GREEN = PUSHED BY A SITE MODULE
        </div>
      </div>

      {/* event detail card */}
      {detail && (
        <div onClick={() => setDetail(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(14,14,12,0.45)', zIndex: 200, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: isMobile ? '14px' : '64px 24px', overflowY: 'auto' }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: '480px', background: tokens.bg, border: `1px solid ${tokens.ink}`, borderTop: `4px solid ${tokens.ochre}`, borderRadius: '4px', padding: isMobile ? '18px' : '24px 28px' }}>
            {detail.loading || !detail.ev ? (
              <div style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkMute, letterSpacing: '0.1em' }}>LOADING…</div>
            ) : (() => {
              const ev = detail.ev;
              const dayLabel = new Date(ev.start.slice(0, 10) + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
              const endDayLabel = ev.end && ev.end.slice(0, 10) !== ev.start.slice(0, 10)
                ? new Date((ev.all_day ? dstr(addDays(new Date(ev.end.slice(0, 10) + 'T12:00:00'), -1)) : ev.end.slice(0, 10)) + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' })
                : null;
              return (
                <React.Fragment>
                  <div style={{ fontSize: '17px', fontWeight: 600, letterSpacing: '-0.01em', marginBottom: '6px' }}>{ev.title}</div>
                  <div style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkSoft, lineHeight: 1.9 }}>
                    {dayLabel.toUpperCase()}{endDayLabel && endDayLabel.toUpperCase() !== dayLabel.toUpperCase() ? ` → ${endDayLabel.toUpperCase()}` : ''}{ev.all_day ? ' · ALL DAY' : ` · ${ev.start.slice(11, 16)}–${(ev.end || '').slice(11, 16)}`}
                    {ev.recurring && <span style={{ color: tokens.ochre }}> · ↻ RECURRING</span>}
                    {ev.location && <div>📍 {ev.location}</div>}
                  </div>
                  {ev.join_url && <a href={ev.join_url} target="_blank" rel="noopener noreferrer" style={{ display: 'inline-block', fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.06em', color: tokens.paper, background: '#39557E', padding: '6px 14px', marginTop: '10px', cursor: 'pointer' }}>▶ JOIN TEAMS CALL</a>}
                  {(ev.attendees || []).length > 0 && (
                    <div style={{ marginTop: '12px' }}>
                      <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '4px' }}>ATTENDEES · ✓ ACCEPTED · ~ TENTATIVE · ✗ DECLINED</div>
                      {ev.attendees.map((a, i) => (
                        <div key={i} style={{ fontSize: '12.5px', padding: '2px 0' }}>{rsvpChip(a.response)}{a.name && a.name !== a.email ? `${a.name} — ` : ''}<span style={{ color: tokens.inkSoft }}>{a.email}</span></div>
                      ))}
                    </div>
                  )}
                  {ev.notes && <div style={{ fontSize: '12.5px', color: tokens.inkSoft, lineHeight: 1.6, marginTop: '12px', whiteSpace: 'pre-wrap', maxHeight: '140px', overflowY: 'auto' }}>{ev.notes}</div>}
                  <div style={{ display: 'flex', gap: '8px', marginTop: '18px', flexWrap: 'wrap' }}>
                    <button onClick={() => startEdit(ev)} style={pdBtn(true)}>✎ EDIT</button>
                    <button onClick={() => deleteEvent(ev.id, ev.series ? 'this occurrence' : `"${ev.title}"`)} style={{ ...pdBtn(false), borderColor: tokens.red, color: tokens.red }}>{ev.series ? '✕ THIS ONE' : '✕ DELETE'}</button>
                    {ev.series && <button onClick={() => deleteEvent(ev.series, 'the WHOLE recurring series')} style={{ ...pdBtn(false), borderColor: tokens.red, color: tokens.red }}>✕ SERIES</button>}
                    {ev.link && <a href={ev.link} target="_blank" rel="noopener noreferrer" style={{ ...pdBtn(false), textDecoration: 'none', display: 'inline-block' }}>OUTLOOK ↗</a>}
                    <span style={{ flexGrow: 1 }} />
                    <button onClick={() => setDetail(null)} style={pdBtn(false)}>CLOSE</button>
                  </div>
                </React.Fragment>
              );
            })()}
          </div>
        </div>
      )}
    </PdShell>
  );
};

// ── Health (section 01) ───────────────────────────────────────────────────────
// Biometrics (weight + BP with AM/PM), training (Garmin + Peloton + gym),
// nutrition (plain-language food log with estimated macros + daily energy
// balance), sleep, labs (Medical) and the Claude Health Coach. Data flows in
// from WhatsApp / add@parse.urdaneta.io (add_to_health) / the quick-add box /
// CSV + PDF uploads — all through the health-hub Edge Function.

const hhInvoke = async (body) => {
  const client = window._supabaseClient;
  if (!client) throw new Error('No backend connected.');
  const { data, error } = await client.functions.invoke('health-hub', { body });
  let payload = data;
  if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
  if (payload && payload.error) throw new Error(payload.error);
  return payload;
};

const hhLondonHour = (iso) => Number(new Date(iso).toLocaleString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', hour12: false }));
const hhWhen = (iso) => {
  const d = new Date(iso);
  const t = d.toLocaleString('en-GB', { timeZone: 'Europe/London', day: '2-digit', month: 'short', year: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
  return `${t} ${hhLondonHour(iso) < 12 ? 'AM' : 'PM'}`;
};
const hhBmr = (profile, weightKg) => {
  if (!profile || !weightKg || !profile.height_cm || !profile.birth_date || !profile.sex) return null;
  const age = (Date.now() - new Date(profile.birth_date).getTime()) / (365.25 * 86400000);
  return Math.round(10 * weightKg + 6.25 * Number(profile.height_cm) - 5 * age + (profile.sex === 'M' ? 5 : -161));
};

// Multi-series SVG line chart in the site's paper style.
const HhLine = ({ series, height = 150, unit = '' }) => {
  const all = series.flatMap((s) => s.pts);
  if (all.length < 2) return <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, padding: '10px 0' }}>NOT ENOUGH DATA YET</div>;
  const w = 640, padL = 42, padR = 14, padT = 10, padB = 18;
  const ts = all.map((p) => p.t), vs = all.map((p) => p.v);
  const t0 = Math.min(...ts), t1 = Math.max(...ts);
  let v0 = Math.min(...vs), v1 = Math.max(...vs);
  const padV = Math.max((v1 - v0) * 0.12, 0.5); v0 -= padV; v1 += padV;
  const X = (t) => padL + (t1 === t0 ? 0 : ((t - t0) / (t1 - t0)) * (w - padL - padR));
  const Y = (v) => padT + (1 - (v - v0) / (v1 - v0)) * (height - padT - padB);
  const fmtD = (t) => new Date(t).toLocaleDateString('en-GB', { month: 'short', year: '2-digit' });
  return (
    <div>
      <svg viewBox={`0 0 ${w} ${height}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
        {[v0 + padV, (v0 + v1) / 2, v1 - padV].map((v, i) => (
          <g key={i}>
            <line x1={padL} y1={Y(v)} x2={w - padR} y2={Y(v)} stroke={tokens.inkLine} strokeWidth="0.6" strokeDasharray="3 4" />
            <text x={padL - 5} y={Y(v) + 3} textAnchor="end" fontSize="8.5" fontFamily="JetBrains Mono, monospace" fill={tokens.inkMute}>{v.toFixed(1)}</text>
          </g>
        ))}
        {series.map((s, si) => s.pts.length > 1 && (
          <polyline key={si} points={s.pts.map((p) => `${X(p.t)},${Y(p.v)}`).join(' ')} fill="none" stroke={s.color} strokeWidth="1.8" strokeLinejoin="round" />
        ))}
        {series.map((s, si) => {
          const last = s.pts[s.pts.length - 1];
          return last ? <circle key={si} cx={X(last.t)} cy={Y(last.v)} r="3" fill={s.color} /> : null;
        })}
        <text x={padL} y={height - 5} fontSize="8.5" fontFamily="JetBrains Mono, monospace" fill={tokens.inkMute}>{fmtD(t0)}</text>
        <text x={w - padR} y={height - 5} textAnchor="end" fontSize="8.5" fontFamily="JetBrains Mono, monospace" fill={tokens.inkMute}>{fmtD(t1)}</text>
      </svg>
      <div style={{ display: 'flex', gap: '14px', flexWrap: 'wrap', marginTop: '4px' }}>
        {series.map((s, i) => (
          <span key={i} style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', color: tokens.inkSoft, display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
            <span style={{ width: '14px', height: '2px', background: s.color, display: 'inline-block' }} />{s.name}{unit ? ` (${unit})` : ''}
          </span>
        ))}
      </div>
    </div>
  );
};

// Compact bar strip (sleep scores, workouts/week).
const HhBars = ({ items, height = 46, color = tokens.ochre, fmt = (v) => v }) => {
  if (!items.length) return <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, padding: '10px 0' }}>NO DATA YET</div>;
  const max = Math.max(...items.map((x) => x.v || 0), 1);
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', gap: '3px', height: `${height}px` }}>
      {items.map((x, i) => (
        <div key={i} title={`${x.label} — ${fmt(x.v)}`}
          style={{ flex: 1, maxWidth: '20px', height: `${Math.max(4, ((x.v || 0) / max) * height)}px`, background: i === items.length - 1 ? color : 'rgba(57,85,126,0.4)' }} />
      ))}
    </div>
  );
};

// Compact pager for the health tables — newest first, small pages so the
// panels stay short as data accumulates.
const HhPager = ({ total, page, setPage, size }) => {
  if (total <= size) return null;
  const last = Math.ceil(total / size) - 1;
  return (
    <div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'flex-end', fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.08em', color: tokens.inkMute, marginTop: '10px', userSelect: 'none' }}>
      <span onClick={() => setPage(Math.max(0, page - 1))} style={{ cursor: page > 0 ? 'pointer' : 'default', opacity: page > 0 ? 1 : 0.3, color: page > 0 ? tokens.ink : tokens.inkMute }}>‹ NEWER</span>
      <span>{page * size + 1}–{Math.min(total, (page + 1) * size)} OF {total}</span>
      <span onClick={() => setPage(Math.min(last, page + 1))} style={{ cursor: page < last ? 'pointer' : 'default', opacity: page < last ? 1 : 0.3, color: page < last ? tokens.ink : tokens.inkMute }}>OLDER ›</span>
    </div>
  );
};

const hhTile = (k, v, sub, c) => (
  <div key={k} style={{ border: `1px solid ${tokens.inkLine}`, padding: '10px 14px', background: tokens.bg }}>
    <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '4px' }}>{k}</div>
    <div style={{ fontSize: '20px', letterSpacing: '-0.02em', fontFamily: fontDisplay, color: c || tokens.ink }}>{v}</div>
    {sub && <div style={{ fontFamily: fontMono, fontSize: '8px', color: tokens.inkMute, marginTop: '2px' }}>{sub}</div>}
  </div>
);

const PdHealthSection = () => {
  const isMobile = useIsMobile();
  const [bio, setBio] = React.useState([]);
  const [workouts, setWorkouts] = React.useState([]);
  const [food, setFood] = React.useState([]);
  const [sleep, setSleep] = React.useState([]);
  const [labs, setLabs] = React.useState([]);
  const [profile, setProfile] = React.useState(null);
  const [reports, setReports] = React.useState({});

  const load = React.useCallback(async () => {
    const client = window._supabaseClient;
    if (!client) return;
    const [b, w, f, s, l, p, r] = await Promise.all([
      client.from('health_biometrics').select('*').order('measured_at', { ascending: true }).limit(2000),
      client.from('health_workouts').select('*').order('started_at', { ascending: false }).limit(400),
      client.from('health_food').select('*').order('eaten_at', { ascending: false }).limit(300),
      client.from('health_sleep').select('*').order('period_start', { ascending: true }).limit(120),
      client.from('health_labs').select('*').order('taken_at', { ascending: false }),
      client.from('health_profile').select('*').eq('id', 'me').maybeSingle(),
      client.from('health_reports').select('*').order('generated_at', { ascending: false }).limit(10),
    ]);
    setBio(b.data || []); setWorkouts(w.data || []); setFood(f.data || []);
    setSleep(s.data || []); setLabs(l.data || []); setProfile(p.data || null);
    const rep = {};
    for (const row of (r.data || [])) if (!rep[row.kind]) rep[row.kind] = row;
    setReports(rep);
  }, []);
  React.useEffect(() => { load(); }, [load]);

  // derived
  const weights = bio.filter((x) => x.weight_kg != null);
  const bps = bio.filter((x) => x.systolic != null);
  const latestW = weights.length ? weights[weights.length - 1] : null;
  const wAgo = (days) => { const cut = Date.now() - days * 86400000; const past = weights.filter((x) => new Date(x.measured_at).getTime() <= cut); return past.length ? past[past.length - 1] : weights[0]; };
  const d30 = latestW && weights.length > 1 ? latestW.weight_kg - wAgo(30).weight_kg : null;
  const lastAm = [...bps].reverse().find((x) => hhLondonHour(x.measured_at) < 12);
  const lastPm = [...bps].reverse().find((x) => hhLondonHour(x.measured_at) >= 12);
  const sleepScored = sleep.filter((s) => s.score != null);
  const sleep4w = sleepScored.slice(-4);
  const sleepAvg = sleep4w.length ? Math.round(sleep4w.reduce((a, s) => a + s.score, 0) / sleep4w.length) : null;
  const wk4 = workouts.filter((w) => new Date(w.started_at).getTime() > Date.now() - 28 * 86400000);
  const bmr = hhBmr(profile, latestW ? latestW.weight_kg : null);
  const burnForDay = (day) => bmr == null ? null : Math.round(bmr * ((profile && profile.activity_factor) || 1.2)) +
    workouts.filter((w) => String(w.started_at).slice(0, 10) === day).reduce((a, w) => a + (w.calories || 0), 0);
  const foodDays = {};
  for (const f of food) (foodDays[String(f.eaten_at).slice(0, 10)] = foodDays[String(f.eaten_at).slice(0, 10)] || []).push(f);
  const dayNet = (day) => {
    const eaten = (foodDays[day] || []).reduce((a, f) => a + (f.calories || 0), 0);
    const burned = burnForDay(day);
    return { eaten, burned, net: burned == null ? null : eaten - burned };
  };
  const nets7 = Object.keys(foodDays).sort().slice(-7).map(dayNet).filter((x) => x.net != null);
  const netAvg = nets7.length ? Math.round(nets7.reduce((a, x) => a + x.net, 0) / nets7.length) : null;

  const weeksBack = (n) => { const out = []; const mon = (d) => { const x = new Date(d); const g = (x.getUTCDay() + 6) % 7; x.setUTCDate(x.getUTCDate() - g); return x.toISOString().slice(0, 10); };
    for (let i = n - 1; i >= 0; i--) out.push(mon(Date.now() - i * 7 * 86400000)); return out; };
  const wkWeeks = weeksBack(12).map((start) => ({
    label: start,
    v: workouts.filter((w) => { const d = String(w.started_at).slice(0, 10); return d >= start && d < new Date(new Date(start + 'T00:00:00Z').getTime() + 7 * 86400000).toISOString().slice(0, 10); }).length,
  }));

  return (
    <React.Fragment>
      <PdHealthOverview {...{ isMobile, weights, bps, latestW, d30, lastAm, lastPm, sleepScored, sleepAvg, wk4, netAvg, bmr, wkWeeks, profile }} />
      <PdHealthLog {...{ isMobile, bio, profile, reload: load }} />
      <PdHealthTraining {...{ isMobile, workouts, reload: load }} />
      <PdHealthNutrition {...{ isMobile, foodDays, dayNet, food, reload: load, bmr }} />
      <PdHealthCoach report={reports.coach} reload={load} />
      <PdHealthMedical {...{ isMobile, labs, report: reports.medical, reload: load }} />
    </React.Fragment>
  );
};

const PdHealthOverview = ({ isMobile, weights, bps, latestW, d30, lastAm, lastPm, sleepScored, sleepAvg, wk4, netAvg, bmr, wkWeeks, profile }) => (
  <PdShell collapseKey="pd.health.overview" title="HEALTH OVERVIEW · LONG-ARC TRENDS">
    <div style={{ padding: '16px 20px' }}>
      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(6, 1fr)', gap: '10px', marginBottom: '16px' }}>
        {hhTile('WEIGHT', latestW ? `${Number(latestW.weight_kg).toFixed(1)}kg` : '—', latestW ? `${d30 != null && Math.abs(d30) > 0.05 ? `${d30 > 0 ? '+' : ''}${d30.toFixed(1)}KG 30D · ` : ''}${String(latestW.measured_at).slice(0, 10)}` : 'LOG ONE TO START', d30 != null ? (d30 <= 0 ? tokens.green : tokens.ochre) : tokens.ink)}
        {hhTile('BP · LAST AM', lastAm ? `${lastAm.systolic}/${lastAm.diastolic}` : '—', lastAm ? String(lastAm.measured_at).slice(0, 10) : 'NO AM READING', lastAm && lastAm.systolic >= 140 ? tokens.red : lastAm && lastAm.systolic >= 130 ? tokens.ochre : tokens.ink)}
        {hhTile('BP · LAST PM', lastPm ? `${lastPm.systolic}/${lastPm.diastolic}` : '—', lastPm ? String(lastPm.measured_at).slice(0, 10) : 'NO PM READING', lastPm && lastPm.systolic >= 140 ? tokens.red : lastPm && lastPm.systolic >= 130 ? tokens.ochre : tokens.ink)}
        {hhTile('SLEEP · 4W AVG', sleepAvg != null ? sleepAvg : '—', sleepAvg != null ? (sleepAvg >= 70 ? 'GOOD' : sleepAvg >= 60 ? 'FAIR' : 'POOR') + ' · GARMIN' : 'UPLOAD SLEEP CSV', sleepAvg == null ? tokens.ink : sleepAvg >= 70 ? tokens.green : sleepAvg >= 60 ? tokens.ochre : tokens.red)}
        {hhTile('WORKOUTS · 4W', wk4.length, `${(wk4.length / 4).toFixed(1)}/WK`, wk4.length >= 12 ? tokens.green : tokens.ink)}
        {hhTile('NET KCAL · 7D', netAvg != null ? `${netAvg > 0 ? '+' : ''}${netAvg}` : '—', bmr == null ? 'SET PROFILE BELOW' : netAvg == null ? 'LOG FOOD TO TRACK' : netAvg <= 0 ? 'DEFICIT' : 'SURPLUS', netAvg == null ? tokens.ink : netAvg <= 0 ? tokens.green : tokens.ochre)}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '18px 28px' }}>
        <div>
          <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.inkMute, marginBottom: '6px' }}>WEIGHT · FULL HISTORY</div>
          <HhLine unit="kg" series={[{ name: 'Weight', color: '#39557E', pts: weights.map((x) => ({ t: new Date(x.measured_at).getTime(), v: Number(x.weight_kg) })) }]} />
        </div>
        <div>
          <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.inkMute, marginBottom: '6px' }}>BLOOD PRESSURE</div>
          <HhLine unit="mmHg" series={[
            { name: 'Systolic', color: '#A04A3E', pts: bps.map((x) => ({ t: new Date(x.measured_at).getTime(), v: x.systolic })) },
            { name: 'Diastolic', color: '#5A7A3E', pts: bps.map((x) => ({ t: new Date(x.measured_at).getTime(), v: x.diastolic })) },
          ]} />
        </div>
        <div>
          <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.inkMute, marginBottom: '6px' }}>SLEEP SCORE · WEEKLY (GARMIN)</div>
          <HhBars items={sleepScored.slice(-26).map((s) => ({ label: s.period_start, v: s.score }))} fmt={(v) => `score ${v}`} />
        </div>
        <div>
          <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.12em', color: tokens.inkMute, marginBottom: '6px' }}>WORKOUTS PER WEEK · 12W</div>
          <HhBars items={wkWeeks} color={tokens.green} fmt={(v) => `${v} workouts`} />
        </div>
      </div>
      {bmr != null && (
        <div style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.05em', color: tokens.inkMute, marginTop: '12px' }}>
          BMR (MIFFLIN-ST JEOR) ≈ {bmr} KCAL · RESTING BURN ≈ {Math.round(bmr * ((profile && profile.activity_factor) || 1.2))} KCAL/DAY + WORKOUT CALORIES
        </div>
      )}
    </div>
  </PdShell>
);

const PdHealthLog = ({ isMobile, bio, profile, reload }) => {
  const [text, setText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState('');
  const [prof, setProf] = React.useState(null);
  const [page, setPage] = React.useState(0);
  const all = [...bio].reverse();                      // newest first
  const rows = all.slice(page * 10, page * 10 + 10);
  const p = prof || profile || {};
  const quickLog = async () => {
    if (!text.trim()) return;
    setBusy(true); setMsg('');
    try { const out = await hhInvoke({ action: 'ingest_text', text, source: 'ui' }); setMsg(`LOGGED: ${(out.logged || []).join(' · ') || 'nothing recognized'}`); setText(''); await reload(); }
    catch (e) { setMsg(`ERROR — ${e.message}`); }
    finally { setBusy(false); }
  };
  const saveProfile = async () => {
    const client = window._supabaseClient;
    await client.from('health_profile').upsert({ id: 'me', height_cm: p.height_cm || null, birth_date: p.birth_date || null, sex: p.sex || null, activity_factor: p.activity_factor || 1.2 });
    setMsg('PROFILE SAVED'); await reload();
  };
  return (
    <PdShell collapseKey="pd.health.log" title="LOG · WEIGHT, BLOOD PRESSURE & QUICK ADD">
      <div style={{ padding: '16px 20px' }}>
        <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '8px' }}>
          <textarea value={text} onChange={(e) => setText(e.target.value)} rows={2}
            placeholder={'Plain English — "92.6kg this morning", "BP 132/86", "bench 3x8 at 80kg", "lunch: chicken burrito and a coke". Dates work too ("7/29/25 93.8kg").'}
            style={{ ...pdInp, flex: 1, resize: 'vertical', fontSize: '12.5px' }} />
          <button onClick={quickLog} disabled={busy} style={pdBtn(true)}>{busy ? 'PARSING…' : '⚡ LOG'}</button>
        </div>
        {msg && <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: msg.startsWith('ERROR') ? tokens.red : tokens.green, marginBottom: '10px' }}>{msg}</div>}
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '480px' }}>
            <thead><tr>
              <th style={pdTh}>WHEN (LONDON)</th><th style={pdTh}>AM/PM</th>
              <th style={{ ...pdTh, textAlign: 'right' }}>WEIGHT</th><th style={{ ...pdTh, textAlign: 'right' }}>BP</th>
              <th style={pdTh}>SOURCE</th><th style={pdTh} />
            </tr></thead>
            <tbody>
              {rows.map((r) => (
                <tr key={r.id}>
                  <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '11px', whiteSpace: 'nowrap' }}>{hhWhen(r.measured_at).slice(0, -3)}</td>
                  <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '9.5px', color: hhLondonHour(r.measured_at) < 12 ? tokens.ochre : tokens.inkMute }}>{hhLondonHour(r.measured_at) < 12 ? 'AM' : 'PM'}</td>
                  <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '12px' }}>{r.weight_kg != null ? `${Number(r.weight_kg).toFixed(1)}kg` : ''}</td>
                  <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '12px', color: r.systolic >= 140 ? tokens.red : r.systolic >= 130 ? tokens.ochre : tokens.ink }}>{r.systolic != null ? `${r.systolic}/${r.diastolic}${r.pulse ? ` · ${r.pulse}bpm` : ''}` : ''}</td>
                  <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '9px', color: tokens.inkMute }}>{(r.source || '').toUpperCase()}</td>
                  <td style={pdTd}><span onClick={async () => { if (window.confirm('Delete this reading?')) { await window._supabaseClient.from('health_biometrics').delete().eq('id', r.id); reload(); } }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <HhPager total={all.length} page={page} setPage={setPage} size={10} />
        <div style={{ borderTop: `1px solid ${tokens.inkLine}`, marginTop: '14px', paddingTop: '12px' }}>
          <div style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, marginBottom: '8px' }}>PROFILE · POWERS THE BMR / DAILY-BURN MATH</div>
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
            <input type="number" placeholder="Height cm" value={p.height_cm || ''} onChange={(e) => setProf({ ...p, height_cm: e.target.value })} style={{ ...pdInp, width: '110px' }} />
            <input type="date" value={p.birth_date || ''} onChange={(e) => setProf({ ...p, birth_date: e.target.value })} style={{ ...pdInp, width: '150px' }} />
            <select value={p.sex || ''} onChange={(e) => setProf({ ...p, sex: e.target.value })} style={{ ...pdInp, width: '90px' }}>
              <option value="">Sex</option><option value="M">M</option><option value="F">F</option>
            </select>
            <input type="number" step="0.05" title="Resting multiplier on BMR — 1.2 = desk life, 1.35 = on your feet a lot" value={p.activity_factor || 1.2} onChange={(e) => setProf({ ...p, activity_factor: e.target.value })} style={{ ...pdInp, width: '90px' }} />
            <button onClick={saveProfile} style={pdBtn(false)}>SAVE PROFILE</button>
          </div>
        </div>
      </div>
    </PdShell>
  );
};

const PdHealthTraining = ({ isMobile, workouts, reload }) => {
  const fileRef = React.useRef(null);
  const kindRef = React.useRef('');
  const [busy, setBusy] = React.useState('');
  const [msg, setMsg] = React.useState('');
  const [openId, setOpenId] = React.useState(null);
  const pick = (kind) => { kindRef.current = kind; if (fileRef.current) fileRef.current.click(); };
  const onFile = async (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    setBusy(kindRef.current); setMsg('');
    try {
      const out = await hhInvoke({ action: 'ingest_csv', kind: kindRef.current, csv: await f.text() });
      setMsg(`${kindRef.current.toUpperCase()}: ${out.parsed} rows${out.inserted != null ? ` · ${out.inserted} new · ${out.merged || 0} merged with the other source · ${out.updated || 0} updated` : ''}`);
      await reload();
    } catch (err) { setMsg(`ERROR — ${err.message}`); }
    finally { setBusy(''); }
  };
  const [page, setPage] = React.useState(0);
  const rows = workouts.slice(page * 10, page * 10 + 10);   // already newest first
  const srcBadge = (s) => <span style={{ fontFamily: fontMono, fontSize: '8px', letterSpacing: '0.08em', color: s.includes('+') ? tokens.green : tokens.inkMute, border: `1px solid ${s.includes('+') ? tokens.green : tokens.inkLine}`, padding: '1px 5px' }}>{s.toUpperCase()}</span>;
  return (
    <PdShell collapseKey="pd.health.training" title="TRAINING · GARMIN + PELOTON + GYM"
      actions={[{ label: '⬆ PELOTON CSV', onClick: () => pick('peloton'), running: busy === 'peloton', primary: false },
                { label: '⬆ GARMIN ACTIVITIES', onClick: () => pick('garmin_activities'), running: busy === 'garmin_activities', primary: false },
                { label: '⬆ GARMIN SLEEP', onClick: () => pick('garmin_sleep'), running: busy === 'garmin_sleep', primary: false },
                { label: '⬆ GARMIN HRV', onClick: () => pick('garmin_hrv'), running: busy === 'garmin_hrv', primary: false }]}>
      <div style={{ padding: '16px 20px' }}>
        <input ref={fileRef} type="file" accept=".csv,text/csv" style={{ display: 'none' }} onChange={onFile} />
        {msg && <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: msg.startsWith('ERROR') ? tokens.red : tokens.green, marginBottom: '10px' }}>{msg}</div>}
        {rows.length === 0 && <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>Upload your Peloton workout export and Garmin CSVs (buttons above), or text the WhatsApp bot a gym session — everything lands here, de-duplicated across sources.</div>}
        {rows.map((w) => (
          <div key={w.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}`, padding: '7px 0' }}>
            <div onClick={() => setOpenId(openId === w.id ? null : w.id)} style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap', cursor: w.exercises ? 'pointer' : 'default' }}>
              <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute, whiteSpace: 'nowrap' }}>{String(w.started_at).slice(0, 10)}</span>
              <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.1em', color: tokens.ochre }}>{(w.kind || '').toUpperCase()}</span>
              <span style={{ fontWeight: 500, fontSize: '13px' }}>{w.title}{w.exercises ? (openId === w.id ? ' −' : ' +') : ''}</span>
              <span style={{ flexGrow: 1 }} />
              <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkSoft, whiteSpace: 'nowrap' }}>
                {w.duration_min ? `${Math.round(w.duration_min)}min` : ''}{w.calories ? ` · ${w.calories}kcal` : ''}{w.avg_hr ? ` · ${w.avg_hr}bpm` : ''}{w.output_kj ? ` · ${w.output_kj}kJ` : ''}
              </span>
              {srcBadge(w.source || 'manual')}
              <span onClick={async (e) => { e.stopPropagation(); if (window.confirm(`Delete this workout? (${w.title})`)) { await window._supabaseClient.from('health_workouts').delete().eq('id', w.id); reload(); } }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
            </div>
            {openId === w.id && Array.isArray(w.exercises) && (
              <div style={{ padding: '8px 0 4px 12px' }}>
                {w.exercises.map((ex, i) => (
                  <div key={i} style={{ fontSize: '12.5px', padding: '2px 0' }}>
                    <span style={{ fontWeight: 500 }}>{ex.name}</span>
                    <span style={{ fontFamily: fontMono, fontSize: '11px', color: tokens.inkSoft, marginLeft: '10px' }}>
                      {(ex.sets || []).map((s) => `${s.reps}×${s.weight_kg != null ? s.weight_kg + 'kg' : 'bw'}`).join('  ')}
                    </span>
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}
        <HhPager total={workouts.length} page={page} setPage={setPage} size={10} />
      </div>
    </PdShell>
  );
};

const PdHealthNutrition = ({ isMobile, foodDays, dayNet, food, reload, bmr }) => {
  const [page, setPage] = React.useState(0);
  const allDays = Object.keys(foodDays).sort().reverse();   // newest first
  const days = allDays.slice(page * 5, page * 5 + 5);
  return (
    <PdShell collapseKey="pd.health.nutrition" title="NUTRITION · FOOD LOG & ENERGY BALANCE">
      <div style={{ padding: '16px 20px' }}>
        {days.length === 0 && <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>Text the bot or email the parser what you ate ("lunch was a chicken burrito and a coke") — calories and macros are estimated automatically. Net balance needs the profile set (Log panel) so the daily burn can be computed.</div>}
        {days.map((day) => {
          const { eaten, burned, net } = dayNet(day);
          const totals = foodDays[day].reduce((a, f) => ({ p: a.p + (Number(f.protein_g) || 0), c: a.c + (Number(f.carbs_g) || 0), f: a.f + (Number(f.fat_g) || 0) }), { p: 0, c: 0, f: 0 });
          return (
            <div key={day} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}`, padding: '9px 0' }}>
              <div style={{ display: 'flex', gap: '12px', alignItems: 'baseline', flexWrap: 'wrap' }}>
                <span style={{ fontFamily: fontMono, fontSize: '11px', fontWeight: 500 }}>{day}</span>
                <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkSoft }}>IN {eaten} kcal · P{Math.round(totals.p)} C{Math.round(totals.c)} F{Math.round(totals.f)}</span>
                {burned != null && <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkSoft }}>OUT ~{burned} kcal</span>}
                {net != null && <span style={{ fontFamily: fontMono, fontSize: '10.5px', fontWeight: 600, color: net <= 0 ? tokens.green : tokens.ochre }}>{net > 0 ? '+' : ''}{net} NET</span>}
              </div>
              {foodDays[day].map((f) => (
                <div key={f.id} style={{ display: 'flex', gap: '8px', alignItems: 'baseline', padding: '3px 0 0 12px', fontSize: '12.5px' }}>
                  <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.1em', color: tokens.inkMute, width: '72px', flexShrink: 0 }}>{(f.meal || 'FOOD').toUpperCase()}</span>
                  <span style={{ color: tokens.inkSoft, flex: 1 }}>{f.description}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkMute, whiteSpace: 'nowrap' }}>{f.calories != null ? `${f.calories} kcal` : ''}</span>
                  <span onClick={async () => { if (window.confirm('Delete this food entry?')) { await window._supabaseClient.from('health_food').delete().eq('id', f.id); reload(); } }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                </div>
              ))}
            </div>
          );
        })}
        <HhPager total={allDays.length} page={page} setPage={setPage} size={5} />
        {bmr == null && days.length > 0 && <div style={{ fontFamily: fontMono, fontSize: '9px', color: tokens.ochre, marginTop: '10px' }}>SET HEIGHT / DOB / SEX IN THE LOG PANEL TO UNLOCK THE BURN + NET CALCULATION.</div>}
      </div>
    </PdShell>
  );
};

const PdHealthCoach = ({ report, reload }) => {
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const d = report && report.report;
  const run = async () => {
    setBusy(true); setErr('');
    try { await hhInvoke({ action: 'coach' }); await reload(); }
    catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };
  const secLbl = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '14px 0 5px' };
  const prose = { fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6 };
  return (
    <PdShell collapseKey="pd.health.coach" title="HEALTH COACH · CLAUDE"
      right={report ? `LAST RUN ${new Date(report.generated_at).toLocaleDateString()}` : ''}
      actions={[{ label: busy ? 'ANALYZING…' : '⚡ RUN HEALTH COACH', onClick: run, running: busy }]}>
      <div style={{ padding: '16px 20px' }}>
        {err && <div style={{ color: tokens.red, fontSize: '12px', marginBottom: '10px' }}>{err}</div>}
        {!d && !busy && <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>Runs a comprehensive review across weight, blood pressure (AM vs PM), sleep, HRV, training load, gym progression, nutrition and labs — emailed to daniel@urdaneta.io and rendered here. Also available by texting the WhatsApp bot "health coach". Tune its judgment in Admin → Claude feedback → Health Coach.</div>}
        {d && (
          <div>
            <div style={{ fontSize: '14.5px', lineHeight: 1.55 }}>{d.headline}</div>
            {Array.isArray(d.wins) && d.wins.length > 0 && (<div><div style={secLbl}>WINS</div>{d.wins.map((w, i) => <div key={i} style={{ ...prose, padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}</div>)}
            {Array.isArray(d.concerns) && d.concerns.length > 0 && (<div><div style={secLbl}>CONCERNS</div>{d.concerns.map((w, i) => <div key={i} style={{ ...prose, padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}</div>)}
            {[['WEIGHT', d.weight_read], ['BLOOD PRESSURE', d.bp_read], ['SLEEP', d.sleep_read], ['TRAINING', d.training_read], ['NUTRITION', d.nutrition_read]].map(([k, v]) => v ? (
              <div key={k}><div style={secLbl}>{k}</div><div style={prose}>{v}</div></div>
            ) : null)}
            {Array.isArray(d.actions) && d.actions.length > 0 && (<div>
              <div style={secLbl}>ACTIONS · THIS WEEK</div>
              {d.actions.map((a, i) => (
                <div key={i} style={{ display: 'grid', gridTemplateColumns: '22px 1fr', gap: '8px', padding: '4px 0', fontSize: '13px', lineHeight: 1.5 }}>
                  <span style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.ochre }}>{String(i + 1).padStart(2, '0')}</span>
                  <span><span style={{ fontWeight: 500 }}>{a.title}</span>{a.detail ? <span style={{ color: tokens.inkSoft }}> — {a.detail}</span> : null}</span>
                </div>
              ))}
            </div>)}
            {Array.isArray(d.watch) && d.watch.length > 0 && (<div><div style={secLbl}>WATCH</div>{d.watch.map((w, i) => <div key={i} style={{ ...prose, fontSize: '12.5px', padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {w}</div>)}</div>)}
          </div>
        )}
      </div>
    </PdShell>
  );
};

const PdHealthMedical = ({ isMobile, labs, report, reload }) => {
  const fileRef = React.useRef(null);
  const [busy, setBusy] = React.useState('');
  const [msg, setMsg] = React.useState('');
  const [openId, setOpenId] = React.useState(null);
  const [page, setPage] = React.useState(0);
  const [folded, toggleFold] = pdCollapsed('fold.medical.summary', false);
  const shownLabs = labs.slice(page * 5, page * 5 + 5);   // already newest first
  const d = report && report.report;
  const flagColor = (f) => f === 'high' ? tokens.red : f === 'low' ? tokens.ochre : f === 'note' ? tokens.ochre : tokens.green;
  const onFile = async (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    setBusy('lab'); setMsg('');
    try {
      const b64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(String(r.result).split(',')[1]); r.onerror = rej; r.readAsDataURL(f); });
      const out = await hhInvoke({ action: 'ingest_lab', filename: f.name, pdf_b64: b64 });
      setMsg(out.duplicate
        ? `ALREADY FILED — ${out.lab_name || 'this report'} ${out.taken_at || ''} is in the record; duplicate skipped.`
        : `FILED — ${out.lab_name || 'lab'} ${out.taken_at || ''} · ${out.flagged || 0} FLAGGED · EMAILED SUMMARY`);
      await reload();
    } catch (err) { setMsg(`ERROR — ${err.message}`); }
    finally { setBusy(''); }
  };
  const runReport = async () => {
    setBusy('report'); setMsg('');
    try { await hhInvoke({ action: 'medical_report' }); await reload(); setMsg('MEDICAL REPORT GENERATED + EMAILED'); }
    catch (err) { setMsg(`ERROR — ${err.message}`); }
    finally { setBusy(''); }
  };
  const secLbl = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.14em', color: tokens.inkMute, margin: '14px 0 5px' };
  const prose = { fontSize: '13px', color: tokens.inkSoft, lineHeight: 1.6 };
  return (
    <PdShell collapseKey="pd.health.medical" title="MEDICAL · LAB REPORTS & DOCTOR SUMMARY"
      actions={[{ label: '⬆ UPLOAD LAB PDF', onClick: () => fileRef.current && fileRef.current.click(), running: busy === 'lab', primary: false },
                { label: busy === 'report' ? 'WRITING…' : '⚡ MEDICAL REPORT', onClick: runReport, running: busy === 'report' }]}>
      <div style={{ padding: '16px 20px' }}>
        <input ref={fileRef} type="file" accept="application/pdf" style={{ display: 'none' }} onChange={onFile} />
        {msg && <div style={{ fontFamily: fontMono, fontSize: '9.5px', color: msg.startsWith('ERROR') ? tokens.red : tokens.green, marginBottom: '10px' }}>{msg}</div>}
        {labs.length === 0 && <div style={{ fontSize: '12.5px', color: tokens.inkSoft }}>Upload a lab PDF here, or forward it to add@parse.urdaneta.io with <span style={{ fontFamily: fontMono }}>add_to_labs</span> in the subject — every marker is extracted with its reference range, flags surface here, and you get a summary email. The Randox panel drops straight in when it arrives.</div>}
        {shownLabs.map((l) => {
          const flagged = (l.markers || []).filter((m) => m.flag === 'high' || m.flag === 'low');
          return (
            <div key={l.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}`, padding: '8px 0' }}>
              <div onClick={() => setOpenId(openId === l.id ? null : l.id)} style={{ display: 'flex', gap: '10px', alignItems: 'baseline', flexWrap: 'wrap', cursor: 'pointer' }}>
                <span style={{ fontFamily: fontMono, fontSize: '11px', fontWeight: 500 }}>{l.taken_at || '—'}</span>
                <span style={{ fontSize: '13px', fontWeight: 500 }}>{l.lab_name || 'Lab report'}{openId === l.id ? ' −' : ' +'}</span>
                <span style={{ fontSize: '11.5px', color: tokens.inkMute }}>{l.panels}</span>
                <span style={{ flexGrow: 1 }} />
                <span style={{ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.06em', padding: '2px 8px', border: `1px solid ${flagged.length ? tokens.red : tokens.green}`, color: flagged.length ? tokens.red : tokens.green }}>
                  {flagged.length ? `${flagged.length} FLAGGED` : 'ALL IN RANGE'}
                </span>
                <span onClick={async (e) => { e.stopPropagation(); if (window.confirm(`Delete this lab report? (${l.lab_name} ${l.taken_at || ''})`)) { await window._supabaseClient.from('health_labs').delete().eq('id', l.id); reload(); } }} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
              </div>
              {openId === l.id && (
                <div style={{ padding: '10px 0 4px' }}>
                  {l.summary && <div style={{ ...prose, marginBottom: '10px' }}>{l.summary}</div>}
                  <div style={{ overflowX: 'auto' }}>
                    <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '460px' }}>
                      <thead><tr><th style={pdTh}>MARKER</th><th style={{ ...pdTh, textAlign: 'right' }}>VALUE</th><th style={pdTh}>UNIT</th><th style={pdTh}>REFERENCE</th><th style={pdTh}>FLAG</th></tr></thead>
                      <tbody>
                        {(l.markers || []).map((m, i) => (
                          <tr key={i}>
                            <td style={{ ...pdTd, fontSize: '12px' }}>{m.name}</td>
                            <td style={{ ...pdTd, textAlign: 'right', fontFamily: fontMono, fontSize: '11.5px', fontWeight: m.flag === 'high' || m.flag === 'low' ? 700 : 400 }}>{String(m.value)}</td>
                            <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>{m.unit || ''}</td>
                            <td style={{ ...pdTd, fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>{m.ref_low != null || m.ref_high != null ? `${m.ref_low ?? '—'}–${m.ref_high ?? '—'}` : ''}</td>
                            <td style={pdTd}><span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.08em', color: flagColor(m.flag) }}>{(m.flag || 'normal').toUpperCase()}</span></td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                  {Array.isArray(l.suggestions) && l.suggestions.length > 0 && (
                    <div style={{ marginTop: '8px' }}>{l.suggestions.map((s, i) => <div key={i} style={{ ...prose, fontSize: '12.5px', padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {s}</div>)}</div>
                  )}
                </div>
              )}
            </div>
          );
        })}
        <HhPager total={labs.length} page={page} setPage={setPage} size={5} />
        {d && (
          <div style={{ borderTop: `1px solid ${tokens.inkLine}`, marginTop: '14px', paddingTop: '12px' }}>
            <div onClick={toggleFold} title={folded ? 'Show the Claude output' : 'Hide the Claude output'} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.16em', color: tokens.inkMute, cursor: 'pointer', userSelect: 'none' }}>
              {folded ? '▸' : '▾'} MEDICAL SUMMARY · {report ? new Date(report.generated_at).toLocaleDateString() : ''}{folded ? ' · HIDDEN' : ''}
            </div>
            {!folded && (<React.Fragment>
            <div style={{ fontSize: '14px', lineHeight: 1.55, marginTop: '8px' }}>{d.headline}</div>
            {d.patient_summary && <div style={{ ...prose, marginTop: '6px' }}>{d.patient_summary}</div>}
            {d.bp_summary && (<div><div style={secLbl}>BLOOD PRESSURE</div><div style={prose}>{d.bp_summary}</div></div>)}
            {Array.isArray(d.marker_trends) && d.marker_trends.length > 0 && (<div>
              <div style={secLbl}>MARKER TRENDS</div>
              {d.marker_trends.map((t, i) => (
                <div key={i} style={{ padding: '4px 0', fontSize: '12.5px' }}>
                  <span style={{ fontWeight: 500 }}>{t.name}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '8.5px', letterSpacing: '0.08em', color: flagColor(t.flag), marginLeft: '8px' }}>{(t.flag || '').toUpperCase()}</span>
                  <span style={{ fontFamily: fontMono, fontSize: '10.5px', color: tokens.inkMute, marginLeft: '10px' }}>{(t.readings || []).map((r) => `${r.date}: ${r.value}${r.unit ? ` ${r.unit}` : ''}`).join(' → ')}</span>
                  <div style={{ ...prose, fontSize: '12.5px' }}>{t.read}</div>
                </div>
              ))}
            </div>)}
            {[['FLAGS', d.flags], ['SUGGESTIONS', d.suggestions], ['QUESTIONS FOR THE DOCTOR', d.questions_for_doctor]].map(([k, v]) => Array.isArray(v) && v.length ? (
              <div key={k}><div style={secLbl}>{k}</div>{v.map((x, i) => <div key={i} style={{ ...prose, fontSize: '12.5px', padding: '2px 0 2px 14px', textIndent: '-14px' }}>· {x}</div>)}</div>
            ) : null)}
            </React.Fragment>)}
          </div>
        )}
      </div>
    </PdShell>
  );
};

// ── Page ──────────────────────────────────────────────────────────────────────

const PdPlaceholder = ({ title, note }) => (
  <PdShell collapseKey={`pd.ph.${title}`} title={title} defaultCollapsed>
    <div style={{ padding: '16px 20px', fontSize: '13px', color: tokens.inkSoft }}>{note}</div>
  </PdShell>
);

const PersonalFinancial = () => {
  const [txns, setTxns] = React.useState(null);
  const [trips, setTrips] = React.useState(null);

  const loadTxns = React.useCallback(async () => {
    const client = window._supabaseClient;
    if (!client) { setTxns([]); return; }
    // Page through everything (YTD files add up) — capped at 8k rows. The final
    // id tiebreak keeps the order DETERMINISTIC: batch-ingested rows share the
    // same created_at, and without it same-day rows reshuffle on every refetch.
    const all = [];
    for (let from = 0; from < 8000; from += 1000) {
      const { data } = await client.from('personal_transactions').select('*')
        .order('txn_date', { ascending: false }).order('created_at', { ascending: false }).order('id', { ascending: true })
        .range(from, from + 999);
      all.push(...(data || []));
      if (!data || data.length < 1000) break;
    }
    setTxns(all);
  }, []);
  const loadTrips = React.useCallback(async () => {
    const client = window._supabaseClient;
    if (!client) { setTrips([]); return; }
    const { data } = await client.from('personal_trips').select('*').order('created_at', { ascending: false });
    setTrips(data || []);
  }, []);
  React.useEffect(() => { loadTxns(); loadTrips(); }, [loadTxns, loadTrips]);
  const reload = React.useCallback(async () => { await Promise.all([loadTxns(), loadTrips()]); }, [loadTxns, loadTrips]);

  // Edit a transaction in place — DB update + local merge, NO refetch, so the
  // row never moves or flickers when tagging W/P, assigning trips, etc.
  const patchTxn = React.useCallback(async (id, p) => {
    const client = window._supabaseClient;
    if (!client) return;
    await client.from('personal_transactions').update(p).eq('id', id);
    setTxns((prev) => (prev || []).map((t) => (t.id === id ? { ...t, ...p } : t)));
  }, []);
  const removeTxn = React.useCallback(async (id) => {
    const client = window._supabaseClient;
    if (!client) return;
    await client.from('personal_transactions').delete().eq('id', id);
    setTxns((prev) => (prev || []).filter((t) => t.id !== id));
  }, []);

  // Auto-lock the sensitive sections: on leaving the page, and after the tab has
  // been hidden for 2 minutes.
  React.useEffect(() => {
    let hideTimer;
    const onVis = () => {
      clearTimeout(hideTimer);
      if (document.visibilityState === 'hidden') hideTimer = setTimeout(() => pdLocks.lockAll(), 120000);
    };
    document.addEventListener('visibilitychange', onVis);
    return () => { document.removeEventListener('visibilitychange', onVis); clearTimeout(hideTimer); pdLocks.lockAll(); };
  }, []);

  // Net Worth + Portfolio Overview share one lock — verify once, both open.
  return (
    <React.Fragment>
      <PdSpendingPanel txns={txns} trips={trips} reload={reload} patchTxn={patchTxn} removeTxn={removeTxn} />
      <PdTripsPanel txns={txns} trips={trips} reload={reload} patchTxn={patchTxn} />
      <PdLockGate id="pd.sensitive" label="Net Worth"><PdNetWorthPanel /></PdLockGate>
      <PdLockGate id="pd.sensitive" label="Portfolio Overview"><PdPortfolioPanel /></PdLockGate>
      <PdLockGate id="pd.sensitive" label="Options Strategies"><PdOptionsPanel /></PdLockGate>
    </React.Fragment>
  );
};

// Owner-editable tagline (site_copy key 'personal.tagline'); pencil → textarea.
const PD_TAGLINE_DEFAULT = 'The life side of the ledger — spending, net worth, and the admin that goes with them. Real Estate, Health, CRM and Travel arrive in later steps.';
const PdTagline = () => {
  const [text, setText] = React.useState(null);        // null = loading → default
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState('');
  const [err, setErr] = React.useState('');
  React.useEffect(() => {
    let dead = false;
    (async () => {
      const v = await window.siteCopyGet('personal.tagline');
      if (!dead) setText(typeof v === 'string' && v.trim() ? v : PD_TAGLINE_DEFAULT);
    })();
    return () => { dead = true; };
  }, []);
  const save = async () => {
    setErr('');
    try { await window.siteCopySet('personal.tagline', draft.trim()); setText(draft.trim() || PD_TAGLINE_DEFAULT); setEditing(false); }
    catch (e) { setErr(e.message); }
  };
  if (editing) return (
    <div style={{ maxWidth: '62ch' }}>
      <textarea value={draft} onChange={(e) => setDraft(e.target.value)} rows={3} autoFocus
        style={{ width: '100%', padding: '12px 14px', border: `1px solid ${tokens.inkLine}`, background: '#FDFBF7', fontFamily: 'inherit', fontSize: '17px', lineHeight: 1.6, color: tokens.ink, boxSizing: 'border-box', resize: 'vertical' }} />
      {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginTop: '6px' }}>{err}</div>}
      <div style={{ display: 'flex', gap: '14px', marginTop: '10px', fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.08em' }}>
        <span onClick={save} style={{ cursor: 'pointer', color: tokens.paper, background: tokens.ink, padding: '7px 16px' }}>SAVE</span>
        <span onClick={() => setEditing(false)} style={{ cursor: 'pointer', color: tokens.inkMute, padding: '7px 0' }}>CANCEL</span>
      </div>
    </div>
  );
  return (
    <p style={{ fontSize: '21px', lineHeight: 1.6, letterSpacing: '-0.01em', color: tokens.inkSoft, maxWidth: '62ch' }}>
      {text === null ? PD_TAGLINE_DEFAULT : text}
      <span onClick={() => { setDraft(text === null ? PD_TAGLINE_DEFAULT : text); setEditing(true); }} title="Edit tagline"
        style={{ cursor: 'pointer', marginLeft: '10px', fontSize: '14px', color: tokens.inkMute }}>✎</span>
    </p>
  );
};

// ── To-dos — compact capture-and-clear list under the calendar ────────────────
// Natural-language quick-add (todo-hub parses date / reminder cadence and
// smart-classifies work/personal + short/long), daily-weekly-monthly reminder
// digests via the 06:30 cron scan, and optional linking to an Outlook event.
// The WhatsApp bot writes to the same table ("add to my list: …").
const PdTodos = () => {
  const isMobile = useIsMobile();
  const [folded, toggleFold] = pdCollapsed('fold.todos', false);
  const [todos, setTodos] = React.useState(null);
  const [filter, setFilter] = React.useState('all');
  const [addText, setAddText] = React.useState('');
  const [adding, setAdding] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [showDone, setShowDone] = React.useState(false);
  const [linkFor, setLinkFor] = React.useState(null);   // todo id with the event picker open
  const [events, setEvents] = React.useState(null);      // cached next-14d events
  const [jobs, setJobs] = React.useState([]);            // cowork queue
  const [showJobs, setShowJobs] = React.useState(false);
  const [openJob, setOpenJob] = React.useState(null);    // job id with result expanded

  const client = () => window._supabaseClient;
  const todayStr = new Date().toLocaleDateString('en-CA');

  const load = React.useCallback(async () => {
    if (!client()) { setTodos([]); return; }
    const [{ data }, { data: jb }] = await Promise.all([
      client().from('personal_todos').select('*')
        .order('status').order('due_date', { ascending: true, nullsFirst: false }).order('created_at').limit(300),
      client().from('cowork_jobs').select('*').order('created_at', { ascending: false }).limit(10),
    ]);
    setTodos(data || []);
    setJobs(jb || []);
  }, []);
  React.useEffect(() => { load(); }, [load]);
  // Light poll while a dispatched job is in flight so DONE shows up unprompted.
  React.useEffect(() => {
    if (!jobs.some((j) => j.status === 'queued' || j.status === 'running')) return;
    const t = setInterval(load, 20000);
    return () => clearInterval(t);
  }, [jobs, load]);

  const dispatchCowork = async (t) => {
    const preset = t ? `${t.title}${t.notes ? ` — ${t.notes}` : ''}` : '';
    const text = window.prompt('Instructions for Claude on your laptop:', preset);
    if (!text || !text.trim()) return;
    setErr('');
    try {
      const { data, error } = await client().functions.invoke('todo-hub', { body: { action: 'cowork', text: text.trim(), todo_id: t ? t.id : '', source: 'site' } });
      let payload = data;
      if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
      if (payload && payload.error) throw new Error(payload.error);
      setShowJobs(true);
      await load();
    } catch (e) { setErr(e.message); }
  };

  const add = async () => {
    const text = addText.trim();
    if (!text || adding) return;
    setAdding(true); setErr('');
    try {
      const { data, error } = await client().functions.invoke('todo-hub', { body: { action: 'add', text, source: 'site' } });
      let payload = data;
      if (error) { try { payload = await error.context.json(); } catch {} if (!payload || !payload.error) throw new Error(error.message || 'Edge function error'); }
      if (payload && payload.error) throw new Error(payload.error);
      setAddText('');
      await load();
    } catch (e) { setErr(e.message); }
    setAdding(false);
  };

  const patch = async (id, p) => { await client().from('personal_todos').update(p).eq('id', id); await load(); };
  const del = async (t) => { if (window.confirm(`Delete "${t.title}"?`)) { await client().from('personal_todos').delete().eq('id', t.id); await load(); } };
  const cycleRemind = (t) => {
    const order = ['none', 'daily', 'weekly', 'monthly'];
    patch(t.id, { remind: order[(order.indexOf(t.remind) + 1) % order.length] });
  };
  const editDue = (t) => {
    const v = window.prompt('Due date (YYYY-MM-DD, blank to clear):', t.due_date || '');
    if (v === null) return;
    if (v.trim() === '') patch(t.id, { due_date: null });
    else if (/^\d{4}-\d{2}-\d{2}$/.test(v.trim())) patch(t.id, { due_date: v.trim() });
  };
  const openLink = async (t) => {
    if (t.event_id) { if (window.confirm(`Unlink "${t.event_title}"?`)) patch(t.id, { event_id: '', event_title: '', event_start: null }); return; }
    setLinkFor(linkFor === t.id ? null : t.id);
    if (!events) {
      try {
        const { data } = await client().functions.invoke('calendar-hub', { body: { action: 'list_events', time_min: new Date(Date.now() - 86400000).toISOString(), time_max: new Date(Date.now() + 14 * 86400000).toISOString(), tz: 'Europe/London' } });
        setEvents((data && data.events) || []);
      } catch { setEvents([]); }
    }
  };
  const linkEvent = (t, ev) => { setLinkFor(null); patch(t.id, { event_id: ev.id, event_title: ev.title, event_start: ev.start.includes('T') ? ev.start : null }); };

  if (todos === null) return null;
  const open = todos.filter((t) => t.status === 'open');
  const done = todos.filter((t) => t.status === 'done').sort((a, b) => (a.done_at < b.done_at ? 1 : -1));
  const shown = open.filter((t) =>
    filter === 'work' ? t.area === 'work' : filter === 'personal' ? t.area === 'personal' : filter === 'long' ? t.horizon === 'long' : true);
  const dueToday = open.filter((t) => t.due_date && t.due_date <= todayStr).length;

  const chip = (on) => ({ fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.08em', padding: '4px 10px', cursor: 'pointer', border: `1px solid ${on ? tokens.ink : tokens.inkLine}`, background: on ? tokens.ink : 'none', color: on ? tokens.paper : tokens.inkMute, borderRadius: '999px' });
  const tag = { fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.05em', padding: '2px 8px', borderRadius: '999px', border: `1px solid ${tokens.inkLine}`, color: tokens.inkMute, cursor: 'pointer', whiteSpace: 'nowrap' };

  return (
    <div style={{ marginTop: '20px', border: `1px solid ${tokens.inkLine}`, borderRadius: '8px', background: tokens.paper }}>
      <div onClick={toggleFold} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '14px', padding: '12px 18px', cursor: 'pointer', userSelect: 'none', flexWrap: 'wrap' }}>
        <span style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.18em', color: folded ? tokens.inkMute : tokens.ink }}>
          TO-DOS{folded ? ' · HIDDEN' : ''}
        </span>
        <span style={{ display: 'flex', gap: '14px', alignItems: 'baseline' }}>
          <span style={{ fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.08em', color: tokens.inkMute }}>
            {open.length} OPEN{dueToday ? ` · ${dueToday} DUE` : ''}
          </span>
          <span style={{ fontSize: '12px', color: tokens.inkMute }}>{folded ? '▸' : '▾'}</span>
        </span>
      </div>
      {!folded && (
        <div style={{ padding: '0 18px 16px' }}>
          {/* quick add — natural language, parsed + classified server-side */}
          <div style={{ display: 'flex', gap: '8px' }}>
            <input value={addText} onChange={(e) => setAddText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && add()}
              placeholder={isMobile ? 'Add a to-do…' : 'Add a to-do — try "renew passport by Friday, remind me weekly"'}
              style={{ flex: 1, padding: '9px 12px', border: `1px solid ${tokens.inkLine}`, background: '#FDFBF7', fontSize: '13px', color: tokens.ink, minWidth: 0, borderRadius: '4px' }} />
            <button onClick={add} disabled={adding} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.08em', padding: '9px 16px', background: tokens.ink, color: tokens.paper, border: 'none', cursor: adding ? 'default' : 'pointer', borderRadius: '4px', whiteSpace: 'nowrap' }}>
              {adding ? 'FILING…' : '+ ADD'}
            </button>
          </div>
          {err && <div style={{ fontFamily: fontMono, fontSize: '10px', color: tokens.red, marginTop: '8px' }}>{err}</div>}

          {/* filters */}
          <div style={{ display: 'flex', gap: '6px', margin: '12px 0 4px', flexWrap: 'wrap' }}>
            {[['all', 'ALL'], ['work', 'WORK'], ['personal', 'PERSONAL'], ['long', 'LONG-TERM']].map(([k, l]) => (
              <span key={k} onClick={() => setFilter(k)} style={chip(filter === k)}>{l}</span>
            ))}
          </div>

          {/* open list */}
          {shown.length === 0 ? (
            <div style={{ fontSize: '12.5px', color: tokens.inkMute, padding: '12px 0 4px' }}>
              {open.length === 0 ? 'Nothing open — capture anything above, or text the bot "add to my list: …".' : 'Nothing in this filter.'}
            </div>
          ) : shown.map((t) => {
            const overdue = t.due_date && t.due_date < todayStr;
            return (
              <div key={t.id} style={{ borderBottom: `1px solid ${tokens.inkLineSoft}`, padding: '8px 0' }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' }}>
                  <span onClick={() => patch(t.id, { status: 'done', done_at: new Date().toISOString() })} title="Mark done"
                    style={{ cursor: 'pointer', fontSize: '13px', color: tokens.inkMute, flexShrink: 0 }}>○</span>
                  <span style={{ fontSize: '13px', letterSpacing: '-0.01em', minWidth: 0, flex: '1 1 200px' }}>
                    {t.title}
                    {t.notes && <span style={{ color: tokens.inkMute, fontSize: '11.5px' }}> — {t.notes}</span>}
                  </span>
                  <span style={{ display: 'flex', gap: '6px', alignItems: 'baseline', flexWrap: 'wrap', flexShrink: 0 }}>
                    <span onClick={() => patch(t.id, { area: t.area === 'work' ? 'personal' : 'work' })} title="Toggle work / personal"
                      style={{ ...tag, borderColor: t.area === 'work' ? '#4A6FD1' : tokens.inkLine, color: t.area === 'work' ? '#4A6FD1' : tokens.inkMute }}>
                      {t.area === 'work' ? 'WORK' : 'PERS'}
                    </span>
                    {t.horizon === 'long' && <span onClick={() => patch(t.id, { horizon: 'short' })} title="Make short-term" style={tag}>LT</span>}
                    <span onClick={() => editDue(t)} title="Edit due date"
                      style={{ ...tag, color: overdue ? tokens.red : t.due_date ? tokens.ink : tokens.inkMute, borderColor: overdue ? tokens.red : tokens.inkLine }}>
                      {t.due_date ? t.due_date.slice(5) : '+ DUE'}
                    </span>
                    <span onClick={() => cycleRemind(t)} title="Reminder: none → daily → weekly → monthly"
                      style={{ ...tag, color: t.remind !== 'none' ? tokens.ochre : tokens.inkMute, borderColor: t.remind !== 'none' ? tokens.ochre : tokens.inkLine }}>
                      ⟳ {t.remind === 'none' ? 'OFF' : t.remind.slice(0, 1).toUpperCase()}
                    </span>
                    <span onClick={() => openLink(t)} title={t.event_id ? `Linked: ${t.event_title}` : 'Link to a calendar event'}
                      style={{ ...tag, maxWidth: '150px', overflow: 'hidden', textOverflow: 'ellipsis', color: t.event_id ? tokens.green : tokens.inkMute }}>
                      {t.event_id ? `📅 ${t.event_title}` : '🔗'}
                    </span>
                    <span onClick={() => dispatchCowork(t)} title="Dispatch to Claude on your laptop" style={{ ...tag }}>🤖</span>
                    <span onClick={() => del(t)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                  </span>
                </div>
                {linkFor === t.id && (
                  <div style={{ margin: '8px 0 4px 23px', border: `1px solid ${tokens.inkLine}`, borderRadius: '4px', background: '#FDFBF7', maxHeight: '180px', overflowY: 'auto' }}>
                    {events === null ? (
                      <div style={{ padding: '10px 12px', fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>LOADING EVENTS…</div>
                    ) : events.length === 0 ? (
                      <div style={{ padding: '10px 12px', fontFamily: fontMono, fontSize: '10px', color: tokens.inkMute }}>NO EVENTS IN THE NEXT 14 DAYS (OR CALENDAR NOT CONNECTED)</div>
                    ) : events.map((ev) => (
                      <div key={ev.id} onClick={() => linkEvent(t, ev)} className="case-row"
                        style={{ padding: '7px 12px', cursor: 'pointer', display: 'flex', gap: '10px', alignItems: 'baseline', borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
                        <span style={{ fontFamily: fontMono, fontSize: '9.5px', color: tokens.ochre, whiteSpace: 'nowrap' }}>
                          {ev.start.slice(5, 10)}{ev.start.includes('T') ? ` ${ev.start.slice(11, 16)}` : ''}
                        </span>
                        <span style={{ fontSize: '12px' }}>{ev.title}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            );
          })}

          {/* Claude queue — jobs dispatched to the laptop runner */}
          <div style={{ marginTop: '12px', display: 'flex', gap: '16px', alignItems: 'baseline' }}>
            <span onClick={() => setShowJobs(!showJobs)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.1em', color: jobs.some((j) => j.status === 'queued' || j.status === 'running') ? tokens.ochre : tokens.inkMute, userSelect: 'none' }}>
              {showJobs ? '▾' : '▸'} CLAUDE QUEUE · {jobs.length}
            </span>
            <span onClick={() => dispatchCowork(null)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.08em', color: tokens.inkMute, borderBottom: `1px solid ${tokens.inkLine}` }}>+ DISPATCH</span>
          </div>
          {showJobs && jobs.map((j) => {
            const col = j.status === 'done' ? tokens.green : j.status === 'failed' ? tokens.red : j.status === 'running' ? tokens.ochre : tokens.inkMute;
            return (
              <div key={j.id} style={{ padding: '6px 0', borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
                <div style={{ display: 'flex', gap: '10px', alignItems: 'baseline' }}>
                  <span onClick={() => setOpenJob(openJob === j.id ? null : j.id)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9px', letterSpacing: '0.08em', color: col, border: `1px solid ${col}`, borderRadius: '999px', padding: '2px 8px', whiteSpace: 'nowrap' }}>
                    {j.status.toUpperCase()}
                  </span>
                  <span onClick={() => setOpenJob(openJob === j.id ? null : j.id)} style={{ fontSize: '12px', color: tokens.inkSoft, cursor: 'pointer', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{j.prompt}</span>
                  {j.status === 'queued' && (
                    <span onClick={async () => { await client().from('cowork_jobs').update({ status: 'cancelled' }).eq('id', j.id); load(); }}
                      style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                  )}
                </div>
                {openJob === j.id && (
                  <div style={{ margin: '6px 0 2px', padding: '10px 12px', border: `1px solid ${tokens.inkLineSoft}`, borderRadius: '4px', background: '#FDFBF7', fontFamily: fontMono, fontSize: '10.5px', lineHeight: 1.6, whiteSpace: 'pre-wrap', maxHeight: '220px', overflowY: 'auto', color: tokens.inkSoft }}>
                    {j.result || (j.status === 'queued' ? 'Waiting for the laptop runner to pick this up (see COWORK-SETUP.md if it never does).' : j.status === 'running' ? 'Running on your laptop…' : '(no output)')}
                  </div>
                )}
              </div>
            );
          })}

          {/* done fold */}
          {done.length > 0 && (
            <div style={{ marginTop: '10px' }}>
              <span onClick={() => setShowDone(!showDone)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '9.5px', letterSpacing: '0.1em', color: tokens.inkMute, userSelect: 'none' }}>
                {showDone ? '▾' : '▸'} DONE · {done.length}
              </span>
              {showDone && done.slice(0, 8).map((t) => (
                <div key={t.id} style={{ display: 'flex', alignItems: 'baseline', gap: '10px', padding: '5px 0', borderBottom: `1px solid ${tokens.inkLineSoft}` }}>
                  <span onClick={() => patch(t.id, { status: 'open', done_at: null })} title="Reopen" style={{ cursor: 'pointer', fontSize: '13px', color: tokens.green }}>●</span>
                  <span style={{ fontSize: '12.5px', color: tokens.inkMute, textDecoration: 'line-through', flex: 1 }}>{t.title}</span>
                  <span onClick={() => del(t)} style={{ cursor: 'pointer', fontFamily: fontMono, fontSize: '11px', color: tokens.inkLine }}>✕</span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
};

const Personal = ({ user }) => {
  if (getRole(user) !== 'owner') return null;   // owner-only, also enforced by routing + RLS
  return (
    <div>
      <RailSection index="" rail="">
        <div style={{ fontFamily: fontMono, fontSize: '11px', letterSpacing: '0.18em', color: tokens.inkMute, marginBottom: '30px', textTransform: 'uppercase' }}>Private · Owner only</div>
        <h1 style={{ fontSize: '68px', lineHeight: 1.0, letterSpacing: '-0.03em', fontWeight: 400, margin: 0, marginBottom: '34px' }}>Personal Dashboard<span style={{ color: tokens.ochre }}>.</span></h1>
        <PdTagline />

        {/* The calendar leads the page — the default view when landing here.
            Deliberately outside the Financial section and its privacy lock. */}
        <div style={{ marginTop: '36px', maxWidth: '1200px' }}>
          <PdCalendarPanel />
          <PdTodos />
        </div>
      </RailSection>

      <RailSection index="00" rail="Financial">
        <RailHead title="Financial" lead="Card spend across Amex UK, Amex US and Chase — categorized, taggable and reimbursable — plus the full asset and liability picture, with Claude reads on both." />
        <PersonalFinancial />
      </RailSection>

      <RailSection index="01" rail="Health">
        <RailHead title="Health" lead="Weight, blood pressure (AM vs PM), Garmin sleep and HRV, Peloton + gym training, plain-language food logging with estimated macros, daily energy balance, labs — and a Claude Health Coach across all of it. Log anything by texting the WhatsApp bot or emailing add@parse.urdaneta.io with add_to_health in the subject." />
        <PdHealthSection />
      </RailSection>

      <RailSection index="02–04" rail="Coming Next" style={{ borderBottom: 'none' }}>
        <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
          {['Real Estate', 'CRM', 'Travel'].map((s, i) => (
            <span key={s} style={{ fontFamily: fontMono, fontSize: '10px', letterSpacing: '0.1em', color: tokens.inkMute, border: `1px dashed ${tokens.inkLine}`, borderRadius: '999px', padding: '8px 16px' }}>
              {String(i + 2).padStart(2, '0')} {s.toUpperCase()} — LATER STEP
            </span>
          ))}
        </div>
      </RailSection>
    </div>
  );
};

window.Personal = Personal;
// The Claude/AI analysis renderers — exposed for reuse and for the visual
// test harness (they render pure row-data, no backend needed).
window.PpAnalysis = PpAnalysis;
window.PdTodos = PdTodos;
window.PdCalendarPanel = PdCalendarPanel;
window.PoAnalysis = PoAnalysis;
window.PdInsights = PdInsights;
