/* ============================================================================
   DESIGN TOKENS — the single source of visual style for every widget.
   Change values here. Never hardcode a colour, size, or font in a widget.

   The categorical series palette is a validated set: fixed slot order (never
   cycled), CVD-separated on adjacent pairs, with selected dark-mode steps
   rather than an automatic flip.
   ========================================================================= */

:root {
  color-scheme: light;

  /* --- surfaces & ink ---------------------------------------------------- */
  --surface-1: #fcfcfb;              /* figure surface                       */
  --surface-2: #f9f9f7;              /* page plane                           */
  --surface-3: #f1f1ec;              /* control wells, table stripes         */
  --ink-1: #0b0b0b;                  /* primary text                         */
  --ink-2: #52514e;                  /* secondary text                       */
  --ink-3: #898781;                  /* muted: axis labels, ticks            */
  --grid: #e1e0d9;                   /* hairline gridline                    */
  --axis: #c3c2b7;                   /* baseline / axis line                 */
  --border: rgba(11, 11, 11, 0.10);  /* hairline ring                        */

  /* A draft notice is CHROME, not data. It deliberately does not take a --c-
     role: those name what a mark MEANS inside a figure (observed, theoretical,
     past the threshold), and a banner means nothing about the statistics. Amber
     because it has to be noticed by someone who landed here by accident, warm
     because the rest of the palette is. */
  --notice-bg:   #fdf4da;
  --notice-ink:  #6b5410;
  --notice-rule: #e0be5c;
  --focus: #2a78d6;

  /* --- categorical series: FIXED ORDER, NEVER CYCLED -------------------- */
  --series-1: #2a78d6;               /* blue                                 */
  --series-2: #eb6834;               /* orange                               */
  --series-3: #1baf7a;               /* aqua                                 */
  --series-4: #eda100;               /* yellow                               */
  --series-5: #e87ba4;               /* magenta                              */
  --series-6: #008300;               /* green                                */
  --series-7: #4a3aa7;               /* violet                               */
  --series-8: #e34948;               /* red                                  */

  /* --- semantic roles for teaching figures ------------------------------ *
   * Widgets reference THESE, not the numbered slots. One idea per role, so
   * "empirical" is the same blue in all 40 widgets and students learn it.
   *
   * "Empirical" is what the reader BUILT from the data — a pile of collected
   * draws, a likelihood, a fitted line, an arrangement, an embedding. It has
   * never meant "raw data": raw samples wear group or outcome colours, or
   * --c-unknown before anything is known about them, and the measured table a
   * fit is checked against is ink (mds, t-sne). Audited across all 24 widgets
   * on 2026-08-27 and consistent under exactly that reading — a claim brought
   * from OUTSIDE the data is what --c-theory is for.                        */
  --c-empirical: var(--series-1);    /* what the reader built from the data  */
  --c-theory:    var(--series-2);    /* the theoretical / asymptotic result  */
  --c-smoothed:  var(--series-3);    /* a smoothed reading of the empirical  */
  --c-highlight: var(--series-7);    /* the one thing to look at right now   */

  /* The fixed benchmark the moving thing is judged against: the truth where
     one exists (a true mean, a true parameter, the curves the data really
     came from), else the baseline a fit is compared to (logistic's lm line,
     linear-regularization's unpenalised fit, umap's entropy lower bound).
     Audited across all 25 widgets on 2026-08-27: every use is one of those
     two, so the wider reading was blessed rather than the baselines
     repainted. It stays ink, not a series slot — a benchmark is not data. */
  --c-reference: var(--ink-3);       /* the benchmark a fit is judged against */

  /* Two arms of a comparison. Widgets 4, 5 and 6 all put two groups on screen
     and none of the roles above means "group B", which was forcing widgets to
     reach for numbered slots directly.

     Blue and amber, chosen against the two obvious alternatives: --series-2 is
     already --c-theory, so an orange group would collide with "the curve the
     pile is checked against" that clt and bootstrap establish; --series-7 is
     --c-highlight, which a permutation test needs for the tail. Blue/amber is
     also the most colour-blind-safe pair in the ramp and the highest-contrast
     one, which matters when the figure is projected.

     Group A deliberately shares its hue with --c-empirical: both mean "our
     data", they never appear in the same panel, and giving the first arm its
     own fourth colour would spend a hue to say nothing.

     THE SECOND OPERAND READING (widget 53, 2026-09-08; recorded 2026-09-10):
     where a figure combines the reader's tensor with something else — the
     weights, a kernel, the keys, a mask, a gate — the tensor is --c-group-a
     and the other thing is --c-group-b, and the result is --c-empirical. That
     is not an A/B comparison, but it is the same shape of statement: two
     things you can name, one of them yours. Three widgets share it now, and a
     fourth token for "the weights" would split one idiom across them.

     --c-group-c is the THIRD arm, added 2026-09-10 for a figure with three
     parallel branches (slot 51's Routing page, from Kenneth's own figure of
     three). His figure draws it red; red here is --c-extreme, and the same
     panel prints a torch error in it, so the third arm is green. Green is also
     --c-dim-b, which is safe on that panel: a dimension hue appears only on a
     rank-3 tensor's frames and every tensor on a branch figure is rank 2. */
  --c-group-a:   var(--series-1);    /* one arm of a comparison              */
  --c-group-b:   var(--series-4);    /* the other arm                        */
  --c-group-c:   var(--series-6);    /* a third arm: three parallel branches */

  /* GROUPS NOBODY ASSIGNED — the unsupervised case, and not the A/B pair above.
     An arm of a comparison is a thing you decided; a cluster in a projection is
     what you are trying to find out, so the colour means only "these samples
     travelled together".

     A RAMP OF SIX, ORDERED BRIGHTEST AND MOST FAMILIAR FIRST: blue, yellow,
     red, then aqua, green, violet. A figure showing three groups uses the first
     three and should get the three most separable hues, not whatever the other
     roles left over.

     THE LAST THREE WERE MEASURED, NOT CHOSEN BY EYE. With blue/yellow/red
     fixed, all ten ways of picking three more from the ramp were scored on the
     smallest weighted RGB distance between any two of the six, in BOTH themes.
     Aqua+green+violet scores 76; every other option scores 34 to 40, and
     magenta is in all of the worst ones because it sits too close to red.
     A first attempt used magenta and would have put two near-identical dots on
     the same six-group figure.

     Six because that is what a group count control can reach, and the sixth
     must not repeat the first. These deliberately DO overlap other roles by
     hue: a cluster figure has no p-value tail and no theoretical curve in it,
     so --c-extreme's red and --c-cluster-c's red never share a panel. */
  --c-cluster-a: var(--series-1);    /* blue                                 */
  --c-cluster-b: var(--series-4);    /* yellow                               */
  --c-cluster-c: var(--series-8);    /* red                                  */
  --c-cluster-d: var(--series-3);    /* aqua                                 */
  --c-cluster-e: var(--series-6);    /* green                                */
  --c-cluster-f: var(--series-7);    /* violet                               */

  /* At or beyond a threshold: the tail a p-value counts, and later the tests a
     correction flags. Red because this role's whole job is to be found first —
     it is the one thing on the figure a reader is meant to count. Distinct from
     --c-highlight, which means "moving right now": an arrival is highlighted
     while it falls and turns THIS colour only if it lands somewhere that
     counts, so the two never mean the same thing. */
  --c-extreme:   var(--series-8);    /* past the line: what a p-value counts  */

  /* A Bayesian figure holds three curves and only one of them is data. The
     LIKELIHOOD is --c-empirical, because it is a function of your observations
     and of nothing else — widget 8 already draws it that way and widget 9 has
     to agree, or the curve a student swept in one lesson changes colour in the
     next. The other two are neither observed nor theoretical: a prior is an
     assumption you brought with you, and a posterior is what is left of it
     after the data. Putting either on --c-theory would say it is a result.

     Both share a slot with a role they can never meet. --c-prior takes amber
     from --c-group-b: a figure with a prior in it has no A/B arms, and
     blue/amber is the most colour-blind-safe and highest-contrast pair in the
     ramp — which is what the two adjacent panels of the multiplication need.
     --c-posterior takes violet from --c-highlight, which is nearly true anyway:
     the posterior IS the thing moving right now, and it is the darkest slot in
     the ramp, which the projector wants for the mark that carries the answer. */
  --c-prior:     var(--series-4);    /* what you believed before the data    */
  --c-posterior: var(--series-7);    /* what the data left you believing     */

  /* Whether the outcome HAPPENED to a person, which is the other axis of a
     2x2 from --c-group-a/b and has to be legible beside them: a contingency
     table needs both pairs on screen at once, exposure down the rows and
     outcome across the columns.

     --c-event shares red with --c-extreme, and the kinship is real rather than
     a saving: both mean "this is the thing you are counting". They differ in
     what makes a mark qualify — a threshold there, an outcome here — and no
     figure has both, because a 2x2 has no tail.

     --c-nonevent is deliberately NOT grey. It is the denominator of the odds
     and the widget's whole lesson is that it shrinks, so drawing it as
     furniture would hide the one quantity that moves. Blue is the same
     --series-1 as --c-empirical and --c-group-a: all three mean "the people we
     actually have", and none of them ever appears in a panel with another. */
  --c-event:     var(--series-8);    /* the outcome happened to this person  */
  --c-nonevent:  var(--series-1);    /* it did not — and this is the odds'
                                        denominator, not background          */

  /* AND THE THIRD STATE, which is not a third outcome: we have not looked yet.
     A cohort recruits by exposure, so at enrolment nobody's outcome is known
     and every dot is this colour; the outcome colour is what ARRIVES. A
     case-control recruits by outcome, so its dots are --c-event/--c-nonevent
     before it starts. Two designs drawn with the same marks, and the only
     difference is which of them opens grey — an asymmetry that needs no words.

     Grey is right HERE for the same reason it is wrong for --c-nonevent: this
     one really is absence of information, and it must lose to both outcome
     colours sitting next to it. It aliases --ink-3 the way --c-reference does
     rather than taking a series slot, because it is not a series. */
  --c-unknown:   var(--ink-3);       /* not measured yet, not a third outcome */

  /* DATA SET ASIDE, AND SCORED EXACTLY ONCE, AT THE END. Red because a test set
     is the one partition a reader must not touch while working, and red is the
     collection's colour for "this is the line you do not cross" — it already
     carries --c-extreme, past a threshold, and --c-event, the outcome nobody
     wants. A held-out test set is the same kind of thing: consequential, and
     spent when you use it.

     Added rather than borrowed. The train/test distinction is not an extreme
     value and not an outcome, so reaching for either of those roles would have
     meant a colour that says something the figure does not. It also will not
     stay a one-widget need: every widget in the PHM5005 arc that splits data
     has this same partition to name. */
  --c-holdout:   var(--series-8);    /* set aside, and scored once at the end */

  /* A COST PAINTED OVER A PARAMETER SPACE — the two ends of a sequential ramp
     for surfaces where every point is a model the reader could have picked and
     the colour says how badly it fits (lm-least-squares' sum-of-squares
     surface; any later loss landscape). Added rather than borrowed:
     --c-nonevent/--c-event are outcomes that happened to a person, and
     --c-extreme is past a threshold — a fit's cost is none of those. The ends
     alias the same blue and red series slots those wear, because the KINSHIP
     is real (calm where the fit is good, the warning colour where it is bad);
     the ROLE is what is new. Widgets lerp between the two ends; the low end is
     where the minimum lives, so it must recede next to --c-highlight marks. */
  --c-cost-low:  var(--series-1);    /* a good fit — the trench, the minimum  */
  --c-cost-high: var(--series-8);    /* a bad fit — far from every good line  */

  /* WHERE A MEASURED VALUE FALLS IN ITS OWN RANGE — one variable, coloured from
     its low end to its high end, so a figure can carry the DIRECTION of a
     relationship and not only its size. Widget 38's SHAP swarm colours each
     patient's dot by that patient's value of the feature the row is about; high
     creatinine on the right and low on the left IS the reading.

     Added rather than borrowed from --c-cost-low/high, which is the same shape
     making a different claim: a cost ramp says how badly a model fits, and a
     feature value is neither good nor bad — nobody's age is a worse fit. The
     ends alias the same blue and red slots because the SHAP library's own
     beeswarm is blue-low/red-high, and a student who has run
     `shap.plots.beeswarm` should meet the same convention here.

     A PANEL USING THIS MUST NOT ALSO COLOUR BY IDENTITY. Colour means one thing
     at a time, and widget 38's swarm therefore drops the per-feature
     --c-cluster-* hues its other page uses and labels its rows in ink: without
     that, a low-Age dot and the Age row would be the same blue for two
     unrelated reasons. */
  --c-value-low:  var(--series-1);   /* the low end of one feature's range   */
  --c-value-high: var(--series-8);   /* the high end of the same range       */

  /* THE LOCAL SLOPE — a tangent, a secant, a gradient arrow: the rate of change
     at one point. It is the only role whose mark lies ON another mark, and that
     is what earned it. A tangent sits on the curve it touches, so the contrast
     that decides whether it can be seen is against --ink-2 and not against the
     surface: --c-highlight, which every tangent wore until now, is 1.08 against
     the curve in the light theme — the same luminance — and 1.74 in the dark,
     so the one thing to look at was the one thing that could not be seen
     (reported on widget 48, 2026-09-08). Red was the obvious answer and is the
     wrong one: it reads 2.0 and 1.8, and it is already --c-extreme, past a
     threshold — a gradient arrow crossing the hot end of a --c-cost-high ramp
     scores 1.00 there, which is invisible. --series-5 was the one slot carrying
     no role at all, and it is 2.95 and 2.20 against the curve and 1.22 on that
     hot end. `_lab/gd-colour.html` holds the table and re-measures it live.

     A MARK IN THIS ROLE IS CASED wherever it crosses another mark — a halo of
     the page's own ground under the line, drawn wider than it. No hue in the
     ramp separates from every surface a slope can be drawn over, so colour is
     what says "this is a slope" and the casing is what makes it findable: the
     same division of labour --c-group-a's slicing planes already use.

     One widget so far, as --c-value-low/high were when they arrived. It will
     not stay one — every later widget that draws a rate of change wants it. */
  --c-slope:      var(--series-5);   /* the rate of change at one point      */

  /* --- the dimensions of a tensor, one hue each ------------------------ *
     Widget 53 (2026-09-09) names a tensor's dimensions on its frames, its
     edge rules and its arrows — NOT on their text, which stays in ink: text
     wears text tokens, a coloured mark beside it carries the identity. Four,
     assigned from the LAST dimension backwards so the feature dimension keeps
     --c-dim-d at every rank. Blue, yellow and violet are left out because the
     cells, the second operand and the highlight already mean those. */
  --c-dim-a:      var(--series-2);   /* orange */
  --c-dim-b:      var(--series-6);   /* green  */
  --c-dim-c:      var(--series-3);   /* aqua   */
  --c-dim-d:      var(--series-8);   /* red    */

  --c-wash: color-mix(in srgb, var(--c-empirical) 10%, transparent);

  /* --- type ------------------------------------------------------------- */
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --fs-xs: 11px;
  --fs-sm: 12px;
  --fs-md: 13px;
  --fs-lg: 15px;
  --fs-fig: 20px;                    /* stat-tile value                      */

  /* --- geometry --------------------------------------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --radius: 6px;
  --bar-radius: 4px;                 /* rounded data-end on bars             */
  --surface-gap: 2px;                /* the gap that separates touching marks */
  --line-w: 2px;
}

