/* ==========================================================================
   components.css - every class the site's markup is allowed to use.

   Naming: flat, lowercase, hyphenated, one level of nesting in the name.
   No BEM, no utility system (that would be reimplementing the thing we refuse
   to ship). Sections here appear in the order a page uses them.

   Cascade note: section spacing lives ONLY on .section, and no element selector in
   this file sets vertical rhythm. A page stylesheet must not add a second class that
   sets padding-block on the same element: two classes are equal specificity, so the
   winner would be decided by <link> order rather than by intent. compare.css's
   .chart-section did exactly that until 2026-08-22 and now stands alone instead.
   ========================================================================== */

/* TWO THINGS THAT LOOK LIKE MISTAKES AND ARE DECISIONS.

   THE LADDER (sections 5 to 7, around 5.5KB) can only be matched by the home
   page and ships on all eight. It stays here deliberately: the build contract
   calls the ladder the site's signature component, and splitting it into a
   seventh stylesheet would contradict "six stylesheets, one job each" and cost
   a request on the one page that publishes its own request count. The bytes are
   in a file every page already fetches and caches once.

   There was a `.mono` primitive in base.css for a monospace numeric column. Its
   only user was the retired /mission page, so after the 2026-08-27 restructure it
   shipped on every page and matched nothing - two declarations of dead weight in a
   file every reader fetches, on a site whose argument is that you should not ship
   what you do not run. It is gone. The component rules that need those two
   declarations already carry them inline, which is the right trade where the
   component needs other things too; bring the primitive back the day a second
   component wants exactly those two and nothing else. */

/* ==========================================================================
   1. Icons and controls
   ========================================================================== */

.icon {
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
}

/* A control that does nothing without JavaScript must not be on the page at all.
   #theme-toggle reveals itself off the data-js flag the inline head script sets;
   #nav-toggle ships with [hidden] in the markup and nav.js removes it. Two
   mechanisms because theme.js does not unhide its own button and nav.js does. */
#theme-toggle {
  display: none;
}

:root[data-js] #theme-toggle {
  display: inline-flex;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  color: var(--fg-muted);
  border: 1px solid transparent;
  border-radius: var(--r-md);
}

.icon-btn:hover {
  color: var(--fg-strong);
  background: var(--bg-well);
}

/* The theme button shows the theme you would GET, not the one you are in: sun in
   dark, moon in light.

   This is resolved from the theme itself, in the same three steps tokens.css uses for
   the palette, and deliberately NOT from aria-pressed. theme.js sets aria-pressed, and
   theme.js is deferred, so it runs only after the whole document is parsed - 219 KB of
   it on the cheatsheet. Keying the icon off that attribute meant every light-theme
   reader watched the sun paint and then flip to the moon. The inline head script sets
   data-theme before first paint, so resolving it this way there is nothing to flip.

   Light is the default, matching :root in tokens.css. */
#theme-toggle .icon-sun {
  display: none;
}

/* SCOPED TO `screen`, both of them, exactly as tokens.css scopes the palette.
   `prefers-color-scheme` still evaluates on paper and `[data-theme]` is not a media
   query at all, so a dark-OS reader - or any reader who chose dark with the toggle -
   printed the SUN on a page rendering in the light palette, because the palette is
   screen-scoped and this was not. Fixing only the media query would have fixed only
   half the readers. Any state keyed to a theme has to be scoped wherever the palette
   it belongs to is.

   DEFENCE IN DEPTH, not a visible fix: the print rule at the foot of this file hides
   .site-header, and the toggle lives inside it, so no reader can print the icon in any
   state today. The scoping is here so a future page or print layout that does show the
   toggle cannot inherit the wrong icon. */
@media screen and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #theme-toggle .icon-sun {
    display: block;
  }
  :root:not([data-theme="light"]) #theme-toggle .icon-moon {
    display: none;
  }
}

@media screen {
  :root[data-theme="dark"] #theme-toggle .icon-sun {
    display: block;
  }

  :root[data-theme="dark"] #theme-toggle .icon-moon {
    display: none;
  }
}

#nav-toggle[aria-expanded="true"] .icon-menu,
#nav-toggle .icon-close {
  display: none;
}

#nav-toggle[aria-expanded="true"] .icon-close {
  display: block;
}

