/* ==========================================================================
   kiaribot.com — site.css
   Everything that is not a raw token. Read this header before you write markup.
   ==========================================================================

   ## 1. HOW THE CASCADE IS STRUCTURED (read this or you will fight it)

   All rules in this file live in cascade layers, declared in this order:

       @layer reset, base, ground, layout, components, utilities;

   Later layer always wins, regardless of selector specificity. So:
     - a `.tone-bot` utility beats `.panel__title` without !important
     - a component never accidentally cancels a base rule
     - nobody has to count selectors

   Two consequences you need to know:

   a) YOUR PAGE CSS, IF ANY, GOES UNLAYERED AND AUTOMATICALLY WINS over
      everything here. Unlayered declarations outrank every layer. You should
      not need it (see rule 3 below), but if you do, no !important required.

   b) tokens.css is unlayered and defines only RAW names (--ink, --bot,
      --t-body...). This file defines the RESOLVED names (--ground, --text,
      --bot-text...). The two sets never share a name. Do not add a resolved
      name to tokens.css: unlayered would beat the ground layer and the whole
      light/dark system would stop switching.

   ## 2. THE GROUND SYSTEM — the structural idea

   Every section declares its ground:

       <section class="section" data-ground="ink">   ... dark. watching the machine.
       <section class="section" data-ground="paper"> ... light. reading about the product.

   That one attribute rewrites a set of custom properties on the element.
   Everything inside resolves from them. A component NEVER needs to know which
   ground it is on, and there must be no `[data-ground="paper"] .panel` rules
   anywhere. If you find yourself writing one, the component is reading the
   wrong variable.

   The resolved names, all available inside any [data-ground]:

     --ground         the section background
     --ground-lift    one step off the ground (rows, hovers, insets)
     --ground-panel   a raised surface (.panel, .card use this)
     --line           hairline / border colour
     --line-strong    a border you are meant to notice
     --text           body text. >= 4.5:1 on every surface in this ground.
     --text-dim       secondary text. >= 4.5:1.
     --text-faint     decorative / large only. ~3.3:1. NOT for body copy.
     --bot-text       blue, safe as body text on this ground
     --human-text     orange, safe as body text on this ground
     --warn-text      amber, safe as body text on this ground
     --bot-mark       blue at full chroma, for fills/strokes/dots (>=3:1)
     --human-mark     orange at full chroma, for fills/strokes/dots (>=3:1)
     --warn-mark      amber at full chroma, for fills/strokes/dots
     --bot-wash       ~12% blue, for tinted backgrounds
     --human-wash     ~12% orange
     --bot-solid      fill for a solid blue control
     --human-solid    fill for a solid orange control
     --bot-edge       the border that goes around --bot-solid
     --human-edge     the border that goes around --human-solid
     --on-solid       label colour on top of any *-solid fill
     --focus          focus-ring colour for this ground
     --scrim          translucent ground, for sticky bars over content

   `<body data-ground="ink">` is required in base.html. It sets the page
   default and its background propagates to the browser canvas (so overscroll
   is the right colour with no extra rule).

   Colour is meaning: blue = the bot, orange = the human. Never garnish.

   ## 3. WHAT YOU SHOULD NOT WRITE

   - No colour literal. tokens.css is the only place one exists.
   - No font-size that is not one of the seven --t-* steps.
   - No page-specific CSS if a class here does the job. Ask for a class
     instead of forking one.
   - `.section::before` is RESERVED for the automatic ground seam. Do not use
     it. `.section::after` is free.
   - Do not set `padding` on `.section` or on anything that is also a
     `.section`. See rule 4.

   ## 4. SECTION PADDING — the one trap this design has

   `.section` declares its vertical padding EXACTLY ONCE, in the layout layer:

       padding-block: var(--pad-top, var(--pad-block))
                      var(--pad-bottom, var(--pad-block));

   Nothing else in this file, or in yours, ever writes `padding-top` or
   `padding-block` on a section. Modifiers only ASSIGN THE VARIABLE:

       .section--tight  { --pad-block: var(--space-7); }
       .cta             { --pad-block: clamp(2.75rem, 4.5vw, 4.5rem); }

   Because there is only one padding declaration in existence, two rules can
   never cancel each other and specificity is irrelevant. If you need custom
   section padding, set --pad-block / --pad-top / --pad-bottom. Never the
   property.

   ## 5. CLASS VOCABULARY

   LAYOUT
     .wrap                 default 72rem column + page gutter
     .wrap--narrow         34rem, for centred prose blocks
     .wrap--wide           90rem, for charts and the arena
     .wrap--flush          gutter only, no max-width
     .section              a page section. REQUIRES data-ground.
       .section--tight  .section--loose
       .section--flush-top  .section--flush-bottom   (kill one side's padding)
       .section--seamless   (opt out of the ground seam)
     .section__head        eyebrow on its own row, then title and lead side
                           by side when the container is wide enough for
                           both and stacked when it is not. No media query:
                           it is flex-wrap and a basis, so it collapses
                           correctly inside any narrow column.
     .section__eyebrow     small mono caps label above a title
     .section__title       the section heading
     .section__lead        one paragraph under the title
     .stack                vertical rhythm; set --stack to change the gap
     .cluster              horizontal wrapping row; set --cluster-gap
     .grid                 auto-fit grid; set --col-min (default 16rem)
     .split                two columns, collapses under 62rem; set --split
     .scroll-x             wide child scrolls inside itself, not the page
     .rule                 hairline horizontal separator

   TYPE
     .display .title .head .lead .small .micro
     .prose                long-form body copy: paragraphs, lists, links
     .num                  any number on the site. mono + tabular figures.
       .num--lg .num--xl   bigger steps of the same
       .num--slot          fixed-width slot so live updates cannot shift
                           layout; set --slot (in ch), default 8
     .unit                 the suffix on a number ("steps", "%", "/s")

   TONE  (colour as meaning, safe on both grounds)
     .tone-bot .tone-human .tone-warn .tone-dim .tone-faint

   CHROME
     .skip-link
     .nav .nav__inner .nav__brand .nav__mark .nav__wordmark
       .nav__links .nav__link .nav__actions
       Mark the current page with aria-current="page".
       There is no toggle and no hamburger. Under 56rem the links take their
       own row and wrap onto a second if the labels need it. Nothing is
       hidden, nothing scrolls sideways, no JS is involved.
     .footer .footer__inner .footer__brand .footer__col .footer__title
       .footer__links .footer__note

   ACTIONS
     .btn                  base. always add a variant.
       .btn--human         primary. the visitor's action. "Take the wheel".
       .btn--bot           secondary, the bot's side.
       .btn--ghost         outline only
       .btn--sm .btn--lg .btn--block
     .link                 inline text link (also applied inside .prose)

   SURFACES
     .panel .panel--lift .panel__head .panel__title .panel__body .panel__foot
     .card
     .tag  .tag--bot  .tag--human

   DATA
     .stat-band            row of .stat, wraps and scrolls at narrow widths
     .stat .stat__value .stat__label .stat__note
     .kv .kv__k .kv__v     label/value rows
     .bar .bar__fill       set --pct (0-100) and optionally --bar-tone
     .ci                   confidence-interval band under a number
     .live .live__dot .live__label
     .chart                reserved aspect box for a canvas/svg; set --ratio
     .figure .figure__cap

   LIVE STATE CONTRACT  (live.js sets this, CSS reacts — no JS styling)
     [data-live="ok"]      normal
     [data-live="stale"]   last value shown, amber dot, dimmed
     [data-live="down"]    "not reporting". Never a zero, never a spinner.
                           Replace the value's contents with
                           <span class="not-reporting">not reporting</span>.
                           The template must do this server-side too, so a
                           first paint with the API down already reads right.
     .not-reporting        the down-state wording

   TIMELINE
     .timeline .timeline__item .timeline__node .timeline__when
       .timeline__title .timeline__body
       One column capped at the prose measure; two columns from 62rem up,
       filled down-then-across so the order stays first-to-last.

   ARENA MAP  (sections/live-panel.html, panel: "arena")
     .arena-map            the picture the arena section did not have
     .arena-map__source    the machine end: a live reading off the trainer
       .arena-map__label .arena-map__rate
     .arena-map__line      the stream, with the eight seats sitting on it
     .arena-map__seat  .arena-map__seat--driver
       .arena-map__pip .arena-map__wheel .arena-map__role
     .arena-map__cap       says the seats are the rule, not an occupancy
                           count. Nothing here reports who is watching.

   COMPARISON
     .compare .compare__row .compare__label
       .compare__side .compare__side--bot .compare__side--human

   RANK SCALE  (sections/rank-scale.html)
     .rank                 the block. set --tiers to the number of ranks.
     .rank__claim .rank__swatch    the range, said in words above the ladder
     .rank__scale          the ladder. one grid column per tier.
     .rank__tier           one tier. set --i to its 1-based column.
       [data-in-band]      this tier is inside the estimate
     .rank__name .rank__short      full name wide, short code narrow
     .rank__band           the estimate. set --from / --to (1-based grid
                           lines). Soft at both ends, on purpose: the ends
                           are the part nobody measured.
     .rank__anchor .rank__anchor-label   the yardstick, drawn neutral
     .rank__basis .rank__note

   CTA
     .cta .cta__title .cta__lead .cta__actions

   SIGNATURE LINE  (line.js owns the drawing, this file owns the box)
     .signature            fixed full-viewport SVG layer at --z-line,
                           pointer-events:none. Content sits above it.
     .line-node            a point the line is meant to pass through; give it
                           an id and line.js reads its box.

   UTILITIES
     .visually-hidden .u-center .u-nowrap .u-full .u-hide-sm .u-hide-lg

   ## 6. ACCESSIBILITY BUILT IN, NOT BOLTED ON

   - :focus-visible ring on everything interactive, --focus per ground,
     5.3:1 minimum against its ground.
   - prefers-reduced-motion kills transitions and scroll animation. The
     signature line still EXISTS under reduced motion, it just does not move.
   - html/body use overflow-x: clip (not hidden) so position:sticky survives.
   - Body text >= 4.5:1 on every surface. --text-faint is ~3.3:1 and is for
     large or decorative text only; inside .panel it is raised to --text-dim
     because the panel surface is lighter and faint would drop under 3:1.
   - Numbers use tabular figures everywhere and .num--slot reserves width, so
     a live update cannot reflow the page.

   ========================================================================== */