/* Dark values are declared under BOTH scopes: the media query covers the OS
   setting, the [data-theme] scope covers an explicit choice and must win both
   ways. :where() keeps the media block's specificity at zero. */
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --surface-1: #1a1a19;
    --surface-2: #0d0d0d;
    --surface-3: #242422;
    --ink-1: #ffffff;
    --ink-2: #c3c2b7;
    --ink-3: #898781;
    --grid: #2c2c2a;
    --axis: #383835;
    --border: rgba(255, 255, 255, 0.10);
    --notice-bg:   #2b2413;
    --notice-ink:  #ecdcab;
    --notice-rule: #6d5c26;
    --focus: #3987e5;
    --series-1: #3987e5;
    --series-2: #d95926;
    --series-3: #199e70;
    --series-4: #c98500;
    --series-5: #d55181;
    --series-6: #008300;
    --series-7: #9085e9;
    --series-8: #e66767;
  }
}

:root[data-theme="dark"] {
  --notice-bg:   #2b2413;
  --notice-ink:  #ecdcab;
  --notice-rule: #6d5c26;
  color-scheme: dark;
  --surface-1: #1a1a19;
  --surface-2: #0d0d0d;
  --surface-3: #242422;
  --ink-1: #ffffff;
  --ink-2: #c3c2b7;
  --ink-3: #898781;
  --grid: #2c2c2a;
  --axis: #383835;
  --border: rgba(255, 255, 255, 0.10);
  --focus: #3987e5;
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --series-5: #d55181;
  --series-6: #008300;
  --series-7: #9085e9;
  --series-8: #e66767;
}

