// screens-standards.jsx — Standards (Project Management step, 7/28).
// The published truth behind every diagnosis. Two stacked sections, matching the
// Claude Design layout:
//   A. Conversion Rate Standards — the LIVE health_baselines table (22 canonical
//      metrics, per project_type) with a visual healthy band (mean ±1σ). Not a
//      hand-typed table: if a metric has no baseline the row says so rather than
//      inventing a number, because the engine scores it z = 0 either way.
//   B. Rule Book — all 27 diagnosis outcomes across the 18 rule slots in
//      health-engine.js, each with its literal firing condition, the Action
//      Playbook text (live from health_playbook, static Excel text as fallback),
//      and a PROPOSED attribution tag awaiting Brian's redline.
// The Excel-vs-Vault derivation redline lives in the audit document, not on this
// page — engine drift is a review artifact, not a published standard.
// Read-only. No writes, no new tables. Reads listHealthBaselines + listHealthPlaybook.
(function () {
  const { useState, useEffect, useMemo } = React;
  const LINE = "#E4E9F0", MUTE = "#8A97A8", INK = "#22303F";
  const MONO = "var(--f-mono, 'IBM Plex Mono', monospace)";
  const PANEL = { background: "#fff", border: `1px solid ${LINE}`, borderRadius: 14, padding: 20, marginBottom: 18 };
  // App shell wraps screens in an unpadded div, so every screen supplies its own.
  // Zero horizontal padding was why this page sat flush against the sidebar.
  const PAGE_FRAME = { padding: "26px 30px 80px", boxSizing: "border-box", width: "100%", maxWidth: 1480 };
  const KICK = { fontFamily: MONO, fontSize: 11, fontWeight: 600, letterSpacing: ".16em", textTransform: "uppercase", color: "#7C8DA1" };
  const TH = { textAlign: "left", padding: "8px 10px", borderBottom: `1px solid ${LINE}`, fontFamily: MONO, fontSize: 10, fontWeight: 600, letterSpacing: ".12em", textTransform: "uppercase", color: "#7C8DA1", whiteSpace: "nowrap" };
  const TD = { padding: "9px 10px", borderBottom: "1px solid #F0F3F7", fontSize: 12.5, color: INK, verticalAlign: "top" };
  const NUM = { ...TD, fontVariantNumeric: "tabular-nums", textAlign: "right", whiteSpace: "nowrap" };

  // ── A. metric dictionary ────────────────────────────────────────────────
  // key order == health-engine.js METRIC_KEYS. label/desc/unit are presentation
  // only; the numbers all come from the health_baselines rows.
  const METRICS = [
    ["leadRate",           "Lead Rate",            "leads ÷ approved",                    "%"],
    ["approvalRate",       "Approval Rate",        "approved ÷ targets sent",             "%"],
    ["contactRate",        "Contact Rate",         "contacted ÷ approved",                "%"],
    ["ccRate",             "CC Conversion Rate",   "conference calls ÷ leads",            "%"],
    ["visitRate",          "Visit Rate",           "visits ÷ conference calls",           "%"],
    ["offersRate",         "Offers Rate",          "offers ÷ visits",                     "%"],
    ["callsPerLead",       "Calls / Lead",         "call effort per lead",                ""],
    ["mailingsPerLead",    "Mailings / Lead",      "mail effort per lead",                ""],
    ["avgMailPerMo",       "Avg Mail / Mo",        "mailings ÷ months since anchor",      ""],
    ["pctCallDM1",         "% Call by DM1",        "calls completed by the module lead",  "%"],
    ["mailingPerApproval", "Mailing / Approval",   "mailings ÷ approved",                 ""],
    ["callPerApproval",    "Call / Approval",      "calls ÷ approved",                    ""],
    ["broadcastDrag",      "Broadcast Drag",       "calls/lead − mailings/lead",          ""],
    ["daysSinceApproval",  "Days Since Approval",  "days since the last list went out",   "d"],
    ["listsPerMo",         "Lists / Mo",           "research cadence",                    ""],
    ["targetsApproved",    "Targets Approved",     "approved in the window",              ""],
    ["callsPerMailing",    "Calls / Mailing",      "call-to-mail balance",                ""],
    ["touchesPerLead",     "Touches / Lead",       "(calls + mailings) ÷ leads",          ""],
    ["obRate",             "O&B Rate",             "(offers + LOIs + closed) ÷ CCs",      "%"],
    ["approvedPerMo",      "Approved / Mo",        "approval velocity",                   ""],
    ["sentPerMo",          "Sent / Mo",            "names sent per month",                ""],
    ["avgCallsPerMo",      "Avg Calls / Mo",       "calls ÷ months since anchor",         ""],
  ];

  // ── B. rule book ────────────────────────────────────────────────────────
  // slot = the Diagnostics column letter. cond = the literal predicate as ported
  // in health-engine.js. attribution is PROPOSED (Excel colour key row 3 only
  // separates client-engagement / our-process / healthy; the per-rule mapping is
  // mine and is what Brian is redlining).
  const A_PROC = "OUR PROCESS", A_CLI = "CLIENT", A_STR = "STRENGTH", A_MIX = "MIXED";
  const RULES = [
    ["Z",  "Bad Research", A_PROC,
     "Approval Rate z > 1 AND Lead Rate z ≥ −1 AND CC Conversion z < −1",
     "Loose list criteria. The client approves plenty and leads arrive, but they do not survive to a conference call — the names are not real fits."],
    ["AA", "Picky (selective – converts well downstream)", A_CLI,
     "CC Conversion z < −1, no approach rule fired, AND (Visit z > 1 OR Offers z > 1)",
     "Client takes few calls but the ones they take convert. Selectivity, not disengagement."],
    ["AA", "Universe is Picked Over", A_CLI,
     "CC Conversion z < −1, no approach rule fired, Visit z ≤ 0 AND Offers z ≤ 0, AND Approval z < −1",
     "Low conversion with a starved approval rate: the TAM is exhausted."],
    ["AA", "Low Client Engagement – Harvey process is fine", A_CLI,
     "CC Conversion z < −1, no approach rule fired, Visit z ≤ 0 AND Offers z ≤ 0, Approval z ≥ −1",
     "Conversion is soft while our mechanics are clean and approvals are normal. The constraint is on the client side."],
    ["AB", "Review Call Quality / Pitch", A_PROC,
     "Lead z < −1 AND Calls/Lead z > 1 AND Calls/Mailing z > 0.75 AND %Call-DM1 z < −0.75",
     "Many calls into the same companies, not reaching the decision maker, no leads. Junior or ineffective calling."],
    ["AC", "Call Coverage (DM1)", A_PROC,
     "%Call-DM1 z < −0.75 AND Lead z < −1 AND (Calls/Lead z > 1 OR Call/Approval z > 1) AND Review Call Quality did not fire",
     "Decision-maker coverage gap. Suppressed when Review Call Quality already explains it."],
    ["AD", "Brochure Quality / Pitch", A_PROC,
     "Lead z < −1 AND Mailings/Lead z > 1 AND Mailing/Approval z ≤ 1 AND Approval z ≥ −1",
     "Mail-heavy and not pulling leads, with a normal approach cycle. The collateral is the suspect."],
    ["AE", "Process Integrity (Follow-Through) - also Evaluate Brochure/Pitch", A_PROC,
     "Mailing/Approval z > 1 AND Calls/Mailing z ≥ −0.5 AND Touches/Lead z > 1",
     "Re-mailing before finishing the cycle, at very high total touches per lead. Both process and collateral."],
    ["AE", "Process Integrity (Follow-Through)", A_PROC,
     "Mailing/Approval z > 1 AND Calls/Mailing z ≥ −0.5 AND Touches/Lead z ≤ 1",
     "Approach cycles are being restarted before they complete."],
    ["AE", "Process Integrity (Needs Calls) - High Mailings, Low Calls", A_PROC,
     "Mailing/Approval z > 1 AND Calls/Mailing z < −0.5 AND Call/Approval z > 0",
     "Mail is going out without the calls that make it work."],
    ["AE", "Requires Call Attention", A_PROC,
     "Mailing/Approval z > 1 AND Calls/Mailing z < −0.5 AND Call/Approval z ≤ 0",
     "The approved pool is under-called on every measure."],
    ["AF", "Over-Working Universe - Needs Research or TAM Expansion", A_PROC,
     "Call/Approval z > 1 AND Approved/Mo z < −0.75",
     "Squeezing a maxed-out universe. The constraint is the map, not the motion."],
    ["AF", "Over-Worked Universe (targets receive lots of inquiries) / or calls ineffective", A_MIX,
     "Call/Approval z > 1 AND Approved/Mo z ≥ −0.75 AND Calls/Lead z > 1",
     "Many calls to reach a target: a saturated or hard-to-reach universe, or an ineffective pitch."],
    ["AF", "Over-working Universe", A_PROC,
     "Call/Approval z > 1 AND Approved/Mo z ≥ −0.75 AND Calls/Lead z ≤ 1",
     "Call cadence is heavy against a small approved pool."],
    ["AG", "Needs Approval List", A_CLI,
     "Days Since Approval z > 1",
     "The client has not fed the pipeline with new approvals. Supplying the list is our job."],
    ["AH", "Mid-funnel Disengagement", A_CLI,
     "CC Conversion z ≥ −1 AND (Visit z < −1 OR Offers z < −1)",
     "The client takes the call and then stalls before a visit or an offer."],
    ["AI", "Needs Call Love (mail-reliant, add calls)", A_PROC,
     "Calls/Lead z < 0 AND Broadcast Drag z < −0.5 AND Lead z ≥ 0 AND CC Conversion z ≥ 0",
     "Solid lead and CC performance carried by mail. Adding calls is cheap upside."],
    ["AJ", "Good Return per Call -- efficient, low effort / lead", A_STR,
     "Calls/Lead z < 0.5 AND Mailings/Lead z < 0 AND Avg Mail/Mo z < 0 AND Lead z ≥ 0, no problem rule firing, AND (Needs Approval List silent OR health above cohort average)",
     "Leads arrive cheaply. This is the best marginal return on senior time in the book."],
    ["AK", "Low Health – investigate", A_MIX,
     "health z < −1 AND no other rule fired",
     "Below-cohort health with no single pattern. Manual review."],
    ["AL", "Aggressive Client - Invest in Research", A_STR,
     "Sent/Mo z < −0.5 AND Approved/Mo z < −0.5 AND CC Conversion z > 0.67 AND Visit z > 0.67",
     "Thin supply against strong downstream conversion. Feed it more targets."],
    ["AM", "Aggressive Client - Improve Contact Rate", A_STR,
     "Contact z < −0.5 AND CC Conversion z > 0 AND Offers z > 0 AND Lead z ≥ −1",
     "They convert what they touch. Touch more."],
    ["AR", "Funnel Conversion Soft (strong top-funnel, weak conversion)", A_CLI,
     "health z < −1 AND (Contact z ≥ 0 OR Lead z ≥ 0) AND CC Conversion z < 0 AND Visit z < 0 AND Offers z < 0, nothing above fired",
     "Good lead generation, weak conversion. Coach the client through CC → visit → offer and revisit the buy-box."],
    ["AT", "Aggressive Client - high conversion from CC to Offer", A_STR,
     "O&B Rate z > 1",
     "A strong converter. Protect and scale."],
    ["AU", "Clean Process", A_STR,
     "All eight outreach z within ±1 (Calls/Lead, Mailings/Lead, Avg Mail/Mo, Mailing/Approval, Call/Approval, Broadcast Drag, Calls/Mailing, Touches/Lead) AND raw Broadcast Drag ≥ 0",
     "No outreach abnormality anywhere. On track."],
    ["AU", "Clean Process - potential trend issue", A_STR,
     "All eight outreach z within ±1 AND raw Broadcast Drag < 0",
     "Healthy now, but the mix is turning mail-heavy. Watch for TAM exhaustion and declining approvals."],
    ["AX", "High-Volume Project", A_STR,
     "Approved/Mo z > 1 AND O&B Rate z > −0.25",
     "High-velocity against a fragmented universe. Keep the pedal down."],
    ["AX", "High-Volume Project - CC to Offer Conversion Low", A_MIX,
     "Approved/Mo z > 1 AND O&B Rate z ≤ −0.25",
     "Volume is there, conversion is not. Push the client to make offers and weed out window shoppers."],
  ];

  // Excel 'Action Playbook' A2:B28, verbatim — the fallback when health_playbook
  // has no row for a diagnosis (and the seed text if that table is ever empty).
  const PLAYBOOK_FALLBACK = {
    "Bad Research": "Tighten research criteria; refine target profile / buy-box with client",
    "Picky (selective – converts well downstream)": "Pre-qualify conference calls harder; focus on best-fit targets only",
    "Universe is Picked Over": "Expand TAM / commission new research; level-set expectations with client",
    "Low Client Engagement – Harvey process is fine": "Re-engage client; revisit fit & buy-box — not a process problem",
    "Review Call Quality / Pitch": "Coach callers; review call script / pitch; consider ways to bolster the message; new hook; check DM1 reach",
    "Call Coverage (DM1)": "Improve decision-maker coverage; prioritize outreach from DM1",
    "Brochure Quality / Pitch": "Revise brochure; test new messaging; improve hook - may be specific to the industry dynamic",
    "Process Integrity (Follow-Through) - also Evaluate Brochure/Pitch": "Finish approach cycles before re-mailing; also review collateral (very high touch volume)",
    "Process Integrity (Follow-Through)": "Enforce approach-cycle follow-through; stop premature re-mailing; consider adding RA/Analyst call resources to C2/3s",
    "Process Integrity (Needs Calls) - High Mailings, Low Calls": "Add calls to mailed targets; don't re-mail without calling",
    "Requires Call Attention": "Increase calling effort; targets are under-called",
    "Over-Working Universe - Needs Research or TAM Expansion": "Expand TAM / commission research; or level-set expectations (limited universe)",
    "Over-Worked Universe (targets receive lots of inquiries) / or calls ineffective": "Saturated / hard-to-reach universe; assess call / pitch effectiveness; consider easing cadence",
    "Over-working Universe": "Review call cadence — risk of over-contacting a small approved universe",
    "Needs Approval List": "Send a fresh approval list; current list has gone stale",
    "Mid-funnel Disengagement": "Client has trouble engaging - diagnose drop-off after the conference call; tighten conference call to visit/offer conversion",
    "Needs Call Love (mail-reliant, add calls)": "Shift outreach mix toward calling; currently mail-reliant",
    "Good Return per Call -- efficient, low effort / lead": "Efficient account — invest more senior deal-maker time; scale up",
    "Low Health – investigate": "Manual review — sporadic out-of-the-norm stats with no single pattern; investigate holistically",
    "Aggressive Client - Invest in Research": "Client pushing hard with thin supply; invest in research / expanding the TAM",
    "Aggressive Client - Improve Contact Rate": "Lift contact rate; client wants more activity and they convert",
    "Aggressive Client - high conversion from CC to Offer": "Strong converter — feed more volume; protect and scale",
    "Funnel Conversion Soft (strong top-funnel, weak conversion)": "Good lead-gen, weak conversion — coach client on CC→visit→offer; reassess buy-box",
    "Clean Process": "On track — maintain current approach",
    "Clean Process - potential trend issue": "Healthy now, but outreach turning mail-heavy — watch for TAM exhaustion / declining approvals and reduced lead flow",
    "High-Volume Project": "Keep the pedal down; focus DT on lead conversion and client on offer conversion",
    "High-Volume Project - CC to Offer Conversion Low": "Encourage client to make offers and weed out the window shoppers",
  };

  const ATTR = {
    [A_PROC]: { bg: "#FBF3E3", fg: "#8A5B08", bd: "#F0DCB4", dot: "#C9901C" },
    [A_CLI]:  { bg: "#F1EDFB", fg: "#5C43A8", bd: "#DDD3F4", dot: "#7A5BD0" },
    [A_STR]:  { bg: "#EAF6EF", fg: "#1B7A46", bd: "#CBE8D6", dot: "#2F9E58" },
    [A_MIX]:  { bg: "#F4F7FB", fg: "#5B6A7A", bd: "#DEE5EE", dot: "#8A97A8" },
  };

  // value formatting: baselines are stored in the metric's own native units, so
  // ratio-style metrics are shown as percentages and the rest as plain numbers.
  function fmtVal(v, unit) {
    if (v == null || isNaN(Number(v))) return "—";
    const n = Number(v);
    if (unit === "%") return (n * 100).toFixed(n * 100 >= 10 ? 0 : 1) + "%";
    if (unit === "d") return Math.round(n) + "d";
    return Math.abs(n) >= 100 ? Math.round(n).toLocaleString() : n.toFixed(Math.abs(n) >= 10 ? 1 : 2);
  }

  function Tag({ spec, children }) {
    return (
      <span style={{ display: "inline-flex", alignItems: "center", gap: 5, background: spec.bg, color: spec.fg,
        border: `1px solid ${spec.bd}`, borderRadius: 999, padding: "2px 9px",
        fontFamily: MONO, fontSize: 9.5, fontWeight: 600, letterSpacing: ".1em", whiteSpace: "nowrap" }}>
        {spec.dot && <span style={{ width: 5, height: 5, borderRadius: 999, background: spec.dot }}/>}
        {children}
      </span>
    );
  }

  function StandardsScreen({ user }) {
    const [baselines, setBaselines] = useState(null);
    const [playbook, setPlaybook] = useState([]);
    const [err, setErr] = useState(null);
    const [cohort, setCohort] = useState("Add-on");
    const [openRule, setOpenRule] = useState(0);

    useEffect(() => {
      let live = true;
      Promise.all([
        window.VaultAPI.listHealthBaselines().catch(() => []),
        window.VaultAPI.listHealthPlaybook ? window.VaultAPI.listHealthPlaybook().catch(() => []) : Promise.resolve([]),
      ]).then(([b, p]) => { if (!live) return; setBaselines(b || []); setPlaybook(p || []); })
        .catch(e => { if (live) setErr(String(e && e.message || e)); });
      return () => { live = false; };
    }, []);

    // playbook rows keyed by diagnosis, falling back to the Excel text
    const actionFor = useMemo(() => {
      const m = new Map();
      (playbook || []).forEach(r => {
        const k = r.diagnosis || r.rule || r.name;
        const v = r.action || r.recommended_action || r.text;
        if (k && v) m.set(String(k).trim(), String(v));
      });
      return name => m.get(name) || PLAYBOOK_FALLBACK[name] || "—";
    }, [playbook]);

    const cohorts = useMemo(() => {
      const s = new Set((baselines || []).map(b => b.project_type).filter(Boolean));
      const list = Array.from(s);
      return list.length ? list : ["Add-on", "Platform"];
    }, [baselines]);

    const rows = useMemo(() => {
      const byMetric = new Map();
      (baselines || []).filter(b => b.project_type === cohort)
        .forEach(b => byMetric.set(b.metric, b));
      return METRICS.map(([key, label, desc, unit]) => {
        const b = byMetric.get(key);
        const avg = b ? Number(b.avg) : null, sd = b ? Number(b.sd) : null;
        const has = b && avg != null && !isNaN(avg) && sd != null && !isNaN(sd);
        return {
          key, label, desc, unit, has,
          n: b && b.n != null ? Number(b.n) : null,
          avg, sd, lo: has ? avg - sd : null, hi: has ? avg + sd : null,
          // geometry for the healthy-band bar: axis runs 0 .. mean + 2.2σ so the
          // ±1σ band sits visually centred with headroom on both sides.
          bar: has && (avg + 2.2 * sd) > 0 ? (() => {
            const max = avg + 2.2 * sd, lo = Math.max(0, avg - sd), hi = avg + sd;
            return { left: (lo / max) * 100, width: ((hi - lo) / max) * 100, mean: (avg / max) * 100 };
          })() : null,
          thin: b && b.n != null && Number(b.n) < 10,
          flat: has && sd === 0,
        };
      });
    }, [baselines, cohort]);

    const stamp = useMemo(() => {
      const ts = (baselines || []).map(b => b.computed_at).filter(Boolean).sort().pop();
      if (!ts) return null;
      const who = ((baselines || []).find(b => b.computed_at === ts) || {}).computed_by;
      return String(ts).slice(0, 10) + (who ? " · " + who : "");
    }, [baselines]);

    const missing = rows.filter(r => !r.has).length;

    return (
      <div style={PAGE_FRAME}>
        <div style={{ display: "flex", alignItems: "flex-start", gap: 16, flexWrap: "wrap", marginBottom: 16 }}>
        <window.PageToolbar title="Standards"
          subtitle="The published truth behind every diagnosis — cohort baselines and the rule book that reads them"
          style={{ marginBottom: 0, flex: 1 }}/>
        {stamp && (
          <span style={{ fontFamily: MONO, fontSize: 10.5, letterSpacing: ".08em", color: "#7C8DA1",
            background: "#EDF1F6", border: `1px solid #E2E8F1`, borderRadius: 99, padding: "5px 11px",
            whiteSpace: "nowrap", marginTop: 6 }}>BASELINES COMPUTED {stamp}</span>
        )}
        </div>

        {err && (
          <div style={{ ...PANEL, borderColor: "#F0D4D4", background: "#FDF1F1", color: "#A03636", fontSize: 13 }}>
            Could not load baselines: {err}
          </div>
        )}

        {/* A */}
        {(
          <div style={PANEL}>
            <div style={{ display: "flex", alignItems: "baseline", gap: 14, flexWrap: "wrap", marginBottom: 14 }}>
              <div>
                <div style={KICK}>A · Conversion Rate Standards</div>
                <div style={{ fontSize: 12.5, color: "#6B7C93", marginTop: 3 }}>
                  Live from health_baselines. Healthy = mean ±1σ; every z-score in the rule book is measured against these exact numbers.
                </div>
              </div>
              <div style={{ flex: 1 }}/>
              <div style={{ display: "flex", gap: 6 }}>
                {cohorts.map(c => (
                  <button key={c} onClick={() => setCohort(c)} style={{
                    border: `1px solid ${cohort === c ? "#CFE2F2" : LINE}`, background: cohort === c ? "#EBF3FB" : "#fff",
                    color: cohort === c ? "#1B6EA8" : "#5B6A7A", fontWeight: cohort === c ? 600 : 500,
                    borderRadius: 9, padding: "5px 13px", fontSize: 12, cursor: "pointer" }}>{c}</button>
                ))}
              </div>
            </div>

            {baselines && missing > 0 && (
              <div style={{ background: "#FBF3E3", border: "1px solid #F0DCB4", color: "#8A5B08",
                borderRadius: 10, padding: "9px 13px", fontSize: 12.5, marginBottom: 12 }}>
                {missing} of {METRICS.length} metrics have no stored baseline for {cohort}. Those rows show a dash rather than a
                guess — any rule that reads them scores z = 0 and stays silent. Run Recompute Baseline to fill them.
              </div>
            )}

            <div style={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse" }}>
                <thead><tr>
                  <th style={TH}>Metric</th>
                  <th style={{ ...TH, textAlign: "right" }}>Cohort Mean</th>
                  <th style={{ ...TH, textAlign: "right" }}>SD</th>
                  <th style={{ ...TH, textAlign: "right" }}>Healthy Range</th>
                  <th style={{ ...TH, width: 190 }}>Band</th>
                  <th style={{ ...TH, textAlign: "right" }}>N</th>
                </tr></thead>
                <tbody>
                  {rows.map(r => (
                    <tr key={r.key}>
                      <td style={TD}>
                        <div style={{ fontWeight: 600 }}>{r.label}</div>
                        <div style={{ fontSize: 11.5, color: MUTE, marginTop: 1 }}>{r.desc}</div>
                        <div style={{ fontFamily: MONO, fontSize: 9.5, color: "#C2CDDB", marginTop: 2 }}>{r.key}</div>
                      </td>
                      <td style={NUM}>{r.has ? fmtVal(r.avg, r.unit) : <span style={{ color: "#C2CDDB" }}>—</span>}</td>
                      <td style={NUM}>
                        {r.has ? fmtVal(r.sd, r.unit) : <span style={{ color: "#C2CDDB" }}>—</span>}
                        {r.flat && <div style={{ fontFamily: MONO, fontSize: 9, color: "#A03636", marginTop: 2 }}>SD 0 · z forced to 0</div>}
                      </td>
                      <td style={NUM}>
                        {r.has ? `${fmtVal(r.lo, r.unit)} – ${fmtVal(r.hi, r.unit)}` : <span style={{ color: "#C2CDDB" }}>—</span>}
                      </td>
                      <td style={{ ...TD, width: 190 }}>
                        {r.bar ? (
                          <div style={{ position: "relative", height: 8, borderRadius: 999, background: "#F1F4F8", marginTop: 5 }}>
                            <div style={{ position: "absolute", top: 0, bottom: 0, left: r.bar.left + "%",
                              width: r.bar.width + "%", background: "#CFE2F2", borderRadius: 999 }}/>
                            <div style={{ position: "absolute", top: -2, bottom: -2, left: `calc(${r.bar.mean}% - 1px)`,
                              width: 2, background: "#1B6EA8", borderRadius: 2 }}/>
                          </div>
                        ) : <span style={{ color: "#C2CDDB", fontSize: 11.5 }}>—</span>}
                      </td>
                      <td style={NUM}>
                        {r.n == null ? <span style={{ color: "#C2CDDB" }}>—</span> : r.n}
                        {r.thin && <div style={{ fontFamily: MONO, fontSize: 9, color: "#8A5B08", marginTop: 2 }}>thin</div>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div style={{ fontSize: 11.5, color: MUTE, marginTop: 12, lineHeight: 1.6 }}>
              Cohort scope is the Scott module, split by project type, over a trailing 12-month window. The workbook this engine
              was ported from used a single blended cohort of 52 retainer clients over client lifetime, so figures here are not
              directly comparable to that workbook. A metric with N under 10 or an SD of 0 cannot produce a meaningful z-score.
            </div>
          </div>
        )}

        {/* B */}
        {(
          <div style={PANEL}>
            <div style={{ marginBottom: 14 }}>
              <div style={KICK}>B · Rule Book</div>
              <div style={{ fontSize: 12.5, color: "#6B7C93", marginTop: 3 }}>
                {RULES.length} diagnosis outcomes across {new Set(RULES.map(r => r[0])).size} rule slots in health-engine.js.
                Conditions are the literal ported predicates. Attribution tags are proposed and awaiting your redline.
              </div>
            </div>
            <div style={{ display: "flex", gap: 14, flexWrap: "wrap", marginBottom: 14 }}>
              {[A_PROC, A_CLI, A_STR, A_MIX].map(k => (
                <div key={k} style={{ display: "flex", alignItems: "center", gap: 7 }}>
                  <Tag spec={ATTR[k]}>{k}</Tag>
                  <span style={{ fontSize: 11.5, color: MUTE }}>{RULES.filter(r => r[2] === k).length}</span>
                </div>
              ))}
            </div>

            {RULES.map((r, i) => {
              const [slot, name, attribution, cond, rationale] = r;
              const open = openRule === i;
              const spec = ATTR[attribution];
              return (
                <div key={i} style={{ border: `1px solid ${open ? "#CFE2F2" : "#EDF1F6"}`, borderRadius: 12,
                  marginBottom: 8, background: open ? "#FCFDFF" : "#fff", overflow: "hidden" }}>
                  <div onClick={() => setOpenRule(open ? -1 : i)} style={{ display: "flex", alignItems: "center",
                    gap: 12, padding: "11px 14px", cursor: "pointer" }}>
                    <span style={{ fontFamily: MONO, fontSize: 11, color: "#C2CDDB", width: 26 }}>{slot}</span>
                    <span style={{ fontSize: 13.5, fontWeight: 600, color: INK, flex: 1 }}>{name}</span>
                    <Tag spec={spec}>{attribution}</Tag>
                    <span style={{ color: "#C2CDDB", fontSize: 12, width: 12 }}>{open ? "▾" : "▸"}</span>
                  </div>
                  {open && (
                    <div style={{ padding: "0 14px 14px 52px" }}>
                      <div style={{ fontFamily: MONO, fontSize: 11.5, color: "#3E5063", background: "#F6F8FB",
                        border: "1px solid #EDF1F6", borderRadius: 9, padding: "9px 12px", lineHeight: 1.65 }}>{cond}</div>
                      <div style={{ marginTop: 11 }}>
                        <div style={{ ...KICK, fontSize: 9.5 }}>Rationale</div>
                        <div style={{ fontSize: 12.5, color: "#4A5B6E", marginTop: 3, lineHeight: 1.6 }}>{rationale}</div>
                      </div>
                      <div style={{ marginTop: 10 }}>
                        <div style={{ ...KICK, fontSize: 9.5 }}>Recommended Action</div>
                        <div style={{ fontSize: 12.5, color: "#4A5B6E", marginTop: 3, lineHeight: 1.6 }}>{actionFor(name)}</div>
                      </div>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}

      </div>
    );
  }

  window.StandardsScreen = StandardsScreen;
})();