@layer reset, base, ground, layout, components, utilities;

/* ==========================================================================
   RESET
   ========================================================================== */
@layer reset {

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

  * { margin: 0; }

  html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    scroll-behavior: smooth;
    /* clip, not hidden: does not create a scroll container, so sticky works */
    overflow-x: clip;
  }

  body {
    min-height: 100svh;
    overflow-x: clip;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  img, svg, video, canvas, picture {
    display: block;
    max-width: 100%;
    height: auto;
  }

  input, button, textarea, select { font: inherit; color: inherit; }

  button { background: none; border: 0; }

  ul[class], ol[class] { list-style: none; padding: 0; }

  a { color: inherit; }

  table { border-collapse: collapse; }

  :target { scroll-margin-top: calc(var(--nav-h) + var(--space-6)); }
}

/* ==========================================================================
   BASE — element defaults, focus, motion
   ========================================================================== */
@layer base {

  :root {
    --nav-h: 4.25rem;
    color-scheme: dark light;
  }

  body {
    font-family: var(--font-body);
    font-size: var(--t-body);
    font-weight: var(--wt-body);
    line-height: var(--lh-loose);
    letter-spacing: var(--tr-normal);
    font-optical-sizing: auto;
  }

  h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: var(--wt-display);
    line-height: var(--lh-snug);
    letter-spacing: var(--tr-snug);
    text-wrap: balance;
  }

  h1 { font-size: var(--t-title); }
  h2 { font-size: var(--t-head); }
  h3 { font-size: var(--t-lead); }
  h4 { font-size: var(--t-body); }

  p { text-wrap: pretty; }

  code, kbd, samp, pre {
    font-family: var(--font-mono);
    font-size: 0.9em;
    font-variant-numeric: tabular-nums;
  }

  hr {
    border: 0;
    border-top: var(--stroke) solid var(--line);
  }

  /* Visible keyboard focus, everywhere, on both grounds.
     No border-radius here: that would reshape the element itself (squaring
     off every pill button the moment it is focused). Browsers already draw
     the outline following the element's own radius. */
  :focus-visible {
    outline: var(--stroke-bold) solid var(--focus);
    outline-offset: 3px;
  }
  /* Mouse users do not get the ring; keyboard users always do. */
  :focus:not(:focus-visible) { outline: none; }

  ::selection {
    background: var(--bot-solid);
    color: var(--on-solid);
  }

  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
      animation-duration: 1ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 1ms !important;
      scroll-behavior: auto !important;
    }
    /* The signature line still exists. It is simply drawn complete and
       does not respond to scroll. line.js must check this query too. */
    .signature { opacity: 1 !important; }
  }
}

/* ==========================================================================
   GROUND — the light/dark system. The whole idea lives in this block.
   Nothing below this layer may reference --ink/--paper/--bot/--human
   directly. Read the resolved names instead.
   ========================================================================== */
@layer ground {

  /* Default = ink. :root and [data-ground="ink"] carry the same set, so a
     missing attribute degrades to dark rather than to unstyled. */
  :root,
  [data-ground="ink"] {
    --ground:       var(--ink);
    --ground-lift:  var(--ink-lift);
    --ground-panel: var(--ink-panel);
    --line:         var(--ink-line);
    --line-strong:  var(--on-ink-faint);

    --text:         var(--on-ink);
    --text-dim:     var(--on-ink-dim);
    --text-faint:   var(--on-ink-faint);

    /* on ink the vivid pair already clears 4.5:1 (6.10 and 7.53) */
    --bot-text:     var(--bot);
    --human-text:   var(--human);
    --warn-text:    var(--warn);

    --bot-mark:     var(--bot);
    --human-mark:   var(--human);
    --warn-mark:    var(--warn);

    --bot-wash:     color-mix(in oklab, var(--bot) 14%, var(--ground));
    --human-wash:   color-mix(in oklab, var(--human) 14%, var(--ground));

    /* filled controls: fill / its border / the label on top of it */
    --bot-solid:    var(--bot);
    --human-solid:  var(--human);
    --bot-edge:     var(--bot);
    --human-edge:   var(--human);
    --on-solid:     var(--ink);

    --focus:        var(--bot);
    --scrim:        var(--tint-ink-90);
    --shadow:       var(--shadow-lg);
  }

  [data-ground="paper"] {
    --ground:       var(--paper);
    --ground-lift:  color-mix(in oklab, var(--paper-line) 34%, var(--paper));
    --ground-panel: var(--paper-lift);
    --line:         var(--paper-line);
    --line-strong:  var(--on-paper-dim);

    --text:         var(--on-paper);
    --text-dim:     var(--on-paper-dim);
    /* no faint tier exists in the palette for paper; derive one at the same
       ~3.3:1 role the ink faint plays. Measured 3.45:1 on --paper. */
    --text-faint:   color-mix(in oklab, var(--on-paper-dim) 70%, var(--paper));

    /* the vivid pair fails as text on paper (2.80 and 2.27). Step down.
         --bot-text   = --bot-quiet                        5.34:1  AA
         --human-text = --human-quiet darkened 15%         5.05:1  AA
         --warn-text  = --warn darkened 50%                5.13:1  AA
       Derivations, not new palette entries: no literal is introduced. */
    --bot-text:     var(--bot-quiet);
    --human-text:   color-mix(in oklab, var(--human-quiet) 85%, var(--on-paper));
    --warn-text:    color-mix(in oklab, var(--warn) 50%, var(--on-paper));

    /* marks are graphics, not text: 3:1 is the bar, and the quiet pair
       clears it, so the encoding stays legible without shouting. */
    --bot-mark:     var(--bot-quiet);
    --human-mark:   var(--human-quiet);
    --warn-mark:    color-mix(in oklab, var(--warn) 70%, var(--on-paper));

    --bot-wash:     color-mix(in oklab, var(--bot) 12%, var(--paper-lift));
    --human-wash:   color-mix(in oklab, var(--human) 12%, var(--paper-lift));

    /* Filled controls keep the VIVID fill on paper, not the quiet one.
       --human-quiet is a dead zone: nothing in the palette reaches 4.5:1 on
       it (best is 4.38 dark, 3.90 light), so a solid quiet button cannot
       carry a legible label. Vivid fill + near-black label gives 7.43:1 for
       human and 6.02:1 for bot; the quiet tone becomes the 1px edge, which
       restores the 3:1 boundary the vivid fill lacks against paper. */
    --bot-solid:    var(--bot);
    --human-solid:  var(--human);
    --bot-edge:     var(--bot-quiet);
    --human-edge:   var(--human-quiet);
    --on-solid:     var(--on-paper);

    --focus:        var(--bot-quiet);
    --scrim:        var(--tint-paper-90);
    --shadow:       var(--shadow-md);
  }

  /* Paint the ground. Applies to <body data-ground> too, and body's
     background propagates to the browser canvas, so overscroll is correct
     without touching <html>. */
  [data-ground] {
    background-color: var(--ground);
    color: var(--text);
  }
}

