/* eslint-disable */

// ─── tweaks-panel.jsx (inlined) ──────────────────────────────────────────────

const __TWEAKS_STYLE = `
  .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
    max-height:calc(100vh - 32px);display:flex;flex-direction:column;
    transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
    background:rgba(250,249,247,.78);color:#29261b;
    -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
    border:.5px solid rgba(255,255,255,.6);border-radius:14px;
    box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
    font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
  .twk-hd{display:flex;align-items:center;justify-content:space-between;
    padding:10px 8px 10px 14px;cursor:move;user-select:none}
  .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
  .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
    width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
  .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
  .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
    overflow-y:auto;overflow-x:hidden;min-height:0;
    scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
  .twk-body::-webkit-scrollbar{width:8px}
  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
  .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
    border:2px solid transparent;background-clip:content-box}
  .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
    border:2px solid transparent;background-clip:content-box}
  .twk-row{display:flex;flex-direction:column;gap:5px}
  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
    color:rgba(41,38,27,.72)}
  .twk-lbl>span:first-child{font-weight:500}
  .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}

  .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    color:rgba(41,38,27,.45);padding:10px 0 0}
  .twk-sect:first-child{padding-top:0}

  .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;
    background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
  .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
  select.twk-field{padding-right:22px;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
    background-repeat:no-repeat;background-position:right 8px center}

  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
    border-radius:999px;background:rgba(0,0,0,.12);outline:none}
  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
    width:14px;height:14px;border-radius:50%;background:#fff;
    border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
  .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
    background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}

  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
    background:rgba(0,0,0,.06);user-select:none}
  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
    background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
    transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
  .twk-seg.dragging .twk-seg-thumb{transition:none}
  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
    background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
    border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
    overflow-wrap:anywhere}

  .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
    background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
  .twk-toggle[data-on="1"]{background:#34c759}
  .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
    background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
  .twk-toggle[data-on="1"] i{transform:translateX(14px)}

  .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
    border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
  .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
    user-select:none;padding-right:8px}
  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
    font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
    outline:none;color:inherit;-moz-appearance:textfield}
  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
    -webkit-appearance:none;margin:0}
  .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}

  .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
    background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
  .twk-btn:hover{background:rgba(0,0,0,.88)}
  .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
  .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}

  .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
    border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
    background:transparent;flex-shrink:0}
  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}

  .twk-chips{display:flex;gap:6px}
  .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
    padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
    box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
    transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
  .twk-chip:hover{transform:translateY(-1px);
    box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
  .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
    0 2px 6px rgba(0,0,0,.15)}
  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
    display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
  .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
  .twk-chip>span>i:first-child{box-shadow:none}
  .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
`;

// ── useTweaks ────────────────────────────────────────────────────────────────
function useTweaks(defaults) {
  const [values, setValues] = React.useState(defaults);
  const setTweak = React.useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues((prev) => ({ ...prev, ...edits }));
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
    window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
  }, []);
  return [values, setTweak];
}

// ── TweaksPanel ──────────────────────────────────────────────────────────────
function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) {
  const [open, setOpen] = React.useState(false);
  const dragRef = React.useRef(null);
  const hasDeckStage = React.useMemo(
    () => typeof document !== 'undefined' && !!document.querySelector('deck-stage'),
    [],
  );
  const [railEnabled, setRailEnabled] = React.useState(
    () => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled,
  );
  React.useEffect(() => {
    if (!hasDeckStage || railEnabled) return undefined;
    const onMsg = (e) => {
      if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true);
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, [hasDeckStage, railEnabled]);
  const [railVisible, setRailVisible] = React.useState(() => {
    try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; }
  });
  const toggleRail = (on) => {
    setRailVisible(on);
    window.postMessage({ type: '__deck_rail_visible', on }, '*');
  };
  const offsetRef = React.useRef({ x: 16, y: 16 });
  const PAD = 16;

  const clampToViewport = React.useCallback(() => {
    const panel = dragRef.current;
    if (!panel) return;
    const w = panel.offsetWidth, h = panel.offsetHeight;
    const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
    const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
    offsetRef.current = {
      x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
      y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
    };
    panel.style.right = offsetRef.current.x + 'px';
    panel.style.bottom = offsetRef.current.y + 'px';
  }, []);

  React.useEffect(() => {
    if (!open) return;
    clampToViewport();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', clampToViewport);
      return () => window.removeEventListener('resize', clampToViewport);
    }
    const ro = new ResizeObserver(clampToViewport);
    ro.observe(document.documentElement);
    return () => ro.disconnect();
  }, [open, clampToViewport]);

  React.useEffect(() => {
    const onMsg = (e) => {
      const t = e?.data?.type;
      if (t === '__activate_edit_mode') setOpen(true);
      else if (t === '__deactivate_edit_mode') setOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const dismiss = () => {
    setOpen(false);
    window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
  };

  const onDragStart = (e) => {
    const panel = dragRef.current;
    if (!panel) return;
    const r = panel.getBoundingClientRect();
    const sx = e.clientX, sy = e.clientY;
    const startRight = window.innerWidth - r.right;
    const startBottom = window.innerHeight - r.bottom;
    const move = (ev) => {
      offsetRef.current = {
        x: startRight - (ev.clientX - sx),
        y: startBottom - (ev.clientY - sy),
      };
      clampToViewport();
    };
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };

  if (!open) return null;
  return (
    <>
      <style>{__TWEAKS_STYLE}</style>
      <div ref={dragRef} className="twk-panel" data-noncommentable=""
           style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
        <div className="twk-hd" onMouseDown={onDragStart}>
          <b>{title}</b>
          <button className="twk-x" aria-label="Close tweaks"
                  onMouseDown={(e) => e.stopPropagation()}
                  onClick={dismiss}>✕</button>
        </div>
        <div className="twk-body">
          {children}
          {hasDeckStage && railEnabled && !noDeckControls && (
            <TweakSection title="Deck">
              <TweakToggle label="Thumbnail rail" value={railVisible} onChange={toggleRail} />
            </TweakSection>
          )}
        </div>
      </div>
    </>
  );
}

function TweakSection({ title, label, children }) {
  return (
    <>
      <div className="twk-sect">{title || label}</div>
      {children}
    </>
  );
}

function TweakRow({ label, value, children, inline = false }) {
  return (
    <div className={inline ? 'twk-row twk-row-h' : 'twk-row'}>
      <div className="twk-lbl">
        <span>{label}</span>
        {value != null && <span className="twk-val">{value}</span>}
      </div>
      {children}
    </div>
  );
}

function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
  return (
    <TweakRow label={label} value={`${value}${unit}`}>
      <input type="range" className="twk-slider" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </TweakRow>
  );
}

function TweakToggle({ label, value, onChange }) {
  return (
    <div className="twk-row twk-row-h">
      <div className="twk-lbl"><span>{label}</span></div>
      <button type="button" className="twk-toggle" data-on={value ? '1' : '0'}
              role="switch" aria-checked={!!value}
              onClick={() => onChange(!value)}><i /></button>
    </div>
  );
}

function TweakRadio({ label, value, options, onChange }) {
  const trackRef = React.useRef(null);
  const [dragging, setDragging] = React.useState(false);
  const valueRef = React.useRef(value);
  valueRef.current = value;

  const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
  const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
  const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
  if (!fitsAsSegments) {
    const resolve = (s) => {
      const m = options.find((o) => String(typeof o === 'object' ? o.value : o) === s);
      return m === undefined ? s : typeof m === 'object' ? m.value : m;
    };
    return <TweakSelect label={label} value={value} options={options}
                        onChange={(s) => onChange(resolve(s))} />;
  }
  const opts = options.map((o) => (typeof o === 'object' ? o : { value: o, label: o }));
  const idx = Math.max(0, opts.findIndex((o) => o.value === value));
  const n = opts.length;

  const segAt = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    const inner = r.width - 4;
    const i = Math.floor(((clientX - r.left - 2) / inner) * n);
    return opts[Math.max(0, Math.min(n - 1, i))].value;
  };

  const onPointerDown = (e) => {
    setDragging(true);
    const v0 = segAt(e.clientX);
    if (v0 !== valueRef.current) onChange(v0);
    const move = (ev) => {
      if (!trackRef.current) return;
      const v = segAt(ev.clientX);
      if (v !== valueRef.current) onChange(v);
    };
    const up = () => {
      setDragging(false);
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  return (
    <TweakRow label={label}>
      <div ref={trackRef} role="radiogroup" onPointerDown={onPointerDown}
           className={dragging ? 'twk-seg dragging' : 'twk-seg'}>
        <div className="twk-seg-thumb"
             style={{ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
                      width: `calc((100% - 4px) / ${n})` }} />
        {opts.map((o) => (
          <button key={o.value} type="button" role="radio" aria-checked={o.value === value}>
            {o.label}
          </button>
        ))}
      </div>
    </TweakRow>
  );
}

function TweakSelect({ label, value, options, onChange }) {
  return (
    <TweakRow label={label}>
      <select className="twk-field" value={value} onChange={(e) => onChange(e.target.value)}>
        {options.map((o) => {
          const v = typeof o === 'object' ? o.value : o;
          const l = typeof o === 'object' ? o.label : o;
          return <option key={v} value={v}>{l}</option>;
        })}
      </select>
    </TweakRow>
  );
}

// ─── End tweaks-panel ────────────────────────────────────────────────────────


const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ─── Taxonomy (display) ─────────────────────────────────────────────────────
// Backend returns the canonical `category` and `family` keys per org.
// These constants are display-only.
const CATEGORY_LABELS = {
  physical_assets:      "Physical Asset",
  personal_valuables:   "Personal Valuable",
  digital_devices:      "Digital Devices",
  financial_accounts:   "Financial Accounts",
  investment_business:  "Investment & Business",
  financial_protection: "Financial Protection",
  employment_benefits:  "Employment Benefits",
  digital_accounts:     "Digital Accounts",
  government_legal:     "Government & Legal",
  cards_memberships:    "Cards & Memberships",
  subscriptions:        "Subscriptions",
  unknown:              "Other",
};

const CATEGORY_ORDER = [
  "physical_assets",
  "personal_valuables",
  "digital_devices",
  "financial_accounts",
  "investment_business",
  "financial_protection",
  "employment_benefits",
  "digital_accounts",
  "government_legal",
  "cards_memberships",
  "subscriptions",
  "unknown",
];

// CATEGORY_TO_FAMILY now comes from the backend (/org-summaries -> category_family),
// threaded to <Sidebar> as a prop. Kept out of the frontend to avoid a duplicate list.

if (typeof window !== "undefined") {
  window.CATEGORY_LABELS = CATEGORY_LABELS;
  window.CATEGORY_ORDER  = CATEGORY_ORDER;
}

// ─── Clerk (person identity) ─────────────────────────────────────────────────
let clerk = null;

function loadScript(src, attrs = {}) {
  return new Promise((resolve, reject) => {
    const s = document.createElement("script");
    s.src = src;
    s.crossOrigin = "anonymous";
    for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v);
    s.onload = resolve;
    s.onerror = () => reject(new Error(`Failed to load ${src}`));
    document.head.appendChild(s);
  });
}

// Memoised so the mount effect + checkAuth share one init (no duplicate script
// tags or repeat clerk.load() calls).
let __clerkInit = null;
function initClerk() {
  if (__clerkInit) return __clerkInit;
  __clerkInit = _initClerk();
  return __clerkInit;
}

async function _initClerk() {
  const res = await fetch("/auth/config");
  const { publishable_key } = await res.json();
  if (!publishable_key) return null;            // Clerk not configured (dev bypass)
  // Frontend API domain is encoded in the publishable key (Clerk-documented derivation)
  const fapi = atob(publishable_key.split("_")[2]).slice(0, -1);
  // @clerk/ui must load first; clerk.browser.js reads the key from its data-attr
  // and exposes window.Clerk as a ready-made INSTANCE (not a constructor).
  await loadScript(`https://${fapi}/npm/@clerk/ui@1/dist/ui.browser.js`);
  await loadScript(`https://${fapi}/npm/@clerk/clerk-js@6/dist/clerk.browser.js`, {
    "data-clerk-publishable-key": publishable_key,
  });
  if (!window.Clerk) throw new Error("ClerkJS failed to initialise");
  clerk = window.Clerk;                          // global instance, no `new`
  await clerk.load({ ui: { ClerkUI: window.__internal_ClerkUICtor } });
  window.__clerk = clerk;
  return clerk;
}

async function authHeaders() {
  if (!clerk || !clerk.session) return {};
  const token = await clerk.session.getToken(); // cached by ClerkJS, ~60s TTL
  return token ? { Authorization: `Bearer ${token}` } : {};
}

// ─── API ─────────────────────────────────────────────────────────────────────
// Build a readable message from a failed response. FastAPI/pydantic 422s return
// { detail: [{ loc, msg, type }] } — pull out the field name + reason so the UI
// can flag exactly which value didn't match its expected type.
async function errorFromRes(res, fallback) {
  const body = await res.json().catch(() => null);
  const detail = body && body.detail;
  if (Array.isArray(detail)) {
    return detail
      .map(e => `${(e.loc || []).slice(-1)[0] || "value"}: ${e.msg}`)
      .join("; ");
  }
  if (typeof detail === "string") return detail;
  return fallback || `Request failed: ${res.status}`;
}

async function apiGet(path) {
  const res = await fetch(path, { credentials: "include", headers: await authHeaders() });
  if (res.status === 401) throw Object.assign(new Error("Not authenticated"), { status: 401 });
  if (!res.ok) throw new Error(`${path} failed: ${res.status}`);
  return res;
}

async function fetchMe() {
  const res = await apiGet("/auth/status");   // { status, person_id }
  return res.json();
}

async function fetchOrgSummaries() {
  const res = await apiGet("/org-summaries");
  return res.json();
}

async function fetchAccounts() {
  const res = await apiGet("/accounts");
  return res.json();
}

async function postAccount(grantId, email) {
  const res = await fetch("/accounts", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify({ grant_id: grantId, email }),
  });
  if (!res.ok) throw new Error(await errorFromRes(res, `Couldn't connect mailbox (${res.status})`));
  return res.json();
}

