/* ==========================================================================
   AI — site stylesheet

   The site chrome: one document-level reset, the type scale, the layout
   shell, the navigation surfaces, and the handful of composites (card,
   button, tag, stat) that the rest of the repo reaches for by class name.

   THE CONTRACT, IN ONE LINE: this file declares no values of its own.
   Every colour, space, radius, shadow, duration, type size and font below is
   a `var()` into assets/css/theme.css, which is loaded on every page that
   includes it — after this file, which is fine: a custom property is
   substituted at computed-value time, not at parse time, so a token declared
   in a later stylesheet resolves exactly as one declared in an earlier one.
   What does NOT survive that ordering is a redefinition, and the previous
   version of this file had a `:root` block full of them: a warm "Broadsheet"
   palette of literal hexes and literal rems, sitting a few lines below the
   theme's own and quietly overriding it wherever this file was linked second.
   Two copies of a palette is how they drift apart, so there is no `:root`
   block here at all now. The theme layer also owns the webfont — Inter is
   self-hosted and declared there — so this file loads no fonts of its own.

   The chrome below is written in full rather than left to the chrome layer
   because this file has always been loadable on its own, and on the guides
   site it still is: the standalone pages there link this file and nothing
   else, so for them the rules in this file ARE the navigation. That contract
   is inherited here rather than exercised. Every real page in this repo
   renders through _layouts/default.html, which is the only file with a
   <head>, and it loads theme.css, this file and chrome.css in that order;
   the 341 generated redirect stubs link no stylesheet at all. The rules stay
   because the contract is the contract, and because chrome.css refines the
   same values where both files are present rather than replacing them.

   THE CLASS CONTRACT IS NOT THE STYLE CONTRACT. Every selector, id and
   element rule below survives from the previous version, because the layouts
   and the chrome includes address this stylesheet by name and cannot be
   updated from here. The `--color-*` / `--space-*` / `--radius-*` /
   `--font-*` names this file has always published are still consumed outside
   it: five references to them live in _layouts/page.html and
   _layouts/robotics-article.html. On the guides site the same names are also
   leaned on by the per-series wrapper classes (.g, .llmt, .mvg, .nlo, …),
   which live in guide.css and in each page's own inline <style> block — none
   of which exist here. Only the values underneath moved.

   FOUR LITERALS SURVIVE, and each for a stated reason rather than by
   oversight. A rhythm that couples layout to a reader's density or text-size
   preference is a bug, not a feature, so these four are the measurements
   that are NOT on a scale:

     · 1px hairlines. A border that is a space step computes to a fraction
       of a pixel at a tight density and the browser rounds it to 0 or 1
       depending on where it lands, so adjacent rules join and separate at
       random. The theme makes the same call for its own dividers.
     · 32px/40px control sizes. A button's box is a REACH (a target a finger
       has to find), not a rhythm, so it must not shrink when the reader
       chooses a denser page. The space around it is a rhythm and does scale.
     · Reading measures: `max-width` in px/ch and grid track minimums in px.
       A track minimum is a measure of "how narrow can a column get before it
       stops being a column"; expressing it as a rem would make the NUMBER
       of columns depend on the reader's text size. `ch` is only correct on
       the element that declares the prose size — `ch` resolves against the
       element's own computed font size, so a container measured in `ch`
       while its contents are set larger is a different width than intended.
       That is why `.wrap--article` stays in px and the ledes, which DO
       declare `--text-prose` on themselves, use `--pref-measure`.
     · List indents. A hanging indent is part of the text, not a gap in the
       layout, and it has to keep up with the size of the type it hangs.

   Everything else — every gap, inset, margin, padding, border-radius,
   transition and font-size — is a token.
   ========================================================================== */

/* --------------------------------------------------------------------------
   PROVENANCE — a synced copy, not an original.

   A copy of assets/css/styles.css in the interactive-courses repo; the two
   were byte-identical before this pass. CLAUDE.md records that the shared
   chrome is duplicated by hand rather than pulled from a submodule, so
   nothing will tell you they have drifted.

   Consequence for editing: a change belongs in BOTH repos. Comments that
   described the other repo's scale — its page count, its standalone pages,
   its guide stylesheets — were wrong here and have been corrected to
   describe this repo instead. The reasoning below is the same on both
   sides and is the part worth reading before changing anything.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Document & reset
   --------------------------------------------------------------------------
   The single `box-sizing` reset, kept. It is the reason a range input or a
   scrollbox can rely on a predictable box, and the reason a restyled control
   that wants a content box has to say `content-box` out loud. */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* A `#fragment` link has to clear the sticky site bar — and, on the guides
     site, the series ribbon under it — and theme.css already owns that number,
     in rem, because the things it clears are in rem. Reusing
     `--read-anchor-offset` rather than repeating a px literal is what keeps an
     in-page table of contents from landing under the chrome at 130% text. */
  scroll-padding-top: var(--read-anchor-offset);
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  /* Body leading is the reader's leading, expressed as a ratio against the
     1.6 this site shipped with: 1 is the page as it looked before
     preferences existed, so Tight and Relaxed reach the running text and not
     just the chrome. */
  line-height: calc(var(--text-base-lh) * var(--read-lead, 1));
  font-weight: var(--font-body-weight, 400);
  text-wrap: pretty;
}

