
/* ============================================================================
   PC5251 suite tokens v1
   ----------------------------------------------------------------------------
   Extracted into the PC5251 design system; see design/DESIGN.md ("the master").
   Every value in section 1 is copied VERBATIM from the master's :root blocks.
   Sections 2-5 are the additions the suite needs; each one records where its
   value came from and, where a colour carries a label, the measured contrast.

   Inlined at build time at the <!--__TOKENS__--> marker. No CDN, no external
   stylesheet, no @import — a demo page must stay a single self-contained file.

   THEME CONTRACT (all three layers below are mandatory; the page must also
   ship a ?theme=dark|light reader, an in-page #themeBtn toggle, and a canvas
   repaint wired to BOTH matchMedia("(prefers-color-scheme: dark)") change AND
   a MutationObserver on documentElement's data-theme attribute — CSS
   re-themes itself, canvas pixels do not):

     1. :root                                            -> light values
     2. @media (prefers-color-scheme: dark)
          :root:where(:not([data-theme="light"]))        -> OS dark
     3. :root[data-theme="dark"]                         -> explicit dark

   Layer 3 is emitted VERBATIM as a second copy of layer 2's body. CSS has no
   way to share a declaration block between a media query and a selector, so
   the duplication is structural, not an oversight. Keep the two in sync.
   :where() keeps layer 2 at specificity (0,1,0) so layer 3's (0,2,0) attribute
   selector always wins — the toggle beats the OS in both directions, and the
   :not([data-theme="light"]) guard lets an explicit light stamp beat OS dark.
   ============================================================================ */

:root{
  color-scheme: light;

  /* -- 1. FROM THE MASTER, VERBATIM ---------------------------------------- */
  /* surfaces & ink */
  --surface-1:#fcfcfb; --page:#f9f9f7;
  --text-primary:#0b0b0b; --text-secondary:#52514e; --muted:#898781;
  --grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,0.10);
  /* categorical series 1-3 (slots 4-8 are added in section 2) */
  --series-1:#2a78d6; --series-2:#eb6834; --series-3:#1baf7a;
  /* sequential ramp, light: blue steps 250 -> 700 */
  --ramp-0:#86b6ef; --ramp-1:#3987e5; --ramp-2:#256abf; --ramp-3:#184f95; --ramp-4:#0d366b;
  /* sequential "heat" ramp: matplotlib plasma, sampled at 0, .25, .5, .75, 1.
     A SECOND sequential encoder, not a replacement: the house --ramp-* stays the
     default for every sequential encoding, and --heat-* is for an UNSIGNED
     INTENSITY read on a spatial layout (a detector map, an image, a pixel grid),
     where single-hue blue spends its whole range on lightness and the structure
     of the layout goes with it. Plasma varies hue AND lightness monotonically
     (near-black violet -> magenta -> orange -> bright yellow), which is what makes
     the structure legible. Values are IDENTICAL in the dark blocks below: the ramp
     is already dark at its low end and bright at its high end, so it reads on
     #fcfcfb and on #1a1a19 without flipping, and re-anchoring it per theme would
     change which value is "hot". The declarations are still repeated in both dark
     layers because the theme contract is structural — every non-status token that
     the light block declares is declared in both dark layers. */
  --heat-0:#0d0887; --heat-1:#7e03a8; --heat-2:#cc4778; --heat-3:#f89540; --heat-4:#f0f921;
  /* diverging: blue <-> neutral <-> red */
  --div-mid:#f0efec; --div-neg:#2a78d6; --div-pos:#e34948;
  /* status. FIXED — deliberately NOT re-declared in the dark blocks, exactly as
     the master leaves them: a status colour never changes meaning with theme.
     --warning is 1.79:1 on the light surface by design; the icon + label pairing
     in components.css .flag is the mitigation, plus a light-mode text override. */
  --good:#0ca30c; --warning:#fab219; --critical:#d03b3b;

  /* -- 2. ADDED: type stacks, recessed surface, accent wash, series 4-8 ----- */

  /* The master hard-codes these four times (body, and three canvas font
     literals). Tokenised so one change reaches prose AND chart labels. */
  --font: system-ui,-apple-system,"Segoe UI",sans-serif;
  --mono: ui-monospace,SFMono-Regular,Menlo,monospace;

  /* Recessed panel — the well inside a card (evidence blocks, code, callouts).
     The master has no token for this and paints such wells with var(--page),
     which collapses when the card itself sits directly on the page.
     Light value #f0efec is NOT invented: it is the master's own --div-mid, i.e.
     an existing member of its neutral family, one step deeper than --page
     (OKLCH L .952 vs page .982 vs surface-1 .991, grid .906). Muted text still
     reads on it at 3.12:1, secondary at 6.90:1, primary at 17.12:1. */
  --surface-2:#f0efec;

  /* 10% wash of --series-1 — selected rows, hovered cells, active pills. */
  --accent-soft: rgba(42,120,214,.10);

  /* Categorical slots 4-8. Slots 1-3 above are the master's; 4-7 are the
     design-audit candidates; slot 8 was re-derived (see the freeze note at the
     bottom of this file). Validated as an 8-slot set — ALL CHECKS PASS, light
     surface #fcfcfb:
       lightness band OK · chroma floor OK
       worst adjacent CVD  #1baf7a<->#eb6834  dE 9.2 (deutan), tritan 11.4
       worst adjacent normal  #0d9488<->#b07d18  dE 18.4
       relief required for --series-3 (2.74:1) — direct label or table view. */
  --series-4:#8a57b5;   /* violet   4.98:1 */
  --series-5:#b07d18;   /* gold     3.53:1 */
  --series-6:#0d9488;   /* teal     3.65:1 */
  --series-7:#a83232;   /* red      6.46:1 */
  --series-8:#d06bbc;   /* magenta  3.14:1 */

  /* -- 3. WORKFLOW STAGE CHROME (course diagram) --------------------------- */
  /* Numbered tab circles, ribbon segments, stage rules. Contrast measured
     against --stage-ink, which is the label colour sitting ON the chip. The
     4.5:1 target is the suite rule; tbb_cluster_explorer recorded the only
     prior reasoning ("darkened from the diagram's #c55a11 to clear 4.5:1 with
     white label text") and it is now written down here for every stage. */
  --stage-1:#24466f;   /* navy  — data import & preprocessing   9.63:1 on white */
  --stage-2:#a75c1d;   /* rust  — data transformation           5.01:1 on white */
  --stage-3:#28734e;   /* green — modelling & training          5.75:1 on white */
  --stage-4:#59616e;   /* slate — evaluation & output           6.25:1 on white */
  --stage-ink:#ffffff;

  /* -- 4. DESIGN-MATRIX SEMANTICS ------------------------------------------ */
  /* Stable meanings across the whole suite: a row highlight is this blue on
     every page, never a per-demo choice. Pair the colour with slice notation
     (X[i,:] / X[:,j] / x_ij) so the encoding survives colour blindness. */
  --matrix-row:#2a78d6;    /* X[i,:]  4.30:1 on --surface-1 */
  --matrix-column:#bb5410; /* X[:,j]  4.66:1 */
  --matrix-cell:#7446a8;   /* x_ij    6.42:1 */

  /* -- 5. FROZEN ENTITY COLOURS -------------------------------------------- */
  /* The four CS2 battery cells. The cell -> hue ASSIGNMENT IS FROZEN and must
     never be re-mapped: these hexes already ship in battery_soh_explorer, where
     #d36528 is simultaneously CS2_38 and the held-out test-cell orange. Dark
     mode lifts lightness only; the assignment is identical in both modes. */
  --cs2-35:#3077c9;  /* 4.44:1 on --surface-1 */
  --cs2-36:#27835a;  /* 4.57:1 */
  --cs2-37:#8a57b5;  /* 4.98:1 */
  --cs2-38:#d36528;  /* 3.61:1 — also the held-out / test-protocol orange */

  /* -- 6. COMPAT ALIASES ---------------------------------------------------- */
  /* The migration trick. The suite currently runs three incompatible token
     vocabularies (A: --bg/--panel/--ink...; B: --page/--surface/--text...;
     C: tbb's --surface-1/--text-primary/--baseline/--sel-ring). Every canvas in
     every demo reads its colours through
     getComputedStyle(documentElement).getPropertyValue(name), so a stylesheet
     keyed on the wrong vocabulary is a silent no-op on BOTH the CSS and the
     charts. These aliases let a page keep every existing var(--old) reference
     and every css("--old") canvas call working while the VALUES move onto the
     master's palette — a demo migrates by deleting its own :root block, nothing
     else, and is then renamed to the canonical names at leisure.

     Declared once, in the light block only, ON PURPOSE: an alias resolves
     var(--page) against whichever --page declaration wins on :root, so it
     follows the theme automatically. Re-declaring them in the dark blocks would
     be dead weight that can silently drift out of sync.

     Aliases are a migration shim, not API. New pages use the canonical names. */
  --bg:              var(--page);
  --panel:           var(--surface-1);
  --ink:             var(--text-primary);
  --ink2:            var(--text-secondary);
  --ink3:            var(--muted);
  --line:            var(--grid);
  --line2:           var(--axis);
  --surface:         var(--surface-1);
  --text:            var(--text-primary);
  --secondary:       var(--text-secondary);
  --accent:          var(--series-1);
  --baseline:        var(--axis);
  --sel-ring:        var(--series-1);
  --science-bg:      var(--surface-2);   /* retires the one-off teal #eaf3f4 */
  --science-border:  var(--axis);        /* retires the one-off teal #bdd3d7 */
}

/* ---------------------------------------------------------------------------
   DARK — layer 2 of 3: the OS preference.
   Body is identical to layer 3 below. Edit both or neither.
   --------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark){
  :root:where(:not([data-theme="light"])){
    color-scheme: dark;

    /* from the master, verbatim */
    --surface-1:#1a1a19; --page:#0d0d0d;
    --text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
    --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
    --series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
    --ramp-0:#184f95; --ramp-1:#256abf; --ramp-2:#3987e5; --ramp-3:#6da7ec; --ramp-4:#cde2fb;
    /* plasma, unchanged from light — see the note on --heat-* above */
    --heat-0:#0d0887; --heat-1:#7e03a8; --heat-2:#cc4778; --heat-3:#f89540; --heat-4:#f0f921;
    --div-mid:#383835; --div-neg:#3987e5; --div-pos:#e66767;
    /* --good/--warning/--critical intentionally absent: fixed status palette */

    /* added */
    --surface-2:#121211;                 /* between --page (L .159) and --surface-1
                                            (L .217) at L .182, same warm-neutral
                                            r=g>b as the rest of the dark family;
                                            muted still reads at 5.22:1 */
    --accent-soft: rgba(57,135,229,.13);

    /* series 4-8, dark. Slots 4-6 are the light hues re-stepped to the master's
       own dark lightness (every one of its dark slots sits at OKLCH L ~ .622),
       hue and chroma preserved. Slot 7 is deliberately stepped to L .579 rather
       than .622: at .622 red and magenta collapsed to dE 6.9 (normal vision) and
       5.9 (deutan) and the set FAILED. Validated as an 8-slot set — ALL CHECKS
       PASS, dark surface #1a1a19:
         lightness band OK · chroma floor OK · all 8 clear 3:1
         worst adjacent CVD  #199e70<->#d95926  dE 9.4 (deutan), tritan 8.1
         worst adjacent normal  #cc68b9<->#c54d49  dE 15.6 */
    --series-4:#9f6bcb;   /* violet   4.51:1 */
    --series-5:#af7c16;   /* gold     4.74:1 */
    --series-6:#1d9b8f;   /* teal     5.09:1 */
    --series-7:#c54d49;   /* red      3.76:1 */
    --series-8:#cc68b9;   /* magenta  5.18:1 */

    /* stage chrome — ink flips to near-black on the lifted chips */
    --stage-1:#86afe0;   /*  8.53:1 on --stage-ink */
    --stage-2:#e3a26d;   /*  8.94:1 */
    --stage-3:#75c69c;   /*  9.55:1 */
    --stage-4:#b6bec9;   /* 10.36:1 */
    --stage-ink:#0d0d0d;

    --matrix-row:#68a9f4;    /* 7.11:1 on --surface-1 */
    --matrix-column:#ee985c; /* 7.70:1 */
    --matrix-cell:#b993e6;   /* 6.95:1 */

    /* frozen assignment, lightness lifted only */
    --cs2-35:#69a8ed;  /* 6.99:1 */
    --cs2-36:#61bc8d;  /* 7.54:1 */
    --cs2-37:#bc8ae0;  /* 6.51:1 */
    --cs2-38:#f19b65;  /* 7.98:1 */
  }
}