/* ==========================================================================
   2. Header and primary navigation

   NAV BREAKPOINT: 640px. nav.js uses the same number - change one, change both.
   Below it the nav sits on its own row. With no JS there is no data-open
   attribute and no toggle, so the nav is simply always open; nav.js writes
   data-open="false" on #site-nav and the rule below collapses it.

   THE NUMBER IS DERIVED, NEVER CHOSEN, AND ITS INPUT MOVED ON 2026-08-27.
   The restructure took the nav from six labels to three - Docs, Compare, Videos -
   and 960 was derived from the six. Re-measured in Chromium against the live page:
   the three labels are 65.5 + 99.1 + 80.2px set in Rubik, which is the WIDEST face
   in this site's own fallback stack, so that is the worst case rather than the
   lucky one; with the list's 4px gaps the nav is 252.8px. Beside a 147.8px brand
   and a 44px theme button, with the bar's two 16px gaps, the row needs 476.5px
   inside the gutter - which the gutter's own clamp() satisfies from a 518px
   viewport up.

   640, not 520: 518 is the width at which it exactly stops fitting, and a
   breakpoint set there has no slack for a font that measures differently. 640
   leaves 122px and is a step this stylesheet already uses, so it adds no new
   number to the design. Leaving 960 would have handed a menu button to every
   reader between 518 and 960px for three links that fit.

   It was 820 while the nav had five labels, and five overflowed that by 7px: the
   desktop row was already wrapping there.
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.header-bar {
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-template-areas:
    "brand menu theme"
    "nav   nav  nav";
  align-items: center;
  column-gap: var(--sp-2);
  min-height: var(--header-h);
}

.brand {
  grid-area: brand;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  text-decoration: none;
}

.brand-mark {
  width: 2rem;
  height: 2rem;
}

.brand-name {
  font-size: var(--step-1);
  font-weight: 800;
  letter-spacing: var(--track-sub);
  color: var(--fg-strong);
}

.brand-version {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-dim);
}

#nav-toggle {
  grid-area: menu;
}

#theme-toggle {
  grid-area: theme;
}

.site-nav {
  grid-area: nav;
  border-top: 1px solid var(--border);
}

/* Without JS there is no toggle, so the nav sits open on its own row and the
   header is around 150px tall. Sticking that to the top of a phone would cost a
   fifth of the screen for the whole scroll, so on narrow screens it scrolls. */
@media (max-width: 639.98px) {
  :root:not([data-js]) .site-header {
    position: static;
  }
}

/* The collapsed state. CSS owns the breakpoint, nav.js owns only the state:
   it writes data-open on #site-nav and never touches [hidden], because
   [hidden] is `!important` and would keep the nav hidden after a resize past
   640px, where the menu button no longer exists to reopen it.
   Guarded on [data-js] so a no-JS reader - who has no button - keeps the nav. */
@media (max-width: 639.98px) {
  :root[data-js] #site-nav[data-open="false"] {
    display: none;
  }
}

.nav-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  list-style: none;
  margin: 0;
  padding: var(--sp-2) 0;
}

.nav-link {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  border-radius: var(--r-md);
  color: var(--fg-muted);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  text-decoration: none;
}

.nav-link:hover {
  color: var(--fg-strong);
  background: var(--bg-well);
}

.nav-link[aria-current="page"] {
  color: var(--fg-strong);
  box-shadow: inset 0 -2px 0 var(--gold);
}

@media (min-width: 640px) {
  /* Shared with nav.js - see the note at the top of this section. */
  .header-bar {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "brand nav theme";
    column-gap: var(--sp-4);
  }

  #nav-toggle {
    display: none;
  }

  .site-nav {
    border-top: 0;
  }

  .nav-list {
    justify-content: flex-end;
    padding: 0;
  }
}

/* ==========================================================================
   3. Section scaffolding
   ========================================================================== */

.section {
  padding-block: var(--section-y);
}

.section-band {
  background: var(--bg-well);
  border-block: 1px solid var(--border);
}

/* Wide screens get a left rail carrying the eyebrow, so the role each section
   plays in the argument - problem, evidence, objection, rebuttal - runs down
   the margin as a spine. Below 1024px it simply sits above the heading. */
@media (min-width: 1024px) {
  .beat {
    display: grid;
    grid-template-columns: 12rem minmax(0, 1fr);
    column-gap: var(--sp-8);
  }

  .beat > * {
    grid-column: 2;
  }

  .beat > .eyebrow {
    grid-column: 1;
    grid-row: 1;
    margin-bottom: 0;
    padding-top: .45em; /* optical alignment with the heading's cap height */
  }
}

/* The small uppercase label. One visual component with two roles: the section
   eyebrow in the left rail, and the column heading in the footer. The two carried
   byte-identical declarations in two places; only the eyebrow's own spacing differs.

   This rule must stay ABOVE .eyebrow-them, which overrides colour at the same
   specificity and therefore wins on source order alone. */
.eyebrow,
.footer-title {
  font-size: var(--step--1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--fg-muted);
}

.eyebrow {
  margin-bottom: var(--sp-3);
}

/* The objection is quoted in its own voice, so it wears the problem colour. */
.eyebrow-them {
  color: var(--clay);
}

.section-title {
  max-width: 30ch;
}

.section-body {
  max-width: var(--measure);
  margin-top: var(--sp-5);
}

.section-body + .section-body {
  margin-top: var(--sp-4);
}

.lede {
  font-size: var(--step-2);
  color: var(--fg);
}

.kicker {
  max-width: 46ch;
  margin-top: var(--sp-8);
  padding-left: var(--sp-5);
  border-left: 3px solid var(--gold);
  font-size: var(--step-2);
  font-weight: 600;
  color: var(--fg-strong);
}

/* Inline figures in prose: clay marks the bloat, gold marks RayClay. */
.num-problem {
  color: var(--clay);
}