// NOTE: backend endpoint DELETE /accounts/{email_account_id} may not exist yet.
async function deleteAccount(emailAccountId) {
  const res = await fetch(`/accounts/${encodeURIComponent(emailAccountId)}`, {
    method: "DELETE",
    credentials: "include",
    headers: await authHeaders(),
  });
  if (!res.ok && res.status !== 204) {
    throw new Error(`DELETE /accounts/${emailAccountId} failed: ${res.status}`);
  }
}

async function triggerIngest() {
  const res = await fetch("/emails/ingest", { method: "POST", credentials: "include", headers: await authHeaders() });
  if (!res.ok) throw new Error(`POST /emails/ingest failed: ${res.status}`);
  return res.json();
}

async function shareReport(to) {
  const res = await fetch("/report/share", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify({ to }),
  });
  if (!res.ok) {
    const detail = await res.json().catch(() => ({}));
    throw new Error(detail.detail || `Could not send (${res.status})`);
  }
}

// Print a single report doc to PDF. `cls` ("print-report" | "print-org") scopes
// the print CSS so only that doc is shown. Cleaned up after the dialog closes.
function printDoc(cls) {
  document.body.classList.add(cls);
  const cleanup = () => {
    document.body.classList.remove(cls);
    window.removeEventListener("afterprint", cleanup);
  };
  window.addEventListener("afterprint", cleanup);
  window.print();
}

async function fetchIngestStatus() {
  const res = await apiGet("/emails/ingest/status");
  return res.json();   // { status: queued|active|complete|failed|idle|no_user }
}

async function fetchWizard() {
  const res = await apiGet("/wizard");   // { sections, profile, contacts, completed }
  return res.json();
}

async function postWizardAnswers(answers) {
  const res = await fetch("/wizard/answers", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify({ answers }),
  });
  if (!res.ok) throw new Error(`/wizard/answers failed: ${res.status}`);
  return res.json();
}

async function fetchResources() {
  const res = await apiGet("/wizard/resources");   // { categories, items }
  return res.json();
}

async function fetchUserResources() {
  const res = await apiGet("/user-resources");   // { total, user_resources }
  return res.json();
}