/* ---------------------------------------------------------------------------
   DARK — layer 3 of 3: the explicit stamp (?theme=dark or the #themeBtn
   toggle). VERBATIM COPY of layer 2's body. Higher specificity than layer 2,
   so it wins over the OS preference in both directions.
   --------------------------------------------------------------------------- */
:root[data-theme="dark"]{
  color-scheme: dark;

  /* from the master, verbatim */
  --surface-1:#1a1a19; --page:#0d0d0d;
  --text-primary:#ffffff; --text-secondary:#c3c2b7; --muted:#898781;
  --grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10);
  --series-1:#3987e5; --series-2:#d95926; --series-3:#199e70;
  --ramp-0:#184f95; --ramp-1:#256abf; --ramp-2:#3987e5; --ramp-3:#6da7ec; --ramp-4:#cde2fb;
  /* plasma, unchanged from light — see the note on --heat-* above */
  --heat-0:#0d0887; --heat-1:#7e03a8; --heat-2:#cc4778; --heat-3:#f89540; --heat-4:#f0f921;
  --div-mid:#383835; --div-neg:#3987e5; --div-pos:#e66767;
  /* --good/--warning/--critical intentionally absent: fixed status palette */

  /* added */
  --surface-2:#121211;
  --accent-soft: rgba(57,135,229,.13);

  --series-4:#9f6bcb;
  --series-5:#af7c16;
  --series-6:#1d9b8f;
  --series-7:#c54d49;
  --series-8:#cc68b9;

  --stage-1:#86afe0;
  --stage-2:#e3a26d;
  --stage-3:#75c69c;
  --stage-4:#b6bec9;
  --stage-ink:#0d0d0d;

  --matrix-row:#68a9f4;
  --matrix-column:#ee985c;
  --matrix-cell:#b993e6;

  --cs2-35:#69a8ed;
  --cs2-36:#61bc8d;
  --cs2-37:#bc8ae0;
  --cs2-38:#f19b65;
}

/* ============================================================================
   SERIES FREEZE NOTE — how slots 4-8 were settled (do not re-litigate silently)

   Command of record (both must exit 0 before this file changes):
     node <dataviz>/scripts/validate_palette.js \
       "#2a78d6,#eb6834,#1baf7a,#8a57b5,#b07d18,#0d9488,#a83232,#d06bbc" \
       --mode light --surface "#fcfcfb"
     node <dataviz>/scripts/validate_palette.js \
       "#3987e5,#d95926,#199e70,#9f6bcb,#af7c16,#1d9b8f,#c54d49,#cc68b9" \
       --mode dark  --surface "#1a1a19"

   Two candidates from the audit had to move:

   * slot 8, light. The audit proposed #6b7280 (jae's "Other" slate). It FAILS
     the chroma floor at OKLCH C 0.023 — below 0.10 a hue reads as gray. The
     obvious repair, lifting chroma at constant L and hue, is WORSE than the
     defect: it walks slot 8 straight into slot 1's blue (measured normal-vision
     dE to --series-1 drops from 14.2 at C .023 to 6.6 at C .104). The next
     nearest passing step by hue was olive (h ~120), which FAILS CVD against the
     adjacent slot-7 red at dE 1.1 under deutan — a red/green pair, predictably.
     The only unoccupied region of the wheel that clears every gate is magenta,
     h ~335, which is also the hue family the reference palette itself uses.
     Frozen at #d06bbc / #cc68b9.

   * slot 7, dark. See the inline note above: L .579 not the house .622.

   All-pairs caveat: this is an ADJACENT-pair palette (stacks, bars, lines,
   legends read top to bottom). Under --pairs all — scatter, bubble, choropleth,
   small multiples — eight slots cannot clear the floors in ANY ordering. Those
   chart forms carry a series cap: use the first three or four slots and fold the
   rest into "Other", or facet. That cap is a rule about chart FORM, not a defect
   in these hexes.
   ============================================================================ */

/* ============================================================================
   PC5251 suite base v1  —  reset, document type scale, layout primitives
   ----------------------------------------------------------------------------
   Extracted into the PC5251 design system; see design/DESIGN.md. Rules here are
   the master's, with hard-coded font stacks replaced by var(--font)/var(--mono)
   so a suite-level type change reaches every page (and, via the same tokens,
   every canvas label).

   Inlined at build time at the <!--__BASE__--> marker, AFTER tokens.css.
   Component chrome (tabs, ribbon, filters, legends, findings, heatmaps, tips)
   lives in components.css.

   THE SUITE TYPE SCALE, in one place:
     h1        26px / 650 / -.01em      page title
     h2        18px / 620 / -.005em     section (card) title
     h3        13px / 600               sub-head inside a card
     body      15px / 1.55              var(--font)
     .sub      14px                     deck under h1, or under an h2
     .note     12.5px  --muted          caption, provenance, scale footnote
     table     13.5px ; th 12.5 / 600 / uppercase / .02em
     .hero     52px / 640 / -.02em      the one big number on a panel
     .stat .v  24px / 620 / -.01em      stat-row values
     .kicker   11.5px / 600 / uppercase suite eyebrow  (components.css)
   Sizes below 12px belong to canvas labels and legends only — never to prose.
   .hero/.stat are DECLARED in components.css (they are components); they are
   listed here so the scale can be read as one ladder.

   Numerals: proportional figures everywhere by default. font-variant-numeric:
   tabular-nums is reserved for columns that must align vertically — table
   number cells, axis ends, stat rows, tooltip readouts.
   ============================================================================ */

/* -- reset ----------------------------------------------------------------- */
*{box-sizing:border-box}

body{
  margin:0; background:var(--page); color:var(--text-primary);
  font:15px/1.55 var(--font);
  -webkit-font-smoothing:antialiased;
  /* A DOI, a file path or a 40-char accession has no break opportunity in it.
     In a 296px card on a phone such a token is wider than its box and the text
     is simply lost off the right edge (measured: pleiades .note, 475px of
     content in a 296px column). Breaking inside the word is the only way a
     narrow column can hold it, and it costs nothing at desktop widths where the
     token already fits. */
  overflow-wrap:break-word;
}

/* -- page frame ------------------------------------------------------------ */
/* 24px of gutter each side is 12% of a 390px phone screen. The content column
   is the scarcest thing on a phone, so the gutter gives some of it back — the
   card's own 22px of padding still separates text from the card edge. */
.wrap{max-width:1240px;margin:0 auto;padding:32px 24px 64px}
/* Insight shell: full available width, with the shared responsive page gutters. */
[data-page-role="insight"] .wrap{max-width:none}
@media (max-width:640px){.wrap{padding:22px 16px 48px}}
@media (max-width:400px){.wrap{padding:20px 13px 44px}}

/* -- document type --------------------------------------------------------- */
h1{font-size:26px;font-weight:650;letter-spacing:-.01em;margin:0 0 6px}
h2{font-size:18px;font-weight:620;letter-spacing:-.005em;margin:0 0 4px}
h3{font-size:13px;font-weight:600;margin:0 0 2px;color:var(--text-primary)}
p{margin:0 0 10px;color:var(--text-secondary)}
.sub{color:var(--text-secondary);font-size:14px;margin:0 0 4px}
.note{font-size:12.5px;color:var(--muted);margin:6px 0 0}
ul.tight{margin:6px 0 0;padding-left:18px;color:var(--text-secondary);font-size:13.5px}
ul.tight li{margin-bottom:5px}
.kbd{font-size:11.5px;color:var(--muted)}

code{
  font-family:var(--mono);font-size:.9em;background:var(--surface-2);
  padding:1px 5px;border-radius:4px;border:1px solid var(--border)
}
a{color:var(--series-1)}

/* -- keyboard focus --------------------------------------------------------
   One visible ring for every focusable thing on every page: tabs, ribbon
   segments, hand-off buttons, selects, links. :focus-visible only, so a mouse
   click never paints it — DESIGN.md 3.2 requires ribbon segments to be
   "keyboard-reachable, focus-visible" and the UA default ring is not a
   suite decision. */
:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}

/* -- card: the only container. FLAT — the suite ships no card shadow.
      A shadow appears on exactly one thing, the tooltip (components.css #tip),
      because a tooltip genuinely floats above the page and a card does not. -- */
.card{
  background:var(--surface-1);border:1px solid var(--border);
  border-radius:12px;padding:20px 22px;margin:0 0 20px
}

/* -- tables ---------------------------------------------------------------- */
table{border-collapse:collapse;width:100%;font-size:13.5px}
th,td{text-align:left;padding:7px 12px 7px 0;border-bottom:1px solid var(--grid);vertical-align:top}
th{font-weight:600;color:var(--text-secondary);font-size:12.5px;letter-spacing:.02em;text-transform:uppercase}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums;padding-right:18px}
tr:last-child td{border-bottom:none}

/* A long table is scrolled inside its own box, never by the page. Sticky
   header keeps the column names attached to the rows. */
.tablescroll{
  width:100%;max-width:100%;min-width:0;box-sizing:border-box;contain:inline-size;
  max-height:340px;overflow:auto;margin-top:12px;border:1px solid var(--border);border-radius:8px
}
.tablescroll table{font-size:12.5px}
.tablescroll th{position:sticky;top:0;background:var(--surface-1);z-index:1;padding:8px 12px 8px 10px;border-bottom:1px solid var(--axis)}
.tablescroll td{padding:5px 12px 5px 10px}
.tablescroll td:first-child,.tablescroll th:first-child{padding-left:14px}
/* -- grids ----------------------------------------------------------------- */
/* minmax(0,1fr) — not 1fr — plus min-width:0 on the children. A grid track
   defaults to min-content, so ONE wide table or canvas inside a 1fr column
   pushes the whole page into horizontal scroll. This pair is what keeps a
   wide child scrolling inside its own box instead. */
.gridtop{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;margin-bottom:20px}
.gridmain{display:grid;grid-template-columns:minmax(0,1.42fr) minmax(0,1fr);gap:20px;margin-bottom:20px}
.gridtop > *,.gridmain > *{min-width:0}
@media (max-width:1000px){.gridtop,.gridmain{grid-template-columns:minmax(0,1fr)}}

/* -- disclosure ------------------------------------------------------------ */
/* Everything a reader can check but does not have to read — full tables,
   test statistics, per-row dumps — goes behind one of these. */