/* ============================================================================
   WIDGET CHROME — every widget gets the same frame, so the collection reads
   as one system. Nothing here is widget-specific.
   ========================================================================= */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-2);
  color: var(--ink-1);
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Embedded in an iframe: no page chrome, the host page provides the margin. */
body[data-embed="1"] { background: transparent; }
body[data-embed="1"] .w-root { border: none; border-radius: 0; }

.w-root {
  max-width: 780px;
  margin: 0 auto;
  padding: var(--sp-4);
  background: var(--surface-1);
  border: var(--hairline, 1px) solid var(--border);
  border-radius: var(--radius);
}

/* --- header --------------------------------------------------------------- */
.w-title {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* 57ch, and the number is measured rather than reasoned about. `ch` is the
   width of the "0" glyph, which in this face runs ~30% narrow against real
   prose — so 62ch was believed to be "about 76 characters, the top of the
   readable range" and is actually 82, seven PAST it. Measured at --fs-sm:
   62ch = 469px = 82 characters; 57ch = 429px = 75, the limit.

   The instinct this corrects is the opposite one — that a subtitle stopping at
   40% of the page should be let out to fill the width. Full width here is 1120px
   and 196 characters per line, about 2.6x the maximum, where the return sweep
   starts landing on the wrong line. The empty space to the right is not a
   problem stretching text can fix; it is answered by shorter copy.
   _lab/subtitle-measure.html holds the comparison and re-measures it live. */
.w-subtitle {
  margin: var(--sp-1) 0 0;
  color: var(--ink-2);
  font-size: var(--fs-sm);
  max-width: 57ch;
}

/* --- side layout: controls beside the figure ------------------------------ *
 * OPT-IN via defineWidget({ layout: "side" }), because widening the shell for
 * everyone would move every widget's canvas and invalidate all 39 fingerprint
 * baselines in one commit. Only a widget that asks for it is affected.
 *
 * It exists because a tall widget forces a scroll BACK PAST THE FIGURE to reach
 * the control that changes it, which is the opposite of "the control sits beside
 * the thing it controls" (principle 3.1). Below 880px it stacks again, so the
 * narrow case is exactly what it was.                                        */
.w-root[data-layout="side"] {
  max-width: 1120px;
}

.w-split {
  display: grid;
  /* 300px, and the number is measured. The drive row's worst case is the widest
     run-button label — "Resume" — beside the step label and Reset, which comes to
     283px. At 270 that overflowed and dropped Reset to a second line the moment
     an animation had been paused, so the rail is sized to the row rather than the
     row squeezed into the rail. The stage still gets 776px, wider than the 694 a
     stacked widget has. */
  grid-template-columns: minmax(250px, 300px) 1fr;
  gap: var(--sp-4);
  align-items: start;
}

.w-rail {
  border-top: 1px solid var(--grid);
  padding-top: var(--sp-4);
  margin-top: var(--sp-4);
}

/* One column in the rail, and its own top rule replaces the one .w-controls
   draws, so the column does not get a stray line across it. */
.w-split .w-controls {
  grid-template-columns: 1fr;
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}

/* Drive buttons are the last thing in the rail, behind their own divider, so
   the column reads: what am I looking at -> what stage am I in -> make it go. */
.w-split .w-drive {
  border-top: 1px solid var(--grid);
  padding-top: var(--sp-4);
  margin-top: var(--sp-4);
}

/* IN THE RAIL THE DRIVE ROW IS TWO FULL-WIDTH ROWS, NOT A WRAPPING LINE.

   Every action in one cluster, Reset beneath it, both spanning the rail. The
   point is that it is a BLOCK rather than a flex line that happens to fit:
   nothing here depends on how long a label is, so nothing can come adrift when
   one changes.

   Two earlier attempts, both measured in _lab/drive-rail.html at the real 300px
   track. Letting the line wrap and growing Reset left the rows' right edges
   disagreeing by up to 42px — and failed ALL FIVE hostile future widgets in
   that page's fragility table. Putting the lead on its own row above instead
   was worse still, 89px, because a long step label wraps the remaining line
   anyway. This rule is the only one of the three with flush edges on all seven
   widgets AND no failures under stress; it costs 20px of height for it.

   The reservation on the run button is dropped here on purpose: the cluster is
   full width and its segments are flex:1, so a relabel cannot reflow anything,
   and honouring the reserve would break the equal split instead. */
.w-split .w-drive {
  flex-direction: column;
  align-items: stretch;
  /* NOWRAP IS LOAD-BEARING, not tidiness. `.w-drive` sets `flex-wrap: wrap` for
     the stacked layout; leaving it on while the direction is `column` asks the
     container to wrap items into extra COLUMNS, and with an indefinite main
     size that resolves the group 19.5px — exactly one line-height — taller than
     it should be. The buttons then stretch to match and the cluster sits taller
     than Reset beneath it, for no reason visible in any of their own styles.
     A block does not wrap. */
  flex-wrap: nowrap;
}
.w-split .w-drive-group {
  display: flex;
}
/* Four classes deep so it beats `.w-drive .w-btn[data-key="run"]`, which is the
   same specificity and declared later in this file. Without that the reserved
   run width survives into the rail and breaks the equal split — invisibly at two
   segments, where the share is wider than the reserve, and visibly at three. */
.w-split .w-drive .w-drive-group .w-btn {
  flex: 1 1 0;
  min-width: 0;
}
.w-split .w-drive .w-btn:not(.w-btn--primary) {
  margin-left: 0;
}
.w-split .w-drive[hidden] { display: none; }

/* The gate: a full-width button that opens a stage, sitting in the control flow
   at the point the stage begins. Its divider marks where setup ends. */
.w-gate {
  border-top: 1px solid var(--grid);
  padding-top: var(--sp-4);
  margin-top: var(--sp-1);
}
.w-gate-btn { width: 100%; justify-content: center; }
.w-gate .w-detail { margin: var(--sp-2) 0 0; }

/* A labelled divider between groups of controls. Same rule as the gate's, so a
   reader learns one mark: a hairline above means "a different kind of thing
   starts here". The first one in a block has no divider — there is nothing
   above it to separate from.

   IT HAS TO OUTRANK A FIELD LABEL, and the first attempt did not. Both were
   --fs-sm on --ink-2 and the heading's only edge was its weight, so a group
   title read as one more control label and the blocks were reported as not
   apparent. A heading needs to differ in KIND: primary ink against the labels'
   secondary, and a full line of air above it rather than the gap between two
   fields. Four signals — colour, weight, rule, space — where there was one. */
.w-section {
  /* Full row, always. The controls grid goes multi-column in the stacked
     layout, and as an ordinary grid item the heading's hairline spanned only
     its own cell — a divider that stopped halfway across the block, reported
     on mobile (2026-08-27). Spanning also puts the controls it heads BELOW it
     rather than beside it, which is what a heading claims. */
  grid-column: 1 / -1;
  margin: var(--sp-5) 0 var(--sp-1);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--grid);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-1);
}
.w-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* `.w-controls` is a grid, so an item's margins ADD to the row gap: 12 + 24
   above the heading against 12 + 4 below it. The asymmetry is what binds a
   heading to the group under it rather than floating between two. */