/* ==========================================================================
   LAYOUT
   ========================================================================== */
@layer layout {

  /* --- container ---------------------------------------------------------- */
  .wrap {
    position: relative;
    /* lifts content above the fixed signature line (--z-line: 1) while the
       section ground and the seam stay under it. This is why every section's
       content belongs in a .wrap. Anything full-bleed that must also sit
       above the line sets z-index: var(--z-raise) itself. */
    z-index: var(--z-raise);
    width: 100%;
    max-width: var(--w-wrap);
    margin-inline: auto;
    padding-inline: var(--space-gutter);
  }
  .wrap--narrow { max-width: var(--w-narrow); }
  .wrap--wide   { max-width: var(--w-wide); }
  .wrap--flush  { max-width: none; }

  /* --- section ------------------------------------------------------------ *
   * THE ONLY PLACE SECTION PADDING IS DECLARED. Modifiers set the variable,
   * never the property. See header rule 4.                                  */
  .section {
    /* RHYTHM. Retuned twice now, and this is the measurement behind the
       current numbers rather than an opinion about them.

       At 6.5vw the gap between two sections measured 187px at 1440 and the
       page had eight of them. A sweep for vertical runs with nothing painted
       in them found 1,404px of empty band on a 7,938px page: 247px between
       the curriculum and the evaluation, 226px before the closing title,
       199px twice, 187px, 144px above the footer. Those are the numbers to
       re-run before changing this line (px/gaps.mjs in the round-six notes,
       or any sweep that walks painted boxes and reports the holes).

       3.2vw puts the same gap at 92px, which is still a clear beat between
       two sections and no longer a screen-and-a-bit of nothing on a laptop.
       The ends of the clamp are what matter: 2.5rem is a phone, where the
       gap was never the problem, and 4rem caps it above about 2000.
       --space-sec is left alone in tokens.css; a section that wants the old
       measure asks for it by name. */
    --pad-block: clamp(2.5rem, 3.2vw, 4rem);
    /* relative, but deliberately NO z-index: a z-index here would create a
       stacking context and trap all content beneath the fixed signature
       line. Content rises via .wrap instead. */
    position: relative;
    padding-block: var(--pad-top, var(--pad-block))
                   var(--pad-bottom, var(--pad-block));
  }

  .section--tight        { --pad-block: var(--space-6); }
  .section--loose        { --pad-block: var(--space-sec); }
  .section--flush-top    { --pad-top: 0; }
  .section--flush-bottom { --pad-bottom: 0; }

  /* --- the ground seam ---------------------------------------------------- *
   * Where ink meets paper, the join gets a soft edge instead of a hard cut.
   * The seam belongs to the SECOND section (which knows its own ground) and
   * is coloured from the FIRST via the sibling selector. Two mutually
   * exclusive rules, identical specificity, nothing to cancel.
   *
   * Keep it SHORT. Any ink-to-paper ramp passes through a mid-grey that
   * belongs to neither palette; over 96px that reads as fog or a rendering
   * artifact, which is worse than the cut it was meant to soften. At ~24px,
   * with the ramp weighted toward the near end, the muddy middle is a few
   * pixels and the join just looks soft. Continuity across the boundary is
   * the signature line's job, not this gradient's.
   *
   * .section::before is reserved for this. Do not use it elsewhere.        */
  .section::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: clamp(1rem, 2vw, 1.75rem);
    z-index: var(--z-under);
    pointer-events: none;
  }
  [data-ground="ink"] + [data-ground="paper"]::before {
    background: linear-gradient(
      var(--ink),
      color-mix(in srgb, var(--ink), transparent 55%) 45%,
      transparent);
  }
  [data-ground="paper"] + [data-ground="ink"]::before {
    background: linear-gradient(
      var(--paper),
      color-mix(in srgb, var(--paper), transparent 55%) 45%,
      transparent);
  }
  .section--seamless::before { content: none; }

  /* --- section head ------------------------------------------------------- *
   * A MASTHEAD, NOT A STACK. Eyebrow on its own row; then the title and the
   * lead sit side by side when there is room for both and stack when there
   * is not. Two things follow from doing it with flex-wrap and a basis
   * rather than with a media query or :has():
   *
   *   - it fills the right of the head. A title, then a lead capped at the
   *     prose measure, then a body also capped at it, left the right third
   *     of the column empty for the height of the whole head. Now the head
   *     is as wide as its section.
   *   - it costs nothing in a narrow container. 22rem + 26rem + the column
   *     gap does not fit a 29rem column, so anywhere the head is put in a
   *     column of its own (.rank__intro, .split, a partial's own grid) the
   *     two wrap back into a stack with no second rule needed, at every
   *     width, without anyone maintaining a list of them.
   *
   * align-items: baseline sits the lead's first line on the title's
   * baseline. That is the join that makes the two read as one masthead
   * rather than as two blocks that happen to be adjacent. */
  .section__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--space-7);
    row-gap: var(--space-4);
    margin-bottom: var(--space-5);
  }
  .section__head > .section__eyebrow { flex: 1 1 100%; }
  .section__head > .section__title   { flex: 1 1 22rem; }
  .section__head > .section__lead    { flex: 1 1 26rem; }
  .section__eyebrow {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: var(--wt-medium);
    line-height: var(--lh-flat);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  /* --t-title tops out at 4.5rem, which is the right ceiling for a page
     title and too much for a section opener: at 1440 it put "Roughly where
     that puts it" on three 72px lines beside a right column holding six
     lines of body copy, and the imbalance is what read as a hole rather
     than as a heading. The vw term is a FIT CONSTRAINT, not a new step in
     the scale (same idiom as the step counter in live-panel.html): the size
     is still --t-title, never allowed past 3.4vw, and never under the
     token's own 2.25rem floor, so every width at or below about 1060 --
     every phone and small tablet -- renders exactly what it rendered
     before. The hero keeps --t-display and is now unmistakably the largest
     thing on the page, which is the point of a hero. */
  .section__title {
    font-family: var(--font-display);
    font-size: min(var(--t-title), max(2.25rem, 3.4vw));
    font-weight: var(--wt-display);
    line-height: var(--lh-snug);
    letter-spacing: var(--tr-snug);
    text-wrap: balance;
  }
  .section__lead {
    font-size: var(--t-lead);
    line-height: var(--lh-normal);
    color: var(--text-dim);
    max-width: var(--w-prose);
  }

  /* --- the editorial rail: REMOVED, and do not put it back ---------------- *
   * There used to be a three-selector :has() rule here that turned any
   * section whose body was narrower than its column into a two-column grid:
   * a sticky heading in a 0.85fr rail on the left, the body in 1.15fr on the
   * right. It was written to fix an empty right third and it fixed it by
   * moving the hole to the left, under the heading. Measured at 1440 on the
   * page it shipped: the timeline's rail column held a 196px head beside a
   * 789px body, so 590px of that column was empty, and the arena's head was
   * TALLER than its body, so the rail made that section 120px taller than
   * its content for nothing.
   *
   * .section__head above does the same job without a second grid: the lead
   * moves up beside the title and fills the width the rail was invented to
   * fill. Bodies that want two columns say so themselves (.timeline does,
   * below).
   *
   * The rule also carried its exclusion list three times over, once per
   * selector, which is the shape of thing this file is meant not to have:
   * three places to keep in sync and no error if you miss one. If a section
   * genuinely needs a rail, give it .split in its own partial the way
   * prose.html does. Do not re-derive one from :has().                     */

  /* --- primitives --------------------------------------------------------- */
  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--stack, var(--space-5));
  }

  .cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cluster-gap, var(--space-4));
  }

  .grid {
    display: grid;
    gap: var(--grid-gap, var(--space-5));
    grid-template-columns: repeat(
      auto-fit, minmax(min(var(--col-min, 16rem), 100%), 1fr));
  }

  .split {
    display: grid;
    gap: var(--split-gap, var(--space-7));
    grid-template-columns: 1fr;
  }
  @media (min-width: 62rem) {
    .split { grid-template-columns: var(--split, 1fr 1fr); }
  }

  /* Wide things scroll inside their own box. The page body never scrolls
     sideways, at any width down to 360px. */
  .scroll-x {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }

  .rule {
    border: 0;
    border-top: var(--stroke) solid var(--line);
    margin-block: var(--space-6);
  }
}