main { display: block; }

/* --------------------------------------------------------------------------
   Type
   --------------------------------------------------------------------------
   One family, deliberately: Inter carries prose, chrome and numbers without a
   size or weight jump at the seams. The old theme's second display face is
   what made it read as a reading nook, and a `h6` set in one face above a
   paragraph in another is a seam you can see at 1.0. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  color: var(--color-text);
  margin: 0 0 var(--space-2);
  text-wrap: balance;
}
h1 { font-size: var(--text-display-sm); line-height: var(--text-display-sm-lh); letter-spacing: var(--text-display-sm-ls); }
h2 { font-size: var(--text-xl); line-height: var(--text-xl-lh); letter-spacing: var(--text-xl-ls); margin-top: var(--space-10); }
h3 { font-size: var(--text-lg); line-height: var(--text-lg-lh); margin-top: var(--space-8); }
h4 { font-size: var(--text-base); line-height: var(--text-base-lh); }
h5 { font-size: var(--text-sm); line-height: var(--text-sm-lh); }
/* The one heading that is a LABEL rather than a heading: an eyebrow over a
   section, so it is micro type, uppercased, in the quiet ink. */
h6 {
  font-size: var(--text-micro); line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls); text-transform: uppercase;
  color: var(--color-text-subtle);
}

p { margin: 0 0 var(--space-3); }

a {
  color: var(--color-accent);
  /* In `em` so the gap between the text and its underline grows with the
     text: a fixed 3px is a hair's breadth at 16px and a hole at 24px. */
  text-underline-offset: 0.18em;
}
a:hover { color: var(--c-accent-ink); }

img { display: block; max-width: 100%; }

figure { margin: 0 0 var(--space-3); }
figcaption {
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  margin-top: var(--space-1);
  color: var(--color-text-subtle);
}

.text-muted { color: var(--color-text-muted); }

/* The focus ring is a 2px accent outline with a 2px offset: the one place on
   the site where the accent is drawn as a shape rather than as ink, because a
   ring has to be visible on a plate, on the canvas and inside a tinted row.
   `:focus` is kept ahead of it for engines without `:focus-visible`. */
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* 22% is the site's one accent wash that has to be translucent: a selection
   has to let the text's own background through, or reading the selection is
   reading a rectangle. */
::selection { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }

/* --------------------------------------------------------------------------
   Lists, quotes, rules
   --------------------------------------------------------------------------
   The 22px hanging indent is one of the four measurements that stay in px:
   it hangs off the type, so it has to scale with the type and not with the
   layout. */
ul, ol { padding-left: 22px; margin: 0 0 var(--space-3); }
li { margin-bottom: var(--space-1); line-height: var(--text-base-lh); }
li > ul, li > ol { margin-top: var(--space-1); }
strong { font-weight: 600; }
em, i { font-style: italic; }

/* A quote is a plate of muted ink, divided by a hairline. The previous theme
   divided it with a 2px ACCENT rule, which spent the accent on decoration;
   the accent is reserved for interaction. Italic stays on the container
   because with one family in the system the italic is the only thing
   distinguishing quoted matter from quoted matter. */
blockquote {
  margin: 0 0 var(--space-5);
  padding: var(--space-1) 0 var(--space-1) var(--space-5);
  border-left: 1px solid var(--c-border-strong);
  font-style: italic;
  color: var(--color-text-muted);
}
blockquote p:last-child { margin-bottom: 0; }

hr {
  height: 1px; border: 0;
  margin: var(--space-8) 0;
  background: var(--color-divider);
}

/* --------------------------------------------------------------------------
   Code, tables
   --------------------------------------------------------------------------
   Code and pre are RECESSED, not raised: `--color-code-bg` is the code well,
   a fill a step off the surface, so they take no shadow. Giving a recess a
   cast shadow asserts the opposite of what the fill already says. */