details{border-top:1px solid var(--grid);margin-top:16px;padding-top:12px}
summary{cursor:pointer;font-size:13.5px;font-weight:560;color:var(--text-secondary);list-style:none}
summary::-webkit-details-marker{display:none}
summary:before{content:"\25B8\00A0";color:var(--muted)}      /* ▸ */
details[open] summary:before{content:"\25BE\00A0"}           /* ▾ */
/* A bare summary line is ~19px tall — half the 40px a finger needs, and a
   <details> is the one control standing between the reader and a table twin
   (DESIGN.md 7.9). Padding, not a height, so the disclosure marker keeps its
   place in the text flow and the toggle keeps working in every engine. */
@media (max-width:640px){summary{padding:11px 0}}

/* -- canvas ---------------------------------------------------------------- */
/* SCROLL FIRST (2026-08-31, owner review on a real handset). This was
   touch-action:none, so that a pointermove hover on a chart would not scroll
   the page out from under the reader's finger. On a phone that trade is
   backwards: the hover it was protecting is unreadable there anyway (#tip
   lands under the finger and is taken away at lift), and what it actually
   bought was a page the reader could not scroll off a chart — on a handset
   most of the analysis tab IS charts.

   So the vertical gesture and the pinch go back to the browser, where they
   belong, and the readout problem is solved where it lives: PC.hover switches
   to tap-to-select on a touch device and docks the payload in a .pc-readout
   strip above the plot (components.css §23E). Nothing in the suite calls
   preventDefault on a touch event, so this declaration is the whole of the
   scroll policy.

   pan-x is deliberately NOT granted: the horizontal gesture stays with the
   page, held in reserve for a future scrub-along-the-x-axis, and withholding
   it also stops a diagonal drag from being read as a chart interaction. */
canvas{touch-action:pan-y pinch-zoom}
img,canvas,svg{max-width:100%}

/* -- motion ---------------------------------------------------------------- */
/* Kills transitions AND any auto-advancing animation loop's visible effect.
   A page that auto-plays (epoch sweeps, cycling panels) must ALSO gate its
   timer in JS on matchMedia("(prefers-reduced-motion: reduce)").matches —
   CSS cannot stop a setInterval. */
@media (prefers-reduced-motion: reduce){
  *{transition:none !important;animation:none !important;scroll-behavior:auto !important}
}

/* ============================================================================
   PC5251 suite components v1
   ----------------------------------------------------------------------------
   Extracted into the PC5251 design system; see design/DESIGN.md. Everything in
   sections 1-11 is the master's component set, with three systematic edits:

     a) hard-coded font stacks -> var(--font) / var(--mono)
     b) recessed fills that the master paints with var(--page) -> var(--surface-2)
        (--page is only 0.9% lighter than --surface-1 in light mode, so a well
        drawn in it vanishes on a card; --surface-2 is the token for this)
     c) grid children carry min-width:0 (see base.css for why)

   Sections 3, 3B and 12-16 are NEW and have no counterpart in the master:
   the two workflow figures (3 the header pipeline diagram, 3B the analysis
   workflow map that replaces the old .learning-map tiles), the suite identity
   chrome (kicker, back-link, series-nav) and the next-stage footer link.

   Inlined at build time at the <!--__COMPONENTS__--> marker, AFTER base.css.
   ============================================================================ */


/* == 1. HEADER ============================================================== */
header.top{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:18px}
header.top .id{min-width:0}

#themeBtn{
  background:var(--surface-1);color:var(--text-secondary);border:1px solid var(--border);
  border-radius:8px;padding:7px 13px;font:inherit;font-size:13px;cursor:pointer;white-space:nowrap
}
#themeBtn:hover{color:var(--text-primary);border-color:var(--axis)}


/* == 1B. PLAIN STORY STRIP (.storystrip) ====================================
   The first thing on the page, directly under the masthead and ABOVE the
   workflow figure: title → story strip → pipeline diagram → tabs. A headline
   question, then three panels — the data · the question · the answer — one
   sentence each, from PROVENANCE.md's `story` block, each with a small
   thumbnail the demo's own painter draws from its own payload.
   Rendered by PC.renderStory(). DESIGN.md 2.4, 10.4.

   Its reader has ZERO BACKGROUND, and the styling is what protects that: the
   sentence is the panel's largest element, the thumbnail is small enough to be
   read as an illustration rather than a chart to interrogate, and nothing here
   is a control.

     - no <button>, no href, no click handler, no cursor:pointer, no hover
       lift, no focus ring. It is a figure, marked role="figure".
     - the panel ordinal is a PLAIN numeral in --muted. It is never a numbered
       circle and never carries a stage colour: a numbered circle on this page
       means "pipeline stage" and nothing else (3.5).
   ========================================================================== */
.storystrip{margin:0 0 22px}
/* The headline is a question, and it is the page's second-loudest line after
   the h1 — so it takes the h2 step of the house type scale (5.1) rather than a
   size of its own. */
.storystrip .storyq{
  font-size:18px;font-weight:620;letter-spacing:-.005em;
  color:var(--text-primary);margin:0 0 12px;max-width:78ch
}
.storypanels{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.storypanels > *{min-width:0}
.storypanel{
  display:flex;flex-direction:column;gap:8px;min-width:0;
  background:var(--surface-1);border:1px solid var(--border);
  border-radius:10px;padding:13px 15px
}
.storypanel .storylabel{
  font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);font-weight:600
}
.storypanel .storyn{margin-right:7px;font-variant-numeric:tabular-nums}
/* Painted by the demo's own callback through PC.fit(), so the box is CSS-sized
   here and DPR-sized there. --surface-2 is the recessed fill, so a painter that
   draws nothing leaves a well rather than a hole. */
.storypanel .storycanvas{
  display:block;width:100%;height:72px;border-radius:5px;
  background:var(--surface-2)
}
.storypanel .storytext{font-size:13px;color:var(--text-secondary);line-height:1.45;margin:0}
/* A panel whose painter is missing (or threw) is sentence-only: no canvas
   element at all, so no empty box and no reserved space. */
.storypanel.nothumb .storycanvas{display:none}

/* Three panels of prose need room. Below this the strip stacks, which also
   puts the three sentences in reading order on a phone. */
@media (max-width:760px){
  .storypanels{grid-template-columns:minmax(0,1fr)}
  .storystrip .storyq{font-size:17px}
  .storypanel .storycanvas{height:64px}
}


/* == 2. TABS ================================================================
   Two levels. .tabs alone = the demo's own sub-tabs inside a panel.
   .tabs.toptabs = the five canonical suite tabs, which scope the whole view.
   ========================================================================== */
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin:0 0 20px;flex-wrap:wrap}
.tab{
  background:none;border:none;border-bottom:2px solid transparent;margin-bottom:-1px;
  font:inherit;font-size:14px;font-weight:560;color:var(--text-secondary);
  padding:10px 15px;cursor:pointer;border-radius:8px 8px 0 0
}
.tab:hover{color:var(--text-primary);background:var(--surface-1)}
.tab[aria-selected="true"]{color:var(--text-primary);border-bottom-color:var(--series-1)}
.tab .t2{display:block;font-size:11px;font-weight:400;color:var(--muted);margin-top:1px}

/* top-level tabs sit above everything and scope the whole view */
.toptabs{
  position:sticky;top:0;z-index:30;background:var(--page);
  margin:0 0 24px;padding-top:2px;gap:2px
}
.toptabs .tab{
  font-size:14.5px;font-weight:600;padding:11px 15px;
  display:inline-flex;align-items:center
}
.toptabs .tab[aria-selected="true"]{border-bottom-width:2px}
.toptabs .tab .tlabel{display:flex;flex-direction:column;align-items:flex-start;text-align:left;min-width:0}

/* NO stage circle on a tab chip, and no data-stage attribute on one either.
   The workflow is the DATA pipeline — preprocess, transform, model, evaluate —
   and a tab is a place to read, not a step the data goes through. Five tabs
   never lined up with four stages, and painting the chips sent the reader
   hunting for the workflow in the chrome instead of in the two figures that
   actually draw it (section 3 .ribbon, section 3B .wfmap). DESIGN.md 3.1. */

/* Glossary: right-aligned — reference material, outside the reading order. */
.toptabs .tab[data-role="glossary"]{margin-left:auto}

.toppanel[hidden]{display:none}
.panel[hidden]{display:none}
.tab:focus-visible{outline:2px solid var(--series-1);outline-offset:2px;border-radius:4px}

/* -- the tab strip on a phone ----------------------------------------------
   MEASURED DEFECT: five two-line chips in a flex-wrap strip stack to one chip
   per row on a narrow screen — 263px tall on most demos, 329px on
   superconductor and jae. The strip is position:sticky;top:0, so that block
   stays pinned: on an 844px phone it holds 31-39% of the screen for chrome,
   and on a 667px handset it leaves barely half the viewport for the page.

   The fix is the one idiom that keeps five chips on one row without shrinking
   them below a fingertip: the strip becomes a horizontal scroller. The chips
   keep their size, their sub-labels and their order; the reader swipes the
   strip instead of the strip eating the page. Height drops from 263-329px to
   about 60px.

   Nothing here changes tab SEMANTICS: role, aria-selected, roving tabindex,
   the hidden attribute on panels and the arrow/Home/End keys are all JS
   (DESIGN.md 3.1) and are untouched. selectTop() scrolls the selected chip
   back into view through scroll-margin-inline, so a keyboard or .continue
   hand-off cannot leave the current tab parked off-screen. */
@media (max-width:640px){
  .toptabs{
    flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;                       /* the chips are the affordance */
    overscroll-behavior-x:contain;              /* a swipe must not page-back */
    scroll-snap-type:x proximity
  }
  .toptabs::-webkit-scrollbar{display:none}
  .toptabs .tab{
    flex:0 0 auto;scroll-snap-align:start;scroll-margin-inline:12px;
    min-height:44px;padding:9px 12px         /* 44px: the fingertip floor */
  }
  /* Glossary's margin-left:auto is a right-edge push that only means anything
     in a strip that spans the viewport. In a scroller it would open a gap of
     whatever slack is left, so the chip simply keeps its place at the end. */
  .toptabs .tab[data-role="glossary"]{margin-left:0}
  .toptabs .tab .tlabel{white-space:nowrap}
  /* The .t2 sub-label is hidden at phone width (owner review, 2026-08-28): it
     doubles every chip's height and roughly doubles the strip's scroll length,
     and on a handset the five tab NAMES are what the reader is choosing
     between. It is display:none rather than removed, so the text stays in the
     DOM — the payload still carries it, the desktop layout still shows it, and
     it costs nothing to restore. Chip height falls 63px -> 44px, which is also
     the fingertip floor. */
  .toptabs .tab .t2{display:none}
}


/* == 3. HEADER PIPELINE DIAGRAM (.ribbon) ===================================
   The workflow is the DATA pipeline — preprocess, transform, model, evaluate —
   so this strip is a FIGURE of that pipeline: the whole of it, starting at
   stage 1, drawn once under the masthead with the page's own payload numbers.
   It is NOT a nav bar and NOT a tab index, and the styling has to say so:

     - no <button>, no href, no click handler (see the role skeletons)
     - no cursor:pointer, no hover lift, no focus ring — nothing in here may
       offer itself as something to press
     - no navigation role and no current-step ARIA marker: a figure of the
       whole pipeline has no notion of a "current" segment. The container is
       marked as a figure, labelled "Analysis workflow".

   The class name is historical — the component is a diagram. DESIGN.md 3.2.
   ========================================================================== */