/* THE FITTED MODEL, AS MathML, ABOVE THE FIGURE. Widget 14 writes its equation
   here rather than painting it on the canvas: MathML italicises a one-letter
   identifier and leaves a multi-letter one upright on its own, makes the
   definition of z a real fraction, and is read by a screen reader as an
   expression that can be stepped through — canvas text is read as nothing.

   FONT-FAMILY IS DELIBERATELY NOT SET. The UA stylesheet gives `math` its own
   `font-family: math`, and the stretchy glyphs — fraction bars, big parentheses,
   radicals — are driven by the OpenType MATH table, which an interface font does
   not carry. Overriding this with the body sans produces subtly broken maths.
   The size IS nudged, because Chromium renders math noticeably small beside body
   text.

   Colour is inherited rather than set with `mathcolor`, so dark mode just works. */
/* A CARD, so the equation is its own object. Unboxed it floated between the
   subtitle and the figure with nothing to say which it belonged to, and it is
   neither: it is what the two dials just produced. `--surface-1` is the figure's
   own plane and the ring is the same hairline every other card in the collection
   carries, so it reads as a second panel rather than as a new kind of thing.

   It also pays for itself. The rail beside it holds a 300px matrix, and the rail
   may not end below the stage without making the whole widget taller; the
   padding here makes the stage 26px taller, which is 26px the rail no longer
   overhangs by. Measured across both frames.

   THE HORIZONTAL PADDING IS SMALLER THAN THE VERTICAL, AND THAT IS NOT TASTE.
   Padding narrows the column the equation wraps in. Thirteen terms needed a
   FOURTH line below 512px, and 12px each side plus the ring leaves 508 of the
   narrowest 534px column — so the block jogged 15.5px as a dial moved, the very
   fault the reserve on `.w-math-eq` exists to prevent, reintroduced by the box.
   8px leaves 516 and cleared it.

   THAT ARGUMENT NO LONGER BINDS, and the number is left at 8px anyway. The
   hanging indent costs a line at every column under 600px, so 508 and 516 both
   need four lines now and the reserve covers both. A wider column is still never
   worse for a block that wraps, and `--sp-3` would buy nothing but a slightly
   fatter card. */
.w-math {
  margin: 0 0 var(--sp-2);
  padding: var(--sp-3) var(--sp-2);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--ink-1);
  font-size: var(--fs-xs);
  line-height: 1.55;
}
.w-math-eq {
  /* A FLOOR, AND DELIBERATELY NO CEILING. How long the equation is depends on
     how many terms survive; how many lines that takes depends on the column
     width. Reserving the worst case holds the block at a constant height for
     every width the side layout reaches — measured 121.3px flat from 884px to
     1400px, across both dial sweeps — so dragging a dial no longer moves the
     figure under the reader (3.4d).

     A `-webkit-line-clamp: 3` ceiling was tried and removed. Below the 880px
     breakpoint the column narrows to ~350px, thirteen terms need four lines,
     and the clamp hid one — silently, with no scrollbar and no ellipsis. A
     block that jogs is untidy; a block that drops a term from the model is
     false, and the model is what this widget is about.

     A LINE IS TALLER THAN THE LINE-HEIGHT, so three of them are not 3 x 1.55em.
     Each term is an <msub> — z with the measurement subscripted — and its
     inline box descends past the 17.05px strut, which pushes the line box to
     18.54px. The reserve was 4.65em while the terms were z(Name), and the block
     was flat; the subscripts made it 51.15px, 54.13px or 55.62px depending on
     which terms land on which line, i.e. the figure jogged 4.5px as a dial
     moved. Measured worst case is 55.62px = 5.056em, over all 49 dial settings
     at every column width the side layout reaches (534px to 770px). 5.2em
     reserves that with about 3% to spare, which is roughly what a font
     substitution on another platform costs.

     FOUR LINES RATHER THAN THREE, because of the hanging indent below. It costs
     exactly one line at the narrowest column the side layout reaches: with the
     indent, thirteen terms need four lines at 534px and under, and three at
     640px and over — measured over all 49 dial settings. The reserve has to
     cover the worst case at the NARROWEST width, because it is one number for
     every width, so a wide frame carries a line of slack inside the card. That
     is the price of the alignment and it was paid knowingly. */
  min-height: 6.93em;   /* 4 lines of 1.733em — see above, NOT 4 x 1.55 */

  /* A HANGING INDENT, so the right-hand side of the equation is a column.
     Every term after the first line starts where `19.2` starts, rather than
     under `body fat %`, and thirteen terms over three or four lines read as one
     sum instead of as a paragraph.

     `padding-left` moves every line right; `text-indent` pulls the FIRST one
     back to the edge, so line one keeps the full width and only continuations
     pay. The flex alternative — a fixed left column and the terms in their own
     box — makes every line pay, which is one line worse.

     8.3em is the rendered width of `body fat % =` plus the space after it,
     measured at 91px against this block's 11px type. It is a constant for the
     same reason `1.45em` above is one: it depends on the maths font's metrics,
     and being a few pixels out on another platform is a cosmetic misalignment
     rather than a block that changes height. */
  padding-left: 8.3em;
  text-indent: -8.3em;
}
/* SIZED BY THE SMALLEST THING IN IT, not by the biggest. MathML shrinks each
   nested level — a fraction's numerator lands at about 0.71 of its parent — so
   at the 1.15 that made the equation itself look right, `z(x) = (x - x̄)/s`
   rendered its numerator at 9.0px, which is under this repo's floor for
   anything meant to survive a projector.

   1.45 puts the numerator at 11.3px and the equation at 16.0px, and is the
   largest multiplier that still fits thirteen terms in the three reserved lines
   at the narrowest column the side layout reaches (534px). 1.6 tips it to four
   lines and the block grows to 88px. */
