/* screens-pipeline-summary.jsx — Pipeline Summary Report (print export).

   Built 2026-09-04 from the Claude Design package "Pipeline Summary Report".
   Same shape and process as the Weekly Activity Report: a button on the
   Activity Report toolbar, a module picker, an HTML string, a new window, print.
       window.PipelineSummaryReport = { open, _build, _health }

   DATA IS THE PIPELINE REPORT'S OWN. screens-pipeline.jsx now exposes
   window.VaultPipelineData.load(moduleId): the same six calls, the same row
   build, the same ctx. Nothing here re-derives a stage, a threshold or a court.

   HEALTH SCORE (Brian, 2026-09-04: "take a stab"). Five parts, weights fixed
   as the design shows. Every input is a number the diagnostic engine already
   measures; the composite is the only thing new, and _health() is exported so
   the harness and the next ruling can see exactly how each part is earned.
     Stage Aging      30  share of MEASURED stages (n >= 4, threshold known) whose
                          median age is within their own p75. Unmeasured stages
                          do not count either way -- a NULL threshold suppresses,
                          it never defaults (lib-pipeline-diag.js note 3).
     Distribution     20  Origination share inside 35-60% AND offer-or-beyond
       Shape               share inside 10-30%, each fading linearly outside its
                          band; the two halves average.
     Court Staleness  20  targets in the client's court 14d+ plus targets past
                          turnaround while waiting on the target, against a
                          15%-of-scope bar: at 15% the part earns nothing.
     Backlog Coverage 15  value at CC + Visit as a share of scope, against the
                          30% bar Strong Backlog already uses.
     Diagnostic Load  15  minus 6 per risk finding, minus 3 per caution, floor 0.
   Bands: STRONG >= 81 · STEADY 60-80 · WATCH 40-59 · AT RISK < 40.
   Below MIN_TOTAL (8 rows) the score prints an AWAITING block, not a number. */