async function postResourceItem(body) {
  const res = await fetch("/wizard/resources", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(await errorFromRes(res, `/wizard/resources failed: ${res.status}`));
  return res.json();
}

async function deleteResourceItem(id) {
  const res = await fetch(`/wizard/resources/${encodeURIComponent(id)}`, {
    method: "DELETE",
    credentials: "include",
    headers: await authHeaders(),
  });
  if (!res.ok) throw new Error(`DELETE /wizard/resources failed: ${res.status}`);
  return res.json();
}

async function patchResourceItem(id, body) {
  const res = await fetch(`/wizard/resources/${encodeURIComponent(id)}`, {
    method: "PATCH",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(await errorFromRes(res, `PATCH /wizard/resources failed: ${res.status}`));
  return res.json();
}

async function confirmResourceItem(id) {
  const res = await fetch(`/wizard/resources/${encodeURIComponent(id)}/confirm`, {
    method: "POST",
    credentials: "include",
    headers: await authHeaders(),
  });
  if (!res.ok) throw new Error(`POST /wizard/resources/confirm failed: ${res.status}`);
  return res.json();
}

const RESOURCES_API = { load: fetchResources, add: postResourceItem, remove: deleteResourceItem, edit: patchResourceItem, confirm: confirmResourceItem };

async function fetchGaps() {
  const res = await apiGet("/wizard/gaps");   // { missing_details, coverage }
  return res.json();
}

async function fillGapDetails(body) {
  const res = await fetch("/wizard/gaps/details", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`/wizard/gaps/details failed: ${res.status}`);
  return res.json();
}

async function saveGapOpenNotes(body) {
  const res = await fetch("/wizard/gaps/open", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`/wizard/gaps/open failed: ${res.status}`);
  return res.json();
}

const GAPS_API = { load: fetchGaps, fill: fillGapDetails, saveOpen: saveGapOpenNotes };

async function lookupAddress(postcode) {
  const res = await apiGet(`/wizard/address-lookup?postcode=${encodeURIComponent(postcode)}`);
  if (!res.ok) throw new Error(`address lookup failed: ${res.status}`);
  return res.json();   // { valid, town, county, postcode }
}

const ADDRESS_API = { lookup: lookupAddress };

// First-run gate: the personal-details step shows until these core profile fields
// exist. County/town are optional, so they're not required here.
const REQUIRED_PROFILE_KEYS = ["first_name", "surname", "dob", "address_line1", "postcode"];
function needsPersonalDetails(profile) {
  return REQUIRED_PROFILE_KEYS.some(k => !(profile?.[k] || "").trim());
}

async function fetchFilters() {
  const res = await apiGet("/filters");
  return res.json();
}

async function postSender(email) {
  const res = await fetch("/filters/senders", {
    method: "POST",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`POST /filters/senders failed: ${res.status} ${text}`);
  }
  return res.json();
}

async function deleteSender(email) {
  const res = await fetch(`/filters/senders/${encodeURIComponent(email)}`, {
    method: "DELETE",
    credentials: "include",
    headers: await authHeaders(),
  });
  if (!res.ok && res.status !== 204) {
    throw new Error(`DELETE /filters/senders/${email} failed: ${res.status}`);
  }
}

async function putSelectedSender(email) {
  const res = await fetch("/filters/selected", {
    method: "PUT",
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(await authHeaders()) },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) {
    throw new Error(`PUT /filters/selected failed: ${res.status}`);
  }
  return res.json();
}

// ─── Transform — backend already returns the canonical shape, we just group ──
function transformApiData(apiResponse) {
  const orgs = (apiResponse.summaries || []).map(s => ({
    ...s,
    category: s.category || "unknown",
    family: s.family || "unknown",
    accounts: s.accounts || [],
  }));

  const orgs_by_category = {};
  for (const c of CATEGORY_ORDER) orgs_by_category[c] = [];
  for (const o of orgs) (orgs_by_category[o.category] ||= []).push(o);

  const categoryCount = Object.keys(orgs_by_category).filter(k => orgs_by_category[k].length > 0).length;

  return {
    org_count: orgs.length,
    category_count: categoryCount,
    orgs,
    orgs_by_category,
    category_types: apiResponse.category_types || {},
    category_family: apiResponse.category_family || {},
  };
}

// ─── Heuristic — only used in the per-org report modal to colour the
//     priority pills when the backend doesn't supply an explicit priority.
const HIGH_KW   = /(overdue|expir|urgent|verify|suspend|locked|fraud|breach|cancel|close|deadline|outstanding)/i;
const MEDIUM_KW = /(renew|update|change|confirm|review|upcoming|file|rebalance|reconcile|transfer|add )/i;

function actionPriority(text) {
  const s = String(text || "");
  if (HIGH_KW.test(s)) return "HIGH";
  if (MEDIUM_KW.test(s)) return "MEDIUM";
  return "LOW";
}

if (typeof window !== "undefined") window.actionPriority = actionPriority;

// ─── Org-level helpers (over accounts) ───────────────────────────────────────
function orgHasAttention(org) {
  return (org.accounts || []).some(a => (a.actions && a.actions.length > 0) || a.status === "lapsing");
}

function orgPaymentLine(org) {
  for (const a of (org.accounts || [])) {
    const p = a.payment;
    if (!p || !p.amount) continue;
    if (p.cadence === "month")  return `${p.amount}/month`;
    if (p.cadence === "annual") return `${p.amount}/year`;
    if (p.cadence && p.cadence !== "n/a" && p.cadence !== "—") return `${p.amount} (${p.cadence})`;
    return p.amount;
  }
  return null;
}

function orgNextCharge(org) {
  for (const a of (org.accounts || [])) {
    const n = a.payment?.next;
    if (n) return n;
  }
  return null;
}

function orgAllActions(org) {
  const out = [];
  for (const a of (org.accounts || [])) for (const x of (a.actions || [])) out.push({ acct: a, text: x });
  return out;
}

function orgAllDocs(org) {
  const out = [];
  for (const a of (org.accounts || [])) for (const d of (a.docs || [])) out.push({ acct: a, ...d });
  return out;
}

function orgAllEvents(org) {
  const out = [];
  for (const a of (org.accounts || [])) for (const e of (a.events || [])) out.push({ acct: a, ...e });
  out.sort((x, y) => (x.date < y.date ? 1 : -1));
  return out;
}

// ─── Brand ───────────────────────────────────────────────────────────────────
function Wordmark({ size = 28, tone = "ink" }) {
  return (
    <div className="wordmark" style={{ fontSize: size, color: tone === "cream" ? "var(--cream)" : "var(--brand)" }}>
      <span className="wm-glyph" aria-hidden="true">
        <svg viewBox="0 0 24 24" width={size * 0.95} height={size * 0.95} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" strokeLinecap="round">
          <path d="M4 6l8 7 8-7v12H4z" />
        </svg>
      </span>
      <span className="wm-text">myhandover</span>
    </div>
  );
}

// ─── Ambient gradient backdrop (login) ───────────────────────────────────────
function AmbientCanvas({ enabled }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!enabled || !ref.current) return;
    const canvas = ref.current;
    const ctx = canvas.getContext("2d");
    let raf, t = 0;
    function resize() {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = canvas.clientWidth * dpr;
      canvas.height = canvas.clientHeight * dpr;
      ctx.scale(dpr, dpr);
    }
    resize();
    window.addEventListener("resize", resize);
    function tick() {
      t += 0.0035;
      const w = canvas.clientWidth, h = canvas.clientHeight;
      ctx.clearRect(0, 0, w, h);
      const blobs = [
        { x: w * (0.3 + 0.12 * Math.sin(t)),       y: h * (0.35 + 0.1 * Math.cos(t * 0.7)), r: w * 0.55, c: "oklch(0.82 0.06 90 / 0.55)" },
        { x: w * (0.7 + 0.14 * Math.cos(t * 0.5)), y: h * (0.65 + 0.12 * Math.sin(t * 0.6)), r: w * 0.5,  c: "oklch(0.72 0.05 140 / 0.5)" },
        { x: w * (0.55 + 0.1 * Math.sin(t * 1.2)), y: h * (0.2 + 0.1 * Math.cos(t * 0.9)),   r: w * 0.45, c: "oklch(0.88 0.04 60 / 0.55)" },
      ];
      for (const b of blobs) {
        const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
        g.addColorStop(0, b.c);
        g.addColorStop(1, "oklch(0.97 0.008 85 / 0)");
        ctx.fillStyle = g;
        ctx.fillRect(0, 0, w, h);
      }
      raf = requestAnimationFrame(tick);
    }
    tick();
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, [enabled]);
  return <canvas ref={ref} className="ambient-canvas" />;
}

// ─── UI primitives ───────────────────────────────────────────────────────────
function StatusBadge({ source }) {
  // Confirmation state, to match the wizard: AI guesses stay "Unconfirmed" until a
  // human confirms/edits them (source flips to MANUAL); everything else is "Active".
  const unconfirmed = source === "AI_INGESTION";
  const label = unconfirmed ? "Unconfirmed" : "Active";
  return <span className={`status status--${unconfirmed ? "unconfirmed" : "active"}`}>{label}</span>;
}

function ArrowIcon() {
  return <svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden="true">
    <path d="M3.5 10.5L10.5 3.5M10.5 3.5H5M10.5 3.5V9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
  </svg>;
}

function DocIcon() {
  return <svg width="12" height="12" viewBox="0 0 14 14" fill="none" aria-hidden="true">
    <path d="M3 1.5h5L11 4.5v8a0.5 0.5 0 01-.5 0.5h-7a0.5 0.5 0 01-.5-0.5v-11a0.5 0.5 0 01.5-0.5z" stroke="currentColor" strokeWidth="1"/>
    <path d="M8 1.5V4.5h3" stroke="currentColor" strokeWidth="1" strokeLinejoin="round"/>
  </svg>;
}

function CheckTinyIcon() {
  return <svg width="9" height="9" viewBox="0 0 14 14" fill="none" aria-hidden="true">
    <path d="M2.5 7.5l3 3 6-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
  </svg>;
}

// ─── OrgCard helpers ─────────────────────────────────────────────────────────
// Domain for the favicon lookup, derived from whatever URL the org carries.
function orgDomain(org) {
  const url = org.website_url || org.signin || org.website || "";
  if (!url) return "";
  try {
    const u = new URL(url.startsWith("http") ? url : `https://${url}`);
    return u.hostname.replace(/^www\./, "");
  } catch {
    return "";
  }
}

// First 1–2 letters, for the monogram behind a missing favicon.
function orgMonogram(name) {
  const n = (name || "").trim();
  if (!n) return "?";
  const parts = n.split(/\s+/);
  if (parts.length >= 2 && parts[0] && parts[1]) return (parts[0][0] + parts[1][0]).toUpperCase();
  return n.slice(0, 2).toUpperCase();
}

// Map the org's relationship status → status-pill variant + label.
function orgStatusPill(org) {
  const s = org.relationship_status || "unknown";
  if (s === "lapsing" || orgHasAttention(org)) return { variant: "attention", label: "Attention" };
  if (s === "active") return { variant: "active", label: "Active" };
  if (s === "churned") return { variant: "expired", label: "Expired" };
  return { variant: "neutral", label: s.charAt(0).toUpperCase() + s.slice(1) };
}

// One-line payment note for the footer (mirrors the mock's "payment" slot).
function orgPaymentNote(org) {
  const pay = orgPaymentLine(org);
  if (!pay) return "No recurring payment";
  const next = orgNextCharge(org);
  return next ? `${pay} · next ${next}` : pay;
}

// ─── OrgDetailsModal — docs + actions popup, opened from a card ──────────────
function OrgDetailsModal({ org, docs, actions, onClose }) {
  const closeRef = React.useRef(onClose);
  closeRef.current = onClose;

  React.useEffect(() => {
    function onKey(e) { if (e.key === "Escape") closeRef.current && closeRef.current(); }
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, []);

  // Local-only "done" state — not persisted to the backend, matches the old card.
  const [doneActions, setDoneActions] = React.useState({});
  const toggle = (i) => setDoneActions((prev) => ({ ...prev, [i]: !prev[i] }));

  return (
    <div className="rdet__overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="rdet__sheet" role="dialog" aria-modal="true" aria-label={`Details for ${org.organisation_name}`}>
        <div className="rdet__bar">
          <span className="rdet__bar-title">{org.organisation_name}</span>
          <button className="rdet__close" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="rdet__body">
          <section className="rdet__sect">
            <h4 className="rdet__h">Documents{docs.length > 0 && <span className="rdet__n">{docs.length}</span>}</h4>
            {docs.length === 0 ? (
              <p className="rdet__empty">No documents on file.</p>
            ) : (
              <ul className="rdet__docs">
                {docs.map((d, i) => (
                  <li key={i}>
                    <DocIcon />
                    <span className="rdet__doc-name" title={d.name}>{d.name}</span>
                    {d.date && <span className="rdet__doc-date">{d.date}</span>}
                  </li>
                ))}
              </ul>
            )}
          </section>

          <section className="rdet__sect">
            <h4 className="rdet__h">Actions required{actions.length > 0 && <span className="rdet__n">{actions.length}</span>}</h4>
            {actions.length === 0 ? (
              <p className="rdet__empty">No actions required.</p>
            ) : (
              <ul className="rdet__actions">
                {actions.map((a, i) => {
                  const done = !!doneActions[i];
                  return (
                    <li key={i} className={done ? "is-done" : ""}>
                      <label>
                        <input
                          type="checkbox"
                          checked={done}
                          onChange={() => toggle(i)}
                          aria-label={`Mark "${a.text}" as done`}
                        />
                        <span>{a.text}</span>
                      </label>
                    </li>
                  );
                })}
              </ul>
            )}
          </section>
        </div>
      </div>
    </div>
  );
}

// ─── OrgCard — logo-forward card (shared by every category grid) ─────────────
function OrgCard({ org }) {
  const [showDetails, setShowDetails] = React.useState(false);
  const [favOk, setFavOk] = React.useState(true);

  const domain = orgDomain(org);
  const fav = domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=128` : "";
  const pill = orgStatusPill(org);
  const docs = orgAllDocs(org);
  const actions = orgAllActions(org);
  const resCount = org.accounts.length;
  const hasDetails = docs.length > 0 || actions.length > 0;
  const desc = org.relationship_summary
    || `${resCount} ${resCount === 1 ? "resource" : "resources"} held with ${org.organisation_name}.`;

  return (
    <>
      <article className="orgx">
        <div className="orgx__head">
          <div className="orgx__logo">
            <span className="orgx__mono" aria-hidden="true">{orgMonogram(org.organisation_name)}</span>
            {fav && favOk && (
              <img className="orgx__fav" src={fav} alt="" loading="lazy" onError={() => setFavOk(false)} />
            )}
          </div>
          <div className="orgx__name-block">
            <div className="orgx__category">{CATEGORY_LABELS[org.category] || org.category}</div>
            <div className="orgx__org">{org.organisation_name}</div>
          </div>
          <span className={`orgx__pill orgx__pill--${pill.variant}`}>
            <span className="orgx__pill-dot" aria-hidden="true" />{pill.label}
          </span>
        </div>

        <p className="orgx__desc">{desc}</p>

        <div className="orgx__divider" />

        <div className="orgx__foot">
          <span className="orgx__pay">{orgPaymentNote(org)}</span>
          <div className="orgx__foot-r">
            {hasDetails && (
              <button
                className="orgx__details-btn"
                onClick={() => setShowDetails(true)}
                aria-label={`Documents and actions for ${org.organisation_name}`}
              >
                Details
              </button>
            )}
            <span className="orgx__count">{resCount} {resCount === 1 ? "resource" : "resources"}</span>
          </div>
        </div>
      </article>

      {showDetails && (
        <OrgDetailsModal org={org} docs={docs} actions={actions} onClose={() => setShowDetails(false)} />
      )}
    </>
  );
}

// ─── LoginScreen ──────────────────────────────────────────────────────────────
// Two-step landing: marketing hero + "Get Started" first; Clerk's SignIn only
// mounts once the user opts in. After auth, Clerk redirects to "/" and
// checkAuth() takes the signed-in user straight to the dashboard. Connecting an
// inbox (Nylas) is a separate action, handled on the dashboard.
function LoginScreen({ tweaks }) {
  const [showAuth, setShowAuth] = React.useState(false);
  const signInRef = React.useRef(null);

  React.useEffect(() => {
    if (showAuth && signInRef.current && window.__clerk) {
      window.__clerk.mountSignIn(signInRef.current, {
        signInForceRedirectUrl: "/",
        signUpForceRedirectUrl: "/",
      });
      const el = signInRef.current;
      return () => window.__clerk.unmountSignIn(el);
    }
  }, [showAuth]);

  // Auth view: a distinct, centered page — same branding (wordmark + ambient
  // background), Clerk's SignIn in the middle. Not the split landing.
  if (showAuth) {
    return (
      <div className="login-root">
        <AmbientCanvas enabled={tweaks.ambient} />
        <div className="auth-page">
          <header className="login-mark login-mark--center">
            <button className="auth-back" onClick={() => setShowAuth(false)} aria-label="Back">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M15 18l-6-6 6-6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
              Back
            </button>
            <Wordmark size={22} />
          </header>
          <div className="auth-page__center">
            <div className="auth-card auth-card--clerk">
              <div ref={signInRef} />
            </div>
          </div>
        </div>
      </div>
    );
  }

  // Landing: marketing hero on the left, Get Started on the right.
  return (
    <div className="login-root">
      <AmbientCanvas enabled={tweaks.ambient} />
      <div className="login-grid">
        <section className="login-left">
          <header className="login-mark"><Wordmark size={22} /></header>

          <div className="login-hero">
            <p className="eyebrow">A considered handover, for the moments that matter.</p>
            <h1 className="display">
              Every account.<br/>
              Every renewal.<br/>
              <em>One clear record.</em>
            </h1>
            <p className="lede">
              Connect your inbox once. We organise subscriptions, financial accounts and digital
              identities into a single, deterministic handover document — ready to print, share,
              or settle.
            </p>
          </div>

          <footer className="login-foot">
            <span className="dot" /> All data encrypted in transit · GDPR · UK
          </footer>
        </section>

        <section className="login-right">
          <div className="auth-card auth-card--intro">
            <h2 className="auth-intro__title">Your handover, in minutes.</h2>
            <p className="auth-intro__lede">Create an account or sign in to begin.</p>
            <button className="btn-primary btn-primary--lg btn-primary--block" onClick={() => setShowAuth(true)}>
              Get Started
            </button>
            <button className="auth-intro__signin" onClick={() => setShowAuth(true)}>
              Already have an account? Sign in
            </button>
          </div>
        </section>
      </div>
    </div>
  );
}

// ─── Category icons (sidebar) ────────────────────────────────────────────────
const CATEGORY_ICON = {
  physical_assets:      (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M3 17h18M5 17l1.5-5h11L19 17M7 17v2m10-2v2M8 12V8h8v4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  personal_valuables:   (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M12 3l9 7-9 11L3 10l9-7z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>,
  digital_devices:      (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3" y="4" width="18" height="12" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M2 20h20" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
  financial_accounts:   (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M3 10l9-6 9 6M5 10v8h14v-8M9 18v-5M12 18v-5M15 18v-5M3 20h18" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/></svg>,
  investment_business:  (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M4 19h16M5 16l4-5 4 3 6-8" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/></svg>,
  financial_protection: (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>,
  employment_benefits:  (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3" y="7" width="18" height="12" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2M3 12h18" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  digital_accounts:     (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M7 18a4 4 0 01-.5-7.97A5 5 0 0116.9 9.1 4 4 0 0117 18H7z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>,
  government_legal:     (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M6 3h9l3 3v15H6V3z M14 3v4h4M8 12h8M8 16h8" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/></svg>,
  cards_memberships:    (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3" y="6" width="18" height="12" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M3 10h18M6 15h4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
  subscriptions:        (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M20 12a8 8 0 11-3-6.2M20 4v4h-4" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/></svg>,
};

// Per-TYPE icon for the category type-summary tiles. A few type-specific glyphs,
// otherwise reuse the category icons by keyword, with a cube fallback.
const _ICON_ART   = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" strokeWidth="1.6"/><circle cx="8.5" cy="9.5" r="1.4" stroke="currentColor" strokeWidth="1.6"/><path d="M21 15l-5-4L5 20" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>;
const _ICON_WATCH = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="5" stroke="currentColor" strokeWidth="1.6"/><path d="M12 10v2.5l1.5 1M9 4.5L9.5 2h5l.5 2.5M9 19.5l.5 2.5h5l.5-2.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>;
const _ICON_CUBE  = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z M4 7.5l8 4.5 8-4.5 M12 12v9" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>;
const _ICON_HOUSE = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M4 11l8-7 8 7M6 10v10h12V10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/><path d="M10 20v-6h4v6" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>;
const _ICON_TOOL  = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M14.5 6.5a3.5 3.5 0 00-4.6 4.2L4 16.6V20h3.4l5.9-5.9a3.5 3.5 0 004.2-4.6l-2.4 2.4-2-2 2.4-2.4z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/></svg>;
const _ICON_PHONE = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="7" y="2" width="10" height="20" rx="2" stroke="currentColor" strokeWidth="1.6"/><path d="M11 18h2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>;
const _ICON_PLAY  = (s) => <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6"/><path d="M10 8.5l6 3.5-6 3.5v-7z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>;

function typeIcon(name, s = 18) {
  const n = (name || "").toLowerCase();
  if (n.includes("art")) return _ICON_ART(s);
  if (n.includes("watch")) return _ICON_WATCH(s);
  if (n.includes("jewel")) return CATEGORY_ICON.personal_valuables(s);
  // These two must run before the broader account/subscription/share/insurance/car
  // checks below, which are loose enough to shadow several employment_benefits and
  // digital_accounts leaf-type names (e.g. "Death In Service Benefit" contains
  // "benefit", "Gaming Account" contains "account").
  if (/(employer|benefit|rsu|restricted stock|redundancy|notice pay|share option|deferred bonus|death in service|company car|car allowance)/.test(n)) return CATEGORY_ICON.employment_benefits(s);
  if (/(email|cloud|password|vpn|domain|social media|gaming account|software development|other digital)/.test(n)) return CATEGORY_ICON.digital_accounts(s);
  if (/(car|vehicle|van|truck|motor|boat|aircraft)/.test(n)) return CATEGORY_ICON.physical_assets(s);
  if (/(house|land|property)/.test(n)) return _ICON_HOUSE(s);
  if (/(tractor|generator|power tools|workshop|machinery|equipment)/.test(n)) return _ICON_TOOL(s);
  if (/(laptop|computer|phone|tablet|device)/.test(n)) return CATEGORY_ICON.digital_devices(s);
  if (n.includes("card")) return CATEGORY_ICON.cards_memberships(s);
  if (/(account|mortgage|savings|bank|benefit)/.test(n)) return CATEGORY_ICON.financial_accounts(s);
  if (/(insurance|protection)/.test(n)) return CATEGORY_ICON.financial_protection(s);
  if (/(invest|broker|share|crypto)/.test(n)) return CATEGORY_ICON.investment_business(s);
  if (/(passport|licence|license|visa|legal|government)/.test(n)) return CATEGORY_ICON.government_legal(s);
  if (/(subscription|streaming|plan)/.test(n)) return CATEGORY_ICON.subscriptions(s);
  if (/(member|loyalty|club)/.test(n)) return CATEGORY_ICON.cards_memberships(s);
  return _ICON_CUBE(s);
}

// Sub-grids shown on a category's landing page, for the categories whose
// resource types split into genuine, evenly-sized concepts. Categories not
// listed here (e.g. Personal Valuables, only 3 types with no real split)
// fall back to the flat type-tile grid.
const ASSET_GROUPS = [
  { key: "vehicles",  label: "Vehicles",             icon: s => CATEGORY_ICON.physical_assets(s), types: ["Car", "Motorcycle", "Van", "Truck", "Boat", "Aircraft"] },
  { key: "property",  label: "Property",              icon: _ICON_HOUSE, types: ["House", "Land"] },
  { key: "machinery", label: "Machinery / Equipment", icon: _ICON_TOOL,  types: ["Tractor", "Generator", "Power Tools", "Workshop Equipment"] },
];
const CATEGORY_GROUPS = {
  physical_assets: ASSET_GROUPS,
  digital_devices: [
    { key: "mobile",    label: "Mobile",    icon: s => _ICON_PHONE(s), types: ["Smartphone", "Tablet"] },
    { key: "computers", label: "Computers", icon: s => CATEGORY_ICON.digital_devices(s), types: ["Laptop", "Desktop Computer"] },
  ],
  financial_accounts: [
    { key: "banking", label: "Everyday Banking", icon: s => CATEGORY_ICON.financial_accounts(s), types: ["Current Account", "Savings Account"] },
    { key: "credit",  label: "Credit & Lending",  icon: s => CATEGORY_ICON.cards_memberships(s),  types: ["Credit Card Account", "Mortgage Account"] },
  ],
  financial_protection: [
    { key: "personal", label: "Personal Insurance", icon: s => CATEGORY_ICON.financial_protection(s), types: ["Life Insurance Policy", "Health Insurance Policy"] },
    { key: "property", label: "Property & Vehicle",  icon: s => _ICON_HOUSE(s), types: ["Vehicle Insurance Policy", "Home Insurance Policy"] },
  ],
  subscriptions: [
    { key: "entertainment", label: "Entertainment",        icon: s => _ICON_PLAY(s), types: ["Video Streaming Subscription", "Music Streaming Subscription"] },
    { key: "utilities",     label: "Utilities & Services",  icon: s => CATEGORY_ICON.subscriptions(s), types: ["Software Subscription", "Mobile Phone Plan"] },
  ],
};

// ─── Sidebar (Dashboard only) ────────────────────────────────────────────────
function Sidebar({ activeCategory, onSelect, collapsed, onToggleCollapsed, counts, categoryFamily }) {
  const item = (key, label) => {
    const Icon = CATEGORY_ICON[key];
    const active = activeCategory === key;
    const n = counts?.[key] || 0;
    return (
      <button
        key={key}
        className={`sidebar__item ${active ? "is-active" : ""} sidebar__item--${key}`}
        data-family={(categoryFamily || {})[key]}
        onClick={() => onSelect(key)}
        title={collapsed ? label : undefined}>
        <span className="sidebar__icon">{Icon ? Icon(18) : null}</span>
        {!collapsed && <span className="sidebar__label">{label}</span>}
        {!collapsed && n > 0 && <span className="sidebar__count mono">{n}</span>}
      </button>
    );
  };

  return (
    <aside className={`sidebar ${collapsed ? "is-collapsed" : ""}`}>
      <div className="sidebar__head">
        {!collapsed && <span className="sidebar__title">Categories</span>}
        <button className="sidebar__toggle" onClick={onToggleCollapsed} aria-label="Toggle sidebar">
          {collapsed ? "›" : "‹"}
        </button>
      </div>

      <nav className="sidebar__nav">
        <button
          className={`sidebar__item ${activeCategory === null ? "is-active" : ""}`}
          onClick={() => onSelect(null)}
          title={collapsed ? "Summary" : undefined}>
          <span className="sidebar__icon">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
              <rect x="3" y="4" width="7" height="7" rx="1.2" stroke="currentColor" strokeWidth="1.6"/>
              <rect x="14" y="4" width="7" height="7" rx="1.2" stroke="currentColor" strokeWidth="1.6"/>
              <rect x="3" y="13" width="7" height="7" rx="1.2" stroke="currentColor" strokeWidth="1.6"/>
              <rect x="14" y="13" width="7" height="7" rx="1.2" stroke="currentColor" strokeWidth="1.6"/>
            </svg>
          </span>
          {!collapsed && <span className="sidebar__label">Summary</span>}
        </button>

        {CATEGORY_ORDER.map(k => item(k, CATEGORY_LABELS[k]))}
      </nav>
    </aside>
  );
}

// ─── ReportTab ────────────────────────────────────────────────────────────────
function ReportTab({ report, user, onOpenOrgReport }) {
  if (!window.SlimReportDoc) return <div className="page-wrap"><div className="page page--md">Loading…</div></div>;
  return (
    <>
      <div className="report-tab__bar">
        <OrgReportPicker orgs={report.orgs} onPick={onOpenOrgReport} />
        <ShareMenu />
      </div>
      <window.SlimReportDoc report={report} user={user} />
    </>
  );
}

// ─── DashboardView (router) ───────────────────────────────────────────────────
function DashboardView({ report, activeCategory, onOpenOrgReport, onSelectCategory }) {
  if (activeCategory) {
    return <DashboardCategoryView key={activeCategory} report={report} activeCategory={activeCategory}
                                  onOpenOrgReport={onOpenOrgReport} />;
  }
  return <DashboardSummaryView report={report} onOpenOrgReport={onOpenOrgReport} onSelectCategory={onSelectCategory} />;
}

// ─── Actions & Documents panels ──────────────────────────────────────────────
// Category → accent colour for the Actions panel. The spec names three; every
// other category falls back to the neutral "Other" grey.
const ACTION_ACCENTS = {
  subscriptions:      "#8B5CF6",
  financial_accounts: "#2F9E5B",
};
function actionAccent(category) {
  return ACTION_ACCENTS[category] || "#6E6A57";
}

// Actions are stored as plain strings with the start date baked in as
// "… [Initiated 12 Jun 2026]" (see org_summaries/mapper.py). Pull it back out
// so the row can show the date on its own line.
function splitInitiated(text) {
  const s = String(text || "");
  const m = s.match(/\s*\[Initiated ([^\]]+)\]\s*$/);
  if (m) return { title: s.slice(0, m.index).trim(), initiated: m[1].trim() };
  return { title: s.trim(), initiated: null };
}

function buildActionRows(orgs) {
  const out = [];
  for (const o of orgs) {
    for (const a of (o.accounts || [])) {
      for (const t of (a.actions || [])) {
        const { title, initiated } = splitInitiated(t);
        if (title) out.push({ org: o, acct: a, title, initiated });
      }
    }
  }
  return out;
}

// File tile — infer format from the document name's extension. The data has no
// size and no reliable file type, so anything but PDF/DOC gets a neutral tile.
function docFileKind(name) {
  const m = String(name || "").match(/\.([a-z0-9]+)$/i);
  const ext = m ? m[1].toLowerCase() : "";
  if (ext === "pdf") return { label: "PDF", variant: "pdf", fg: "#C0392B" };
  if (ext === "doc" || ext === "docx") return { label: "DOC", variant: "doc", fg: "#2A5CB8" };
  return { label: ext ? ext.toUpperCase().slice(0, 4) : "FILE", variant: "file", fg: "#6E6A57" };
}

function buildDocRows(orgs) {
  const out = [];
  for (const o of orgs) {
    for (const a of (o.accounts || [])) {
      for (const d of (a.docs || [])) {
        out.push({ org: o, acct: a, name: d.name, date: d.date, type: d.kind });
      }
    }
  }
  return out;
}

// Shared panel shell — cream card with a serif title, mono subtitle + count pill.
function Panel({ title, subtitle, count, noun, children }) {
  return (
    <section className="panel">
      <header className="panel__head">
        <div className="panel__titles">
          <h3 className="panel__title">{title}</h3>
          <span className="panel__sub">{subtitle}</span>
        </div>
        <span className="panel__pill">{count} {count === 1 ? noun : noun + "s"}</span>
      </header>
      {children}
    </section>
  );
}

function ActionStatusPill({ source }) {
  // Mirrors StatusBadge: AI guesses stay "Unconfirmed" until a human confirms.
  const unconfirmed = source === "AI_INGESTION";
  return (
    <span className={`apill apill--${unconfirmed ? "unconfirmed" : "active"}`}>
      <span className="apill__dot" />
      {unconfirmed ? "Unconfirmed" : "Active"}
    </span>
  );
}

function ActionsPanel({ rows, onOpenOrgReport }) {
  return (
    <Panel title="Actions" subtitle="outstanding" count={rows.length} noun="action">
      {rows.length === 0 ? (
        <div className="panel__empty">Nothing outstanding right now.</div>
      ) : (
        <ul className="panel__rows">
          {rows.map((r, i) => {
            const accent = actionAccent(r.org.category);
            const href = r.org.website_url || r.org.signin || r.org.website;
            return (
              <li key={i} className="arow">
                <span className="arow__bar" style={{ background: accent }} aria-hidden="true" />
                <div className="arow__main">
                  <div className="arow__crumb">
                    <span className="arow__cat" style={{ color: accent }}>
                      {(CATEGORY_LABELS[r.org.category] || r.org.category).toUpperCase()}
                    </span>
                    <span className="arow__slash">/</span>
                    <span className="arow__dim">{r.org.organisation_name}</span>
                    <span className="arow__slash">/</span>
                    <span className="arow__dim">{r.acct.name}</span>
                  </div>
                  <div className="arow__title">{r.title}</div>
                  {r.initiated && <div className="arow__date">Initiated {r.initiated}</div>}
                </div>
                <ActionStatusPill source={r.acct.source} />
                {href ? (
                  <a className="arow__cta" href={href} target="_blank" rel="noopener"
                     title={`Open ${r.org.organisation_name}`}>
                    Open Account <ArrowIcon />
                  </a>
                ) : (
                  <button className="arow__cta" onClick={() => onOpenOrgReport(r.org)}>
                    Open Account <ArrowIcon />
                  </button>
                )}
              </li>
            );
          })}
        </ul>
      )}
    </Panel>
  );
}

function DocumentsPanel({ rows, onOpenOrgReport }) {
  return (
    <Panel title="Documents" subtitle="recently added" count={rows.length} noun="document">
      {rows.length === 0 ? (
        <div className="panel__empty">No documents yet.</div>
      ) : (
        <ul className="panel__rows">
          {rows.map((r, i) => {
            const fk = docFileKind(r.name);
            const cat = CATEGORY_LABELS[r.org.category] || r.org.category;
            return (
              <li key={i} className="drow">
                <span className={`drow__file drow__file--${fk.variant}`} aria-hidden="true">
                  <span className="drow__file-corner" />
                  <span className="drow__file-kind">{fk.label}</span>
                </span>
                <div className="drow__main">
                  <div className="drow__name" title={r.name}>{r.name}</div>
                  <div className="drow__meta">
                    <span className="drow__cat" style={{ color: fk.fg }}>{cat.toUpperCase()}</span>
                    {r.type && <><span className="drow__dot">·</span><span>{r.type}</span></>}
                    {r.date && <><span className="drow__dot">·</span><span>{r.date}</span></>}
                  </div>
                </div>
                <button className="drow__cta" onClick={() => onOpenOrgReport(r.org)}>
                  Open <ArrowIcon />
                </button>
              </li>
            );
          })}
        </ul>
      )}
    </Panel>
  );
}

// ── Display helpers for LLM-generated text that may be too long for cards ──
function firstSentence(s, fallbackMax = 160) {
  if (!s) return "";
  const m = String(s).match(/^(.{20,}?[.!?])(\s|$)/);
  if (m) return truncateText(m[1], fallbackMax);
  return truncateText(s, fallbackMax);
}

function truncateText(s, max = 100) {
  if (!s) return "";
  const t = String(s);
  if (t.length <= max) return t;
  return t.slice(0, max - 1).trimEnd() + "…";
}

function DashboardSummaryView({ report, onOpenOrgReport, onSelectCategory }) {
  const orgs = report.orgs || [];
  const actionRows = useMemo(() => buildActionRows(orgs), [orgs]);
  const docRows = useMemo(() => buildDocRows(orgs), [orgs]);

  const totals = useMemo(() => {
    let activities = 0, actions = 0, relationships = 0;
    for (const o of orgs) {
      for (const a of (o.accounts || [])) {
        relationships += 1;
        activities += (a.events || []).length;
        actions += (a.actions || []).length;
      }
    }
    return { activities, actions, relationships };
  }, [orgs]);

  const byCat = useMemo(() => {
    const out = {};
    for (const c of CATEGORY_ORDER) {
      const list = report.orgs_by_category[c] || [];
      out[c] = list.reduce((n, o) => n + (o.accounts?.length || 0), 0);
    }
    return out;
  }, [report]);
  const visibleCats = CATEGORY_ORDER.filter(c => (byCat[c] || 0) > 0);

  return (
    <div className="sumv">
      <header className="sumv__hero">
        <p className="eyebrow">Summary · live</p>
        <h1 className="display display--md">Your handover, <em>at a glance</em>.</h1>
        <p className="lede">
          {totals.relationships} resources across {visibleCats.length} categories.
          {" "}{totals.actions} need your attention.
        </p>
      </header>

      <section className="sumv__stats">
        <KpiTile n={orgs.length} label="Organisations" sub="connected" />
        <KpiTile n={totals.relationships} label="Resources" sub="tracked" />
        <KpiTile n={totals.actions} label="Actions" sub="outstanding" />
        <KpiTile n={visibleCats.length} label="Categories" sub="in use" />
      </section>

      <div className="panels">
        <ActionsPanel rows={actionRows} onOpenOrgReport={onOpenOrgReport} />
        <DocumentsPanel rows={docRows} onOpenOrgReport={onOpenOrgReport} />
      </div>

      <section className="sumv__block">
        <header className="sumv__block-h">
          <h3>By category</h3>
          <span className="sumv__block-n">{visibleCats.length} categories</span>
        </header>
        <ul className="sumv__cats">
          {visibleCats.map(c => {
            const fam = (report.orgs_by_category[c][0] || {}).family || "unknown";
            return (
              <li key={c} data-family={fam}>
                <button type="button" className="sumv__cat" onClick={() => onSelectCategory(c)}>
                  <span className="sumv__cat-dot" />
                  <span className="sumv__cat-l">{CATEGORY_LABELS[c]}</span>
                  <span className="sumv__cat-n mono">{byCat[c]}</span>
                  <ArrowIcon />
                </button>
              </li>
            );
          })}
        </ul>
      </section>
    </div>
  );
}

function KpiTile({ n, label, sub }) {
  return (
    <div className="kpi">
      <div className="kpi__n">{n}</div>
      <div className="kpi__lbl">{label}</div>
      {sub && <div className="kpi__sub">{sub}</div>}
    </div>
  );
}

function DashboardCategoryView({ report, activeCategory, onOpenOrgReport }) {
  const list = (report.orgs_by_category && report.orgs_by_category[activeCategory]) || [];
  const [openGroup, setOpenGroup] = React.useState(null);
  const [openType, setOpenType] = React.useState(null);
  const groups = CATEGORY_GROUPS[activeCategory]; // categories with a genuine 2-way type split get a group sub-grid; others fall back to the flat list
  // Tally the resource types held in this category, from each org's accounts.
  const typeCounts = {};
  for (const o of list) for (const a of (o.accounts || [])) {
    const t = a.resource_type || "Other";
    typeCounts[t] = (typeCounts[t] || 0) + 1;
  }
  // Show every type this category can hold, even with 0 records, plus any
  // unexpected types that turned up in the data. Counts overlaid from typeCounts.
  const knownTypes = (report.category_types && report.category_types[activeCategory]) || [];
  const extraTypes = Object.keys(typeCounts).filter(t => !knownTypes.includes(t));
  const typeEntries = [...knownTypes, ...extraTypes].map(t => [t, typeCounts[t] || 0]);
  const totalRecords = typeEntries.reduce((s, [, n]) => s + n, 0);
  const label = CATEGORY_LABELS[activeCategory];
  const group = groups && openGroup ? groups.find(g => g.key === openGroup) : null;

  // Drill-in view: one resource type, showing its cards. Reached by tapping a tile.
  if (openType) {
    const shown = list.filter(o => (o.accounts || []).some(a => (a.resource_type || "Other") === openType));
    const backLabel = group ? group.label : label;
    return (
      <div className="dash">
        <button className="typesum__back" onClick={() => setOpenType(null)}>← All {backLabel.toLowerCase()}</button>
        <div className="typesum__head">
          <h2 className="typesum__title">{openType}</h2>
        </div>
        <div className="card-grid">
          {shown.length === 0 && <div className="card-grid__empty">Nothing here yet.</div>}
          {shown.map(o => (
            <div key={o.resource_id || o.organisation_name}>
              <OrgCard org={o} />
            </div>
          ))}
        </div>
      </div>
    );
  }

  // Grouped category landing: sub-grid of the category's groups.
  if (groups && !openGroup) {
    return (
      <div className="dash">
        <div className="typesum">
          <div className="typesum__head">
            <h2 className="typesum__title">{label}</h2>
            <span className="typesum__meta mono">{groups.length} categories · {totalRecords} record{totalRecords === 1 ? "" : "s"}</span>
          </div>
          <p className="typesum__sub">Pick a category to explore what we found.</p>
          <div className="assetgroup-grid">
            {groups.map(g => {
              const n = g.types.reduce((sum, t) => sum + (typeCounts[t] || 0), 0);
              return (
                <button className="assetgroup-card" key={g.key} onClick={() => setOpenGroup(g.key)}>
                  <div className="assetgroup-card__row">
                    <span className="assetgroup-card__icon">{g.icon(20)}</span>
                    <span className="assetgroup-card__chev">›</span>
                  </div>
                  <span className="assetgroup-card__name">{g.label}</span>
                  <span className="assetgroup-card__meta mono">{g.types.length} type{g.types.length === 1 ? "" : "s"} · {n} record{n === 1 ? "" : "s"}</span>
                </button>
              );
            })}
          </div>
        </div>
      </div>
    );
  }

  // Landing view (or, for grouped categories, a group's type list): tap a type to open its records.
  const shownEntries = group ? typeEntries.filter(([t]) => group.types.includes(t)) : typeEntries;
  const shownTotal = group ? shownEntries.reduce((sum, [, n]) => sum + n, 0) : totalRecords;
  const heading = group ? group.label : label;
  return (
    <div className="dash">
      <div className="typesum">
        {group && <button className="typesum__back" onClick={() => setOpenGroup(null)}>← {label}</button>}
        <div className="typesum__head">
          <h2 className="typesum__title">{heading}</h2>
          <span className="typesum__meta mono">{shownEntries.length} type{shownEntries.length === 1 ? "" : "s"} · {shownTotal} record{shownTotal === 1 ? "" : "s"}</span>
        </div>
        <p className="typesum__sub">Tap a type to see what we found.</p>
        {shownEntries.length === 0 ? (
          <div className="card-grid__empty">Nothing in {heading.toLowerCase()} yet.</div>
        ) : (
          <div className={group ? "typesum__list" : "typesum__grid"}>
            {shownEntries.map(([t, n]) => (
              <button className={"typetile" + (n === 0 ? " typetile--empty" : "")} key={t} onClick={() => setOpenType(t)}>
                <span className="typetile__icon">{typeIcon(t, 18)}</span>
                <span className="typetile__name">{t}</span>
                <span className="typetile__count mono">{n}</span>
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ─── OrgReportPicker ─────────────────────────────────────────────────────────
function OrgReportPicker({ orgs, onPick }) {
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState("");
  const ref = React.useRef(null);

  React.useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const grouped = React.useMemo(() => {
    const byCat = {};
    for (const o of orgs) (byCat[o.category] ||= []).push(o);
    const out = [];
    for (const c of CATEGORY_ORDER) if (byCat[c]) out.push([c, byCat[c]]);
    if (byCat.unknown) out.push(["unknown", byCat.unknown]);
    return out;
  }, [orgs]);

  const needle = q.trim().toLowerCase();
  const matches = (o) => !needle || o.organisation_name.toLowerCase().includes(needle);

  return (
    <div className="orgpicker" ref={ref}>
      <button className="org-report-btn" onClick={() => setOpen(v => !v)}>
        <span className="org-report-btn__dot" />
        Report by organisation
        <svg className="org-report-btn__chev" width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
      </button>
      {open && (
        <div className="orgpicker__menu" role="menu">
          <input
            className="orgpicker__search"
            placeholder="Search organisations…"
            value={q}
            onChange={(e) => setQ(e.target.value)}
            autoFocus
          />
          <div className="orgpicker__list">
            {grouped.map(([cat, list]) => {
              const shown = list.filter(matches);
              if (!shown.length) return null;
              return (
                <div className="orgpicker__group" key={cat}>
                  <div className="orgpicker__group-h">{CATEGORY_LABELS[cat] || cat}</div>
                  {shown.map(o => (
                    <button
                      key={o.resource_id || o.organisation_name}
                      className="orgpicker__item"
                      onClick={() => { setOpen(false); setQ(""); onPick(o); }}>
                      {o.organisation_name}
                    </button>
                  ))}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// ─── ShareMenu ───────────────────────────────────────────────────────────────
function ShareMenu() {
  const [open, setOpen] = React.useState(false);
  const [emailMode, setEmailMode] = React.useState(false);
  const [to, setTo] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState(null); // { ok: bool, text }
  const ref = React.useRef(null);

  React.useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) close(); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  function close() { setOpen(false); setEmailMode(false); setMsg(null); }

  async function handleSend(e) {
    e?.preventDefault();
    const recipient = to.trim();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)) {
      setMsg({ ok: false, text: "Enter a valid email address." });
      return;
    }
    setBusy(true); setMsg(null);
    try {
      await shareReport(recipient);
      setMsg({ ok: true, text: `Sent to ${recipient}.` });
      setTo("");
    } catch (err) {
      setMsg({ ok: false, text: err.message || "Could not send." });
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="sharemenu" ref={ref}>
      <button className="share-btn" onClick={() => (open ? close() : setOpen(true))}>
        <svg className="share-btn__icon" width="15" height="15" viewBox="0 0 24 24" fill="none">
          <path d="M12 15V4M12 4L8 8M12 4l4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
          <path d="M5 14v4a2 2 0 002 2h10a2 2 0 002-2v-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
        Share
      </button>
      {open && (
        <div className="sharemenu__pop" role="menu">
          {!emailMode ? (
            <>
              <button className="sharemenu__action" onClick={() => setEmailMode(true)}>Email a copy</button>
              <div className="sharemenu__divider" />
              <button className="sharemenu__action" onClick={() => { close(); printDoc("print-report"); }}>Download PDF</button>
            </>
          ) : (
            <form className="sharemenu__form" onSubmit={handleSend}>
              <label className="sharemenu__label">Send report to</label>
              <input
                className="sharemenu__input"
                type="email"
                placeholder="name@example.com"
                value={to}
                onChange={(e) => setTo(e.target.value)}
                autoFocus
                disabled={busy}
              />
              <button className="sharemenu__send" type="submit" disabled={busy}>
                {busy ? "Sending…" : "Send"}
              </button>
              {msg && (
                <div className={"sharemenu__msg " + (msg.ok ? "sharemenu__msg--ok" : "sharemenu__msg--err")}>
                  {msg.text}
                </div>
              )}
            </form>
          )}
        </div>
      )}
    </div>
  );
}

// ─── SenderFilter ────────────────────────────────────────────────────────────
function SenderFilter({ filter, onFilterChange }) {
  const [open, setOpen] = React.useState(false);
  const [draft, setDraft] = React.useState("");
  const [err, setErr] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const ref = React.useRef(null);

  React.useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const senders = filter.senders || [];
  const selected = filter.selected || null;
  const pillLabel = selected ? selected : "All senders";

  async function handleAdd(e) {
    e?.preventDefault();
    const email = draft.trim();
    if (!email) return;
    setBusy(true); setErr(null);
    try {
      const next = await postSender(email);
      onFilterChange(next);
      setDraft("");
    } catch (e) {
      setErr(e.message || "Could not add sender");
    } finally {
      setBusy(false);
    }
  }

  async function handleRemove(email) {
    setBusy(true); setErr(null);
    try {
      await deleteSender(email);
      const nextSelected = selected === email ? null : selected;
      if (selected === email) await putSelectedSender(null);
      onFilterChange({ senders: senders.filter(s => s !== email), selected: nextSelected });
    } catch (e) {
      setErr(e.message || "Could not remove sender");
    } finally {
      setBusy(false);
    }
  }

  async function handleSelect(value) {
    const email = value === "__all__" ? null : value;
    setBusy(true); setErr(null);
    try {
      const next = await putSelectedSender(email);
      onFilterChange(next);
    } catch (e) {
      setErr(e.message || "Could not change selection");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="filter-pill-wrap" ref={ref}>
      <button className="filter-pill" onClick={() => setOpen(v => !v)} aria-haspopup="dialog" aria-expanded={open}>
        Filter: <strong>{pillLabel}</strong> ▾
      </button>
      {open && (
        <div className="filter-popover" role="dialog" aria-label="Sender filter">
          <div className="filter-popover__section">
            <label className="filter-popover__lbl">Ingest from</label>
            <select
              className="filter-popover__select"
              value={selected || "__all__"}
              disabled={busy}
              onChange={(e) => handleSelect(e.target.value)}>
              <option value="__all__">All senders</option>
              {senders.map(s => <option key={s} value={s}>{s}</option>)}
            </select>
          </div>

          <div className="filter-popover__section">
            <label className="filter-popover__lbl">Saved senders</label>
            {senders.length === 0 ? (
              <p className="filter-popover__empty">No saved senders yet.</p>
            ) : (
              <ul className="filter-popover__list">
                {senders.map(s => (
                  <li key={s}>
                    <span>{s}</span>
                    <button
                      className="filter-popover__rm"
                      onClick={() => handleRemove(s)}
                      disabled={busy}
                      aria-label={`Remove ${s}`}>×</button>
                  </li>
                ))}
              </ul>
            )}
            <form className="filter-popover__add" onSubmit={handleAdd}>
              <input
                type="email"
                placeholder="sender@example.com"
                value={draft}
                onChange={(e) => setDraft(e.target.value)}
                disabled={busy}
              />
              <button className="app-topbar__btn" type="submit" disabled={busy || !draft.trim()}>Add</button>
            </form>
            <p className="filter-popover__hint">Pick one to limit Sync to that sender, or “All senders” for no filter.</p>
            {err && <p className="filter-popover__err">{err}</p>}
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Nylas Connect (mailbox identity) ────────────────────────────────────────
// @nylas/connect is ESM-only; index.html loads it as a module and sets
// window.NylasConnect. We init a single client lazily and reuse it.
let nylasConnect = null;
let nylasCallbackHandled = false;

function defaultNylasRedirectUri() {
  return `${window.location.origin}/auth/callback`;
}

function hasNylasCallbackParams() {
  const params = new URLSearchParams(window.location.search);
  return window.location.pathname === "/auth/callback" &&
    (params.has("code") || params.has("error"));
}

function waitForNylasConnect() {
  if (window.NylasConnect) return Promise.resolve();
  return new Promise((resolve, reject) => {
    const timeout = window.setTimeout(() => {
      window.removeEventListener("nylasconnect:ready", onReady);
      reject(new Error("Nylas Connect SDK not loaded"));
    }, 10000);
    function onReady() {
      window.clearTimeout(timeout);
      resolve();
    }
    window.addEventListener("nylasconnect:ready", onReady, { once: true });
  });
}

async function getNylasConnect() {
  if (nylasConnect) return nylasConnect;
  await waitForNylasConnect();
  const res = await fetch("/auth/config");
  const { nylas_client_id, nylas_redirect_uri } = await res.json();
  if (!nylas_client_id) throw new Error("Nylas client id not configured");
  nylasConnect = new window.NylasConnect({
    clientId: nylas_client_id,
    redirectUri: nylas_redirect_uri || defaultNylasRedirectUri(),
    apiUrl: "https://api.eu.nylas.com",   // SDK defaults to US; our Nylas app is EU
    // DIAGNOSTIC: idp_claims disabled to test the Google 45002 (grant creation
    // failed) bug. This Clerk JWT is injected during Nylas token exchange; if
    // window.__clerk is absent in the popup/callback context it returns undefined
    // and breaks the exchange. Returning null skips idp_claims entirely.
    // If Google connects with this off, the Clerk token was the cause.
    identityProviderToken: async () => null,
    // Google needs explicit scopes or it won't grant mail access (Microsoft works
    // on the SDK defaults). Matches the Nylas connector's configured scopes.
    defaultScopes: {
      google: [
        "openid",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/gmail.modify",
      ],
    },
  });
  return nylasConnect;
}

async function handleNylasCallbackIfPresent() {
  if (nylasCallbackHandled || !hasNylasCallbackParams()) return false;
  nylasCallbackHandled = true;
  const client = await getNylasConnect();
  await client.callback(window.location.href);
  return true;
}

const InboxIcon = (s) => (
  <svg width={s} height={s} viewBox="0 0 24 24" fill="none">
    <path d="M3 13h5l1.5 3h5L15 13h6M5 5h14l2 8v4a1 1 0 01-1 1H4a1 1 0 01-1-1v-4L5 5z"
          stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/>
  </svg>
);

const _ICON_CHEVRON = (s) => (
  <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <polyline points="9 18 15 12 9 6" />
  </svg>
);

function HomeSection({ eyebrow, sub, emptyText, items, onOpenOrgReport }) {
  return (
    <section className="home-section">
      <p className="eyebrow">{eyebrow}</p>
      <p className="home-section__sub">{sub}</p>
      {items.length === 0 ? (
        <p className="home-section__sub">{emptyText}</p>
      ) : (
        <div className="home-list">
          {items.map((it, i) => (
            <div className="home-item" key={i} onClick={() => onOpenOrgReport(it.org)}>
              <span className="home-item__dot" />
              <div className="home-item__body">
                <p className="eyebrow home-item__label">{it.label}</p>
                <p className="home-item__title">{it.title}</p>
                <p className="home-item__hint">{it.hint}</p>
              </div>
              <span className="home-item__chev">{_ICON_CHEVRON(16)}</span>
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

// Reuses the same action rows + priority heuristic as the Dashboard's Actions
// panel (buildActionRows / actionPriority) — HIGH-worded actions ("overdue",
// "expir…", "urgent"…) surface as Today, everything else as This month.
function HomeTab({ user, report, onOpenOrgReport }) {
  const [now, setNow] = React.useState(new Date());
  React.useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  const timeStr = now.toLocaleTimeString("en-GB", { hour12: false });
  const dateStr = now.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" });

  const actionRows = useMemo(() => buildActionRows(report.orgs || []), [report]);
  const toHomeItem = (r) => ({
    label: CATEGORY_LABELS[r.org.category] || r.org.category,
    title: r.title,
    hint: r.initiated ? `Initiated ${r.initiated}` : r.org.organisation_name,
    org: r.org,
  });
  const todayItems = actionRows.filter(r => actionPriority(r.title) === "HIGH").map(toHomeItem);
  const monthItems = actionRows.filter(r => actionPriority(r.title) !== "HIGH").map(toHomeItem);

  return (
    <div className="home">
      <div className="home__clock mono">{timeStr}</div>
      <div className="home__date">{dateStr}</div>

      <header className="home__head">
        <h1 className="home__title">{_greeting(user.name)}.</h1>
      </header>

      <HomeSection
        eyebrow="Today"
        sub={`${todayItems.length} thing${todayItems.length === 1 ? "" : "s"} need your attention.`}
        emptyText="Nothing urgent right now."
        items={todayItems}
        onOpenOrgReport={onOpenOrgReport} />
      <HomeSection
        eyebrow="This month"
        sub="On your radar — no rush today."
        emptyText="Nothing else on your radar."
        items={monthItems}
        onOpenOrgReport={onOpenOrgReport} />

      <button className="home-cta" onClick={() => alert("Talking to your agent is coming soon.")}>
        <div>
          <div className="home-cta__title">Talk to your agent</div>
          <div className="home-cta__hint">Ask questions, take action on any of these</div>
        </div>
        {_ICON_CHEVRON(18)}
      </button>
    </div>
  );
}

// ─── IngestionTab ─────────────────────────────────────────────────────────────
// All mailbox + ingestion controls live here, segregated from the report.
// Live ingest progress. Scanning = count up (no total yet, Nylas gives none until
// paged); processing = accurate "N of total" bar where total is locked once.
function IngestProgress({ progress }) {
  const { phase, processed, total } = progress || {};
  const isProcessing = phase === "processing" && total > 0;
  if (isProcessing) {
    const pct = Math.min(100, Math.round((processed / total) * 100));
    return (
      <div className="ingest-progress">
        <div className="ingest-progress__label">Found {total.toLocaleString()} emails. Processed {processed.toLocaleString()} of {total.toLocaleString()}.</div>
        <div className="ingest-progress__bar"><div className="ingest-progress__fill" style={{ width: `${pct}%` }} /></div>
      </div>
    );
  }
  return (
    <div className="ingest-progress">
      <div className="ingest-progress__label">Scanning inbox… {(processed || 0).toLocaleString()} emails found</div>
      <div className="ingest-progress__bar ingest-progress__bar--indet"><div className="ingest-progress__fill" /></div>
    </div>
  );
}

function IngestionTab({ filter, onFilterChange, onIngest, ingestState, ingestProgress, user, resourcesApi }) {
  const [accounts, setAccounts] = React.useState(null);   // null = loading
  const [connecting, setConnecting] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [removed, setRemoved] = React.useState(null);     // last removed email, for the toast
  const [aiResources, setAiResources] = React.useState([]);   // AI_INGESTION resources awaiting confirmation
  const [selected, setSelected] = React.useState(() => new Set());   // ids ticked for bulk confirm/remove
  const [editId, setEditId] = React.useState(null);   // ai resource being inline-renamed
  const [editName, setEditName] = React.useState("");
  const [bulkBusy, setBulkBusy] = React.useState(false);

  const refresh = React.useCallback(async () => {
    try { setAccounts(await fetchAccounts()); }
    catch (err) { setError(err.message); setAccounts([]); }
  }, []);
  React.useEffect(() => { refresh(); }, [refresh]);

  const refreshAi = React.useCallback(async () => {
    try {
      const d = await resourcesApi.load();
      setAiResources((d.items || []).filter(it => it.source === "AI_INGESTION"));
    } catch (err) { /* leave list as-is on error */ }
  }, [resourcesApi]);
  React.useEffect(() => { refreshAi(); }, [refreshAi]);

  function toggleOne(id) {
    setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  }
  function toggleAll() {
    setSelected(s => s.size === aiResources.length ? new Set() : new Set(aiResources.map(r => r.id)));
  }
  async function bulkConfirm() {
    setBulkBusy(true);
    try { for (const id of selected) await resourcesApi.confirm(id); setSelected(new Set()); await refreshAi(); }
    catch (err) { setError(err.message); } finally { setBulkBusy(false); }
  }
  async function bulkRemove() {
    if (!window.confirm(`Remove ${selected.size} resource(s)? This can't be undone.`)) return;
    setBulkBusy(true);
    try { for (const id of selected) await resourcesApi.remove(id); setSelected(new Set()); await refreshAi(); }
    catch (err) { setError(err.message); } finally { setBulkBusy(false); }
  }
  function startEditAi(it) { setEditId(it.id); setEditName(it.name || ""); }
  async function saveEditAi(it) {
    setBulkBusy(true);
    try {
      await resourcesApi.edit(it.id, { resource_type_id: it.resource_type_id, name: editName.trim(), fields: it.details || {} });
      setEditId(null); await refreshAi();
    } catch (err) { setError(err.message); } finally { setBulkBusy(false); }
  }

  async function handleConnect() {
    setError(null); setConnecting(true);
    try {
      const client = await getNylasConnect();
      // connect() reuses any cached session and skips the popup, so you can't add
      // a second mailbox (or re-pick after deleting one). logout() clears the
      // cached session first, forcing a fresh account picker. Safe: the SDK's
      // local session isn't used after connect — the grant lives in our DB.
      try { await client.logout(); } catch (e) {}
      const result = await client.connect({ method: "popup" });
      const grantId = result?.grantId;
      const email = result?.grantInfo?.email || result?.email;
      if (!grantId) throw new Error("Connection cancelled");
      await postAccount(grantId, email);
      await refresh();
    } catch (err) {
      console.error("Nylas connect failed:", err);
      setError(err.message || "Could not connect mailbox");
    } finally { setConnecting(false); }
  }

  async function handleRemove(id, email) {
    if (!window.confirm(`Remove ${email || "this mailbox"}? This permanently deletes its synced email data — your handover won't include it again unless you reconnect and re-ingest.`)) return;
    setError(null); setRemoved(null);
    try {
      await deleteAccount(id);
      // No need to clear the SDK session here — handleConnect calls logout()
      // before every connect(), so a reconnect always forces a fresh picker.
      await refresh();
      setRemoved(email);
    }
    catch (err) { setError(err.message || "Could not remove mailbox"); }
  }

  const busy = ingestState === "loading";

  if (accounts === null) {
    return <div className="ingest"><p className="ingest__muted">Loading…</p></div>;
  }

  // Welcome — no mailbox yet
  if (accounts.length === 0) {
    const firstName = (user?.name || "").trim().split(/\s+/)[0] || "";
    return (
      <div className="ingest ingest--welcome">
        <div className="ingest-welcome">
          <p className="ingest__eyebrow">AI Mailbox Ingestion</p>
          <h1 className="ingest-welcome__title">Welcome{firstName ? " " + firstName : ""}.</h1>
          <p className="ingest-welcome__lede">
            Would you like to connect your mailbox? We read it once and turn renewals,
            accounts and subscriptions into one clear handover.
          </p>
          <button className="btn-primary btn-primary--lg" onClick={handleConnect} disabled={connecting}>
            {connecting ? "Connecting…" : "Connect my mailbox"}
          </button>
          {removed && <p className="ingest__ok">Removed mailbox ({removed}).</p>}
          {error && <p className="ingest__err">{error}</p>}
        </div>
      </div>
    );
  }

  // Connected — guided steps
  return (
    <div className="ingest">
      <header className="ingest__head">
        <div>
          <p className="ingest__eyebrow">AI Mailbox Ingestion</p>
          <h2 className="ingest__title">Your mailbox, organised.</h2>
        </div>
        <button className="ingest-resync" onClick={onIngest} disabled={busy}>
          {busy ? "Syncing…" : "↻ Resync my mailbox"}
        </button>
      </header>

      <ol className="ingest-steps">
        <li className="ingest-step">
          <div className="ingest-step__no">1</div>
          <div className="ingest-step__body">
            <h3 className="ingest-step__title">Connected mailboxes</h3>
            <ul className="mailbox-list">
              {accounts.map(a => (
                <li key={a.email_account_id} className="mailbox-row">
                  <span className="mailbox-row__icon">{InboxIcon(18)}</span>
                  <span className="mailbox-row__email">{a.email_address}</span>
                  <span className="mailbox-row__dot" title="Connected" />
                  <button className="mailbox-row__rm" onClick={() => handleRemove(a.email_account_id, a.email_address)}
                    aria-label={`Remove ${a.email_address}`}>Remove</button>
                </li>
              ))}
            </ul>
            <button className="btn-secondary mailbox-connect" onClick={handleConnect} disabled={connecting}>
              {connecting ? "Connecting…" : "Connect another mailbox"}
            </button>
            {removed && <p className="ingest__ok">Removed mailbox ({removed}).</p>}
            {error && <p className="ingest__err">{error}</p>}
          </div>
        </li>

        <li className="ingest-step">
          <div className="ingest-step__no">2</div>
          <div className="ingest-step__body">
            <h3 className="ingest-step__title">Ignore any organisations?</h3>
            <p className="ingest-step__hint">Narrow ingestion to specific senders, or leave it open to all.</p>
            <SenderFilter filter={filter} onFilterChange={onFilterChange} />
          </div>
        </li>

        <li className="ingest-step">
          <div className="ingest-step__no">3</div>
          <div className="ingest-step__body">
            <h3 className="ingest-step__title">Run ingestion</h3>
            <p className="ingest-step__hint">Pull your emails and build the handover.</p>
            <button className="btn-primary" onClick={onIngest} disabled={busy}>
              {busy ? "Ingesting…" : "Ingest"}
            </button>
            {busy && ingestProgress && <IngestProgress progress={ingestProgress} />}
          </div>
        </li>

        <li className="ingest-step">
          <div className="ingest-step__no">4</div>
          <div className="ingest-step__body">
            <h3 className="ingest-step__title">Confirm your AI-discovered resources</h3>
            <p className="ingest-step__hint">Review what the AI found. Tick the ones to confirm or remove, edit a name inline, or describe changes below.</p>
            {aiResources.length === 0 ? (
              <p className="ingest-step__hint">Nothing to confirm yet — run an ingestion above.</p>
            ) : (
              <React.Fragment>
                <label className="estate__selectall">
                  <input type="checkbox" checked={aiResources.length > 0 && selected.size === aiResources.length} onChange={toggleAll} /> Select all
                </label>
                <div className="estate__list">
                  {aiResources.map(it => (
                    <div className="estate__item" key={it.id}>
                      <input type="checkbox" checked={selected.has(it.id)} onChange={() => toggleOne(it.id)} />
                      {editId === it.id
                        ? <input className="wizard__input" value={editName} onChange={e => setEditName(e.target.value)} />
                        : <div className="estate__item-name">{it.name}</div>}
                      {editId === it.id
                        ? <button className="btn-secondary" onClick={() => saveEditAi(it)} disabled={bulkBusy}>Save</button>
                        : <button className="btn-secondary" onClick={() => startEditAi(it)} disabled={bulkBusy}>Edit</button>}
                    </div>
                  ))}
                </div>
                <div className="estate__bulk">
                  <button className="btn-primary" onClick={bulkConfirm} disabled={bulkBusy || selected.size === 0}>Confirm selected ({selected.size})</button>
                  <button className="wizard__remove" onClick={bulkRemove} disabled={bulkBusy || selected.size === 0}>Remove selected ({selected.size})</button>
                </div>
              </React.Fragment>
            )}
          </div>
        </li>
      </ol>
    </div>
  );
}

// ─── Ask AI (global button + popup) ──────────────────────────────────────────
function AskAiButton({ onClick }) {
  return (
    <button className="ask-ai-fab" onClick={onClick} aria-haspopup="dialog">
      <span className="ask-ai-fab__spark" aria-hidden="true">✨</span>
      Ask AI
    </button>
  );
}

function AskAiModal({ onClose }) {
  const [text, setText] = React.useState("");
  const [recording, setRecording] = React.useState(false);
  const [seconds, setSeconds] = React.useState(0);
  const [submitted, setSubmitted] = React.useState(false);

  // recording timer — visual only, no real microphone access (coming-soon shell)
  React.useEffect(() => {
    if (!recording) return;
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(id);
  }, [recording]);

  // Escape closes the popup
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const toggleRec = () => {
    if (recording) { setRecording(false); return; }
    setSeconds(0); setRecording(true);
  };

  const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
  const canSubmit = text.trim().length > 0 || seconds > 0;

  return (
    <div className="modal-scrim ask-ai-scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal ask-ai-modal" role="dialog" aria-modal="true" aria-label="Ask AI">
        <div className="modal__head">
          <div className="modal__title">
            <div className="modal__eyebrow">Ask AI</div>
            <h2>What would you like to change?</h2>
          </div>
          <div className="modal__head-actions">
            <button className="modal__btn modal__btn--close" onClick={onClose} aria-label="Close">×</button>
          </div>
        </div>
        <div className="modal__body">
          {submitted ? (
            <div className="ask-ai-done">
              <div className="ask-ai-done__icon" aria-hidden="true">✓</div>
              <p>Thanks — this is coming soon.</p>
              <button className="btn-primary" onClick={onClose}>Close</button>
            </div>
          ) : (
            <React.Fragment>
              <p className="ask-ai-sub">Speak or type what you’d like to change.</p>
              <div className="ask-ai-mic-row">
                <button
                  className={`ask-ai-mic ${recording ? "is-recording" : ""}`}
                  onClick={toggleRec}
                  aria-pressed={recording}
                  aria-label={recording ? "Stop recording" : "Start recording"}>
                  <span className="ask-ai-mic__icon" aria-hidden="true">{recording ? "■" : "🎙"}</span>
                </button>
                <span className="ask-ai-mic__label">
                  {recording
                    ? `Recording… ${fmt(seconds)}`
                    : seconds > 0
                      ? `Recorded ${fmt(seconds)} — tap to re-record`
                      : "Tap to record"}
                </span>
              </div>
              <textarea
                className="ask-ai-text"
                rows={4}
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="e.g. “Merge my two Barclays accounts”" />
              <div className="ask-ai-actions">
                <button className="btn-secondary" onClick={onClose}>Cancel</button>
                <button className="btn-primary" disabled={!canSubmit} onClick={() => setSubmitted(true)}>Submit</button>
              </div>
            </React.Fragment>
          )}
        </div>
      </div>
    </div>
  );
}

// ─── AuthedShell ──────────────────────────────────────────────────────────────
// Time-of-day greeting for the top bar. Falls back to no name if Clerk has none.
function _greeting(name) {
  const h = new Date().getHours();
  const part = h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
  return name ? `${part}, ${name}` : part;
}

function AuthedShell({ user, report, tab, onTabChange, onLogout, onIngest, ingestState, ingestProgress, filter, onFilterChange, onOpenOrgReport, wizardData, onWizardSave, onResourcesChanged }) {
  const initials = (user.email || "").split("@")[0].split(".").map(s => s[0]).join("").slice(0, 2).toUpperCase();
  const [activeCategory, setActiveCategory] = React.useState(null);
  const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
  const [askAiOpen, setAskAiOpen] = React.useState(false);
  const [mapItems, setMapItems] = React.useState(null);   // /user-resources for the Map tab
  const userBtnRef = React.useRef(null);

  // Lazy-load the resource map data the first time the Map tab is opened.
  React.useEffect(() => {
    if (tab !== "map" || mapItems !== null) return;
    let alive = true;
    fetchUserResources()
      .then((d) => { if (alive) setMapItems(d.user_resources || []); })
      .catch(() => { if (alive) setMapItems([]); });
    return () => { alive = false; };
  }, [tab, mapItems]);
  React.useEffect(() => {
    if (userBtnRef.current && window.__clerk && window.__clerk.isSignedIn) {
      window.__clerk.mountUserButton(userBtnRef.current);
      const el = userBtnRef.current;
      return () => window.__clerk.unmountUserButton(el);
    }
  }, []);

  return (
    <div className="authed">
      {askAiOpen && <AskAiModal onClose={() => setAskAiOpen(false)} />}
      <header className="app-topbar">
        <a href="/" className="app-topbar__brand" aria-label="Go to home"><Wordmark size={22} /></a>

        <nav className="app-topbar__tabs" role="tablist" aria-label="Pages">
          <button
            role="tab"
            aria-selected={tab === "home"}
            className={`app-topbar__tab ${tab === "home" ? "is-active" : ""}`}
            onClick={() => onTabChange("home")}>
            Home
          </button>
          <button
            role="tab"
            aria-selected={tab === "dashboard"}
            className={`app-topbar__tab ${tab === "dashboard" ? "is-active" : ""}`}
            onClick={() => onTabChange("dashboard")}>
            Dashboard
          </button>
          <button
            role="tab"
            aria-selected={tab === "report"}
            className={`app-topbar__tab ${tab === "report" ? "is-active" : ""}`}
            onClick={() => onTabChange("report")}>
            Report
          </button>
          <button
            role="tab"
            aria-selected={tab === "map"}
            className={`app-topbar__tab ${tab === "map" ? "is-active" : ""}`}
            onClick={() => onTabChange("map")}>
            Lifegraph
          </button>
          <button
            role="tab"
            aria-selected={tab === "people"}
            className={`app-topbar__tab ${tab === "people" ? "is-active" : ""}`}
            onClick={() => onTabChange("people")}>
            People
          </button>
          <span className="app-topbar__divider" aria-hidden="true" />
          <button
            role="tab"
            aria-selected={tab === "ingestion"}
            className={`app-topbar__tab ${tab === "ingestion" ? "is-active" : ""}`}
            onClick={() => onTabChange("ingestion")}>
            Auto-Discovery
          </button>
          <button
            role="tab"
            aria-selected={tab === "wizard"}
            className={`app-topbar__tab ${tab === "wizard" ? "is-active" : ""}`}
            onClick={() => onTabChange("wizard")}>
            Manual Entry
          </button>
        </nav>

        <div className="app-topbar__actions">
          <span className="app-topbar__greeting">{_greeting(user.name)}</span>
          <AskAiButton onClick={() => setAskAiOpen(true)} />
          <div ref={userBtnRef} />
        </div>
      </header>

      <main className="app-main">
        {tab === "home" ? (
          <HomeTab user={user} report={report} onOpenOrgReport={onOpenOrgReport} />
        ) : tab === "report" ? (
          <ReportTab report={report} user={user} onOpenOrgReport={onOpenOrgReport} />
        ) : tab === "ingestion" ? (
          <IngestionTab
            filter={filter}
            onFilterChange={onFilterChange}
            onIngest={onIngest}
            ingestState={ingestState}
            ingestProgress={ingestProgress}
            user={user}
            resourcesApi={RESOURCES_API} />
        ) : tab === "wizard" ? (
          wizardData && window.WizardHub
            ? <window.WizardHub data={wizardData} onSubmit={onWizardSave} resourcesApi={RESOURCES_API} gapsApi={GAPS_API} addressApi={ADDRESS_API} onResourcesChanged={onResourcesChanged} />
            : <div className="page-wrap"><div className="page page--md">Loading…</div></div>
        ) : tab === "map" ? (
          (mapItems !== null && window.ResourceMap)
            ? <window.ResourceMap items={mapItems} name={user.name} />
            : <div className="page-wrap"><div className="page page--md">Loading…</div></div>
        ) : tab === "people" ? (
          wizardData && window.PeopleTab
            ? <window.PeopleTab data={wizardData} onSubmit={onWizardSave} />
            : <div className="page-wrap"><div className="page page--md">Loading…</div></div>
        ) : (
          <>
          <div className="dash-layout">
            <Sidebar
              activeCategory={activeCategory}
              onSelect={setActiveCategory}
              collapsed={sidebarCollapsed}
              onToggleCollapsed={() => setSidebarCollapsed(v => !v)}
              counts={Object.fromEntries(Object.entries(report.orgs_by_category || {}).map(([k, v]) => [k, v.length]))}
              categoryFamily={report.category_family} />
            <DashboardView
              report={report}
              activeCategory={activeCategory}
              onOpenOrgReport={onOpenOrgReport}
              onSelectCategory={setActiveCategory} />
          </div>
          </>
        )}
      </main>
    </div>
  );
}

// ─── Tweaks / Palettes ────────────────────────────────────────────────────────
const PALETTES = {
  vivid:   { accent: "#E25A4C", accentInk: "#fafafa", swatch: "#E25A4C", label: "Vivid (default)" },
  citrus:  { accent: "#D97706", accentInk: "#fafafa", swatch: "#D97706", label: "Citrus" },
  electric:{ accent: "#2563EB", accentInk: "#fafafa", swatch: "#2563EB", label: "Electric" },
  moss:    { accent: "#4f5a3d", accentInk: "#fafafa", swatch: "#5a6647", label: "Moss (calm)" },
  ink:     { accent: "#161616", accentInk: "#faf7f2", swatch: "#161616", label: "Ink (monochrome)" },
};

function AppTweaks({ tweaks, setTweak }) {
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection title="Accent">
        <TweakSelect
          value={tweaks.palette}
          onChange={(v) => setTweak("palette", v)}
          options={Object.entries(PALETTES).map(([k, p]) => ({ value: k, label: p.label }))}
        />
      </TweakSection>
      <TweakSection title="Category color-coding">
        <TweakToggle
          label="Family colors"
          value={tweaks.familyColors}
          onChange={(v) => setTweak("familyColors", v)}
        />
      </TweakSection>
      <TweakSection title="Login backdrop">
        <TweakToggle
          label="Ambient gradient"
          value={tweaks.ambient}
          onChange={(v) => setTweak("ambient", v)}
        />
      </TweakSection>
      <TweakSection title="Display typeface">
        <TweakRadio
          value={tweaks.displayFont}
          onChange={(v) => setTweak("displayFont", v)}
          options={[
            { value: "Instrument Serif", label: "Instrument Serif" },
            { value: "Newsreader",       label: "Newsreader" },
            { value: "Geist",            label: "Sans (Geist)" },
          ]}
        />
      </TweakSection>
    </TweaksPanel>
  );
}