.w-math math { font-size: 1.45em; }
.w-math-note {
  margin: 2px 0 0;
  color: var(--ink-3);
}

/* THE FRAMEWORK ROW: one equation, and the links the course touches, in a card
   above the figure. Widget 15's organising idea is WHERE the link goes — on the
   LEFT-hand side, applied to the mean — so the row states `f(μ) = β₀ + β₁X`
   once and marks which link the figure below is showing.

   IT REUSES `.w-math` AS ITS CARD and adds only what `.w-math`'s own children
   cannot give it. `.w-math-eq` reserves 6.93em of height and hangs an 8.3em
   indent, both measured for a thirteen-term sum that wraps; for one short line
   they are a tall empty box with its first line pulled out of the card.

   THE PLACEMENT WAS MEASURED, not chosen — `_lab/logistic-scales.html`, round
   two. At the 550px stage the row is one line and 58px tall. In the rail's
   300px the same wording wraps to two lines and 79px, and the only way back to
   one line there is to drop what each link is FOR, which is the whole content
   of the row. */
.w-link-eq {
  font-size: var(--fs-lg);
  color: var(--ink-1);
  /* A LINE IS TALLER THAN ITS LINE-HEIGHT WHEN IT CONTAINS A FRACTION. The
     equation's <mfrac> renders about 2.2x the base, so at the 1.55 inherited
     from `.w-math` the line box is shorter than the thing inside it and the
     two wrapped lines close up on each other — reported as the formula looking
     squashed. 2.3 clears the fraction with a little air, and it is set here
     rather than on `.w-math` so the links row underneath keeps its own
     spacing. */
  line-height: 2.3;
}
/* The equation and the drawn link sit side by side, and wrap rather than shrink
   below about 26em: a 108px figure squeezed to 40px is worse than one on its
   own line. */
.w-link-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--sp-1);
}
.w-link-row > div { flex: 1 1 17em; }
.w-link-fig { flex: 0 0 auto; }
/* The term the figure is currently stepping. MathML inherits `color`, so one
   class on the term's own <math> is the whole of it. */
.w-link-eq .lit { color: var(--c-highlight); }
.w-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  list-style: none;
  margin: 0;
  padding: 0;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}
/* The current link is marked twice — weight and a dot — because colour alone is
   not a distinction every reader has. */
.w-links li[aria-current] {
  color: var(--ink-1);
  font-weight: 600;
}
.w-links li[aria-current]::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--c-highlight);
  margin-right: 5px;
  vertical-align: 1px;
}

/* A MATRIX CONTROL: a labelled grid of cells, one option per cell, each shaded
   by a magnitude the widget supplies. For a parameter that is a PAIR — 156
   ordered pairs of thirteen body measurements in widget 14 — where a flat
   dropdown is technically operable and practically not, and where the shading
   is teaching content rather than decoration.

   FOUR AREAS ON ONE SET OF TRACKS: corner, column names, row names, cells. The
   grid track sizes them, so no name can come adrift from the cell it names.

   THE COLUMN NAMES ARE TURNED NINETY DEGREES BY `writing-mode`, WHICH IS THE
   WHOLE TRICK. It makes a name's own length its HEIGHT, so the band above the
   grid sizes itself off the longest name with no measurement anywhere — where a
   45-degree rotation needs an authored band height and a sin 45. Forty-five was
   built first and measured: at widget 14's column pitch the rotated names clear
   each other by 2.3px against an 11px type size, where ninety clears by 7.8px.
   The band is 12px taller for it. */
.w-matrix {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  gap: 3px;
  margin-top: var(--sp-1);
}
.w-matrix-cols {
  display: grid;
  grid-template-columns: repeat(var(--w-matrix-cols), 1fr);
  gap: 1px;
}
.w-matrix-cols span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);   /* bottom-to-top, the way an axis label reads */
  justify-self: center;
  align-self: end;
}
.w-matrix-rows {
  display: grid;
  grid-template-rows: repeat(var(--w-matrix-rows), 1fr);
  gap: 1px;
}
.w-matrix-rows span {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 4px;
}
.w-matrix-cols span,
.w-matrix-rows span {
  font-size: var(--fs-xs);
  line-height: 1;
  color: var(--ink-3);
}
/* The names of the row and the column you are on, in the same token the figure
   marks the selection with — one token, one meaning. */
.w-matrix-cols span.on,
.w-matrix-rows span.on { color: var(--c-highlight); }

.w-matrix-grid {
  display: grid;
  grid-template-columns: repeat(var(--w-matrix-cols), 1fr);
  gap: 1px;
  aspect-ratio: 1;
}
.w-matrix-grid > [role="row"] { display: contents; }
.w-matrix-cell {
  position: relative;
  cursor: pointer;
  background: var(--surface-2);
}
.w-matrix-cell--off { cursor: default; }
/* THE SHADE IS AN OPACITY ON THE CHILD. Fading the cell would fade the selected
   cell's ring with it, brightest exactly where it is least needed. */
.w-matrix-cell > i {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--w-matrix-token, var(--c-empirical));
}
.w-matrix-cell[aria-selected="true"] {
  outline: 2px solid var(--c-highlight);
  outline-offset: -1px;
  z-index: 1;
}
.w-matrix-grid:focus-visible { outline: 2px solid var(--c-highlight); outline-offset: 3px; }

/* A READBACK: a small case table that names which of a few labelled outcomes
   the controls above it currently produce. It carries no value and sets
   nothing — it is the rail's answer to "so what have I just chosen".

   In the rail rather than on the figure because the thing it describes is the
   CONTROLS, not the data: principle 2.7 puts a reading next to what produced it,
   and the two dials are what produce it. Drawn on the canvas first, where it sat
   beside a figure it was not describing. */
.w-readback {
  display: grid;
  gap: 1px;
  background: var(--grid);
  border: 1px solid var(--grid);
  border-radius: var(--radius);
  overflow: hidden;
  font-size: var(--fs-xs);
  margin-top: var(--sp-1);
}
.w-readback > * {
  background: var(--surface-1);
  padding: 5px 6px;
  text-align: center;
  color: var(--ink-2);
}
.w-readback .w-readback-hd { color: var(--ink-3); }
/* The live cell takes --c-highlight, which means "the one thing to look at right
   now" — and a case table has exactly one of those. */
.w-readback .w-readback-on {
  background: var(--c-highlight);
  color: var(--surface-1);
  font-weight: 600;
}

/* The control block BELOW the drive row. Empty for every widget that declares
   no `afterDrive` field, and an empty grid with padding would leave a stray gap
   under the buttons — `:empty` collapses it to nothing. */
.w-controls--after { border-top: 0; padding-top: 0; margin-top: var(--sp-4); }
.w-controls--after:empty { display: none; margin: 0; }

/* TWO RELATED NUMBERS ON ONE LINE. A population's centre and its spread are one
   idea, and four of them stacked read as four unrelated sliders. `flex: 1 1 0`
   with `min-width: 0` is what stops a long value pushing one field wider than
   the other — they split the track evenly whatever they contain. */