.ribbon{display:flex;flex-wrap:wrap;align-items:stretch;row-gap:8px;margin:0 0 22px}
.ribbon .rseg{
  flex:1 1 168px;min-width:0;
  display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:11px;row-gap:1px;
  align-items:start;
  background:var(--surface-1);border:1px solid var(--border);
  border-left:3px solid var(--stage-1);border-radius:9px;
  padding:9px 13px;text-align:left
}
.ribbon .rseg[data-stage="2"]{border-left-color:var(--stage-2)}
.ribbon .rseg[data-stage="3"]{border-left-color:var(--stage-3)}
.ribbon .rseg[data-stage="4"]{border-left-color:var(--stage-4)}
/* The stage circle, at the same geometry the workflow map uses (3B), so a
   numbered circle means "pipeline stage" everywhere on the page and nothing
   else does. Full opacity on every segment: none of them is "current". */
.ribbon .rseg[data-stage]::before{
  content:attr(data-stage);grid-column:1;grid-row:1 / span 3;align-self:center;
  width:21px;height:21px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;
  font-size:11.5px;font-weight:650;line-height:1;font-variant-numeric:tabular-nums;
  background:var(--stage-1);color:var(--stage-ink)
}
.ribbon .rseg[data-stage="2"]::before{background:var(--stage-2)}
.ribbon .rseg[data-stage="3"]::before{background:var(--stage-3)}
.ribbon .rseg[data-stage="4"]::before{background:var(--stage-4)}
.ribbon .rseg .rl{
  grid-column:2;
  font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
.ribbon .rseg .rv{grid-column:2;font-size:16px;font-weight:620;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.ribbon .rseg .rn{grid-column:2;font-size:11.5px;color:var(--text-secondary);line-height:1.35}
.ribbon .rsep{
  flex:0 0 auto;display:flex;align-items:center;padding:0 8px;
  color:var(--muted);font-size:15px;line-height:1;user-select:none
}
/* Below the wrap point the arrows would point at nothing — drop them and let
   the segments stack full width. */
@media (max-width:760px){
  .ribbon .rsep{display:none}
  .ribbon .rseg{flex:1 1 100%}
}


/* == 3C. HEADER WORKFLOW DAG (.dagfig) ======================================
   The header figure, when the payload carries PROVENANCE.md's v2 card
   (`provenance_card`). Four stage columns in --stage-1..4, each headed by the
   stage name and the headline number section 3's rollup already carried, and
   filled with THAT DEMO'S OWN workflow steps as nodes, with the card's `needs`
   edges drawn between them. DESIGN.md 3.2, 10.4, 10.6.

   Everything section 3 says about the rollup applies here unchanged:
     - no <button>, no href, no click handler, no cursor:pointer, no hover
       lift, no focus ring. It is a figure, marked role="figure".
     - no navigation role and no current-step marker: a picture of the whole
       pipeline has no notion of a "current" column.

   Two structural notes:
     - .dagcols carries position:relative and z-index:1 so the absolutely
       positioned edge layer under it paints BEHIND the node cards. An edge
       therefore emerges from behind a card rather than crossing it.
     - .dagroll is the SAME .ribbon markup section 3 styles, kept in the DOM
       and swapped in by the 720px query below. A display:none subtree is
       hidden from assistive tech as well, so the figure is never announced
       twice, and the narrow layout can never drift from the wide one.
   ========================================================================== */
.dagfig{margin:0 0 22px}
.dagfig .dagwide{position:relative}
.dagedges{position:absolute;left:0;top:0;pointer-events:none;overflow:visible}
.dagcols{
  position:relative;z-index:1;
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0 38px;align-items:start
}
.dagcols > *{min-width:0}
.dagcol{display:flex;flex-direction:column;gap:10px;min-width:0}

/* The column head is the rollup segment, unchanged: same stage circle
   geometry, same uppercase label, same tabular headline number. */
.dagcol-h{
  display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:11px;row-gap:1px;
  align-items:start;
  background:var(--surface-1);border:1px solid var(--border);
  border-left:3px solid var(--stage-1);border-radius:9px;padding:9px 13px
}
.dagcol[data-stage="2"] .dagcol-h{border-left-color:var(--stage-2)}
.dagcol[data-stage="3"] .dagcol-h{border-left-color:var(--stage-3)}
.dagcol[data-stage="4"] .dagcol-h{border-left-color:var(--stage-4)}
.dagcol-h .dagn{
  grid-column:1;grid-row:1 / span 3;align-self:center;flex:none;
  width:21px;height:21px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:11.5px;font-weight:650;line-height:1;font-variant-numeric:tabular-nums;
  background:var(--stage-1);color:var(--stage-ink)
}
.dagcol[data-stage="2"] .dagcol-h .dagn{background:var(--stage-2)}
.dagcol[data-stage="3"] .dagcol-h .dagn{background:var(--stage-3)}
.dagcol[data-stage="4"] .dagcol-h .dagn{background:var(--stage-4)}
.dagcol-h .dagl{
  grid-column:2;
  font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
.dagcol-h .dagv{grid-column:2;font-size:16px;font-weight:620;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.dagcol-h .dagnote{grid-column:2;font-size:11.5px;color:var(--text-secondary);line-height:1.35}

/* One node per workflow step, in topological order down the column. The step
   text is clamped to four lines and the full text rides in the title
   attribute — a figure discloses, it does not hide. */
.dagnodes{display:flex;flex-direction:column;gap:10px;min-width:0}
.dagnode{
  display:grid;row-gap:2px;min-width:0;
  background:var(--surface-1);border:1px solid var(--border);
  border-left:3px solid var(--stage-1);border-radius:8px;padding:8px 11px
}
.dagnode[data-stage="2"]{border-left-color:var(--stage-2)}
.dagnode[data-stage="3"]{border-left-color:var(--stage-3)}
.dagnode[data-stage="4"]{border-left-color:var(--stage-4)}
.dagnode .dagid{
  font-family:var(--mono);font-size:11px;font-weight:600;
  color:var(--text-primary);letter-spacing:.01em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
.dagnode .dagstep{
  font-size:11.5px;color:var(--text-secondary);line-height:1.4;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden
}
.dagempty{
  font-size:11.5px;color:var(--muted);margin:0;padding:8px 11px;
  border:1px dashed var(--border);border-radius:8px
}

/* Under 720px the columns would be 60px wide and the edges would point at
   nothing, so the figure collapses to the four-box rollup of section 3. */
.dagfig .dagroll{display:none;margin:0}
@media (max-width:720px){
  .dagfig .dagwide{display:none}
  .dagfig .dagroll{display:flex}
}


/* == 3B. ANALYSIS WORKFLOW MAP (.wfmap) =====================================
   The house four-part workflow map visual — top border in the stage colour, a
   numbered circle, a title and ONE line of description — scoped to the
   analysis tab, which opens with it.

   Two rules the old .learning-map tiles broke and this component keeps:
     - it shows only the stages THAT TAB performs, at the course diagram's own
       numbers (2 transformation, 3 modelling & training, 4 evaluation). Stage
       1, preprocessing, is tab 2 (the design matrix) and never appears here.
       The surviving cards are never renumbered 1, 2, 3.
     - every card is an in-page anchor into the SAME panel. It never selects
       another tab: this is a map of the analysis, not a page index.
   DESIGN.md 3.3.
   ========================================================================== */
.wfmap{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(238px,100%),1fr));
  gap:14px;margin:0 0 22px
}
.wfmap > *{min-width:0}
.wfcard{
  display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:11px;row-gap:3px;
  align-items:start;text-decoration:none;color:inherit;
  background:var(--surface-1);border:1px solid var(--border);
  border-top:3px solid var(--stage-1);border-radius:10px;padding:13px 15px;
  transition:background .12s,border-color .12s
}
.wfcard[data-stage="2"]{border-top-color:var(--stage-2)}
.wfcard[data-stage="3"]{border-top-color:var(--stage-3)}
.wfcard[data-stage="4"]{border-top-color:var(--stage-4)}
.wfcard .wfn{
  grid-column:1;grid-row:1 / span 2;align-self:center;flex:none;
  width:22px;height:22px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:12px;font-weight:620;line-height:1;font-variant-numeric:tabular-nums;
  background:var(--stage-1);color:var(--stage-ink)
}
.wfcard[data-stage="2"] .wfn{background:var(--stage-2)}
.wfcard[data-stage="3"] .wfn{background:var(--stage-3)}
.wfcard[data-stage="4"] .wfn{background:var(--stage-4)}
.wfcard .wft{grid-column:2;font-size:13.5px;font-weight:620;color:var(--text-primary)}
.wfcard .wfd{grid-column:2;font-size:12.5px;color:var(--text-secondary);line-height:1.4}
/* An anchor inside one panel is the only affordance a workflow figure carries,
   and it stays quieter than a control. Only the three neutral sides move on
   hover: the shorthand would repaint border-top-color and take the stage
   colour off the card, which is the same trap the ribbon fell into. */
.wfcard:hover{
  background:var(--accent-soft);
  border-left-color:var(--axis);border-right-color:var(--axis);border-bottom-color:var(--axis)
}
.wfcard:hover .wft{text-decoration:underline}
.wfcard:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}
/* the section a card jumps to, offset so the sticky tab strip cannot cover its
   heading */
.wfstage{scroll-margin-top:96px}


/* == 4. FILTERS & CONTROLS ================================================== */
.filters{
  display:flex;flex-wrap:wrap;gap:18px;align-items:flex-end;
  background:var(--surface-1);border:1px solid var(--border);border-radius:12px;
  padding:14px 18px;margin:0 0 18px;position:sticky;top:0;z-index:20
}
.fgroup{display:flex;flex-direction:column;gap:5px}
.fgroup label{font-size:11.5px;letter-spacing:.03em;text-transform:uppercase;color:var(--muted);font-weight:600}
select{
  font:inherit;font-size:14px;padding:7px 10px;border-radius:8px;
  border:1px solid var(--border);background:var(--surface-2);color:var(--text-primary);min-width:230px
}
.dirbtn{
  font:inherit;font-size:14px;padding:7px 12px;border-radius:8px;cursor:pointer;
  border:1px solid var(--border);background:var(--surface-2);color:var(--text-primary);
  min-width:140px;text-align:left
}
.dirbtn:hover{border-color:var(--axis)}


/* == 5. LEGENDS ============================================================= */
.legend-wrap{display:flex;flex-direction:column;gap:5px;margin-left:auto}
.rampbar{height:12px;width:210px;border-radius:3px;border:1px solid var(--border)}
.rampends{display:flex;justify-content:space-between;font-size:11.5px;color:var(--text-secondary);font-variant-numeric:tabular-nums}
.catlegend{display:flex;gap:14px;font-size:12.5px;color:var(--text-secondary);align-items:center;flex-wrap:wrap}
.swatch{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:5px;vertical-align:-1px}
.scalebar{height:10px;border-radius:3px;border:1px solid var(--border)}
.scaleends{display:flex;justify-content:space-between;font-size:11px;color:var(--text-secondary);font-variant-numeric:tabular-nums;margin-top:3px}


/* == 6. HERO & STAT ROW ===================================================== */
.hero{font-size:52px;font-weight:640;line-height:1.05;letter-spacing:-.02em;margin:2px 0 2px}
.hero-l{font-size:13px;color:var(--text-secondary);margin:0}
.statrow{display:flex;flex-wrap:wrap;gap:34px;align-items:flex-end;margin:4px 0 14px}
.stat .v{font-size:24px;font-weight:620;letter-spacing:-.01em}
.stat .l{font-size:12.5px;color:var(--text-secondary)}


/* == 7. VERDICT FLAGS =======================================================
   Icon + label always, never colour alone: --warning is 1.79:1 on the light
   surface, so on light the TEXT is darkened to #8a6100 while the chip colour
   token stays fixed. Copied from the master, including the three-rule dance
   needed to make the override follow the theme.
   ========================================================================== */
.flag{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;white-space:nowrap}
.flag svg{flex:none}
.flag.ok{color:var(--good)} .flag.warn{color:#8a6100} .flag.bad{color:var(--critical)}
:root[data-theme="dark"] .flag.warn,
:root:where(:not([data-theme="light"])) .flag.warn{color:var(--warning)}
@media (prefers-color-scheme: light){:root:where(:not([data-theme="dark"])) .flag.warn{color:#8a6100}}


/* == 8. FINDINGS ============================================================
   The evidence-card grid: claim (h4) -> what it means (p) -> the numbers that
   establish it (.ev) -> so-what (.so). The numbers are never asserted in prose
   alone; .ev is the recessed block that holds them.
   ========================================================================== */
/* minmax(min(310px,100%),1fr) — not minmax(310px,1fr). A bare 310px floor is a
   floor the container is not allowed to go under, so inside the 296px content
   box of a card on a 390px phone the track stays 310px and the last 14px of
   every finding is cut off (measured on ceemdan, satellite, superconductor,
   tbb and battery: client=296, scroll=310). min(310px,100%) keeps the 310px
   ideal wherever there is room for it and yields to the container where there
   is not. Same correction on the two other auto-fit grids below. */
.findings{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(310px,100%),1fr));gap:22px 26px;margin-top:22px}
.findings > *{min-width:0}
.finding{border-top:2px solid var(--series-1);padding-top:11px}
.finding h4{font-size:13.5px;font-weight:620;margin:0 0 5px;letter-spacing:-.005em}
.finding p{font-size:13px;margin:0 0 7px}
.finding .ev{
  font-size:12px;color:var(--text-secondary);font-variant-numeric:tabular-nums;
  background:var(--surface-2);border:1px solid var(--border);border-radius:7px;
  padding:7px 10px;margin:0 0 7px
}
.finding .so{font-size:12.5px;color:var(--text-primary);margin:0}
.finding .so b{font-weight:620}


/* == 9. CONCLUSIONS & GLOSSARY ============================================== */
.concl{counter-reset:c;margin-top:14px}
.concl > div{counter-increment:c;position:relative;padding:0 0 0 42px;margin-bottom:22px}
.concl > div:before{
  content:counter(c);position:absolute;left:0;top:-2px;
  width:27px;height:27px;border-radius:50%;background:var(--series-1);color:#fff;
  display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:620
}
.concl h4{font-size:15px;font-weight:620;margin:2px 0 6px;letter-spacing:-.005em}
.concl p{font-size:13.5px;margin:0 0 6px}
.concl .num-ev{font-size:12.5px;color:var(--text-secondary);font-variant-numeric:tabular-nums}
.concl .num-ev b{color:var(--text-primary);font-weight:620}

/* newspaper flow so short sections don't leave dead space under them */
.gloss{columns:3 330px;column-gap:34px;margin-top:14px}
.gloss h3{
  margin:24px 0 2px;font-size:11.5px;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);break-after:avoid;break-inside:avoid;
  padding-bottom:5px;border-bottom:1px solid var(--grid)
}
.gloss h3:first-child{margin-top:0}
.gloss .term{break-inside:avoid;margin-top:15px}
.gloss .term b{display:block;font-size:13.5px;font-weight:620;color:var(--text-primary)}
.gloss .term p{margin:3px 0 0;font-size:13px;color:var(--text-secondary)}
.gloss .term code{font-size:.88em}


/* == 10. DESIGN-MATRIX SMALL MULTIPLES ====================================== */
#dmGrid{display:flex;gap:18px;flex-wrap:wrap;margin-top:18px;align-items:flex-start}
.dm{display:flex;flex-direction:column;gap:6px}
.dm h4{font-size:12.5px;font-weight:620;margin:0;white-space:nowrap}
.dm .oxide{font-size:11px;color:var(--muted);font-weight:400}
.dm canvas{border-radius:5px;display:block;cursor:crosshair}
.dmaxis{display:flex;justify-content:space-between;font-size:10px;color:var(--muted);font-variant-numeric:tabular-nums}
.dm.keycol{gap:6px;margin-right:4px}
.dm.keycol h4{color:var(--text-secondary)}
.dm.keycol canvas{border-radius:3px}


/* == 11. HEATMAP, PAIRGRID, TOOLTIP ========================================= */
/* correlation heatmap — a 2px surface gap does the separating, no cell borders */
.hmwrap{display:grid;grid-template-columns:minmax(0,3.6fr) minmax(0,1.25fr);gap:26px}
.hmwrap > *{min-width:0}
@media (max-width:900px){.hmwrap{grid-template-columns:minmax(0,1fr)}}
/* both heatmaps share one row rhythm so rows line up across the two panels */
.hm{display:grid;gap:2px;background:var(--surface-1);margin-top:10px;grid-template-rows:auto repeat(4, 62px)}
.hm-h{
  font-size:11px;line-height:1.25;color:var(--text-secondary);font-weight:600;
  display:flex;align-items:flex-end;justify-content:center;text-align:center;
  padding:0 2px 5px;hyphens:auto
}
.hm-r{
  font-size:12px;color:var(--text-secondary);font-weight:600;
  display:flex;align-items:center;padding-right:8px;font-variant-numeric:tabular-nums
}
.hm-c{
  border-radius:4px;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:0;cursor:default;
  font-variant-numeric:tabular-nums;position:relative
}
.hm-c .v{font-size:13px;font-weight:620;letter-spacing:-.01em}
.hm-c .ns{font-size:9px;letter-spacing:.06em;text-transform:uppercase;opacity:.72;margin-top:1px}
.hm-c:hover{outline:2px solid var(--text-primary);outline-offset:-2px}

.pairgrid{display:grid;gap:6px;margin-top:10px}
.cell{position:relative}
.cell canvas{display:block;width:100%;border-radius:4px}
.cell.blank{visibility:hidden}
.axlabel{font-size:11.5px;color:var(--muted);text-align:center;margin-top:3px}
.rowlabel{
  font-size:11.5px;color:var(--muted);writing-mode:vertical-rl;transform:rotate(180deg);
  display:flex;align-items:center;justify-content:center
}

/* The ONE shadow in the suite. A tooltip really does float above the page;
   cards do not, and must stay flat. */
#tip{
  position:fixed;pointer-events:none;z-index:100;opacity:0;transition:opacity .08s;
  background:var(--surface-1);border:1px solid var(--border);border-radius:9px;
  padding:9px 12px;font-size:12.5px;line-height:1.5;color:var(--text-primary);
  box-shadow:0 6px 22px rgba(0,0,0,.16);max-width:280px
}
#tip .k{color:var(--text-secondary)}
#tip b{font-weight:620}


/* == 12. SUITE EYEBROW ====================================================== */
/* "PC5251 · Demo 4" above the h1. The one mark that says every page in the
   suite is the same product. */
.kicker{
  display:block;font-size:11.5px;font-weight:600;letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);margin:0 0 4px
}