.num-rayclay {
  color: var(--gold);
}

/* ==========================================================================
   4. Hero
   ========================================================================== */

.hero {
  padding-top: clamp(2.5rem, 1rem + 4vw, 4.5rem);
  padding-bottom: var(--section-y);
}

.hero-grid {
  display: grid;
  gap: var(--sp-6);
}

.hero-title {
  max-width: 17ch;
  font-size: var(--step-5);
}

.hero-sub {
  max-width: 48ch;
  font-size: var(--step-2);
}

/* A row of buttons under a heading: the hero's three and the closing call to
   action's two. Identical rules 616 lines apart until they were merged. */
.hero-actions,
.cta-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

@media (min-width: 900px) {
  .hero-grid {
    grid-template-columns: 1.05fr .95fr;
    align-items: end;
    gap: var(--sp-10);
  }
}

/* ==========================================================================
   5. THE LADDER - the signature. Four bars, one linear scale, no minimum
   width on the small one. Values are ink, never the colour of their bar.
   ========================================================================== */

.ladder-figure {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding: clamp(1.25rem, 3vw, 2.5rem);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-2xl);
}

.ladder-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-4);
  margin-bottom: var(--sp-6);
  border-bottom: 1px solid var(--border);
}

.ladder-title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: var(--track-head);
  color: var(--fg-strong);
}

.ladder-scale {
  font-size: var(--step--1);
  color: var(--fg-muted);
}

.ladder {
  display: grid;
  gap: var(--sp-5);
}

.ladder-row {
  display: grid;
  gap: var(--sp-2);
}

/* Grid children default to min-width:auto, and a long label would then push the
   track out of the row instead of wrapping. */
.ladder-term {
  min-width: 0;
}

.ladder-name {
  display: block;
  font-weight: 600;
  letter-spacing: var(--track-ui);
  color: var(--fg-strong);
}

.ladder-what {
  display: block;
  font-size: var(--step--1);
  line-height: 1.4;
  color: var(--fg-muted);
}

/* Fixed value column, so every track ends at the same x and the four bars
   really do share one scale. */
.ladder-def {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 6.5ch;
  align-items: center;
  column-gap: var(--sp-4);
}

.ladder-track {
  display: block;
  height: 14px;
  background: var(--bg-well);
  border-radius: var(--r-xs);
}

.ladder-bar {
  display: block;
  height: 100%;
  width: calc(var(--pct, 0) * 1%);
  background: var(--clay-solid);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}

/* The gold FILL (#ffd23f) is 1.5:1 on the light surface and this bar is 1.7%
   wide - it would disappear. --gold is the text-safe gold and clears 3:1 as a
   graphic in both themes. No minimum width: the size of this bar is the point. */
/* Bar lengths live HERE, not in a style="" attribute on the row.
   The production CSP is `style-src 'self'` with no 'unsafe-inline', so an inline
   style attribute is DROPPED by the browser: --pct never lands, calc() falls back
   to 0, and every bar renders empty while its label still reads "~100MB". That
   shipped to rayclay.dev on 2026-08-21. tools/serve.py now sends the real CSP so
   the same mistake fails locally, and tools/check_links.py refuses to let
   a style attribute into any page again.

   One shared linear scale: 300MB is 100%. Do not give the RayClay bar a minimum
   width to make it visible - it being nearly invisible is the entire argument. */
.ladder-chromium  { --pct: 33.3; }   /* ~100MB - Chromium engine alone            */
.ladder-floor     { --pct: 50; }     /* ~150MB - Electron empty-window floor      */
.ladder-vendored  { --pct: 100; }    /* ~300MB - Electron as vendored in an app   */
.ladder-rc        { --pct: 1.67; }   /* ~5MB   - RayClay                          */

.ladder-rc .ladder-bar {
  background: var(--gold);
}

/* The numeric value in a row, wherever a row has one: the ladder's sizes and the
   split card's parts. One component, one rule - they were two identical rules 73
   lines apart, so a change to the figure ink would have reached only one. */
.ladder-value,
.part-size {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
  color: var(--fg-strong);
}

.ladder-foot {
  max-width: 76ch;
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--border);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

@media (min-width: 640px) {
  .ladder-row {
    grid-template-columns: clamp(9rem, 22vw, 17rem) minmax(0, 1fr);
    align-items: center;
    column-gap: var(--sp-6);
  }
}

/* ==========================================================================
   6. The motley split - two panels, teal and violet, for anything with two
   sides that cannot be shared.
   ========================================================================== */

.split {
  display: grid;
  gap: var(--sp-5);
  margin-top: var(--sp-8);
}

.split-card {
  padding: var(--sp-6);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-top: 3px solid var(--border-strong);
  border-radius: var(--r-xl);
}

.split-teal {
  border-top-color: var(--teal-solid);
}

.split-violet {
  border-top-color: var(--violet-solid);
}

.split-title {
  font-size: var(--step-2);
}

.parts {
  margin-top: var(--sp-4);
}

.part {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-items: baseline;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
  border-top: 1px solid var(--border);
}