/* ==========================================================================
   COMPONENTS
   ========================================================================== */
@layer components {

  /* --- signature line box ------------------------------------------------- *
   * line.js draws into this. It never receives pointer events and never
   * blocks selection. Content sits above it via .section > * z-index.      */
  .signature {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: var(--z-line);
    pointer-events: none;
    contain: layout paint style;
  }
  .line-node { scroll-margin-top: var(--nav-h); }

  /* --- skip link ---------------------------------------------------------- */
  .skip-link {
    position: absolute;
    left: var(--space-4);
    top: var(--space-4);
    z-index: var(--z-toast);
    transform: translateY(-200%);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--r-sm);
    background: var(--ground-panel);
    color: var(--text);
    font-size: var(--t-small);
    font-weight: var(--wt-semi);
    text-decoration: none;
    transition: transform var(--dur-fast) var(--ease-out);
  }
  .skip-link:focus-visible { transform: translateY(0); }

  /* --- nav ---------------------------------------------------------------- */
  .nav {
    position: sticky;
    top: 0;
    z-index: var(--z-nav);
    background: var(--scrim);
    border-bottom: var(--stroke) solid var(--line);
    backdrop-filter: blur(14px) saturate(1.3);
  }
  .nav__inner {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    min-height: var(--nav-h);
  }
  .nav__brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    margin-right: auto;
    font-family: var(--font-display);
    font-size: var(--t-body);
    font-weight: var(--wt-heavy);
    letter-spacing: var(--tr-snug);
    text-decoration: none;
    white-space: nowrap;
  }
  .nav__mark {
    width: 1.5rem;
    height: 1.5rem;
    flex: none;
    border-radius: var(--r-xs);
    background: var(--bot-mark);
  }
  .nav__wordmark { color: var(--text); }

  .nav__links {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    margin: 0;
  }
  .nav__link {
    display: inline-block;
    padding-block: var(--space-2);
    font-size: var(--t-small);
    font-weight: var(--wt-medium);
    color: var(--text-dim);
    text-decoration: none;
    transition: color var(--dur-fast) var(--ease-out);
  }
  .nav__link:hover { color: var(--text); }
  .nav__link[aria-current="page"] {
    color: var(--text);
    box-shadow: inset 0 -2px 0 var(--bot-mark);
  }

  .nav__actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
  }

  /* NARROW NAV — no hamburger, nothing hidden, nothing cut off.
   *
   * What used to be here was a second, complete mobile nav: a .nav__toggle
   * button and an absolutely positioned sheet collapsed by
   * .nav:has(.nav__toggle[aria-expanded="false"]). This site has never
   * rendered a .nav__toggle, so none of it ever applied, and base.html's
   * unlayered block spent twenty lines turning each of those declarations
   * back off. Two implementations of the same component, one of them dead and
   * the other one written as its antidote. It is one implementation now, and
   * it lives here rather than in base.html because it is not page CSS.
   *
   * Under 56rem the links drop to their own row and WRAP onto a second one if
   * they need it. They used to scroll sideways instead, which put the last
   * link half past the right edge of a 390px screen with nothing to say it
   * was there — measured: the strip needed 453px of a 390px viewport, and
   * "Questions" ran from x=367 to x=433. Wrapping cannot do that at any width
   * and cannot hide a link behind a gesture. Adding a sixth link costs a
   * shorter second row, not a scrollbar.
   *
   * The brand and "Take the wheel" hold the first row at every width. */
  @media (max-width: 56rem) {
    /* The nav is two rows at these widths, and --nav-h is what every
       scroll-margin and sticky offset on the site is measured against. Leaving
       it at the one-row 4.25rem put "/#the-numbers" — a link in this very nav —
       under the nav it had just been clicked in. Measured at 360px: the two
       rows come to 121px, so 7.5rem is that plus a hair. */
    :root { --nav-h: 7.5rem; }
    .nav__inner {
      flex-wrap: wrap;
      gap: var(--space-3);
      padding-block: var(--space-3);
    }
    .nav__links {
      order: 3;
      flex: 1 0 100%;
      flex-wrap: wrap;
      /* column-gap carries the separation; row-gap only has to keep two rows
         off each other, and the links' own padding does most of that */
      gap: 0 var(--space-5);
      padding: 0;
      border-top: var(--stroke) solid var(--line);
    }
    .nav__link {
      padding-block: var(--space-3);
      white-space: nowrap;
    }
  }

  /* --- footer -------------------------------------------------------------- */
  .footer {
    border-top: var(--stroke) solid var(--line);
    padding-block: var(--space-6) var(--space-5);
  }
  .footer__inner {
    display: grid;
    gap: var(--space-6);
    grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  }
  .footer__brand {
    font-family: var(--font-display);
    font-size: var(--t-head);
    font-weight: var(--wt-heavy);
    letter-spacing: var(--tr-snug);
  }
  .footer__title {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: var(--space-3);
  }
  .footer__links {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    font-size: var(--t-small);
  }
  .footer__links a { color: var(--text-dim); text-decoration: none; }
  .footer__links a:hover { color: var(--text); text-decoration: underline; }
  .footer__note {
    margin-top: var(--space-6);
    padding-top: var(--space-5);
    border-top: var(--stroke) solid var(--line);
    font-size: var(--t-small);
    color: var(--text-dim);
  }

  /* --- buttons ------------------------------------------------------------- */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-5);
    min-height: 2.75rem;          /* 44px touch target */
    border: var(--stroke) solid transparent;
    border-radius: var(--r-pill);
    font-size: var(--t-small);
    font-weight: var(--wt-semi);
    line-height: var(--lh-flat);
    letter-spacing: var(--tr-normal);
    text-align: center;
    text-decoration: none;
    /* action labels are short imperatives ("Take the wheel") and must not
       break mid-phrase. .btn--block re-enables wrapping for long labels. */
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out),
                transform var(--dur-fast) var(--ease-out);
  }
  .btn:active { transform: translateY(1px); }
  .btn[disabled], .btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
  }

  /* orange = the human = the visitor's action. This is the primary. */
  .btn--human {
    background: var(--human-solid);
    border-color: var(--human-edge);
    color: var(--on-solid);
  }
  .btn--human:hover {
    background: color-mix(in oklab, var(--human-solid) 86%, var(--on-solid));
  }

  .btn--bot {
    background: var(--bot-solid);
    border-color: var(--bot-edge);
    color: var(--on-solid);
  }
  .btn--bot:hover {
    background: color-mix(in oklab, var(--bot-solid) 86%, var(--on-solid));
  }

  .btn--ghost {
    border-color: var(--line-strong);
    color: var(--text);
  }
  .btn--ghost:hover {
    background: var(--ground-lift);
    border-color: var(--text);
  }

  .btn--sm    { padding: var(--space-2) var(--space-4); min-height: 2.25rem;
                font-size: var(--t-micro); }
  .btn--lg    { padding: var(--space-4) var(--space-7); min-height: 3.25rem;
                font-size: var(--t-body); }
  .btn--block { display: flex; width: 100%; white-space: normal; }

  /* --- links --------------------------------------------------------------- */
  .link {
    color: var(--bot-text);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.22em;
    transition: text-decoration-color var(--dur-fast) var(--ease-out);
  }
  .link:hover { text-decoration-thickness: 2px; }

  /* --- prose --------------------------------------------------------------- */
  .prose {
    max-width: var(--w-prose);
    font-size: var(--t-body);
    line-height: var(--lh-loose);
    color: var(--text-dim);
  }
  .prose > * + * { margin-top: var(--space-4); }
  .prose h2, .prose h3 {
    margin-top: var(--space-7);
    color: var(--text);
  }
  .prose strong { color: var(--text); font-weight: var(--wt-semi); }
  .prose a { color: var(--bot-text); text-underline-offset: 0.22em; }
  .prose ul, .prose ol { padding-left: 1.35em; }
  .prose li + li { margin-top: var(--space-2); }
  .prose li::marker { color: var(--text-faint); }

  /* --- surfaces ------------------------------------------------------------ */
  .panel {
    background: var(--ground-panel);
    border: var(--stroke) solid var(--line);
    border-radius: var(--r-lg);
    padding: var(--space-5);
    /* the panel surface is lighter than the section ground, so the faint
       tier would drop under 3:1 here. Raise it for anything nested. */
    --text-faint: var(--text-dim);
  }
  .panel--lift { box-shadow: var(--shadow); }
  .panel__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-4);
    border-bottom: var(--stroke) solid var(--line);
  }
  .panel__title {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: var(--wt-medium);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .panel__body { display: flex; flex-direction: column; gap: var(--space-4); }
  .panel__foot {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: var(--stroke) solid var(--line);
    font-size: var(--t-small);
    color: var(--text-dim);
  }

  .card {
    background: var(--ground-panel);
    border: var(--stroke) solid var(--line);
    border-radius: var(--r-md);
    padding: var(--space-5);
    --text-faint: var(--text-dim);
  }

  .tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: var(--stroke) solid var(--line);
    border-radius: var(--r-pill);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-wide);
    color: var(--text-dim);
    white-space: nowrap;
  }
  .tag--bot   { background: var(--bot-wash);   border-color: var(--bot-mark);
                color: var(--bot-text); }
  .tag--human { background: var(--human-wash); border-color: var(--human-mark);
                color: var(--human-text); }

  /* --- numbers ------------------------------------------------------------- */
  .num {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums lining-nums;
    font-feature-settings: "tnum" 1;
    line-height: var(--lh-flat);
    letter-spacing: var(--tr-normal);
  }
  .num--lg { font-size: var(--t-head); font-weight: var(--wt-medium); }
  .num--xl { font-size: var(--t-title); font-weight: var(--wt-medium);
             letter-spacing: var(--tr-snug); }
  /* Reserve the width so a live update cannot reflow anything around it. */
  .num--slot {
    display: inline-block;
    min-width: calc(var(--slot, 8) * 1ch);
    text-align: right;
  }
  .unit {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-wide);
    color: var(--text-dim);
  }

  /* --- stat band ----------------------------------------------------------- */
  .stat-band {
    display: grid;
    gap: var(--space-5);
    /* 11rem here meant one stat per row under about 430px: six stats, six
       rows, 584px of phone for six numbers. 9.5rem puts two on a row there
       and changes nothing at 72rem, where six stats already fill six
       columns. */
    grid-template-columns: repeat(auto-fit, minmax(min(9.5rem, 100%), 1fr));
    border-top: var(--stroke) solid var(--line);
    border-bottom: var(--stroke) solid var(--line);
    padding-block: var(--space-5);
  }
  /* The curriculum grid is eleven cards deep and is the tallest block on the
     page that is neither the hero nor a chart. Four columns of it at 72rem
     leave 24px of card padding looking like a fifth column of nothing. */
  [data-curriculum] { --grid-gap: var(--space-4); }
  [data-curriculum] > .card { padding: var(--space-4); }
  /* flex-start matters: without it the value span stretches to the full
     column width, and .num--slot's right-align throws the number to the far
     edge instead of keeping it beside its label. */
  .stat {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
  }
  .stat__value {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--t-head);
    font-weight: var(--wt-medium);
    line-height: var(--lh-flat);
    letter-spacing: var(--tr-snug);
    /* space is reserved even before the first value arrives */
    min-height: 1.2em;
  }
  .stat__label {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .stat__note { font-size: var(--t-small); color: var(--text-faint); }

  /* --- key/value rows ------------------------------------------------------ */
  .kv { display: flex; flex-direction: column; gap: 0; margin: 0; }
  .kv > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding-block: var(--space-3);
    border-bottom: var(--stroke) solid var(--line);
  }
  .kv > div:last-child { border-bottom: 0; }
  .kv__k { font-size: var(--t-small); color: var(--text-dim); }
  .kv__v {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--t-small);
    text-align: right;
    white-space: nowrap;
  }

  /* --- bar ----------------------------------------------------------------- *
   * <div class="bar" style="--pct:26"><span class="bar__fill"></span></div>
   * Give the bar an aria-label or pair it with visible text; it is a graphic. */
  .bar {
    position: relative;
    height: 0.5rem;
    border-radius: var(--r-pill);
    background: var(--ground-lift);
    overflow: hidden;
  }
  .bar__fill {
    display: block;
    height: 100%;
    width: clamp(0%, calc(var(--pct, 0) * 1%), 100%);
    border-radius: inherit;
    background: var(--bar-tone, var(--bot-mark));
    transition: width var(--dur-slow) var(--ease-out);
  }

  /* --- confidence interval -------------------------------------------------- */
  .ci {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    font-variant-numeric: tabular-nums;
    color: var(--text-dim);
    white-space: nowrap;
  }

  /* --- live state ----------------------------------------------------------- *
   * live.js sets data-live on a wrapper. CSS does all the styling.           */
  .live {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .live__dot {
    width: 0.5rem;
    height: 0.5rem;
    flex: none;
    border-radius: var(--r-pill);
    background: var(--bot-mark);
  }

  [data-live="ok"] .live__dot { animation: pulse 2.4s var(--ease-inout) infinite; }
  [data-live="stale"] .live__dot { background: var(--warn-mark); animation: none; }
  [data-live="down"]  .live__dot { background: var(--text-faint); animation: none; }

  [data-live="stale"] { color: var(--warn-text); }
  [data-live="down"]  { color: var(--text-dim); }

  /* Down means "not reporting", spelled out in words. Never a zero, never a
     spinner. The TEXT is written by the template or by live.js, not injected
     by CSS — a server render with the API already down must say it on first
     paint. Swap the value's contents for <span class="not-reporting">not
     reporting</span>. .stat__value reserves its height, so nothing shifts. */
  [data-live="down"] .stat__value,
  [data-live="down"] .num { color: var(--text-dim); }
  .not-reporting {
    font-family: var(--font-mono);
    font-size: var(--t-small);
    font-weight: var(--wt-body);
    letter-spacing: var(--tr-wide);
    text-transform: none;
    color: var(--text-dim);
  }

  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.35; }
  }

  /* --- chart box ------------------------------------------------------------ *
   * Reserves its space before the data lands, so nothing shifts.             */
  .chart {
    position: relative;
    width: 100%;
    aspect-ratio: var(--ratio, 16 / 9);
    min-height: 12rem;
    border-radius: var(--r-md);
    background: var(--ground-lift);
    overflow: hidden;
  }
  .chart > canvas, .chart > svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }

  .figure { display: flex; flex-direction: column; gap: var(--space-3); }
  .figure__cap { font-size: var(--t-small); color: var(--text-dim); }

  /* --- timeline -------------------------------------------------------------- *
   * One column of moments under a wide masthead left 590px of measured white
   * beside it at 1440. Two columns at 62rem and up: 62ch of text is about
   * half the 72rem column, so the second column costs nothing that was being
   * used, and the section stops being the tallest thing on the page.
   *
   * columns, not grid, on purpose. Multicolumn fills column one to the
   * bottom and then starts column two, which is the reading order a timeline
   * needs — first to last down, then across. A two-column grid would lay the
   * same five items out 1-2 / 3-4 / 5, reading across the page, and put the
   * run's history in the wrong order. It also balances the columns itself,
   * so adding a sixth moment to story.json needs no row count updated here.
   *
   * The connector moves from the list to the item so each column gets its
   * own. Items carry padding-bottom rather than a gap so their borders meet
   * and the line stays continuous down a column instead of breaking into
   * ticks.                                                                 */
  .timeline {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    margin: 0;
    max-width: calc(var(--w-prose) + var(--space-6));
    padding-left: var(--space-6);
    border-left: var(--stroke) solid var(--line);
  }
  .timeline__item { position: relative; display: flex; flex-direction: column;
                    gap: var(--space-2); }
  @media (min-width: 62rem) {
    .timeline {
      display: block;
      max-width: none;
      columns: 2;
      column-gap: var(--space-8);
      padding-left: 0;
      border-left: 0;
    }
    .timeline__item {
      break-inside: avoid;
      padding-left: var(--space-6);
      padding-bottom: var(--space-6);
      border-left: var(--stroke) solid var(--line);
    }
    .timeline__item .timeline__node { left: -0.3125rem; }
  }
  .timeline__node {
    position: absolute;
    left: calc(-1 * var(--space-6) - 0.3125rem);
    top: 0.45em;
    width: 0.625rem;
    height: 0.625rem;
    border-radius: var(--r-pill);
    background: var(--bot-mark);
    box-shadow: 0 0 0 4px var(--ground);
  }
  .timeline__when {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--tr-wide);
    color: var(--text-dim);
  }
  .timeline__title {
    font-family: var(--font-display);
    font-size: var(--t-lead);
    font-weight: var(--wt-semi);
    letter-spacing: var(--tr-snug);
  }
  .timeline__body { font-size: var(--t-body); color: var(--text-dim); }

  /* --- comparison ------------------------------------------------------------ *
   * Blue side is the bot, orange side is you. The colour is the encoding, so
   * every row also carries text — colour is never the only signal.           */
  .compare { display: flex; flex-direction: column; gap: 0; }
  .compare__row {
    display: grid;
    gap: var(--space-3) var(--space-5);
    grid-template-columns: 1fr;
    padding-block: var(--space-4);
    border-bottom: var(--stroke) solid var(--line);
  }
  .compare__row:last-child { border-bottom: 0; }
  @media (min-width: 48rem) {
    .compare__row { grid-template-columns: 14rem 1fr 1fr; align-items: baseline; }
  }
  .compare__label {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .compare__side {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-left: var(--space-4);
    border-left: var(--stroke-bold) solid var(--line);
  }
  .compare__side--bot   { border-left-color: var(--bot-mark); }
  .compare__side--human { border-left-color: var(--human-mark); }

  /* --- rank scale ------------------------------------------------------------- *
   * sections/rank-scale.html.
   *
   * ONE COORDINATE SYSTEM. The tiers are equal shares of the track, and every
   * placement on it — the band's two ends, the estimate's point, each marker —
   * is a percentage of the whole track, straight out of story.json. Nothing
   * here counts tiers, so retiering the axis in JSON cannot silently move the
   * bot.
   *
   * The claim this carries is the only one on the site nobody measured, so the
   * drawing hedges exactly where the copy does: the BAND is the filled thing
   * with the name on it, its two ends fade out, and the point estimate inside
   * it is a hairline. Never the other way round — a pin on a scale reads as a
   * measurement.
   *
   * Colour is never the only signal. The range is written out under the track,
   * the track carries an aria-label saying the same, and every tier keeps its
   * name.                                                                    */
  .rank {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    --rank-h: clamp(2.75rem, 4.5vw, 3.75rem);
    /* one label row above the ladder for the estimate's name, and one more
       under it for any other placement's. Both are reserved as margin on the
       track, so nothing above the ladder ever collides with anything else. */
    --rank-lab: calc(var(--t-micro) * 1.25 + var(--space-2));
  }

  /* the one measured number the estimate rests on, when it is bound here. It
     never appears without its interval — the template drops both together. */
  .rank__measured {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-4);
  }
  .rank__measured-label { font-size: var(--t-small); color: var(--text-dim); }

  .rank__scale { display: flex; flex-direction: column; gap: var(--space-3); }

  /* --- the track ---------------------------------------------------------- */
  .rank__track {
    position: relative;
    display: grid;
    grid-template-columns: repeat(var(--tiers, 8), minmax(0, 1fr));
    margin-top: calc(var(--rank-lab) * 2);
  }

  .rank__tier {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
  }
  /* the ladder: one cell per tier, hairline between them. Neutral — a rank is
     not a side, so it is neither blue nor orange. It sits on --ground-panel
     rather than --ground-lift: on ink, lift is four points off the ground and
     the whole ladder read as an empty strip. */
  .rank__tier::before {
    content: "";
    display: block;
    width: 100%;
    height: var(--rank-h);
    background: var(--ground-panel);
    border-left: var(--stroke) solid var(--line);
  }
  .rank__tier:first-child::before {
    border-left: 0;
    border-radius: var(--r-sm) 0 0 var(--r-sm);
  }
  .rank__tier:last-child::before { border-radius: 0 var(--r-sm) var(--r-sm) 0; }

  .rank__name,
  .rank__short {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-wide);
    line-height: var(--lh-flat);
    color: var(--text-dim);
    text-align: center;
    text-wrap: balance;
  }
  .rank__short { display: none; }

  /* --- the band ----------------------------------------------------------- *
   * Blue, because blue is the bot. Exactly as wide as story.json says, and
   * soft at both ends because the ends are the guess. The fade is capped at a
   * quarter of the band so that a one-tier band — the narrowest the copy ever
   * asks for — still has a solid middle instead of dissolving into a smudge. */
  .rank__band {
    position: absolute;
    left: var(--low, 0%);
    width: calc(var(--high, 100%) - var(--low, 0%));
    top: 0;
    height: var(--rank-h);
    z-index: var(--z-raise);
    pointer-events: none;
    background: color-mix(in oklab, var(--bot-mark) 26%, var(--ground-panel));
    border-top: var(--stroke-bold) solid var(--bot-mark);
    border-bottom: var(--stroke-bold) solid var(--bot-mark);
    /* currentColor, not a literal: a mask reads alpha only, so any opaque
       value means "keep this part" and the file's no-colour-literal rule stays
       intact. The fade is a fixed distance capped at a quarter of the band, so
       a one-tier band — the narrowest the copy ever asks for — keeps a solid
       middle instead of dissolving into a smudge. */
    -webkit-mask-image: linear-gradient(90deg, transparent,
      currentColor min(var(--space-6), 25%),
      currentColor calc(100% - min(var(--space-6), 25%)), transparent);
    mask-image: linear-gradient(90deg, transparent,
      currentColor min(var(--space-6), 25%),
      currentColor calc(100% - min(var(--space-6), 25%)), transparent);
  }

  /* the point estimate: a hairline inside the band, not a pin on top of it.
     The template only emits one when story.json actually gave a position. */
  .rank__point {
    position: absolute;
    left: var(--at, 50%);
    top: 0;
    width: var(--stroke-bold);
    height: var(--rank-h);
    margin-left: calc(-0.5 * var(--stroke-bold));
    z-index: var(--z-raise);
    pointer-events: none;
    background: var(--bot-mark);
  }

  /* the estimate's name, standing over the band on the upper label row */
  .rank__flag {
    position: absolute;
    left: var(--at, 50%);
    bottom: calc(100% + var(--rank-lab));
    transform: translateX(-50%);
    z-index: var(--z-raise);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: var(--wt-medium);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    white-space: nowrap;
    pointer-events: none;
  }

  /* another placement on the same axis. A tick, not a fill: it is a reference
     point, and drawing it like the estimate would say it was measured here.
     Its name takes the lower label row, so an estimate and a marker in the
     same tier stack instead of overprinting. */
  .rank__marker {
    position: absolute;
    left: var(--at, 50%);
    top: 0;
    height: var(--rank-h);
    border-left: var(--stroke) dashed var(--line-strong);
    z-index: var(--z-raise);
    pointer-events: none;
  }
  .rank__marker[data-accent="bot"]   { border-left-color: var(--bot-mark); }
  .rank__marker[data-accent="human"] { border-left-color: var(--human-mark); }
  .rank__marker-name {
    position: absolute;
    bottom: calc(100% + var(--space-1));
    left: 50%;
    transform: translateX(-50%);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
    white-space: nowrap;
  }

  .rank__axis { color: var(--text-faint); }

  /* --- head beside the reasoning ------------------------------------------ *
   * sections/rank-scale.html's own two-column shape, kept. The editorial
   * rail in the layout layer is gone (see the note there) but this grid is
   * not the same thing: the rail put a SHORT head beside a LONG body and
   * left the difference blank, while these two columns are both full. The
   * ladder stays full width below them: eight tier names do not fit in a
   * 1.15fr column, and abbreviating them to make room for a heading would be
   * the wrong trade.                                                        */
  .rank__intro {
    display: grid;
    gap: var(--space-6);
    margin-bottom: var(--space-6);
  }
  @media (min-width: 62rem) {
    .rank__intro { grid-template-columns: 0.85fr 1.15fr; align-items: start; }
    .rank__intro > .section__head { margin-bottom: 0; }
  }

  /* the estimate's reasoning: one paragraph, or chain[] as numbered steps */
  .rank__basis {
    max-width: var(--w-prose);
    font-size: var(--t-small);
    line-height: var(--lh-loose);
    color: var(--text-dim);
  }
  .rank__basis > * + * { margin-top: var(--space-5); }

  /* --- the key ------------------------------------------------------------ */
  .rank__keys {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-width: var(--w-prose);
    font-size: var(--t-small);
    color: var(--text-dim);
  }
  .rank__key { display: flex; align-items: baseline; gap: var(--space-3); }
  .rank__key-mark {
    align-self: center;
    flex: none;
    width: var(--space-5);
    height: var(--space-2);
    border-radius: var(--r-xs);
    background: var(--ground-lift);
    border: var(--stroke) solid var(--line-strong);
  }
  .rank__key[data-accent="bot"] .rank__key-mark {
    background: var(--bot-wash);
    border-color: var(--bot-mark);
  }
  .rank__key[data-accent="human"] .rank__key-mark {
    background: var(--human-wash);
    border-color: var(--human-mark);
  }
  .rank__key-name { color: var(--text); }
  .rank__key[data-accent="bot"] .rank__key-name { color: var(--bot-text); }

  /* key and small print share one row under the ladder, on the same grid as
     .rank__intro, so the section does not tail off into an empty right half */
  .rank__foot { display: grid; gap: var(--space-6); margin-top: var(--space-6); }
  @media (min-width: 62rem) {
    .rank__foot { grid-template-columns: 0.85fr 1.15fr; align-items: start; }
  }
  .rank__note { max-width: var(--w-prose); }

  /* the reasoning, when story.json writes it as numbered steps instead of one
     paragraph. Same column, so the two are interchangeable. */
  .rank__chain {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
  }
  .rank__step {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-top: var(--space-3);
    border-top: var(--stroke-bold) solid var(--line);
  }
  .rank__step-n {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    color: var(--bot-text);
  }
  .rank__step-title {
    font-family: var(--font-display);
    font-size: var(--t-body);
    font-weight: var(--wt-semi);
    letter-spacing: var(--tr-snug);
    line-height: var(--lh-snug);
  }
  .rank__step-body { font-size: var(--t-small); color: var(--text-dim); }


  /* Narrow: eight full tier names cannot be read in 40px cells, so the short
     codes take over. Same track, same band, nothing scrolls sideways. */
  @media (max-width: 56rem) {
    .rank__name  { display: none; }
    .rank__short { display: block; }
    /* The flag is centred on its position, so half of it hangs to the right of
       a band that sits high on the scale. At 390px "KiariBot, estimated" in
       caps with --tr-caps ended 4px from the viewport edge. Mixed case at
       --tr-wide is about 30% narrower and clears it.
       ponytail: a label long enough to overflow even this would still be
       clipped; the key under the ladder repeats it in words, so nothing is
       lost. Positioning it per-tier is the fix if that ever happens. */
    .rank__flag { text-transform: none; letter-spacing: var(--tr-wide); }
  }

  /* THE REVEAL. story.js puts data-shown on a section once it crosses the
   * line. The band wipes in from its low end — the direction the ladder is
   * read — and the point, the flag and the markers arrive after it, so the eye
   * gets the range first and the guess second. That order is the argument.
   *
   * THE WHOLE HIDDEN STATE LIVES INSIDE no-preference, which is story.js's own
   * pattern and is load-bearing rather than tidy. Relying on the base layer's
   * `transition-duration: 1ms` instead looked equivalent and was not: under
   * reduce, story.js's hiding rules never apply, so nothing else needs
   * data-shown — and a section the observer has not reached yet kept a band at
   * scaleX(0) with no animation left to bring it back. Measured: at 1440 with
   * reduce emulated, the band was 0px wide and the flag was at opacity 0.
   * Under reduce nothing here is ever hidden, so the scale is simply there.
   *
   * The html:not([data-story]) arm covers story.js not running at all: without
   * it the band would sit at scaleX(0) forever on a browser with no
   * IntersectionObserver. No JS knows about any of this.                     */
  @media (prefers-reduced-motion: no-preference) {
    .rank__band {
      transform: scaleX(0);
      transform-origin: left center;
      transition: transform var(--dur-line) var(--ease-out) 100ms;
    }
    .rank__point,
    .rank__marker {
      opacity: 0;
      transition: opacity var(--dur-slow) var(--ease-out) 700ms;
    }
    .rank__flag {
      opacity: 0;
      transform: translateX(-50%) translateY(var(--space-2));
      transition: opacity var(--dur-slow) var(--ease-out) 700ms,
                  transform var(--dur-slow) var(--ease-out) 700ms;
    }

    .section[data-shown] .rank__band,
    html:not([data-story]) .rank__band { transform: scaleX(1); }
    .section[data-shown] .rank__point,
    .section[data-shown] .rank__marker,
    .section[data-shown] .rank__flag,
    html:not([data-story]) .rank__point,
    html:not([data-story]) .rank__marker,
    html:not([data-story]) .rank__flag { opacity: 1; }
    .section[data-shown] .rank__flag,
    html:not([data-story]) .rank__flag { transform: translateX(-50%); }
  }

  /* --- the arena map ---------------------------------------------------------- *
   * THE ONE PICTURE ON THIS PAGE, and the reason it is allowed to move.
   *
   * The arena section described a live 3D match you can drive using nothing
   * but type. It cannot show the match: one render process feeds every
   * viewer, and autoplaying it for everyone who scrolls past would put the
   * landing page's whole traffic on the machine that is training. So it
   * shows the SYSTEM instead, which is the honest picture available:
   *
   *     [ the trainer, live steps/s ]  o===o==o==o==o==o==o==o==o
   *                                    1   2  3  4  5  6  7  8
   *
   * Everything drawn is either a fact from section 7 of the brief (one
   * render, one stream, eight seats, first in line drives) or a live reading
   * off the trainer. The seat row is the RULE, not an occupancy count -- the
   * caption says so, because a row of eight lit seats would otherwise read
   * as eight people watching and nothing on this machine reports that.
   *
   * The motion is one thing: dashes travelling the stream, once, continuous,
   * because the stream genuinely is. The seats do not blink and the wheel
   * does not hop between them; a wheel that moved every four seconds would
   * be drawing a turn length nobody set. Under prefers-reduced-motion the
   * dashes stop and the picture is unchanged in every other respect.
   *
   * Colour is the site's encoding, not decoration: the wheel seat is orange
   * because it is the human's, the stream and the queue are blue because
   * they are the machine's. The seat numbers carry the same information for
   * anyone who cannot use the colour.                                       */
  .arena-map {
    display: grid;
    gap: var(--space-5) var(--space-7);
    align-items: center;
    margin: 0;
    padding: var(--space-6);
    border: var(--stroke) solid var(--line);
    border-radius: var(--r-lg);
    background: var(--ground-panel);
  }
  @media (min-width: 52rem) {
    .arena-map { grid-template-columns: auto minmax(0, 1fr); }
    .arena-map__cap { grid-column: 1 / -1; }
  }

  /* the source: the machine, saying what it is doing while you drive on it */
  .arena-map__source {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    /* held narrow so the wire beside it gets the width. The label wrapping
       to two lines is the point: it is the sentence, not a caption. */
    max-width: 14rem;
    padding-right: var(--space-6);
    border-right: var(--stroke) solid var(--line);
  }
  @media (max-width: 51.9375rem) {
    .arena-map__source {
      max-width: none;      /* so the rule under it spans the panel */
      padding-right: 0;
      padding-bottom: var(--space-5);
      border-right: 0;
      border-bottom: var(--stroke) solid var(--line);
    }
  }
  .arena-map__label {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .arena-map__rate {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums lining-nums;
    font-size: var(--t-head);
    font-weight: var(--wt-medium);
    line-height: var(--lh-flat);
    letter-spacing: var(--tr-snug);
    color: var(--bot-text);
    min-height: 1.2em;   /* reserved before the first reading lands */
  }

  /* the stream and the seats on it. The line is a background of the row, so
     the seats sit ON it rather than beside it: one stream, eight seats. */
  /* One wire, eight seats, and it must stay ONE ROW at every width: the wire
     is a single absolutely-positioned line, so a row that wrapped left four
     seats sitting on nothing. The seat scales with the viewport instead and
     the row's own overflow is the backstop -- at 360 the eight seats and
     their gaps come to 240px inside a 256px box, and if a future label ever
     pushed past that the clip keeps it off the page's scrollbar. */
  .arena-map__line {
    --seat: clamp(1.5rem, 6vw, 3.25rem);
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-2);
    margin: 0;
    padding: 0.5rem 0.5rem 0;
    overflow: hidden;      /* keeps the travelling light inside the wire */
  }
  /* the wire: one stream, drawn once, running under every seat */
  .arena-map__line::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: calc(0.5rem + var(--seat) / 2);
    height: var(--stroke-bold);
    background: var(--bot-quiet);
    z-index: var(--z-under);
  }
  /* what travels down it. One light, not a dash pattern: dashes read as a
     dotted border and collided with the seat rings at every width where the
     spacing did not happen to land between two seats. */
  .arena-map__line::before {
    content: "";
    position: absolute;
    left: 0;
    top: calc(0.5rem + var(--seat) / 2 - 0.0625rem);
    width: 22%;
    height: calc(var(--stroke-bold) + 0.125rem);
    border-radius: var(--r-pill);
    background: linear-gradient(90deg,
      transparent, var(--bot-mark) 55%, var(--bot-mark) 70%, transparent);
    z-index: var(--z-under);
    /* static under reduced motion: parked at the machine end, still reading
       as a stream leaving it, just not moving. */
    transform: translateX(-30%);
  }
  @media (prefers-reduced-motion: no-preference) {
    .arena-map__line::before { animation: arena-stream 3.6s linear infinite; }
  }

  .arena-map__seat {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
  }
  .arena-map__pip {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--seat);
    height: var(--seat);
    border: var(--stroke-bold) solid var(--bot-quiet);
    border-radius: var(--r-pill);
    background: var(--ground-panel);
    /* the halo is what makes the seat sit ON the wire instead of over it */
    box-shadow: 0 0 0 0.5rem var(--ground-panel);
    font-family: var(--font-mono);
    font-size: var(--t-small);
    font-variant-numeric: tabular-nums;
    color: var(--text-dim);
  }
  /* the one seat that is yours. The halo is the same trick as every other
     seat's, with an orange ring drawn inside it: emphasis by the site's own
     encoding, and the word under it says the same thing without colour. */
  .arena-map__seat--driver .arena-map__pip {
    border-color: var(--human-edge);
    background: var(--human-solid);
    color: var(--on-solid);
    box-shadow: 0 0 0 0.3125rem var(--human-wash),
                0 0 0 0.5rem var(--ground-panel);
  }
  .arena-map__wheel { width: 1.625rem; height: 1.625rem; }
  .arena-map__role {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
    color: var(--text-faint);
  }
  .arena-map__seat--driver .arena-map__role { color: var(--human-text); }
  /* Under 52rem the seats are 24px and two words under adjacent ones would
     touch. The driver's keeps its label because that is the one the picture
     is about; the queue's order is in the caption and in the numbers. */
  @media (max-width: 51.9375rem) {
    .arena-map { padding: var(--space-5); }
    .arena-map__seat:not(.arena-map__seat--driver) .arena-map__role { display: none; }
  }

  .arena-map__cap { color: var(--text-dim); }

  /* The one animation on the page below the hero: the light leaves the
     machine, crosses all eight seats, and starts again. Both ends are fully
     off the row, so the loop has no seam and nothing flashes at the join. */
  @keyframes arena-stream {
    from { transform: translateX(-30%); }
    to   { transform: translateX(455%); }
  }

  /* --- cta -------------------------------------------------------------------- *
   * .cta is itself a .section, so it only ever assigns --pad-block. */
  .cta {
    /* 11vw was 144px top and bottom at 1440, on 356px of content: the
       closing section was two thirds air. 5vw holds the "this is the end of
       the page" beat at 65px without it. */
    --pad-block: clamp(2.75rem, 4.5vw, 4.5rem);
    text-align: center;
  }
  .cta__title {
    font-family: var(--font-display);
    font-size: var(--t-title);
    font-weight: var(--wt-heavy);
    line-height: var(--lh-snug);
    letter-spacing: var(--tr-snug);
    text-wrap: balance;
  }
  .cta__lead {
    margin: var(--space-4) auto 0;
    max-width: 46rem;
    font-size: var(--t-lead);
    line-height: var(--lh-normal);
    color: var(--text-dim);
  }
  .cta__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-4);
    margin-top: var(--space-6);
  }
}