/* == 13. BACK LINK ========================================================== */
/* Every page routes home. The pipeline diagram and the workflow map are
   figures and the tab strip scopes the view — none of the three is navigation.
   This link, plus .series-nav between the parts of a two-part demo, is the only
   navigation a demo carries. */
a[data-role="back"]{
  display:inline-flex;align-items:center;gap:5px;
  font-size:12.5px;color:var(--text-secondary);text-decoration:none;
  margin:0 0 10px
}
a[data-role="back"]:hover{color:var(--text-primary);text-decoration:underline}


/* == 14. SERIES NAV ========================================================= */
/* Intra-demo only: a demo split over two files ("Part 1 / Part 2"). Deliberately
   pill-shaped and small so it cannot be mistaken for the tab strip or for the
   suite's back link. */
.series-nav{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 0}
.series-nav a{
  font-size:12.5px;font-weight:560;text-decoration:none;
  color:var(--text-secondary);background:var(--surface-1);
  border:1px solid var(--border);border-radius:999px;padding:5px 12px;
  transition:background .12s,border-color .12s,color .12s
}
.series-nav a:hover{color:var(--text-primary);border-color:var(--axis)}
/* The current part's link carries BOTH class="current" (styling) and the ARIA
   current-page marker (semantics). They are split because that ARIA marker is
   the suite's "the reader is here" flag, and a stylesheet that selects on it
   invites the workflow figures to grow a current segment again. */
.series-nav a.current{
  color:var(--text-primary);background:var(--accent-soft);border-color:var(--series-1)
}


/* == 15. NEXT-STAGE LINK ==================================================== */
/* Footer hand-off to the next demo in the course sequence. */
a[data-role="next-stage"]{
  display:inline-flex;align-items:center;gap:6px;
  font-size:13px;font-weight:560;color:var(--series-1);text-decoration:none
}
a[data-role="next-stage"]:hover{text-decoration:underline}


/* == 16. PAGE FOOTER ======================================================== */
/* Data source, DOI and licence. Never omitted — the suite's provenance rule
   applies to the page itself, not only to the tables inside it. */
.pagefoot{
  margin-top:34px;padding-top:14px;border-top:1px solid var(--grid);
  display:flex;flex-wrap:wrap;gap:10px 22px;align-items:baseline;
  font-size:12px;color:var(--muted)
}
/* min-width:0 for the same reason base.css puts it on every grid child: a flex
   item defaults to min-width:auto and so refuses to shrink below its own
   min-content width. The source line carries a DOI and a URL with no break
   opportunity, which made #footSource 311-323px wide inside a 294px footer and
   pushed the whole PAGE into horizontal scroll at 320px (measured on tbb and
   satellite: documentElement.scrollWidth 324 and 336 against innerWidth 320).
   With this the string wraps under body's overflow-wrap instead. */
.pagefoot > *{min-width:0}
.pagefoot a{color:var(--text-secondary)}
/* Part C of the card lands here (section 17): who built this file, and the
   commands that rebuild it. The <details> is footer furniture, so it drops the
   base rule's top border and top margin. */
.pagefoot details{border-top:none;margin-top:0;padding-top:0;flex-basis:100%}
.pagefoot summary{font-size:12px}


/* == 17. PROVENANCE CARD SECTIONS (tab 1) ===================================
   PROVENANCE.md's v2 card, rendered by PC.renderProvenance() ABOVE the claims
   table that has always opened tab 1. The two are halves of one argument and
   the order is the argument: the card SAYS this (these sections), and the
   bytes SAY that (the claims table). DESIGN.md 10.4, 10.6.

   Every value here is prose a contributor typed into the record, so the
   renderer writes it with textContent and this stylesheet does the rest. No
   value is ever styled by what it says — an `unknown — <why>` licence looks
   exactly like a CC BY one, because the reader is meant to read it, not to
   scan for a colour.
   ========================================================================== */
