// Perdura — Financial Overview dashboard surface.
//
// Clean, self-contained, full-width grid layout wired to LIVE data only. No
// hardcoded financial numbers: every value comes from `periodTotals`, the
// monthly P&L series (`data.pl` / `data.plHistory`), the cash projection
// (`data.cash`), transaction-derived category breakdowns (`data.txns`), or the
// synthesis edge function (CFO "what matters now" + exceptions inbox). Any data
// source that isn't available renders an honest "Awaiting data" placeholder.
//
// Charts are dependency-free SVG (the house pattern). Reusable
// window.SparklineMini / window.BarRanking primitives are reused where they fit.
//
// Exposes window.FinancialOverviewGrid({ data, profile, periodTotals, setPage,
// setDrillKey, tenantId }).

(function () {
  const R = window.React;
  if (!R) return;
  const h = R.createElement;
  const { useState, useEffect, useMemo, useRef } = R;

  // ── formatting ─────────────────────────────────────────────────────────────
  const fmtUSD = (n, o) => (window.fmtUSD ? window.fmtUSD(n, o || { compact: true }) : ("$" + Math.round(Number(n) || 0).toLocaleString()));
  const isNum = (v) => v != null && Number.isFinite(Number(v));
  const fmtPct = (v) => (isNum(v) ? (v * 100).toFixed(1) + "%" : "—");
  const fmtDays = (v) => (isNum(v) ? Math.round(v) + "d" : "—");
  const cap = (s) => (s ? String(s).replace(/[_-]+/g, " ").replace(/\b\w/g, (m) => m.toUpperCase()) : s);

  // tickEvery — show roughly one x-axis label per month (FIX 1). For a 12-point
  // series this is 1 (all shown); for a 90-day series it thins to ~8.
  const tickEvery = (n) => Math.max(1, Math.ceil(n / 12));

  // ── transaction bucketing (mirrors src/data-live.js bucketForCategory) ───────
  const LEGACY_BUCKET = {
    Revenue: "revenue", COGS: "cogs", Opex: "opex", AR: "ar", AP: "ap", Cash: "cash",
    "Contra-revenue": "revenue", "Interest Expense": "opex", "Interest Income": "revenue",
    "Tax Expense": "opex", "Other Income/Expense": "opex",
  };
  const SECTION_BUCKET = {
    revenue: "revenue", contra_revenue: "revenue", other_income: "revenue",
    cogs: "cogs",
    opex: "opex", da: "opex", other_op_exp: "opex", interest: "opex", tax: "opex", other_expense: "opex",
  };
  function bucketOf(category, profile) {
    if (!category) return null;
    if (LEGACY_BUCKET[category]) return LEGACY_BUCKET[category];
    const tax = window.PerduraTaxonomy;
    if (!tax || !tax.sectionForCategory) return null;
    let sec = null;
    try { sec = tax.sectionForCategory(category, profile); } catch (e) { sec = null; }
    if (!sec) return null;
    if (sec === "current_assets") {
      if (category === "Cash & Bank" || category === "Cash") return "cash";
      if (category === "Accounts Receivable") return "ar";
      return null;
    }
    if (sec === "current_liab") {
      if (category === "Accounts Payable" || category === "Credit Cards") return "ap";
      return null;
    }
    if (sec === "non_current_assets" || sec === "long_term_liab" || sec === "equity" || sec === "other") return null;
    return SECTION_BUCKET[sec] || null;
  }

  const INCOME_COLORS = ["#1C4ED8", "#059669", "#F59E0B", "#7C3AED", "#0891B2", "#DB2777", "#94A3B8"];
  const EXPENSE_COLORS = ["#0891B2", "#1C4ED8", "#7C3AED", "#F59E0B", "#DC2626", "#10B981", "#94A3B8"];
  const OTHER_COLOR = "#CBD5E1";
  const C_UP = "#059669", C_DOWN = "#DC2626", C_FLAT = "#6B7A99", C_WARN = "#D97706";

  // ── profile-driven KPI labels + terminology ─────────────────────────────────
  function getKpiConfig(archetype, industry, selectedKpis) {
    const configs = {
      retail:       { income: "Total Revenue",            expenditure: "Total COGS + Opex", surplus: "Net Profit",       margin: "Net Margin %",     row2: ["Gross Margin %", "Inventory Turns", "AOV", "YTD Cash Flow"] },
      nonprofit:    { income: "Total Donations & Income",  expenditure: "Total Expenditure", surplus: "Operating Surplus", margin: "Surplus Margin %", row2: ["Days of Cash", "General Donations %", "Opex Ratio", "YTD Net Cash Flow"] },
      saas:         { income: "Total Revenue",            expenditure: "Total Opex",        surplus: "Net Income",       margin: "Net Margin %",     row2: ["MRR", "Gross Margin %", "Burn Rate", "Runway"] },
      services:     { income: "Total Revenue",            expenditure: "Total Costs",       surplus: "Net Profit",       margin: "Profit Margin %",  row2: ["Utilisation %", "Gross Margin %", "Avg Project Margin", "YTD Cash Flow"] },
      distribution: { income: "Total Revenue",            expenditure: "Total COGS + Opex", surplus: "Gross Profit",     margin: "Gross Margin %",   row2: ["Inventory Turns", "DPO", "DSO", "Cash Conversion Cycle"] },
      general:      { income: "Total Income",             expenditure: "Total Expenditure", surplus: "Net Surplus",      margin: "Net Margin %",     row2: ["Days of Cash", "Gross Margin %", "Opex Ratio", "YTD Cash Flow"] },
    };
    const base = configs[archetype] || configs.general;
    // When the user customised their KPIs in the Company Profile Wizard, those
    // selections override the archetype's default secondary KPI row. The primary
    // income/expenditure/surplus/margin tiles are structural and stay as-is; the
    // wizard list drives `row2`. KPIs already shown as a primary tile are dropped
    // so we don't duplicate them, and the row is capped at 4.
    if (Array.isArray(selectedKpis) && selectedKpis.length) {
      const primary = new Set(["Revenue", "Total Revenue", "Total Donations",
        "Total Donations & Income", "Net Profit", "Net Income", "Operating Surplus", "Gross Profit"]);
      const row2 = selectedKpis.filter((k) => !primary.has(k)).slice(0, 4);
      if (row2.length) return Object.assign({}, base, { row2 });
    }
    return base;
  }

  // Plain-language vocabulary so prose (drill panels, "What this means") never
  // uses nonprofit-specific words unless the archetype is actually a nonprofit.
  // Prose vocabulary is sourced from the declarative KPI catalog (no archetype
  // branching here). Falls back to the for-profit default if the catalog isn't
  // loaded yet.
  function getVocab(archetype) {
    const KC = window.KPICatalog;
    if (KC && KC.getVocabulary) return KC.getVocabulary(archetype);
    return {
      incomeShort: "revenue", expenditure: "costs", surplusShort: "profit",
      reserves: "retained earnings", activity: "operations", sources: "revenue sources",
      titleIncome: "Revenue", titleExpenditure: "Costs", titleSurplus: "Profit",
      titleSources: "Revenue Sources", funding: "revenue",
    };
  }

  // ── responsive width hook for SVG charts ─────────────────────────────────────
  function useWidth(initial) {
    const ref = useRef(null);
    const [w, setW] = useState(initial || 520);
    useEffect(() => {
      if (!ref.current || typeof ResizeObserver === "undefined") return;
      const ro = new ResizeObserver((entries) => { for (const e of entries) setW(Math.max(180, e.contentRect.width)); });
      ro.observe(ref.current);
      return () => ro.disconnect();
    }, []);
    return [ref, w];
  }

  // ── small UI atoms ───────────────────────────────────────────────────────────
  function Awaiting({ label, height }) {
    return h("div", {
      style: {
        height: height || 120, display: "flex", alignItems: "center", justifyContent: "center",
        textAlign: "center", color: "var(--text-3,#94a3b8)", fontSize: 12, lineHeight: 1.5,
        border: "1px dashed var(--border,#e4e8f0)", borderRadius: 8, padding: "10px 14px",
      },
    }, label || "Awaiting data");
  }

  const SEV = {
    high: { bg: "#FEE2E2", fg: "#B91C1C", dot: "#DC2626", label: "High" },
    medium: { bg: "#FEF3C7", fg: "#B45309", dot: "#F59E0B", label: "Medium" },
    low: { bg: "#DCFCE7", fg: "#15803D", dot: "#16A34A", label: "Low" },
    info: { bg: "#E0F2FE", fg: "#0369A1", dot: "#0EA5E9", label: "Info" },
  };
  const sevOf = (s) => SEV[(s || "info").toLowerCase()] || SEV.info;
  function SeverityBadge({ severity }) {
    const s = sevOf(severity);
    return h("span", { style: { fontSize: 10, fontWeight: 700, color: s.fg, background: s.bg, borderRadius: 999, padding: "2px 8px", whiteSpace: "nowrap", textTransform: "uppercase", letterSpacing: 0.4 } }, s.label);
  }

  // Month-over-month delta chip from a series (arrow + colour). FIX 6.
  function momDelta(series, opts) {
    const v = (series || []).filter(isNum);
    if (v.length < 2) return null;
    const cur = v[v.length - 1], prev = v[v.length - 2];
    const pp = !!(opts && opts.pp);
    if (cur === prev) return { text: "→ 0.0" + (pp ? "pp" : "%") + " vs last mo", color: C_FLAT };
    const up = cur - prev >= 0;
    const mag = pp ? Math.abs((cur - prev) * 100).toFixed(1) + "pp" : (prev !== 0 ? Math.abs((cur - prev) / Math.abs(prev) * 100).toFixed(1) : "0.0") + "%";
    return { text: (up ? "↑ " : "↓ ") + mag + " vs last mo", color: up ? C_UP : C_DOWN };
  }

  // ── FIX 4 — "What this means" plain-language block ───────────────────────────
  function WhatThisMeans({ whatItIs, whatsHappening, whyItMatters, whatToConsider }) {
    const line = (label, txt) => (txt ? h("div", { style: { fontSize: 11.5, color: "#334155", lineHeight: 1.5, marginTop: 4 } }, h("strong", { style: { color: "#1C4ED8" } }, label + ": "), txt) : null);
    return h("div", { className: "what-means" },
      h("div", { style: { fontSize: 10.5, fontWeight: 800, color: "#1C4ED8", textTransform: "uppercase", letterSpacing: 0.4, marginBottom: 2 } }, "What this means"),
      line("What it is", whatItIs),
      line("What's happening", whatsHappening),
      line("Why it matters", whyItMatters),
      line("What to consider", whatToConsider));
  }

  // ── detail-panel building blocks (FIX 3) ─────────────────────────────────────
  function DetailTable({ rows, fmt }) {
    const f = fmt || fmtUSD;
    const total = rows.reduce((s, r) => s + (Math.abs(Number(r.value)) || 0), 0) || 1;
    return h("table", { className: "detail-table" },
      h("thead", null, h("tr", null, h("th", null, "Item"), h("th", { style: { textAlign: "right" } }, "Amount"), h("th", { style: { textAlign: "right" } }, "% of total"))),
      h("tbody", null, rows.map((r, i) => h("tr", { key: i },
        h("td", null, r.label),
        h("td", { style: { textAlign: "right", fontVariantNumeric: "tabular-nums" } }, isNum(r.value) ? f(r.value) : "—"),
        h("td", { style: { textAlign: "right", color: "#64748b" } }, isNum(r.value) ? ((Math.abs(r.value) / total) * 100).toFixed(1) + "%" : "—")))));
  }

  function VarianceBars({ label, current, prior, fmt }) {
    const f = fmt || fmtUSD;
    const max = Math.max(Math.abs(Number(current) || 0), Math.abs(Number(prior) || 0), 1);
    const bar = (v, color, lbl) => h("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 4, flex: 1 } },
      h("div", { style: { height: 64, display: "flex", alignItems: "flex-end", width: "100%", justifyContent: "center" } },
        h("div", { style: { width: "56%", height: Math.max(4, (Math.abs(Number(v) || 0) / max) * 64), background: color, borderRadius: "4px 4px 0 0" } })),
      h("div", { style: { fontSize: 11.5, fontWeight: 700, color: "#0f172a" } }, isNum(v) ? f(v) : "—"),
      h("div", { style: { fontSize: 10, color: "#94a3b8" } }, lbl));
    const d = (isNum(current) && isNum(prior)) ? current - prior : null;
    return h("div", null,
      label ? h("div", { style: { fontSize: 10.5, color: "#64748b", marginBottom: 8, fontWeight: 600 } }, label) : null,
      h("div", { style: { display: "flex", gap: 16, alignItems: "flex-end" } }, bar(prior, "#CBD5E1", "Prior"), bar(current, "#1C4ED8", "Current")),
      isNum(d) ? h("div", { style: { fontSize: 11, marginTop: 8, fontWeight: 700, color: d >= 0 ? C_UP : C_DOWN } }, (d >= 0 ? "▲ " : "▼ ") + f(Math.abs(d)) + " vs prior") : null);
  }

  function DetailPanel({ detail }) {
    if (!detail) return null;
    return h("div", { className: "detail-panel" },
      (detail.tableRows && detail.tableRows.length) ? h(DetailTable, { rows: detail.tableRows, fmt: detail.tableFmt }) : null,
      detail.explain ? h(WhatThisMeans, detail.explain) : null,
      detail.variance ? h("div", { style: { marginTop: 12 } }, h(VarianceBars, detail.variance)) : null);
  }

  // ── card with optional expandable drill detail (FIX 3) ───────────────────────
  function Card({ title, subtitle, className, detail, children, eye }) {
    const [open, setOpen] = useState(false);
    return h("div", { className: "dash-card dash-reveal" + (className ? " " + className : "") },
      h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 10, marginBottom: 12 } },
        h("div", { style: { minWidth: 0 } },
          eye ? h("div", { className: "section-eye" }, eye) : null,
          h("div", { className: eye ? "section-title" : null, style: eye ? null : { fontSize: 13.5, fontWeight: 700, color: "var(--text,#0f172a)", letterSpacing: "-0.1px" } }, title),
          subtitle ? h("div", { style: { fontSize: 11, color: "var(--text-3,#64748b)", marginTop: 2 } }, subtitle) : null),
        detail ? h("button", { className: "see-detail", onClick: () => setOpen((v) => !v) }, open ? "Hide detail" : "See detail →") : null),
      children,
      open ? h(DetailPanel, { detail }) : null);
  }

  // Mini sparkline (last 12 points, 50×24) for KPI tiles — spec dashboard row.
  function miniSpark(vals) {
    const v = (vals || []).map(Number).filter(Number.isFinite).slice(-12);
    if (v.length < 2) return null;
    const W = 50, H = 24, min = Math.min.apply(null, v), max = Math.max.apply(null, v), rng = (max - min) || 1, step = W / (v.length - 1);
    const d = v.map((x, i) => (i ? "L" : "M") + (i * step).toFixed(1) + "," + (H - ((x - min) / rng) * (H - 2) - 1).toFixed(1)).join(" ");
    const up = v[v.length - 1] >= v[0];
    return h("svg", { width: W, height: H, style: { display: "block", flexShrink: 0 } }, h("path", { d: d, fill: "none", stroke: up ? "#16A34A" : "#DC2626", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }));
  }
  const STATUS_DOT = { green: "#16A34A", amber: "#F59E0B", red: "#DC2626" };
  // Status from a momDelta result; `inverse` flips it for cost-like metrics.
  function tileStatus(delta, inverse) {
    if (!delta) return null;
    const up = delta.color === C_UP, dn = delta.color === C_DOWN;
    if (inverse) return up ? "red" : dn ? "green" : "amber";
    return up ? "green" : dn ? "red" : "amber";
  }

  // ── KPI tile — coloured, clickable, expandable drill (FIX 6 + FIX 3) ─────────
  // `spark` (12-pt series) + `status` (green|amber|red) + `delta2` (second badge)
  // are optional and additive — existing callers are unaffected.
  function KpiTile({ label, value, valueColor, borderColor, sub, delta, delta2, spark, status, awaiting, detail, accentKind }) {
    const [open, setOpen] = useState(false);
    const clickable = !!detail && !awaiting;
    const badgeClass = (d) => ("delta-badge " + (d.color === C_UP ? "delta-up" : d.color === C_DOWN ? "delta-dn" : d.color === C_WARN ? "delta-warn" : "delta-flat"));
    const dot = (!awaiting && status && STATUS_DOT[status]) ? h("span", { style: { width: 8, height: 8, borderRadius: "50%", background: STATUS_DOT[status], flexShrink: 0 } }) : null;
    return h("div", {
      className: "dash-card dash-reveal kpi-tile" + (accentKind ? " kpi-" + accentKind : "") + (clickable ? " clickable" : ""),
      onClick: clickable ? () => setOpen((v) => !v) : undefined,
      style: { borderBottom: "3px solid " + (awaiting ? "#E4E8F0" : (borderColor || "#1C4ED8")) },
    },
      h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
        dot,
        h("div", { style: { fontSize: 10.5, textTransform: "uppercase", letterSpacing: 0.5, color: "var(--text-3,#64748b)", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, label)),
      h("div", { style: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 8, marginTop: 4 } },
        h("div", { style: { fontSize: 24, lineHeight: 1.05, fontWeight: 800, letterSpacing: "-0.6px", color: awaiting ? "var(--text-3,#94a3b8)" : (valueColor || "var(--text,#0f172a)") } }, awaiting ? "—" : h(AnimatedNumber, { text: value })),
        (!awaiting && spark) ? miniSpark(spark) : null),
      h("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 4, flexWrap: "wrap" } },
        (!awaiting && delta) ? h("span", { className: badgeClass(delta) }, delta.text) : null,
        (!awaiting && delta2) ? h("span", { className: badgeClass(delta2) }, delta2.text) : null,
        h("span", { style: { fontSize: 11, color: awaiting ? "var(--text-3,#94a3b8)" : "var(--text-2,#475569)" } }, awaiting ? "Awaiting data" : (sub || ""))),
      clickable ? h("div", { style: { fontSize: 10, color: "#1C4ED8", marginTop: 6, fontWeight: 500 } }, open ? "Hide detail" : "See detail →") : null,
      (open && detail) ? h(DetailPanel, { detail }) : null);
  }

  function Legend({ items }) {
    return h("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginTop: 8, paddingLeft: 4, fontSize: 11, color: "var(--text-2,#475569)" } },
      items.map((it, i) => h("span", { key: i, style: { display: "inline-flex", alignItems: "center", gap: 6 } },
        h("span", { style: { width: it.line ? 14 : 10, height: it.line ? 2 : 10, borderRadius: it.line ? 1 : 2, background: it.color, display: "inline-block" } }), it.label)));
  }

  // ── chart: combo bars (income vs expenditure) + net surplus line ─────────────
  function ComboBarLine({ labels, income, expense, net, incomeLabel, expenseLabel, netLabel }) {
    const iL = incomeLabel || "Income", eL = expenseLabel || "Expenditure", nL = netLabel || "Net surplus";
    const [ref, w] = useWidth(560);
    const H = 240, padL = 54, padR = 14, padT = 16, padB = 46;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const n = labels.length || 1;
    const posNet = net.map((v) => (v > 0 ? v : 0));
    const maxV = Math.max(1, ...income, ...expense, ...posNet);
    const minV = Math.min(0, ...net);
    const range = (maxV - minV) || 1;
    const yFor = (v) => padT + innerH - ((v - minV) / range) * innerH;
    const groupW = innerW / n, barW = Math.max(3, groupW * 0.30);
    const cx = (i) => padL + groupW * i + groupW / 2;
    const netPath = net.map((v, i) => (i === 0 ? "M" : "L") + cx(i).toFixed(1) + "," + yFor(v).toFixed(1)).join(" ");
    const ticks = minV < 0 ? [maxV, 0, minV] : [maxV, maxV / 2, 0];
    const te = tickEvery(n);
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        ticks.map((tv, i) => h("g", { key: "t" + i },
          h("line", { x1: padL, x2: w - padR, y1: yFor(tv), y2: yFor(tv), stroke: "var(--grid,#eef2f7)", strokeDasharray: tv === 0 ? "0" : "2 4" }),
          h("text", { x: padL - 8, y: yFor(tv) + 3, textAnchor: "end", fontSize: 9.5, fill: "var(--text-3,#94a3b8)", fontFamily: "ui-monospace, monospace" }, fmtUSD(tv)))),
        labels.map((lb, i) => {
          const xi = cx(i);
          return h("g", { key: "b" + i },
            h("rect", { x: xi - barW - 1, y: yFor(income[i]), width: barW, height: Math.abs(yFor(income[i]) - yFor(0)), rx: 2, fill: "#1C4ED8" }, h("title", null, lb + " · " + iL + " " + fmtUSD(income[i]))),
            h("rect", { x: xi + 1, y: yFor(expense[i]), width: barW, height: Math.abs(yFor(expense[i]) - yFor(0)), rx: 2, fill: "#94A3B8" }, h("title", null, lb + " · " + eL + " " + fmtUSD(expense[i]))),
            (i % te === 0) ? h("text", { x: xi, y: H - 14, textAnchor: "middle", fontSize: 9.5, fill: "var(--text-3,#94a3b8)" }, lb) : null);
        }),
        h("path", { d: netPath, fill: "none", stroke: "#059669", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }),
        net.map((v, i) => h("circle", { key: "n" + i, cx: cx(i), cy: yFor(v), r: 2.4, fill: "#059669" }, h("title", null, labels[i] + " · " + nL + " " + fmtUSD(v))))),
      h(Legend, { items: [{ label: iL, color: "#1C4ED8" }, { label: eL, color: "#94A3B8" }, { label: nL, color: "#059669", line: true }] }));
  }

  // ── chart: stacked area (income by stream) ───────────────────────────────────
  function StackedArea({ labels, streams }) {
    const [ref, w] = useWidth(420);
    const H = 240, padL = 50, padR = 12, padT = 16, padB = 44;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const n = labels.length || 1;
    const totals = labels.map((_, i) => streams.reduce((s, st) => s + (st.values[i] || 0), 0));
    const maxV = Math.max(1, ...totals);
    const xFor = (i) => padL + (n > 1 ? (innerW / (n - 1)) * i : innerW / 2);
    const yFor = (v) => padT + innerH - (v / maxV) * innerH;
    const cum = labels.map(() => 0);
    const bands = streams.map((st) => {
      const lower = labels.map((_, i) => cum[i]);
      labels.forEach((_, i) => { cum[i] += st.values[i] || 0; });
      const upper = labels.map((_, i) => cum[i]);
      const top = upper.map((v, i) => xFor(i).toFixed(1) + "," + yFor(v).toFixed(1));
      const bot = lower.map((v, i) => xFor(i).toFixed(1) + "," + yFor(v).toFixed(1)).reverse();
      return { d: "M" + top.join(" L") + " L" + bot.join(" L") + " Z", color: st.color };
    });
    const ticks = [maxV, maxV / 2, 0];
    const te = tickEvery(n);
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        ticks.map((tv, i) => h("g", { key: "t" + i },
          h("line", { x1: padL, x2: w - padR, y1: yFor(tv), y2: yFor(tv), stroke: "var(--grid,#eef2f7)", strokeDasharray: "2 4" }),
          h("text", { x: padL - 8, y: yFor(tv) + 3, textAnchor: "end", fontSize: 9.5, fill: "var(--text-3,#94a3b8)", fontFamily: "ui-monospace, monospace" }, fmtUSD(tv)))),
        bands.map((b, i) => h("path", { key: "band" + i, d: b.d, fill: b.color, fillOpacity: 0.82, stroke: b.color, strokeWidth: 0.5 })),
        labels.map((lb, i) => (i % te === 0) ? h("text", { key: "x" + i, x: xFor(i), y: H - 14, textAnchor: "middle", fontSize: 9.5, fill: "var(--text-3,#94a3b8)" }, lb) : null)),
      h(Legend, { items: streams.map((s) => ({ label: s.name, color: s.color })) }));
  }

  // ── chart: donut + legend ────────────────────────────────────────────────────
  function Donut({ items }) {
    const total = items.reduce((s, it) => s + (it.value || 0), 0) || 1;
    let acc = 0;
    const stops = items.map((it) => { const a = (acc / total) * 100; acc += it.value || 0; const b = (acc / total) * 100; return it.color + " " + a.toFixed(2) + "% " + b.toFixed(2) + "%"; });
    return h("div", { style: { display: "grid", gridTemplateColumns: "92px 1fr", gap: 14, alignItems: "center" } },
      h("div", { style: { width: 92, height: 92, borderRadius: "50%", background: "conic-gradient(" + stops.join(",") + ")", position: "relative", flexShrink: 0 } },
        h("div", { style: { position: "absolute", inset: 13, borderRadius: "50%", background: "#fff", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", boxShadow: "inset 0 0 0 1px var(--border,#e4e8f0)" } },
          h("span", { style: { fontSize: 8.5, color: "var(--text-3,#94a3b8)", textTransform: "uppercase", letterSpacing: 0.5 } }, "Total"),
          h("strong", { style: { fontSize: 12, color: "var(--text,#0f172a)" } }, fmtUSD(total)))),
      h("div", { style: { display: "flex", flexDirection: "column", gap: 6, minWidth: 0 } },
        items.map((it, i) => h("div", { key: i, style: { display: "flex", alignItems: "center", gap: 8, fontSize: 11.5, color: "var(--text-2,#475569)" } },
          h("span", { style: { width: 9, height: 9, borderRadius: 2, background: it.color, flexShrink: 0 } }),
          h("span", { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.label),
          h("strong", { style: { color: "var(--text,#0f172a)" } }, fmtUSD(it.value)),
          h("span", { style: { color: "var(--text-3,#94a3b8)", width: 44, textAlign: "right" } }, ((it.value / total) * 100).toFixed(1) + "%")))));
  }

  // ── chart: percent line (surplus margin trend) ───────────────────────────────
  function PctLine({ values }) {
    const [ref, w] = useWidth(360);
    const H = 120, padL = 40, padR = 10, padT = 12, padB = 18;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const pts = values.map((v, i) => ({ v, i })).filter((p) => isNum(p.v));
    if (pts.length < 2) return h("div", { ref }, h(Awaiting, { label: "Not enough history for a trend", height: H }));
    const vals = pts.map((p) => p.v), minV = Math.min(...vals), maxV = Math.max(...vals), range = (maxV - minV) || 1, n = values.length;
    const xFor = (i) => padL + (n > 1 ? (innerW / (n - 1)) * i : innerW / 2);
    const yFor = (v) => padT + innerH - ((v - minV) / range) * innerH;
    const path = pts.map((p, k) => (k === 0 ? "M" : "L") + xFor(p.i).toFixed(1) + "," + yFor(p.v).toFixed(1)).join(" ");
    const area = path + " L" + xFor(pts[pts.length - 1].i).toFixed(1) + "," + (padT + innerH) + " L" + xFor(pts[0].i).toFixed(1) + "," + (padT + innerH) + " Z";
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        [maxV, minV].map((tv, i) => h("text", { key: i, x: padL - 6, y: yFor(tv) + 3, textAnchor: "end", fontSize: 9, fill: "var(--text-3,#94a3b8)", fontFamily: "ui-monospace, monospace" }, fmtPct(tv))),
        h("path", { d: area, fill: "#1C4ED8", fillOpacity: 0.08 }),
        h("path", { d: path, fill: "none", stroke: "#1C4ED8", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }),
        pts.map((p, k) => h("circle", { key: k, cx: xFor(p.i), cy: yFor(p.v), r: 2, fill: "#1C4ED8" }))));
  }

  // ── chart: monthly margin bars with benchmark line (FIX 5) ───────────────────
  function MarginChart({ values, labels, color, benchmark }) {
    const [ref, w] = useWidth(360);
    const H = 168, padL = 44, padR = 12, padT = 14, padB = 24;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const present = values.filter(isNum);
    if (!present.length) return h("div", { ref }, h(Awaiting, { label: "Awaiting margin history", height: H }));
    const n = values.length;
    const maxV = Math.max(...present, isNum(benchmark) ? benchmark : 0, 0.01);
    const minV = Math.min(...present, 0);
    const range = (maxV - minV) || 1;
    const yFor = (v) => padT + innerH - ((v - minV) / range) * innerH;
    const groupW = innerW / n, barW = Math.max(3, groupW * 0.55);
    const cx = (i) => padL + groupW * i + groupW / 2;
    const lastIdx = values.reduce((last, v, i) => (isNum(v) ? i : last), -1);
    const te = tickEvery(n);
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        [maxV, minV < 0 ? minV : 0].map((tv, i) => h("g", { key: "t" + i },
          h("line", { x1: padL, x2: w - padR, y1: yFor(tv), y2: yFor(tv), stroke: "var(--grid,#eef2f7)" }),
          h("text", { x: padL - 6, y: yFor(tv) + 3, textAnchor: "end", fontSize: 9, fill: "var(--text-3,#94a3b8)", fontFamily: "ui-monospace, monospace" }, fmtPct(tv)))),
        isNum(benchmark) ? h("line", { x1: padL, x2: w - padR, y1: yFor(benchmark), y2: yFor(benchmark), stroke: "#10B981", strokeWidth: 1.5, strokeDasharray: "5 4" }) : null,
        isNum(benchmark) ? h("text", { x: w - padR, y: yFor(benchmark) - 4, textAnchor: "end", fontSize: 9, fill: "#10B981", fontWeight: 700 }, "Target " + fmtPct(benchmark)) : null,
        values.map((v, i) => { if (!isNum(v)) return null; const barH = Math.abs(yFor(v) - yFor(0)); return h("rect", { key: i, x: cx(i) - barW / 2, y: v >= 0 ? yFor(v) : yFor(0), width: barW, height: Math.max(1, barH), rx: 2, fill: i === lastIdx ? color : "#CBD5E1" }, h("title", null, (labels[i] || "") + " " + fmtPct(v))); }),
        labels.map((lb, i) => (i % te === 0) ? h("text", { key: "x" + i, x: cx(i), y: H - 8, textAnchor: "middle", fontSize: 9, fill: "var(--text-3,#94a3b8)" }, lb) : null)));
  }

  // Two metric pills (current vs prior, % basis) below a margin chart (FIX 5).
  function MetricPills({ current, prior, color }) {
    const d = (isNum(current) && isNum(prior)) ? (current - prior) * 100 : null;
    const pill = (lbl, val, c) => h("div", { style: { flex: 1, background: "#F7F8FB", borderRadius: 8, padding: "8px 10px" } },
      h("div", { style: { fontSize: 9.5, color: "#94a3b8", textTransform: "uppercase", letterSpacing: 0.4 } }, lbl),
      h("div", { style: { fontSize: 17, fontWeight: 800, color: c } }, fmtPct(val)),
      (lbl !== "Current" && isNum(d)) ? h("div", { style: { fontSize: 10, fontWeight: 700, color: d >= 0 ? C_UP : C_DOWN } }, (d >= 0 ? "▲ " : "▼ ") + Math.abs(d).toFixed(1) + "pp vs current") : null);
    return h("div", { style: { display: "flex", gap: 8, marginTop: 10 } }, pill("Current", current, color), pill("Prior year", prior, "#64748b"));
  }

  // ── chart: cash projection line with month-spaced x labels (FIX 1) ───────────
  function CashLine({ proj }) {
    const [ref, w] = useWidth(280);
    const H = 120, padL = 46, padR = 10, padT = 12, padB = 22;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const vals = (proj || []).map(Number).filter(isNum);
    if (vals.length < 2) return h("div", { ref }, h(Awaiting, { label: "Awaiting cash projection", height: H }));
    const n = vals.length;
    const MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    const today = new Date();
    const labels = vals.map((_, i) => { const d = new Date(today.getTime()); d.setDate(d.getDate() + i); return MON[d.getMonth()]; });
    const te = tickEvery(n);
    const min = Math.min(...vals), max = Math.max(...vals), range = (max - min) || 1;
    const xFor = (i) => padL + (n > 1 ? (innerW / (n - 1)) * i : innerW / 2);
    const yFor = (v) => padT + innerH - ((v - min) / range) * innerH;
    const path = vals.map((v, i) => (i === 0 ? "M" : "L") + xFor(i).toFixed(1) + "," + yFor(v).toFixed(1)).join(" ");
    const area = path + " L" + xFor(n - 1).toFixed(1) + "," + (padT + innerH) + " L" + xFor(0).toFixed(1) + "," + (padT + innerH) + " Z";
    let lastLabel = null;
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        [max, min].map((tv, i) => h("text", { key: i, x: padL - 6, y: yFor(tv) + 3, textAnchor: "end", fontSize: 9, fill: "var(--text-3,#94a3b8)", fontFamily: "ui-monospace, monospace" }, fmtUSD(tv))),
        h("path", { d: area, fill: "#0891B2", fillOpacity: 0.10 }),
        h("path", { d: path, fill: "none", stroke: "#0891B2", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }),
        labels.map((lb, i) => { if (i % te !== 0 || lb === lastLabel) return null; lastLabel = lb; return h("text", { key: "x" + i, x: xFor(i), y: H - 8, textAnchor: "middle", fontSize: 9, fill: "var(--text-3,#94a3b8)" }, lb); })));
  }

  // ── chart: surplus bridge waterfall ──────────────────────────────────────────
  function Waterfall({ priorSurplus, dIncome, dOpex, currentSurplus }) {
    const [ref, w] = useWidth(420);
    const H = 210, padL = 8, padR = 8, padT = 22, padB = 38;
    const innerW = Math.max(20, w - padL - padR), innerH = H - padT - padB;
    const cols = [
      { label: "FY prior", base: 0, val: priorSurplus, color: "#64748B" },
      { label: "+ Income", base: priorSurplus, val: dIncome, color: "#059669" },
      { label: "− Opex", base: priorSurplus + dIncome, val: -dOpex, color: "#DC2626" },
      { label: "FY current", base: 0, val: currentSurplus, color: "#1C4ED8" },
    ];
    const tops = cols.map((c) => c.base + Math.max(0, c.val));
    const bots = cols.map((c) => c.base + Math.min(0, c.val));
    const maxV = Math.max(1, ...tops), minV = Math.min(0, ...bots), range = (maxV - minV) || 1;
    const yFor = (v) => padT + innerH - ((v - minV) / range) * innerH;
    const groupW = innerW / cols.length, barW = groupW * 0.52;
    const sgn = (v) => (!isNum(v) ? "—" : (v >= 0 ? "+" : "−") + fmtUSD(Math.abs(v)));
    return h("div", { ref, style: { width: "100%" } },
      h("svg", { width: w, height: H, style: { display: "block" } },
        h("line", { x1: padL, x2: w - padR, y1: yFor(0), y2: yFor(0), stroke: "var(--grid,#eef2f7)" }),
        cols.map((c, i) => {
          const top = c.base + Math.max(0, c.val), bot = c.base + Math.min(0, c.val);
          const x = padL + groupW * i + (groupW - barW) / 2, y = yFor(top), hgt = Math.max(1, yFor(bot) - yFor(top));
          return h("g", { key: i },
            h("rect", { x, y, width: barW, height: hgt, rx: 2, fill: c.color, opacity: 0.92 }),
            h("text", { x: x + barW / 2, y: y - 5, textAnchor: "middle", fontSize: 9.5, fontWeight: 700, fill: "var(--text,#0f172a)" }, (i === 1 || i === 2) ? sgn(c.val) : fmtUSD(c.val)),
            h("text", { x: x + barW / 2, y: H - 12, textAnchor: "middle", fontSize: 9.5, fill: "var(--text-3,#94a3b8)" }, c.label));
        })));
  }

  // ── synthesis hook (CFO what-matters-now + exceptions inbox) ──────────────────
  function useSynthesis(tenantId, surfaceType, companyContext) {
    const [st, setSt] = useState({ status: "idle", items: [] });
    useEffect(() => {
      let cancelled = false;
      if (!tenantId || !window.PerduraSynthesis || !window.PerduraSynthesis.fetch) { setSt({ status: "unavailable", items: [] }); return; }
      setSt({ status: "loading", items: [] });
      // Forward company context so the synthesis prompt always uses correct
      // terminology for this business type.
      window.PerduraSynthesis.fetch(tenantId, surfaceType, { company_context: companyContext }).then((resp) => {
        if (cancelled) return;
        if (!resp || resp.error) { setSt({ status: "error", items: [], error: resp && resp.error }); return; }
        setSt({ status: "done", items: Array.isArray(resp.prioritized) ? resp.prioritized : [] });
      }).catch(() => { if (!cancelled) setSt({ status: "error", items: [] }); });
      return () => { cancelled = true; };
    }, [tenantId, surfaceType, companyContext]);
    return st;
  }

  // ── aggregation of txns into category breakdowns ─────────────────────────────
  function aggregateByAccount(txns, profile, buckets) {
    const map = new Map();
    for (const tx of txns) {
      const b = bucketOf(tx.canonical_category, profile);
      if (buckets.indexOf(b) === -1) continue;
      const name = String(tx.account_name || tx.canonical_category || "Uncategorized").trim();
      const amt = Math.abs(parseFloat(tx.amount) || 0);
      if (!amt) continue;
      map.set(name, (map.get(name) || 0) + amt);
    }
    return Array.from(map.entries()).map(([name, total]) => ({ name, total })).sort((a, b) => b.total - a.total);
  }
  function topNWithOther(rows, n, colors) {
    const top = rows.slice(0, n).map((r, i) => ({ label: r.name, value: r.total, color: colors[i % colors.length] }));
    const rest = rows.slice(n).reduce((s, r) => s + r.total, 0);
    if (rest > 0) top.push({ label: "Other", value: rest, color: OTHER_COLOR });
    return top;
  }
  // Monthly income-by-stream series for the stacked area (FIX 2).
  function monthlyStreams(txns, profile, topStreamNames) {
    const monthKey = (d) => { const dt = new Date(d); return dt.getFullYear() * 100 + (dt.getMonth() + 1); };
    const monthLabel = (k) => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][(k % 100) - 1] || "?";
    const keys = new Set();
    txns.forEach((tx) => { if (bucketOf(tx.canonical_category, profile) === "revenue" && tx.posted_date) keys.add(monthKey(tx.posted_date)); });
    const sortedKeys = Array.from(keys).sort((a, b) => a - b).slice(-12);
    if (sortedKeys.length < 2) return null;
    const idxByKey = {}; sortedKeys.forEach((k, i) => { idxByKey[k] = i; });
    const nameSet = new Set(topStreamNames);
    const seriesByName = {}; topStreamNames.forEach((nm) => { seriesByName[nm] = sortedKeys.map(() => 0); });
    seriesByName.__other = sortedKeys.map(() => 0);
    txns.forEach((tx) => {
      if (bucketOf(tx.canonical_category, profile) !== "revenue") return;
      const k = monthKey(tx.posted_date); if (!(k in idxByKey)) return;
      const nm = String(tx.account_name || tx.canonical_category || "Uncategorized").trim();
      const target = nameSet.has(nm) ? nm : "__other";
      seriesByName[target][idxByKey[k]] += Math.abs(parseFloat(tx.amount) || 0);
    });
    const streams = topStreamNames.map((nm, i) => ({ name: nm, color: INCOME_COLORS[i % INCOME_COLORS.length], values: seriesByName[nm] }));
    if (seriesByName.__other.some((v) => v > 0)) streams.push({ name: "Other", color: OTHER_COLOR, values: seriesByName.__other });
    return { labels: sortedKeys.map(monthLabel), streams };
  }

  // ── Baseline CFO signals — always-available status read from real numbers ─────
  // When synthesis is unavailable/empty AND no exception detectors fired,
  // "What Matters Now" would otherwise render an empty placeholder. These items
  // summarise the real figures we DO have (revenue trend, margin vs benchmark,
  // EBITDA, runway, opex ratio) so the panel always carries a real signal.
  // Shape matches synth items (headline / supporting_detail / recommended_action
  // / severity / estimated_dollar_impact); benchmarks use KPICatalog fractions.
  function buildBaselineSignals(plH, pt, cash, archetype) {
    const items = [];
    const KC = window.KPICatalog;
    const bench = (k) => (KC && KC.getBenchmark ? KC.getBenchmark(k, archetype) : null);
    const income = isNum(pt.revenue) ? pt.revenue : null;
    const revA = plH && Array.isArray(plH.revenue) ? plH.revenue.filter(isNum) : [];

    // 1) Revenue trend (latest month vs prior).
    if (revA.length >= 2) {
      const last = revA[revA.length - 1], prev = revA[revA.length - 2];
      if (prev > 0) {
        const pct = ((last - prev) / prev) * 100, up = last >= prev;
        items.push({
          severity: Math.abs(pct) < 2 ? "info" : up ? "low" : "medium",
          headline: "Revenue " + (up ? "up" : "down") + " " + Math.abs(pct).toFixed(1) + "% vs prior month",
          supporting_detail: fmtUSD(last) + " this period vs " + fmtUSD(prev) + " prior.",
          recommended_action: up ? "Momentum is positive — watch customer concentration." : "Review revenue drivers on the Revenue page.",
          estimated_dollar_impact: Math.abs(last - prev),
        });
      }
    }

    // 2) Gross margin vs industry benchmark.
    const gm = (isNum(pt.grossProfit) && income) ? pt.grossProfit / income : null;
    if (gm != null) {
      const bm = bench("gross_margin_pct");
      if (bm && isNum(bm.p50)) {
        const below25 = isNum(bm.p25) && gm < bm.p25;
        const band = below25 ? "below industry p25" : (isNum(bm.p75) && gm > bm.p75) ? "above industry p75" : "within industry range";
        items.push({
          severity: below25 ? "high" : gm < bm.p50 ? "medium" : "low",
          headline: "Gross margin " + (gm * 100).toFixed(1) + "% — " + band,
          supporting_detail: "Industry: p25 " + Math.round((bm.p25 || 0) * 100) + "% / p50 " + Math.round(bm.p50 * 100) + "% / p75 " + Math.round((bm.p75 || 0) * 100) + "%.",
          recommended_action: below25 ? "Review COGS — margin is below industry p25." : "Margin is healthy — monitor for compression.",
          estimated_dollar_impact: Math.abs(gm - bm.p50) * income,
        });
      } else {
        items.push({
          severity: "info",
          headline: "Gross margin " + (gm * 100).toFixed(1) + "% this period",
          supporting_detail: fmtUSD(pt.grossProfit) + " gross profit on " + fmtUSD(income) + " revenue.",
          recommended_action: "Track gross-margin trend on the Income Statement.",
          estimated_dollar_impact: Math.abs(pt.grossProfit || 0),
        });
      }
    }

    // 3) EBITDA margin (flags a loss; otherwise vs benchmark).
    const em = (isNum(pt.ebitda) && income) ? pt.ebitda / income : null;
    if (em != null) {
      const bm = bench("ebitda_margin_pct");
      const neg = pt.ebitda < 0;
      const sev = neg ? "high" : (bm && isNum(bm.p50)) ? (em < (isNum(bm.p25) ? bm.p25 : bm.p50) ? "medium" : "low") : "info";
      items.push({
        severity: sev,
        headline: neg ? "EBITDA negative this period" : "EBITDA margin " + (em * 100).toFixed(1) + "%",
        supporting_detail: fmtUSD(pt.ebitda) + " EBITDA on " + fmtUSD(income) + " revenue" + (bm && isNum(bm.p50) ? " · industry p50 " + Math.round(bm.p50 * 100) + "%." : "."),
        recommended_action: neg ? "Operating at a loss — review the cost structure." : "Profitability is tracking — keep opex disciplined.",
        estimated_dollar_impact: Math.abs(pt.ebitda || 0),
      });
    }

    // 4) Cash runway (pre-computed in data.cash).
    if (cash && isNum(cash.runwayDays)) {
      const rd = cash.runwayDays, mo = Math.round(rd / 30);
      const burn = isNum(cash.burnPerDay) ? cash.burnPerDay * 30 : null;
      items.push({
        severity: rd < 90 ? "high" : rd < 180 ? "medium" : "low",
        headline: "~" + mo + " month" + (mo === 1 ? "" : "s") + " cash runway",
        supporting_detail: (isNum(cash.startCash) ? fmtUSD(cash.startCash) + " on hand" : "Current cash") + (burn != null ? " · ~" + fmtUSD(burn) + "/mo burn." : "."),
        recommended_action: rd < 90 ? "Protect liquidity — accelerate collections, defer non-urgent spend." : rd < 180 ? "Runway is moderate — keep an eye on burn." : "Healthy runway — consider putting idle cash to work.",
        estimated_dollar_impact: isNum(cash.startCash) ? cash.startCash : (burn || 0),
      });
    }

    // 5) OpEx ratio (informational baseline).
    if (income && isNum(pt.opex)) {
      items.push({
        severity: "info",
        headline: "OpEx " + ((pt.opex / income) * 100).toFixed(1) + "% of revenue",
        supporting_detail: fmtUSD(pt.opex) + " operating expense on " + fmtUSD(income) + " revenue.",
        recommended_action: "Review the Expenses page for the largest cost categories.",
        estimated_dollar_impact: Math.abs(pt.opex),
      });
    }

    return items
      .sort((a, b) => Math.abs(b.estimated_dollar_impact || 0) - Math.abs(a.estimated_dollar_impact || 0))
      .slice(0, 3);
  }

  // ── CFO "what matters now" rows ──────────────────────────────────────────────
  function WhatMattersNow({ synth, onDrill, fallback }) {
    if (synth.status === "loading" || synth.status === "idle") return h(Awaiting, { label: "Reviewing the ledger…", height: 90 });
    // Prefer live synthesis; when it has nothing (unavailable / error / clean),
    // fall back to computed baseline signals so the panel is never empty.
    const live = (synth.status === "done" && Array.isArray(synth.items)) ? synth.items.slice(0, 3) : [];
    const items = live.length ? live : (Array.isArray(fallback) ? fallback.slice(0, 3) : []);
    if (!items.length) return h(Awaiting, { label: "Nothing urgent to surface — the intelligence layer is clean for this period.", height: 90 });
    return h("div", { style: { display: "flex", flexDirection: "column", gap: 10 } },
      items.map((it, i) => {
        const drillable = it.drill_into && onDrill;
        return h("div", {
          key: i, onClick: drillable ? () => onDrill(it.drill_into) : undefined,
          style: { display: "grid", gridTemplateColumns: "30px 1fr auto", gap: 12, alignItems: "start", padding: "12px 14px", border: "1px solid var(--border,#e4e8f0)", borderRadius: 10, borderLeft: "3px solid " + sevOf(it.severity).dot, background: "rgba(255,255,255,0.6)", cursor: drillable ? "pointer" : "default" },
        },
          h("div", { style: { width: 26, height: 26, borderRadius: "50%", background: sevOf(it.severity).dot, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 800, fontSize: 13 } }, String(i + 1)),
          h("div", { style: { minWidth: 0 } },
            h("div", { style: { fontSize: 13.5, fontWeight: 700, color: "var(--text,#0f172a)", overflowWrap: "break-word" } }, it.headline || "—"),
            it.supporting_detail ? h("div", { style: { fontSize: 12, color: "var(--text-2,#475569)", marginTop: 4, lineHeight: 1.5, overflowWrap: "break-word" } }, it.supporting_detail) : null,
            it.recommended_action ? h("div", { style: { fontSize: 12, color: "var(--text,#0f172a)", marginTop: 6, lineHeight: 1.5, overflowWrap: "break-word" } }, h("span", { style: { fontWeight: 700, color: "#1C4ED8" } }, "Action: "), it.recommended_action) : null),
          h("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6, whiteSpace: "nowrap" } },
            h(SeverityBadge, { severity: it.severity }),
            isNum(it.estimated_dollar_impact) ? h("div", { style: { fontSize: 12, fontWeight: 700, color: "var(--text,#0f172a)" } }, fmtUSD(it.estimated_dollar_impact)) : null));
      }));
  }

  function ExceptionsInbox({ synth, onDrill }) {
    if (synth.status === "loading" || synth.status === "idle") return h(Awaiting, { label: "Pulling exceptions…", height: 120 });
    if (synth.status === "unavailable") return h(Awaiting, { label: "Exceptions feed is not available in this view.", height: 120 });
    if (synth.status === "error") return h(Awaiting, { label: "Awaiting data — exceptions could not be reached.", height: 120 });
    const items = synth.items.slice(0, 3);
    if (!items.length) return h(Awaiting, { label: "No active exceptions for this period.", height: 120 });
    return h("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
      items.map((it, i) => {
        const drillable = it.drill_into && onDrill;
        return h("div", {
          key: i, onClick: drillable ? () => onDrill(it.drill_into) : undefined,
          style: { display: "grid", gridTemplateColumns: "14px 1fr auto", gap: 10, alignItems: "start", padding: "10px 4px", borderBottom: i < items.length - 1 ? "1px solid var(--border,#eef2f7)" : "none", cursor: drillable ? "pointer" : "default" },
        },
          h("span", { style: { width: 9, height: 9, borderRadius: "50%", background: sevOf(it.severity).dot, marginTop: 4, flexShrink: 0 } }),
          h("div", { style: { minWidth: 0 } },
            h("div", { style: { fontSize: 12.5, fontWeight: 600, color: "var(--text,#0f172a)", overflowWrap: "break-word" } }, it.headline || "—"),
            it.supporting_detail ? h("div", { style: { fontSize: 11, color: "var(--text-2,#475569)", marginTop: 3, lineHeight: 1.45, overflowWrap: "break-word" } }, it.supporting_detail) : null),
          isNum(it.estimated_dollar_impact) ? h("div", { style: { fontSize: 12, fontWeight: 700, color: sevOf(it.severity).fg, whiteSpace: "nowrap" } }, fmtUSD(it.estimated_dollar_impact)) : null);
      }));
  }

  const clamp01 = (x) => Math.max(0, Math.min(1, x));

  // ── REAL client-side exception / outlier detection ──────────────────────────
  // computeExceptions(data, plHistory, companyProfile) → sorted array of
  // { id, severity:'HIGH'|'MEDIUM'|'WATCH', title, detail, driver, action,
  //   actionTarget, impact (number $), category }. Every detector is derived
  // strictly from real inputs (data.txns + plHistory + data.cash); a detector
  // that lacks its inputs is skipped silently — nothing is fabricated.
  function computeExceptions(data, plHistory, arSnapshots, apSnapshots, companyProfile) {
    // Back-compat: earlier callers passed (data, plHistory, companyProfile). If
    // arg 3 looks like a profile (object, not an array), shift it into place.
    if (arSnapshots && !Array.isArray(arSnapshots) && companyProfile == null) { companyProfile = arSnapshots; arSnapshots = null; apSnapshots = null; }
    const out = [];
    if (!data) return out;
    const txns = Array.isArray(data.txns) ? data.txns : [];
    const profile = companyProfile || data.companyProfile || null;
    const sec = (cat) => {
      const tx = window.PerduraTaxonomy;
      if (!tx || !tx.sectionForCategory) return null;
      try { return tx.sectionForCategory(cat, profile); } catch (e) { return null; }
    };
    const bkt = (cat) => bucketOf(cat, profile);
    const monthKey = (d) => { const dt = new Date(d); return Number.isNaN(dt.getTime()) ? null : dt.getFullYear() * 100 + (dt.getMonth() + 1); };
    const customerOf = (tx) => String(tx.memo || tx.account_name || tx.canonical_category || "Unknown").trim();
    const num = (v) => { const n = parseFloat(v); return Number.isFinite(n) ? n : 0; };

    // Index revenue txns by month for several revenue/concentration detectors.
    const revMonths = {};        // monthKey -> total signed revenue magnitude
    const revByCustMonth = {};   // monthKey -> { customer -> amount }
    const creditByMonth = {};    // monthKey -> credit/return magnitude (negative revenue)
    let haveRevenue = false;
    txns.forEach((tx) => {
      const b = bkt(tx.canonical_category);
      const s = sec(tx.canonical_category);
      const k = tx.posted_date ? monthKey(tx.posted_date) : null;
      if (k == null) return;
      const isRev = b === "revenue" || s === "revenue" || s === "contra_revenue" || s === "other_income";
      if (!isRev) return;
      haveRevenue = true;
      const amt = num(tx.amount);
      revMonths[k] = (revMonths[k] || 0) + amt;
      if (amt < 0) creditByMonth[k] = (creditByMonth[k] || 0) + Math.abs(amt);
      const cust = customerOf(tx);
      if (!revByCustMonth[k]) revByCustMonth[k] = {};
      revByCustMonth[k][cust] = (revByCustMonth[k][cust] || 0) + amt;
    });
    const revMonthKeys = Object.keys(revMonths).map(Number).sort((a, b) => a - b);
    const curMo = revMonthKeys.length ? revMonthKeys[revMonthKeys.length - 1] : null;
    const prevMo = revMonthKeys.length > 1 ? revMonthKeys[revMonthKeys.length - 2] : null;
    const grossRevCur = curMo != null ? Math.max(0, revMonths[curMo]) : 0;

    // (1) Customer revenue drop-off — had revenue in each of the prior 3 months
    // via memo, $0 this month. HIGH when that customer was >5% of revenue.
    if (haveRevenue && curMo != null && revMonthKeys.length >= 4) {
      const prior3 = revMonthKeys.slice(-4, -1); // three months before current
      const curCust = revByCustMonth[curMo] || {};
      const priorTotalByCust = {};
      prior3.forEach((mk) => { const m = revByCustMonth[mk] || {}; Object.keys(m).forEach((c) => { priorTotalByCust[c] = (priorTotalByCust[c] || 0) + m[c]; }); });
      Object.keys(priorTotalByCust).forEach((cust) => {
        if (cust === "Unknown") return;
        const presentAll3 = prior3.every((mk) => ((revByCustMonth[mk] || {})[cust] || 0) > 0);
        const curAmt = curCust[cust] || 0;
        if (presentAll3 && curAmt <= 0 && priorTotalByCust[cust] > 0) {
          const avgMo = priorTotalByCust[cust] / 3;
          const sharePrior = grossRevCur > 0 ? avgMo / grossRevCur : (priorTotalByCust[cust] / Math.max(1, prior3.reduce((s, mk) => s + Math.max(0, revMonths[mk]), 0) / 3));
          const high = sharePrior > 0.05;
          out.push({ id: "rev_dropoff_" + cust, severity: high ? "HIGH" : "MEDIUM", title: "Customer revenue stopped: " + cust, detail: cust + " billed ~" + fmtUSD(avgMo) + "/mo across the prior 3 months but $0 this month" + (high ? " (was " + (sharePrior * 100).toFixed(1) + "% of revenue)" : "") + ".", driver: cust, action: "Check " + cust + " status", actionTarget: cust, impact: avgMo, category: "Revenue" });
        }
      });
    }

    // (2) Large credits / returns — negative revenue > 3% of gross revenue in
    // the current month.
    if (haveRevenue && curMo != null && grossRevCur > 0) {
      const credits = creditByMonth[curMo] || 0;
      if (credits > 0.03 * grossRevCur) {
        out.push({ id: "credits_returns_" + curMo, severity: credits > 0.06 * grossRevCur ? "HIGH" : "MEDIUM", title: "Elevated credits / returns", detail: fmtUSD(credits) + " of credits/returns this month — " + ((credits / grossRevCur) * 100).toFixed(1) + "% of gross revenue.", driver: "Credit memos", action: "Review credit memos", actionTarget: "credits", impact: credits, category: "Revenue" });
      }
    }

    // (3) Revenue concentration up >5pp MoM — top-customer share by memo.
    if (haveRevenue && curMo != null && prevMo != null) {
      const topShare = (mk) => {
        const m = revByCustMonth[mk] || {}; const tot = Object.keys(m).reduce((s, c) => s + Math.max(0, m[c]), 0);
        if (tot <= 0) return null;
        let top = 0, name = null; Object.keys(m).forEach((c) => { if (c !== "Unknown" && m[c] > top) { top = m[c]; name = c; } });
        return name ? { share: top / tot, name, amt: top } : null;
      };
      const cs = topShare(curMo), ps = topShare(prevMo);
      if (cs && ps && (cs.share - ps.share) > 0.05) {
        out.push({ id: "rev_concentration_" + curMo, severity: cs.share > 0.4 ? "HIGH" : "MEDIUM", title: "Revenue concentration rising", detail: cs.name + " is now " + (cs.share * 100).toFixed(1) + "% of revenue, up " + ((cs.share - ps.share) * 100).toFixed(1) + "pp from last month.", driver: cs.name, action: "Assess concentration risk", actionTarget: cs.name, impact: cs.amt, category: "Revenue" });
      }
    }

    // ── Margin detectors from plHistory (monthly arrays) ──
    const plH = plHistory || (data && (data.plHistory || data.pl)) || null;
    const arr = (k) => (plH && Array.isArray(plH[k]) ? plH[k] : null);
    const revA = arr("revenue"); const gpA = arr("grossProfit") || arr("gp"); const cogsA = arr("cogs"); const labels = arr("labels");
    const gmAt = (i) => { if (!revA || !revA[i]) return null; const gp = gpA ? Number(gpA[i]) : (cogsA ? Number(revA[i]) - Number(cogsA[i]) : null); return gp == null ? null : gp / Number(revA[i]); };
    if (revA && revA.length >= 4) {
      const n = revA.length, ci = n - 1;
      const gmCur = gmAt(ci);
      const trail = [gmAt(ci - 1), gmAt(ci - 2), gmAt(ci - 3)].filter((v) => v != null);
      if (gmCur != null && trail.length) {
        const avg = trail.reduce((s, v) => s + v, 0) / trail.length;
        const bps = (avg - gmCur) * 10000; // positive = compression
        if (bps > 200) {
          const impact = (avg - gmCur) * Number(revA[ci]); // $ margin lost vs trailing avg
          out.push({ id: "gm_compression_" + ci, severity: bps > 300 ? "HIGH" : "MEDIUM", title: "Gross-margin compression", detail: "Gross margin " + (gmCur * 100).toFixed(1) + "% this month vs " + (avg * 100).toFixed(1) + "% trailing-3mo avg (" + Math.round(bps) + "bps lower).", driver: "Margin", action: "Investigate cost of sales", actionTarget: "gross_margin", impact: Math.abs(impact), category: "Margin" });
        }
      }
    }

    // Top-level revenue drop — current month < 90% of prior month (plHistory).
    if (revA && revA.length >= 2) {
      const n = revA.length, rev = Number(revA[n - 1]) || 0, prevRev = Number(revA[n - 2]) || 0;
      if (prevRev > 0 && rev < prevRev * 0.9) {
        out.push({ id: "rev_drop", severity: "HIGH", title: "Revenue declined materially", detail: "Revenue " + fmtUSD(rev) + " this period vs " + fmtUSD(prevRev) + " prior — down " + ((1 - rev / prevRev) * 100).toFixed(1) + "%.", driver: "Revenue", action: "Review revenue by customer and channel for the driver", actionTarget: "revenue", impact: prevRev - rev, category: "Revenue" });
      }
    }

    // EBITDA negative this period (plHistory).
    (function () {
      const ebA = arr("ebitda"); if (!ebA || !ebA.length) return;
      const n = ebA.length, eb = Number(ebA[n - 1]); const rev = revA && revA[n - 1] ? Number(revA[n - 1]) : 0;
      if (Number.isFinite(eb) && eb < 0) {
        out.push({ id: "ebitda_neg", severity: "HIGH", title: "EBITDA is negative this period", detail: "EBITDA (" + fmtUSD(Math.abs(eb)) + ") — operating at a loss. EBITDA margin: " + (rev > 0 ? ((eb / rev) * 100).toFixed(1) + "%" : "—") + ".", driver: "Profitability", action: "Review cost structure to restore positive EBITDA", actionTarget: "ebitda", impact: Math.abs(eb), category: "Profitability" });
      }
    })();

    // (5) Per-revenue-category GM% drop >500bps — needs per-category COGS pairing,
    // which is not separable from data.txns/plHistory here, so skipped (see notes).

    // ── OpEx detectors from data.txns ──
    if (txns.length && curMo != null) {
      // Build opex-by-category monthly + vendor-by-category-month for naming.
      const opexCatMonth = {};   // cat -> { monthKey -> amount }
      const vendorByCatMonth = {}; // cat -> { monthKey -> { vendor -> amount } }
      const firstSeen = {};      // vendor -> earliest monthKey
      const vendorAmtCurMo = {}; // vendor -> amount this month
      txns.forEach((tx) => {
        const k = tx.posted_date ? monthKey(tx.posted_date) : null; if (k == null) return;
        const vendor = customerOf(tx);
        if (firstSeen[vendor] == null || k < firstSeen[vendor]) firstSeen[vendor] = k;
        if (k === curMo) vendorAmtCurMo[vendor] = (vendorAmtCurMo[vendor] || 0) + Math.abs(num(tx.amount));
        if (bkt(tx.canonical_category) !== "opex") return;
        const cat = String(tx.canonical_category || tx.account_name || "Opex").trim();
        const amt = Math.abs(num(tx.amount));
        if (!opexCatMonth[cat]) opexCatMonth[cat] = {};
        opexCatMonth[cat][k] = (opexCatMonth[cat][k] || 0) + amt;
        if (!vendorByCatMonth[cat]) vendorByCatMonth[cat] = {};
        if (!vendorByCatMonth[cat][k]) vendorByCatMonth[cat][k] = {};
        vendorByCatMonth[cat][k][vendor] = (vendorByCatMonth[cat][k][vendor] || 0) + amt;
      });
      // (6) Expense category > 2x trailing-3mo avg.
      Object.keys(opexCatMonth).forEach((cat) => {
        const m = opexCatMonth[cat];
        const cur = m[curMo] || 0; if (cur <= 0) return;
        const trailKeys = revMonthKeys; // any month index isn't needed; use sorted opex months
        const monthsSorted = Object.keys(m).map(Number).sort((a, b) => a - b);
        const idx = monthsSorted.indexOf(curMo); if (idx < 3) return;
        const prior3 = monthsSorted.slice(idx - 3, idx).map((mk) => m[mk] || 0);
        const avg = prior3.reduce((s, v) => s + v, 0) / 3;
        if (avg <= 0) return;
        const ratio = cur / avg, delta = cur - avg;
        if (ratio > 2) {
          const high = ratio > 3 || delta > 50000;
          const vmap = (vendorByCatMonth[cat] && vendorByCatMonth[cat][curMo]) || {};
          let topVendor = null, topV = 0; Object.keys(vmap).forEach((v) => { if (vmap[v] > topV) { topV = vmap[v]; topVendor = v; } });
          out.push({ id: "opex_spike_" + cat, severity: high ? "HIGH" : "MEDIUM", title: cat + " spike", detail: cat + " is " + fmtUSD(cur) + " this month, " + ratio.toFixed(1) + "x its " + fmtUSD(avg) + " trailing-3mo average" + (topVendor && topVendor !== "Unknown" ? " (largest: " + topVendor + " " + fmtUSD(topV) + ")" : "") + ".", driver: topVendor || cat, action: "Review " + cat, actionTarget: cat, impact: delta, category: "OpEx" });
        }
      });
      // (7) New vendor first-seen this month with amount >$1K.
      Object.keys(vendorAmtCurMo).forEach((vendor) => {
        if (vendor === "Unknown") return;
        if (firstSeen[vendor] === curMo && vendorAmtCurMo[vendor] > 1000) {
          out.push({ id: "new_vendor_" + vendor, severity: vendorAmtCurMo[vendor] > 10000 ? "MEDIUM" : "WATCH", title: "New vendor: " + vendor, detail: "First-time payment to " + vendor + " of " + fmtUSD(vendorAmtCurMo[vendor]) + " this month.", driver: vendor, action: "Confirm " + vendor, actionTarget: vendor, impact: vendorAmtCurMo[vendor], category: "OpEx" });
        }
      });
    }

    // (8) OpEx/Revenue above p75 benchmark for archetype — only if a benchmark is
    // present on the profile overlay; else skipped (no fabrication).
    (function () {
      const ov = (profile && profile.industry_overlay) || {};
      const bm = (ov.benchmark_ranges || {});
      const p75 = bm.opex_ratio_pct && isNum(bm.opex_ratio_pct.p75) ? bm.opex_ratio_pct.p75 / 100 : null;
      if (p75 == null || curMo == null) return;
      const ci = revA ? revA.length - 1 : -1;
      const rev = revA && ci >= 0 ? Number(revA[ci]) : grossRevCur;
      const opexA = arr("opex");
      const opex = opexA && ci >= 0 ? Number(opexA[ci]) : null;
      if (!rev || opex == null) return;
      const ratio = opex / rev;
      if (ratio > p75) {
        out.push({ id: "opex_ratio_bench", severity: ratio > p75 * 1.25 ? "HIGH" : "MEDIUM", title: "OpEx ratio above benchmark", detail: "OpEx is " + (ratio * 100).toFixed(1) + "% of revenue vs the " + (p75 * 100).toFixed(0) + "% p75 benchmark for " + ((profile && profile.archetype) || "your") + " businesses.", driver: "OpEx", action: "Benchmark cost base", actionTarget: "opex", impact: (ratio - p75) * rev, category: "OpEx" });
      }
    })();

    // (13) Cash runway < 90 days — uses data.cash.runwayDays when present.
    if (data.cash && isNum(data.cash.runwayDays) && data.cash.runwayDays < 90) {
      const rd = data.cash.runwayDays;
      const burn = isNum(data.cash.burnPerDay) ? data.cash.burnPerDay * 30 : null;
      out.push({ id: "cash_runway", severity: rd < 45 ? "HIGH" : "MEDIUM", title: "Cash runway under 90 days", detail: "Roughly " + Math.round(rd) + " days of runway left at the current burn rate" + (burn != null ? " (~" + fmtUSD(burn) + "/mo)" : "") + ".", driver: "Cash", action: "Protect liquidity", actionTarget: "cash", impact: isNum(data.cash.startCash) ? data.cash.startCash : (burn || 0), category: "Cash" });
    }

    // Snapshot-derived detectors (DSO / AR concentration / AP aging). These fire
    // only when the AR/AP aging snapshots are supplied AND carry the relevant
    // fields (dso_days, top_customer_pct, 61+ bucket). Absent → skipped silently.
    const snapSort = (s) => (Array.isArray(s) ? s.slice().sort((a, b) => String(a.period_end || a.snapshot_month || "").localeCompare(String(b.period_end || b.snapshot_month || ""))) : []);
    const arS = snapSort(arSnapshots);
    if (arS.length >= 2) {
      const curS = arS[arS.length - 1], prevS = arS[arS.length - 2];
      const curDSO = num(curS.dso_days), prevDSO = num(prevS.dso_days);
      if (curDSO > 0 && prevDSO > 0 && curDSO - prevDSO > 5) {
        out.push({ id: "dso_extend", severity: "MEDIUM", title: "DSO extended " + (curDSO - prevDSO).toFixed(0) + " days", detail: "Days Sales Outstanding rose from " + prevDSO.toFixed(0) + " to " + curDSO.toFixed(0) + " days — customers paying slower.", driver: "Working Capital", action: "Prioritise collections on 60+ day accounts", actionTarget: "arap_analytics", impact: 0, category: "Working Capital" });
      }
    }
    if (arS.length) {
      const topCustPct = num(arS[arS.length - 1].top_customer_pct);
      if (topCustPct > 0.4) {
        out.push({ id: "ar_concentration", severity: "HIGH", title: "High AR concentration", detail: "Top customer is " + (topCustPct * 100).toFixed(0) + "% of total AR — collections at risk if that relationship changes.", driver: "AR Risk", action: "Diversify the customer base; review credit terms with the top customer", actionTarget: "arap_analytics", impact: 0, category: "AR Risk" });
      }
    }
    const apS = snapSort(apSnapshots);
    if (apS.length) {
      const last = apS[apS.length - 1];
      const total = num(last.total_balance) || (num(last.current_balance) + num(last.days_1_30) + num(last.days_31_60) + num(last.days_61_90) + num(last.days_91_plus));
      const aged61 = num(last.days_61_90) + num(last.days_91_plus);
      if (total > 0 && aged61 / total > 0.2) {
        out.push({ id: "ap_aging", severity: "MEDIUM", title: "Payables aging into 61+ days", detail: (aged61 / total * 100).toFixed(0) + "% of AP is 61+ days (" + fmtUSD(aged61) + ") — late-payment risk to suppliers.", driver: "AP Risk", action: "Clear the oldest payables to protect supplier terms", actionTarget: "arap_analytics", impact: aged61, category: "AP Risk" });
      }
      const dpo = num(last.dpo_days);
      if (dpo > 90) {
        out.push({ id: "dpo_stretch", severity: "MEDIUM", title: "DPO stretched to " + dpo.toFixed(0) + " days", detail: "Days Payable Outstanding at " + dpo.toFixed(0) + " days — vendors are being stretched; risk of supply disruption.", driver: "AP Risk", action: "Confirm no supplier relationships are at risk", actionTarget: "arap_analytics", impact: 0, category: "AP Risk" });
      }
    }

    // Archetype-benchmark detectors — thresholds come from the KPI catalog so
    // they adapt to the company's industry (no hardcoded margin/DSO cutoffs).
    (function () {
      const KC = window.KPICatalog; if (!KC || !KC.getBenchmark) return;
      const arch = (profile && (profile.archetype || (profile.industry_overlay && profile.industry_overlay.archetype_slug))) || "default";
      if (revA && revA.length) {
        const gmNow = gmAt(revA.length - 1);
        const gmBm = KC.getBenchmark("gross_margin_pct", arch);
        if (gmNow != null && gmBm && gmNow < gmBm.p25) {
          out.push({ id: "gm_below_benchmark", severity: "HIGH", title: "Gross margin below industry p25", detail: "Gross margin is " + (gmNow * 100).toFixed(1) + "% — below the p25 benchmark of " + (gmBm.p25 * 100).toFixed(1) + "% for " + arch + " businesses.", driver: "Margin", action: "Investigate cost of sales vs peers", actionTarget: "gross_margin", impact: revA[revA.length - 1] ? (gmBm.p25 - gmNow) * Number(revA[revA.length - 1]) : 0, category: "Margin" });
        }
      }
      if (typeof arS !== "undefined" && arS.length) {
        const dso = num(arS[arS.length - 1].dso_days);
        const dsoBm = KC.getBenchmark("dso_days", arch);
        if (dso > 0 && dsoBm && dso > dsoBm.p75) {
          out.push({ id: "dso_above_benchmark", severity: "MEDIUM", title: "DSO above industry p75", detail: "DSO is " + dso.toFixed(0) + " days — above the p75 benchmark of " + dsoBm.p75 + " days for " + arch + " businesses.", driver: "Working Capital", action: "Tighten collections on the oldest accounts", actionTarget: "arap_analytics", impact: 0, category: "Working Capital" });
        }
      }
    })();

    const sevRank = { HIGH: 0, MEDIUM: 1, WATCH: 2 };
    out.sort((a, b) => {
      const sr = (sevRank[a.severity] || 9) - (sevRank[b.severity] || 9);
      if (sr) return sr;
      return Math.abs(b.impact || 0) - Math.abs(a.impact || 0);
    });
    return out;
  }
  // Expose for reuse/testing.
  window.computeExceptions = computeExceptions;

  // ── acknowledged-state persistence helpers (localStorage) ────────────────────
  function ackStorageKey(companyId) { return "perdura_ack_exceptions_" + (companyId || "default"); }
  function loadAcks(companyId) {
    try { const raw = localStorage.getItem(ackStorageKey(companyId)); const a = JSON.parse(raw || "[]"); return Array.isArray(a) ? a : []; } catch (e) { return []; }
  }
  function saveAcks(companyId, ids) {
    try { localStorage.setItem(ackStorageKey(companyId), JSON.stringify(ids || [])); } catch (e) {}
  }

  // ── Renderer for computed exceptions (reuses PageKit ExceptionCard; falls back
  // to an equivalent inline card) with per-item acknowledge toggle. ────────────
  function ComputedExceptions({ items, companyId, max }) {
    const [acks, setAcks] = useState(() => loadAcks(companyId));
    useEffect(() => { setAcks(loadAcks(companyId)); }, [companyId]);
    const toggle = (id) => {
      setAcks((prev) => { const next = prev.indexOf(id) === -1 ? prev.concat(id) : prev.filter((x) => x !== id); saveAcks(companyId, next); return next; });
    };
    const ackSet = new Set(acks);
    // Unacknowledged first (preserving severity/impact sort), then acknowledged.
    const ordered = items.slice().sort((a, b) => (ackSet.has(a.id) ? 1 : 0) - (ackSet.has(b.id) ? 1 : 0));
    const shown = max ? ordered.slice(0, max) : ordered;
    const K = window.PerduraPageKit;
    const sevCap = (s) => (s === "HIGH" ? "High" : s === "WATCH" ? "Watch" : "Medium");
    const ackedCount = items.reduce((n, it) => n + (ackSet.has(it.id) ? 1 : 0), 0);
    const activeCount = items.length - ackedCount;
    const badge = h("div", { style: { display: "flex", alignItems: "center", gap: 10, fontSize: 10.5, fontWeight: 700, marginBottom: 8 } },
      h("span", { style: { color: "#b91c1c" } }, activeCount + " Active"),
      h("span", { style: { color: "#cbd5e1" } }, "|"),
      h("span", { style: { color: "#16A34A" } }, ackedCount + " Acknowledged"));
    return h("div", { style: { display: "flex", flexDirection: "column", gap: 2 } },
      badge,
      shown.map((it, i) => {
        const acked = ackSet.has(it.id);
        const ackBtn = h("div", { onClick: (e) => { e.stopPropagation(); toggle(it.id); }, style: { cursor: "pointer", fontSize: 10.5, fontWeight: 700, marginTop: 2, paddingLeft: 26, color: acked ? "#16A34A" : "#64748b", userSelect: "none" } }, acked ? "✓ Acknowledged" : "Acknowledge");
        const impactStr = isNum(it.impact) ? fmtUSD(Math.abs(it.impact)) : null;
        let card;
        if (K && K.ExceptionCard) {
          card = h(K.ExceptionCard, { num: i + 1, title: it.title, detail: it.detail, action: it.action, impact: impactStr, severity: sevCap(it.severity), category: it.category });
        } else {
          // Equivalent inline card matching the same fields.
          const s = sevOf(it.severity);
          card = h("div", { style: { background: "#fff", borderRadius: 10, border: "1px solid rgba(13,32,64,.09)", borderLeft: "4px solid " + s.dot, padding: "12px 14px" } },
            h("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 } },
              h("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } },
                h("span", { style: { width: 18, height: 18, borderRadius: "50%", background: s.dot, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 9, fontWeight: 800, flexShrink: 0 } }, i + 1),
                h("span", { style: { fontSize: 12, fontWeight: 700, color: "#0d2040" } }, it.title)),
              h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
                impactStr ? h("span", { style: { fontSize: 13, fontWeight: 800, color: s.fg } }, impactStr) : null,
                h("span", { style: { fontSize: 8, fontWeight: 700, padding: "2px 7px", borderRadius: 4, background: s.bg, color: s.fg, letterSpacing: 0.5, textTransform: "uppercase" } }, sevCap(it.severity)))),
            h("div", { style: { fontSize: 11, color: "#4a5680", lineHeight: 1.5, marginTop: 4, paddingLeft: 26 } }, it.detail),
            it.action ? h("div", { style: { paddingLeft: 26, marginTop: 6 } }, h("span", { style: { fontSize: 10.5, color: "#009fa0", fontWeight: 600 } }, it.action + " →")) : null);
        }
        return h("div", { key: it.id || i, style: { opacity: acked ? 0.4 : 1, marginBottom: 8 } }, card, ackBtn);
      }));
  }

  // ── Animated count-up value (FIX 6) — counts a formatted string's numeric
  // token up from 0 over 1.2s with easeOutCubic, preserving prefix/suffix. ──
  function AnimatedNumber({ text }) {
    const str = String(text == null ? "" : text);
    const m = str.match(/-?[\d,]+(\.\d+)?/);
    const last = useRef(null);
    const [disp, setDisp] = useState(str);
    useEffect(() => {
      if (!m) { setDisp(str); return; }
      if (last.current === str) { setDisp(str); return; }
      last.current = str;
      const target = parseFloat(m[0].replace(/,/g, ""));
      if (!Number.isFinite(target)) { setDisp(str); return; }
      const decimals = m[1] ? (m[1].length - 1) : 0;
      const dur = 1200;
      let raf, t0 = null;
      const grp = (n) => { const f = Math.abs(n).toFixed(decimals).split("."); f[0] = Number(f[0]).toLocaleString("en-US"); return (n < 0 ? "-" : "") + f.join("."); };
      const step = (ts) => {
        if (t0 == null) t0 = ts;
        const p = Math.min(1, (ts - t0) / dur);
        const e = 1 - Math.pow(1 - p, 3);
        setDisp(str.replace(m[0], grp(target * e)));
        if (p < 1) raf = requestAnimationFrame(step); else setDisp(str);
      };
      raf = requestAnimationFrame(step);
      return () => { if (raf) cancelAnimationFrame(raf); };
    }, [str]);
    return disp;
  }

  // ── Business Health Score gauge (FIX 6) ──────────────────────────────────────
  function HealthGauge({ score }) {
    const s = Math.max(0, Math.min(100, score || 0));
    const color = s >= 70 ? "#18a867" : s >= 40 ? "#D97706" : "#DC2626";
    const label = s >= 70 ? "Good" : s >= 40 ? "Watch" : "At Risk";
    const RAD = 54, CIRC = 2 * Math.PI * RAD, dash = CIRC * (s / 100);
    return h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
      h("svg", { width: 132, height: 132, viewBox: "0 0 132 132" },
        h("circle", { cx: 66, cy: 66, r: RAD, fill: "none", stroke: "#EEF1F6", strokeWidth: 12 }),
        h("circle", { cx: 66, cy: 66, r: RAD, fill: "none", stroke: color, strokeWidth: 12, strokeLinecap: "round", strokeDasharray: CIRC, strokeDashoffset: CIRC - dash, transform: "rotate(-90 66 66)", style: { transition: "stroke-dashoffset 1.1s ease" } }),
        h("text", { x: 66, y: 64, textAnchor: "middle", fontSize: 32, fontWeight: 800, fill: "#0F1520" }, Math.round(s)),
        h("text", { x: 66, y: 86, textAnchor: "middle", fontSize: 12, fontWeight: 700, fill: color }, label)));
  }

  // ── Top 5 Priorities (FIX 6) — ranked intelligence signals ───────────────────
  function TopPriorities({ items, onDrill }) {
    const top = (items || []).slice().sort((a, b) => Math.abs(b.estimated_dollar_impact || 0) - Math.abs(a.estimated_dollar_impact || 0)).slice(0, 5);
    const sev = (sv) => { const k = (sv || "").toLowerCase(); if (k.indexOf("high") >= 0 || k.indexOf("crit") >= 0) return { l: "HIGH", bg: "#FEE2E2", fg: "#B91C1C" }; if (k.indexOf("med") >= 0) return { l: "MEDIUM", bg: "#FEF3C7", fg: "#B45309" }; return { l: "LOW", bg: "#DBEAFE", fg: "#1D4ED8" }; };
    if (!top.length) return h("div", { style: { fontSize: 12, color: "#94a3b8", padding: "8px 0" } }, "No priority signals surfaced for this period.");
    return h("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
      top.map((it, i) => { const sc = sev(it.severity); return h("div", { key: i, onClick: onDrill ? () => onDrill(it) : undefined, className: "gl-row", style: { display: "flex", alignItems: "center", gap: 12, padding: "10px 12px", borderRadius: 8, background: "#F7F8FB", cursor: onDrill ? "pointer" : "default" } },
        h("span", { style: { fontSize: 9.5, fontWeight: 800, color: sc.fg, background: sc.bg, borderRadius: 999, padding: "3px 9px", whiteSpace: "nowrap", letterSpacing: 0.4 } }, sc.l),
        h("div", { style: { flex: 1, minWidth: 0 } },
          h("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" } }, it.headline || "Signal"),
          h("div", { style: { fontSize: 11, color: "#64748b", marginTop: 1 } }, it.recommended_action || it.action || it.supporting_detail || "Review and action this item.")),
        isNum(it.estimated_dollar_impact) ? h("span", { style: { fontSize: 13, fontWeight: 800, color: "#0f172a", whiteSpace: "nowrap" } }, fmtUSD(it.estimated_dollar_impact)) : null); }));
  }

  // ── main component ────────────────────────────────────────────────────────────
  function FinancialOverviewGrid(props) {
    const { data, profile, periodTotals, setPage, setDrillKey, tenantId } = props;
    const pt = periodTotals || {};
    const pl = (data && data.pl) || null;
    const plH = (data && (data.plHistory || data.pl)) || null;
    const cash = (data && data.cash) || null;
    const txns = (data && Array.isArray(data.txns)) ? data.txns : [];

    // ── profile-driven configuration (labels + terminology) ──
    const archetype = (profile && (profile.archetype || (profile.industry_overlay && profile.industry_overlay.archetype_slug))) || "general";
    const industry = (profile && (profile.industry || profile.industry_slug || (profile.industry_overlay && profile.industry_overlay.name))) || "";
    const businessName = (profile && (profile.name || profile.tenant_name || profile.company_name)) || "";
    const cfg = getKpiConfig(archetype, industry, profile && profile.selected_kpis);
    const V = getVocab(archetype);
    // Archetype + industry benchmark context for the synthesis prompts, pulled
    // from the declarative KPI catalog so the AI judges against the right bands.
    const benchContext = (function () {
      const KC = window.KPICatalog; if (!KC || !KC.getBenchmark) return "";
      const gm = KC.getBenchmark("gross_margin_pct", archetype), em = KC.getBenchmark("ebitda_margin_pct", archetype), dso = KC.getBenchmark("dso_days", archetype);
      const parts = [];
      if (gm) parts.push("gross margin p50 " + Math.round(gm.p50 * 100) + "%");
      if (em) parts.push("EBITDA margin p50 " + Math.round(em.p50 * 100) + "%");
      if (dso) parts.push("DSO p50 " + dso.p50 + " days");
      return parts.length ? " Industry benchmarks for this archetype: " + parts.join(", ") + ". Judge performance and word commentary against these bands." : "";
    })();
    const companyContext = "This is a " + archetype + " business in the " + (industry || "unspecified") + " industry. Use appropriate terminology for this business type." + benchContext;

    const wmn = useSynthesis(tenantId, "what_matters_now", companyContext);
    const exc = useSynthesis(tenantId, "exceptions_inbox", companyContext);

    // REAL client-side exception/outlier detection (computed from live data).
    const companyId = (profile && (profile.id || profile.tenant_id || profile.company_id)) || tenantId || "default";
    // Best-effort AR/AP aging snapshots → feed the DSO / concentration / AP-aging
    // detectors. Non-blocking: render never waits on this; absent → those
    // detectors stay dormant. Normalises per-customer/vendor monthly rows plus
    // working_capital_snapshots into [{period_end, dso_days, top_customer_pct}] /
    // [{snapshot_month, total_balance, days_61_90, days_91_plus, dpo_days}].
    const [snaps, setSnaps] = useState({ ar: null, ap: null });
    useEffect(() => {
      const db = window.supabaseClient; if (!db || !tenantId) { setSnaps({ ar: null, ap: null }); return; }
      let cancelled = false;
      const byMonth = (rows, monthField) => { const m = {}; (rows || []).forEach((r) => { const k = String(r[monthField] || "").slice(0, 7); if (!k) return; (m[k] || (m[k] = [])).push(r); }); return m; };
      Promise.all([
        db.from("ar_aging_snapshots").select("snapshot_month,customer_name,total_balance,top_customer_pct").eq("company_id", tenantId).order("snapshot_month", { ascending: true }).limit(5000).then((r) => r.data || [], () => []),
        db.from("ap_aging_snapshots").select("snapshot_month,vendor_name,total_balance,days_61_90,days_91_plus,dpo_days").eq("company_id", tenantId).order("snapshot_month", { ascending: true }).limit(5000).then((r) => r.data || [], () => []),
        db.from("working_capital_snapshots").select("period_end,dso_days,dpo_days").eq("company_id", tenantId).order("period_end", { ascending: true }).limit(400).then((r) => r.data || [], () => []),
      ]).then(([arRows, apRows, wcRows]) => {
        if (cancelled) return;
        const wcByMo = {}; (wcRows || []).forEach((r) => { wcByMo[String(r.period_end || "").slice(0, 7)] = r; });
        const arM = byMonth(arRows, "snapshot_month");
        const ar = Object.keys(arM).sort().map((k) => {
          const rows = arM[k]; const tot = rows.reduce((s, x) => s + (Number(x.total_balance) || 0), 0);
          const top = rows.reduce((mx, x) => Math.max(mx, Number(x.total_balance) || 0), 0);
          const stored = rows.find((x) => x.top_customer_pct != null);
          return { period_end: k, dso_days: wcByMo[k] ? Number(wcByMo[k].dso_days) : null, top_customer_pct: stored ? Number(stored.top_customer_pct) : (tot > 0 ? top / tot : null) };
        });
        const apM = byMonth(apRows, "snapshot_month");
        const ap = Object.keys(apM).sort().map((k) => {
          const rows = apM[k];
          const sum = (f) => rows.reduce((s, x) => s + (Number(x[f]) || 0), 0);
          const dpoRow = rows.find((x) => x.dpo_days != null);
          return { snapshot_month: k, total_balance: sum("total_balance"), days_61_90: sum("days_61_90"), days_91_plus: sum("days_91_plus"), dpo_days: dpoRow ? Number(dpoRow.dpo_days) : (wcByMo[k] ? Number(wcByMo[k].dpo_days) : null) };
        });
        setSnaps({ ar, ap });
      });
      return () => { cancelled = true; };
    }, [tenantId]);
    const computedExceptions = useMemo(() => {
      try { return computeExceptions(data, (data && (data.plHistory || data.pl)) || null, snaps.ar, snaps.ap, profile); }
      catch (e) { return []; }
    }, [data, profile, snaps]);
    const hasComputed = computedExceptions.length > 0;
    const onDrill = (d) => { if (window.PerduraSynthesis && window.PerduraSynthesis.navigateDrill) window.PerduraSynthesis.navigateDrill(d, setPage, setDrillKey); };
    // Baseline status signals from real numbers — used as the "What Matters Now"
    // fallback only when no exception detectors fired (avoids stacking with the
    // "Detected outliers" block that already renders above).
    const baselineSignals = useMemo(() => {
      try { return buildBaselineSignals(plH, pt, cash, archetype); } catch (e) { return []; }
    }, [plH, pt, cash, archetype]);

    // KPI figures (live)
    const income = isNum(pt.revenue) ? pt.revenue : null;
    const expenditure = (isNum(pt.cogs) || isNum(pt.opex)) ? ((pt.cogs || 0) + (pt.opex || 0)) : null;
    const surplus = (income != null && expenditure != null) ? income - expenditure : (isNum(pt.ebitda) ? pt.ebitda : null);
    const surplusMargin = (income && surplus != null) ? surplus / income : null;
    const opexRatio = (income && isNum(pt.opex)) ? pt.opex / income : null;
    const runwayDays = cash && isNum(cash.runwayDays) ? cash.runwayDays : null;
    const gmCurrent = (isNum(pt.grossProfit) && income) ? pt.grossProfit / income : null;

    // transaction-derived breakdowns (live, honest-empty)
    const breakdown = useMemo(() => (!txns.length ? { income: [], expense: [] } : { income: aggregateByAccount(txns, profile, ["revenue"]), expense: aggregateByAccount(txns, profile, ["cogs", "opex"]) }), [txns, profile]);
    const incomeRows = breakdown.income, expenseRows = breakdown.expense;
    const incomeTotal = incomeRows.reduce((s, r) => s + r.total, 0);
    const expenseTotal = expenseRows.reduce((s, r) => s + r.total, 0);
    const genDonRow = incomeRows.find((r) => /general\s*donation/i.test(r.name)) || incomeRows.find((r) => /donation|contribution/i.test(r.name)) || null;
    const genDonPct = (genDonRow && incomeTotal) ? genDonRow.total / incomeTotal : null;

    const ytdNet = useMemo(() => {
      if (!plH || !Array.isArray(plH.ebitda) || !Array.isArray(plH.years)) return null;
      const yr = Math.max.apply(null, plH.years); let sum = 0, any = false;
      plH.years.forEach((y, i) => { if (y === yr && isNum(plH.ebitda[i])) { sum += plH.ebitda[i]; any = true; } });
      return any ? sum : null;
    }, [plH]);

    const chart12 = useMemo(() => {
      if (!pl || !Array.isArray(pl.revenue)) return null;
      const labels = pl.labels || pl.revenue.map((_, i) => "M" + (i + 1));
      const inc = pl.revenue.map((v) => Number(v) || 0);
      const exp = pl.revenue.map((_, i) => (Number(pl.cogs && pl.cogs[i]) || 0) + (Number(pl.opex && pl.opex[i]) || 0));
      return { labels, income: inc, expense: exp, net: inc.map((v, i) => v - exp[i]) };
    }, [pl]);

    const streamArea = useMemo(() => (!txns.length || !incomeRows.length ? null : monthlyStreams(txns, profile, incomeRows.slice(0, 4).map((r) => r.name))), [txns, profile, incomeRows]);
    const singleStream = incomeRows.length === 1 || (streamArea && streamArea.streams.length <= 1);

    const marginSeries = useMemo(() => (!pl || !Array.isArray(pl.revenue) ? [] : pl.revenue.map((rv, i) => (rv > 0 ? ((Number(pl.ebitda && pl.ebitda[i]) || 0) / rv) : null))), [pl]);
    const gmSeries = useMemo(() => (!pl || !Array.isArray(pl.revenue) ? [] : pl.revenue.map((rv, i) => (rv > 0 ? ((Number(pl.gp && pl.gp[i]) || 0) / rv) : null))), [pl]);
    const emSeries = marginSeries; // EBITDA margin == ebitda/revenue (same basis as operating surplus here)

    // TTM current vs prior (used for variance + YoY + waterfall + margin pills)
    const ttm = useMemo(() => {
      if (!plH || !Array.isArray(plH.revenue)) return null;
      const N = plH.revenue.length;
      const sr = (arr, a, b) => { let s = 0; for (let i = a; i < b; i++) s += Number(arr && arr[i]) || 0; return s; };
      const hasPrior = N >= 24;
      const mk = (a, b) => { const inc = sr(plH.revenue, a, b); const exp = sr(plH.cogs, a, b) + sr(plH.opex, a, b); const surp = sr(plH.ebitda, a, b); const gp = sr(plH.gp, a, b); return { income: inc, expenditure: exp, surplus: surp, margin: inc > 0 ? surp / inc : null, gm: inc > 0 ? gp / inc : null }; };
      return { current: mk(Math.max(0, N - 12), N), prior: hasPrior ? mk(N - 24, N - 12) : null, hasPrior };
    }, [plH]);

    const monthlyBurn = cash && isNum(cash.burnPerDay) ? cash.burnPerDay * 30 : null;
    const operatingCF = ttm && ttm.current ? ttm.current.surplus : null;

    const seasonal = useMemo(() => {
      if (!plH || !Array.isArray(plH.revenue) || !Array.isArray(plH.labels)) return null;
      const agg = {};
      plH.labels.forEach((lb, i) => { const v = Number(plH.revenue[i]) || 0; if (!agg[lb]) agg[lb] = { sum: 0, n: 0 }; agg[lb].sum += v; agg[lb].n += 1; });
      const ranked = Object.keys(agg).map((lb) => ({ label: lb, avg: agg[lb].sum / agg[lb].n })).filter((r) => r.avg > 0).sort((a, b) => b.avg - a.avg);
      return ranked.length < 3 ? null : { peak: ranked.slice(0, 3), low: ranked.slice(-3).reverse() };
    }, [plH]);

    // archetype tag (FIX 7) — strictly from the loaded profile, no hardcoded fallback
    const ov = (profile && profile.industry_overlay) || {};
    const profileLoaded = !!profile;
    const archTag = profileLoaded ? (industry ? cap(archetype) + " — " + industry : cap(archetype)) : "Loading profile…";

    // benchmarks for margin charts (FIX 5) — only drawn if present (no fabrication)
    const bm = (ov && ov.benchmark_ranges) || {};
    const gmBench = bm.gross_margin_pct && isNum(bm.gross_margin_pct.p50) ? bm.gross_margin_pct.p50 / 100 : null;
    const emBench = bm.ebitda_margin_pct && isNum(bm.ebitda_margin_pct.p50) ? bm.ebitda_margin_pct.p50 / 100 : null;

    // ── per-card explanations (FIX 4) + detail data (FIX 3) — vocabulary-driven ──
    const varSurplus = (ttm && ttm.current) ? { label: cfg.surplus + " — trailing 12 mo vs prior", current: ttm.current.surplus, prior: ttm.prior ? ttm.prior.surplus : null, fmt: fmtUSD } : null;
    const varIncome = (ttm && ttm.current) ? { label: cfg.income + " — trailing 12 mo vs prior", current: ttm.current.income, prior: ttm.prior ? ttm.prior.income : null, fmt: fmtUSD } : null;
    const varExpense = (ttm && ttm.current) ? { label: cfg.expenditure + " — trailing 12 mo vs prior", current: ttm.current.expenditure, prior: ttm.prior ? ttm.prior.expenditure : null, fmt: fmtUSD } : null;

    const incExpDetail = {
      tableRows: [{ label: cfg.income, value: income }, { label: cfg.expenditure, value: expenditure }, { label: cfg.surplus, value: surplus }],
      tableFmt: fmtUSD, variance: varSurplus,
      explain: {
        whatItIs: cfg.income + " is your total inflow over the period; " + cfg.expenditure.toLowerCase() + " is everything it costs to operate; " + cfg.surplus.toLowerCase() + " is what remains.",
        whatsHappening: cfg.income + " " + fmtUSD(income) + " against " + cfg.expenditure.toLowerCase() + " " + fmtUSD(expenditure) + " gives " + cfg.surplus.toLowerCase() + " of " + fmtUSD(surplus) + " this period.",
        whyItMatters: "A positive " + V.surplusShort + " builds " + V.reserves + " and funds future " + V.activity + "; a deficit draws down cash.",
        whatToConsider: (surplus != null && surplus < 0) ? "Review the largest " + V.expenditure + " lines for savings to close the gap." : "Consider directing part of the " + V.surplusShort + " to " + V.reserves + ".",
      },
    };
    const incomeMixDetail = {
      tableRows: topNWithOther(incomeRows, 5, INCOME_COLORS).map((it) => ({ label: it.label, value: it.value })), tableFmt: fmtUSD, variance: varIncome,
      explain: { whatItIs: "How total " + V.incomeShort + " splits across " + V.sources + " over the trailing 12 months.", whatsHappening: incomeRows.length ? (incomeRows[0].name + " is the largest at " + (incomeTotal ? ((incomeRows[0].total / incomeTotal) * 100).toFixed(1) + "%" : "—") + " of " + V.incomeShort + ".") : ("No " + V.sources + " are classified yet."), whyItMatters: "Concentration in one source raises risk if it dips; diversity is more resilient.", whatToConsider: singleStream ? ("Classify " + V.incomeShort + " accounts in your ledger to unlock source-level analysis.") : "Watch whether reliance on the top source is rising over time." },
    };
    const expMixDetail = {
      tableRows: topNWithOther(expenseRows, 5, EXPENSE_COLORS).map((it) => ({ label: it.label, value: it.value })), tableFmt: fmtUSD, variance: varExpense,
      explain: { whatItIs: "How total " + V.expenditure + " split across lines over the trailing 12 months.", whatsHappening: expenseRows.length ? (expenseRows[0].name + " is the largest line at " + (expenseTotal ? ((expenseRows[0].total / expenseTotal) * 100).toFixed(1) + "%" : "—") + " of spend.") : "No cost lines are classified yet.", whyItMatters: "The biggest lines are where savings have the most impact.", whatToConsider: "Review the top line for pricing, supplier, or timing opportunities." },
    };
    const streamDetail = {
      tableRows: incomeRows.slice(0, 6).map((r) => ({ label: r.name, value: r.total })), tableFmt: fmtUSD, variance: varIncome,
      explain: { whatItIs: "Monthly " + V.incomeShort + " split by source over the trailing 12 months.", whatsHappening: singleStream ? ("Only one " + V.incomeShort + " category is mapped, so the chart shows a single band.") : (streamArea ? streamArea.streams.length + " " + V.sources + " are tracked monthly." : "Not enough monthly history yet."), whyItMatters: "Seeing each source's monthly shape reveals which are growing or seasonal.", whatToConsider: singleStream ? ("Ask your accountant to classify " + V.incomeShort + " accounts so sources separate.") : "Compare source momentum month over month to plan ahead." },
    };
    const marginDetail = {
      tableRows: [{ label: cfg.income + " (TTM)", value: ttm && ttm.current ? ttm.current.income : null }, { label: cfg.surplus + " (TTM)", value: ttm && ttm.current ? ttm.current.surplus : null }], tableFmt: fmtUSD, variance: varSurplus,
      explain: { whatItIs: cfg.margin + " is the share of " + V.incomeShort + " left after all " + V.expenditure + ".", whatsHappening: "Current trailing-12-month margin is " + fmtPct(ttm && ttm.current ? ttm.current.margin : null) + (ttm && ttm.prior ? " vs " + fmtPct(ttm.prior.margin) + " a year ago." : "."), whyItMatters: "A higher, stable margin means each unit of " + V.incomeShort + " contributes more to " + V.reserves + ".", whatToConsider: "If margin is falling, check whether " + V.expenditure + " are rising faster than " + V.incomeShort + "." },
    };
    const sourcesDetail = { tableRows: incomeRows.slice(0, 5).map((r) => ({ label: r.name, value: r.total })), tableFmt: fmtUSD, variance: varIncome, explain: { whatItIs: "The five largest " + V.sources + " by amount.", whatsHappening: incomeRows.length ? ("Top source: " + incomeRows[0].name + " at " + fmtUSD(incomeRows[0].total) + ".") : ("No " + V.sources + " classified."), whyItMatters: "These drive most of your " + V.funding + "; protecting them protects the budget.", whatToConsider: "Deepen the top sources and diversify where you can." } };
    const expLinesDetail = { tableRows: expenseRows.slice(0, 5).map((r) => ({ label: r.name, value: r.total })), tableFmt: fmtUSD, variance: varExpense, explain: { whatItIs: "The five largest " + V.expenditure + " lines by amount.", whatsHappening: expenseRows.length ? ("Top line: " + expenseRows[0].name + " at " + fmtUSD(expenseRows[0].total) + ".") : "No cost lines classified.", whyItMatters: "Cuts to the biggest lines move the budget the most.", whatToConsider: "Benchmark the top line against prior periods for creep." } };
    const exceptionsDetail = { tableRows: exc.items.slice(0, 6).map((it) => ({ label: it.headline || "Exception", value: it.estimated_dollar_impact })), tableFmt: fmtUSD, explain: { whatItIs: "Open review items the intelligence layer flagged this period.", whatsHappening: exc.items.length ? (exc.items.length + " active item(s); highest impact " + fmtUSD(Math.max.apply(null, exc.items.map((i) => Math.abs(i.estimated_dollar_impact || 0)))) + ".") : "No active exceptions.", whyItMatters: "Exceptions are early warnings of errors, overspend, or risk.", whatToConsider: "Action the highest-impact item first; click any row to drill in." } };
    const cfoDetail = { tableRows: wmn.items.slice(0, 6).map((it) => ({ label: it.headline || "Signal", value: it.estimated_dollar_impact })), tableFmt: fmtUSD, explain: { whatItIs: "The most material CFO-level signals ranked by dollar impact and severity.", whatsHappening: wmn.items.length ? (wmn.items.length + " ranked signal(s) for this period.") : "Nothing urgent surfaced.", whyItMatters: "These are the few things most worth your attention right now.", whatToConsider: "Work top-down; each row links to the supporting detail." } };
    const seasonalDetail = seasonal ? { tableRows: seasonal.peak.concat(seasonal.low).map((p) => ({ label: p.label, value: p.avg })), tableFmt: fmtUSD, explain: { whatItIs: "Average " + V.incomeShort + " by calendar month across your history.", whatsHappening: "Peak month: " + seasonal.peak[0].label + "; quietest: " + seasonal.low[0].label + ".", whyItMatters: "Knowing peak and low periods lets you time spend and hold liquidity cover.", whatToConsider: "Build cash buffers ahead of the low-" + V.incomeShort + " months." } } : null;
    const bridgeDetail = (ttm && ttm.hasPrior) ? { tableRows: [{ label: "FY prior " + V.surplusShort, value: ttm.prior.surplus }, { label: cfg.income + " change", value: ttm.current.income - ttm.prior.income }, { label: cfg.expenditure + " change", value: -(ttm.current.expenditure - ttm.prior.expenditure) }, { label: "FY current " + V.surplusShort, value: ttm.current.surplus }], tableFmt: fmtUSD, variance: varSurplus, explain: { whatItIs: "How last year's " + V.surplusShort + " became this year's, step by step.", whatsHappening: cfg.income + " moved " + (ttm.current.income - ttm.prior.income >= 0 ? "+" : "−") + fmtUSD(Math.abs(ttm.current.income - ttm.prior.income)) + " and " + cfg.expenditure.toLowerCase() + " " + (ttm.current.expenditure - ttm.prior.expenditure >= 0 ? "+" : "−") + fmtUSD(Math.abs(ttm.current.expenditure - ttm.prior.expenditure)) + ".", whyItMatters: "It shows whether growth or cost control drove the change.", whatToConsider: "If " + V.expenditure + " growth outpaced " + V.incomeShort + ", focus on cost discipline." } } : null;
    const yoyDetail = (ttm && ttm.hasPrior) ? { variance: varSurplus, explain: { whatItIs: "Trailing-12-month figures this year vs the prior 12 months.", whatsHappening: cfg.surplus + " " + fmtUSD(ttm.current.surplus) + " vs " + fmtUSD(ttm.prior.surplus) + " a year ago.", whyItMatters: "Year-on-year strips out seasonality to show the real trend.", whatToConsider: "Investigate any metric whose delta is unexpectedly large." } } : null;
    const cashDetail = cash ? { tableRows: [{ label: "Current cash", value: cash.startCash }, { label: "Monthly burn", value: monthlyBurn }, { label: "Operating cash flow (TTM)", value: operatingCF }], tableFmt: fmtUSD, explain: { whatItIs: "Your cash position and how fast it is changing.", whatsHappening: "About " + fmtUSD(cash.startCash) + " on hand; runway " + fmtDays(runwayDays) + " at the current rate.", whyItMatters: "Cash, not " + V.surplusShort + ", is what pays the bills — runway is the survival metric.", whatToConsider: (isNum(runwayDays) && runwayDays < 180) ? "Runway is short — protect inflows and defer non-urgent spend." : "Healthy runway — consider putting idle cash to work." } } : null;

    // ── Row-2 KPI resolver: labels are profile-driven; values are computed when
    // available, otherwise an honest "Awaiting data" (no fabricated metrics) ──
    const opexRatioSeries = (pl && pl.revenue) ? pl.revenue.map((rv, i) => (rv > 0 ? (Number(pl.opex && pl.opex[i]) || 0) / rv : null)) : [];
    const row2Defs = {
      "Days of Cash": { value: fmtDays(runwayDays), awaiting: runwayDays == null, sub: runwayDays === 999 ? "Cash-building (no burn)" : "At current net burn", color: "#0891B2", detail: cashDetail },
      "General Donations %": { value: fmtPct(genDonPct), awaiting: genDonPct == null, sub: genDonRow ? genDonRow.name + " of income" : "Share of total income", color: "#1C4ED8", detail: incomeMixDetail },
      "Opex Ratio": { value: fmtPct(opexRatio), awaiting: opexRatio == null, sub: "Operating costs ÷ " + V.incomeShort, color: (opexRatio != null && opexRatio > 0.80) ? "#D97706" : "#059669", delta: momDelta(opexRatioSeries, { pp: true }), detail: expMixDetail },
      "YTD Net Cash Flow": { value: fmtUSD(ytdNet), awaiting: ytdNet == null, sub: "Fiscal year to date · cash basis", color: (ytdNet != null && ytdNet < 0) ? "#DC2626" : "#059669", detail: cashDetail },
      "YTD Cash Flow": { value: fmtUSD(ytdNet), awaiting: ytdNet == null, sub: "Fiscal year to date · cash basis", color: (ytdNet != null && ytdNet < 0) ? "#DC2626" : "#059669", detail: cashDetail },
      "Gross Margin %": { value: fmtPct(gmCurrent), awaiting: gmCurrent == null, sub: "Gross profit ÷ " + V.incomeShort, color: "#1C4ED8", delta: momDelta(gmSeries, { pp: true }), detail: marginDetail },
      "Runway": { value: isNum(runwayDays) ? (runwayDays >= 999 ? "∞" : (runwayDays / 30).toFixed(1) + " mo") : "—", awaiting: runwayDays == null, sub: "Months at current burn", color: "#0891B2", detail: cashDetail },
      "Burn Rate": { value: isNum(monthlyBurn) ? (monthlyBurn > 0 ? fmtUSD(monthlyBurn) + "/mo" : fmtUSD(Math.abs(monthlyBurn)) + " build") : "—", awaiting: monthlyBurn == null, sub: "Net cash per month", color: (monthlyBurn != null && monthlyBurn > 0) ? "#DC2626" : "#059669", detail: cashDetail },
    };
    const resolveRow2 = (label) => row2Defs[label] || { value: "—", awaiting: true, sub: "Not available for this dataset", color: "#64748b", detail: null };

    // ── Dashboard mode (CEO Snapshot · CFO Detail · Board) — persisted ──
    const [mode, setMode] = useState(() => { try { return localStorage.getItem("perdura.dashMode") || "ceo"; } catch (e) { return "ceo"; } });
    const selectMode = (m) => {
      if (m === "board") { if (setPage) setPage("board_reports"); return; }
      setMode(m); try { localStorage.setItem("perdura.dashMode", m); } catch (e) {}
    };

    // ── Business Health Score (0–100) from profitability, liquidity, cost
    // ratio, and revenue trend — only the components that have real data. ──
    const revGrowth = (ttm && ttm.prior && ttm.prior.income) ? (ttm.current.income - ttm.prior.income) / Math.abs(ttm.prior.income) : null;
    const healthScore = (() => {
      let pts = 0, wt = 0;
      if (surplusMargin != null) { wt += 30; pts += 30 * clamp01(surplusMargin / 0.20); }
      if (runwayDays != null) { wt += 25; pts += 25 * clamp01(runwayDays / 180); }
      if (opexRatio != null) { wt += 20; pts += 20 * clamp01((1 - opexRatio) / 0.5); }
      if (revGrowth != null) { wt += 25; pts += 25 * clamp01((revGrowth + 0.1) / 0.3); }
      return wt ? Math.round((pts / wt) * 100) : null;
    })();
    const healthLabel = healthScore == null ? "—" : healthScore >= 70 ? "strong" : healthScore >= 40 ? "mixed" : "fragile";
    const modeSwitcher = h("div", { className: "mode-switcher" },
      [["ceo", "👤 CEO Snapshot"], ["cfo", "📊 CFO Detail"], ["board", "🏛️ Board View"]].map(([k, lab]) =>
        h("button", { key: k, className: "mode-btn" + (mode === k ? " active" : ""), onClick: () => selectMode(k) }, lab)));
    const healthCard = (mode === "ceo" && healthScore != null) ? h("div", { className: "dash-card dash-reveal", style: { display: "flex", alignItems: "center", gap: 24, flexWrap: "wrap" } },
      h(HealthGauge, { score: healthScore }),
      h("div", { style: { flex: 1, minWidth: 220 } },
        h("div", { style: { fontSize: 13.5, fontWeight: 800, color: "#0f172a" } }, "Business Health Score"),
        h("div", { style: { fontSize: 12.5, color: "#475569", marginTop: 6, lineHeight: 1.5 } },
          "Overall business health is " + healthLabel + " based on profitability (" + fmtPct(surplusMargin) + " " + V.surplusShort + " margin), liquidity (" + fmtDays(runwayDays) + " runway), cost discipline, and revenue trend" + (revGrowth != null ? " (" + (revGrowth >= 0 ? "+" : "−") + Math.abs(revGrowth * 100).toFixed(1) + "% YoY)" : "") + "."))) : null;
    const topPrioritiesCard = (mode === "ceo") ? h(Card, { title: "Top 5 Priorities", subtitle: "Highest-impact signals ranked by dollar impact", className: "grid-full" },
      h(TopPriorities, { items: wmn.items, onDrill })) : null;
    const cfoOnly = (el) => (mode === "cfo" ? el : null);

    // P5 — alert banner when the company has HIGH-severity exceptions.
    const highItems = [].concat(exc.items || [], wmn.items || []).filter((it) => /high|critical/i.test(String(it.severity || "")));
    const alertBanner = highItems.length ? h("div", { className: "alert-banner" },
      h("span", { style: { fontSize: 15 } }, "⚠"),
      h("div", null,
        h("strong", null, highItems.length + (highItems.length === 1 ? " high-priority exception" : " high-priority exceptions") + " requiring attention"),
        " · " + highItems.slice(0, 2).map((it) => it.headline || "Signal").join(" · ") + ". Immediate review recommended.")) : null;

    // P8 — archetype-appropriate operational KPIs (CEO Snapshot). Honest "—"
    // where operational data isn't connected yet (no fabricated values).
    const OP_KPIS = {
      retail: ["Inventory Turns", "AOV", "Return Rate", "Same-Store Growth", "Units Sold", "Shrink %"],
      ecommerce: ["Inventory Turns", "AOV", "Return Rate", "Conversion %", "Units Sold", "Cart Abandon %"],
      services: ["Utilization %", "Billable Hours", "Avg Project Size", "NPS", "Headcount", "Rev / Employee"],
      saas: ["MRR", "NRR", "Churn Rate", "CAC Payback", "DAU/MAU", "NPS"],
      nonprofit: ["Active Donors", "Volunteer Hours", "Programs Running", "Beneficiaries", "Donor Retention", "Cost / Beneficiary"],
      clinic: ["Active Patients", "Visits / Day", "No-Show Rate", "Provider Util %", "NPS", "Avg Visit Min"],
      manufacturing: ["Units Produced", "Capacity Util %", "Defect Rate", "On-Time Delivery", "Headcount", "Rev / Employee"],
      distribution: ["Order Fill Rate", "Inventory Turns", "Lines / Day", "On-Time Delivery", "Headcount", "Rev / Employee"],
    };
    const opKeys = OP_KPIS[archetype] || ["Headcount", "Rev / Employee", "NPS", "Active Accounts", "Utilization %", "Growth %"];
    const opKpiCard = (mode === "ceo") ? h(Card, { eye: "OPERATIONS", title: "Operational KPIs", subtitle: cap(archetype) + " metrics · connect operational data to populate", className: "grid-full" },
      h("div", { className: "op-kpi-grid" },
        opKeys.map((k, i) => h("div", { key: i, className: "op-kpi" },
          h("div", { className: "op-kpi-val" }, "—"),
          h("div", { className: "op-kpi-lbl" }, k))))) : null;

    return h("div", { className: "dashboard-wrapper" },
      h("style", null, [
        ".dashboard-wrapper { width:100%; max-width:1400px; margin:0 auto; padding:20px 24px; box-sizing:border-box; display:flex; flex-direction:column; gap:14px; }",
        ".dashboard-wrapper * { box-sizing:border-box; }",
        ".grid-4 { display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; width:100%; align-items:start; }",
        ".grid-3 { display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; width:100%; align-items:start; }",
        ".grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:14px; width:100%; align-items:start; }",
        ".grid-2-wide { display:grid; grid-template-columns:58% 1fr; gap:14px; width:100%; align-items:start; }",
        ".grid-full { width:100%; }",
        ".dash-card { background:#fff; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,0.06), 0 0 0 1px #E4E8F0; padding:16px 18px; min-width:0; box-sizing:border-box; }",
        ".dash-card.cfo-panel { background: linear-gradient(135deg, #F0F4FF 0%, #FFFFFF 100%); box-shadow:0 1px 6px rgba(28,78,216,0.10), 0 0 0 1px #DCE4FF; }",
        ".dash-card.seasonal-card { background: linear-gradient(135deg,#FFFBEB 0%,#FEF3C7 100%); box-shadow:0 1px 4px rgba(180,83,9,0.08), 0 0 0 1px #F5D68B; }",
        ".dash-card.exceptions { overflow:visible; max-width:none; height:100%; }",
        ".kpi-tile.clickable { cursor:pointer; transition:box-shadow .12s ease; }",
        ".kpi-tile.clickable:hover { box-shadow:0 2px 12px rgba(0,0,0,0.10), 0 0 0 1px #C7D2FE; }",
        ".see-detail { font-size:11px; color:#1C4ED8; font-weight:500; cursor:pointer; background:none; border:none; padding:0; white-space:nowrap; }",
        ".see-detail:hover { text-decoration:underline; }",
        ".detail-panel { background:#F7F8FB; border-top:1px solid #E4E8F0; padding:14px 16px; margin-top:12px; border-radius:0 0 8px 8px; }",
        ".detail-table { width:100%; border-collapse:collapse; font-size:11.5px; margin-bottom:4px; }",
        ".detail-table th { text-align:left; font-size:9.5px; text-transform:uppercase; letter-spacing:0.4px; color:#94a3b8; font-weight:600; padding:4px 6px; border-bottom:1px solid #E4E8F0; }",
        ".detail-table td { padding:5px 6px; color:#0f172a; border-bottom:1px solid #EEF2F7; }",
        ".what-means { background: linear-gradient(135deg, #F0F4FF, #EDF9F3); border-left:3px solid #1C4ED8; border-radius:8px; padding:12px 14px; margin-top:10px; }",
        ".stream-notice { font-size:11px; color:#B45309; background:#FFFBEB; border:1px solid #F5D68B; border-radius:8px; padding:8px 10px; margin-top:8px; line-height:1.45; }",
        ".yoy-table { width:100%; border-collapse:collapse; font-size:12px; }",
        ".yoy-table th { text-align:right; font-size:10px; text-transform:uppercase; letter-spacing:0.4px; color:#94a3b8; font-weight:600; padding:6px 6px; border-bottom:1px solid #eef2f7; }",
        ".yoy-table th:first-child, .yoy-table td:first-child { text-align:left; }",
        ".yoy-table td { text-align:right; padding:7px 6px; color:#0f172a; border-bottom:1px solid #f4f6fa; font-variant-numeric:tabular-nums; }",
        "@media (max-width:1100px){ .grid-4,.grid-3,.grid-2,.grid-2-wide{ grid-template-columns:1fr 1fr; } }",
        "@media (max-width:760px){ .grid-4,.grid-3,.grid-2,.grid-2-wide{ grid-template-columns:1fr; } }",
      ].join("\n")),

      // Header (FIX 7 archetype tag)
      h("div", { className: "dash-card", style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12, flexWrap: "wrap" } },
        h("div", { style: { minWidth: 0 } },
          h("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.6, color: "var(--text-3,#94a3b8)", fontWeight: 700 } }, "CFO Dashboard"),
          h("div", { style: { fontSize: 22, fontWeight: 800, color: "var(--text,#0f172a)", letterSpacing: "-0.4px", marginTop: 2 } }, profileLoaded ? (businessName || "CFO dashboard") : "Loading profile…"),
          h("div", { style: { fontSize: 12, color: "var(--text-3,#64748b)", marginTop: 4 } }, "Live KPI signals, intelligence, and seasonality from the current ledger snapshot" + (pt.label ? " · " + pt.label : "")),
          h("div", null, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, background: "#F5F3FF", color: "#7C3AED", fontSize: 11, fontWeight: 600, padding: "3px 9px", borderRadius: 20, marginTop: 4 } }, "⬡ " + archTag))),
        h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
          h("span", { style: { fontSize: 11, fontWeight: 600, color: "#475569", background: "#f1f5f9", borderRadius: 999, padding: "5px 10px" } }, "Cash basis"),
          h("span", { style: { fontSize: 11, fontWeight: 600, color: "#475569", background: "#f1f5f9", borderRadius: 999, padding: "5px 10px" } }, (profile && profile.currency) || "USD"))),

      // Dashboard mode switcher (CEO / CFO / Board)
      modeSwitcher,
      // P5 — high-severity alert banner
      alertBanner,
      // Business Health Score (CEO Snapshot only)
      healthCard,

      // Row 1 — KPI tiles (profile-driven labels, coloured + clickable)
      h("div", { className: "grid-4" },
        h(KpiTile, { label: cfg.income, value: fmtUSD(income), valueColor: "#1C4ED8", borderColor: "#1C4ED8", sub: pt.label || "Current period", awaiting: income == null, delta: momDelta(chart12 ? chart12.income : []), spark: chart12 ? chart12.income : null, status: tileStatus(momDelta(chart12 ? chart12.income : []), false), detail: incExpDetail, accentKind: "income" }),
        h(KpiTile, { label: cfg.expenditure, value: fmtUSD(expenditure), valueColor: "#DC2626", borderColor: "#DC2626", sub: "All costs this period", awaiting: expenditure == null, delta: momDelta(chart12 ? chart12.expense : []), spark: chart12 ? chart12.expense : null, status: tileStatus(momDelta(chart12 ? chart12.expense : []), true), detail: expMixDetail, accentKind: "cost" }),
        h(KpiTile, { label: cfg.surplus, value: fmtUSD(surplus), valueColor: (surplus != null && surplus < 0) ? "#DC2626" : "#059669", borderColor: (surplus != null && surplus < 0) ? "#DC2626" : "#059669", sub: cfg.income + " − " + cfg.expenditure.toLowerCase(), awaiting: surplus == null, delta: momDelta(chart12 ? chart12.net : []), spark: chart12 ? chart12.net : null, status: tileStatus(momDelta(chart12 ? chart12.net : []), false), detail: incExpDetail, accentKind: "profit" }),
        h(KpiTile, { label: cfg.margin, value: fmtPct(surplusMargin), valueColor: "#7C3AED", borderColor: "#7C3AED", sub: cfg.surplus + " ÷ " + V.incomeShort, awaiting: surplusMargin == null, delta: momDelta(marginSeries, { pp: true }), spark: marginSeries, status: tileStatus(momDelta(marginSeries, { pp: true }), false), detail: marginDetail, accentKind: "profit" })),

      // Row 2 — KPI tiles (profile-driven labels + honest values)
      h("div", { className: "grid-4" },
        cfg.row2.map((label) => {
          const r = resolveRow2(label);
          const lc = String(label).toLowerCase();
          const accentKind = /cash|runway|burn|liquid/.test(lc) ? "cash" : /margin|gross|profit|surplus/.test(lc) ? "profit" : /opex|cost|expense|ratio/.test(lc) ? "cost" : /donation|ytd|revenue|income/.test(lc) ? "income" : "watch";
          return h(KpiTile, { key: label, label: label, value: r.value, valueColor: r.color, borderColor: r.color, sub: r.sub, awaiting: r.awaiting, delta: r.delta, detail: r.detail, accentKind });
        })),

      // P8 — Operational KPIs (CEO Snapshot only), below the 8 financial KPIs
      opKpiCard,

      // Row 3 — CFO What Matters Now (full width, gradient)
      h(Card, { eye: "CFO INTELLIGENCE", title: "What Matters Now", subtitle: "Top ranked CFO intelligence signals for this period", className: "cfo-panel grid-full", detail: cfoDetail },
        hasComputed ? h("div", { style: { marginBottom: 12 } },
          h("div", { style: { fontSize: 10.5, fontWeight: 800, color: "#1C4ED8", textTransform: "uppercase", letterSpacing: 0.4, marginBottom: 8 } }, "Detected outliers"),
          h(ComputedExceptions, { items: computedExceptions, companyId: companyId, max: 3 })) : null,
        h(WhatMattersNow, { synth: wmn, onDrill, fallback: hasComputed ? [] : baselineSignals })),

      // Top 5 Priorities (CEO Snapshot only)
      topPrioritiesCard,

      // Rows 4–8 — full CFO detail (hidden in CEO Snapshot mode)
      cfoOnly(h(R.Fragment, null,
      // Row 4 — Income vs Expenditure (58%) + Income by Stream (42%)
      h("div", { className: "grid-2-wide" },
        h(Card, { title: "Monthly " + V.titleIncome + " vs " + V.titleExpenditure, subtitle: "Trailing 12 months · net " + V.surplusShort + " overlay", detail: incExpDetail },
          chart12 ? h(ComboBarLine, { labels: chart12.labels, income: chart12.income, expense: chart12.expense, net: chart12.net, incomeLabel: V.titleIncome, expenseLabel: V.titleExpenditure, netLabel: "Net " + V.surplusShort }) : h(Awaiting, { label: "Awaiting monthly P&L data", height: 240 })),
        h(Card, { title: V.titleIncome + " by Source", subtitle: "Trailing 12 months · stacked", detail: streamDetail },
          streamArea ? h(StackedArea, { labels: streamArea.labels, streams: streamArea.streams }) : h(Awaiting, { label: "Awaiting " + V.incomeShort + "-by-source detail in the ledger", height: 240 }),
          singleStream ? h("div", { className: "stream-notice" }, V.titleIncome + " breakdown requires category mapping — contact your accountant to classify " + V.incomeShort + " accounts in Xero") : null)),

      // Row 5 — Income Mix · Expenditure Mix · Surplus Margin trend
      h("div", { className: "grid-3" },
        h(Card, { title: V.titleIncome + " Mix", subtitle: "Trailing 12 months · by source", detail: incomeMixDetail },
          incomeRows.length ? h(Donut, { items: topNWithOther(incomeRows, 5, INCOME_COLORS) }) : h(Awaiting, { label: "Awaiting " + V.incomeShort + " breakdown" })),
        h(Card, { title: V.titleExpenditure + " Mix", subtitle: "Trailing 12 months · by line", detail: expMixDetail },
          expenseRows.length ? h(Donut, { items: topNWithOther(expenseRows, 5, EXPENSE_COLORS) }) : h(Awaiting, { label: "Awaiting cost breakdown" })),
        h(Card, { title: cfg.margin, subtitle: "Trailing 12 months", detail: marginDetail },
          h(PctLine, { values: marginSeries }),
          h("div", { style: { display: "flex", justifyContent: "space-between", gap: 8, marginTop: 10, fontSize: 11 } },
            h("div", null, h("div", { style: { color: "var(--text-3,#94a3b8)" } }, "Current (TTM)"), h("div", { style: { fontSize: 16, fontWeight: 800, color: "#0f172a" } }, fmtPct(ttm && ttm.current ? ttm.current.margin : null))),
            h("div", { style: { textAlign: "right" } }, h("div", { style: { color: "var(--text-3,#94a3b8)" } }, "Prior year (TTM)"), h("div", { style: { fontSize: 16, fontWeight: 800, color: "var(--text-2,#475569)" } }, fmtPct(ttm && ttm.prior ? ttm.prior.margin : null)))))),

      // Row 5b — Gross Margin & EBITDA Margin charts (FIX 5, full-width 1fr 1fr)
      h("div", { className: "grid-2" },
        h(Card, { title: "Gross Margin %", subtitle: "Trailing 12 months · current month highlighted", detail: marginDetail },
          h(MarginChart, { values: gmSeries, labels: (pl && pl.labels) || gmSeries.map((_, i) => "M" + (i + 1)), color: "#1C4ED8", benchmark: gmBench }),
          h(MetricPills, { current: ttm && ttm.current ? ttm.current.gm : null, prior: ttm && ttm.prior ? ttm.prior.gm : null, color: "#1C4ED8" })),
        h(Card, { title: "EBITDA Margin %", subtitle: "Trailing 12 months · current month highlighted", detail: marginDetail },
          h(MarginChart, { values: emSeries, labels: (pl && pl.labels) || emSeries.map((_, i) => "M" + (i + 1)), color: "#059669", benchmark: emBench }),
          h(MetricPills, { current: ttm && ttm.current ? ttm.current.margin : null, prior: ttm && ttm.prior ? ttm.prior.margin : null, color: "#059669" }))),

      // Row 6 — Top Income Sources · Top Expenditure Lines · Exceptions
      h("div", { className: "grid-3" },
        h(Card, { title: "Top 5 " + V.titleSources, subtitle: "% of total " + V.incomeShort + " · trailing 12 mo", detail: sourcesDetail },
          incomeRows.length && window.BarRanking ? h(window.BarRanking, { rows: incomeRows.slice(0, 5).map((r) => ({ label: r.name, value: r.total, share: incomeTotal ? r.total / incomeTotal : null })), color: "#1C4ED8", maxRows: 5 }) : h(Awaiting, { label: "Awaiting " + V.incomeShort + " breakdown" })),
        h(Card, { title: "Top 5 " + V.titleExpenditure + " Lines", subtitle: "% of total spend · trailing 12 mo", detail: expLinesDetail },
          expenseRows.length && window.BarRanking ? h(window.BarRanking, { rows: expenseRows.slice(0, 5).map((r) => ({ label: r.name, value: r.total, share: expenseTotal ? r.total / expenseTotal : null })), color: "#DC2626", maxRows: 5 }) : h(Awaiting, { label: "Awaiting cost breakdown" })),
        h(Card, { title: "Exceptions", subtitle: hasComputed ? "Detected outliers · " + computedExceptions.length + " flagged" : "Active review items + watchlist", className: "exceptions", detail: exceptionsDetail },
          hasComputed
            ? h(ComputedExceptions, { items: computedExceptions, companyId: companyId, max: 6 })
            : h(ExceptionsInbox, { synth: exc, onDrill }))),

      // Row 7 — Seasonal Intelligence band (full width, amber)
      h(Card, { title: "Seasonal Intelligence", subtitle: "Peak & low " + V.incomeShort + " windows from your history", className: "seasonal-card grid-full", detail: seasonalDetail },
        seasonal
          ? h("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 } },
              h("div", null,
                h("div", { style: { fontSize: 12, fontWeight: 700, color: "#0f172a", marginBottom: 6 } }, "Peak periods (strongest inflow)"),
                h("div", { style: { display: "flex", flexWrap: "wrap", gap: 8 } }, seasonal.peak.map((p, i) => h("span", { key: i, style: { fontSize: 12, fontWeight: 600, color: "#92400E", background: "rgba(255,255,255,0.7)", border: "1px solid #F5D68B", borderRadius: 999, padding: "4px 10px" } }, p.label + " · " + fmtUSD(p.avg))))),
              h("div", null,
                h("div", { style: { fontSize: 12, fontWeight: 700, color: "#0f172a", marginBottom: 6 } }, "Low periods (plan liquidity cover)"),
                h("div", { style: { display: "flex", flexWrap: "wrap", gap: 8 } }, seasonal.low.map((p, i) => h("span", { key: i, style: { fontSize: 12, fontWeight: 600, color: "#92400E", background: "rgba(255,255,255,0.7)", border: "1px solid #F5D68B", borderRadius: 999, padding: "4px 10px" } }, p.label + " · " + fmtUSD(p.avg))))))
          : h(Awaiting, { label: "Awaiting enough monthly history to detect seasonality", height: 70 })),

      // Row 8 — Surplus Bridge · YoY table · Cash & Liquidity
      h("div", { className: "grid-3" },
        h(Card, { title: V.titleSurplus + " Bridge", subtitle: "FY prior → current · trailing 12 mo basis", detail: bridgeDetail },
          (ttm && ttm.hasPrior) ? h(Waterfall, { priorSurplus: ttm.prior.surplus, dIncome: ttm.current.income - ttm.prior.income, dOpex: ttm.current.expenditure - ttm.prior.expenditure, currentSurplus: ttm.current.surplus }) : h(Awaiting, { label: "Awaiting prior-year data to build the bridge", height: 200 })),
        h(Card, { title: "Year-on-Year", subtitle: "Trailing 12 mo vs prior 12 mo", detail: yoyDetail },
          (ttm && ttm.hasPrior)
            ? h("table", { className: "yoy-table" },
                h("thead", null, h("tr", null, h("th", null, "Metric"), h("th", null, "Prior"), h("th", null, "Current"), h("th", null, "Δ"))),
                h("tbody", null, [
                  { k: cfg.income, c: ttm.current.income, p: ttm.prior.income, f: fmtUSD },
                  { k: cfg.expenditure, c: ttm.current.expenditure, p: ttm.prior.expenditure, f: fmtUSD },
                  { k: cfg.surplus, c: ttm.current.surplus, p: ttm.prior.surplus, f: fmtUSD },
                  { k: "Margin", c: ttm.current.margin, p: ttm.prior.margin, f: fmtPct, pp: true },
                  { k: "Days of Cash", c: runwayDays, p: null, f: fmtDays },
                ].map((row, i) => {
                  let delta = "—", dcolor = "var(--text-3,#94a3b8)";
                  if (isNum(row.c) && isNum(row.p)) {
                    if (row.pp) { const d = (row.c - row.p) * 100; delta = (d >= 0 ? "+" : "−") + Math.abs(d).toFixed(1) + "pp"; dcolor = d >= 0 ? C_UP : C_DOWN; }
                    else { const d = row.c - row.p; delta = (d >= 0 ? "+" : "−") + row.f(Math.abs(d)); dcolor = d >= 0 ? C_UP : C_DOWN; }
                  }
                  return h("tr", { key: i }, h("td", null, row.k), h("td", null, isNum(row.p) ? row.f(row.p) : "—"), h("td", { style: { fontWeight: 700 } }, isNum(row.c) ? row.f(row.c) : "—"), h("td", { style: { color: dcolor, fontWeight: 600 } }, delta));
                })))
            : h(Awaiting, { label: "Awaiting prior-year data for comparison", height: 200 })),
        h(Card, { title: "Cash & Liquidity", subtitle: cash ? "90-day projection" : null, detail: cashDetail },
          (cash && Array.isArray(cash.proj)) ? h("div", { style: { marginBottom: 12 } }, h(CashLine, { proj: cash.proj })) : null,
          cash
            ? h("div", { style: { display: "flex", flexDirection: "column", gap: 10 } },
                [
                  { k: "Current Cash", v: isNum(cash.startCash) ? fmtUSD(cash.startCash) : "—", c: "var(--text,#0f172a)" },
                  { k: "Monthly Burn", v: isNum(monthlyBurn) ? (monthlyBurn > 0 ? fmtUSD(monthlyBurn) : fmtUSD(Math.abs(monthlyBurn)) + " build") : "—", c: monthlyBurn > 0 ? C_DOWN : C_UP },
                  { k: "Operating Cash Flow", v: isNum(operatingCF) ? fmtUSD(operatingCF) : "—", c: operatingCF >= 0 ? C_UP : C_DOWN, sub: "Trailing 12 mo" },
                ].map((r, i) => h("div", { key: i, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
                  h("div", null, h("div", { style: { fontSize: 12, color: "var(--text-2,#475569)" } }, r.k), r.sub ? h("div", { style: { fontSize: 10, color: "var(--text-3,#94a3b8)" } }, r.sub) : null),
                  h("div", { style: { fontSize: 16, fontWeight: 800, color: r.c } }, r.v))))
            : h(Awaiting, { label: "Awaiting cash-account data", height: 140 }))))));
  }

  window.FinancialOverviewGrid = FinancialOverviewGrid;
})();