code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--color-code-bg);
  padding: 0.1em 0.35em;
  border-radius: var(--radius-1);
}
pre {
  font-family: var(--font-mono);
  background: var(--color-code-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  padding: var(--space-4);
  overflow: auto;
  font-size: var(--text-sm);
  line-height: var(--text-sm-lh);
  margin: 0 0 var(--space-5);
}
pre code { background: none; padding: 0; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
  margin: 0 0 var(--space-5);
}
/* Two rules, not one: the header rule is the stronger of the two so the
   column headers are findable without bolding every cell. */
th {
  text-align: left;
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  padding: var(--space-2);
  border-bottom: 1px solid var(--c-border-strong);
}
td {
  padding: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}
/* A row highlight is a surface, not a 4% ink wash: a fill is a value the
   theme can re-point, and a wash has to be re-measured for contrast in both
   modes. Hover is gated because on touch it sticks after the tap. */
@media (hover: hover) and (pointer: fine) {
  tbody tr:hover { background: var(--color-surface-2); }
}

/* --------------------------------------------------------------------------
   Layout shell
   --------------------------------------------------------------------------
   The gutter is a clamp between two steps of the scale, so a dense page gets
   a narrower gutter and a relaxed one a wider gutter without either end
   being a hand-picked pixel. `max-width` stays px: a content width is a
   measure, and 900px / 700px are the two reading columns the site's page
   types were set for. */
.wrap {
  max-width: var(--content-width);
  margin: 0 auto;
  padding: 0 clamp(var(--space-5), 5vw, var(--space-16));
}
.wrap--narrow { max-width: 900px; }
.wrap--article { max-width: 700px; }

.section { padding: var(--space-8) 0; }
.section:first-of-type { padding-top: clamp(var(--space-8), 6vw, var(--space-12)); }

/* The eyebrow / kicker / act-label pattern, which appears six times on the
   site under six names. Micro type, uppercased, letter-spaced, in the quiet
   ink — so all six read as one voice. */
.section-kicker {
  display: block;
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.section-kicker--accent { color: var(--c-accent-ink); }

.section-title {
  font-size: clamp(var(--text-xl), 3.5vw, var(--text-display-sm));
  line-height: var(--text-xl-lh);
  letter-spacing: var(--text-xl-ls);
  margin: 0 0 var(--space-2);
}
/* `--pref-measure` rather than a fixed `ch`, and safe here because THIS
   element declares the prose size it is measured against. */
.section-lede {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-3);
}

.era-heading {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--c-accent-ink);
  margin: var(--space-8) 0 var(--space-1);
}
.era-heading:first-of-type { margin-top: var(--space-5); }

/* --------------------------------------------------------------------------
   Site navigation — a sticky top bar
   --------------------------------------------------------------------------
   It was a fixed left rail sampling the page through a 20px blur. It is now a
   real surface: opaque, one hairline along its bottom edge, one step of
   elevation, and stuck to the top of the viewport for the life of the page.
   A bar that blurs the content behind it is a bar you cannot read a link
   through, and a navigation is the one surface a reader has to be able to
   point at.

   `--nav-w` is the WIDTH of the brand band, not of the bar: a full-width bar
   with a fixed-width first column and a flexible link row, so the links all
   start at the same x on every page and the brand never reflows as the link
   count changes between a section page and a part page. */
.nav a { color: inherit; text-decoration: none; }

.nav-brand {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-lg);
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;
}
/* A mark, not a glow. The previous theme ringed the dot with a 4px accent
   halo; a halo is decoration that the instrument vocabulary does not have,
   and a 8px dot in the accent already reads as the mark at any size. */
.nav-brand-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--color-accent);
}

#site-nav {
  position: sticky; top: 0; z-index: 300;
  display: flex; align-items: center; flex-wrap: nowrap;
  gap: var(--space-1);
  /* Full-bleed, with the CONTENT centred against `--content-width`. Doing it
     with `max-width` on the bar would stop the hairline short of the
     viewport edge and leave two stubs of background either side of it. */
  padding: var(--space-2) max(var(--space-4), (100% - var(--content-width)) / 2);
  /* A deterministic height, because the ribbon below parks against it. The
     expression is the bar's own parts rather than a second number: the 32px
     control, the block padding either side of it, and the hairline. Where the
     chrome layer is also loaded it publishes the same measurement as
     `--chrome-nav-h` and sets the height outright — see the ribbon, which
     reads that name first and this expression second. */
  min-height: calc(32px + var(--space-2) * 2 + 1px);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  /* One step of elevation, and no lit top edge: the box runs the full width
     of the viewport and has no top edge to light, and a hairline already says
     where the bar ends. */
  box-shadow: var(--shadow-1);
  transition: transform var(--dur-base) ease;
}

.nav-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  flex: 0 0 var(--nav-w);
  margin-right: var(--space-4);
}

/* The bar's two halves. The link group takes the free space and clips, so a
   narrow window trims the far end of the link row rather than pushing the
   reading controls off the bar; the control group is fixed-width and pinned
   to the far end. */
.nav-links {
  display: flex; align-items: center;
  gap: var(--space-1);
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
}
.nav-controls {
  display: flex; align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  margin-left: auto;
}

/* A 32px control, not a step. A toggle a finger has to find is a reach, and a
   reach that shrank with the density preference would be a smaller reach. */
.nav-collapse-toggle {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-hair);
  width: 32px; height: 32px; padding: 0; margin: 0;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  color: var(--color-text);
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

#site-nav a:not(.nav-brand) {
  font-size: var(--text-sm);
  padding: var(--space-tight) var(--space-2);
  border-radius: var(--radius-control);
  color: var(--color-text-muted);
  /* `color` joins the list because the current-route rule changes the label
     as well as the fill; without it the text swapped instantly while the fill
     eased, which reads as two different presses. */
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* Hover is an ENHANCEMENT and it is gated. A nav row is already a full link
   and the current route is already marked, so on touch — where `:hover`
   sticks after the tap — a bar of lit links reads as a bar that is broken.
   `:active` is deliberately outside the gate, because a press is a state a
   finger actually gets. */
