// PerduraPageKit (Stage 11) — shared, self-contained primitives for the new
// intelligence pages (Revenue, OpEx, KPI Scorecard, Dummies Guide). Loaded
// before those page files. Pure presentation + small data helpers; all values
// come from live `data` (GL txns + plHistory) and kpi_library. No fabrication.

(function () {
  const { useState, useEffect, useMemo } = React;
  const E = React.createElement;
  const F = () => window.PerduraFormat || { money: (v, o) => (o && o.compact && Math.abs(v) >= 1000 ? "$" + (v / 1000).toFixed(0) + "K" : "$" + Math.round(v || 0).toLocaleString()) };
  const INK = "#0F1520", MUTE = "#4B5563", POS = "#059669", NEG = "#DC2626", LINE = "#E4E8F0";
  const CARD = { borderRadius: 12, boxShadow: "0 1px 6px rgba(13,32,64,0.06)", background: "#fff", border: "1px solid " + LINE };
  const MONO = "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace";
  // Distinct rotating palette for ranked lists / mix charts — no two consecutive alike.
  const RANK_COLORS = ["#1C4ED8", "#009fa0", "#b8921e", "#7c3aed", "#18a867", "#d97706", "#d94f47", "#0d2040"];
  const VCOLOR = { green: "#18a867", red: "#d94f47", gold: "#b8921e", teal: "#009fa0", amber: "#d97706", navy: "#0d2040", blue: "#1C4ED8" };
  const fmtCompact = (n) => { const a = Math.abs(n); const s = n < 0 ? "-" : ""; return a >= 1e6 ? s + "$" + (a / 1e6).toFixed(1) + "M" : a >= 1e3 ? s + "$" + (a / 1e3).toFixed(0) + "K" : s + "$" + Math.round(a); };

  const moneyStr = (v, opts) => { const n = Number(v || 0); const b = F().money(Math.abs(n), opts); return n < 0 ? "(" + b + ")" : b; };
  const money = (v, opts) => { const n = Number(v || 0); const o = opts || {}; const b = F().money(Math.abs(n), o); return React.createElement("span", { style: { color: n < 0 ? NEG : (o.color || "inherit"), fontVariantNumeric: "tabular-nums" } }, n < 0 ? "(" + b + ")" : b); };
  const pct = (v, d) => (v == null || !Number.isFinite(v)) ? "—" : v.toFixed(d == null ? 1 : d) + "%";
  const statusDot = (favPct) => (favPct == null || !Number.isFinite(favPct)) ? "⚪" : favPct > 5 ? "🟢" : favPct < -5 ? "🔴" : "🟡";

  // Design-system v2 — navy hero + light content shell. Pages render via Shell
  // so the hero is full-bleed and the body sits on the light analytics canvas.
  function Hero({ eyebrow, title, subtitle, controls }) {
    return React.createElement("div", { className: "pa-hero" },
      eyebrow ? React.createElement("div", { className: "pa-hero-eyebrow" }, eyebrow) : null,
      React.createElement("div", { className: "pa-hero-title" }, title),
      subtitle ? React.createElement("div", { className: "pa-hero-subtitle" }, subtitle) : null,
      controls ? React.createElement("div", { className: "pa-hero-controls" }, controls) : null);
  }
  function Shell({ hero, children }) {
    return React.createElement("div", { className: "pa-page" },
      React.createElement(Hero, hero || {}),
      React.createElement("div", { className: "pa-content" }, React.Children.toArray(children)));
  }

  function SectionHead({ eye, title, sub, right }) {
    return React.createElement("div", { className: "pa-section-head" },
      React.createElement("div", null,
        eye ? React.createElement("div", { className: "pa-section-label" }, eye) : null,
        React.createElement("div", { className: "pa-section-title" }, title),
        sub ? React.createElement("div", { style: { fontSize: 12, color: "#6475a0", marginTop: 2 } }, sub) : null),
      right || null);
  }

  // Pattern A — 3-tier KPI tile. New API: {label,value,delta,deltaDir,sub,valueColor,animDelay}.
  // Backward-compatible: old callers pass {accent (hex), sub, subColor}.
  function Kpi({ label, value, delta, deltaDir, sub, valueColor, accent, subColor, animDelay, onClick }) {
    const accentColor = VCOLOR[valueColor] || valueColor || accent || "#0d2040";
    const dCol = { up: "#18a867", dn: "#d94f47", warn: "#d97706", flat: "#6475a0" };
    const dBg = { up: "rgba(24,168,103,.1)", dn: "rgba(217,79,71,.1)", warn: "rgba(217,119,6,.1)", flat: "rgba(100,117,160,.1)" };
    const dd = deltaDir || "flat";
    return E("div", { className: "pa-kpitile", onClick: onClick || undefined, title: onClick ? "Click to drill in" : undefined, style: { animationDelay: (animDelay || 0) + "s", cursor: onClick ? "pointer" : undefined } },
      E("div", { style: { position: "absolute", top: 0, left: 0, right: 0, height: "3px", background: `linear-gradient(90deg, ${accentColor}, transparent)` } }),
      E("div", { style: { fontSize: "9px", fontWeight: 700, letterSpacing: "1.5px", textTransform: "uppercase", color: "#6475a0", marginBottom: "8px", fontFamily: MONO } }, label),
      E("div", { style: { fontSize: "30px", fontWeight: 800, color: accentColor, letterSpacing: "-1px", lineHeight: "1", marginBottom: "8px", fontFamily: MONO } }, value),
      delta ? E("span", { style: { display: "inline-flex", alignItems: "center", gap: "3px", fontSize: "10px", fontWeight: 700, padding: "2px 7px", borderRadius: "4px", fontFamily: MONO, color: dCol[dd], background: dBg[dd] } }, (dd === "up" ? "▲ " : dd === "dn" ? "▼ " : "") + delta) : null,
      sub != null ? E("div", { style: { fontSize: "10px", color: subColor || "#6475a0", marginTop: "4px", lineHeight: "1.3" } }, sub) : null);
  }

  function Card({ title, sub, children, padding, right }) {
    return React.createElement("div", { className: "pa-card dash-reveal" },
      (title || right) ? React.createElement("div", { className: "pa-card-head" },
        React.createElement("div", null,
          title ? React.createElement("div", { className: "pa-card-title" }, title) : null,
          sub ? React.createElement("div", { className: "pa-card-sub" }, sub) : null),
        right || null) : null,
      React.createElement("div", { className: "pa-card-body", style: padding != null ? { padding } : null }, children));
  }

  function Commentary({ title, items }) {
    return React.createElement("div", { className: "pa-insight" },
      React.createElement("div", { className: "pa-insight-head" }, title),
      items.map((it, i) => React.createElement("p", { key: i, dangerouslySetInnerHTML: { __html: (it.icon ? "<b style='color:#1C4ED8'>" + it.icon + "</b> " : "") + it.text } })));
  }

  // FIX 4 — standardised CFO Intelligence panel, identical across every page.
  // insights: [{type:'warning'|'positive'|'neutral', text, detail?}]; savings:
  // string (or HTML). Single design: 20px/24px padding, 12.5px insight text,
  // 11px detail, left blue rule, green savings box at the bottom. `text`/`detail`
  // accept inline HTML (bolding) via dangerouslySetInnerHTML.
  function CFOCommentaryPanel({ title, insights, savings }) {
    const html = (s) => ({ __html: String(s == null ? "" : s) });
    return E("div", { style: { background: "linear-gradient(135deg, rgba(13,32,64,.04) 0%, rgba(28,78,216,.04) 100%)", border: "1px solid rgba(13,32,64,.12)", borderLeft: "4px solid #1C4ED8", borderRadius: 10, padding: "20px 24px", marginTop: 24 } },
      E("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 14 } },
        E("span", { style: { fontSize: 10, fontWeight: 800, color: "#1C4ED8", letterSpacing: ".1em", textTransform: "uppercase" } }, "📊 CFO Intelligence"),
        title ? E("span", { style: { fontSize: 10, color: "#6475a0" } }, "— " + title) : null),
      E("div", { style: { display: "flex", flexDirection: "column", gap: 8, marginBottom: savings ? 14 : 0 } },
        (insights || []).filter(Boolean).map((it, i) => E("div", { key: i, style: { display: "flex", gap: 10, alignItems: "flex-start" } },
          E("span", { style: { fontSize: 14, flexShrink: 0, marginTop: 1 } }, it.type === "warning" ? "⚠️" : it.type === "positive" ? "✅" : "→"),
          E("div", null,
            E("span", { style: { fontSize: 12.5, color: "#1a2540", fontWeight: 600, lineHeight: 1.5 }, dangerouslySetInnerHTML: html(it.text) }),
            it.detail ? E("div", { style: { fontSize: 11, color: "#6475a0", marginTop: 2, lineHeight: 1.5 }, dangerouslySetInnerHTML: html(it.detail) }) : null)))),
      savings ? E("div", { style: { background: "rgba(24,168,103,.08)", border: "1px solid rgba(24,168,103,.2)", borderRadius: 7, padding: "10px 14px", marginTop: 8, display: "flex", gap: 10, alignItems: "flex-start" } },
        E("span", { style: { fontSize: 14, flexShrink: 0 } }, "💡"),
        E("div", null,
          E("div", { style: { fontSize: 11, fontWeight: 800, color: "#18a867", textTransform: "uppercase", letterSpacing: ".06em", marginBottom: 3 } }, "Where you can save"),
          E("div", { style: { fontSize: 12, color: "#1a2540", lineHeight: 1.5 }, dangerouslySetInnerHTML: html(savings) }))) : null);
  }
  window.CFOCommentaryPanel = CFOCommentaryPanel;

  // Recency gradient for revenue/income bars — latest darkest (navy → light).
  const recencyFill = (i, n, v) => (v < 0) ? "#d94f47"
    : i === n - 1 ? "#0d2040"
    : i >= n - 3 ? "#1C4ED8"
    : i >= n - 6 ? "#3B82F6"
    : "#93C5FD";

  // Bars with value labels + optional prior-year line overlay. Compact (≤200px),
  // recency-gradient fill unless an explicit `colors` array is supplied.
  function Bars({ values, prior, labels, color, target, height, colors }) {
    const vals = (values || []).map(Number);
    if (!vals.length) return React.createElement("div", { style: { padding: 16, fontSize: 11, color: "#94A3B8" } }, "No data.");
    const W = 800, H = height || 200, padT = 20, padB = 28, padX = 14, plotH = H - padT - padB;
    const allV = vals.concat((prior || []).map(Number)).concat(target != null ? [target] : []);
    const maxV = Math.max(0, ...allV), minV = Math.min(0, ...allV), range = (maxV - minV) || 1;
    const y = (v) => padT + ((maxV - v) / range) * plotH;
    const n = vals.length, colW = (W - padX * 2) / n, bw = Math.min(38, colW * 0.64);
    const cx = (i) => padX + colW * i + colW / 2;
    const fillOf = (v, i) => colors ? colors[i] : (color && color !== "#1C4ED8") ? color : recencyFill(i, n, v);
    const priorPts = (prior && prior.length) ? prior.map((v, i) => [cx(i), y(Number(v) || 0)]) : null;
    return React.createElement("svg", { viewBox: `0 0 ${W} ${H}`, width: "100%", style: { display: "block", width: "100%", height: "auto", maxHeight: H + "px" }, preserveAspectRatio: "xMidYMid meet" },
      React.createElement("line", { x1: padX, x2: W - padX, y1: y(0), y2: y(0), stroke: LINE }),
      target != null ? React.createElement("line", { x1: padX, x2: W - padX, y1: y(target), y2: y(target), stroke: "#D97706", strokeWidth: 1.2, strokeDasharray: "5 4" }) : null,
      vals.map((v, i) => {
        const top = v >= 0 ? y(v) : y(0), h = Math.max(1, Math.abs(y(v) - y(0)));
        return React.createElement("g", { key: i },
          React.createElement("rect", { x: cx(i) - bw / 2, y: top, width: bw, height: h, rx: 2.5, fill: fillOf(v, i) }),
          n <= 16 ? React.createElement("text", { x: cx(i), y: top - 4, textAnchor: "middle", fontSize: 9, fontWeight: 700, fill: INK, fontFamily: "Inter, system-ui" }, moneyStr(v, { compact: true })) : null,
          (labels && i % Math.ceil(n / 12) === 0) ? React.createElement("text", { x: cx(i), y: H - 9, textAnchor: "middle", fontSize: 9, fill: "#94A3B8" }, labels[i]) : null);
      }),
      priorPts ? React.createElement("polyline", { points: priorPts.map((p) => p.join(",")).join(" "), fill: "none", stroke: "#94A3B8", strokeWidth: 1.8, strokeDasharray: "4 3" }) : null);
  }

  // Line with moving-average overlay + endpoint/hi/lo labels. Compact (≤200px).
  function Line({ values, labels, color, movingAvg, suffix, height }) {
    const vals = (values || []).map(Number).filter(Number.isFinite);
    if (vals.length < 2) return React.createElement("div", { style: { padding: 16, fontSize: 11, color: "#94A3B8" } }, "Not enough history.");
    const W = 800, H = height || 200, padT = 16, padB = 26, padX = 16, plotH = H - padT - padB, plotW = W - padX * 2;
    const maxV = Math.max(...vals), minV = Math.min(...vals), range = (maxV - minV) || 1;
    const x = (i) => padX + (vals.length <= 1 ? 0 : (plotW / (vals.length - 1)) * i);
    const y = (v) => padT + (1 - (v - minV) / range) * plotH;
    const path = vals.map((v, i) => (i === 0 ? "M" : "L") + x(i).toFixed(1) + "," + y(v).toFixed(1)).join(" ");
    let maPath = null;
    if (movingAvg) { const ma = vals.map((_, i) => { const a = vals.slice(Math.max(0, i - 2), i + 1); return a.reduce((s, v) => s + v, 0) / a.length; }); maPath = ma.map((v, i) => (i === 0 ? "M" : "L") + x(i).toFixed(1) + "," + y(v).toFixed(1)).join(" "); }
    const sfx = suffix || "";
    const hiI = vals.indexOf(maxV), loI = vals.indexOf(minV), last = vals.length - 1;
    const labelSet = Array.from(new Set([hiI, loI, last]));
    return React.createElement("svg", { viewBox: `0 0 ${W} ${H}`, width: "100%", style: { display: "block", width: "100%", height: "auto", maxHeight: H + "px" }, preserveAspectRatio: "xMidYMid meet" },
      React.createElement("path", { d: path + ` L${x(last)},${H - padB} L${x(0)},${H - padB} Z`, fill: color || "#1C4ED8", fillOpacity: 0.08 }),
      maPath ? React.createElement("path", { d: maPath, fill: "none", stroke: "#94A3B8", strokeWidth: 1.6, strokeDasharray: "5 3" }) : null,
      React.createElement("path", { d: path, fill: "none", stroke: color || "#1C4ED8", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round" }),
      vals.map((v, i) => React.createElement("circle", { key: i, cx: x(i), cy: y(v), r: 2, fill: color || "#1C4ED8" })),
      labelSet.map((i) => React.createElement("text", { key: "l" + i, x: x(i), y: y(vals[i]) - 7, textAnchor: i === last ? "end" : "middle", fontSize: 9, fontWeight: 700, fill: INK, fontFamily: "Inter, system-ui" }, moneyStr(vals[i], { compact: true }) + sfx)),
      labels ? labels.map((lb, i) => (i % Math.ceil(labels.length / 8) === 0 ? React.createElement("text", { key: "x" + i, x: x(i), y: H - 9, textAnchor: "middle", fontSize: 8.5, fill: "#94A3B8" }, lb) : null)) : null);
  }

  function Donut({ items }) {
    const data = (items || []).filter((d) => (Number(d.value) || 0) > 0);
    const total = data.reduce((s, d) => s + (Number(d.value) || 0), 0) || 1;
    let acc = 0; const R = 52, C = 2 * Math.PI * R;
    return React.createElement("div", { style: { display: "grid", gridTemplateColumns: "130px 1fr", gap: 16, alignItems: "center" } },
      React.createElement("svg", { width: 130, height: 130, viewBox: "0 0 130 130" },
        React.createElement("circle", { cx: 65, cy: 65, r: R, fill: "none", stroke: "#EEF1F6", strokeWidth: 16 }),
        data.map((d, i) => { const frac = (Number(d.value) || 0) / total; const dash = C * frac; const off = -acc * C; acc += frac; return React.createElement("circle", { key: i, cx: 65, cy: 65, r: R, fill: "none", stroke: d.color, strokeWidth: 16, strokeDasharray: `${dash} ${C - dash}`, strokeDashoffset: off, transform: "rotate(-90 65 65)" }); })),
      React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } },
        data.map((d, i) => React.createElement("div", { key: i, style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 } },
          React.createElement("span", { style: { width: 10, height: 10, borderRadius: 3, background: d.color } }),
          React.createElement("span", { style: { flex: 1, color: "#334155", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, d.label),
          React.createElement("b", { style: { color: INK, fontVariantNumeric: "tabular-nums" } }, moneyStr(d.value, { compact: true })),
          React.createElement("span", { style: { color: MUTE, width: 42, textAlign: "right" } }, ((Number(d.value) || 0) / total * 100).toFixed(0) + "%")))));
  }

  function Spark({ values, color, width, height }) {
    const v = (values || []).map(Number).filter(Number.isFinite);
    if (v.length < 2) return null;
    const W = width || 84, H = height || 24, min = Math.min(...v), max = Math.max(...v), range = (max - min) || 1, step = W / (v.length - 1);
    const pts = v.map((x, i) => [i * step, H - ((x - min) / range) * (H - 2) - 1]);
    const path = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + "," + p[1].toFixed(1)).join(" ");
    return React.createElement("svg", { width: W, height: H, style: { display: "block" } }, React.createElement("path", { d: path, fill: "none", stroke: color || "#94A3B8", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" }));
  }

  // Pattern D — ranked list with colored inline bars (no two consecutive alike).
  function RankedList({ items, title, sub, onViewAll, palette, onItemClick }) {
    const pal = palette || RANK_COLORS;
    const max = Math.max.apply(null, items.map((i) => i.pct || 0).concat([1]));
    return E("div", { className: "pa-card", style: { height: "100%" } },
      (title || onViewAll) ? E("div", { className: "pa-card-head" },
        E("div", null,
          title ? E("div", { className: "pa-card-title" }, title) : null,
          sub ? E("div", { className: "pa-card-sub" }, sub) : null),
        onViewAll ? E("button", { className: "pa-export-btn", onClick: onViewAll }, "All →") : null) : null,
      E("div", { style: { padding: "6px 0" } },
        items.slice(0, 7).map((item, i) => E("div", { key: i, onClick: onItemClick ? () => onItemClick(item, i) : undefined, title: onItemClick ? "View account detail" : undefined, style: { display: "flex", alignItems: "center", gap: "8px", padding: "7px 14px", borderBottom: "1px solid rgba(13,32,64,.05)", cursor: onItemClick ? "pointer" : undefined } },
          E("span", { style: { fontSize: "9px", fontWeight: 700, color: "#6475a0", width: "14px", fontFamily: MONO } }, i + 1),
          E("span", { style: { flex: "1", fontSize: "11.5px", color: "#1a2540", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, item.label),
          E("div", { style: { width: "80px", flexShrink: 0 } },
            E("div", { style: { height: "6px", background: "rgba(13,32,64,.08)", borderRadius: "3px", overflow: "hidden" } },
              E("div", { style: { height: "100%", borderRadius: "3px", width: Math.round((item.pct || 0) / max * 100) + "%", background: pal[i % pal.length] } }))),
          E("span", { style: { fontSize: "10px", fontWeight: 700, color: "#1a2540", fontFamily: MONO, width: "44px", textAlign: "right" } }, item.pct != null ? item.pct.toFixed(1) + "%" : (item.value || ""))))));
  }

  // Pattern E — numbered exception card with severity border + flush-right impact.
  function ExceptionCard({ num, title, detail, action, impact, severity, category }) {
    const sevColor = severity === "High" ? "#d94f47" : severity === "Watch" ? "#d97706" : "#1C4ED8";
    const sevBg = severity === "High" ? "rgba(217,79,71,.1)" : severity === "Watch" ? "rgba(217,119,6,.1)" : "rgba(28,78,216,.1)";
    return E("div", { style: { background: "white", borderRadius: "10px", border: "1px solid rgba(13,32,64,.09)", borderLeft: "4px solid " + sevColor, padding: "12px 14px", marginBottom: "10px", boxShadow: "0 1px 4px rgba(13,32,64,.06)" } },
      E("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "4px" } },
        E("div", { style: { display: "flex", alignItems: "center", gap: "8px" } },
          E("span", { style: { width: "18px", height: "18px", borderRadius: "50%", background: sevColor, color: "white", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "9px", fontWeight: 800, flexShrink: 0 } }, num),
          E("span", { style: { fontSize: "12px", fontWeight: 700, color: "#0d2040" } }, title)),
        E("div", { style: { display: "flex", alignItems: "center", gap: "8px" } },
          impact ? E("span", { style: { fontSize: "13px", fontWeight: 800, color: sevColor, fontFamily: MONO } }, impact) : null,
          E("span", { style: { fontSize: "8px", fontWeight: 700, padding: "2px 7px", borderRadius: "4px", background: sevBg, color: sevColor, letterSpacing: ".5px", fontFamily: MONO, textTransform: "uppercase" } }, severity))),
      E("div", { style: { fontSize: "11px", color: "#4a5680", lineHeight: "1.5", marginBottom: "6px", paddingLeft: "26px" } }, detail),
      action ? E("div", { style: { paddingLeft: "26px" } }, E("span", { style: { fontSize: "10.5px", color: "#009fa0", fontWeight: 600, cursor: "pointer", textDecoration: "underline", textUnderlineOffset: "2px" } }, action + " →")) : null);
  }

  // Pattern F — horizontal waterfall/bridge bars (label · bar · value).
  function BridgeChart({ items }) {
    const max = Math.max.apply(null, items.map((i) => Math.abs(i.value)).concat([1]));
    const fmt = (n) => (n >= 0 ? "+" : "−") + fmtCompact(Math.abs(n)).replace("-", "");
    return E("div", { style: { padding: "6px 0" } },
      items.map((item, i) => {
        const isNeg = item.type === "negative";
        const isEnd = item.type === "end" || item.type === "start";
        const color = isEnd ? "#0d2040" : isNeg ? "#d94f47" : item.type === "positive" ? "#18a867" : "#0d2040";
        const w = Math.round(Math.abs(item.value) / max * 85);
        return E("div", { key: i, style: { display: "flex", alignItems: "center", gap: "10px", padding: "7px 14px", borderBottom: "1px solid rgba(13,32,64,.05)" } },
          E("span", { style: { width: "120px", fontSize: "11px", color: "#1a2540", fontWeight: isEnd ? 700 : 500, flexShrink: 0 } }, item.label),
          E("div", { style: { flex: 1, height: "20px", position: "relative" } },
            E("div", { style: { position: "absolute", left: 0, top: "50%", transform: "translateY(-50%)", height: "14px", width: w + "%", background: color, borderRadius: "3px", opacity: isEnd ? 1 : 0.85 } })),
          E("span", { style: { width: "74px", textAlign: "right", fontSize: "12px", fontWeight: 700, color: color, fontFamily: MONO, flexShrink: 0 } }, isEnd ? fmtCompact(item.value) : fmt(item.value)));
      }));
  }

  // Pattern B — multi-series bars + line/dashed overlays. Bar series support a
  // per-bar `fills` array and `hatchIdx` (bars ≥ idx render hatched). Line series
  // with `secondary:true` are scaled on their own range (e.g. a % overlay).
  function MultiSeriesBarChart({ months, series, height }) {
    const W = 800, H = height || 180, padL = 52, padR = 16, padT = 20, padB = 36;
    const chartW = W - padL - padR, chartH = H - padT - padB;
    const barSeries = series.filter((s) => s.type === "bar");
    const primLines = series.filter((s) => (s.type === "line" || s.type === "dashed-line") && !s.secondary);
    const allVals = barSeries.flatMap((s) => s.data).concat(primLines.flatMap((s) => s.data)).filter((v) => v != null && !isNaN(v));
    const maxV = Math.max.apply(null, allVals.concat([1])), minV = Math.min.apply(null, allVals.concat([0])), range = (maxV - minV) || 1;
    const toY = (v) => padT + chartH * (1 - (v - minV) / range);
    const groups = Math.max(barSeries.length, 1);
    const colW = chartW / Math.max(months.length, 1);
    const barW = colW * 0.7 / groups;
    const fmtN = (v) => Math.abs(v) >= 1e6 ? (v / 1e6).toFixed(1) + "M" : Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(0) + "K" : "" + Math.round(v);
    const els = [];
    els.push(E("defs", { key: "defs" }, E("pattern", { id: "pa-hatch", width: 5, height: 5, patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)" }, E("rect", { width: 5, height: 5, fill: "#bcd2f7" }), E("line", { x1: 0, y1: 0, x2: 0, y2: 5, stroke: "#1C4ED8", strokeWidth: 1.6 }))));
    [0, .25, .5, .75, 1].forEach((p, i) => { const y = padT + chartH * p; const val = maxV - range * p; els.push(E("line", { key: "g" + i, x1: padL, x2: W - padR, y1: y, y2: y, stroke: "rgba(13,32,64,.07)", strokeWidth: .8 }), E("text", { key: "gl" + i, x: padL - 6, y: y + 4, textAnchor: "end", fontSize: 9, fill: "#94a3b8", fontFamily: MONO }, fmtN(val))); });
    if (minV < 0) els.push(E("line", { key: "zero", x1: padL, x2: W - padR, y1: toY(0), y2: toY(0), stroke: "rgba(13,32,64,.2)", strokeWidth: 1 }));
    barSeries.forEach((s, si) => { s.data.forEach((v, mi) => { if (v == null || isNaN(v)) return; const x = padL + mi * colW + colW * 0.15 + si * barW; const yTop = toY(Math.max(v, 0)), yBot = toY(Math.min(v, 0)); const fill = s.fills ? s.fills[mi] : (s.hatchIdx != null && mi >= s.hatchIdx ? "url(#pa-hatch)" : s.color); els.push(E("rect", { key: "b" + si + "-" + mi, x: x, y: yTop, width: Math.max(barW - 1, 1), height: Math.max(yBot - yTop, 1), fill: fill, rx: 2, opacity: 0.92 })); if (months.length <= 14) els.push(E("text", { key: "bl" + si + "-" + mi, x: x + barW / 2, y: yTop - 3, textAnchor: "middle", fontSize: 8, fontWeight: 600, fill: "#374151", fontFamily: MONO }, fmtN(v))); }); });
    series.filter((s) => s.type === "line" || s.type === "dashed-line").forEach((s, si) => {
      let toYL = toY;
      if (s.secondary) { const sv = s.data.filter((v) => v != null && !isNaN(v)); const smin = Math.min.apply(null, sv), smax = Math.max.apply(null, sv), sr = (smax - smin) || 1; toYL = (v) => padT + chartH * 0.12 + chartH * 0.76 * (1 - (v - smin) / sr); }
      const pts = s.data.map((v, mi) => (v != null && !isNaN(v)) ? (padL + mi * colW + colW / 2) + "," + toYL(v) : null).filter(Boolean);
      if (pts.length < 2) return;
      els.push(E("polyline", { key: "ln" + si, points: pts.join(" "), fill: "none", stroke: s.color, strokeWidth: s.type === "dashed-line" ? 1.6 : 2.2, strokeDasharray: s.type === "dashed-line" ? "4,3" : "none", strokeLinejoin: "round", strokeLinecap: "round" }));
      s.data.forEach((v, mi) => { if (v == null || isNaN(v)) return; els.push(E("circle", { key: "d" + si + "-" + mi, cx: padL + mi * colW + colW / 2, cy: toYL(v), r: 2.5, fill: s.color })); });
    });
    months.forEach((m, mi) => { if (mi % Math.ceil(months.length / 12) === 0) els.push(E("text", { key: "xl" + mi, x: padL + mi * colW + colW / 2, y: H - 6, textAnchor: "middle", fontSize: 9, fill: "#94a3b8" }, m)); });
    return E("svg", { viewBox: `0 0 ${W} ${H}`, style: { width: "100%", height: "auto", maxHeight: H + "px", display: "block" } }, els);
  }

  // ── Data helpers ────────────────────────────────────────────────────────────
  // TTM (last 12) vs prior 12 from plHistory arrays.
  function ttm(plH) {
    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) => ({ revenue: sr(plH.revenue, a, b), cogs: sr(plH.cogs, a, b), opex: sr(plH.opex, a, b), gp: sr(plH.gp, a, b), ebitda: sr(plH.ebitda, a, b) });
    return { cur: mk(Math.max(0, N - 12), N), prior: hasPrior ? mk(N - 24, N - 12) : null, hasPrior, N };
  }

  // Group txns into category {name, cur, prior} over TTM vs prior-12 windows by
  // posted_date, restricted to the given taxonomy sections.
  function catBreakdown(txns, profile, sections) {
    const tax = window.PerduraTaxonomy;
    const rows = {};
    let maxDate = 0;
    for (const t of (txns || [])) { if (t.posted_date) { const d = +new Date(t.posted_date); if (d > maxDate) maxDate = d; } }
    if (!maxDate) maxDate = Date.now();
    const cutCur = maxDate - 365 * 86400000, cutPrior = maxDate - 730 * 86400000;
    const inSec = (cat) => { if (!cat) return false; if (!tax || !tax.sectionForCategory) return false; try { return sections.indexOf(tax.sectionForCategory(cat, profile)) >= 0; } catch (e) { return false; } };
    for (const t of (txns || [])) {
      const cat = t.canonical_category; if (!inSec(cat)) continue;
      const d = t.posted_date ? +new Date(t.posted_date) : 0; if (!d) continue;
      const amt = Math.abs(Number(t.amount) || 0);
      const r = rows[cat] || (rows[cat] = { name: cat, cur: 0, prior: 0 });
      if (d > cutCur && d <= maxDate) r.cur += amt;
      else if (d > cutPrior && d <= cutCur) r.prior += amt;
    }
    return Object.values(rows).filter((r) => r.cur || r.prior).sort((a, b) => b.cur - a.cur);
  }

  function hhi(rows) {
    const total = rows.reduce((s, r) => s + (r.cur || 0), 0) || 1;
    return rows.reduce((s, r) => { const sh = (r.cur || 0) / total; return s + sh * sh; }, 0) * 10000;
  }

  const PALETTE = ["#1C4ED8", "#0891B2", "#7C3AED", "#D97706", "#059669", "#DC2626", "#0F2044", "#b8921e", "#64748b"];

  // ── G1–G6 global behaviours ──────────────────────────────────────────────────
  // Reusable cross-cutting primitives required on every analytics page:
  //   G1 period selector · G2 comparison toggle · G3 breadcrumb/back ·
  //   G4 KeyTakeaway · G6 prominent margin rows. Plus period-aware data slicing
  //   so the selectors actually drive every chart and table (not just UI chrome).

  const PERIOD_MODES = [["this_month", "This Month"], ["last_month", "Last Month"], ["qtd", "QTD"], ["ytd", "YTD"], ["last_year", "Last Full Year"], ["l3m", "Last 3 Months"], ["l6m", "Last 6 Months"], ["ltm", "Last 12 Months"], ["l13m", "Last 13 Months (YoY)"], ["custom", "Custom"]];
  const CMP_MODES = [["prior_period", "vs Prior Period"], ["prior_year", "vs Prior Year"], ["budget", "vs Budget"], ["none", "No comparison"]];
  const MON3 = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
  const monthIdx = (name) => MON3.indexOf(String(name || "").slice(0, 3).toLowerCase());

  // Latest data month-end → anchor for relative periods. Falls back to today.
  function anchorFromPlH(plH) {
    if (plH && plH.labels && plH.labels.length) {
      const i = plH.labels.length - 1, m = monthIdx(plH.labels[i]), y = plH.years ? plH.years[i] : null;
      if (m >= 0 && y) return new Date(y, m + 1, 0);
    }
    return new Date();
  }
  const mStart = (y, m) => new Date(y, m, 1);
  const mEnd = (y, m) => new Date(y, m + 1, 0);

  // Resolve a period mode to {start,end,label} relative to an anchor month-end.
  function resolvePeriod(mode, anchor, custom) {
    const a = anchor instanceof Date ? anchor : new Date();
    const y = a.getFullYear(), m = a.getMonth();
    switch (mode) {
      case "this_month": return { start: mStart(y, m), end: mEnd(y, m), label: "This Month" };
      case "last_month": return { start: mStart(y, m - 1), end: mEnd(y, m - 1), label: "Last Month" };
      case "qtd": { const q = Math.floor(m / 3) * 3; return { start: mStart(y, q), end: mEnd(y, m), label: "Quarter to Date" }; }
      case "ytd": return { start: mStart(y, 0), end: mEnd(y, m), label: "Year to Date" };
      case "l3m": return { start: mStart(y, m - 2), end: mEnd(y, m), label: "Last 3 Months" };
      case "l6m": return { start: mStart(y, m - 5), end: mEnd(y, m), label: "Last 6 Months" };
      // Trailing 13 months: current month back through the same month last year,
      // so the window holds both this month and its prior-year twin for direct YoY.
      case "l13m": return { start: mStart(y, m - 12), end: mEnd(y, m), label: "Last 13 Months" };
      case "last_year": return { start: mStart(y - 1, 0), end: mEnd(y - 1, 11), label: "Last Full Year" };
      case "custom": return (custom && custom.start) ? { start: new Date(custom.start), end: custom.end ? new Date(custom.end) : mEnd(y, m), label: "Custom" } : { start: mStart(y, m - 11), end: mEnd(y, m), label: "Custom" };
      case "ltm": default: return { start: mStart(y, m - 11), end: mEnd(y, m), label: "Last 12 Months" };
    }
  }
  // Comparison window for a resolved range. Returns null for budget/none (handled by caller).
  function comparePeriod(range, cmp) {
    if (!range || cmp === "none" || cmp === "budget") return null;
    const s = range.start, e = range.end;
    if (cmp === "prior_year") return { start: new Date(s.getFullYear() - 1, s.getMonth(), s.getDate()), end: new Date(e.getFullYear() - 1, e.getMonth() + 1, 0), label: "Prior Year" };
    const lenM = (e.getFullYear() - s.getFullYear()) * 12 + (e.getMonth() - s.getMonth()) + 1;
    return { start: new Date(s.getFullYear(), s.getMonth() - lenM, 1), end: new Date(s.getFullYear(), s.getMonth(), 0), label: "Prior Period" };
  }
  // Month-array index span [a,b] of plHistory months overlapping a date range (inclusive).
  function plIdxRange(plH, range) {
    if (!plH || !plH.labels || !range) return [0, -1];
    const out = [];
    for (let i = 0; i < plH.labels.length; i++) {
      const m = monthIdx(plH.labels[i]), y = plH.years ? plH.years[i] : null;
      if (m < 0 || !y) continue;
      const d0 = new Date(y, m, 1), d1 = new Date(y, m + 1, 0);
      if (d1 >= range.start && d0 <= range.end) out.push(i);
    }
    return out.length ? [out[0], out[out.length - 1]] : [0, -1];
  }
  const sumIdx = (arr, span) => { let s = 0; for (let i = span[0]; i <= span[1]; i++) s += Number(arr && arr[i]) || 0; return s; };
  const sliceIdx = (arr, span) => (span[1] < span[0] ? [] : (arr || []).slice(span[0], span[1] + 1));
  // Filter GL txns to a date range by posted_date.
  function windowTxns(txns, range) {
    if (!range) return txns || [];
    const s = +range.start, e = +range.end;
    return (txns || []).filter((t) => { const d = t.posted_date ? +new Date(t.posted_date) : 0; return d >= s && d <= e; });
  }
  // Category breakdown over an explicit window + comparison window (vs catBreakdown's fixed TTM).
  function catBreakdownWindow(txns, profile, sections, range, cmpRange) {
    const tax = window.PerduraTaxonomy;
    const inSec = (cat) => { if (!cat || !tax || !tax.sectionForCategory) return false; try { return sections.indexOf(tax.sectionForCategory(cat, profile)) >= 0; } catch (e) { return false; } };
    const rows = {};
    const cs = +range.start, ce = +range.end, ps = cmpRange ? +cmpRange.start : null, pe = cmpRange ? +cmpRange.end : null;
    for (const t of (txns || [])) {
      const cat = t.canonical_category; if (!inSec(cat)) continue;
      const d = t.posted_date ? +new Date(t.posted_date) : 0; if (!d) continue;
      const amt = Math.abs(Number(t.amount) || 0);
      const r = rows[cat] || (rows[cat] = { name: cat, cur: 0, prior: 0 });
      if (d >= cs && d <= ce) r.cur += amt;
      else if (ps != null && d >= ps && d <= pe) r.prior += amt;
    }
    return Object.values(rows).filter((r) => r.cur || r.prior).sort((a, b) => b.cur - a.cur);
  }

  // G1/G2 — persisted period + comparison state, scoped per page key (localStorage).
  function usePeriodState(pageKey, defMode) {
    const k = "perdura.period." + pageKey, ck = "perdura.cmp." + pageKey, xk = "perdura.custom." + pageKey;
    const ls = (key, dflt) => { try { return localStorage.getItem(key) || dflt; } catch (e) { return dflt; } };
    const [mode, setMode] = useState(() => ls(k, defMode || "ytd"));
    const [cmp, setCmp] = useState(() => ls(ck, "prior_year"));
    const [custom, setCustom] = useState(() => { try { return JSON.parse(localStorage.getItem(xk)) || {}; } catch (e) { return {}; } });
    useEffect(() => { try { localStorage.setItem(k, mode); } catch (e) {} }, [mode]);
    useEffect(() => { try { localStorage.setItem(ck, cmp); } catch (e) {} }, [cmp]);
    useEffect(() => { try { localStorage.setItem(xk, JSON.stringify(custom)); } catch (e) {} }, [custom]);
    return { mode, setMode, cmp, setCmp, custom, setCustom };
  }
  // G1+G2 — glassy white-on-dark controls for the (dark) Hero. PeriodSelector /
  // CompareSelector are the named building blocks; both are backed by the real
  // PERIOD_MODES / CMP_MODES values so resolvePeriod/comparePeriod still drive
  // the data. PeriodControls composes them for the usePeriodState API.
  const GLASSY_SELECT = { background: "rgba(255,255,255,.12)", border: "1px solid rgba(255,255,255,.2)", borderRadius: 7, padding: "5px 10px", color: "#eef2ff", fontSize: 11, fontWeight: 600, cursor: "pointer", outline: "none", fontFamily: "Inter, system-ui" };
  const GLASSY_LABEL = { fontSize: 9, color: "rgba(174,184,210,.7)", letterSpacing: "1px", textTransform: "uppercase", fontFamily: MONO };
  const GLASSY_OPT = { background: "#1a3a6e", color: "white" };
  function PeriodSelector(p) {
    const { value, onChange, style } = p || {};
    return E("div", { style: Object.assign({ display: "flex", alignItems: "center", gap: 6 }, style) },
      E("span", { style: GLASSY_LABEL }, "SHOW"),
      E("select", { value: value, onChange: (e) => onChange(e.target.value), style: GLASSY_SELECT },
        PERIOD_MODES.map(([v, l]) => E("option", { key: v, value: v, style: GLASSY_OPT }, l))));
  }
  function CompareSelector(p) {
    const { value, onChange, style } = p || {};
    return E("div", { style: Object.assign({ display: "flex", alignItems: "center", gap: 6 }, style) },
      E("span", { style: GLASSY_LABEL }, "VS"),
      E("select", { value: value, onChange: (e) => onChange(e.target.value), style: GLASSY_SELECT },
        CMP_MODES.map(([v, l]) => E("option", { key: v, value: v, style: GLASSY_OPT }, l.replace(/^vs /, "")))));
  }
  // G1+G2 control cluster — drop into a Hero's `controls` slot.
  function PeriodControls(p) {
    const { mode, setMode, cmp, setCmp, custom, setCustom, showCompare } = p;
    return E("div", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" } },
      E(PeriodSelector, { value: mode, onChange: setMode }),
      (mode === "custom" && setCustom) ? E("input", { type: "month", className: "pc-period-select", value: (custom && custom.start ? custom.start.slice(0, 7) : ""), onChange: (e) => setCustom(Object.assign({}, custom, { start: e.target.value + "-01" })) }) : null,
      (mode === "custom" && setCustom) ? E("input", { type: "month", className: "pc-period-select", value: (custom && custom.end ? custom.end.slice(0, 7) : ""), onChange: (e) => setCustom(Object.assign({}, custom, { end: e.target.value + "-28" })) }) : null,
      (showCompare !== false && cmp != null) ? E(CompareSelector, { value: cmp, onChange: setCmp }) : null);
  }

  // G3 — breadcrumb trail. trail = [{label, onClick?}]; last item is the current page.
  function Breadcrumb({ trail }) {
    if (!trail || !trail.length) return null;
    return E("div", { style: { display: "flex", alignItems: "center", gap: 7, fontSize: 12, marginBottom: 14, flexWrap: "wrap" } },
      trail.map((t, i) => E(React.Fragment, { key: i },
        i > 0 ? E("span", { style: { color: "#94a3b8" } }, "›") : null,
        (t.onClick && i < trail.length - 1)
          ? E("span", { onClick: t.onClick, style: { color: "#1C4ED8", cursor: "pointer", fontWeight: 600, textDecoration: "none" }, onMouseOver: (e) => e.target.style.textDecoration = "underline", onMouseOut: (e) => e.target.style.textDecoration = "none" }, t.label)
          : E("span", { style: { color: i === trail.length - 1 ? "#0d2040" : "#6475a0", fontWeight: i === trail.length - 1 ? 700 : 500 } }, t.label))));
  }
  // G3 — "← Back to X" button for drill-throughs.
  function BackButton({ label, onClick }) {
    return E("button", { onClick: onClick, style: { display: "inline-flex", alignItems: "center", gap: 6, background: "#fff", border: "1px solid rgba(13,32,64,.14)", borderRadius: 8, padding: "7px 13px", fontSize: 12, fontWeight: 600, color: "#1C4ED8", cursor: "pointer", marginBottom: 14 } }, "← Back to " + label);
  }

  // G4 — KEY TAKEAWAY panel under a chart. Pass `text` (HTML string) or children.
  function KeyTakeaway(p) {
    return E("div", { className: "pa-insight", style: Object.assign({ marginTop: 12 }, p.style) },
      E("div", { className: "pa-insight-head" }, "KEY TAKEAWAY"),
      p.text ? E("p", { dangerouslySetInnerHTML: { __html: p.text } }) : E("p", null, p.children));
  }

  // G6 — prominent margin rows. kind ∈ gross|ebitda|net.
  const MARGIN_META = { gross: { color: "#059669", bg: "rgba(5,150,105,.05)" }, ebitda: { color: "#d97706", bg: "rgba(217,119,6,.05)" }, net: { color: "#7c3aed", bg: "rgba(124,58,237,.05)" } };
  const marginRowStyle = (kind) => { const m = MARGIN_META[kind] || MARGIN_META.gross; return { fontSize: "13px", fontWeight: 700, color: m.color, borderLeft: "3px solid " + m.color, background: m.bg }; };
  const marginValueStyle = (kind) => { const m = MARGIN_META[kind] || MARGIN_META.gross; return { fontWeight: 800, fontFamily: MONO, color: m.color }; };
  // Standalone margin row component (table-row) for P&L-style tables.
  function MarginRow({ label, value, kind, span }) {
    const m = MARGIN_META[kind] || MARGIN_META.gross;
    return E("tr", { style: { background: m.bg } },
      E("td", { style: { fontSize: "13px", fontWeight: 700, color: m.color, borderLeft: "3px solid " + m.color, padding: "6px 12px" }, colSpan: span ? span - 1 : 1 }, label),
      E("td", { className: "num", style: { fontSize: "13px", fontWeight: 800, fontFamily: MONO, color: m.color, padding: "6px 12px" } }, value));
  }

  window.PerduraPageKit = {
    INK, MUTE, POS, NEG, LINE, CARD, PALETTE, RANK_COLORS, MONO, useState, useEffect, useMemo,
    moneyStr, money, pct, statusDot, fmtCompact, Hero, Shell, SectionHead, Kpi, Card, Commentary, CFOCommentaryPanel,
    Bars, Line, Donut, Spark, RankedList, ExceptionCard, BridgeChart, MultiSeriesBarChart,
    ttm, catBreakdown, hhi,
    // G1–G6
    PERIOD_MODES, CMP_MODES, anchorFromPlH, resolvePeriod, comparePeriod, plIdxRange, sumIdx, sliceIdx,
    windowTxns, catBreakdownWindow, usePeriodState, PeriodControls, PeriodSelector, CompareSelector, Breadcrumb, BackButton,
    KeyTakeaway, marginRowStyle, marginValueStyle, MarginRow, monthIdx,
  };
})();