.part-name {
  color: var(--fg-muted);
}

.part-shared .part-name {
  font-weight: 600;
  color: var(--fg-strong);
}

.split-note {
  margin-top: var(--sp-5);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

@media (min-width: 720px) {
  .split {
    grid-template-columns: 1fr 1fr;
  }
}

/* ==========================================================================
   6b. THE LAYER STACK - the second diagram, merged in with /mission on
   2026-08-27. The page's argument is that three of the five layers between
   your code and the screen exist only to give that code a DOM, so removing
   the DOM removes them. That is a shape, and it was three paragraphs of
   prose; here it is the shape.

   COLOUR IS NEVER THE ONLY CARRIER. Each layer states "Kept" or "Removed" in
   words, in its own element, so the clay and teal edges are a second reading
   of something already written down. That matters more here than usual: the
   edges are 4px of pure hue, which is the one thing a reader with a colour
   deficiency, a monochrome print-out or a stylesheet turned off cannot use.

   The three removed layers deliberately carry NO shared total. They are three
   descriptions of one vendored bundle, not three downloads, and stacking three
   figures that a reader would naturally add up would overstate this site's own
   case by roughly 250MB. .stack-foot says so in words instead.
   ========================================================================== */

.stack-figure {
  margin-top: var(--sp-8);
  margin-bottom: var(--sp-8);
  padding: clamp(1.25rem, 3vw, 2rem);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-2xl);
}

.stack-caption {
  display: grid;
  gap: var(--sp-1);
  margin-bottom: var(--sp-5);
}

.stack-title {
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: var(--track-sub);
  color: var(--fg-strong);
}

.stack-scale {
  font-size: var(--step--1);
  color: var(--fg-muted);
}

/* An ordered list, because the order IS the content: this is the path a click
   takes from your source to the glass, and reversing it would be false. The
   markers are off because each row states its own position by being drawn in
   that position; with CSS off the numbers come back and still read correctly. */