@media (hover: hover) and (pointer: fine) {
  .nav-collapse-toggle:hover {
    background: var(--color-surface-2);
    border-color: var(--c-border-strong);
  }
  #site-nav a:not(.nav-brand):hover {
    background: var(--color-surface-2);
    color: var(--color-text);
  }
  .nav-collapse-toggle:active { background: var(--color-border); }
  #site-nav a:not(.nav-brand):active {
    background: color-mix(in srgb, var(--color-text) 9%, var(--color-surface-2));
    color: var(--color-text);
  }
}

/* THE ACTIVE ROUTE IS A FILLED ROW. Not a coloured left rule, not a numeral,
   not a wash on the far side of the bar: a row of the nav's own shape, one
   step of accent into the surface, with the accent's INK on it. Ten per cent
   is the site's selected-segment value, and the `-ink` variant is required
   rather than incidental — mixing the accent toward the surface to lighten
   the text on a tint drops it below AA. */
#site-nav a[aria-current='page'] {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  color: var(--c-accent-ink);
  font-weight: 600;
}
/* Eighteen while it is also being pressed, the site's active-while-active
   step. */
#site-nav a[aria-current='page']:active {
  background: color-mix(in srgb, var(--color-accent) 18%, transparent);
}

/* COLLAPSED. The bar keeps its box — same height, same surface, same
   hairline — and gives up its links. The previous design collapsed the rail
   down to a floating plate over the canvas, which for a bar would mean the
   page underneath reflows by the height of the chrome every time the toggle
   is pressed; and it hid the brand, so a reader who collapsed the nav could
   no longer get back to the root. */
@media (min-width: 901px) {
  html.nav-collapsed .nav-links { display: none; }
  html.nav-collapsed .nav-head { flex: 0 0 auto; margin-right: 0; }
  html.nav-collapsed #site-nav .nav--series { display: none; }
}

/* The hamburger bars, shared by the mobile toggle and the desktop collapse
   toggle so both read as the same control. The 2px is the drawn mark, not a
   space. */
.nav-mobile-toggle-bar {
  display: block; width: 18px; height: 2px; margin: 0 auto;
  background: var(--color-text);
  border-radius: var(--radius-1);
}

/* --------------------------------------------------------------------------
   Mobile navigation — an off-canvas drawer
   --------------------------------------------------------------------------
   Below 900px the bar becomes a drawer and a fixed 40px button opens it. The
   button is a 40px REACH; the clearance the drawer needs is 40px plus two
   steps, which is why it is written as the sum rather than as one number.

   The old file reserved `padding-top` on <body> for this button, because
   every guide page repeats its own `.page-header { padding-top: … }` idiom
   inline under its own wrapper class with more specificity than anything here
   could add, so the clearance could only come from the body. That reservation
   is gone: the bar is a sticky surface in normal flow on wide screens, and
   the button that needs the clearance lives inside the drawer flow on narrow
   ones. */
.nav-mobile-toggle { display: none; }
.nav-scrim { display: none; }

@media (max-width: 900px) {
  #site-nav {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(78vw, 280px);
    height: 100vh;
    height: 100dvh;
    flex-direction: column; align-items: stretch; flex-wrap: nowrap;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-3);
    overflow-y: auto;
    transform: translateX(-100%);
    border-right: 1px solid var(--color-border);
    border-bottom: none;
    box-shadow: var(--shadow-3);
  }
  html.nav-open #site-nav { transform: translateX(0); }
  .nav-head { flex: 0 0 auto; margin-right: 0; }
  .nav-collapse-toggle { display: none; }
  .nav-mobile-toggle {
    display: flex; flex-direction: column; justify-content: center;
    gap: var(--space-hair);
    position: fixed; top: var(--space-3); left: var(--space-3); z-index: 350;
    width: 40px; height: 40px; padding: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: var(--shadow-2), var(--elev-inset);
    color: var(--color-text);
    cursor: pointer;
  }
  html.nav-open .nav-scrim {
    display: block; position: fixed; inset: 0; z-index: 320;
    background: var(--c-scrim);
  }
}

@media (prefers-reduced-motion: reduce) {
  #site-nav { transition: none; }
}

/* --------------------------------------------------------------------------
   Series part nav — the pill row under the bar
   --------------------------------------------------------------------------
   Sticky, opaque, one hairline. The previous version was a row of glass pills
   on a transparent strip, which meant the page scrolled visibly through the
   gap above it; an opaque strip pinned at 0 with its own bottom rule is the
   only arrangement where nothing shows through. */