.w-field-row {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
}
.w-field-row > .w-field { flex: 1 1 0; min-width: 0; }

/* The caption ABOVE the row, unlike a segmented group's, which sits below: this
   one names the thing being set and has to be read before the sliders are
   touched, where that one classifies buttons already on screen. The swatch is
   what ties a control block to a colour in the figure, so a reader setting a
   population can see which one it is before any of it is drawn. */
.w-row-cap {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: var(--sp-2) 0 var(--sp-1);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-1);
}
.w-row-cap i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--swatch);
  flex: none;
}
.w-row-detail { margin-top: var(--sp-1); }

/* THE EXPRESSION CONTROL (`type: "expr"`, controls.js): one line of code whose
   slots are <select>s. The line is set in the mono face at the large size so
   it reads as the code a student will type; a slot is one character wide at
   rest with its own caret, and carries the dimension's name in small type
   beneath. A slot away from its default wears the highlight, so the fixed
   dimensions of `T[1, :, 4]` stand out from the colons. Widget 53, 2026-09-09. */
.w-expr {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 3px;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  color: var(--ink-1);
}
.w-expr-text { padding-top: 5px; }
.w-expr-slot {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.w-expr-sel { position: relative; display: inline-block; }
.w-expr-sel::after {
  content: "";
  position: absolute;
  right: 7px;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-top: -4px;
  border-right: 1.5px solid var(--ink-3);
  border-bottom: 1.5px solid var(--ink-3);
  transform: rotate(45deg);
  pointer-events: none;
}
.w-expr-sel select,
.w-expr-sel input {
  appearance: none;
  -webkit-appearance: none;
  min-width: 34px;
  padding: 3px 18px 3px 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--ink-1);
  background: var(--surface-1);
  border: 1px solid var(--axis);
  border-radius: var(--radius);
  cursor: pointer;
}
.w-expr-sel select:hover { background: var(--surface-3); }
.w-expr-slot[data-set="1"] select,
.w-expr-slot[data-set="1"] input {
  border-color: var(--c-highlight);
  background: color-mix(in srgb, var(--c-highlight) 22%, var(--surface-1));
}
/* A typed slot: the same box without the chevron or its room (widget 53, the
   reshape argument, 2026-09-09). */
.w-expr-txt::after { display: none; }
.w-expr-sel input { padding-right: 8px; cursor: text; }
.w-expr-sel input:focus { outline: 2px solid var(--c-highlight); outline-offset: 1px; }
/* What a text field says while it is typed in: a problem, in the colour a
   figure uses for the case that fails, and nothing at all when there is none. */
.w-text-hint {
  font-family: var(--font);
  font-size: var(--fs-xs);
  color: var(--c-extreme);
  max-width: 26ch;
  text-align: center;
}
p.w-text-hint { max-width: none; text-align: left; }
/* A text field on its own line. */
.w-text {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 4px 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--ink-1);
  background: var(--surface-1);
  border: 1px solid var(--axis);
  border-radius: var(--radius);
}
.w-expr-name {
  font-family: var(--font);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}

/* ONE TEXT PARAMETER LAID OUT AS N CELLS UNDER A HEAD ROW (`cells` on a text
   field — controls.js). The outer border and `overflow: hidden` are `.w-seg`'s
   own, so N cells here and N segments below divide exactly the same width and
   a value lands over the control that sets the same class — 0.4 / 0.6 / 0.2px
   off at three, five and two columns, measured in `_lab/loss-rail-mock.html`. */
.w-cells {
  display: grid;
  grid-template-columns: repeat(var(--w-cells-n), 1fr);
  border: 1px solid var(--axis);
  border-radius: var(--radius);
  overflow: hidden;
}
/* The head names the column and sets nothing, so it is recessive and on the
   second surface — the cells below it are the control. */
.w-cell-head {
  grid-row: 1;
  padding: 2px 0;
  font-size: var(--fs-xs);
  color: var(--ink-3);
  text-align: center;
  background: var(--surface-2);
  border-right: 1px solid var(--grid);
}
.w-cells .w-cell-head:last-of-type { border-right: 0; }
.w-cell {
  grid-row: 2;
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  padding: 4px 2px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--ink-1);
  background: var(--surface-1);
  border: 0;
  border-top: 1px solid var(--grid);
  border-right: 1px solid var(--grid);
}
.w-cells .w-cell:last-child { border-right: 0; }
/* inset, because the container clips its own edge and an outset ring on the
   first or last cell would be cut in half by it */
.w-cell:focus { outline: 2px solid var(--c-highlight); outline-offset: -2px; }

/* A caption under one row of a grouped segmented control. Recessive, because
   the buttons are the control and this only says what kind they are. */
.w-seg-cap {
  margin: var(--sp-1) 0 var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}
.w-seg-cap:last-child { margin-bottom: 0; }
/* a group's heading above its row (`groupHeads`): the label's size and ink,
   so the two halves read as sub-labels of the field */
.w-seg-head {
  margin: var(--sp-2) 0 var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-2);
}
.w-seg-head:first-child { margin-top: 0; }

/* A detail with nothing in it is not a blank line. The segmented control builds
   its element up front because the text changes with the selection, so the
   empty case has to be handled here rather than by not building it. */
.w-detail:empty { display: none; }

.w-stage {
  min-width: 0; /* or the canvas refuses to shrink inside the grid track */
}

.w-split .w-figure {
  margin-top: var(--sp-4);
}

@media (max-width: 880px) {
  .w-root[data-layout="side"] { max-width: 780px; }
  .w-split { display: block; }
  .w-rail { border-top: 0; padding-top: 0; margin-top: 0; }
  .w-split .w-controls {
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    border-top: 1px solid var(--grid);
    padding-top: var(--sp-4);
    margin-top: var(--sp-4);
  }
}


/* --- figure --------------------------------------------------------------- */
.w-figure {
  position: relative;
  margin: var(--sp-4) 0 0;
}

.w-figure canvas {
  display: block;
  width: 100%;
  height: auto;
}

/* --- legend: always present for >= 2 series ------------------------------ */
.w-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4);
  margin: var(--sp-3) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--ink-2);
}

.w-legend li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.w-legend .swatch {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--swatch, var(--ink-3));
}

.w-legend .swatch[data-mark="line"] {
  height: 3px;
  border-radius: 2px;
}