.stack {
  display: grid;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.stack-layer {
  display: grid;
  gap: var(--sp-1) var(--sp-5);
  padding: var(--sp-4) var(--sp-5);
  background: var(--bg);
  border: 1px solid var(--border);
  /* The one asymmetric border on the site, and it is load-bearing: it is what
     makes five rows read as one stack rather than five cards. */
  border-left: 4px solid var(--border-strong);
  border-radius: var(--r-lg);
}

/* THE TWO EDGES ARE MEASURED AGAINST EACH OTHER, not just against the surface.
   Written with the *-solid fills, they were 2.41:1 for Kept and 4.79:1 for Removed
   in the light theme: the Removed markers read twice as loudly as the Kept ones, so
   the diagram delivered its own caption ("RayClay removes the middle three") by an
   accident of contrast rather than by the data. The text-safe tokens are 5.87 and
   6.60, which is legible and even-handed, and .ladder-rc already substitutes --gold
   for --gold-solid for exactly this reason. */
.stack-keep {
  border-left-color: var(--teal);
}

/* clay = the problem. tokens.css fixes that meaning; this is one of the two
   places on the site where it is a graphic rather than text. */
.stack-cut {
  border-left-color: var(--clay);
}

.stack-name {
  font-weight: 600;
  letter-spacing: var(--track-sub);
  color: var(--fg-strong);
}

.stack-what {
  max-width: var(--measure);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

.stack-verdict {
  justify-self: start;
  padding: .15em .6em;
  background: var(--bg-well);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  font-size: var(--step--1);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--fg-strong);
}

/* Wide enough for the verdict to sit beside the layer instead of under it.
   640px is where .split-card already changes shape, so the two diagrams on this
   page reflow at the same width rather than at two arbitrary ones. */
@media (min-width: 640px) {
  .stack-layer {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
  }

  .stack-verdict {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: center;
    justify-self: end;
  }
}

.stack-foot {
  max-width: var(--measure);
  margin-top: var(--sp-5);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

/* ==========================================================================
   7. The objection, stated in its own words
   ========================================================================== */

.objection {
  padding-left: clamp(1rem, 3vw, 2rem);
  border-left: 3px solid var(--clay);
}

.objection-quote {
  max-width: 20ch;
  font-size: var(--step-4);
}

/* ==========================================================================
   8. Code
   ========================================================================== */

.code-figure {
  max-width: 46rem;
  margin-top: var(--sp-8);
}

.code {
  padding: var(--sp-5);
  --scroll-cover: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow-x: auto;
  font-size: clamp(.8125rem, .78rem + .2vw, .9375rem);
  line-height: 1.7;
  scrollbar-width: thin;
  scrollbar-color: var(--fg-dim) transparent;
}

/* --------------------------------------------------------------------------
   8.0 Scroll shadow - ONE definition, every consumer

   A clipped line has to LOOK clipped: with overlay scrollbars nothing otherwise
   separates `void rcChart(const char *id, const RC_Series *series` from a whole
   declaration, and the reader misreads the API. The two `local` layers are covers
   that scroll with the content, so each shadow shows only while there is more to
   see that way. No JS, no scroll listener.

   A consumer sets --scroll-cover to the colour it is painted on; this rule paints.
   Never set `background` (the shorthand) on one - it resets background-image and
   takes all four layers with it.

   The consumers are listed in the selector below rather than sharing a utility
   class: their markup is generated, and a class attribute on 300-odd <dt> is real
   weight on a page that publishes its own. A new consumer adds its selector there.
   -------------------------------------------------------------------------- */
.code,
.entries dt,
.example pre,
.table-scroll {
  background-image:
    linear-gradient(to right, var(--scroll-cover, var(--bg)) 60%, transparent),
    linear-gradient(to left,  var(--scroll-cover, var(--bg)) 60%, transparent),
    linear-gradient(to right, var(--border-strong), transparent),
    linear-gradient(to left,  var(--border-strong), transparent);
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-repeat: no-repeat;
  background-size: 24px 100%, 24px 100%, 10px 100%, 10px 100%;
  background-attachment: local, local, scroll, scroll;
  background-color: var(--scroll-cover, var(--bg));
}

/* WHY EVERY CODE SCROLLER NAMES ITS OWN SCROLLBAR. `color-scheme` is `normal` on
   this document, so a UA scrollbar is drawn in light chrome whatever the theme
   says: sampled from the rendered page, track #fcfcfc and thumb #8b8b8b in BOTH
   themes. In dark that track is 17.26:1 against the block behind it - brighter than
   --fg-strong, so the loudest thing in a code block was its scrollbar. In light the
   thumb is 2.88:1, under the 3:1 a control owes. --fg-dim on a transparent track clears
   4.5:1 against both legal code surfaces in both themes (section 8.1 has the table),
   and it is a token, so it follows the theme like everything else.

   Deliberately NOT repeated as ::-webkit-scrollbar rules the way .cs-nav does it:
   defining that pseudo-element switches WebKit from an overlay scrollbar to a
   classic one, which would take 8px of layout out of each of the cheatsheet's 280
   signature rows on the platforms where overlay is the default. Where
   `scrollbar-color` is not supported the UA scrollbar simply stays as it is today. */

.code-caption {
  max-width: var(--measure);
  margin-top: var(--sp-3);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

.code-inline {
  padding: .1em .35em;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--fg-strong);
  /* An inline code token is one unbreakable word, and a grid track cannot
     shrink below its min-content contribution - so a single
     `rcText(rcFormat(rcAppArena(app),` inside a .notice-list pinned the
     track at 314px and the whole PAGE scrolled sideways at 320. Measured
     2026-08-27: / and /docs/for-web-developers both overflowed AT 320px, and were
     clean at 375. The contract names 375 / 768 / 1024 / 1440 and does not name 320;
     the rule is kept because 320 is a real device width and costs nothing at the
     widths the contract does name. `break-word` does not fix this,
     because it does not reduce the min-content contribution that sizes
     the track; `anywhere` does. The same idiom is already in
     cheatsheet.css for the same reason. */
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   8.1 The one syntax vocabulary

   These ten classes are the WHOLE token set for this site, and this is the only
   file that gives any of them a colour. Three producers write them and none of
   them owns the palette: tools/gen_cheatsheet.py (server-side),
   assets/js/codeblock.js (every free-form block, client-side), and the odd
   hand-written sample. If you add a producer, use these names.

   Six inks, and every shared ink is shared for a stated reason. Gold is an
   affordance colour and clay is the problem colour (tokens.css) - neither is
   ever spent on syntax, so what is left is three hues and three greys:

     --teal        the LIBRARY: RayClay's own names               .rc
     --violet      the LANGUAGE: C's keywords and its types       .ty  .kw
     --fg-strong   the NAME being called or declared              .fn
     --fg          literal data the program carries               .st  .nu
     --fg-muted    words that are neither language nor library    .pn  .cm
     --fg-dim      syntax that is not content at all              .op  .pp

   Hue is not what separates the two members of a shared ink, because a reader who
   cannot tell two hues apart would then lose the distinction entirely:

     .ty / .kw   weight - a keyword is 600, a type name is not
     .op / .pp   weight - a directive is 600, punctuation is not
     .cm / .pn   SLOPE - a comment is italic, an identifier is not. Weight is not
                 available here: making the payload heavier than the code it
                 annotates inverts the page, and making an identifier heavier
                 spends .fn's only signal on a parameter name.
     .st / .nu   nothing, deliberately. Both are literal data, they are never
                 confusable in place (a string carries its quotes, a number does
                 not), and there is no reading a person does where the answer
                 depends on telling them apart. One ink, one weight, no italic.

   Measured against the two surfaces a code block ever sits on - --bg-raised
   (.code, .cs-head .example) and --bg (.example inside a listing slab).
   RE-MEASURED 2026-08-29 with `python3 tools/contrast.py`, which is the authority;
   these rows are a convenience and go stale, so re-run it rather than trusting them:

                        dark            light
     --fg-strong      16.31 / 17.41   15.98 / 17.56
     --fg             14.30 / 15.27   13.05 / 14.35
     --fg-muted        7.86 /  8.39    6.65 /  7.31
     --fg-dim          5.54 /  5.92    5.42 /  5.96
     --teal            9.37 / 10.00    5.34 /  5.87
     --violet          5.98 /  6.38    7.22 /  7.94

   Every one clears 4.5:1 in both themes.

   LABELLED FAILURE, corrected 2026-08-29. Two of those light-theme rows carried the
   PRE-darkening values (--fg-dim 5.10/5.61, --teal 5.06/5.56) for two days after
   tokens.css moved both colours on 2026-08-27, and the sentence that followed them
   said "--bg-well is NOT a legal code surface: teal falls to 4.42:1 and --fg-dim to
   4.46:1 there in the light theme". Both now measure 4.67 and 4.74 on a well, so
   THAT REASON HAS EXPIRED - the numbers it rested on are no longer the numbers.

   --bg-well is still not a code surface, and the reason is now a different one: it is
   the site's warning surface (.notice, .section-band, .terminal-head), and putting a
   code block on it would make the two read as one component. A rule kept for a reason
   that has gone is the thing this project calls a caveat with no expiry, so the reason
   is restated rather than the conclusion quietly inherited. */

.fn { color: var(--fg-strong); font-weight: 600; }   /* strongest on the line */
.rc { color: var(--teal); }
/* A RayClay name in a declaration position is both: teal wins, the weight stays. */
.fn.rc { color: var(--teal); }
.ty { color: var(--violet); }
.kw { color: var(--violet); font-weight: 600; }
.st { color: var(--fg); }
.nu { color: var(--fg); }
.pn { color: var(--fg-muted); }
/* The payload - never below --fg-muted. */
.cm { color: var(--fg-muted); }
/* Italic ONLY where the comment sits inside code, which is where it has .pn beside
   it to be told apart from. The mono stack is metric-compatible in italic (and a
   synthesised oblique keeps the advance width), so the cheatsheet's aligned comment
   column does not move. Outside code the same class carries the cheatsheet's 277
   entry descriptions - a whole column of 13px reading matter, and setting THAT in
   italic is the thing build contract section 8.7 objects to in raylib's card. */
pre .cm,
code .cm { font-style: italic; }
/* A cross-reference chip inside a comment is a rendered link, not commentary. */
.cm .apiref { font-style: normal; }
.op { color: var(--fg-dim); }
.pp { color: var(--fg-dim); font-weight: 600; }

/* --------------------------------------------------------------------------
   8.2 Copy button - added by codeblock.js, so nothing here ships in the HTML.

   The button is a SIBLING of the <pre>, never a child: a <pre> is an
   overflow-x:auto box, and an absolutely positioned descendant of one both
   scrolls away from the corner it was pinned to and (see .visually-hidden in
   base.css) can drag the whole document's scrollWidth sideways.
   -------------------------------------------------------------------------- */

/* Column flex at every width, not only where the button leaves the overlay. The
   <pre> is the only in-flow child (both the button and the status line are taken
   out of flow at rest) and it stretches, so this is layout-neutral until something
   DOES fall into the flow - the coarse-pointer button and the fail state below.
   One layout for all three cases beats three sets of rules that have to agree. */
.codewrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.codewrap > pre { align-self: stretch; }

.copy-btn {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--track-ui);
  color: var(--fg-muted);
  background: var(--bg-well);
  /* --border is the right rule between two SURFACES and the wrong one around a
     control: measured on this fill it is 1.35:1 in light and 1.40:1 in dark, so
     the boundary of the one interactive thing inside a code block was invisible
     and only its label identified it. --fg-dim clears 4.5:1 against both legal
     code surfaces in both themes and is comfortably past the 3:1 that 1.4.11 asks of
     non-text UI against the button's own fill. Section 8.1 has the table; run
     `python3 tools/contrast.py` rather than trusting a figure copied here. */
  border: 1px solid var(--fg-dim);
  border-radius: var(--r-md);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

/* Revealed on hover, and ALWAYS present for keyboard and touch: a control that
   only exists under a pointer is a control half the readers never get. */
.codewrap:hover .copy-btn,
.copy-btn:focus-visible,
.copy-btn[data-state="done"],
.copy-btn[data-state="fail"] { opacity: 1; }

/* A coarse pointer cannot hover, so the button can never be revealed on demand - it
   has to be permanently visible, and a permanently visible OVERLAY covers the code
   forever. Measured on the cheatsheet at 375px: an 83px button over a 320px block is
   26% of the line width, and the longest first lines already reach under it. So on
   touch the button leaves the overlay and joins the flow, under the block. Nothing is
   ever covered, the tap target keeps its full 44px, and the cost is one row of space
   on the devices with the least of it - which is the right way round, because a
   reader who cannot see the code has lost more than one row. */
@media (hover: none) {
  .copy-btn {
    position: static;
    opacity: 1;
    margin-top: var(--sp-2);
  }
}

/* The fail state does the same thing, on every pointer, for the same reason. It is
   the one state that is not a receipt: it is an instruction with a live selection
   behind it, it stays until the next attempt, and it is nearly twice the idle width
   ("Press Control-C"). Left in the overlay it would park 154px of panel on the code
   indefinitely - 47% of the line at 375px - which is the failure covering more than
   the success did. In the flow it covers nothing and can be read next to the
   selection it is talking about. */
.copy-btn[data-state="fail"] {
  position: static;
  margin-top: var(--sp-2);
}

.copy-btn:hover { color: var(--fg-strong); border-color: var(--fg-strong); }

.copy-i { flex: none; }
/* State is carried by the GLYPH and by the word, never by colour alone. */
.copy-btn .copy-i-done,
.copy-btn .copy-i-fail { display: none; }
.copy-btn[data-state="done"] .copy-i-idle,
.copy-btn[data-state="fail"] .copy-i-idle { display: none; }
.copy-btn[data-state="done"] .copy-i-done { display: block; }
.copy-btn[data-state="fail"] .copy-i-fail { display: block; }
/* The tint is on the BORDER, not on the label: --bg-well is the tightest light-theme
   surface for teal, while a 1px rule is non-text UI and only owes 3:1. The label
   itself goes to full strength instead.

   Failure is not a colour on this site (tokens.css: "a warning or an error uses
   --fg-strong on --bg-well with an icon"), and --border-strong was not one either
   - at 1.69:1 against the fill it was indistinguishable from the idle border, so
   the state read as unchanged chrome. --fg-strong is 13.96:1 light / 14.22:1 dark
   against the fill: the same ink the X icon and the label already use. */
.copy-btn[data-state="done"] { color: var(--fg-strong); border-color: var(--teal); }
.copy-btn[data-state="fail"] { color: var(--fg-strong); border-color: var(--fg-strong); }

/* Page furniture inside a code block is not source, and a person selecting the code
   by hand has no sourceText() to strip it. Two things would otherwise land in a
   manual Control-C - including the one codeblock.js itself offers as its fallback
   when the clipboard API is refused: the screen-reader label the cheatsheet
   generator injects ("See api notes: "), and the rendered cross-reference chip whose
   marker is a ::before and so runs straight into the comment. Selection skips
   `user-select: none` subtrees, so both leave the same way they leave the button
   path. Scoped to <pre> - an .apiref in prose is ordinary selectable text. */
pre .visually-hidden,
pre .apiref,
/* ...and the '//' that exists only to host a chip: with the chip gone from the
   selection it would paste as a comment marker introducing nothing. The generator
   marks exactly those (gen_cheatsheet.py, render_code_line); a comment with words in
   it is never in this set. */
pre .cm-ref { user-select: none; }

/* ==========================================================================
   8b. THE THREE TERMINALS - one column per operating system, side by side.

   Written for /docs/getting-started's step zero, on the owner's instruction:
   "just a side by side by side Terminal (bash Linux, zsh Mac, Powershell
   Windows) with the commands for each OS".

   Each column is a <figure> whose <figcaption> is the window chrome, so the
   platform label is a caption of the code rather than a decoration floating
   above it - which is what makes it read correctly with the stylesheet off and
   what a screen reader announces with the block.

   The columns stack below 900px rather than shrinking. Three shell commands at
   a third of a phone screen is not a diagram, it is three columns of one word.
   ========================================================================== */

.terminals {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-6);
}

.terminal {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /* BOTH `min-width: 0` RULES ARE LOAD-BEARING, and leaving them out shipped a
     visibly broken diagram: a grid item's automatic minimum size is its
     min-content width, and a shell command does not break, so each column grew
     past its 1fr track. `overflow: hidden` then clipped what had grown - the
     Linux and Windows commands lost their tails and their right-aligned shell
     labels disappeared entirely, with no scrollbar to say anything was missing.
     The one on the children is what lets the <pre> scroll inside the column
     instead of widening it. */
  min-width: 0;
  margin: 0;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}

.terminal > * {
  min-width: 0;
}

.terminal-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-well);
  border-bottom: 1px solid var(--border);
}