.nav--series {
  position: sticky;
  /* Parked DIRECTLY UNDER the bar, not at the top of the viewport: the bar is
     sticky too, so a ribbon that parks at 0 slides underneath it. The bar's
     height belongs to the chrome layer, which publishes it as
     `--chrome-nav-h`; the fallback is this file's own bar, expressed from its
     parts, and it is exact on the pages that load this stylesheet alone. */
  top: var(--chrome-nav-h, calc(32px + var(--space-2) * 2 + 1px));
  z-index: 100;
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2) max(var(--space-4), (100% - var(--content-width)) / 2);
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--text-label-sm);
}
.nav--series a {
  font-size: var(--text-label-sm);
  padding: var(--space-hair) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .nav--series a:hover {
    background: var(--color-surface-2);
    color: var(--color-text);
  }
}

.nav--series-title { display: none; }

/* The same filled row as the bar's own links: one step of accent into the
   surface, the accent's ink on it, and a weight bump so the part you are on
   is findable in a list of twenty chips. */
.nav--series a[aria-current='page'] {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  color: var(--c-accent-ink);
  font-weight: 600;
}

/* Optional act grouping (only when the series YAML declares `acts:`). The
   group is presentational: `display: contents` lets its children sit
   directly in the pill row, so an ungrouped series renders exactly as
   before. */
.nav--series-group { display: contents; }
.nav--series-act {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  white-space: nowrap;
  align-self: center;
  margin-left: var(--space-2);
  color: var(--color-text-subtle);
}

/* On narrow screens the pill row can be 20+ items; wrapping them turns the
   sticky ribbon into a multi-row block that pins to the top of the viewport
   and eats most of the screen. Scroll it horizontally in one row instead,
   and let it scroll away with the page rather than staying sticky. */
@media (max-width: 640px) {
  .nav--series {
    position: static;
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    padding-inline: var(--space-4);
  }
  .nav--series a,
  .nav--series-act { scroll-snap-align: start; }
}

/* Clear the fixed mobile hamburger (top-left) so the first pill, or a page's
   own heading, never sits underneath it: 40px of reach plus two steps. */
@media (max-width: 900px) {
  .nav--series { padding-left: calc(40px + var(--space-4)); }
}

/* The part list as a child of the bar — a second row of chips under the brand,
   rather than the vertical block it was in the rail. INERT on the guides site,
   where series-nav.html no longer relocates the list into #site-nav, and
   doubly inert here, where no include emits `.nav--series` at all — so none of
   these selectors match, and the plain rules above are the whole story. They
   are here because the class contract is the contract, and because a reader
   whose JavaScript never ran still has to get a usable part list — if the
   relocation ever comes back, this is what it has to look like, and the
   current-part fill below has to out-rank the plain rule to survive. */
#site-nav .nav--series {
  position: static; z-index: auto;
  flex: 1 1 100%;
  flex-direction: row; align-items: center; flex-wrap: nowrap;
  gap: var(--space-2);
  margin: 0; padding: var(--space-2) 0 0;
  overflow-x: auto;
  background: none;
  border-bottom: none;
  border-top: 1px solid var(--color-border);
}
#site-nav .nav--series a {
  display: inline-flex; flex-direction: row; gap: var(--space-1);
  padding: var(--space-hair) var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  white-space: nowrap;
}
#site-nav .nav--series-short { font-weight: 600; }
#site-nav .nav--series-title { display: none; }
@media (hover: hover) and (pointer: fine) {
  #site-nav .nav--series a:hover {
    background: var(--color-surface-2);
    color: var(--color-text);
  }
}
#site-nav .nav--series a[aria-current='page'] {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  color: var(--c-accent-ink);
  font-weight: 600;
}
#site-nav .nav--series-act {
  display: block; align-self: center; margin-left: var(--space-2); padding: 0;
  white-space: nowrap;
}
@media (max-width: 900px) {
  html.nav-collapsed #site-nav .nav--series { display: flex; }
}

/* --------------------------------------------------------------------------
   Next-part card
   --------------------------------------------------------------------------
   A flat plate on the elevation scale: a surface fill, a hairline, a corner
   off the radius scale, and the lit top edge composed with the shadow rather
   than written into it. */
.series-prevnext {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-3);
  margin: var(--space-5) 0 var(--space-3);
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-1), var(--elev-inset);
}
.series-prevnext-teaser { margin: 0; }

/* --------------------------------------------------------------------------
   Hero
   --------------------------------------------------------------------------
   The display step, a deck paragraph at the prose size, and the actions row.
   The deck is measured in `--pref-measure` on the same element that declares
   `--text-prose`, which is the only way `ch` measures what you meant. */
.hero { padding: clamp(var(--space-12), 9vw, calc(var(--space-12) * 2)) 0 var(--space-12); }
.hero h1 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: clamp(var(--text-display-md), 6vw, var(--text-display-lg));
  line-height: var(--text-display-lg-lh);
  letter-spacing: var(--text-display-lg-ls);
  margin: 0;
  max-width: 20ch;
}
.hero p {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
  color: var(--color-text-muted);
  margin: var(--space-6) 0 0;
}
.hero-actions {
  display: flex; gap: var(--space-3); flex-wrap: wrap;
  margin-top: var(--space-7);
}