.w-legend .swatch[data-mark="dot"] {
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

/* A DASHED RULE, for the second reference line in one panel.

   Added 2026-09-02, for widget 39: it draws the median of all samples solid and
   the mean of all samples dashed, in one colour, because both are the fixed
   benchmark the ten samples are judged against and the dash is what tells them
   apart — the same distinction `spanningRule` already takes as an argument. The
   legend had no way to say it, so two references would have shown two identical
   swatches and the reader could not have told which line was which.

   A repeating-linear-gradient rather than a border, so the dash keeps the
   swatch's own colour token and needs no second variable. 3px on, 3px off,
   matching the canvas's `setLineDash([4, 3])` closely enough at 12px wide that
   the swatch reads as the same kind of line. Purely additive: nothing else in
   the collection sets `data-mark="dash"`, so no existing legend can move. */
.w-legend .swatch[data-mark="dash"] {
  height: 3px;
  border-radius: 0;
  background: repeating-linear-gradient(
    to right,
    var(--swatch, var(--ink-3)) 0 3px,
    transparent 3px 6px
  );
}

/* A TRIANGLE, and a DASHED BOX — added 2026-09-05 for widget 44.

   That widget puts two groupings on one mark: colour is the arm and SHAPE is
   the confounder, which is what lets both arms share one value axis instead of
   needing separate stacked panels. A legend that named the shapes in words
   while showing two identical squares would be describing marks the figure
   does not draw, so the shapes get swatches.

   `ring` is the dashed box the figure puts round a subject the study selected.
   It is an outline rather than a fill because that is what it is on the canvas.

   Purely additive, like `dash` before them: nothing else in the collection sets
   either mark, so no existing legend can move. */
.w-legend .swatch[data-mark="tri"] {
  border-radius: 0;
  clip-path: polygon(50% 4%, 100% 100%, 0 100%);
}

/* A HOLLOW MARK — added 2026-09-05, the same day and for the same widget as
   `tri` and `ring`. Widget 44 distinguishes the confounder by fill against
   outline, not by fading one of them: half a population is not less important
   than the other half. The legend has to show both weights or it is naming a
   mark the figure does not draw. */
.w-legend .swatch[data-mark="hollow"] {
  background: none;
  border: 1.5px solid var(--swatch, var(--ink-3));
  border-radius: 50%;
  width: 10px;
  height: 10px;
}

.w-legend .swatch[data-mark="ring"] {
  background: none;
  border: 1px dashed var(--swatch, var(--ink-3));
  border-radius: 2px;
}

/* --- readout: stat tiles. Also the accessible text of the figure. -------- */
.w-readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--sp-3);
  margin: var(--sp-4) 0 0;
  padding: var(--sp-3) 0 0;
  border-top: 1px solid var(--grid);
}

/* a readout row break (`{ break: true }` in a widget's tiles): a full-width
   zero-height cell, so tiles of a different kind start their own row */
.w-stat-break {
  grid-column: 1 / -1;
  height: 0;
}

.w-stat-label {
  display: block;
  color: var(--ink-2);
  font-size: var(--fs-xs);
  text-transform: none;
}

.w-stat-value {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-fig);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.w-stat-note {
  display: block;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

/* --- controls ------------------------------------------------------------- *
 * The setup block comes FIRST, above the figure: choose what you are sampling
 * from before you sample from it. Reading order is the instruction.           */
.w-controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--sp-3) var(--sp-4);
  margin: var(--sp-4) 0 0;
  padding: var(--sp-4) 0 0;
  border-top: 1px solid var(--grid);
}

.w-field { min-width: 0; }

.w-field > label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-2);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  margin-bottom: var(--sp-1);
}

.w-field .val {
  color: var(--ink-1);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.w-field input[type="range"] {
  width: 100%;
  margin: 0;
  accent-color: var(--focus);
}

.w-field select {
  width: 100%;
  padding: 5px var(--sp-2);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-1);
  background: var(--surface-1);
  border: 1px solid var(--axis);
  border-radius: var(--radius);
}

/* Adjacent checkboxes share one grid cell and one row: they are usually one
   decision with several switches, and a lone checkbox in a slider-width column
   scatters them across lines. */
.w-bools {
  /* Spans every column so the switches stay on ONE line. In a single
     slider-width cell two labels already wrap, which puts them back on separate
     rows — the thing grouping them was meant to fix. */
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
}

/* THE RUN IN THE SAME N COLUMNS AS A CELLS GRID ABOVE IT, when the run's first
   field declares a `cells` count. Opt-in: the flex line above is 46px off five
   equal columns of a 300px rail, which is fine for two overlay toggles and is a
   whole column's error when the switch under a value is meant to be the switch
   for that class. */
.w-bools--cols {
  display: grid;
  grid-template-columns: repeat(var(--w-cells-n), 1fr);
  gap: 0;
}
.w-bools--cols .w-bool { align-items: center; }
.w-bools--cols label { justify-content: center; gap: 4px; }

/* One switch, and its detail beneath it when it has one. The ROW is the flex
   container and each switch is a column inside it, which is what stops a detail
   landing beside the next checkbox instead of under its own.

   `min-width: 0` so a long detail wraps inside the column rather than widening
   it: the label carries `white-space: nowrap`, and without this the column's
   min-content would be the longest unbroken run in the detail. */
.w-bool {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* A boolean wearing a membership chip — `style: "pill"` on a bool field.
   The button's aria-pressed IS the visual state, so controls.js keeps no
   class in sync. The on-state tints with --c-highlight because joining the
   model is the thing to look at right now. */
.w-pill {
  border: 1.5px solid var(--ink-3);
  border-radius: 999px;
  padding: 5px 14px;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  background: transparent;
  cursor: pointer;
  white-space: nowrap;
}
/* A FULL FILL, not a tint (widget 31 round 15 — Kenneth: the 18% wash
   was hard to see which covariates were in). Membership is binary and
   the pressed state is the one thing to look at, so it wears the
   highlight outright, with surface-colour text — the same
   text-on-highlight move the canvas bars use. Shared by every pill
   rail (the lm arc's model pills included), deliberately: one control,
   one look. */
.w-pill[aria-pressed="true"] {
  border-color: var(--c-highlight);
  background: var(--c-highlight);
  color: var(--surface-1);
  font-weight: 600;
}

/* Descendant rather than child, now that a switch sits inside `.w-bool`. The
   two select the same elements for every widget that declares no bool detail,
   so nothing that exists today moves. */
.w-bools label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  color: var(--ink-2);
  font-size: var(--fs-sm);
  cursor: pointer;
  white-space: nowrap;
}

.w-field input[type="checkbox"] {
  accent-color: var(--focus);
  flex: none;
  margin: 0;
}

/* Tick labels under a `choice` slider. A slider without them shows a position
   and hides what the positions are — worse than the dropdown it replaced. */
.w-ticks {
  display: flex;
  justify-content: space-between;
  margin-top: 1px;
}

.w-ticks span {
  font-size: 10px;
  color: var(--ink-3);
  white-space: nowrap;
}