.terminal-os {
  font-weight: 600;
  letter-spacing: var(--track-sub);
  color: var(--fg-strong);
}

.terminal-shell {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

/* The block inside a terminal loses its own frame: the terminal IS the frame,
   and two nested borders read as a box in a box.

   Smaller and tighter than a normal code block, and MEASURED rather than nudged:
   a third of the content column is about 350px of usable width, the longest line
   in these three is 40 characters, and 40 characters of the mono stack at
   --step--1 is about 315px. At the shared .code size and padding it was 500px,
   so two of the three columns scrolled - which on a diagram whose whole job is
   to be read at a glance is the same as being wrong. */
.terminal .code {
  margin: 0;
  padding: var(--sp-4);
  --scroll-cover: var(--bg-raised);
  border: 0;
  border-radius: 0;
  font-size: var(--step--1);
}

/* THE RAIL IS GIVEN BACK for this one element. .beat reserves 12rem plus a gutter
   for the eyebrow and puts everything else in column 2, which is right for prose
   at --measure and wrong for a three-column diagram: it costs 224px, and the
   columns pay it three times over. A diagram is not prose. */
.beat > .terminals {
  grid-column: 1 / -1;
}

@media (min-width: 900px) {
  .terminals {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* ==========================================================================
   9. The honest warning. Not a fifth colour - ink, a well, and an icon.
   ========================================================================== */

.notice {
  max-width: 72ch;
  margin-top: var(--sp-8);
  padding: clamp(1.25rem, 3vw, 2rem);
  background: var(--bg-well);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-xl);
}

.notice-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.notice-icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--fg-strong);
}