.provlist{margin:14px 0 0}
.provrow{
  display:grid;grid-template-columns:minmax(0,168px) minmax(0,1fr);
  gap:4px 20px;align-items:start;
  padding:11px 0;border-top:1px solid var(--grid)
}
.provrow:first-child{border-top:none;padding-top:2px}
.provrow > dt{
  margin:0;font-size:11.5px;font-weight:600;letter-spacing:.03em;
  text-transform:uppercase;color:var(--muted);line-height:1.5
}
.provrow > dd{
  margin:0;min-width:0;overflow-wrap:anywhere;
  font-size:13.5px;color:var(--text-secondary);line-height:1.55
}
.provrow > dd > .proval{margin:0}
.provrow > dd > ul.tight{margin:0}
.provrow > dd > ul.tight li:last-child{margin-bottom:0}
@media (max-width:640px){
  .provrow{grid-template-columns:minmax(0,1fr);gap:2px}
}

/* The usage line: three yes/no answers the licence field governs. Icon AND
   label, never colour alone — the same contract section 7's verdict flags
   keep, and for the same reason: an unknown licence forbidding publication is
   not a fact a reader may miss because they cannot separate two hues. */
.uchips{display:flex;flex-wrap:wrap;gap:8px;margin:9px 0 0}
.uchip{
  display:inline-flex;align-items:center;gap:6px;white-space:nowrap;
  font-size:12px;font-weight:600;
  background:var(--surface-2);border:1px solid var(--border);
  border-radius:999px;padding:4px 11px
}
.uchip .uicon{font-size:12.5px;line-height:1}
.uchip.yes{color:var(--good)}
.uchip.no{color:var(--critical)}

/* our_changes: a count in the summary, the full list on demand. Thirteen
   deviations are the record's answer to "is this the notebook's result?", and
   they belong on the page — but not ahead of what the page is about. */
.provchanges{border-top:none;margin:0;padding-top:0}
.provchanges > summary{font-size:13px;font-weight:600;color:var(--text-primary)}
.provchanges > ul.tight{margin:9px 0 0}

.provcmd{
  font-family:var(--mono);font-size:12px;line-height:1.55;
  white-space:pre-wrap;overflow-x:auto;
  background:var(--surface-2);border:1px solid var(--border);border-radius:7px;
  padding:9px 11px;margin:9px 0 0;color:var(--text-secondary)
}


/* == 18. RESPONSIVE CHART BOX ==============================================
   A canvas needs a measurable CSS box before PC.fit() can size its bitmap.
   Every ordinary plot sits in this wrapper; demo-local `.pbox` declarations
   are the pre-component version of the same contract. */
.chartbox{position:relative;width:100%;min-width:0}
.chartbox canvas{display:block;width:100%;max-width:100%}
.chartbox.framed canvas{border:1px solid var(--border);border-radius:6px}
.chartbox[aria-busy="true"]{min-height:160px;background:var(--surface-2);border-radius:6px}


/* == 19. LINKED DESIGN-MATRIX EXPLORER =====================================
   Canonical frame for the whole matrix, one selected row and one selected
   column. The pictures inside the cards remain domain-native. */
.matrix-explorer{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:20px;align-items:start;margin-top:16px
}
.matrix-explorer > *{min-width:0}
.matrix-card{
  min-width:0;background:var(--surface-1);border:1px solid var(--border);
  border-radius:10px;padding:14px 16px;transition:opacity .16s,border-color .16s
}
.matrix-card h3{margin-bottom:4px}
.matrix-explorer[data-focus="row"] .matrix-card[data-view="column"],
.matrix-explorer[data-focus="column"] .matrix-card[data-view="row"]{opacity:.5}
.matrix-route{
  display:flex;align-items:center;gap:7px;margin:0 0 8px;
  font-size:11.5px;font-weight:620;color:var(--text-secondary)
}
.matrix-route .matrix-source{
  flex:none;font-family:var(--mono);padding:2px 8px;border-radius:999px;
  background:var(--surface-2);border:1px solid var(--border);color:var(--text-primary)
}
.matrix-route .matrix-line{flex:1 1 20px;min-width:14px;height:2px;background:var(--axis)}
.matrix-slice{font-family:var(--mono);font-size:12.5px;font-weight:620}
.matrix-slice.row{color:var(--matrix-row)}
.matrix-slice.column{color:var(--matrix-column)}
.matrix-slice.cell{color:var(--matrix-cell)}
.matrix-control{display:block;margin:10px 0 6px;font-size:12.5px;color:var(--text-secondary)}
.matrix-control .control-label{display:flex;justify-content:space-between;gap:8px;margin-bottom:4px}
.matrix-control .control-label b{color:var(--text-primary);font-weight:620}
.matrix-control output{font-variant-numeric:tabular-nums}
.matrix-control select,.matrix-control input[type=range]{width:100%;min-width:0}
.matrix-card[data-view="row"] input[type=range]{accent-color:var(--matrix-row)}
.matrix-card[data-view="column"] input[type=range]{accent-color:var(--matrix-column)}
.matrix-readout{
  min-height:34px;margin:8px 0 0;padding-top:8px;border-top:1px solid var(--grid);
  font-size:12.5px;color:var(--text-secondary);font-variant-numeric:tabular-nums
}
@media (max-width:1000px){
  .matrix-explorer{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  .matrix-explorer > :first-child{grid-column:1 / -1}
}
@media (max-width:700px){
  .matrix-explorer{grid-template-columns:minmax(0,1fr)}
  .matrix-explorer > :first-child{grid-column:auto}
}


/* == 20. HONESTY CALLOUT =================================================== */
.callout{
  background:var(--surface-2);border:1px solid var(--border);
  border-left:3px solid var(--series-1);border-radius:7px;
  padding:11px 14px;font-size:13px;color:var(--text-secondary);margin:16px 0 0
}
.callout.warn{border-left-color:var(--warning)}
.callout.critical{border-left-color:var(--critical)}
.callout b{color:var(--text-primary);font-weight:620}


/* == 21. STAGE HAND-OFF ==================================================== */
.handoff{margin:18px 0 0;font-size:13px;color:var(--text-secondary)}
.handoff .continue{
  color:var(--series-1);text-decoration:none;font-weight:560;background:none;
  border:none;font-family:inherit;font-size:13px;cursor:pointer;padding:0
}
.handoff .continue:hover{text-decoration:underline}
.handoff .continue:focus-visible{outline:2px solid var(--series-1);outline-offset:3px;border-radius:3px}


/* == 22. COMPARISON GRID ===================================================
   For embeddings, model branches and other like-for-like small multiples.
   Every card in one grid uses the same axis contract; the selected card is
   marked by class and ARIA, never by colour alone. */
.comparison-grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));
  gap:18px;margin-top:16px
}
.comparison-grid > *{min-width:0}
.comparison-card{
  min-width:0;background:var(--surface-1);border:1px solid var(--border);
  border-radius:10px;padding:12px 14px
}
.comparison-card.is-selected,
.comparison-card[aria-pressed="true"],
.comparison-card[aria-current="true"]{outline:2px solid var(--series-1);outline-offset:-1px}
.comparison-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:6px}
.comparison-head h3{margin:0}
.comparison-meta{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
button.comparison-card{font:inherit;color:inherit;text-align:left;cursor:pointer}
button.comparison-card:focus-visible{outline:2px solid var(--text-primary);outline-offset:2px}


/* == 23. INTERACTIVE TABLE TWIN ============================================
   PC.bindTableRows() supplies tabindex, keyboard movement and aria-selected.
   The row remains a real row; the table is promoted to an ARIA grid only when
   it deliberately participates in the page selection. */
.interactive-table tbody tr{cursor:pointer}
.interactive-table tbody tr:hover td{background:var(--surface-2)}
.interactive-table tbody tr[aria-selected="true"] td{background:var(--accent-soft)}
.interactive-table tbody tr:focus-visible{outline:2px solid var(--series-1);outline-offset:-2px}
.interactive-table tbody tr[aria-disabled="true"]{cursor:not-allowed;opacity:.55}


/* Controls named in DESIGN.md §6.3 now live in the shared sheet rather than
   being reintroduced by each demo. */
input[type=range]{width:200px;accent-color:var(--series-1)}
.seg{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden}
.seg > button{
  font:inherit;font-size:13px;padding:6px 11px;border:0;border-left:1px solid var(--border);
  background:var(--surface-1);color:var(--text-secondary);cursor:pointer
}
.seg > button:first-child{border-left:0}
.seg > button[aria-pressed="true"]{background:var(--series-1);color:var(--stage-ink);font-weight:600}
.seg > button:focus-visible{outline:2px solid var(--text-primary);outline-offset:-3px}


/* == 23B. THE PHONE FOLD (details.pcfold) ===================================
   The story strip (§2.4) and the header workflow figure (§3.2) are each about
   a full phone screen tall, and they sit one after the other ABOVE the tab
   strip — so on a handset a reader scrolls through two screens of preamble
   before reaching anything they can act on. On a phone both are therefore
   folded shut behind a one-line summary, and opened by tapping it.

   The <details> is inserted AROUND each figure by the phone-fold module at the
   end of charts.js; the figure itself is moved inside untouched. That is what
   keeps §2.4 and §3.2 satisfied — .storystrip and .dagfig still contain no
   <button>, no href, no click handler and no focus ring; the disclosure is
   page furniture wrapping the figure, not a control inside it.

   Above the phone breakpoint the module holds the <details> open and the
   summary is display:none, so a laptop renders exactly what it rendered
   before this component existed.
   ========================================================================== */
.pcfold{border-top:none;margin:0;padding-top:0}     /* not the base.css disclosure */
.pcfold > summary{display:none}                     /* desktop: no affordance at all */
.pcfold[open] > summary{display:none}

@media (max-width:640px){
  .pcfold{margin:0 0 16px}
  .pcfold > summary,
  .pcfold[open] > summary{
    display:flex;align-items:center;gap:8px;
    min-height:44px;padding:10px 13px;margin:0;
    background:var(--surface-1);border:1px solid var(--border);border-radius:10px;
    font-size:13.5px;font-weight:600;color:var(--text-primary);
    cursor:pointer;list-style:none
  }
  /* base.css already supplies the ▸ / ▾ marker through summary:before and
     kills the native one, so the chevron comes for free and flips on open. */
  .pcfold > summary:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}
  /* the summary IS the strip's headline question when open, so the strip's own
     .storyq would repeat it */
  .pcfold[open] > .storystrip > .storyq{display:none}
  .pcfold[open] > summary{margin-bottom:12px}
  /* the folded figure carries its own bottom margin already */
  .pcfold > .storystrip,.pcfold > .dagfig,.pcfold > .ribbon{margin-bottom:0}
}


/* == 23C. THE PHONE PARAMETER BAR (.parambar) ===============================
   Owner review on a REAL handset: a `.filters` control panel is most of a phone
   screen tall, so the controls and the plots they drive are never on screen
   together. You change the embedding, then have to scroll to find out what it
   did. Desktop is fine — the panel and the plot are visible at once.

   On a phone the panel therefore collapses behind a slim sticky bar carrying a
   live summary of the current values ("t-SNE · perplexity 30 · k 6"). Tapping
   the bar pushes the full panel down under it; changing a control updates the
   summary and collapses again, so the plot is on screen the instant the value
   changes. Bar and summary are BUILT by the parameter-bar module at the end of
   charts.js out of the panel's own .fgroup labels and control values — no
   per-demo markup, no per-template edit.

   STICKY, AT THE RIGHT OFFSET THIS TIME. DESIGN.md §6.3 calls .filters a
   "sticky control bar … positioned below the sticky tabs" and §5.2 fixes that
   offset at `top: topTabs.offsetHeight`. §24 had dropped sticky on phones
   because a FULL-HEIGHT panel pinned under the tab strip ate the screen; a 44px
   bar does not, so sticky returns here — offset written by JS from the live
   strip height, and z-index BELOW the strip's 30 so the strip always wins.

   It is a control surface, not navigation: one <button aria-expanded> bound to
   the panel by aria-controls. No role="navigation", no aria-current, no stage
   colour. The selects inside stay native (§6.3) and the bar is flat — the only
   shadow in the suite is #tip (§6.1).
   ========================================================================== */