// ─── Root App ─────────────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": "vivid",
  "ambient": true,
  "displayFont": "Instrument Serif",
  "familyColors": true
}/*EDITMODE-END*/;

function App() {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // Possible values: "checking" | "login" | "loading" | "ready" | "error"
  const [appState, setAppState] = useState("checking");
  const [tab, setTab] = useState("home");
  const [user, setUser] = useState(null);
  const [report, setReport] = useState(null);
  const [ingestState, setIngestState] = useState(null); // null | "loading" | "done" | "error"
  const [ingestProgress, setIngestProgress] = useState(null); // { phase, processed, total } | null
  const [error, setError] = useState(null);
  const [transitioning, setTransitioning] = useState(false);
  const [filter, setFilter] = useState({ senders: [], selected: null });
  const [orgReport, setOrgReport] = React.useState(null);    // org object or null
  const [wizardData, setWizardData] = useState(null);        // GET /wizard payload
  const [showDetails, setShowDetails] = useState(false);     // blocking personal-details capture, before the wizard
  const [showWizard, setShowWizard] = useState(false);       // first-login modal over the app
  const [showGaps, setShowGaps] = useState(false);           // post-ingest "fill AI gaps" popup
  const [showIngestDone, setShowIngestDone] = useState(false); // post-ingest "all done" popup (when there are no gaps)

  // Apply palette/font tweaks
  useEffect(() => {
    const p = PALETTES[tweaks.palette] || PALETTES.vivid;
    document.documentElement.style.setProperty("--accent", p.accent);
    document.documentElement.style.setProperty("--accent-ink", p.accentInk);
    document.documentElement.style.setProperty("--display-font", `"${tweaks.displayFont}"`);
    document.documentElement.dataset.familyColors = tweaks.familyColors ? "on" : "off";
  }, [tweaks.palette, tweaks.displayFont, tweaks.familyColors]);

  // Re-entrancy guard + current-state ref (listener closure would otherwise see
  // stale `appState` / re-fire checkAuth while one is already in flight).
  const checkingRef = useRef(false);
  const appStateRef = useRef(appState);
  appStateRef.current = appState;

  // On mount: check auth + listen for Clerk auth-state flips.
  // The mounted SignIn component's forceRedirectUrl is a fragile fallback in an
  // SPA; this listener reliably moves us to the dashboard the moment Clerk signs
  // the user in, without a hard page reload.
  useEffect(() => {
    let cancelled = false;
    let unsub = null;
    (async () => {
      try {
        if (await handleNylasCallbackIfPresent()) return;
        if (cancelled) return;
        checkAuth();
        await initClerk();
        if (!clerk) return;
        unsub = clerk.addListener(() => {
          // Only react to a fresh sign-in while we're sitting on the login screen;
          // ignore while checking/loading/ready so we never double-load.
          if (clerk.isSignedIn && appStateRef.current === "login" && !checkingRef.current) {
            checkAuth();
          }
        });
      } catch (err) {
        if (cancelled) return;
        setError(err.message || "Authentication callback failed");
        setAppState("error");
      }
    })();
    return () => {
      cancelled = true;
      if (unsub) unsub();
    };
  }, []);

  async function checkAuth() {
    if (checkingRef.current) return;   // already in flight
    checkingRef.current = true;
    try {
      await initClerk();
      if (clerk && !clerk.isSignedIn) {
        setAppState("login");          // show Clerk sign-in
        return;
      }
      const me = await fetchMe();
      if (!me.person_id) {             // not signed in (status: "not signed in")
        setAppState("login");
        return;
      }
      // Signed in → straight to the dashboard. An inbox connection is optional
      // and handled separately by the "Sync" action (handleIngest), not here.
      // /auth/status has no email; pull it from Clerk for the avatar initials.
      const email = clerk?.user?.primaryEmailAddress?.emailAddress || "";
      // Clerk name is only a fallback. The report's full name + address are the
      // source of truth from the DB-backed wizard profile, loaded just below.
      const cu = clerk?.user;
      const clerkName = cu?.firstName || cu?.fullName || (email ? email.split("@")[0] : "") || "";
      // Load the wizard (profile) first so the user object carries the real full
      // name + address; then pop the one-time wizard modal if not done.
      let profile = {};
      try {
        const w = await fetchWizard();
        setWizardData(w);
        profile = w.profile || {};
        // Personal details come first and block. Only pop the discovery wizard
        // once we already have the core details (otherwise it waits — see the
        // details modal's onSubmit, which chains into the wizard).
        if (needsPersonalDetails(profile)) setShowDetails(true);
        else if (!w.completed) setShowWizard(true);
      } catch (err) {
        console.error("wizard load failed:", err);   // fail loud; don't block the app
      }
      // Profile keys: first_name/surname (name) and address_line1/town/county/postcode (address).
      const profileName = [profile.first_name, profile.surname].filter(Boolean).join(" ");
      const address = [profile.address_line1, profile.town, profile.county, profile.postcode]
        .filter(Boolean).join(", ");
      setUser({ ...me, email, name: profileName || clerkName, address });
      await loadData(me);
      resumeIngestIfRunning();   // fire-and-forget: re-attach to an in-flight ingest from a prior visit
    } catch (err) {
      if (err.status === 401) {
        setAppState("login");
      } else {
        setError(err.message);
        setAppState("error");
      }
    } finally {
      checkingRef.current = false;
    }
  }

  async function loadData(me) {
    setAppState("loading");
    try {
      const [resourcesResp, filterResp] = await Promise.all([
        fetchOrgSummaries(),
        fetchFilters().catch(() => ({ senders: [], selected: null })),
      ]);
      const transformed = transformApiData(resourcesResp);
      setReport(transformed);
      setFilter(filterResp);
      setTab("home");
      setAppState("ready");
    } catch (err) {
      setError(err.message);
      setAppState("error");
    }
  }

  // Reload the dashboard/report data. One source of truth for "the underlying
  // resources changed — pull them again" (ingest finished, gaps filled, manual add).
  async function refreshReport() {
    try { setReport(transformApiData(await fetchOrgSummaries())); } catch (_) {}
  }

  // Ingest finished for real — refresh the dashboard and surface any AI gaps.
  // Shared by the foreground poll and the on-load resume check, so the completion
  // popup only ever fires when the worker is genuinely done.
  async function finishIngest() {
    await refreshReport();
    setIngestProgress(null);
    setIngestState("done");
    setTimeout(() => setIngestState(null), 2500);
    // Always surface a completion popup when the ingest finishes. If the AI left
    // gaps, show the actionable gaps modal; otherwise show a simple "done"
    // confirmation so the user always gets a clear signal it finished.
    try {
      const gaps = await fetchGaps();
      if (gaps.missing_details.length > 0 || gaps.coverage.length > 0) setShowGaps(true);
      else setShowIngestDone(true);
    } catch (_) { setShowIngestDone(true); }   // gaps fetch failed → still confirm completion
  }

  // Poll the worker's job status until it stops running, then act on the result.
  // SAQ reports "complete" when done, or drops the record → "idle"; both mean done.
  // We poll for the full 30-min job lifetime so we never give up early and pop the
  // "done" modal while the worker is still running (the bug with large inboxes).
  async function pollIngestToCompletion() {
    const RUNNING = new Set(["queued", "active", "new", "aborting"]);
    let status = "queued";
    let seenRunning = false;                     // did we ever observe the job actually running?
    let idleStreak = 0;
    for (let i = 0; i < 900; i++) {            // 900 × 2s = 30 min, matches the SAQ job timeout
      await new Promise(r => setTimeout(r, 2000));
      let resp;
      try { resp = await fetchIngestStatus(); status = resp.status; } catch (_) { continue; }
      if (RUNNING.has(status)) {
        seenRunning = true;
        // Surface scanning/processing counts for the UI bar.
        setIngestProgress({ phase: resp.phase, processed: resp.processed, total: resp.total });
        continue;   // job is working — keep waiting
      }
      // A status of "idle" means SAQ has no job record for us. Right after enqueue
      // that just means the job isn't visible yet — NOT that it finished. Only believe
      // an "idle" we never saw run after a short grace period (else a fast-failing job
      // that gets swept looks like instant success → premature popup).
      if (status === "idle" && !seenRunning && ++idleStreak < 5) continue;   // ~10s grace
      break;                                      // complete | failed | no_user | settled idle
    }
    if (status === "failed" || status === "no_user") { setIngestProgress(null); setIngestState("error"); return; }
    if (RUNNING.has(status)) { setIngestProgress(null); setIngestState(null); return; }              // 30-min cap, still running
    if (status === "idle" && !seenRunning) { setIngestProgress(null); setIngestState("error"); return; }  // job vanished without ever running = failed + swept
    await finishIngest();                         // complete, or idle after we saw it run → genuinely done
  }

  async function handleIngest() {
    setIngestState("loading");
    try {
      await triggerIngest();                    // 401/errors throw → caught below
      await pollIngestToCompletion();
    } catch (err) {
      setIngestState("error");
    }
  }

  // On load, re-attach to an ingest that's still running from a previous visit, so
  // the completion popup fires even if the page was reloaded or left mid-ingest. If
  // it already finished, the dashboard load already has fresh data — no popup, to
  // avoid re-popping the gaps modal on every visit.
  async function resumeIngestIfRunning() {
    try {
      const { status } = await fetchIngestStatus();
      if (["queued", "active", "new", "aborting"].includes(status)) {
        setIngestState("loading");
        await pollIngestToCompletion();
      }
    } catch (_) {}
  }

  async function closeGaps() {
    setShowGaps(false);
    // The user may have filled gaps — refresh so newly-completed resources show.
    await refreshReport();
  }

  async function handleTabChange(t) {
    // The Wizard and People tabs both edit saved answers — always open with fresh data.
    if (t === "wizard" || t === "people") {
      try { setWizardData(await fetchWizard()); } catch (_) {}
    }
    setTab(t);
  }

  async function handleWizardSave(answers) {
    const w = await postWizardAnswers(answers);
    setWizardData(w);
  }

  function handleLogout() {
    setTransitioning(true);
    // Sign Clerk out first, then clear the Nylas cookie via /auth/logout
    const go = () => { window.location.href = "/auth/logout"; };
    if (window.__clerk) window.__clerk.signOut().then(go, go);
    else setTimeout(go, 220);
  }

  if (appState === "checking") {
    return (
      <div className="loading-screen">
        <Wordmark size={22} />
        <p>Checking authentication…</p>
      </div>
    );
  }

  if (appState === "error") {
    return (
      <div className="error-screen">
        <Wordmark size={22} />
        <h2>Something went wrong</h2>
        <p>{error}</p>
        <button className="btn-primary" onClick={() => { setAppState("checking"); checkAuth(); }}>Retry</button>
      </div>
    );
  }

  if (appState === "loading") {
    return (
      <div className="loading-screen">
        <Wordmark size={22} />
        <p>Loading your handover…</p>
      </div>
    );
  }

  return (
    <>
      <div className={`shell ${transitioning ? "is-fading" : ""}`}>
        {appState === "login" && <LoginScreen tweaks={tweaks} mode="signin" />}
        {appState === "ready" && (
          <AuthedShell
            user={user}
            report={report}
            tab={tab}
            onTabChange={handleTabChange}
            onLogout={handleLogout}
            onIngest={handleIngest}
            ingestState={ingestState}
            ingestProgress={ingestProgress}
            filter={filter}
            onFilterChange={setFilter}
            onOpenOrgReport={setOrgReport}
            wizardData={wizardData}
            onWizardSave={handleWizardSave}
            onResourcesChanged={refreshReport}
          />
        )}
      </div>
      {showDetails && window.PersonalDetailsSection && wizardData && (
        <div className="wizard-overlay">
          <window.PersonalDetailsSection
            data={wizardData}
            addressApi={ADDRESS_API}
            onboarding={true}
            onSubmit={async (answers) => {
              const w = await postWizardAnswers(answers);
              setWizardData(w);
              setShowDetails(false);
              // Chain straight into the discovery wizard if it's still pending.
              if (!w.completed) setShowWizard(true);
            }}
          />
        </div>
      )}
      {showWizard && window.DiscoveryWizard && wizardData && (
        <div className="wizard-overlay">
          <window.DiscoveryWizard
            data={wizardData}
            onSubmit={handleWizardSave}
            onPickPath={async (path) => {
              setShowWizard(false);
              if (path === "email") setTab("ingestion");
              else await handleTabChange("wizard");
            }}
          />
        </div>
      )}
      {showGaps && window.GapsSection && (
        <div className="wizard-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) closeGaps(); }}>
          <div className="gaps-popup">
            <button className="gaps-popup__close" onClick={closeGaps} aria-label="Close">Skip for now ✕</button>
            <window.GapsSection api={GAPS_API} resourcesApi={RESOURCES_API} onChanged={refreshReport} />
          </div>
        </div>
      )}
      {showIngestDone && (
        <div className="wizard-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) setShowIngestDone(false); }}>
          <div className="gaps-popup">
            <button className="gaps-popup__close" onClick={() => setShowIngestDone(false)} aria-label="Close">Close ✕</button>
            <h2 style={{ marginTop: 0 }}>✓ Your handover is ready</h2>
            <p>We’ve finished reading your mailbox and updated your handover.</p>
            <button className="btn-primary" onClick={() => setShowIngestDone(false)}>View my handover</button>
          </div>
        </div>
      )}
      <AppTweaks tweaks={tweaks} setTweak={setTweak} />
      {orgReport && window.OrgReportModal && (
        <window.OrgReportModal
          org={orgReport}
          user={user}
          onClose={() => setOrgReport(null)} />
      )}
    </>
  );
}

if (typeof window !== "undefined") window.Wordmark = Wordmark;

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