/* What the current setting actually does, so a short tick label can stay short. */
.w-detail {
  margin: 2px 0 0;
  min-height: 1.2em;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

/* Segmented control: a small set of alternative readings, all visible at rest. */
.w-seg {
  display: flex;
  border: 1px solid var(--axis);
  border-radius: var(--radius);
  overflow: hidden;
}

.w-seg-btn {
  flex: 1;
  min-width: 0;
  padding: 5px 6px;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  background: var(--surface-1);
  border: 0;
  border-right: 1px solid var(--grid);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The bits chips (an int worn as toggles — controls.js): ten of them have
   to share one rail row, so the horizontal padding goes and a minimum
   keeps a one-digit chip square enough to press. A pressed chip fills
   with the highlight like a pressed pill does — both mean "in", and the
   segmented grey was invisible at chip size (round 15). */
.w-seg-bits .w-seg-btn {
  padding: 5px 0;
  min-width: 22px;
  font-size: var(--fs-xs);
}
.w-seg-bits .w-seg-btn[aria-pressed="true"] {
  background: var(--c-highlight);
  color: var(--surface-1);
}

/* TWO COLUMNS INSTEAD OF ONE ROW, for an option list too long to share a rail
   row. A row is `flex: 1`, so it divides the rail by the option count: five
   options get 45px at a 250px rail and "Min-max" needs 61. Two columns give
   each option 111px at the same width. Measured in `_lab/norm-picker.html`.

   The cost is rail height — 87px against a dropdown's 30 — and what it buys is
   every option visible at rest, which is what 3.3 asks for whenever the width
   allows it. An option marked `span: true` takes a full row. */
.w-seg-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.w-seg-grid .w-seg-btn { border-bottom: 1px solid var(--grid); }
.w-seg-grid .w-seg-btn:nth-child(2n) { border-right: 0; }
.w-seg-grid .w-seg-btn--span { grid-column: 1 / -1; border-right: 0; }
/* A FACE OVER ITS QUALIFIER (`qual` on an option — controls.js). The name keeps
   the face's own size and weight; the second line is the detail's size and ink,
   because it qualifies the name rather than competing with it. The class is on
   the RUN, so every button in a row that has a qualifier takes the two-line box
   and the row stays level (3.4d); a row with none is untouched. */
.w-seg--two .w-seg-btn {
  display: block;
  padding-top: 3px;
  padding-bottom: 3px;
  white-space: normal;
  line-height: 1.25;
}
.w-seg-name,
.w-seg-qual { display: block; }
.w-seg-qual {
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--ink-3);
}

/* The container clips its own outer edge, so the final row's bottom border and
   the last column's right border fall outside it and no rule doubles up. */
.w-seg-btn:last-child { border-right: 0; }
.w-seg-btn:hover { background: var(--surface-3); }

/* an option wearing a semantic token (spec `token:`) carries a swatch dot,
   so the button and the mark it selects share a hue */
.w-seg-btn .swatch {
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 6px;
  border-radius: 50%;
  background: var(--swatch, var(--ink-3));
  vertical-align: -1px;
}

.w-seg-btn[aria-pressed="true"] {
  background: var(--surface-3);
  color: var(--ink-1);
  font-weight: 600;
}

.w-seg-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

/* --- actions -------------------------------------------------------------- *
 * Two rows, deliberately unequal. The drive row is what a student presses to
 * make something happen and sits directly above the figure; the utility row is
 * housekeeping and sits at the very bottom, styled to recede. Making the
 * important control look important is the cheapest scaffolding there is.      */
.w-drive {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 0;
}

.w-utility {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 0;
  padding: var(--sp-3) 0 0;
  border-top: 1px solid var(--grid);
}

.w-btn {
  padding: 5px 10px;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-1);
  background: var(--surface-1);
  border: 1px solid var(--axis);
  border-radius: var(--radius);
  cursor: pointer;
}

/* `style: "action"`: a bool that reads as a press rather than a chip. It takes
   the drive row's sizing and the full width of its rail cell, so it sits as a
   sibling of Reset. Declared AFTER `.w-btn` — at equal specificity the later
   rule wins, and its padding and font-size are the ones being replaced — and
   the FIELD carries the flex basis because the bools row that holds it is a
   flex line and the field, not the button, is its item — styling the button
   alone left it at content width. */
.w-action-field { flex: 1 1 100%; }
.w-action-btn {
  width: 100%;
  justify-content: center;
  padding: 7px 16px;
  font-size: var(--fs-md);
}

/* Every button in the drive row is the SAME SIZE. They are pressed
   interchangeably, and a smaller Reset reads as a different class of control
   rather than a quieter member of the same one. Emphasis comes from weight and
   ink, and separation from a gap — never from geometry. */
.w-drive .w-btn {
  padding: 7px 16px;
  font-size: var(--fs-md);
}

/* Draft bar. Sits above everything, including the heading, because its whole
   job is to be read before the figure is believed. */
.w-draft {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  background: var(--notice-bg);
  color: var(--notice-ink);
  border-left: 3px solid var(--notice-rule);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
}
.w-draft strong { font-weight: 600; }

/* Heading left, tools right, aligned to the title's first line. */
.w-root > header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}
.w-heading { flex: 1 1 auto; min-width: 0; }
.w-header-tools { flex: 0 0 auto; }

.w-btn--primary { font-weight: 600; }

/* An icon button is square and optically the same weight as a text button
   beside it: same height, same border, same hover. The icon inherits
   currentColor so it tracks the button's ink in both themes without a second
   colour token. */
/* Square by construction. It sits alone in the header rather than in a row of
   text buttons, so there is no neighbour to take a height from — an explicit
   square is honest here, where restating a text button's height would not be. */
.w-btn--icon {
  display: inline-grid;
  place-items: center;
  padding: 0;
  width: 2rem;
  height: 2rem;
  color: var(--ink-2);
}
.w-btn--icon:hover { color: var(--ink-1); }
.w-btn--icon svg { display: block; }

/* A connected cluster of buttons that are PACES of one action — step and play.
   Mocked against five alternatives in _lab/drive-row.html, where the deciding
   fact was measured rather than judged: three separate buttons need 273px and
   the control rail is 262, so the row wrapped and Reset fell to its own line.
   Fencing the pair reclaims exactly the gap that overflowed.

   3.4 says separation comes from a gap and never from geometry. That was written
   about making Reset SMALLER; every button here is still the same size, and the
   fence groups rather than ranks. Reset is never inside one. */
.w-drive-group {
  display: inline-flex;
  border: 1px solid var(--axis);
  border-radius: var(--radius);
  overflow: hidden;
}
.w-drive-group .w-btn {
  border: 0;
  border-right: 1px solid var(--grid);
  border-radius: 0;
  margin-left: 0;
}
.w-drive-group .w-btn:last-child { border-right: 0; }
.w-drive-group .w-btn:disabled { opacity: 0.5; }
/* `display: inline-flex` outranks the UA sheet's `[hidden] { display: none }`,
   the same collision the drive row itself hit. A widget that declares
   `anim.inert` hides this group, and without the rule it would stay on screen
   as an empty bordered box while reporting `hidden === true`. */
.w-drive-group[hidden] { display: none; }

/* Set by core to the widest label the run button can hold — see 3.4d. A
   variable rather than an inline style so the rail below can drop it. */
.w-drive .w-btn[data-key="run"] { min-width: var(--run-reserve, auto); }
/* The same, for a step button whose label depends on a parameter. Both are
   dropped in the rail by the flex:1 override above, which is correct there. */
.w-drive .w-btn[data-key="step"] { min-width: var(--step-reserve, auto); }

.w-drive .w-btn:not(.w-btn--primary) {
  margin-left: var(--sp-3);
  color: var(--ink-2);
}

.w-utility .w-btn {
  color: var(--ink-2);
  border-color: var(--grid);
}

.w-btn:hover { background: var(--surface-3); }
.w-btn:active { transform: translateY(0.5px); }
.w-btn[aria-pressed="true"] { background: var(--surface-3); }

.w-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.w-btn:disabled:hover { background: var(--surface-1); }

:where(.w-btn, select, input):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.w-flash {
  align-self: center;
  color: var(--ink-2);
  font-size: var(--fs-sm);
}

/* --- table view: nothing is gated behind the figure --------------------- */
.w-table-wrap {
  margin: var(--sp-3) 0 0;
  max-height: 240px;
  overflow: auto;
  border: 1px solid var(--grid);
  border-radius: var(--radius);
}

.w-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.w-table th,
.w-table td {
  padding: var(--sp-1) var(--sp-2);
  text-align: right;
  border-bottom: 1px solid var(--grid);
}

.w-table th {
  position: sticky;
  top: 0;
  background: var(--surface-1);
  color: var(--ink-2);
  font-weight: 600;
  text-align: right;
}

.w-table th:first-child,
.w-table td:first-child { text-align: left; }

.w-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