/* The class owns weight and tracking as well as size. It used to set size alone,
   so the two instances written as <h2> came out heavier and more tightly tracked
   than the seven written as <h3>, and nothing said so. The heading LEVEL stays
   free to follow the document outline; the look does not follow the level. */
.notice-title {
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: var(--track-sub);
}

.notice-list {
  display: grid;
  gap: var(--sp-5);
  margin-top: var(--sp-5);
  padding-left: 0;
  list-style: none;
}

.notice-item {
  padding-left: var(--sp-5);
  border-left: 2px solid var(--border-strong);
}

.notice-term {
  display: block;
  font-weight: 600;
  color: var(--fg-strong);
}

/* ==========================================================================
   10. Buttons and the closing call to action
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--r-lg);
  font-weight: 600;
  letter-spacing: var(--track-ui);
  text-align: center;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease);
}

.btn:hover {
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
}

.btn .icon {
  width: 1.125rem;
  height: 1.125rem;
}

.btn-primary {
  /* The fill is the only boundary a filled button has, and gold on cream measures
     1.34:1 - so the button's EDGE is invisible in the light theme even though its
     label is 13:1. --gold is the darkened text-weight gold, 4.71:1 on --bg, which
     draws the edge without changing the fill. Dark theme was already fine at 13:1. */
  background: var(--gold-solid);
  border-color: var(--gold);
  color: var(--on-accent);
}