/* --------------------------------------------------------------------------
   Stats
   --------------------------------------------------------------------------
   A strip of numbers, so the numbers are tabular and the labels are micro.
   The strip itself is a pair of hairlines: the old 5px band with a 2px rule
   was a rule wearing a box's clothes, and `.layout-home` hides it outright. */
.stats {
  height: 0;
  border-top: 1px solid var(--c-border-strong);
  border-bottom: 1px solid var(--color-border);
}
.stats-row {
  display: flex; justify-content: space-between; flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  margin: 0; padding: var(--space-2) 0;
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--color-text-muted);
  font-feature-settings: 'pnum' 1;
}
.stats-rule { height: 1px; border-top: 1px solid var(--color-border); }
.stats-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-2) var(--space-10);
  padding: var(--space-5) 0 var(--space-2);
}
.stat {
  display: flex; align-items: baseline; gap: var(--space-2);
  margin: 0;
  font-size: var(--text-base);
  /* A reader comparing two figures is comparing digit strings, and
     proportional digits make that comparison a guess. */
  font-variant-numeric: tabular-nums;
}
.stat-fill {
  flex: 1; min-width: 20px; align-self: flex-end;
  margin-bottom: 0.34em;
  border-bottom: 1px dotted var(--c-border-strong);
}
.stat-value {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}
.stat-value--accent { color: var(--c-accent-ink); }

/* --------------------------------------------------------------------------
   Timeline & release rows
   --------------------------------------------------------------------------
   A year and a sentence on one baseline, divided by hairlines. The year
   column is a fixed measure, so it stays px: it is a gutter for the dates to
   line up in, not a gap in the layout. */
.timeline-row {
  display: flex; gap: var(--space-6); align-items: flex-start;
  padding: var(--space-3) 0;
}
.timeline-year {
  flex: 0 0 96px;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-xl);
  font-variant-numeric: tabular-nums;
  color: var(--c-accent-ink);
}
.timeline-row p { margin: 0; font-size: var(--text-base); line-height: var(--read-line); }

.release-row {
  display: flex; align-items: baseline; gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.release-row:last-child { border-bottom: none; }
.release-date {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  flex: 0 0 84px;
}
.release-row p { margin: 0; flex: 1; font-size: var(--text-sm); }

/* --------------------------------------------------------------------------
   Tags
   --------------------------------------------------------------------------
   A tag is a BADGE: it labels something, it is not a button. It has no href,
   no tabindex and no pressed state, so it deliberately has no `:hover` and no
   `:active` — a hover on a non-control is a promise the DOM does not keep,
   and "this element has no hover state" should be a decision on the record
   rather than an apparent omission for the next pass to fix. */
.tag {
  display: inline-flex; align-items: center;
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  font-weight: 600;
  padding: var(--space-hair) var(--space-2);
  border-radius: var(--radius-pill);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
/* A tag on the canvas has no plate of its own, so it needs a fill to be
   legible against; the quiet ramp step is that fill, and the 800 step is the
   ink that clears AA on it. */
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--c-accent-ink); }
.tag-legend {
  display: flex; gap: var(--space-2);
  margin-bottom: var(--space-6);
  flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   Buttons
   --------------------------------------------------------------------------
   One press response, one duration, one corner. `color` is in the transition
   list because the secondary and ghost states change the label as well as the
   fill, and without it the text swapped instantly while the fill eased.

   The inset is a space step, so the density preference reaches the button's
   padding. The previous theme held the button box at a fixed 30px for the
   opposite reason — a control a reader has to find should not shrink — and
   that is still true of the box, so the target is protected by a `min-height`
   rather than by a padding that the reader can move. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-1);
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-sm);
  line-height: var(--text-sm-lh);
  color: var(--color-text);
  background: transparent;
  border: 1px solid transparent;
  padding: var(--space-2) var(--space-4);
  min-height: calc(var(--space-2) * 2 + 1.2em);
  border-radius: var(--radius-control);
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease,
    color var(--dur-fast) ease, opacity var(--dur-fast) ease;
  -webkit-tap-highlight-color: transparent;
}
.btn:hover { text-decoration: none; }
.btn:disabled { cursor: not-allowed; opacity: 0.5; }

/* PRIMARY IS INVERTED, NOT ACCENT-FILLED. A page with twenty of these has to
   be able to recede until a reader goes looking for one, and the accent is
   the site's interaction colour — a button fill would be a background wash of
   the one hue that is supposed to mean "you can act on this". */
.btn-primary { background: var(--color-text); color: var(--color-bg); }
.btn-primary:hover { color: var(--color-bg); }

.btn-secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--c-border-strong);
}
.btn-ghost {
  color: var(--c-accent-ink);
  padding-inline: var(--space-2);
}