(() => {
  const D = () => window.VaultPipelineDiag;
  const localYMD = (d) => d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
  const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
  const fmtY = (s) => window.VaultDate ? window.VaultDate(s, { month: "short", day: "numeric", year: "numeric" }) : s;
  const money = (n) => { const a = Math.abs(Number(n) || 0); return a >= 1e9 ? "$" + (a / 1e9).toFixed(2) + "B" : a >= 1e6 ? "$" + (a / 1e6).toFixed(1) + "M" : a >= 1e3 ? "$" + (a / 1e3).toFixed(0) + "K" : "$" + Math.round(a); };
  const clamp01 = (x) => Math.max(0, Math.min(1, x));
  const ORIGIN = (typeof location !== "undefined" && location.origin) || "";

  // ---- the composite ----------------------------------------------------------
  function _health(rows, ctx, findings) {
    const Dg = D();
    const N = rows.length;
    const scopeSales = Dg.sum(rows, r => r.sales);
    const parts = [];
    if (N < Dg.MIN_TOTAL) return { score: null, band: null, parts, reason: "Fewer than " + Dg.MIN_TOTAL + " targets in scope \u2014 no score is trustworthy at this size." };

    // 1. Stage Aging
    let measured = 0, late = 0;
    Dg.STAGES.forEach((_, i) => {
      const inStage = rows.filter(r => r.st === i);
      const ages = inStage.map(Dg.ageOf).filter(a => a != null);
      const p75 = ctx.stageThreshold(i);
      if (inStage.length < Dg.MIN_STAGE_N || p75 == null || !ages.length) return;
      measured++; if (Dg.median(ages) > p75) late++;
    });
    const aging = measured ? 1 - late / measured : null;
    parts.push({ key: "aging", label: "Stage Aging", weight: 30, share: aging,
      note: measured ? late + " of " + measured + " measured stages past their own p75" : "No stage has a measured threshold yet" });

    // 2. Distribution Shape
    const orig = Dg.sum(rows.filter(r => Dg.ORIGINATION.indexOf(r.st) !== -1), () => 1) / N;
    const offer = Dg.sum(rows.filter(r => Dg.OFFER_STAGES.indexOf(r.st) !== -1), () => 1) / N;
    const bandFit = (x, lo, hi, fade) => x >= lo && x <= hi ? 1 : clamp01(1 - (x < lo ? lo - x : x - hi) / fade);
    const shape = (bandFit(orig, 0.35, 0.60, 0.25) + bandFit(offer, 0.10, 0.30, 0.15)) / 2;
    parts.push({ key: "shape", label: "Distribution Shape", weight: 20, share: shape,
      note: Math.round(orig * 100) + "% Origination / " + Math.round(offer * 100) + "% at offer or beyond \u2014 " + (shape >= 0.999 ? "inside band" : "outside band") });

    // 3. Court Staleness
    const heldDays = (r) => Dg.actionAgeOf(r) != null ? Dg.actionAgeOf(r) : Dg.ageOf(r);
    const clientStale = rows.filter(r => ctx.court(r) === "client" && heldDays(r) != null && heldDays(r) >= 14);
    const silent = rows.filter(r => ctx.constraintOf(r) === "target" && Dg.pastTurn(r, ctx) === true);
    const staleShare = (clientStale.length + silent.length) / N;
    parts.push({ key: "court", label: "Court Staleness", weight: 20, share: 1 - clamp01(staleShare / 0.15),
      note: clientStale.length + " client-court 14d+ \u00b7 " + silent.length + " silent targets" });

    // 4. Backlog Coverage
    const backlog = Dg.sum(rows.filter(r => r.st === Dg.IX.CC || r.st === Dg.IX.VISIT), r => r.sales);
    const bShare = scopeSales ? backlog / scopeSales : 0;
    parts.push({ key: "backlog", label: "Backlog Coverage", weight: 15, share: clamp01(bShare / 0.30),
      note: Math.round(bShare * 100) + "% of value at CC + Visit, against a 30% bar" });

    // 5. Diagnostic Load
    const risks = (findings || []).filter(f => f.tone === "risk").length;
    const cautions = (findings || []).filter(f => f.tone === "gold").length;
    parts.push({ key: "load", label: "Diagnostic Load", weight: 15, share: clamp01(1 - (risks * 6 + cautions * 3) / 15),
      note: cautions + (cautions === 1 ? " caution" : " cautions") + " fired, " + risks + (risks === 1 ? " risk" : " risks") });

    // Unmeasurable parts drop out and the rest are rescaled to 100, so a
    // module with no thresholds yet is not punished for silence.
    const live = parts.filter(p => p.share != null);
    const wSum = live.reduce((a, p) => a + p.weight, 0) || 1;
    parts.forEach(p => { p.earned = p.share == null ? null : p.share * p.weight; });
    const score = Math.round(live.reduce((a, p) => a + p.share * p.weight, 0) / wSum * 100);
    const band = score >= 81 ? { label: "STRONG", range: "81\u2013100", cls: "ok" } : score >= 60 ? { label: "STEADY", range: "60\u201380", cls: "gold" } : score >= 40 ? { label: "WATCH", range: "40\u201359", cls: "gold" } : { label: "AT RISK", range: "< 40", cls: "risk" };
    return { score, band, parts, measuredWeight: wSum };
  }

  // ---- the page ---------------------------------------------------------------
  function _build({ moduleLabel, rows, ctx, generatedBy }) {
    const Dg = D();
    const N = rows.length;
    const scopeSales = Dg.sum(rows, r => r.sales);
    // evaluate() returns { shown, all, ... }: shown is the ranked three, all is
    // every finding that fired (what the composite's Diagnostic Load counts).
    const result = N >= Dg.MIN_TOTAL ? Dg.evaluate(rows, ctx) : null;
    const findings = (result && result.shown) || [];
    const h = _health(rows, ctx, (result && result.all) || []);
    const _viewer = (() => {
      const v = generatedBy; if (!v) return "";
      const p = window.VAULT_FIRM && window.VAULT_FIRM.PEOPLE_BY_ID && window.VAULT_FIRM.PEOPLE_BY_ID[String(v.personId || v.id || "").replace(/^p_/, "").replace(/_gmail$/, "")];
      return (p && p.name) || v.name || "";
    })();
    const kicker = (t) => '<span class="v-kicker">' + t + "</span>";
    const secHd = (n, title, right) => '<div style="display:flex;align-items:baseline;gap:8px;margin:18px 0 9px">'
      + '<span style="font:700 9.5px/1 var(--f-mono);color:var(--accent);background:var(--accent-soft);border-radius:4px;padding:3px 5px;letter-spacing:.06em">' + n + "</span>"
      + '<span style="font:600 15px/1 var(--f-sans)">' + esc(title) + "</span>"
      + '<span style="flex:1;border-bottom:1px solid var(--line)"></span>' + kicker(right || "") + "</div>";
    const tone = (cls) => cls === "ok" ? "var(--ok)" : cls === "risk" ? "var(--risk)" : cls === "gold" ? "var(--gold)" : "var(--muted)";
    const bandColor = (pct) => pct >= 81 ? "var(--ok)" : pct >= 60 ? "var(--gold)" : "var(--risk)";

    // 01 Health
    const healthRows = h.parts.map(p => {
      const pct = p.share == null ? 0 : Math.round(p.share * 100);
      return '<div><div style="display:flex;align-items:baseline;gap:7px">'
        + '<span style="font:600 10.5px/1 var(--f-sans);color:var(--ink);width:9.5rem">' + esc(p.label) + "</span>"
        + '<span style="font:400 9px/1 var(--f-sans);color:var(--muted);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(p.note) + "</span>"
        + '<span style="font:600 9.5px/1 var(--f-mono);color:var(--ink)">' + (p.earned == null ? "\u2014" : p.earned.toFixed(1)) + "</span>"
        + '<span style="font:500 9.5px/1 var(--f-mono);color:var(--muted-2)">/ ' + p.weight + "</span></div>"
        + '<div style="height:6px;border-radius:3px;background:var(--surface-3);margin-top:4px;position:relative"><div style="position:absolute;left:0;top:0;bottom:0;border-radius:3px;width:' + pct + "%;background:" + (p.share == null ? "var(--line)" : bandColor(pct)) + '"></div></div></div>';
    }).join("");
    const scoreCard = h.score == null
      ? '<div style="border:1px dashed var(--line-strong);border-radius:7px;padding:13px 14px;text-align:center">' + kicker("HEALTH SCORE") + '<div style="font:600 10px/1.4 var(--f-sans);color:var(--ink-2);margin-top:8px">AWAITING</div><div style="font:400 9px/1.4 var(--f-sans);color:var(--muted);margin-top:4px">' + esc(h.reason || "") + "</div></div>"
      : '<div style="border:1px solid var(--line);border-radius:7px;padding:13px 14px;text-align:center">' + kicker("HEALTH SCORE")
        + '<div style="display:flex;align-items:baseline;justify-content:center;gap:4px;margin-top:6px"><span style="font:700 42px/1 var(--f-mono);color:var(--ink)">' + h.score + '</span><span style="font:600 13px/1 var(--f-mono);color:var(--muted-2)">/100</span></div>'
        + '<div style="display:inline-block;font:600 8.5px/1 var(--f-mono);background:var(--' + (h.band.cls) + '-soft);color:var(--' + (h.band.cls === "gold" ? "gold-ink" : h.band.cls) + ');border-radius:4px;padding:3px 7px;margin-top:7px;letter-spacing:.06em">' + h.band.label + " \u00b7 " + h.band.range + "</div>"
        + '<div style="font:500 7.5px/1.5 var(--f-mono);color:var(--muted-2);letter-spacing:.03em;margin-top:9px;border-top:1px solid var(--line-2);padding-top:7px">HEALTH = .30\u00b7AGING + .20\u00b7SHAPE<br>+ .20\u00b7COURT + .15\u00b7BACKLOG + .15\u00b7SIGNALS' + (h.measuredWeight < 100 ? "<br>RESCALED \u00b7 " + h.measuredWeight + " OF 100 MEASURED" : "") + "</div></div>";
    const s01 = secHd("01", "Pipeline Health", "WEIGHTS FIXED \u00b7 NORMS = MODULE P75")
      + '<div style="display:grid;grid-template-columns:11.5rem minmax(0,1fr);gap:20px;align-items:start">' + scoreCard + '<div style="display:flex;flex-direction:column;gap:7px;padding-top:2px">' + healthRows + "</div></div>";

    // 02 Stage cards
    const maxN = Math.max(1, ...Dg.STAGES.map((_, i) => rows.filter(r => r.st === i).length));
    let pastNorm = 0;
    const cards = Dg.STAGES.map((name, i) => {
      const inStage = rows.filter(r => r.st === i);
      const ages = inStage.map(Dg.ageOf).filter(a => a != null);
      const med = ages.length ? Math.round(Dg.median(ages)) : null;
      const p75 = ctx.stageThreshold(i);
      const late = med != null && p75 != null && med > p75; if (late) pastNorm++;
      const severe = late && med / p75 > 1.5;
      const exec = Dg.EXECUTION_STAGES.indexOf(i) !== -1;
      const ageLine = med == null ? "No measured age" : "Med " + med + "d " + (exec ? "since signing" : "in stage") + (p75 != null ? " \u00b7 p75 " + p75 + "d" : " \u00b7 p75 n/a");
      return '<div style="border:1px solid ' + (late ? (severe ? "var(--risk)" : "var(--gold-line)") : "var(--line)") + ";border-radius:7px;padding:8px 9px;background:" + (late ? (severe ? "var(--risk-soft)" : "var(--gold-soft)") : "var(--surface)") + '">'
        + kicker(Dg.PHASES[i].toUpperCase()) + '<div style="font:600 10px/1.2 var(--f-sans);color:var(--ink);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(name) + "</div>"
        + '<div style="display:flex;align-items:baseline;gap:5px;margin-top:4px"><span style="font:700 15px/1 var(--f-mono);color:var(--ink)">' + inStage.length + '</span><span style="font:500 8.5px/1 var(--f-mono);color:var(--muted)">' + money(Dg.sum(inStage, r => r.sales)) + "</span></div>"
        + '<div style="height:3px;background:var(--surface-3);border-radius:99px;overflow:hidden;margin-top:5px"><div style="width:' + Math.max(2, Math.round(inStage.length / maxN * 100)) + '%;height:100%;background:var(--accent)"></div></div>'
        + '<div style="font:500 8px/1.3 var(--f-mono);margin-top:4px;color:' + (late ? (severe ? "var(--risk)" : "var(--gold-ink)") : "var(--muted)") + '">' + esc(ageLine) + "</div></div>";
    }).join("");
    const s02 = secHd("02", "Stage Cards", "MEDIAN AGE VS STAGE P75 \u00b7 EXECUTION MEASURED SINCE SIGNING")
      + '<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:6px">' + cards
      + '<div style="border:1px dashed var(--line-strong);border-radius:7px;padding:8px 9px;display:flex;flex-direction:column;justify-content:center;gap:3px">' + kicker("SCOPE TOTAL")
      + '<div style="display:flex;align-items:baseline;gap:5px"><span style="font:700 15px/1 var(--f-mono);color:var(--ink)">' + N + '</span><span style="font:500 8.5px/1 var(--f-mono);color:var(--muted)">' + money(scopeSales) + "</span></div>"
      + '<div style="font:500 8px/1.3 var(--f-mono);color:var(--muted)">' + pastNorm + (pastNorm === 1 ? " stage" : " stages") + " past norm</div></div></div>";

    // 03 Diagnostics
    const fCards = findings.length ? findings.map(f => '<div style="border:1px solid var(--line);border-radius:7px;padding:10px 12px">'
      + '<div style="display:flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:99px;flex:none;background:' + tone(f.tone) + '"></span><span style="font:600 11px/1.2 var(--f-sans);color:var(--ink)">' + esc(f.label) + "</span></div>"
      + '<div style="font:400 9.5px/1.5 var(--f-sans);color:var(--ink-2);margin-top:5px">' + esc(f.signal) + "</div>"
      + (f.evidence && f.evidence.length ? '<div style="margin-top:6px;border-top:1px solid var(--line-2);padding-top:5px">' + f.evidence.slice(0, 4).map(e => '<div style="display:flex;gap:5px;font:400 8.5px/1.45 var(--f-sans);color:var(--muted);margin-top:2px"><span style="flex:none">&bull;</span><span>' + esc(e) + "</span></div>").join("") + "</div>" : "")
      + (f.actions && f.actions.length ? '<div style="font:600 8.5px/1 var(--f-mono);color:var(--accent);margin-top:7px;letter-spacing:.03em">' + esc(f.actions[0].label || f.actions[0]) + " \u2192</div>" : "")
      + "</div>").join("")
      : '<div style="grid-column:1/-1;border:1px dashed var(--line-strong);border-radius:7px;padding:12px;font:400 10px/1.5 var(--f-sans);color:var(--muted)">Fewer than ' + Dg.MIN_TOTAL + " targets in scope \u2014 no finding is trustworthy at this size.</div>";
    const s03 = secHd("03", "Diagnostics", "TOP 3 BY SEVERITY \u00b7 SLOT 3 IS ALWAYS A STRENGTH")
      + '<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px">' + fCards + "</div>";

    // 04 Push list
    const heldDays = (r) => Dg.actionAgeOf(r) != null ? Dg.actionAgeOf(r) : Dg.ageOf(r);
    const oldest = rows.filter(r => Dg.ageOf(r) != null && ctx.threshold(r) != null && Dg.ageOf(r) > ctx.threshold(r))
      .sort((a, b) => (Dg.ageOf(b) / ctx.threshold(b)) - (Dg.ageOf(a) / ctx.threshold(a))).slice(0, 5)
      .map(r => ({ name: r.target, id: "H-" + r.id, meta: Dg.STAGES[r.st] + " \u00b7 " + (r.action || "\u2014"), age: Dg.ageOf(r) + "d / " + ctx.threshold(r) }));
    const lateAction = rows.filter(r => Dg.actionAgeOf(r) != null && ctx.eTurn(r) != null && Dg.actionAgeOf(r) > ctx.eTurn(r))
      .sort((a, b) => (Dg.actionAgeOf(b) / ctx.eTurn(b)) - (Dg.actionAgeOf(a) / ctx.eTurn(a))).slice(0, 5)
      .map(r => ({ name: r.target, id: "H-" + r.id, meta: (r.action || "\u2014") + " \u00b7 " + (ctx.owner(r) || "Unassigned"), age: Dg.actionAgeOf(r) + "d / " + ctx.eTurn(r) }));
    const clientCourt = rows.filter(r => ctx.court(r) === "client" && heldDays(r) != null && heldDays(r) >= 14)
      .sort((a, b) => heldDays(b) - heldDays(a)).slice(0, 5)
      .map(r => ({ name: r.target, id: "H-" + r.id, meta: (r.buyerName || r.projectName || "\u2014") + " \u00b7 " + (r.action || "\u2014"), age: heldDays(r) + "d" }));
    const pushCols = [
      { title: "OLDEST IN STAGE \u00b7 PAST P75", metric: "AGE / P75", hue: "var(--risk)", rows: oldest },
      { title: "ACTION PAST TURNAROUND", metric: "AGE / ETURN", hue: "var(--gold-ink)", rows: lateAction },
      { title: "CLIENT COURT \u00b7 14D+", metric: "DAYS HELD", hue: "var(--accent)", rows: clientCourt },
    ];
    const pushHtml = pushCols.map(col => '<div><div style="display:flex;align-items:center;gap:6px;margin-bottom:5px"><span style="font:700 8.5px/1 var(--f-mono);color:' + col.hue + ';letter-spacing:.09em">' + col.title + '</span><span style="flex:1;height:1px;background:var(--line)"></span><span style="font:500 8px/1 var(--f-mono);color:var(--muted-2)">' + col.metric + "</span></div>"
      + (col.rows.length ? col.rows.map(r => '<div style="display:flex;align-items:baseline;gap:6px;border-bottom:1px solid var(--line-2);padding:4px 0"><div style="min-width:0;flex:1"><div style="display:flex;align-items:baseline;gap:5px"><span style="font:600 9.5px/1.3 var(--f-sans);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(r.name) + '</span><span style="font:500 7.5px/1 var(--f-mono);color:var(--muted-2);flex:none">' + esc(r.id) + '</span></div><div style="font:400 8.5px/1.4 var(--f-sans);color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(r.meta) + '</div></div><span style="font:700 9.5px/1 var(--f-mono);color:' + col.hue + ';flex:none">' + esc(r.age) + "</span></div>").join("")
        : '<div style="font:400 9px/1.5 var(--f-sans);color:var(--muted);padding:4px 0">Nothing in this category.</div>')
      + "</div>").join("");
    const s04 = secHd("04", "Push List", "5 MOST STALE PER CATEGORY \u00b7 AGE FROM HARVEY EVENTS")
      + '<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px">' + pushHtml + "</div>";

    const courtKnown = rows.filter(r => r.courtSetBy).length;
    const fileName = "Vault_Pipeline_Summary_Report_" + String(moduleLabel || "").replace(/[^A-Za-z0-9]+/g, "_") + "_" + localYMD(new Date());
    return `<!doctype html><html><head><meta charset="utf-8"><title>${fileName}</title>
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@400;500;600;700&family=Archivo:wght@400;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="${ORIGIN}/theme.css">
<style>
  @page { size: Letter; margin: 0; }
  html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  body { margin:0; background: var(--bg); font-family: var(--f-sans); color: var(--ink); }
  :root { color-scheme: light; }
  .v-kicker { font: 500 8.5px/1 var(--f-mono); color: var(--muted); letter-spacing: .1em; text-transform: uppercase; }
  @media print { body { background: var(--surface); } .page { border: 0 !important; border-radius: 0 !important; } }
</style></head><body>
<div style="min-height:100vh;padding:28px 0"><div style="width:816px;margin:0 auto"><div class="page" style="background:var(--surface);border:1px solid var(--line);border-radius:10px;border-top:3px solid var(--accent);padding:30px 36px 26px">
  <div style="display:flex;align-items:flex-start;justify-content:space-between;border-bottom:2px solid var(--ink);padding-bottom:12px">
    <div style="display:flex;align-items:flex-end;gap:10px">
      <img src="${ORIGIN}/vault-mark.png" alt="Vault" style="width:28px;height:28px;object-fit:contain">
      <div>
        <div class="v-kicker">Vault \u00b7 Pipeline Report</div>
        <div style="font-size:21px;font-weight:700;letter-spacing:-.015em;line-height:1.15;color:var(--ink);margin-top:5px">Pipeline Summary Report</div>
        <div class="v-kicker" style="margin-top:6px;white-space:nowrap">${esc(moduleLabel)} \u00b7 ${N} Targets \u00b7 ${money(scopeSales)}</div>
      </div>
    </div>
    <div style="text-align:right">
      <div class="v-kicker">Generated ${fmtY(localYMD(new Date()))}</div>
      <div class="v-kicker" style="margin-top:4px">${esc(_viewer)}</div>
      <div style="display:inline-block;font:600 8.5px/1 var(--f-mono);color:var(--accent);background:var(--accent-soft);border-radius:4px;padding:3px 6px;letter-spacing:.06em;margin-top:6px">SNAPSHOT \u00b7 LIVE FILTERS</div>
    </div>
  </div>
  ${s01}${s02}${s03}${s04}
  <div style="display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--line);margin-top:16px;padding-top:9px">
    <span class="v-kicker">Scope follows the Pipeline Report's live filters \u00b7 Thresholds are the module's own measured p75 \u00b7 Court recorded on ${courtKnown} of ${N}</span>
    <span class="v-kicker">Vault \u00b7 Confidential \u00b7 Page 1 of 1</span>
  </div>
</div></div></div></body></html>`;
  }

  // THE PIPELINE REPORT'S SCOPE (Brian, 2026-09-04): rows after every toolbar
  // filter, so a Jordan/Tari pair prints Jordan/Tari's statistics and
  // diagnostics, not the module's. The label names the filters in force.
  function openWith({ rows, ctx, label }) {
    if (!window.VaultPipelineDiag) { window.VaultUI && window.VaultUI.toast("error", "Pipeline engine not loaded"); return; }
    const html = _build({ moduleLabel: label || "Pipeline", rows: rows || [], ctx, generatedBy: window.VAULT_VIEWER || window.VAULT_USER || null });
    const w = window.open("", "_blank");
    if (!w) { window.VaultUI && window.VaultUI.toast("error", "Pop-up blocked \u2014 allow pop-ups for vault-harvey.com"); return; }
    w.document.write(html); w.document.close();
    setTimeout(() => { try { w.focus(); w.print(); } catch (e) {} }, 1100);
  }

  async function open(moduleId, moduleLabel) {
    const P = window.VaultPipelineData;
    if (!P || !window.VaultPipelineDiag) { window.VaultUI && window.VaultUI.toast("error", "Pipeline engine not loaded"); return; }
    window.VaultUI && window.VaultUI.toast("info", "Building pipeline summary\u2026");
    let data;
    try { data = await P.load(moduleId); }
    catch (e) { window.VaultUI && window.VaultUI.toast("error", "Pipeline load failed: " + String((e && e.message) || e)); return; }
    const html = _build({ moduleLabel: moduleLabel || "Module", rows: data.rows, ctx: data.ctx, generatedBy: window.VAULT_VIEWER || window.VAULT_USER || null });
    const w = window.open("", "_blank");
    if (!w) { window.VaultUI && window.VaultUI.toast("error", "Pop-up blocked \u2014 allow pop-ups for vault-harvey.com"); return; }
    w.document.write(html); w.document.close();
    setTimeout(() => { try { w.focus(); w.print(); } catch (e) {} }, 1100);
  }

  window.PipelineSummaryReport = { open, openWith, _build, _health };
})();
