/* Performance — platform revenue + impressions desk (Assigns window.LedgerPerformance)

   Ported from the standalone Platform Performance Desk
   (Projects/analytics-unification/apps/platform-performance/template.html) so the
   board lives behind Ledger's Google sign-in and the ledger_access allow-list
   instead of an unauthenticated link.

   TWO PIECES ON PURPOSE.

   `createDesk(root, D)` is the ORIGINAL imperative renderer, kept close to
   verbatim: it owns everything inside one host <div> and talks to the DOM
   directly. React owns the host and the page chrome, and never reaches inside.
   That is deliberate — the renderer is the piece that was verified against
   BigQuery to the cent, and a rewrite into React idioms would have re-earned all
   of that verification for no user-visible gain. Both surfaces stay in sync
   because both read the same payload shape.

   The payload comes from analytics_platform_snapshot() (migration 0087), which
   returns exactly what build.py writes to data.json — dictionary-encoded, dates as
   integer offsets. ~2,800 daily rows arrive in one call and every grain and filter
   is a rollup in the browser, so a cut costs no query.

   The desk is a SNAPSHOT of the warehouse, refreshed nightly (cron -> the
   analytics-sync function). It says its own age in the header, and says so loudly
   when the last refresh failed. */
(function () {
  const MS = 86400000;
  const MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

  // `typical` is the average calendar length of the unit, used only to estimate an
  // "average per unit" when the range holds no complete period of that unit.
  const GRAINS = [
    { k: "day", label: "Daily", unit: "day" },
    { k: "week", label: "Weekly", unit: "week", typical: 7 },
    { k: "month", label: "Monthly", unit: "month", typical: 30.44 },
    { k: "quarter", label: "Quarterly", unit: "quarter", typical: 91.31 },
    { k: "year", label: "Yearly", unit: "year", typical: 365.25 },
    { k: "all", label: "Whole range" },
  ];

  // Relative ranges are anchored on the LAST DAY IN THE DATA, not on today. The
  // Scripps feed lands at a fixed D-3 lag, so "last 7 days" ending today would
  // always be three days short and read as a collapse.
  const RANGES = [
    { k: "all", label: "All data" },
    { k: "7d", label: "Last 7 days", n: 7 },
    { k: "30d", label: "Last 30 days", n: 30 },
    { k: "90d", label: "Last 90 days", n: 90 },
    { k: "mtd", label: "Month to date" },
    { k: "qtd", label: "Quarter to date" },
    { k: "ytd", label: "Year to date" },
    { k: "custom", label: "Custom range…" },
  ];

  const COMPARES = [
    { k: "prev", label: "Prev period" },
    { k: "avg", label: "Range avg" },
    { k: "off", label: "Off" },
  ];

  const TBL_VIEWS = [
    { k: "totals", label: "Totals" },
    { k: "pivot", label: "Pivot" },
    { k: "detail", label: "Detail" },
  ];

  // `dims: true` needs the backfill/inventory fields (migration 0120). A payload
  // without them still renders; these options just don't appear.
  const BREAKOUTS = [
    { k: "platform", label: "Platform" },
    { k: "both", label: "Platform × Brand" },
    { k: "psupply", label: "Platform × Supply", dims: true },
    { k: "supply", label: "Supply", dims: true },
    { k: "inventory", label: "Inventory", dims: true },
    { k: "content", label: "Content", sub: true },
  ];
  // "Backfill" here is the Scripps PLATFORM-label backfill ("Samsung TV Plus
  // (Backfill)"), not the $866 demand-side `line_item_type = Backfill`. See 0120.
  const SUPPLY = ["Direct", "Platform backfill"];
  const SUPPLY_KEYS = ["direct", "backfill"];
  const INV_LABEL = (v) => ({ video: "Video", display: "Display", undefined: "Unspecified",
    unknown: "Unknown" }[v] || v);
  // Scripps INVENTORY_SUBTYPE (migration 0152), delivered from the 2026-10-12 file.
  // "unspecified" is every earlier row: the feed did not split live from VOD then,
  // and that video was not all live, so it is labelled as unsplit, never as Live.
  const SUB_LABEL = (v) => ({ live: "Live", vod: "VOD", display: "Display",
    interstitial: "Interstitial", unspecified: "Not split" }[v] || v);

  // Categorical slots for the share donuts (validated palette, see perf.css
  // --s1..--s8). Eight is the ceiling: a ninth series folds into "Other", it never
  // gets a generated colour.
  const SLOTS = 8;

  const SKELETON = `
    <div class="controls">
      <div class="ctl-row">
        <div class="ctl"><div class="ctl-label">Period</div>
          <div class="seg" data-el="segGrain" role="group" aria-label="Time grain"></div></div>
        <div class="ctl"><div class="ctl-label">Date range</div>
          <div class="range" data-el="range"></div></div>
        <div class="ctl"><div class="ctl-label">Compare to</div>
          <div class="seg" data-el="segCmp" role="group" aria-label="Comparison"></div></div>
        <div class="ctl"><div class="ctl-label">Brand</div>
          <div class="ctl-set" data-el="setBrand"></div></div>
        <div class="ctl"><div class="ctl-label">Platform class</div>
          <div class="ctl-set" data-el="setClass"></div></div>
        <div class="ctl" data-el="ctlSupply"><div class="ctl-label">Supply</div>
          <div class="ctl-set" data-el="setSupply"></div></div>
        <div class="ctl" data-el="ctlInv"><div class="ctl-label">Inventory</div>
          <div class="ctl-set" data-el="setInv"></div></div>
        <div class="ctl" data-el="ctlSub"><div class="ctl-label">Content</div>
          <div class="ctl-set" data-el="setSub"></div></div>
        <div class="ctl"><div class="ctl-label">Platforms</div>
          <div class="picker">
            <button class="picker-btn" data-el="pickBtn" aria-expanded="false">
              <span data-el="pickLabel">All platforms</span><span class="caret">▼</span>
            </button>
            <div class="picker-pop" data-el="pickPop" hidden></div>
          </div></div>
        <div class="ctl"><div class="ctl-label">Break out by</div>
          <div class="seg" data-el="segBreak" role="group" aria-label="Breakout"></div></div>
      </div>
    </div>

    <div class="kpis" data-el="kpis"></div>

    <section data-el="secTrend">
      <div class="sec-head">
        <div><h2>Revenue &amp; impressions over time</h2>
          <div class="sec-sub" data-el="trendSub"></div></div>
        <div class="legend">
          <span><i class="sw-rev"></i>Revenue</span>
          <span><i class="sw-imp"></i>Impressions</span>
          <span><i class="sw-bench"></i>Range avg</span>
          <span><i class="sw-part"></i>Partial period</span>
        </div>
      </div>
      <div data-el="trendNote"></div>
      <div class="chart-scroll" data-el="trendWrap"></div>
    </section>

    <section>
      <div class="sec-head">
        <div><h2>Share of selection</h2>
          <div class="sec-sub" data-el="shareSub"></div></div>
        <div class="seg" data-el="segShare" role="group" aria-label="Share by"></div>
      </div>
      <div data-el="shareBody"></div>
    </section>

    <section>
      <div class="sec-head">
        <div><h2>Performance by platform</h2>
          <div class="sec-sub" data-el="platSub"></div></div>
        <div class="seg" data-el="segView" role="group" aria-label="Platform view"></div>
      </div>
      <div data-el="platBody"></div>
    </section>

    <section>
      <div class="sec-head"><div><h2>Table</h2>
        <div class="sec-sub" data-el="tblSub"></div></div>
        <div class="tbl-ctls">
          <div class="seg" data-el="segPiv" role="group" aria-label="Pivot metric"></div>
          <div class="seg" data-el="segTbl" role="group" aria-label="Table view"></div>
        </div></div>
      <div class="tbl-scroll" data-el="tblWrap"></div>
    </section>

    <div class="foot" data-el="foot"></div>
    <div class="ppd-tip" data-el="tip" hidden></div>`;

  /* ============================================================
     The desk. One instance owns `root`; destroy() detaches it.
     ============================================================ */
  function createDesk(root, D) {
    root.innerHTML = SKELETON;
    const $ = (n) => root.querySelector(`[data-el="${n}"]`);

    const D0 = Date.parse(D.d0 + "T00:00:00Z");
    const LAST = D.ndays - 1;
    const COVERED = new Set(D.covered);
    const dayMs = (i) => D0 + i * MS;
    const idxOfMs = (ms) => Math.round((ms - D0) / MS);
    const isoOf = (i) => new Date(dayMs(i)).toISOString().slice(0, 10);
    const idxOfIso = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s || "")
      ? idxOfMs(Date.parse(s + "T00:00:00Z")) : null;
    const clampIdx = (i) => Math.max(0, Math.min(LAST, i));
    const CLASSES = [...new Set(D.classes)];
    // Phase 2 fields ride at the END of each fact: [.., is_backfill 0|1, inventory idx].
    const HAS_DIMS = Array.isArray(D.inventories) && D.facts.length > 0 && D.facts[0].length >= 7;
    const INVS = HAS_DIMS ? D.inventories : [];
    const bfOf = (f) => (HAS_DIMS ? f[5] : 0);
    const invOf = (f) => (HAS_DIMS ? f[6] : -1);
    // 0152 field rides after those: [.., inventory idx, subtype idx].
    const HAS_SUB = HAS_DIMS && Array.isArray(D.subtypes) && D.facts[0].length >= 8;
    const SUBS = HAS_SUB ? D.subtypes : [];
    const subOf = (f) => (HAS_SUB ? f[7] : -1);
    const SUB_FROM = HAS_SUB && D.subtype_from ? idxOfIso(String(D.subtype_from).slice(0, 10)) : null;
    const BREAKS = BREAKOUTS.filter((b) => (HAS_DIMS || !b.dims) && (HAS_SUB || !b.sub));

    // Prefix sum of reported days, so "how many days in [a, b] carry data" is O(1).
    // Coverage is asked per period, per previous period and per window on every render.
    const CUM = new Array(D.ndays + 1).fill(0);
    for (let i = 0; i < D.ndays; i++) CUM[i + 1] = CUM[i] + (COVERED.has(i) ? 1 : 0);
    const covCount = (a, b) => {
      a = Math.max(0, a); b = Math.min(LAST, b);
      return b < a ? 0 : CUM[b + 1] - CUM[a];
    };

    const DEFAULTS = () => ({
      grain: "month",
      range: "all",
      from: null,          // custom range bounds, as day indexes into the extract
      to: null,
      compare: "prev",
      brands: new Set(D.brands.keys()),
      classes: new Set(CLASSES),
      platforms: new Set(D.platforms.keys()),
      supply: new Set([0, 1]),
      inv: new Set(INVS.keys()),
      sub: new Set(SUBS.keys()),
      breakout: "platform",
      shareBy: "series",
      view: "ranked",
      tbl: "totals",
      pivotMetric: "rev",
    });
    const state = DEFAULTS();
    const grainDef = () => GRAINS.find((g) => g.k === state.grain);

    /* ---- period bucketing ------------------------------------ */
    const daysInMonth = (y, m) => new Date(Date.UTC(y, m + 1, 0)).getUTCDate();

    /** The calendar bucket a UTC midnight falls in, at the current grain. */
    function bucketOfMs(ms) {
      const d = new Date(ms);
      const y = d.getUTCFullYear(), m = d.getUTCMonth(), dd = d.getUTCDate();
      switch (state.grain) {
        case "day":
          return { key: ms, start: ms, span: 1, label: `${MON[m]} ${dd}, ${y}`, short: `${MON[m]} ${dd}` };
        case "week": {
          const dow = (d.getUTCDay() + 6) % 7;            // Monday = 0
          const s = ms - dow * MS, sd = new Date(s);
          return { key: s, start: s, span: 7,
            label: `Week of ${MON[sd.getUTCMonth()]} ${sd.getUTCDate()}, ${sd.getUTCFullYear()}`,
            short: `${MON[sd.getUTCMonth()]} ${sd.getUTCDate()}` };
        }
        case "month": {
          const s = Date.UTC(y, m, 1);
          return { key: s, start: s, span: daysInMonth(y, m), label: `${MON[m]} ${y}`, short: `${MON[m]} ${y}` };
        }
        case "quarter": {
          const q = Math.floor(m / 3);
          const s = Date.UTC(y, q * 3, 1), e = Date.UTC(y, q * 3 + 3, 1);
          return { key: s, start: s, span: Math.round((e - s) / MS),
            label: `Q${q + 1} ${y}`, short: `Q${q + 1} ${y}` };
        }
        case "year": {
          const s = Date.UTC(y, 0, 1);
          const span = (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) ? 366 : 365;
          return { key: s, start: s, span, label: String(y), short: String(y) };
        }
        default:
          return { key: 0, start: D0, span: D.ndays, label: "Whole range", short: "Whole range" };
      }
    }
    const bucketOf = (i) => bucketOfMs(dayMs(i));

    let BK = [];
    function rebuildBuckets() {
      BK = new Array(D.ndays);
      for (let i = 0; i < D.ndays; i++) BK[i] = bucketOf(i);
    }

    /* ---- date range ------------------------------------------ */
    /** [lo, hi] day indexes of the selected range, always inside the extract. */
    function rangeBounds() {
      const ld = new Date(dayMs(LAST));
      const Y = ld.getUTCFullYear(), M = ld.getUTCMonth();
      const r = RANGES.find((x) => x.k === state.range) || RANGES[0];
      let lo = 0, hi = LAST;
      if (r.n) lo = LAST - r.n + 1;
      else if (r.k === "mtd") lo = idxOfMs(Date.UTC(Y, M, 1));
      else if (r.k === "qtd") lo = idxOfMs(Date.UTC(Y, M - (M % 3), 1));
      else if (r.k === "ytd") lo = idxOfMs(Date.UTC(Y, 0, 1));
      else if (r.k === "custom") {
        lo = state.from ?? 0; hi = state.to ?? LAST;
        if (lo > hi) [lo, hi] = [hi, lo];
      }
      return { lo: clampIdx(lo), hi: clampIdx(hi) };
    }

    /** Reported days of a bucket inside [lo, hi], against its full calendar span.
        A period the range cuts through therefore reads as partial — honestly. */
    function coverageOf(b, lo, hi) {
      if (state.grain === "all") { const n = covCount(lo, hi); return { have: n, span: n }; }
      const a = idxOfMs(b.start);
      return { have: covCount(Math.max(a, lo), Math.min(a + b.span - 1, hi)), span: b.span };
    }
    /** Same, ignoring the range — for the period BEFORE one in view. */
    function histCoverage(b) {
      const a = idxOfMs(b.start);
      return { have: covCount(a, a + b.span - 1), span: b.span };
    }
    function prevBucket(b) {
      if (state.grain === "all") return null;
      const pb = bucketOfMs(b.start - MS);
      return histCoverage(pb).have ? pb : null;
    }

    /* ---- selection ------------------------------------------- */
    const passes = (f) =>
      state.brands.has(f[1]) && state.platforms.has(f[2]) && state.classes.has(D.classes[f[2]])
      && state.supply.has(bfOf(f)) && (!HAS_DIMS || state.inv.has(f[6]))
      && (!HAS_SUB || state.sub.has(f[7]));

    /* A series key names the dimensions it was cut on — "p:3", "p:3|b:1",
       "p:3|s:1", "s:1", "v:2", "c:1" — so a click can filter on exactly those. */
    function seriesKey(f) {
      switch (state.breakout) {
        case "both": return `p:${f[2]}|b:${f[1]}`;
        case "psupply": return `p:${f[2]}|s:${bfOf(f)}`;
        case "supply": return `s:${bfOf(f)}`;
        case "inventory": return `v:${invOf(f)}`;
        case "content": return `c:${subOf(f)}`;
        default: return `p:${f[2]}`;
      }
    }
    const parseKey = (k) => Object.fromEntries(String(k).split("|").map((x) => [x[0], +x.slice(2)]));
    function seriesName(k) {
      const o = parseKey(k);
      if (o.p !== undefined) {
        const tag = o.b !== undefined ? D.brands[o.b] : o.s !== undefined ? SUPPLY[o.s] : null;
        return { name: D.platforms[o.p], tag, sub: (tag ? tag + " · " : "") + D.classes[o.p], cls: D.classes[o.p] };
      }
      if (o.s !== undefined) return { name: SUPPLY[o.s], tag: null, sub: "supply", cls: null };
      if (o.c !== undefined) return { name: SUB_LABEL(SUBS[o.c] ?? "unspecified"), tag: null, sub: "content", cls: null };
      return { name: INV_LABEL(INVS[o.v] ?? "unknown"), tag: null, sub: "inventory", cls: null };
    }
    /** Narrow every filter to the dimensions a series key names. */
    function filterTo(k) {
      const o = parseKey(k);
      if (o.p !== undefined) state.platforms = new Set([o.p]);
      if (o.b !== undefined) state.brands = new Set([o.b]);
      if (o.s !== undefined) state.supply = new Set([o.s]);
      if (o.v !== undefined && o.v >= 0) state.inv = new Set([o.v]);
      if (o.c !== undefined && o.c >= 0) state.sub = new Set([o.c]);
    }

    /** The single rollup every render reads from.

        Two passes over the same filtered rows: everything in range builds the
        periods and series on screen; EVERY row (range ignored) builds `hist` and
        `shist`, because the period before the first one in view — the thing a
        % change compares against — usually sits outside the range. */
    function compute() {
      const { lo, hi } = rangeBounds();
      const len = hi - lo + 1;
      const plo = lo - len, phi = lo - 1;
      // A prior window only counts if all of it lies inside the extract.
      const hasPrior = state.range !== "all" && plo >= 0;

      const rows = [];
      const periods = new Map();
      const series = new Map();
      const hist = new Map();
      const shist = new Map();
      const days = new Set();
      let rev = 0, imps = 0, prRev = 0, prImps = 0;
      const supplyMix = [{ rev: 0, imps: 0 }, { rev: 0, imps: 0 }];
      const invMix = new Map();
      const subMix = new Map();
      const sPrior = new Map();

      for (const f of D.facts) {
        if (!passes(f)) continue;
        const b = BK[f[0]], sk = seriesKey(f);

        let h = hist.get(b.key);
        if (!h) hist.set(b.key, h = { rev: 0, imps: 0 });
        h.rev += f[4]; h.imps += f[3];
        let sh = shist.get(sk);
        if (!sh) shist.set(sk, sh = new Map());
        let shp = sh.get(b.key);
        if (!shp) sh.set(b.key, shp = { rev: 0, imps: 0 });
        shp.rev += f[4]; shp.imps += f[3];

        if (hasPrior && f[0] >= plo && f[0] <= phi) {
          prRev += f[4]; prImps += f[3];
          const sp = sPrior.get(sk) || { rev: 0, imps: 0 };
          sp.rev += f[4]; sp.imps += f[3]; sPrior.set(sk, sp);
        }
        if (f[0] < lo || f[0] > hi) continue;

        rows.push(f);
        rev += f[4]; imps += f[3]; days.add(f[0]);
        const bf = bfOf(f), iv = invOf(f);
        supplyMix[bf].rev += f[4]; supplyMix[bf].imps += f[3];
        const im = invMix.get(iv) || { rev: 0, imps: 0 };
        im.rev += f[4]; im.imps += f[3]; invMix.set(iv, im);
        const su = subOf(f), um = subMix.get(su) || { rev: 0, imps: 0 };
        um.rev += f[4]; um.imps += f[3]; subMix.set(su, um);

        let p = periods.get(b.key);
        if (!p) periods.set(b.key, p = { b, rev: 0, imps: 0, bfRev: 0, bfImps: 0, days: new Set() });
        p.rev += f[4]; p.imps += f[3]; p.days.add(f[0]);
        if (bf) { p.bfRev += f[4]; p.bfImps += f[3]; }

        let s = series.get(sk);
        if (!s) series.set(sk, s = { rev: 0, imps: 0, days: new Set(), per: new Map() });
        s.rev += f[4]; s.imps += f[3]; s.days.add(f[0]);

        let sp = s.per.get(b.key);
        if (!sp) s.per.set(b.key, sp = { rev: 0, imps: 0 });
        sp.rev += f[4]; sp.imps += f[3];
      }

      const plist = [...periods.values()].sort((a, b) => a.b.key - b.b.key);
      for (const p of plist) {
        p.cov = coverageOf(p.b, lo, hi);
        p.prevB = prevBucket(p.b);
        p.prevCov = p.prevB ? histCoverage(p.prevB) : null;
        p.prev = p.prevB ? (hist.get(p.prevB.key) || { rev: 0, imps: 0 }) : null;
      }
      const slist = [...series.entries()].map(([k, v]) => ({ k, ...v, ...seriesName(k),
        hist: shist.get(k), prior: sPrior.get(k) || { rev: 0, imps: 0 } }))
        .sort((a, b) => b.rev - a.rev);

      const covDays = covCount(lo, hi);
      return { rows, periods: plist, series: slist, days, rev, imps, lo, hi, len, covDays, supplyMix, invMix, subMix,
        rate: covDays ? rev / covDays : 0, irate: covDays ? imps / covDays : 0,
        prior: hasPrior ? { rev: prRev, imps: prImps, have: covCount(plo, phi) } : null };
    }

    /* ---- % change -------------------------------------------
       Two periods are compared on raw totals only when BOTH are complete. If either
       is partial — the current month, a period the range cuts, the first quarter the
       feed started in — they are compared on their daily rate instead, and the figure
       is marked. Otherwise the running month always reads as a collapse against the
       last full one. */
    function chg(cur, have, span, prev, phave, pspan) {
      if (prev == null || !have || !phave || prev <= 0) return null;
      if (have === span && phave === pspan) return { v: cur / prev - 1, perDay: false };
      return { v: (cur / have) / (prev / phave) - 1, perDay: true };
    }
    /** A period's change under the current Compare-to setting. `avgRate` is the
        per-reported-day average of the same measure across the range. */
    function cmp(cur, cov, prev, prevCov, avgRate) {
      if (state.compare === "off") return null;
      if (state.compare === "avg") {
        return avgRate > 0 && cov.have ? { v: (cur / cov.have) / avgRate - 1, perDay: true } : null;
      }
      if (!prevCov) return null;
      return chg(cur, cov.have, cov.span, prev, prevCov.have, prevCov.span);
    }
    const periodCmp = (p, m, key) =>
      cmp(p[key], p.cov, p.prev ? p.prev[key] : null, p.prevCov, key === "rev" ? m.rate : m.irate);
    function seriesCmp(s, p, m, key) {
      const v = s.per.get(p.b.key);
      if (!v) return null;
      const pv = p.prevB && s.hist.get(p.prevB.key);
      return cmp(v[key], p.cov, pv ? pv[key] : 0, p.prevCov, m.covDays ? s[key] / m.covDays : 0);
    }
    const cmpNoun = (p) => state.compare === "avg" ? "range avg"
      : p && p.prevB ? p.prevB.short : "prev period";

    /* ---- formatting ------------------------------------------ */
    const money = (v) => v >= 1e6 ? "$" + (v / 1e6).toFixed(2) + "M"
      : v >= 1e4 ? "$" + (v / 1e3).toFixed(0) + "K"
      : v >= 1e3 ? "$" + (v / 1e3).toFixed(1) + "K"
      : "$" + v.toFixed(0);
    const num = (v) => v >= 1e6 ? (v / 1e6).toFixed(1) + "M"
      : v >= 1e3 ? (v / 1e3).toFixed(0) + "K" : String(Math.round(v));
    const usd = (v) => "$" + v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    const int = (v) => Math.round(v).toLocaleString("en-US");
    const cpm = (r, i) => i > 0 ? "$" + (r / i * 1000).toFixed(2) : "—";
    const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
    const splitUnit = (s) => { const m = String(s).match(/^(.*?)([KM])?$/); return [m[1], m[2] || ""]; };
    const shortDay = (i) => { const d = new Date(dayMs(i)); return `${MON[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`; };
    const pctTxt = (v) => (v >= 0 ? "+" : "−") + Math.abs(v * 100).toFixed(1) + "%";
    const share = (v, t) => t > 0 ? (v / t * 100).toFixed(1) + "%" : "—";

    /** A change as a direction + sign + number, never colour alone. */
    function dl(c) {
      if (!c || !isFinite(c.v)) return `<span class="dl na">—</span>`;
      const pct = c.v * 100, flat = Math.abs(pct) < 0.05;
      const cls = flat ? "flat" : pct > 0 ? "up" : "down";
      const arrow = flat ? "■" : pct > 0 ? "▲" : "▼";
      return `<span class="dl ${cls}"${c.perDay ? ` title="Compared per reported day — a partial period is involved"` : ""}>`
        + `${arrow} ${Math.abs(pct).toFixed(1)}%${c.perDay ? "<sup>*</sup>" : ""}</span>`;
    }

    /* ---- controls -------------------------------------------- */
    function segment(el, items, get, set) {
      el.innerHTML = items.map((o) =>
        `<button type="button" data-k="${o.k}" aria-pressed="${get() === o.k}">${o.label}</button>`).join("");
      el.onclick = (e) => {
        const b = e.target.closest("button"); if (!b) return;
        set(b.dataset.k); render();
      };
    }

    function chips(el, items, sel, onToggle) {
      el.innerHTML = items.map((o) =>
        `<button type="button" class="chip" data-k="${o.k}" aria-pressed="${sel.has(o.raw)}">
           <span class="dot"></span>${esc(o.label)}</button>`).join("");
      el.onclick = (e) => {
        const b = e.target.closest("button"); if (!b) return;
        const item = items.find((o) => String(o.k) === b.dataset.k);
        onToggle(item.raw);
        render();
      };
    }

    /** Toggle within a set, but never let it empty out — re-selecting all is the
        useful reading of "I just turned off the last one". */
    function toggleIn(set, v, all) {
      if (set.has(v)) set.delete(v); else set.add(v);
      if (set.size === 0) all.forEach((x) => set.add(x));
    }

    function buildControls() {
      segment($("segGrain"), GRAINS, () => state.grain, (k) => { state.grain = k; rebuildBuckets(); });
      segment($("segCmp"), COMPARES, () => state.compare, (k) => { state.compare = k; });
      segment($("segBreak"), BREAKS, () => state.breakout, (k) => { state.breakout = k; });
      const shareSeg = $("segShare");
      shareSeg.hidden = !HAS_DIMS;
      if (HAS_DIMS) {
        segment(shareSeg, [{ k: "series", label: "By breakout" }, { k: "supply", label: "Supply" },
          { k: "inventory", label: "Inventory" }, ...(HAS_SUB ? [{ k: "content", label: "Content" }] : [])],
          () => state.shareBy, (k) => { state.shareBy = k; });
      }
      $("ctlSupply").hidden = !HAS_DIMS;
      $("ctlInv").hidden = !HAS_DIMS;
      if (HAS_DIMS) {
        chips($("setSupply"), SUPPLY.map((l, i) => ({ k: i, raw: i, label: l })),
          state.supply, (v) => toggleIn(state.supply, v, [0, 1]));
        chips($("setInv"), INVS.map((v, i) => ({ k: i, raw: i, label: INV_LABEL(v) })),
          state.inv, (v) => toggleIn(state.inv, v, INVS.map((_, i) => i)));
      }
      $("ctlSub").hidden = !HAS_SUB;
      if (HAS_SUB) {
        // Live and VOD first: they are the split people filter on.
        const order = ["live", "vod", "display", "interstitial", "unspecified"];
        const ix = SUBS.map((v, i) => i).sort((a, b) =>
          (order.indexOf(SUBS[a]) + 99) % 99 - (order.indexOf(SUBS[b]) + 99) % 99 || a - b);
        chips($("setSub"), ix.map((i) => ({ k: i, raw: i, label: SUB_LABEL(SUBS[i]) })),
          state.sub, (v) => toggleIn(state.sub, v, SUBS.map((_, i) => i)));
      }
      segment($("segView"), [{ k: "ranked", label: "Ranked" }, { k: "trends", label: "Trends" }],
        () => state.view, (k) => { state.view = k; });
      segment($("segTbl"), TBL_VIEWS, () => state.tbl, (k) => { state.tbl = k; });
      const piv = $("segPiv");
      piv.hidden = state.tbl !== "pivot";
      segment(piv, [{ k: "rev", label: "Revenue" }, { k: "imps", label: "Impressions" }],
        () => state.pivotMetric, (k) => { state.pivotMetric = k; });
      chips($("setBrand"), D.brands.map((b, i) => ({ k: i, raw: i, label: b })),
        state.brands, (v) => toggleIn(state.brands, v, D.brands.map((_, i) => i)));
      chips($("setClass"), CLASSES.map((c) => ({ k: c, raw: c, label: c })),
        state.classes, (v) => toggleIn(state.classes, v, CLASSES));
      buildRange();
      buildPicker();
    }

    /** Preset dropdown; two date inputs appear for a custom range. Inputs commit
        on `change`, so re-rendering the controls never interrupts typing. */
    function buildRange() {
      const el = $("range");
      const { lo, hi } = rangeBounds();
      const n = hi - lo + 1;
      const custom = state.range === "custom";
      el.innerHTML = `<select class="range-sel" data-el="rangeSel" aria-label="Date range">
          ${RANGES.map((r) => `<option value="${r.k}"${r.k === state.range ? " selected" : ""}>${r.label}</option>`).join("")}
        </select>
        ${custom ? `<span class="range-dates">
          <input type="date" data-k="from" aria-label="From" min="${D.d0}" max="${D.d1}" value="${isoOf(lo)}">
          <span class="range-dash">–</span>
          <input type="date" data-k="to" aria-label="To" min="${D.d0}" max="${D.d1}" value="${isoOf(hi)}">
        </span>` : ""}
        <span class="range-note">${shortDay(lo)} – ${shortDay(hi)} · ${n} day${n === 1 ? "" : "s"}</span>`;

      el.querySelector("select").onchange = (e) => {
        const v = e.target.value;
        if (v === "custom" && state.range !== "custom") { state.from = lo; state.to = hi; }
        state.range = v;
        render();
      };
      el.querySelectorAll("input[type=date]").forEach((inp) => {
        inp.onchange = () => {
          const i = idxOfIso(inp.value);
          if (i === null) return;
          state[inp.dataset.k] = clampIdx(i);
          render();
        };
      });
    }

    function buildPicker() {
      const btn = $("pickBtn"), pop = $("pickPop"), lab = $("pickLabel");
      const n = state.platforms.size, total = D.platforms.length;
      lab.textContent = n === total ? `All ${total} platforms`
        : n === 1 ? D.platforms[[...state.platforms][0]] : `${n} of ${total} platforms`;

      // Revenue per platform across the whole extract — orders the list usefully.
      const tot = new Map();
      for (const f of D.facts) tot.set(f[2], (tot.get(f[2]) || 0) + f[4]);

      let html = `<div class="picker-tools">
          <button type="button" data-all="1">Select all</button>
          <button type="button" data-all="0">Clear</button></div>`;
      for (const c of CLASSES) {
        const idx = D.platforms.map((_, i) => i)
          .filter((i) => D.classes[i] === c)
          .sort((a, b) => (tot.get(b) || 0) - (tot.get(a) || 0));
        if (!idx.length) continue;
        html += `<div class="pgroup">${esc(c)}</div>`;
        for (const i of idx) {
          html += `<label class="popt"><input type="checkbox" data-i="${i}"
            ${state.platforms.has(i) ? "checked" : ""}><span>${esc(D.platforms[i])}</span>
            <span class="pv">${money(tot.get(i) || 0)}</span></label>`;
        }
      }
      pop.innerHTML = html;

      btn.onclick = (e) => {
        e.stopPropagation();
        const open = pop.hidden;
        // Decide which edge to hang the popup from BEFORE showing it. The control
        // usually lands near the right of the pane, and a left-anchored 330px list
        // there runs off screen — reachable only by scrolling the page sideways,
        // scrollbar and all. Measured rather than assumed, so it is right at any width.
        if (open) {
          const r = btn.getBoundingClientRect();
          const w = pop.offsetWidth || 330;
          pop.classList.toggle("picker-pop--flip", r.left + w > window.innerWidth - 16);
        }
        pop.hidden = !open; btn.setAttribute("aria-expanded", String(open));
      };
      pop.onclick = (e) => {
        e.stopPropagation();
        const all = e.target.closest("[data-all]");
        if (all) {
          state.platforms.clear();
          if (all.dataset.all === "1") D.platforms.forEach((_, i) => state.platforms.add(i));
          render(); return;
        }
        const cb = e.target.closest("input[data-i]");
        if (cb) {
          const i = +cb.dataset.i;
          if (cb.checked) state.platforms.add(i); else state.platforms.delete(i);
          render();
        }
      };
    }

    const closePicker = () => {
      const pop = $("pickPop");
      if (pop && !pop.hidden) { pop.hidden = true; $("pickBtn").setAttribute("aria-expanded", "false"); }
    };
    document.addEventListener("click", closePicker);

    /* ============================================================
       URL state — every cut is a link, so "Pluto, weekly" can be pasted
       to someone rather than described as a sequence of clicks.

       Namespaced `#perf:` because this is one tab of an app: app.jsx reads
       the prefix to open this tab on load, and Supabase's own `#access_token`
       must never be mistaken for a saved cut.

       Both directions are best-effort. history.replaceState throws SecurityError
       on an opaque origin, and that once rendered the whole page blank — link
       sharing is a convenience, so it degrades to nothing instead.

       A custom range is stored as ISO dates, not day indexes: indexes are offsets
       from the extract's first day, which moves when the snapshot grows backwards.
       ============================================================ */
    const HASH_PREFIX = "#perf:";
    let hashOk = true;

    function writeHash() {
      if (!hashOk) return;
      try {
        const p = new URLSearchParams();
        if (state.grain !== "month") p.set("g", state.grain);
        if (state.range !== "all") p.set("r", state.range);
        if (state.range === "custom") {
          const { lo, hi } = rangeBounds();
          p.set("f", isoOf(lo)); p.set("t", isoOf(hi));
        }
        if (state.compare !== "prev") p.set("cmp", state.compare);
        if (state.view !== "ranked") p.set("v", state.view);
        if (state.breakout !== "platform") p.set("x", state.breakout);
        if (state.shareBy !== "series") p.set("sh", state.shareBy);
        // Supply and inventory by NAME: inventory indexes are positions in a list the
        // next refresh may extend.
        if (state.supply.size !== 2) p.set("s", [...state.supply].map((i) => SUPPLY_KEYS[i]).join(","));
        if (HAS_DIMS && state.inv.size !== INVS.length) p.set("i", [...state.inv].map((i) => INVS[i]).join(","));
        if (HAS_SUB && state.sub.size !== SUBS.length) p.set("ct", [...state.sub].map((i) => SUBS[i]).join(","));
        if (state.tbl !== "totals") p.set("tv", state.tbl);
        if (state.pivotMetric !== "rev") p.set("pm", state.pivotMetric);
        if (state.brands.size !== D.brands.length) p.set("b", [...state.brands].join(","));
        if (state.classes.size !== CLASSES.length) p.set("c", [...state.classes].join(","));
        if (state.platforms.size !== D.platforms.length) p.set("p", [...state.platforms].join(","));
        const s = p.toString();
        history.replaceState(null, "", s ? HASH_PREFIX + s : location.pathname + location.search + "#perf");
      } catch (e) { hashOk = false; }
    }

    function readHash() {
      // A link is the WHOLE cut: start from defaults, so pasting one into a tab
      // that already has filters applied shows exactly the linked view, not a merge.
      Object.assign(state, DEFAULTS());
      try {
        const h = location.hash || "";
        if (!h.startsWith(HASH_PREFIX)) { rebuildBuckets(); return; }
        const p = new URLSearchParams(h.slice(HASH_PREFIX.length));
        const nums = (v) => v.split(",").map(Number).filter((n) => Number.isInteger(n));
        if (GRAINS.some((g) => g.k === p.get("g"))) state.grain = p.get("g");
        if (RANGES.some((r) => r.k === p.get("r"))) state.range = p.get("r");
        if (state.range === "custom") {
          const f = idxOfIso(p.get("f")), t = idxOfIso(p.get("t"));
          state.from = f === null ? 0 : clampIdx(f);
          state.to = t === null ? LAST : clampIdx(t);
        }
        if (COMPARES.some((c) => c.k === p.get("cmp"))) state.compare = p.get("cmp");
        if (["ranked", "trends"].includes(p.get("v"))) state.view = p.get("v");
        if (BREAKS.some((x) => x.k === p.get("x"))) state.breakout = p.get("x");
        if (HAS_DIMS && ["series", "supply", "inventory"].includes(p.get("sh"))) state.shareBy = p.get("sh");
        if (HAS_SUB && p.get("sh") === "content") state.shareBy = "content";
        const sp = p.get("s");
        if (sp && HAS_DIMS) {
          const v = sp.split(",").map((x) => SUPPLY_KEYS.indexOf(x)).filter((i) => i >= 0);
          if (v.length) state.supply = new Set(v);
        }
        const iv = p.get("i");
        if (iv && HAS_DIMS) {
          const v = iv.split(",").map((x) => INVS.indexOf(x)).filter((i) => i >= 0);
          if (v.length) state.inv = new Set(v);
        }
        const ct = p.get("ct");
        if (ct && HAS_SUB) {
          const v = ct.split(",").map((x) => SUBS.indexOf(x)).filter((i) => i >= 0);
          if (v.length) state.sub = new Set(v);
        }
        if (TBL_VIEWS.some((t) => t.k === p.get("tv"))) state.tbl = p.get("tv");
        if (["rev", "imps"].includes(p.get("pm"))) state.pivotMetric = p.get("pm");

        const b = p.get("b");
        if (b) { const v = nums(b).filter((i) => i >= 0 && i < D.brands.length); if (v.length) state.brands = new Set(v); }
        const c = p.get("c");
        if (c) { const v = c.split(",").filter((x) => CLASSES.includes(x)); if (v.length) state.classes = new Set(v); }
        const pl = p.get("p");
        if (pl) { const v = nums(pl).filter((i) => i >= 0 && i < D.platforms.length); if (v.length) state.platforms = new Set(v); }
      } catch (e) { /* fall through to defaults */ }
      rebuildBuckets();
    }

    /* ---- render ---------------------------------------------- */
    function render() {
      writeHash();
      const m = compute();
      const wasOpen = !$("pickPop").hidden;
      buildControls();
      if (wasOpen) { $("pickPop").hidden = false; $("pickBtn").setAttribute("aria-expanded", "true"); }
      renderKpis(m); renderTrend(m); renderShare(m); renderPlatforms(m); renderTable(m);
    }

    /** Average per unit of the current grain: the mean of the COMPLETE periods in
        range, or — when there is none — the daily average scaled to the unit's
        typical length, labelled as an estimate. */
    function avgPerUnit(m) {
      const g = grainDef();
      if (!g.typical) return null;
      const full = m.periods.filter((p) => p.cov.have === p.cov.span);
      if (full.length) {
        return { rev: full.reduce((a, p) => a + p.rev, 0) / full.length,
          imps: full.reduce((a, p) => a + p.imps, 0) / full.length,
          note: `mean of ${full.length} complete ${g.unit}${full.length === 1 ? "" : "s"}` };
      }
      return { rev: m.rate * g.typical, imps: m.irate * g.typical, est: true,
        note: `est. from daily avg · no complete ${g.unit} in range` };
    }

    function renderKpis(m) {
      const el = $("kpis");
      if (!m.rows.length) { el.innerHTML = ""; return; }
      const partial = m.periods.filter((p) => p.cov.have < p.cov.span).length;
      const g = grainDef();

      const tile = (l, v, s, cls) => {
        const [head, unit] = splitUnit(v);
        return `<div class="kpi${cls ? " " + cls : ""}"><div class="kpi-l">${l}</div>
          <div class="kpi-v">${esc(head)}${unit ? `<span class="u">${unit}</span>` : ""}</div>
          <div class="kpi-s">${s}</div></div>`;
      };
      const vsPrior = (key) => {
        if (!m.prior) return "";
        const c = chg(m[key], m.covDays, m.len, m.prior[key], m.prior.have, m.len);
        return `<br>${dl(c)} vs prior ${m.len} days`;
      };

      const tiles = [
        tile("Revenue", money(m.rev), usd(m.rev) + vsPrior("rev"), "accent"),
        tile("Impressions", num(m.imps), int(m.imps) + vsPrior("imps")),
        tile("CPM", cpm(m.rev, m.imps), "revenue per 1,000 impressions"),
        tile("Avg / day", money(m.rate), `${num(m.irate)} impressions / day<br>over ${m.covDays} reported days`),
      ];

      const au = avgPerUnit(m);
      if (au) tiles.push(tile(`Avg / ${g.unit}`, money(au.rev), `${num(au.imps)} impressions<br>${au.note}`));

      // Tracking: the latest period against the benchmark. A partial latest period
      // (the running month, always, given the feed lag) is projected at its own
      // daily rate to a full period before comparing.
      const latest = m.periods[m.periods.length - 1];
      if (latest && state.grain !== "all") {
        const bench = state.grain === "day" ? m.rate : au.rev;
        const part = latest.cov.have < latest.cov.span;
        const val = part ? latest.rev / latest.cov.have * latest.cov.span : latest.rev;
        if (bench > 0 && latest.cov.have) {
          const d = val / bench - 1;
          tiles.push(tile(`${part ? "Tracking" : "Latest"} · ${esc(latest.b.short)}`, pctTxt(d),
            `${part ? "projected " : ""}${money(val)} vs avg ${money(bench)}`
            + (part ? `<br>${latest.cov.have}/${latest.cov.span} days in`
              + (latest.cov.have < 3 ? ` · <span class="pill part">early read</span>` : "") : ""),
            d >= 0 ? "t-up" : "t-down"));
        }
      }

      // VOD only exists from the day the feed started splitting it, so its share is
      // measured over that stretch of the selection, not diluted by earlier months.
      const vi = SUBS.indexOf("vod");
      if (HAS_SUB && vi >= 0 && state.sub.has(vi) && SUB_FROM !== null && m.hi >= SUB_FROM) {
        let vr = 0, vm = 0, tr = 0, tm = 0;
        for (const f of m.rows) {
          if (f[0] < SUB_FROM) continue;
          tr += f[4]; tm += f[3];
          if (f[7] === vi) { vr += f[4]; vm += f[3]; }
        }
        if (tr > 0 || vm > 0) {
          tiles.push(tile("VOD revenue", money(vr),
            `${share(vr, tr)} of revenue · ${num(vm)} imps · CPM ${cpm(vr, vm)}`
            + `<br>since ${shortDay(Math.max(SUB_FROM, m.lo))}`));
        }
      }

      if (HAS_DIMS && state.supply.size === 2 && m.rev > 0) {
        const bf = m.supplyMix[1];
        tiles.push(tile("Backfill share", share(bf.rev, m.rev),
          `${money(bf.rev)} platform backfill revenue<br>${share(bf.imps, m.imps)} of impressions`));
      }

      tiles.push(tile("Coverage", `${m.days.size}/${m.covDays}`,
        partial ? `<span class="pill part">${partial} partial period${partial === 1 ? "" : "s"}</span>`
          : "days reported · all periods complete"));
      el.innerHTML = tiles.join("");
    }

    function renderTrend(m) {
      const sec = $("secTrend"), wrap = $("trendWrap"), sub = $("trendSub"), note = $("trendNote");

      if (state.grain === "all") { sec.hidden = true; return; }
      sec.hidden = false;

      const P = m.periods;
      sub.textContent = `${P.length} ${grainDef().label.toLowerCase()} periods · ${money(m.rev)} revenue · `
        + `${num(m.imps)} impressions in range. Revenue as bars against the left axis, impressions `
        + `as a line against the right. The dashed mark is the range's daily average scaled to each `
        + `period's reported days.`;

      const partial = P.filter((p) => p.cov.have < p.cov.span);
      note.innerHTML = partial.length
        ? `<div class="note warn"><span><b>${partial.length} partial ${partial.length === 1 ? "period" : "periods"}</b>
           — ${partial.map((p) => esc(p.b.short) + ` (${p.cov.have}/${p.cov.span}d)`).join(", ")}.
           Hatched bars cover fewer days than the period, so they are not comparable to
           the full ones. Nothing is scaled up to fill the gap; % changes involving them
           compare daily rates (marked *).</span></div>`
        : "";

      if (!P.length) { wrap.innerHTML = `<div class="empty">No data in this selection.</div>`; return; }

      const bench = P.map((p) => m.rate * p.cov.have);
      const W = 1200, H = 340, ML = 74, MR = 76, MT = 18, MB = 52;
      const iw = W - ML - MR, ih = H - MT - MB;
      const maxR = Math.max(...P.map((p) => p.rev), ...bench, 1);
      const maxI = Math.max(...P.map((p) => p.imps), 1);
      const bw = iw / P.length;
      const barW = Math.max(1.5, Math.min(bw * 0.72, 62));
      const x = (i) => ML + bw * i + bw / 2;
      const yR = (v) => MT + ih - (v / maxR) * ih;
      const yI = (v) => MT + ih - (v / maxI) * ih;

      // Thin the x labels so they never collide.
      const every = Math.max(1, Math.ceil(P.length / (W / 86)));

      let g = "";
      for (let t = 0; t <= 4; t++) {
        const y = MT + ih - (ih * t / 4);
        g += `<line class="gridline" x1="${ML}" x2="${W - MR}" y1="${y}" y2="${y}"></line>
          <text class="axis-t" x="${ML - 9}" y="${y + 4}" text-anchor="end">${money(maxR * t / 4)}</text>
          <text class="axis-t" x="${W - MR + 9}" y="${y + 4}">${num(maxI * t / 4)}</text>`;
      }

      let bars = "", ticks = "", hits = "", pts = [];
      P.forEach((p, i) => {
        const part = p.cov.have < p.cov.span;
        const h = Math.max(0, ih - (yR(p.rev) - MT));
        bars += `<rect class="bar-rev${part ? " part" : ""}" x="${x(i) - barW / 2}" y="${yR(p.rev)}"
          width="${barW}" height="${h}"></rect>`;
        const tw = Math.min(bw * 0.92, barW + 14);
        if (P.length > 1) {
          ticks += `<line class="bench" x1="${(x(i) - tw / 2).toFixed(1)}" x2="${(x(i) + tw / 2).toFixed(1)}"
            y1="${yR(bench[i]).toFixed(1)}" y2="${yR(bench[i]).toFixed(1)}"></line>`;
        }
        pts.push([x(i), yI(p.imps)]);
        if (i % every === 0) {
          bars += `<text class="axis-t" x="${x(i)}" y="${H - MB + 20}"
            text-anchor="middle">${esc(p.b.short)}</text>`;
        }
        hits += `<rect class="hit" data-i="${i}" x="${ML + bw * i}" y="${MT}"
          width="${bw}" height="${ih}"></rect>`;
      });

      const line = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" ");
      const dots = P.length <= 40
        ? pts.map((p) => `<circle class="dot-imp" cx="${p[0].toFixed(1)}" cy="${p[1].toFixed(1)}" r="3"></circle>`).join("")
        : "";

      // NOTE: the hatch pattern id is `ppdhatch`, not `hatch` — this SVG now shares
      // a document with the rest of Ledger, and a bare `hatch` is exactly the kind of
      // id that collides.
      wrap.innerHTML = `<svg viewBox="0 0 ${W} ${H}" role="img"
        aria-label="Revenue and impressions by ${state.grain}">
        <defs><pattern id="ppdhatch" width="7" height="7" patternUnits="userSpaceOnUse"
            patternTransform="rotate(45)">
            <rect width="7" height="7" fill="var(--card)"></rect>
            <line x1="0" y1="0" x2="0" y2="7" stroke="var(--yellow-deep)" stroke-width="3.5"></line>
          </pattern></defs>
        ${g}
        <line class="gridline" x1="${ML}" x2="${W - MR}" y1="${MT + ih}" y2="${MT + ih}"
          stroke="var(--rule)" stroke-width="1.5"></line>
        ${bars}${ticks}<path class="line-imp" d="${line}"></path>${dots}${hits}
        <text class="axis-t" x="${ML}" y="${MT - 4}" font-weight="700">REVENUE</text>
        <text class="axis-t" x="${W - MR}" y="${MT - 4}" text-anchor="end" font-weight="700">IMPRESSIONS</text>
      </svg>`;

      const svg = wrap.querySelector("svg");
      svg.addEventListener("mousemove", (e) => {
        const t = e.target.closest(".hit");
        if (!t) { hideTip(); return; }
        const i = +t.dataset.i, p = P[i];
        const cR = periodCmp(p, m, "rev"), cI = periodCmp(p, m, "imps");
        const withCmp = state.compare !== "off";

        // Contributors: who made up this period's total when broken out.
        let contrib = "";
        if (m.series.length > 1) {
          const top = m.series.map((s) => ({ s, v: s.per.get(p.b.key) }))
            .filter((o) => o.v).sort((a, b) => b.v.rev - a.v.rev);
          contrib = `<div class="tsep">Top contributors</div>` + top.slice(0, 5).map((o) =>
            `<div class="tr"><span>${esc(o.s.name)}${o.s.tag ? ` · ${esc(o.s.tag)}` : ""}</span>`
            + `<b>${money(o.v.rev)} · ${share(o.v.rev, p.rev)}</b></div>`).join("")
            + (top.length > 5 ? `<div class="tr"><span>+ ${top.length - 5} more</span><b></b></div>` : "");
        }
        showTip(e, `<div class="tt">${esc(p.b.label)}</div>
          <div class="tr"><span>Total revenue</span><b>${usd(p.rev)}${withCmp ? " " + dl(cR) : ""}</b></div>
          <div class="tr"><span>Total impressions</span><b>${int(p.imps)}${withCmp ? " " + dl(cI) : ""}</b></div>
          <div class="tr"><span>CPM</span><b>${cpm(p.rev, p.imps)}</b></div>
          ${HAS_DIMS && state.supply.size === 2 ? `<div class="tr"><span>Platform backfill</span><b>${money(p.bfRev)} · ${share(p.bfRev, p.rev)}</b></div>` : ""}
          <div class="tr"><span>Range avg (${p.cov.have}d)</span><b>${money(bench[i])}</b></div>
          <div class="tr"><span>Days covered</span><b>${p.cov.have} / ${p.cov.span}</b></div>
          ${withCmp ? `<div class="tr tnote"><span>Change vs ${esc(cmpNoun(p))}</span><b></b></div>` : ""}
          ${contrib}`);
      });
      svg.addEventListener("mouseleave", hideTip);
    }

    /* ---- share donuts -----------------------------------------
       Revenue and impressions share side by side, over the same slices in the same
       colours, so the gap between the two (web display: big impression share, small
       revenue share) is visible at a glance. Slices are the top series by revenue in
       the selection; past eight, the tail folds into "Other". */
    function shareItems(m) {
      // Supply and inventory slices keep one colour per value, whatever is filtered.
      if (state.shareBy === "supply") {
        return m.supplyMix.map((v, i) => ({ k: `s:${i}`, name: SUPPLY[i], sub: "supply",
          rev: v.rev, imps: v.imps, color: `var(--s${i + 1})` })).filter((it) => it.rev || it.imps);
      }
      if (state.shareBy === "content") {
        return [...m.subMix.entries()].sort((a, b) => b[1].rev - a[1].rev).map(([i, v]) => ({
          k: `c:${i}`, name: SUB_LABEL(SUBS[i] ?? "unspecified"), sub: "content", rev: v.rev, imps: v.imps,
          color: i >= 0 && i < SLOTS ? `var(--s${i + 1})` : "var(--s-other)" }));
      }
      if (state.shareBy === "inventory") {
        return [...m.invMix.entries()].sort((a, b) => b[1].rev - a[1].rev).map(([i, v]) => ({
          k: `v:${i}`, name: INV_LABEL(INVS[i] ?? "unknown"), sub: "inventory", rev: v.rev, imps: v.imps,
          color: i >= 0 && i < SLOTS ? `var(--s${i + 1})` : "var(--s-other)" }));
      }
      const S = m.series;
      const top = S.length <= SLOTS ? S : S.slice(0, SLOTS - 1);
      const items = top.map((s, i) => ({ k: s.k, name: s.name, sub: s.sub, rev: s.rev, imps: s.imps,
        color: `var(--s${i + 1})` }));
      if (S.length > SLOTS) {
        const rest = S.slice(SLOTS - 1);
        items.push({ k: null, name: `Other (${rest.length})`, sub: "smaller platforms",
          rev: rest.reduce((a, s) => a + s.rev, 0), imps: rest.reduce((a, s) => a + s.imps, 0),
          color: "var(--s-other)" });
      }
      return items;
    }

    function donutSvg(items, key, total, label, fmt) {
      const R = 88, r = 56, C = 100;
      let a0 = -Math.PI / 2, paths = "";
      const pt = (rad, a) => `${(C + rad * Math.cos(a)).toFixed(2)} ${(C + rad * Math.sin(a)).toFixed(2)}`;
      items.forEach((it, i) => {
        const frac = total > 0 ? it[key] / total : 0;
        if (frac <= 0) return;
        // A single slice is a full ring: an arc from a point to itself draws nothing.
        const a1 = a0 + Math.min(frac, 0.99999) * 2 * Math.PI;
        const big = a1 - a0 > Math.PI ? 1 : 0;
        paths += `<path class="slice${it.k === null ? " other" : ""}" data-j="${i}" data-m="${key}"
          fill="${it.color}" d="M${pt(R, a0)} A${R} ${R} 0 ${big} 1 ${pt(R, a1)}
          L${pt(r, a1)} A${r} ${r} 0 ${big} 0 ${pt(r, a0)} Z"></path>`;
        a0 = a1;
      });
      return `<figure class="donut"><svg viewBox="0 0 200 200" role="img" aria-label="${label} share by platform">
          ${paths}
          <text class="donut-v" x="100" y="100" text-anchor="middle">${esc(fmt(total))}</text>
          <text class="donut-l" x="100" y="120" text-anchor="middle">${label}</text>
        </svg></figure>`;
    }

    function renderShare(m) {
      const body = $("shareBody");
      if (!m.series.length) { body.innerHTML = `<div class="empty">No data in this selection.</div>`; $("shareSub").textContent = ""; return; }
      const items = shareItems(m);
      const what = state.shareBy === "supply" ? "Direct vs platform backfill"
        : state.shareBy === "inventory" ? "Video vs display inventory"
        : state.shareBy === "content" ? "Live vs VOD content"
        : `${m.series.length} ${BREAKS.find((b) => b.k === state.breakout).label.toLowerCase()} series`;
      $("shareSub").textContent = `${what} in the selection. The same slices and colours in both rings.`
        + ` Click a row or slice to filter to it.`;

      const rowsHtml = items.map((it, j) => `<tr${it.k !== null ? ` class="pick" data-sk="${it.k}"` : ""}>
          <td class="l"><i class="sw" style="background:${it.color}"></i>${esc(it.name)}
            <span class="pill">${esc(it.sub)}</span></td>
          <td>${money(it.rev)}</td><td class="sh">${share(it.rev, m.rev)}</td>
          <td>${num(it.imps)}</td><td class="sh">${share(it.imps, m.imps)}</td>
          <td>${cpm(it.rev, it.imps)}</td></tr>`).join("");

      body.innerHTML = `<div class="share">
          <div class="donuts">
            ${donutSvg(items, "rev", m.rev, "Revenue", money)}
            ${donutSvg(items, "imps", m.imps, "Impressions", num)}
          </div>
          <div class="share-tbl"><table>
            <thead><tr><th class="l">${state.shareBy === "series" ? "Series" : state.shareBy === "supply" ? "Supply" : state.shareBy === "content" ? "Content" : "Inventory"}</th><th>Revenue</th><th>Share</th>
              <th>Impressions</th><th>Share</th><th>CPM</th></tr></thead>
            <tbody>${rowsHtml}</tbody></table></div>
        </div>`;

      body.onclick = (e) => {
        const row = e.target.closest("[data-sk]");
        const sl = e.target.closest(".slice");
        const sk = row ? row.dataset.sk : sl && items[+sl.dataset.j].k;
        if (!sk) return;
        hideTip();
        filterTo(sk);
        render();
      };
      body.onmousemove = (e) => {
        const sl = e.target.closest(".slice");
        if (!sl) { hideTip(); return; }
        const it = items[+sl.dataset.j];
        showTip(e, `<div class="tt">${esc(it.name)}</div>
          <div class="tr"><span>Revenue</span><b>${usd(it.rev)} · ${share(it.rev, m.rev)}</b></div>
          <div class="tr"><span>Impressions</span><b>${int(it.imps)} · ${share(it.imps, m.imps)}</b></div>
          <div class="tr"><span>CPM</span><b>${cpm(it.rev, it.imps)}</b></div>`);
      };
      body.onmouseleave = hideTip;
    }

    function renderPlatforms(m) {
      const body = $("platBody");
      $("platSub").textContent = state.view === "ranked"
        ? "Every platform in the selection, ranked by revenue. Bars are scaled to the "
          + "largest platform in view — revenue and impressions are scaled independently."
          + (m.prior ? ` Change is revenue vs the prior ${m.len} days.` : "")
        : `One panel per platform, each trending revenue across the selected period. `
          + `A break in the line is a period the platform reported nothing.`;

      if (!m.series.length) { body.innerHTML = `<div class="empty">No data in this selection.</div>`; return; }
      body.innerHTML = state.view === "ranked" ? rankedHtml(m) : smallsHtml(m);
    }

    function rankedHtml(m) {
      const maxR = Math.max(...m.series.map((s) => s.rev), 1);
      const maxI = Math.max(...m.series.map((s) => s.imps), 1);
      const totalDays = m.days.size;
      // The change column exists only when there is a prior window to compare to.
      const withD = !!m.prior && state.compare !== "off";
      const dCell = (s) => withD
        ? `<div class="num c-d">${dl(chg(s.rev, m.covDays, m.len, s.prior.rev, m.prior.have, m.len))}</div>` : "";

      const rows = m.series.map((s) => {
        const gap = s.days.size < totalDays;
        return `<div class="rank-row">
          <div class="rname">${esc(s.name)}<small>${esc(s.sub)}</small></div>
          <div class="c-revbar"><div class="track"><span class="f-rev"
            style="width:${Math.max(1, (s.rev / maxR) * 100).toFixed(2)}%"></span></div></div>
          <div class="num">${usd(s.rev)}</div>
          ${dCell(s)}
          <div class="c-impbar"><div class="track"><span class="f-imp"
            style="width:${Math.max(1, (s.imps / maxI) * 100).toFixed(2)}%"></span></div></div>
          <div class="num dim">${num(s.imps)}</div>
          <div class="num dim c-cpm">${cpm(s.rev, s.imps)}</div>
          <div class="cov ${gap ? "gap" : "full"}"><b>${s.days.size}</b>/${totalDays}d</div>
        </div>`;
      }).join("");

      // The header cells carry the SAME column classes as the data cells. In the
      // standalone page the "Impressions" header had no `c-impbar`, so at any width
      // where the responsive rule hides that column the header kept a cell the rows
      // had dropped and every heading sat one column to the left. Invisible on a
      // 1280px page; immediately visible inside Ledger's narrower content column.
      return `<div class="ranked${withD ? " ranked--d" : ""}">
        <div class="rank-row rank-head">
          <div>Platform</div><div>Revenue</div><div class="num">USD</div>
          ${withD ? `<div class="num c-d">vs prior</div>` : ""}
          <div class="c-impbar">Impressions</div><div class="num">Imps</div>
          <div class="num c-cpm">CPM</div>
          <div class="cov">Days</div>
        </div>${rows}</div>`;
    }

    function smallsHtml(m) {
      const keys = m.periods.map((p) => p.b.key);
      const W = 200, H = 44;

      return `<div class="smalls">` + m.series.map((s) => {
        const vals = keys.map((k) => s.per.has(k) ? s.per.get(k).rev : null);
        const ivals = keys.map((k) => s.per.has(k) ? s.per.get(k).imps : null);
        const maxV = Math.max(...vals.filter((v) => v !== null), 1);
        const maxIv = Math.max(...ivals.filter((v) => v !== null), 1);
        const step = keys.length > 1 ? W / (keys.length - 1) : 0;
        const px = (i) => keys.length > 1 ? i * step : W / 2;

        // Break the path wherever a period has no row, so a gap reads as a gap.
        const path = (arr, mx) => {
          let d = "", pen = false;
          arr.forEach((v, i) => {
            if (v === null) { pen = false; return; }
            const y = H - 4 - (v / mx) * (H - 9);
            d += (pen ? "L" : "M") + px(i).toFixed(1) + " " + y.toFixed(1) + " ";
            pen = true;
          });
          return d.trim();
        };
        // Area only under the first contiguous run so a gap never gets filled across.
        let area = "", run = [];
        const flush = () => {
          if (run.length) {
            area += `M${px(run[0].i).toFixed(1)} ${H - 2} `
              + run.map((r) => `L${px(r.i).toFixed(1)} ${(H - 4 - (r.v / maxV) * (H - 9)).toFixed(1)}`).join(" ")
              + ` L${px(run[run.length - 1].i).toFixed(1)} ${H - 2} Z `;
          }
          run = [];
        };
        vals.forEach((v, i) => { if (v === null) flush(); else run.push({ i, v }); });
        flush();

        const gap = vals.some((v) => v === null);
        return `<div class="sm">
          <div class="sm-h"><span class="sm-n">${esc(s.name)}</span>
            <span class="sm-v">${money(s.rev)}</span></div>
          <div class="sm-m">${esc(s.sub)} · ${num(s.imps)} imps · ${cpm(s.rev, s.imps)} CPM${
            gap ? ` · <span class="pill part">gaps</span>` : ""}</div>
          <svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">
            <path class="sm-area" d="${area}"></path>
            <path class="sm-imp" d="${path(ivals, maxIv)}"></path>
            <path class="sm-line" d="${path(vals, maxV)}"></path>
          </svg></div>`;
      }).join("") + `</div>`;
    }

    /* ---- table: totals · pivot · detail ----------------------- */
    const TBL_CAP = 400, PIV_CAP = 36;

    function renderTable(m) {
      const wrap = $("tblWrap");
      if (!m.rows.length) {
        $("tblSub").textContent = "";
        wrap.innerHTML = `<div class="empty">No data in this selection.</div>`; return;
      }
      if (state.tbl === "pivot") return pivotTable(m, wrap);
      if (state.tbl === "detail") return detailTable(m, wrap);
      return totalsTable(m, wrap);
    }

    const cmpHead = () => state.compare === "avg" ? "vs avg" : "vs prev";
    const perDayFoot = `Changes marked * compare daily rates, because a partial period is involved.`;

    /** One row per period, summed across every partner in the selection. */
    function totalsTable(m, wrap) {
      const withCmp = state.compare !== "off";
      const P = [...m.periods].reverse();
      $("tblSub").textContent = `${P.length} period${P.length === 1 ? "" : "s"}, each summed across `
        + `${m.series.length} ${BREAKS.find((b) => b.k === state.breakout).label.toLowerCase()} series. `
        + (withCmp ? (state.compare === "avg"
          ? "Change is each period's daily rate vs the range's daily average. "
          : "Change is vs the previous period. ") + perDayFoot : "");
      const priorC = (key) => m.prior ? chg(m[key], m.covDays, m.len, m.prior[key], m.prior.have, m.len) : null;

      wrap.innerHTML = `<table><thead><tr>
          <th class="l">Period</th><th>Revenue</th>${withCmp ? `<th>${cmpHead()}</th>` : ""}
          <th>Impressions</th>${withCmp ? `<th>${cmpHead()}</th>` : ""}
          <th>CPM</th><th>Rev / day</th><th>Coverage</th></tr></thead><tbody>` +
        P.map((p) => {
          const part = p.cov.have < p.cov.span;
          return `<tr><td class="l k">${esc(p.b.label)}</td>
            <td>${usd(p.rev)}</td>${withCmp ? `<td>${dl(periodCmp(p, m, "rev"))}</td>` : ""}
            <td>${int(p.imps)}</td>${withCmp ? `<td>${dl(periodCmp(p, m, "imps"))}</td>` : ""}
            <td>${cpm(p.rev, p.imps)}</td><td>${p.cov.have ? money(p.rev / p.cov.have) : "—"}</td>
            <td>${p.cov.have}/${p.cov.span}d ${part ? `<span class="pill part">partial</span>` : ""}</td></tr>`;
        }).join("") + `</tbody><tfoot>
          <tr><td class="l">Total · ${shortDay(m.lo)} – ${shortDay(m.hi)}</td>
            <td>${usd(m.rev)}</td>${withCmp ? `<td title="vs prior ${m.len} days">${m.prior ? dl(priorC("rev")) : ""}</td>` : ""}
            <td>${int(m.imps)}</td>${withCmp ? `<td title="vs prior ${m.len} days">${m.prior ? dl(priorC("imps")) : ""}</td>` : ""}
            <td>${cpm(m.rev, m.imps)}</td><td>${money(m.rate)}</td><td>${m.covDays}d</td></tr>
          <tr class="sub"><td class="l">Avg per reported day</td>
            <td>${usd(m.rate)}</td>${withCmp ? "<td></td>" : ""}
            <td>${int(m.irate)}</td>${withCmp ? "<td></td>" : ""}
            <td></td><td></td><td></td></tr>
        </tfoot></table>`;
    }

    /** Periods across, partners down, totals on both edges. */
    function pivotTable(m, wrap) {
      const key = state.pivotMetric, fmt = key === "rev" ? usd : int;
      const cols = m.periods.slice(-PIV_CAP);
      const capped = m.periods.length > cols.length;
      $("tblSub").textContent = `${key === "rev" ? "Revenue" : "Impressions"} by period, one row per `
        + `${BREAKS.find((b) => b.k === state.breakout).label.toLowerCase()} series. `
        + (capped ? `Showing the ${PIV_CAP} most recent of ${m.periods.length} periods; the Range total column covers all of them. ` : "")
        + (state.compare !== "off" ? `Hover a cell for its change ${state.compare === "avg" ? "vs the platform's range average" : "vs the previous period"}.` : "");

      const cell = (s, p) => {
        const v = s.per.get(p.b.key);
        if (!v) return `<td class="nil">—</td>`;
        const c = seriesCmp(s, p, m, key);
        const t = c && isFinite(c.v) ? ` title="${pctTxt(c.v)} vs ${esc(cmpNoun(p))}${c.perDay ? " (per day)" : ""}"` : "";
        return `<td${t}>${fmt(v[key])}</td>`;
      };
      wrap.innerHTML = `<table class="pivot"><thead><tr><th class="l">Series</th>`
        + cols.map((p) => `<th${p.cov.have < p.cov.span ? ` class="part" title="${p.cov.have}/${p.cov.span} days"` : ""}>${esc(p.b.short)}</th>`).join("")
        + `<th class="tot">Range total</th></tr></thead><tbody>`
        + m.series.map((s) => `<tr><td class="l k">${esc(s.name)}<br><span class="pill">${esc(s.sub)}</span></td>`
          + cols.map((p) => cell(s, p)).join("") + `<td class="tot">${fmt(s[key])}</td></tr>`).join("")
        + `</tbody><tfoot><tr><td class="l">Total</td>`
        + cols.map((p) => `<td>${fmt(p[key])}</td>`).join("")
        + `<td class="tot">${fmt(m[key])}</td></tr></tfoot></table>`;
    }

    /** Every platform-period row, most recent first. */
    function detailTable(m, wrap) {
      const withCmp = state.compare !== "off";
      const rows = [];
      for (const s of m.series) {
        for (const p of m.periods) {
          const v = s.per.get(p.b.key);
          if (!v) continue;
          rows.push({ p, s, ...v });
        }
      }
      rows.sort((a, b) => b.p.b.key - a.p.b.key || b.rev - a.rev);
      const shown = rows.slice(0, TBL_CAP);

      $("tblSub").textContent = (rows.length > TBL_CAP
        ? `${int(rows.length)} platform-period rows; showing the ${TBL_CAP} most recent. `
          + `Narrow the filters or use a coarser period to see the rest. `
        : `${int(rows.length)} platform-period rows. `) + (withCmp ? perDayFoot : "");

      wrap.innerHTML = `<table><thead><tr>
          <th class="l">Period</th><th class="l">Platform</th><th>Revenue</th>
          ${withCmp ? `<th>${cmpHead()}</th>` : ""}<th>Impressions</th>
          <th>CPM</th><th>Coverage</th></tr></thead><tbody>` +
        shown.map((r) => {
          const part = r.p.cov.have < r.p.cov.span;
          return `<tr><td class="l k">${esc(r.p.b.short)}</td>
            <td class="l">${esc(r.s.name)}<br><span class="pill">${esc(r.s.sub)}</span></td>
            <td>${usd(r.rev)}</td>${withCmp ? `<td>${dl(seriesCmp(r.s, r.p, m, "rev"))}</td>` : ""}
            <td>${int(r.imps)}</td><td>${cpm(r.rev, r.imps)}</td>
            <td>${r.p.cov.have}/${r.p.cov.span}d ${part ? `<span class="pill part">partial</span>` : ""}</td>
          </tr>`;
        }).join("") + `</tbody></table>`;
    }

    /* ---- tooltip --------------------------------------------- */
    const tipEl = $("tip");
    function showTip(e, html) {
      tipEl.innerHTML = html; tipEl.hidden = false;
      const r = tipEl.getBoundingClientRect();
      let x = e.clientX + 16, y = e.clientY + 16;
      if (x + r.width > innerWidth - 8) x = e.clientX - r.width - 16;
      if (y + r.height > innerHeight - 8) y = e.clientY - r.height - 16;
      tipEl.style.left = Math.max(8, x) + "px";
      tipEl.style.top = Math.max(8, y) + "px";
    }
    const hideTip = () => { tipEl.hidden = true; };

    function buildFoot() {
      $("foot").innerHTML =
        `Source <code>${esc(D.source || "dwh.scripps_ad_performance_daily")}</code>.
         Scripps-sold ad inventory only. Third-party FAST rev-share (<code>dwh.ctv_revenue</code>,
         monthly, no impressions) is deliberately excluded — unioning it would place a month's
         revenue on a single day and report CPM against impressions that were never counted.
         Revenue is gross.${HAS_DIMS ? ` <b>Platform backfill</b> is inventory Scripps sells into a
         platform's backfill feed (e.g. "Samsung TV Plus (Backfill)"); it is not the demand-side
         "Backfill" line item.` : ""}${HAS_SUB && SUB_FROM !== null ? ` <b>Content</b> (Live / VOD) is
         split by Scripps from ${shortDay(SUB_FROM)} on; earlier video shows as <b>Not split</b>, because
         it was never broken out and was not all live.` : ""} Relative date ranges end on the last day in the data
         (${shortDay(LAST)}), not today, because the feed lands three days behind.
         Changes marked * compare daily rates because a partial period is involved.${hashOk
          ? " The address bar tracks the current cut, so any view can be copied as a link." : ""}`;
    }

    readHash();
    writeHash();          // probes once, so buildFoot knows whether links work
    buildFoot();
    render();

    const onHashChange = () => { readHash(); render(); };
    addEventListener("hashchange", onHashChange);

    return {
      destroy() {
        document.removeEventListener("click", closePicker);
        removeEventListener("hashchange", onHashChange);
        root.innerHTML = "";
      },
    };
  }

  /* ============================================================
     React wrapper — chrome, freshness and the refresh control.
     ============================================================ */
  function fmtDay(s) {
    const d = new Date(s + "T00:00:00Z");
    return `${MON[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
  }
  function fmtWhen(iso) {
    if (!iso) return "never";
    const d = new Date(iso);
    return d.toLocaleString("en-US",
      { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
  }
  const hoursSince = (iso) => iso ? (Date.now() - Date.parse(iso)) / 3600000 : Infinity;

  const THEMES = [
    { k: "auto", label: "Auto", title: "Follow your system appearance" },
    { k: "light", label: "Light", title: "Cream paper" },
    { k: "dark", label: "Dark", title: "Navy ink" },
  ];
  const THEME_KEY = "lc-ppd-theme";

  function readTheme() {
    try {
      const v = window.localStorage.getItem(THEME_KEY);
      return (v === "light" || v === "dark" || v === "auto") ? v : "auto";
    } catch { return "auto"; }   // private mode / storage disabled
  }

  function Performance() {
    const { Button } = window.LawCrimeDesignSystem_e18d68;
    const [data, setData] = React.useState(undefined);   // undefined = loading
    const [err, setErr] = React.useState(null);
    const [busy, setBusy] = React.useState(false);
    const [msg, setMsg] = React.useState(null);
    // Read at first render, not in an effect, so an explicit choice never flashes
    // the other palette. "auto" leaves the attribute off — that is what hands the
    // decision to prefers-color-scheme.
    const [theme, setTheme] = React.useState(readTheme);
    const host = React.useRef(null);

    function pickTheme(k) {
      setTheme(k);
      try { window.localStorage.setItem(THEME_KEY, k); } catch { /* non-fatal */ }
    }

    const load = React.useCallback(async () => {
      setErr(null);
      try { setData(await window.LedgerAPI.analyticsSnapshot()); }
      catch (e) { setErr(e.message || String(e)); setData(null); }
    }, []);

    React.useEffect(() => { load(); }, [load]);
    React.useEffect(() => { setTimeout(() => window.lucide && window.lucide.createIcons(), 20); });

    // Mount the desk once the payload is in. Re-mounts on refresh; destroy()
    // takes its document-level listeners with it.
    React.useEffect(() => {
      if (!data || data.empty || !host.current) return;
      const desk = createDesk(host.current, data);
      return () => desk.destroy();
    }, [data]);

    async function refresh() {
      setBusy(true); setMsg(null); setErr(null);
      try {
        const r = await window.LedgerAPI.refreshAnalytics();
        if (r && r.skipped === "cooldown") {
          setMsg(`Refreshed ${Math.round(r.seconds_since_last / 60)} min ago — try again shortly.`);
        } else {
          setMsg(`Refreshed. ${r && r.pulled_rows ? r.pulled_rows.toLocaleString() + " rows." : ""}`);
          await load();
        }
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusy(false); }
    }

    const stale = data && !data.empty && hoursSince(data.refreshed_at) > 36;
    const failed = data && data.status === "error";

    return (
      <div className="ppd" data-ppd-theme={theme === "auto" ? undefined : theme}>
        <div className="page-head">
          <div>
            <div className="eyebrow">Performance · CTV Revenue</div>
            <h1 className="page-title">CTV Revenue</h1>
          </div>
          <div className="ppd-head-actions">
            <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
              <div className="seg ppd-theme" role="group" aria-label="Theme">
                {THEMES.map((t) => (
                  <button key={t.k} type="button" title={t.title}
                    aria-pressed={theme === t.k} onClick={() => pickTheme(t.k)}>
                    {t.label}
                  </button>
                ))}
              </div>
              <Button variant="secondary" icon="refresh-cw" disabled={busy} onClick={refresh}>
                {busy ? "Refreshing…" : "Refresh"}
              </Button>
            </div>
            {data && !data.empty && (
              <div className="ppd-meta">
                <b>{fmtDay(data.d0)} – {fmtDay(data.d1)}</b><br />
                {data.covered.length} days reported
                {data.ndays - data.covered.length > 0
                  ? <> · <b>{data.ndays - data.covered.length} missing</b></>
                  : " · no missing days"}
                {" "}· {data.platforms.length} platforms · {data.brands.length} brands<br />
                <span className={stale || failed ? "stale" : undefined}>
                  Snapshot as of {fmtWhen(data.refreshed_at)}
                  {failed ? " · last refresh FAILED" : ""}
                </span>
              </div>
            )}
          </div>
        </div>

        {failed && (
          <div className="note warn" style={{ marginBottom: 18 }}>
            <span><b>The last refresh failed</b> — you are looking at the previous snapshot,
              not stale-but-fresh data. {data.message}</span>
          </div>
        )}
        {err && <div className="login__err" style={{ margin: "12px 0" }}>{err}</div>}
        {msg && <div className="ai-saved" style={{ margin: "12px 0" }}>{msg}</div>}

        {data === undefined && <div className="ai-empty">Loading the snapshot…</div>}
        {data && data.empty && (
          <div className="empty" style={{ marginTop: 18 }}>
            No snapshot loaded yet. Press <b>Refresh</b> to pull one from the warehouse.
          </div>
        )}
        <div ref={host} />
      </div>
    );
  }

  window.LedgerPerformance = Performance;
})();