/* Hover is gated for the reason it is gated on the nav: on touch it sticks
   after the tap. `:active` is outside the gate, and its `:not(:disabled)`
   guard is load-bearing — a disabled button still matches `:active` in both
   engines while it is being pointed at, so without it a control brightens at
   the moment the reader tries to use it. */
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { opacity: 0.85; }
  .btn-secondary:hover { background: var(--color-surface-2); }
  .btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
}
.btn-primary:active:not(:disabled) { opacity: 0.7; }
.btn-secondary:active:not(:disabled) { background: color-mix(in srgb, var(--color-text) 10%, var(--color-surface-2)); }
.btn-ghost:active:not(:disabled) { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
.btn-primary:disabled:hover { opacity: 0.5; }
.btn-secondary:disabled:hover { background: transparent; }
.btn-ghost:disabled:hover { background: transparent; }

/* --------------------------------------------------------------------------
   Cards
   --------------------------------------------------------------------------
   A resting card is `--elev-1` and a hovered card is `--elev-2`, and the lit
   top edge is COMPOSED with the shadow rather than written into it: a
   `box-shadow` written as a bare elevation token can only be changed by
   writing another bare token, and the hover rule below would silently drop
   the lit edge. Both layers are in every state.

   The lift is a `--space-1` step rather than a -4px literal, which is the same
   distance and lets the density preference take it with everything else. */
.card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-5);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-1), var(--elev-inset);
  transition: transform var(--dur-base) cubic-bezier(0.2, 0.7, 0.3, 1),
    box-shadow var(--dur-base) ease, border-color var(--dur-base) ease;
}
.card-kicker {
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--c-accent-ink);
}
.card-title {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--text-lg);
  line-height: var(--text-lg-lh);
  letter-spacing: -0.015em;
}
.card-body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--text-sm-lh);
  color: var(--color-text-muted);
  flex: 1;
}
.card-meta {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  color: var(--color-text-subtle);
}
/* A pull quote inside a card: displayed, not read, so it is set a step larger
   than the card's own body and it does not carry the hairline. */
.card blockquote {
  margin: 0; border-left: none; padding: 0;
  font-style: italic;
  font-weight: 400;
  font-size: var(--text-base);
  line-height: var(--text-base-lh);
  color: var(--color-text);
}

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: translateY(calc(-1 * var(--space-1)));
    box-shadow: var(--shadow-2), var(--elev-inset);
    border-color: var(--c-border-strong);
  }
}

/* The elevation utilities, as classes, because the layouts compose them onto
   cards in their own markup (`class="card elev-sm reveal"`). Composed with the
   lit edge for the same reason the card's own hover is: a bare `--elev-N`
   would write `box-shadow` wholesale and drop the inset. */
.elev-sm { box-shadow: var(--shadow-1), var(--elev-inset); }
.elev-md { box-shadow: var(--shadow-2), var(--elev-inset); }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-5);
}
.card-grid--tight {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-4);
}

/* --------------------------------------------------------------------------
   Page header
   --------------------------------------------------------------------------
   A title and a deck, set at the display step the page type calls for, with
   the top padding coming off the scale so a dense page's header does not
   open a hole above the fold. */
.page-header { padding: clamp(var(--space-8), 7vw, var(--space-16)) 0 var(--space-8); }
.page-header h1 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: clamp(var(--text-display-sm), 5vw, var(--text-display-md));
  line-height: var(--text-display-md-lh);
  letter-spacing: var(--text-display-md-ls);
  margin: 0;
}
.page-header p {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
  color: var(--color-text-muted);
  margin: var(--space-4) 0 0;
}

/* --------------------------------------------------------------------------
   Listing rows
   --------------------------------------------------------------------------
   A title, a dotted leader of nothing, and a date. The date is tabular
   because a column of dates is a column of numbers. */
.note-row {
  display: flex; align-items: baseline; gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.note-row a, .note-row span.note-title {
  flex: 1;
  font-size: var(--text-base);
  text-decoration: none;
  color: var(--color-text);
}
.note-date {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-subtle);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Article
   --------------------------------------------------------------------------
   The site's long-form reading type: the prose size, the prose leading, and a
   `ch` measure — all three of which are the reader's settings rather than
   constants, which is the whole point of expressing the body this way. */
.article-kicker {
  display: block;
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--c-accent-ink);
  margin-bottom: var(--space-2);
}
.article-header h1 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: clamp(var(--text-xl), 4.5vw, var(--text-display-md));
  line-height: var(--text-display-md-lh);
  letter-spacing: var(--text-display-md-ls);
  margin: 0;
}
.article-date {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-subtle);
  margin: var(--space-2) 0 var(--space-8);
}
.article-body {
  font-size: var(--text-prose);
  line-height: var(--read-line);
  max-width: var(--pref-measure);
}
.back-link { margin: 0 0 var(--space-5); font-size: var(--text-sm); }
.back-link a { text-decoration: none; }

/* --------------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------------
   Quiet ink, a rule, and the same generous bottom padding it has always had:
   the last thing on a page should not sit on the last pixel of it. */