/* DESKTOP: display:contents, not display:none. The module moves .filters INSIDE
   the bar so the phone layout can anchor it as a dropdown; display:contents
   removes the bar's own box from the layout entirely, so off the phone the
   panel renders in the flow exactly where it always did — same position, same
   sticky rule, same everything. display:none here would take the panel with
   it. */
.parambar{display:contents}
.parambar > .parambar-btn{display:none}    /* desktop: no affordance at all */

@media (max-width:640px){
  .parambar{
    display:block;position:relative;         /* the dropdown's containing block */
    position:sticky;z-index:25;              /* 25 < the tab strip's 30 */
    top:0;                                   /* real offset written by JS */
    background:var(--page);                  /* content must not show through */
    padding:6px 0 8px;margin:0 0 4px
  }
  .parambar > .parambar-btn{display:flex}
  .parambar-btn{
    display:flex;align-items:center;gap:9px;width:100%;
    min-height:44px;padding:9px 13px;           /* the fingertip floor again */
    background:var(--surface-1);border:1px solid var(--border);border-radius:10px;
    font:inherit;font-size:13px;font-weight:560;color:var(--text-primary);
    cursor:pointer;text-align:left
  }
  .parambar-btn:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}
  .parambar-k{
    flex:none;font-size:11.5px;letter-spacing:.05em;text-transform:uppercase;
    color:var(--muted);font-weight:600
  }
  /* the live summary: ONE line, ellipsized rather than wrapped, so the bar
     keeps its 44px however verbose the panel is */
  .parambar-sum{
    flex:1 1 auto;min-width:0;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    color:var(--text-secondary);font-variant-numeric:tabular-nums
  }
  .parambar-chev{flex:none;color:var(--muted);font-size:12px;line-height:1}

  /* collapsed: the panel leaves the flow entirely, so the plot below it rises
     into the viewport. The controls stay in the DOM — check_design's acceptance
     pass drives them in JS, and a reader reaches them in one tap. */
  .filters[data-pc-param="collapsed"]{display:none}

  /* expanded: a DROPDOWN anchored to the bar, not an inline push-down. The
     push-down was tried first and rejected on measurement: it opens at the
     bar's NATURAL position, so once the bar is pinned under the tab strip the
     panel appears off-screen above it, and Chrome's scroll anchoring then
     shifts scrollY by the panel's own height to hold the visual position —
     re-hiding what the tap just revealed (scrollY 902 -> 1264, twice, with and
     without a rAF correction). Anchored to the pinned bar the panel is on
     screen by construction and there is no scroll maths to lose.
     max-height so a tall panel scrolls inside itself rather than off the
     bottom of the screen. */
  .filters[data-pc-param="open"]{
    position:absolute;top:100%;left:0;right:0;z-index:1;
    margin:0;max-height:calc(100vh - 150px);overflow-y:auto;
    overscroll-behavior:contain
  }
}


/* == 23D. THE PHONE CHART OVERLAY (.pczoom-*) ===============================
   A plot panel is ~330px wide on a phone whatever it draws, and most of them
   are shorter than they are wide, so the chart the reader came for is a strip.
   Every plot therefore carries an expand affordance at phone width: tap it and
   the panel is promoted to cover the viewport, rotate and it redraws with the
   landscape width, ✕ or Escape puts it back.

   PROMOTED IN PLACE, NOT MOVED. The chart-overlay module at the end of
   charts.js only adds a class: the panel keeps its position in the DOM, inside
   its own .toppanel, so every demo's redraw still finds it and its PC.hover
   binding and listeners are untouched. position:fixed does the rest.

   The redraw is the demo's OWN: the module dispatches a synthetic window
   `resize`, the path all 8 templates already bind. A panel that computes its
   height from an aspect (tbb's map: h = w × GEO.aspect) therefore letterboxes
   itself correctly at whatever width the overlay gives it — nothing here
   stretches anything, and no panel needs a keep-ratio marker to stay honest.

   Z-ORDER 90: above the tab strip (30) and the parameter bar (25), below #tip
   (100) so a tooltip still paints over a full-screen chart.
   ========================================================================== */
.pczoom-btn{display:none}                  /* desktop: no affordance at all */
.pczoom-chrome{display:none}

/* WIDTH **OR** HEIGHT. Every other phone rule in this sheet is width-only,
   which is right for them — they are about a narrow column. The overlay is the
   one feature whose whole point is ROTATION, and a phone in landscape is
   844x390: wider than 640, so a width-only query would switch the overlay off
   at exactly the moment the reader turned the handset to use it (measured: the
   overlay closed itself on rotation, zoomOpen true -> false).
   The second clause is bounded by max-width:950px so a genuinely short DESKTOP
   window — 1360x600, say — does not sprout expand buttons; 950px clears the
   widest phone landscape (932px) and stays well under a laptop. */
@media (max-width:640px), (max-height:640px) and (max-width:950px){
  .pczoom-host{position:relative}
  .pczoom-btn{
    display:flex;align-items:center;justify-content:center;
    position:absolute;top:4px;right:4px;z-index:2;
    width:40px;height:40px;                /* the fingertip floor */
    padding:0;border:1px solid var(--border);border-radius:9px;
    background:var(--surface-1);color:var(--text-secondary);
    font:inherit;font-size:15px;line-height:1;cursor:pointer
  }
  .pczoom-btn:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}

  /* The promoted panel. THE DOUBLED CLASS IS DELIBERATE — `.pczoom-on.pczoom-on`
     is specificity 0,2,0 instead of 0,1,0, and every declaration in here has to
     beat whatever the panel's own box already says about position, margin and
     size. A plot box is a demo-local class (.pbox, .plotwrap, .plot-card, .card)
     declared in the demo's own <style>, which is inlined AFTER this sheet, so a
     single-class rule there ties on specificity and wins on order.
     MEASURED, and this is not hypothetical: alzheimers' template line 46 says
     `.plotwrap{position:relative;min-width:0}`, and it kept every alzheimers
     overlay at position:relative — the panel got the class, the scroll lock and
     the ✕ bar but never left the flow, on a phone, since the overlay shipped
     (verified identical on the pre-touch-work page: box [29,877,332,488],
     covers=false). jae's box lost 14px of height the same way. Raising the
     specificity here fixes both without editing a demo, and immunises the
     overlay against the next demo-local one-class rule. */
  .pczoom-on.pczoom-on{
    position:fixed;inset:0;z-index:90;
    /* width/height, not inset alone. `inset:0` derives the size from top/bottom,
       which a surviving MARGIN then eats into — and a margin can survive: jae's
       `#view-explorer .card` is an ID rule (1,1,0) and outranks any class stack
       here, so its `margin-bottom:14px` left the overlay 830px tall on an 844px
       screen. With an explicit height the box is over-constrained, `bottom` is
       ignored, and the margin can no longer shrink it (measured 830 -> 844). */
    width:100%;height:100%;
    margin:0;max-width:none;max-height:none;min-height:0;
    background:var(--page);
    padding:calc(env(safe-area-inset-top,0px) + 56px) 12px
            calc(env(safe-area-inset-bottom,0px) + 12px);
    overflow:auto;-webkit-overflow-scrolling:touch;
    display:flex;flex-direction:column;justify-content:center
  }
  /* the panel's own expand button is redundant once it is open */
  .pczoom-on > .pczoom-btn{display:none}
  /* the canvas takes the overlay's width; its HEIGHT stays whatever the demo's
     own draw code computes, which is what keeps an aspect-locked panel honest */
  .pczoom-on canvas{max-width:100%}

  /* scroll lock behind the overlay */
  .pczoom-lock,.pczoom-lock body{overflow:hidden}

  .pczoom-chrome{
    display:flex;align-items:center;gap:10px;
    position:fixed;top:env(safe-area-inset-top,0px);left:0;right:0;z-index:2;
    padding:6px 10px;background:var(--page);border-bottom:1px solid var(--grid)
  }
  .pczoom-hint{
    flex:1 1 auto;min-width:0;margin:0;
    font-size:12px;color:var(--muted);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    transition:opacity .5s
  }
  .pczoom-hint.gone{opacity:0}
  .pczoom-close{
    flex:none;display:flex;align-items:center;justify-content:center;
    width:44px;height:44px;padding:0;      /* ≥44px, as specified */
    border:1px solid var(--border);border-radius:9px;
    background:var(--surface-1);color:var(--text-primary);
    font:inherit;font-size:16px;line-height:1;cursor:pointer
  }
  .pczoom-close:focus-visible{outline:2px solid var(--series-1);outline-offset:2px}
}


/* == 23E. THE TOUCH READOUT AND SELECTION RING (.pc-readout / .pc-mark) =====
   Owner review on a REAL handset: a hover readout is useless on a phone. #tip
   is placed at clientX+14 / clientY+14 — under the very finger that summoned
   it — and it is taken away the instant that finger lifts. So on a touch
   device PC.hover switches to TAP-TO-SELECT and the payload is docked here
   instead: a strip directly BELOW the chart, inside the plot's own box, out
   from under the hand, which stays until the next tap.

   BELOW, NOT ABOVE — corrected 2026-08-31 on an owner phone screenshot. Docked
   above, the strip became the box's first child and therefore slid UNDER the ⤢
   button, which §23D anchors to the BOX's top-right corner: the button rendered
   on the strip's corner, clipped at the box edge, instead of on the chart
   ("the full screen button is at a wrong space"). Reserving padding for it
   fixed the text collision but not the button's PLACE. Below the canvas there
   is no conflict at all — and the chart no longer jumps down by the strip's own
   height on the first tap, so the point the reader just touched stays put.

   BUILT BY THE LIBRARY, NOT BY A TEMPLATE. The touch-mode block in charts.js
   creates both nodes out of the panel's OWN tooltip HTML (its onHit still
   calls PC.tip; PC.tip docks it), so every demo inherits this by rebuilding
   and no demo's draw code or markup changes.

   NO MEDIA QUERY, ON PURPOSE. The mode is gated in JS on input capability —
   matchMedia("(hover: none) and (pointer: coarse)") — not on width, so a
   narrow desktop window keeps its mouse tooltip and a big tablet gets the
   strip. These nodes therefore only ever EXIST in touch mode, and styling
   them at every width is what lets the same rules serve the phone, the
   tablet, and the full-screen overlay (§23D) that promotes the same box.

   FLAT. The one shadow in the suite is #tip (§6.1), which genuinely floats;
   this is docked chrome inside a panel and takes the recessed fill instead.
   ========================================================================== */
.pc-readout{
  margin:9px 0 0;padding:8px 11px;min-width:0;
  background:var(--surface-2);border:1px solid var(--border);border-radius:8px;
  font-size:12.5px;line-height:1.5;color:var(--text-primary);
  font-variant-numeric:tabular-nums;overflow-wrap:anywhere
}
/* the same two inner conventions #tip carries, so one payload string reads the
   same whichever of the two renders it */