/* ==========================================================================
   UTILITIES — last layer, so these always win
   ========================================================================== */
@layer utilities {

  .display {
    font-family: var(--font-display);
    font-size: var(--t-display);
    font-weight: var(--wt-heavy);
    line-height: var(--lh-tight);
    letter-spacing: var(--tr-tight);
    text-wrap: balance;
  }
  .title {
    font-family: var(--font-display);
    font-size: var(--t-title);
    font-weight: var(--wt-display);
    line-height: var(--lh-snug);
    letter-spacing: var(--tr-snug);
  }
  .head {
    font-family: var(--font-display);
    font-size: var(--t-head);
    font-weight: var(--wt-semi);
    line-height: var(--lh-snug);
    letter-spacing: var(--tr-snug);
  }
  .lead {
    font-size: var(--t-lead);
    line-height: var(--lh-normal);
    color: var(--text-dim);
  }
  .small { font-size: var(--t-small); }
  .micro {
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--tr-caps);
    text-transform: uppercase;
  }

  .tone-bot   { color: var(--bot-text); }
  .tone-human { color: var(--human-text); }
  .tone-warn  { color: var(--warn-text); }
  .tone-dim   { color: var(--text-dim); }
  .tone-faint { color: var(--text-faint); }   /* large / decorative only */

  .visually-hidden:not(:focus):not(:active) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .u-center { text-align: center; }
  .u-nowrap { white-space: nowrap; }
  .u-full   { width: 100%; }

  @media (max-width: 48rem) { .u-hide-sm { display: none !important; } }
  @media (min-width: 48.0625rem) { .u-hide-lg { display: none !important; } }
}