.site-footer {
  padding: var(--space-10) 0 var(--space-16);
  font-size: var(--text-sm);
  line-height: var(--text-base-lh);
  color: var(--color-text-muted);
}
.site-footer .stats-rule {
  border-top-color: var(--color-border);
  margin-bottom: var(--space-6);
}

/* --------------------------------------------------------------------------
   Home page
   --------------------------------------------------------------------------
   The old home page carried the heaviest glass in the repo: two 480px blurred
   colour pools drifting behind the hero, and a card treatment heavier than
   the shared `.card`. All of it is gone. A decorative background that moves
   behind the words a reader is about to read is a second thing competing with
   them, and there is no decorative vocabulary in the instrument theme to
   express it in.

   What survives is what was carrying content: the fade-up, the reveal
   transition, and the stats section as a plate. The one rise is a keyframe
   now, named for what it does, on the slow duration. */
.layout-home .hero h1,
.layout-home .hero p,
.layout-home .hero-actions {
  animation: rise-in var(--dur-slow) cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
/* The stagger is two steps of the FAST duration rather than two hand-typed
   delays, so a page whose motion is turned off gets no stagger either. */
.layout-home .hero p { animation-delay: var(--dur-fast); }
.layout-home .hero-actions { animation-delay: calc(var(--dur-fast) * 2); }

@keyframes rise-in {
  from { opacity: 0; transform: translateY(var(--space-5)); }
  to   { opacity: 1; transform: translateY(0); }
}

.layout-home .stats { display: none; }
.layout-home section:has(> .stats) {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-plate);
  box-shadow: var(--shadow-1), var(--elev-inset);
  /* The section's own padding is an inline style in index.md, and an inline
     style is only beatable by `!important`. */
  padding: var(--space-5) clamp(var(--space-5), 4vw, var(--space-8)) !important;
}
.layout-home .stats-rule { display: none; }
.layout-home .stat-fill { display: none; }
.layout-home .stat {
  flex-direction: column; align-items: flex-start; gap: var(--space-hair);
}
.layout-home .stat > span:first-child {
  font-size: var(--text-micro);
  line-height: var(--text-micro-lh);
  letter-spacing: var(--text-micro-ls);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  order: 2;
}
.layout-home .stat-value { font-size: var(--text-xl); order: 1; }

/* The reveal. Kept as its own transition on opacity and transform rather than
   as an animation, because the class is toggled by assets/js/reveal.js and
   the toggle is the whole contract. */
.reveal {
  opacity: 0;
  transform: translateY(var(--space-5));
  transition: opacity var(--dur-slow) ease, transform var(--dur-slow) ease;
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* --------------------------------------------------------------------------
   Narrow screens
   --------------------------------------------------------------------------
   The two measures that had to move at 640px, and nothing else: below that
   the date gutter and the timeline year stop being worth their width. */
@media (max-width: 640px) {
  .nav { gap: var(--space-3); }
  .timeline-row { gap: var(--space-4); }
  .timeline-year { flex-basis: 64px; font-size: var(--text-base); }
}

/* --------------------------------------------------------------------------
   Scrollbars
   --------------------------------------------------------------------------
   A persistently visible scrollbar is how a reader knows a wide table or a
   long equation continues past the measure, so this is a fixed 10px rather
   than `overflow: auto` plus a fade. It is chrome, not rhythm, and it does
   not scale with the density preference.

   The standard properties are gated on a POSITIVE Blink/WebKit check, and
   that is not a stylistic choice. The obvious `@supports not
   selector(::-webkit-scrollbar)` was written first and is wrong: Chromium
   reports `false` for `CSS.supports('selector(::webkit-scrollbar)')` even
   though it implements the pseudo-elements, so the block applied everywhere
   — and because a non-`auto` `scrollbar-width` makes the user agent IGNORE
   `::-webkit-scrollbar`, it quietly replaced the mechanism it was
   protecting. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--c-border-strong);
  border-radius: var(--radius-pill);
}
::-webkit-scrollbar-thumb:hover { background: var(--color-text-subtle); }

@supports not (selector(:-webkit-any-link)) {
  html {
    scrollbar-width: thin;
    scrollbar-color: var(--c-border-strong) transparent;
  }
}

/* --------------------------------------------------------------------------
   Reduced motion
   --------------------------------------------------------------------------
   The theme layer's durations already multiply `--pref-motion-scale`, which
   the boot script sets from this same media query, so a `transition: none`
   here is belt-and-braces. It is here anyway for the one thing a zero
   duration does not stop: a transform that is applied as a STATE rather than
   animated, which would otherwise leave a card parked four pixels off its own
   grid slot for as long as the pointer rests on it. */
@media (prefers-reduced-motion: reduce) {
  .card,
  .btn,
  .nav-collapse-toggle,
  #site-nav a,
  .nav--series a,
  .nav-mobile-toggle { transition: none; }
  .card:hover { transform: none; }
  .layout-home .hero h1,
  .layout-home .hero p,
  .layout-home .hero-actions { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
}