.pc-readout .k{color:var(--text-secondary)}
.pc-readout b{font-weight:620}

/* Inside the full-screen overlay the box is a centred flex column and the
   landscape viewport is only 390px tall, so a verbose payload must not push the
   chart it describes off the screen: the strip caps itself and scrolls. */
.pczoom-on > .pc-readout{max-height:32vh;overflow-y:auto;flex:0 0 auto}

/* The ring marks the point the strip is about — the reader has lifted their
   finger by the time they read it, so without this the readout refers to
   nothing they can see. A DOM element, not a canvas draw: no demo's draw code
   changes, and it survives the redraw a tap's own onClick triggers. Positioned
   by charts.js in the box's coordinates; --accent is the suite's selection
   colour (tokens.css declares it as --series-1).

   :not(.pczoom-on) IS LOAD-BEARING, and this is the bug it fixes. `.pc-plotbox`
   and §23D's `.pczoom-on{position:fixed}` are both one class — equal
   specificity — and §23E comes LATER in this file, so a bare
   `.pc-plotbox{position:relative}` WON the cascade and defeated the overlay:
   with a selection open, tapping ⤢ added the class, set the scroll lock and
   drew the ✕ bar, but the panel stayed in the flow at 302px instead of
   covering the viewport (measured on tbb/jae/pleiades/superconductor:
   boxPosition "relative", coversViewport false, canvas 302 -> 278px). To the
   reader that is "the full-screen button no longer works". Excluding the
   promoted state costs nothing: position:fixed establishes a containing block
   for the ring just as well as position:relative does. */
.pc-plotbox:not(.pczoom-on){position:relative}
.pc-mark{
  position:absolute;z-index:1;pointer-events:none;   /* 1 < the ⤢ button's 2 */
  width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;
  border:2px solid var(--accent,var(--series-1));background:transparent
}


/* == 24. THE PHONE BREAKPOINT ===============================================
   One block, one width, for everything that is merely TOO BIG on a handset
   rather than structurally wrong there. The structural collapses keep the
   breakpoints DESIGN.md already fixes and are NOT touched here:

     1000px  .gridtop/.gridmain -> one column, .matrix-explorer -> two   (5.2, 2.3)
      900px  .hmwrap -> one column                                        (5.2)
      760px  .storypanels -> one column, .ribbon arrows dropped           (2.4)
      720px  .dagfig .dagwide -> the .ribbon rollup                       (3.2)
      700px  .matrix-explorer -> one column                               (2.3)
      640px  THIS BLOCK — the phone

   Verified at 390x844 before and after: the page body never scrolls
   horizontally at any of these widths (9.3 item 7 asserts the same thing at
   380x844, and every gated page still passes it).
   ========================================================================== */
@media (max-width:640px){

  /* -- masthead: the deck is the widest thing in the header. In the desktop
        space-between row the theme button takes ~87px of a 358px column and
        the .sub deck wraps into a narrow ten-line block beside it (measured on
        tbb). Stacking gives the deck the whole column back. The button keeps
        its top-right position — order:-1 plus align-self, so it does not move
        to somewhere new on a phone and does not sit buried under the deck. */
  header.top{flex-direction:column;align-items:stretch;gap:10px;margin-bottom:14px}
  header.top #themeBtn{order:-1;align-self:flex-end}
  h1{font-size:22px}

  /* 36px is under the fingertip floor, and #themeBtn is the one control every
     page is required to carry (8.3). */
  #themeBtn{min-height:40px;padding:9px 14px}

  /* -- controls go full width. A 230px select in a 296px card leaves no room
        for a second one beside it, so the .filters row silently overflowed its
        own gap rhythm; full width is both bigger to hit and tidier. */
  .filters{gap:12px;padding:12px 14px}
  /* Two sticky bars at top:0 on a 390px screen is most of the viewport. The
     tab strip (z-index 30) is the one that must stay; a filter row that is
     one swipe away does not earn the other 60px. */
  .filters{position:static}
  .fgroup{width:100%}
  select,.dirbtn{min-width:0;width:100%;min-height:40px}
  input[type=range]{width:100%;min-width:0}
  .seg{width:100%}
  .seg > button{flex:1 1 0;min-height:40px}

  /* -- .continue is an inline sentence-ending control (3.4), so it keeps its
        place in the sentence and grows only its HIT AREA: the negative margin
        cancels the padding, so the line box does not move.
        BOTH selectors are needed. ceemdan, superconductor and tbb put the
        button in a plain <p>, so `.handoff .continue` alone reached only half
        the suite; but section 21's `.handoff .continue{padding:0}` is
        specificity 0,2,0 and outranks a lone `.continue`, so dropping it put
        satellite and pleiades back to 16px. Listing both — the 0,2,0 selector
        ties section 21 and wins on order, the 0,1,0 one catches the plain-<p>
        pages. Measured after: 36px on all eight. */
  .handoff .continue,.continue{padding:10px 2px;margin:-10px 0}

  /* -- type: the one big number and the stat row were sized for a 1240px
        column. 52px of hero on a 358px screen is a line break waiting to
        happen. Everything stays on the 5.1 weight ladder. */
  .hero{font-size:40px}
  .statrow{gap:16px 22px}
  .storystrip .storyq{font-size:16.5px}

  /* -- glossary: `columns:3 330px` already resolves to one column in a 296px
        box; saying so drops the 34px gutter the browser still reserves. */
  .gloss{columns:1;column-gap:0}

  /* -- tooltip: 280px of fixed-position tooltip plus charts.js's edge offset
        can still land past the right edge of a 390px screen. Clamping to the
        viewport keeps the readout on-screen wherever the finger is. */
  #tip{max-width:calc(100vw - 24px)}

  /* -- cards give back a little of their 22px side padding, which is the last
        cheap width on the page. */
  .card{padding:16px 15px;border-radius:10px}

  /* -- the findings/comparison grids are one column here anyway; drop the
        26px column gutter that no longer separates anything. */
  .findings{gap:20px 0}
}

/* Canonical fold component for future Workflow and Dataset pages.
   JavaScript adds .pc-folds-ready before it collapses bodies. Without JS the
   authored content stays readable. Closed bodies remain in the DOM; charts
   repaint after every open operation. */
.fold-controls{
  display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;
  margin:14px 0
}
.fold-controls button{
  background:var(--surface-1);color:var(--text-secondary);border:1px solid var(--border);
  border-radius:8px;padding:7px 13px;font:inherit;font-size:13px;cursor:pointer;white-space:nowrap
}
.fold-controls button:hover{color:var(--text-primary);border-color:var(--axis)}
.fold-stack{display:grid;gap:14px}
.fold{scroll-margin-top:24px}
.fold.card{padding:0;margin:0}
.fold.card > h2{padding:18px 20px;margin:0}
.fold-head{
  display:flex;align-items:baseline;gap:10px;width:100%;min-height:24px;
  background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;text-align:left;cursor:pointer
}
.fold-head::before{content:"▾";flex:0 0 auto;font-size:.72em;color:var(--text-secondary)}
.fold-head[aria-expanded="false"]::before{content:"▸"}
.fold-head:focus-visible{outline:2px solid var(--series-1);outline-offset:3px}
.fold-sub{margin-left:auto;font-size:12px;font-weight:400;color:var(--text-secondary)}
.fold-body{padding:0 20px 20px}
.pc-folds-ready .fold-body[data-folded]{
  padding-top:0;padding-bottom:0;max-height:0;overflow:hidden;visibility:hidden
}
.fold-body > :first-child{margin-top:0}
.fold-body > :last-child{margin-bottom:0}
.fold .card{margin:0 0 16px}
.fold .card:last-child{margin-bottom:0}
/* Missing metadata is an authoring concern, not a published form. */
[data-page-role="dataset"] .empty-slot,
[data-page-role="dataset"] [data-slot-state="empty"],
[data-page-role="dataset"] [data-slot-state="unavailable"],
[data-page-role="dataset"] [data-author-todo],
[data-page-role="dataset"] .fold[hidden]{display:none}
.documented-fields{margin:16px 0 0;border-top:1px solid var(--border)}
.documented-field{display:grid;grid-template-columns:minmax(140px,.25fr) minmax(0,1fr);gap:12px 28px;padding:20px 0;border-bottom:1px solid var(--border)}
.documented-field strong,.documented-field h3{margin:0;font-size:14px;font-weight:620;line-height:1.6;overflow-wrap:anywhere}
.documented-field p{margin:0;color:var(--text-secondary);font-size:14px;line-height:1.7;max-width:90ch}
@media(max-width:640px){.documented-field{grid-template-columns:minmax(0,1fr);gap:6px;padding:16px 0}}
.preview-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
.preview-grid > *{min-width:0}
.preview-bars{display:grid;gap:7px;margin-top:14px}
.preview-bar{height:10px;border-radius:999px;background:var(--series-1)}
.preview-bar:nth-child(2){background:var(--series-2)}
.preview-bar:nth-child(3){background:var(--series-3)}
.preview-bar:nth-child(4){background:var(--series-4)}
@media (max-width:700px){
  .fold-controls{justify-content:flex-start}
  .fold-head{align-items:flex-start;flex-wrap:wrap}
  .fold-sub{display:block;flex-basis:100%;margin-left:calc(.72em + 10px)}
  .fold.card > h2{padding:15px 16px}
  .fold-body{padding:0 16px 16px}
  .preview-grid{grid-template-columns:minmax(0,1fr)}
}

/* Insight premise: a compact, always-visible question and answer before the figure. */
[data-page-role="insight"] .insight-premise{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin:0 0 24px}
[data-page-role="insight"] .insight-premise > .card{min-width:0;margin:0;padding:20px 22px}
[data-page-role="insight"] .insight-premise h2{margin:0 0 8px;font-size:11px;font-weight:650;line-height:1.4;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
[data-page-role="insight"] .insight-premise p{max-width:78ch;margin:0;font-size:14px;line-height:1.6;color:var(--text-primary)}
@media(max-width:600px){
  [data-page-role="insight"] .insight-premise{grid-template-columns:minmax(0,1fr);gap:12px;margin-bottom:20px}
  [data-page-role="insight"] .insight-premise > .card{padding:16px 18px}
}


/* == 25. EVIDENCE TAGS & LINEAGE STRIP (Dataset pages) =======================
   Every lineage box and step-table row on a Dataset page carries one tag:
   Documented (the provider describes it; not rerun), Reproduced (rerun or
   checked here), Held (the bytes the page reads), Not recorded (it happened,
   but nothing held says how). Tag text is always visible: colour never carries
   the meaning alone. See skeletons/DATASET_STAGES.md. */
.lineage{list-style:none;margin:10px 0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr));gap:10px}
.lineage li{background:var(--surface-2);border:1px solid var(--border);border-radius:7px;padding:9px 11px;min-width:0}
.lineage li p{margin:4px 0 0;font-size:12.5px;color:var(--text-secondary)}
.lin-head{display:flex;flex-direction:column;gap:4px}
.ev{display:inline-block;width:max-content;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:1px 7px;border-radius:999px;border:1px solid var(--border);white-space:nowrap}
.ev.doc{color:var(--text-secondary)}
.ev.rep{color:var(--series-1);border-color:var(--series-1)}
.ev.held{color:var(--stage-2);border-color:var(--stage-2)}
.ev.none{color:var(--muted);border-style:dashed}