.btn-ghost {
  /* --fg-dim, not --border-strong. A ghost button has no fill, so its border IS the
     control's boundary, and --border-strong measures 2.13:1 on --bg and 1.69:1 on a
     well - under the 3:1 that 1.4.11 asks of non-text UI. --fg-dim is 5.96:1 light and
     5.54:1 dark. This is the same substitution .copy-btn already makes, for the same
     reason, and the rule saying why is a few hundred lines above. */
  border-color: var(--fg-dim);
  color: var(--fg-strong);
}

.btn-ghost:hover {
  background: var(--bg-raised);
}

.cta-title {
  font-size: var(--step-4);
}

.closer {
  max-width: 46ch;
  margin-top: var(--sp-10);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--border);
  font-size: var(--step-2);
  font-weight: 500;
  color: var(--fg-strong);
}

/* ==========================================================================
   11. Footer
   ========================================================================== */

.site-footer {
  padding-block: var(--sp-12);
  background: var(--bg-raised);
  border-top: 1px solid var(--border);
}

.footer-grid {
  display: grid;
  gap: var(--sp-8);
}

.footer-brand {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  max-width: 34ch;
}

.footer-mark {
  width: 2.75rem;
  height: 2.75rem;
}

.footer-line {
  font-size: var(--step-0);
  color: var(--fg-muted);
}

.footer-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--sp-6);
}

.footer-col {
  min-width: 0;
}

.footer-list {
  margin-top: var(--sp-1);
  padding-left: 0;
  list-style: none;
}

.footer-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--fg);
  text-decoration: none;
}

.footer-link:hover {
  color: var(--fg-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-10);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--border);
  font-size: var(--step--1);
  color: var(--fg-muted);
}

#page-weight {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--fg-strong);
}

@media (min-width: 720px) {
  .footer-grid {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--sp-10);
  }
}

/* ------------------------------------------------------------------ on paper -----
   The print behaviour of a shared component belongs with the shared component. Both
   of these lived in cheatsheet.css, which only ONE page links, so the other seven
   printed a sticky header, the whole navigation and a theme button that does nothing
   on paper. A page added tomorrow gets this for free instead of inheriting a page
   stylesheet's opinion. */
@media print {
  .site-header, .site-footer { display: none; }

  /* THE SIGNATURE FIGURE HAD NO PRINTED FORM. Both the track and every bar are
     painted with background-color alone, and browsers suppress backgrounds on paper -
     the same fact tokens.css scopes the dark palette to `screen` for. So the ladder
     printed as its frame, its caption promising "four sizes, one linear scale", four
     labels, four values, and four empty rows where the comparison should be. Meanwhile
     /compare printed fine, because those bars are SVG rect fills: the printed site
     showed every chart RayClay loses on and not the one it wins on.

     The colour request is the narrow fix and keeps the linear scale exactly. The
     hairlines are the fallback for a greyscale printer or a driver that ignores it:
     the bar's extent stays readable with no colour at all. */
  .ladder-track,
  .ladder-bar {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  .ladder-track {
    border: 1px solid var(--border-strong);
  }

  .ladder-bar {
    border-right: 2px solid var(--fg);
  }
}
