/*! walnut.css v0.4.0 | MIT License | github.com/SKMinWoo/walnut.css */
@layer reset, tokens, base, layout, components, finish, utilities, cinema;

/* ── reset ── */
@layer reset {
/* ═══════════════════════════════════════════════════
   00 — RESET
   Modern CSS reset. Opinionated but minimal.
   Based on Andy Bell's modern reset + additions
   for walnut.css's typography-forward design.
═══════════════════════════════════════════════════ */

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

* {
  margin: 0;
  padding: 0;
}

html {
  -moz-text-size-adjust: none;
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
  scroll-behavior: smooth;
}

body {
  min-height: 100vh;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

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

textarea {
  field-sizing: content;    /* auto-grow — zero JS */
  min-height: 2.5lh;       /* at least 2.5 lines */
}

p,
h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

h1, h2, h3, h4, h5, h6 {
  line-height: 1.1;
  text-wrap: balance;
}

p, li, figcaption {
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration-skip-ink: auto;
}

ul, ol {
  list-style: none;
}

table {
  border-collapse: collapse;
  width: 100%;
}

/* The `hidden` attribute has to beat every walnut rule that sets `display`.
   Specificity cannot do it: .wal-flex, .wal-badge and a dozen others outrank
   the UA sheet's [hidden], and utilities come after every other layer, so
   `<div class="wal-flex" hidden>` rendered. `!important` inverts layer order,
   which puts the earliest layer on top — so this sits in reset, and even an
   unlayered `!important` cannot un-hide the element: remove the attribute,
   which is what it means. `until-found` is excluded: it hides with
   content-visibility and must stay findable. */
[hidden]:not([hidden="until-found"]) { display: none !important; }

/* Remove animations and transitions for users who prefer not to see them */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Pseudo-elements `*` does not reach, each in a rule of its own: a
     browser that does not know one drops only that rule, not the list. */
  *::backdrop { transition-duration: 0.01ms !important; }
  *::details-content { transition-duration: 0.01ms !important; }
  *::picker(select) { transition-duration: 0.01ms !important; }
}

}

/* ── tokens ── */
@layer tokens {
/* ═══════════════════════════════════════════════════
   01 — TOKENS · THE COLOUR SCRIPT
   ───────────────────────────────────────────────────
   A film's colour script is the sequence of colours that carries its emotional
   arc — fixed by intent, not derived from a formula. walnut borrows the term
   literally. Theming walnut means writing a colour script:

       ONE GROUND ROD      every neutral — page, card stock, hairline, ink —
                           is the same hue at a different lightness.
       FIVE CUES           accent · bloom · gold · olive · cool. Each is a
                           hue + a chroma. Everything else is derived.

   That is the whole input surface. A theme file is those numbers, plus any
   ramp stops it nudges, and nothing else; see src/themes/*.css.

   ───────────────────────────────────────────────────
   HOW THE DERIVATION WORKS, AND WHY THERE IS ONLY ONE OF IT

   Every colour below is declared exactly once, as `light-dark(light, dark)`.
   Two properties of CSS make that sufficient:

     1. A custom property's computed value is a token stream. Its var()s
        are substituted where it is declared, but a `light-dark()` in it is
        just more tokens, so it is NOT resolved there — it is resolved where
        the token is finally *used*.
     2. `light-dark()` picks its branch from the used element's `color-scheme`,
        which inherits.

   So one declaration at :root covers dark mode, light mode, and any subtree
   that sets a different `color-scheme` — a light island on a dark page needs
   no second palette, just `.wal-light` on a wrapper.

   Before this, each theme restated all ~30 derived tokens three times (dark,
   light-by-class, light-by-media-query). That is why v0.3.0 had to fix "theme
   files defeated the chroma tokens": the moment a theme restates a derived
   value, the rod stops driving it. Nothing here restates anything.

   ───────────────────────────────────────────────────
   THE KNOBS, IN THE ORDER YOU WILL REACH FOR THEM

     --wal-hue / --wal-chroma        the ground rod
     --wal-<cue>-hue / -chroma       the five cues
     --wal-l-*                       lightness stops (the ramp)
     --wal-cx-*                      chroma multipliers per surface family
     --wal-radius / --wal-elevation / --wal-wash    geometry and atmosphere

   Only the first two are needed for a coherent theme. The ramp exists so a
   theme can nudge one stop without inheriting the job of restating it.
   Geometry and atmosphere are not a theme's to set at all: they are the
   finish's knobs, and a palette that touches them overrides every finish it
   is paired with (see 07-finish.css).
═══════════════════════════════════════════════════ */


/* ═══ REGISTRATION ═══
   @property buys three things: type checking (a typo'd hue falls back to the
   initial value instead of invalidating every colour that reads it), animation
   (a registered <number> interpolates, so a palette change can crossfade), and
   a documented initial value.

   Only scalars are registered. The colour tokens are deliberately left
   unregistered — registering them as <color> would force `light-dark()` to
   resolve at the declaring element, which is exactly the behaviour this file
   depends on NOT happening. */

/* ── The ground rod ── */
@property --wal-hue        { syntax: "<number>"; inherits: true; initial-value: 55; }
@property --wal-chroma     { syntax: "<number>"; inherits: true; initial-value: 0.04; }

/* ── The five cues ──
   Hue is an angle on the oklch wheel; chroma is how far from grey it sits.
   Chroma is a per-cue input rather than a constant because the sRGB gamut is
   not the same width at every hue: at 62% lightness c=0.17 is in gamut around
   terracotta and clips across much of the green–blue arc, where the browser
   silently flattens it and the rendered colour stops matching the hue you
   asked for. CSS cannot compute the gamut boundary, so rotating a hue rod far
   from its shipped value means lowering its chroma too. */
@property --wal-accent-hue    { syntax: "<number>"; inherits: true; initial-value: 25; }
@property --wal-accent-chroma { syntax: "<number>"; inherits: true; initial-value: 0.17; }
@property --wal-bloom-hue     { syntax: "<number>"; inherits: true; initial-value: 52; }
@property --wal-bloom-chroma  { syntax: "<number>"; inherits: true; initial-value: 0.12; }
@property --wal-gold-hue      { syntax: "<number>"; inherits: true; initial-value: 85; }
@property --wal-gold-chroma   { syntax: "<number>"; inherits: true; initial-value: 0.15; }
@property --wal-olive-hue     { syntax: "<number>"; inherits: true; initial-value: 130; }
@property --wal-olive-chroma  { syntax: "<number>"; inherits: true; initial-value: 0.10; }
@property --wal-cool-hue      { syntax: "<number>"; inherits: true; initial-value: 234; }
@property --wal-cool-chroma   { syntax: "<number>"; inherits: true; initial-value: 0.11; }

/* ── The ramp ──
   Registered for the same reasons as the rods, and for one more: .wal-retune
   (06-cinema.css) can only tween what is registered. A palette nudges these
   stops, and left unregistered they would snap on the first frame of a
   palette switch while the hues were still travelling — Press → Café would
   drop its body text eight points of lightness before anything else moved.
   Initial values are the stock ramp in :root below. */
@property --wal-l-bg-light         { syntax: "<percentage>"; inherits: true; initial-value: 93%; }
@property --wal-l-bg-dark          { syntax: "<percentage>"; inherits: true; initial-value: 16%; }
@property --wal-l-subtle-light     { syntax: "<percentage>"; inherits: true; initial-value: 90%; }
@property --wal-l-subtle-dark      { syntax: "<percentage>"; inherits: true; initial-value: 19%; }
@property --wal-l-surface-light    { syntax: "<percentage>"; inherits: true; initial-value: 97%; }
@property --wal-l-surface-dark     { syntax: "<percentage>"; inherits: true; initial-value: 22%; }
@property --wal-l-surface-2-light  { syntax: "<percentage>"; inherits: true; initial-value: 91%; }
@property --wal-l-surface-2-dark   { syntax: "<percentage>"; inherits: true; initial-value: 26%; }
@property --wal-l-line-light       { syntax: "<percentage>"; inherits: true; initial-value: 84%; }
@property --wal-l-line-dark        { syntax: "<percentage>"; inherits: true; initial-value: 32%; }
@property --wal-l-line-soft-light  { syntax: "<percentage>"; inherits: true; initial-value: 88%; }
@property --wal-l-line-soft-dark   { syntax: "<percentage>"; inherits: true; initial-value: 28%; }
@property --wal-l-line-input-light { syntax: "<percentage>"; inherits: true; initial-value: 57%; }
@property --wal-l-line-input-dark  { syntax: "<percentage>"; inherits: true; initial-value: 56%; }
@property --wal-l-text-light       { syntax: "<percentage>"; inherits: true; initial-value: 20%; }
@property --wal-l-text-dark        { syntax: "<percentage>"; inherits: true; initial-value: 92%; }
@property --wal-l-text-soft-light  { syntax: "<percentage>"; inherits: true; initial-value: 42%; }
@property --wal-l-text-soft-dark   { syntax: "<percentage>"; inherits: true; initial-value: 72%; }
@property --wal-l-text-muted-light { syntax: "<percentage>"; inherits: true; initial-value: 49%; }
@property --wal-l-text-muted-dark  { syntax: "<percentage>"; inherits: true; initial-value: 64%; }
@property --wal-l-accent-light     { syntax: "<percentage>"; inherits: true; initial-value: 52%; }
@property --wal-l-accent-dark      { syntax: "<percentage>"; inherits: true; initial-value: 62%; }
@property --wal-l-bloom-light      { syntax: "<percentage>"; inherits: true; initial-value: 54%; }
@property --wal-l-bloom-dark       { syntax: "<percentage>"; inherits: true; initial-value: 70%; }
@property --wal-l-gold-light       { syntax: "<percentage>"; inherits: true; initial-value: 60%; }
@property --wal-l-gold-dark        { syntax: "<percentage>"; inherits: true; initial-value: 75%; }
@property --wal-l-olive-light      { syntax: "<percentage>"; inherits: true; initial-value: 48%; }
@property --wal-l-olive-dark       { syntax: "<percentage>"; inherits: true; initial-value: 60%; }
@property --wal-l-cool-light       { syntax: "<percentage>"; inherits: true; initial-value: 46%; }
@property --wal-l-cool-dark        { syntax: "<percentage>"; inherits: true; initial-value: 62%; }
@property --wal-hover-shift-light  { syntax: "<percentage>"; inherits: true; initial-value: -6%; }
@property --wal-hover-shift-dark   { syntax: "<percentage>"; inherits: true; initial-value: 6%; }
@property --wal-ink-shift-light    { syntax: "<percentage>"; inherits: true; initial-value: -12%; }
@property --wal-ink-shift-dark     { syntax: "<percentage>"; inherits: true; initial-value: 8%; }
@property --wal-cx-ground-light    { syntax: "<number>";     inherits: true; initial-value: 0.65; }
@property --wal-cx-ground-dark     { syntax: "<number>";     inherits: true; initial-value: 1; }
@property --wal-cx-surface-light   { syntax: "<number>";     inherits: true; initial-value: 0.40; }
@property --wal-cx-surface-dark    { syntax: "<number>";     inherits: true; initial-value: 1.15; }
@property --wal-cx-line-light      { syntax: "<number>";     inherits: true; initial-value: 0.75; }
@property --wal-cx-line-dark       { syntax: "<number>";     inherits: true; initial-value: 0.60; }
@property --wal-cx-ink-light       { syntax: "<number>";     inherits: true; initial-value: 0.85; }
@property --wal-cx-ink-dark        { syntax: "<number>";     inherits: true; initial-value: 0.45; }

/* ── Geometry and atmosphere ── */
@property --wal-radius     { syntax: "<length>";     inherits: true; initial-value: 12px; }
@property --wal-radius-sm  { syntax: "<length>";     inherits: true; initial-value: 6px; }
@property --wal-radius-lg  { syntax: "<length>";     inherits: true; initial-value: 18px; }
@property --wal-elevation  { syntax: "<number>";     inherits: true; initial-value: 1; }
@property --wal-wash       { syntax: "<number>";     inherits: true; initial-value: 1; }
@property --wal-hairline   { syntax: "<length>";     inherits: true; initial-value: 1px; }
@property --wal-line-boost { syntax: "<number>";     inherits: true; initial-value: 1; }


/* ═══ THE DEFAULT SCRIPT ═══
   Terracotta, apricot, goldenrod, botanical and adriatic on a warm amber
   ground. Café used to restate this script as a theme; it now has a script of
   its own (src/themes/cafe.css), and this one belongs to no class: it is what
   a page wears when it names no palette. */
:root {
  /* ── The ground rod ── */
  --wal-hue:    55;
  --wal-chroma: 0.04;

  /* ── The five cues ──
     The bloom and cool hues are not stated here: they default to offsets from
     the accent, declared with the derivation below. */
  --wal-accent-hue:    25;    --wal-accent-chroma: 0.17;  /* terracotta   */
                              --wal-bloom-chroma:  0.12;  /* apricot      */
  --wal-gold-hue:      85;    --wal-gold-chroma:   0.15;  /* goldenrod    */
  --wal-olive-hue:     130;   --wal-olive-chroma:  0.10;  /* botanical    */
                              --wal-cool-chroma:   0.11;  /* adriatic     */

  /* ── The ramp: lightness stops ──
     `-light` is the value used when the page is light; `-dark` when it is
     dark. Neutrals invert across the pair, which is the whole of mode support.
     text-muted is set as far from text as it can go and still clear 4.5:1 on
     all four grounds, surface-2 included; a palette that moves a ground has
     to move it too (see press.css). */
  --wal-l-bg-light:          93%;   --wal-l-bg-dark:          16%;
  --wal-l-subtle-light:      90%;   --wal-l-subtle-dark:      19%;
  --wal-l-surface-light:     97%;   --wal-l-surface-dark:     22%;
  --wal-l-surface-2-light:   91%;   --wal-l-surface-2-dark:   26%;
  --wal-l-line-light:        84%;   --wal-l-line-dark:        32%;
  --wal-l-line-soft-light:   88%;   --wal-l-line-soft-dark:   28%;
  --wal-l-line-input-light:  57%;   --wal-l-line-input-dark:  56%;
  --wal-l-text-light:        20%;   --wal-l-text-dark:        92%;
  --wal-l-text-soft-light:   42%;   --wal-l-text-soft-dark:   72%;
  --wal-l-text-muted-light:  49%;   --wal-l-text-muted-dark:  64%;

  /* Cue lightness. Cues do NOT invert — a terracotta stays terracotta — they
     only shift enough to hold their weight against the ground. */
  --wal-l-accent-light: 52%;   --wal-l-accent-dark: 62%;
  --wal-l-bloom-light:  54%;   --wal-l-bloom-dark:  70%;
  --wal-l-gold-light:   60%;   --wal-l-gold-dark:   75%;
  --wal-l-olive-light:  48%;   --wal-l-olive-dark:  60%;
  --wal-l-cool-light:   46%;   --wal-l-cool-dark:   62%;

  /* Two shifts applied on top of the cue stops:
     hover  — a cue as an interactive fill brightens on dark, deepens on light.
     ink    — a cue used as TEXT needs more separation from the ground than the
              same cue used as a fill. This is the single most common way a
              hand-made palette fails: goldenrod is a fine button and an
              illegible paragraph. `--wal-<cue>-ink` is the text-safe variant
              and every component that puts a cue on type reads it. */
  --wal-hover-shift-light: -6%;   --wal-hover-shift-dark:  6%;
  --wal-ink-shift-light:  -12%;   --wal-ink-shift-dark:    8%;

  /* ── The ramp: chroma multipliers ──
     Fractions of --wal-chroma, grouped by surface family rather than per
     token, so the ground stays one hue at one saturation logic. Light grounds
     need less chroma than dark ones to read as the same warmth; light ink
     needs more. */
  --wal-cx-ground-light:  0.65;   --wal-cx-ground-dark:  1;
  --wal-cx-surface-light: 0.40;   --wal-cx-surface-dark: 1.15;
  --wal-cx-line-light:    0.75;   --wal-cx-line-dark:    0.60;
  --wal-cx-ink-light:     0.85;   --wal-cx-ink-dark:     0.45;

  /* ── Geometry ── */
  --wal-radius:       12px;
  --wal-radius-sm:    6px;
  --wal-radius-lg:    18px;
  --wal-radius-full:  999px;
  --wal-hairline:     1px;
  --wal-line-boost:   1;
  /* The shape of every corner a radius makes (04-components.css, 43.
     Corners). round is the circle's arc; squircle is Piet Hein's
     superellipse, the Superellipse table's own edge; bevel is a cut
     corner, which is the catalogue finish's. A browser without
     corner-shape draws round whatever this says. */
  --wal-corner-shape: round;

  /* Atmosphere. --wal-elevation scales every shadow offset (0 flattens the
     page completely); --wal-wash scales the tinted light on <body>. Both are
     how a finish changes the *feel* without touching a single colour. */
  --wal-elevation:    1;
  --wal-wash:         1;

  /* ── Spacing scale ── */
  --wal-space-xs:     0.25rem;
  --wal-space-sm:     0.5rem;
  --wal-space-md:     1rem;
  --wal-space-lg:     1.5rem;
  --wal-space-xl:     2.5rem;
  --wal-space-2xl:    4rem;
  --wal-space-3xl:    6rem;

  /* ── Motion ── */
  --wal-ease:          cubic-bezier(0.16, 1, 0.3, 1);
  --wal-duration:      0.16s;
  --wal-duration-slow: 0.28s;

  /* Things on runners. Glide is a drawer on ball-bearing runners: a little
     resistance, a long run, and it comes to rest without a bump. Settle is a
     soft-close damper: most of the travel is quick, then the damper takes
     the rest. */
  --wal-ease-glide:  linear(0, 0.008 3%, 0.04 8%, 0.12 15%, 0.3 25%, 0.53 36%, 0.73 47%, 0.86 58%, 0.94 70%, 0.982 84%, 1);
  --wal-ease-settle: linear(0, 0.18 8%, 0.46 20%, 0.7 32%, 0.84 43%, 0.92 54%, 0.963 66%, 0.987 80%, 1);

  /* How the openings move: .wal-chest, .wal-door and .wal-window. Open and
     close are separate so a material (06-cinema.css, Materials) can give
     the two directions different weights; a drawer pulled out and a drawer
     pushed shut do not land the same way. */
  --wal-open-duration:  280ms;
  --wal-open-ease:      var(--wal-ease-glide);
  --wal-close-duration: 200ms;
  --wal-close-ease:     var(--wal-ease-settle);
  --wal-recolour-duration: 340ms;

  /* How each material comes to a stop. Not drawn by eye: each is a small
     model of the thing, sampled into linear() stops.

     walnut       Heavy. It starts slowly, arrives at its stop with speed,
                  knocks back 6.5% of its travel, comes in again, and has a
                  last 1.6% hop before it is still.
     walnut-shut  The same drawer pushed home: it shuts at 60%, bounces 4.5%
                  open, and closes.
     brass        Light and already moving. It strikes its stop and rings
                  like a spring: 5%, then 1.8%, then 0.6%, each quicker.
     stone        Too heavy to bounce. It takes a long time to get going,
                  then friction brings it down to a dead stop.

     A collision never passes its stop, so every value stays between 0 and
     1: nothing overshoots a size or slides past the edge of the screen. */
  --wal-ease-walnut:      linear(0, 0.003 2.1%, 0.011 4.2%, 0.026 6.4%, 0.045 8.6%, 0.071 10.9%, 0.103 13.3%, 0.185 18.3%, 0.256 22%, 0.344 26.1%, 0.454 30.9%, 0.812 45.9%, 0.916 50.7%, 1 55%, 0.971 57.9%, 0.952 60.7%, 0.939 63.6%, 0.935 66.5%, 0.939 69.3%, 0.951 72.2%, 0.971 75.1%, 1 78%, 0.988 80.7%, 0.984 83.5%, 0.988 86.2%, 1 89%, 1);
  --wal-ease-walnut-shut: linear(0, 0.003 2.3%, 0.011 4.6%, 0.026 7%, 0.045 9.4%, 0.071 11.9%, 0.103 14.5%, 0.185 20%, 0.269 24.7%, 0.375 30%, 0.731 46.3%, 0.841 51.5%, 0.927 55.9%, 1 60%, 0.98 62.5%, 0.966 65%, 0.958 67.5%, 0.955 70%, 0.958 72.5%, 0.966 75%, 0.98 77.5%, 1 80%, 0.99 85%, 0.992 87.5%, 1 90%, 1);
  --wal-ease-brass:       linear(0, 0.167 6%, 0.321 12%, 0.459 18%, 0.584 24%, 0.694 30%, 0.792 36.1%, 0.873 42.1%, 0.94 48.1%, 1 55%, 0.978 56.6%, 0.963 58.2%, 0.953 59.8%, 0.95 61.5%, 0.953 63.1%, 0.963 64.8%, 0.978 66.4%, 1 68%, 0.987 70%, 0.982 72%, 0.986 74%, 1 76%, 0.994 78.2%, 1 80.5%, 1);
  --wal-ease-stone:       linear(0, 0.006 8.5%, 0.015 12%, 0.029 15.4%, 0.047 18.7%, 0.07 21.8%, 0.1 24.9%, 0.134 27.9%, 0.198 32.4%, 0.273 36.7%, 0.339 39.9%, 0.455 44.9%, 0.534 48.5%, 0.612 52.4%, 0.681 56.2%, 0.745 60.1%, 0.802 64%, 0.852 67.9%, 0.894 71.8%, 0.925 75.1%, 0.95 78.4%, 0.97 81.7%, 0.985 85%, 0.999 90.9%, 1);

  /* ── Hardware ──
     Brass is the hardware, not the palette. It stays brass on every palette
     and in both modes, the way a drawer pull does not change when the
     cabinet is repainted. The charts' rail, feet and bezel are brass. -lo
     is the shadowed edge, and is what holds a rail at 3:1 on a light
     ground. */
  --wal-brass:    oklch(0.75 0.1 82);
  --wal-brass-hi: oklch(0.9 0.07 92);
  --wal-brass-lo: oklch(0.5 0.08 68);

  /* ── Furniture ── */
  --wal-nav-h:         64px;
  --wal-tracking-label: 0.1em;

  /* ── The desk ──
     The grain under .wal-desk, drawn by the browser rather than shipped as a
     photograph: ridged noise stretched along the board, so its creases run
     long the way walnut's figure does. It is a MASK — only its alpha is read —
     so the colour comes from --wal-desk-ink, which is the ground rod, and the
     grain re-themes with the script like every other neutral. stitchTiles
     makes the 640px tile repeat without a seam. */
  --wal-desk-grain: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='640' height='640'><filter id='g' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'><feTurbulence type='turbulence' baseFrequency='0.003 0.06' numOctaves='3' seed='4' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 0.85'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>");
}


/* ═══ MODE ═══
   Mode is `color-scheme`, not a palette. Setting it is the *only* thing these
   rules do, because `light-dark()` above does the rest — including for native
   form controls, scrollbars and the canvas, which a class-based dark mode has
   to chase separately.

   `light dark` means "follow the system", so the default is dark-by-night,
   light-by-day with no media query anywhere in this framework.

   Two axes, two syntaxes, deliberately: PALETTE BY CLASS, MODE BY ATTRIBUTE
   (or by .wal-light / .wal-dark, which are the same axis in class form).
   A selector like [data-theme="cafe"][data-theme="light"] can never match —
   an element has exactly one value per attribute — so the fused
   [data-theme="cafe-light"] forms exist for when you only have one hook.

   The default is written `:where(:root)` rather than `:root`, and that matters.
   `:root` is a pseudo-class and therefore carries specificity (0,1,0), while
   the mode rules below are inside :where() at (0,0,0) — so a plain `:root`
   default would OUTRANK every one of them and `data-theme="light"` would do
   nothing at all. With all three at zero specificity, source order expresses
   the intent (a stated mode beats the default) and any consumer rule of any
   kind beats all of them. */
:where(:root) { color-scheme: light dark; }

:where(.wal-light, [data-theme="light"], [data-theme$="-light"]) { color-scheme: light; }
:where(.wal-dark,  [data-theme="dark"],  [data-theme$="-dark"])  { color-scheme: dark; }


/* ═══ THE DERIVATION ═══
   Every derived colour, declared once.

   The selector list is what makes a palette *scopable*. On the common path
   only `:root` matches and this is a single rule on <html>. Add `.wal-palette`
   (or a palette class) to any element and the whole script re-derives from
   whatever rods are in effect there — so a section can carry its own colour
   script without a second stylesheet.

   Why the theme class names are listed here rather than in the theme files:
   the derivation must not live in the theme files, or we are back to four
   copies that drift. Listing the names centrally costs one line each and is
   harmless when a theme file is not loaded. `[data-theme|="cafe"]` matches
   both "cafe" and the fused "cafe-light" / "cafe-dark". */
:root,
:where(.wal-palette,
       .wal-cafe, .wal-forest, .wal-dusk, .wal-bone, .wal-press,
       [data-theme|="cafe"], [data-theme|="forest"], [data-theme|="dusk"],
       [data-theme|="bone"], [data-theme|="press"]) {

  /* ── Derived cues ──
     bloom and cool default to *offsets from the accent* rather than to fixed
     numbers, so rotating one rod keeps the script in tune instead of pulling
     the lead colour away from the rest of the cast. The offsets are not
     arbitrary: +27° is the warm step from a tomato red to a soft apricot, and
     +209° lands 29° past the accent's direct opposite — a split complement,
     across the wheel without being dead against it. That is the structure a
     hand-mixed mid-century palette tends to converge on anyway.
     Override either with a plain number to break the relationship.

     They live here, not in :root, because the hues are registered <number>s:
     a registered value computes where it is declared and descendants inherit
     the number, not the calc(). Stated only on :root, a scoped
     <section class="wal-forest"> inherited bloom at 52° instead of 87°. */
  --wal-bloom-hue: calc(var(--wal-accent-hue) + 27);
  --wal-cool-hue:  calc(var(--wal-accent-hue) + 209);

  /* ── Ground: surfaces ── */
  --wal-bg: light-dark(
    oklch(var(--wal-l-bg-light) calc(var(--wal-chroma) * var(--wal-cx-ground-light)) var(--wal-hue)),
    oklch(var(--wal-l-bg-dark)  calc(var(--wal-chroma) * var(--wal-cx-ground-dark))  var(--wal-hue)));
  --wal-bg-subtle: light-dark(
    oklch(var(--wal-l-subtle-light) calc(var(--wal-chroma) * var(--wal-cx-ground-light)) var(--wal-hue)),
    oklch(var(--wal-l-subtle-dark)  calc(var(--wal-chroma) * var(--wal-cx-ground-dark))  var(--wal-hue)));
  --wal-surface: light-dark(
    oklch(var(--wal-l-surface-light) calc(var(--wal-chroma) * var(--wal-cx-surface-light)) var(--wal-hue)),
    oklch(var(--wal-l-surface-dark)  calc(var(--wal-chroma) * var(--wal-cx-surface-dark))  var(--wal-hue)));
  --wal-surface-2: light-dark(
    oklch(var(--wal-l-surface-2-light) calc(var(--wal-chroma) * var(--wal-cx-ground-light)) var(--wal-hue)),
    oklch(var(--wal-l-surface-2-dark)  calc(var(--wal-chroma) * var(--wal-cx-ground-dark))  var(--wal-hue)));

  /* The grain of the desk under .wal-desk: the ground a little darker and a
     little richer — figure in the wood, not light on it.

     The two modes are bounded differently, and the bound is contrast. Page
     text sits directly on the grain. On a dark page the ink is lighter than
     the ground, so a darker grain line only adds contrast and the grain can be
     stated relative to the ground. On a light page it takes contrast away, so
     the grain goes no darker than --wal-bg-subtle: a ground every ink in the
     script, muted included, is already tuned to clear 4.5:1 on. */
  --wal-desk-ink: light-dark(
    oklch(var(--wal-l-subtle-light)          calc(var(--wal-chroma) * var(--wal-cx-ground-light) * 1.6) var(--wal-hue)),
    oklch(calc(var(--wal-l-bg-dark) - 4%)    calc(var(--wal-chroma) * var(--wal-cx-ground-dark)  * 1.4) var(--wal-hue)));
  /* ── Ground: hairlines ──
     --wal-line and --wal-line-soft are derived in the next rule, with the
     shadows, because a finish scales them. */

  /* The edge of a form control. A hairline is a divider and may be faint; the
     only thing marking an empty text field is its border, which WCAG 1.4.11
     holds to 3:1 against everything around it. --wal-line measured 1.2–1.6. */
  --wal-line-input: light-dark(
    oklch(var(--wal-l-line-input-light) calc(var(--wal-chroma) * var(--wal-cx-line-light)) var(--wal-hue)),
    oklch(var(--wal-l-line-input-dark)  calc(var(--wal-chroma) * var(--wal-cx-line-dark))  var(--wal-hue)));

  /* ── Ground: ink ── */
  --wal-text: light-dark(
    oklch(var(--wal-l-text-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light)) var(--wal-hue)),
    oklch(var(--wal-l-text-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark))  var(--wal-hue)));
  --wal-text-soft: light-dark(
    oklch(var(--wal-l-text-soft-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light) * 1.1) var(--wal-hue)),
    oklch(var(--wal-l-text-soft-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark) * 1.6) var(--wal-hue)));
  --wal-text-muted: light-dark(
    oklch(var(--wal-l-text-muted-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light)) var(--wal-hue)),
    oklch(var(--wal-l-text-muted-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark) * 1.4) var(--wal-hue)));

  /* ── Card surface ──
     A surface that may deliberately NOT follow the page — a printed plate,
     receipt, ticket or code card that stays light on a dark page. .wal-card
     and friends rebind --wal-text* from these, so every descendant flips with
     the surface and no per-child override is needed.

     These MUST be independent expressions, never `var(--wal-text)`. On the
     common path an alias would work, because the var() is substituted here
     before .wal-card rebinds anything. But an element matching both this
     rule and .wal-card — a card that is also a .wal-palette — would declare
     --wal-card-ink: var(--wal-text) and --wal-text: var(--wal-card-ink) on
     itself: a cycle, both invalid, and the card loses its text colour. It
     reads as a random rendering bug and is painful to trace. Repeating the
     expression is the cheap price of not having that bug. */
  --wal-card-bg: light-dark(
    oklch(var(--wal-l-surface-light) calc(var(--wal-chroma) * var(--wal-cx-surface-light)) var(--wal-hue)),
    oklch(var(--wal-l-surface-dark)  calc(var(--wal-chroma) * var(--wal-cx-surface-dark))  var(--wal-hue)));
  --wal-card-ink: light-dark(
    oklch(var(--wal-l-text-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light)) var(--wal-hue)),
    oklch(var(--wal-l-text-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark))  var(--wal-hue)));
  --wal-card-ink-soft: light-dark(
    oklch(var(--wal-l-text-soft-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light) * 1.1) var(--wal-hue)),
    oklch(var(--wal-l-text-soft-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark) * 1.6) var(--wal-hue)));
  --wal-card-ink-muted: light-dark(
    oklch(var(--wal-l-text-muted-light) calc(var(--wal-chroma) * var(--wal-cx-ink-light)) var(--wal-hue)),
    oklch(var(--wal-l-text-muted-dark)  calc(var(--wal-chroma) * var(--wal-cx-ink-dark) * 1.4) var(--wal-hue)));

  /* ══ THE FIVE CUES ══
     Each cue gets four forms, and picking the right one is most of what good
     theming is:

       --wal-<cue>        the colour. A fill, a rule, a swatch.
       --wal-<cue>-ink    the same cue, shifted to be legible AS TEXT.
       --wal-<cue>-soft   a 12% wash (15% for gold), for chip and badge
                          backgrounds.
       --wal-<cue>-fg     a foreground that is legible ON the cue. Computed,
                          not chosen — see the @supports block below.
     Plus --wal-accent-hover, since the accent is the one interactive cue. */

  /* Cue 1 — accent. The lead: the one cue that means "you can act on this". */
  --wal-accent: light-dark(
    oklch(var(--wal-l-accent-light) var(--wal-accent-chroma) var(--wal-accent-hue)),
    oklch(var(--wal-l-accent-dark)  var(--wal-accent-chroma) var(--wal-accent-hue)));
  --wal-accent-hover: light-dark(
    oklch(calc(var(--wal-l-accent-light) + var(--wal-hover-shift-light)) calc(var(--wal-accent-chroma) + 0.02) var(--wal-accent-hue)),
    oklch(calc(var(--wal-l-accent-dark)  + var(--wal-hover-shift-dark))  calc(var(--wal-accent-chroma) + 0.02) var(--wal-accent-hue)));
  --wal-accent-ink: light-dark(
    oklch(calc(var(--wal-l-accent-light) + var(--wal-ink-shift-light)) var(--wal-accent-chroma) var(--wal-accent-hue)),
    oklch(calc(var(--wal-l-accent-dark)  + var(--wal-ink-shift-dark))  var(--wal-accent-chroma) var(--wal-accent-hue)));
  --wal-accent-soft: color-mix(in oklch, var(--wal-accent) 12%, transparent);

  /* Cue 2 — bloom. The warm mid: gradients, washes, the second voice. */
  --wal-bloom: light-dark(
    oklch(var(--wal-l-bloom-light) var(--wal-bloom-chroma) var(--wal-bloom-hue)),
    oklch(var(--wal-l-bloom-dark)  var(--wal-bloom-chroma) var(--wal-bloom-hue)));
  --wal-bloom-ink: light-dark(
    oklch(calc(var(--wal-l-bloom-light) + var(--wal-ink-shift-light)) var(--wal-bloom-chroma) var(--wal-bloom-hue)),
    oklch(calc(var(--wal-l-bloom-dark)  + var(--wal-ink-shift-dark))  var(--wal-bloom-chroma) var(--wal-bloom-hue)));
  --wal-bloom-soft: color-mix(in oklch, var(--wal-bloom) 12%, transparent);

  /* Cue 3 — gold. The metal: rules, markers, ornament. */
  --wal-gold: light-dark(
    oklch(var(--wal-l-gold-light) var(--wal-gold-chroma) var(--wal-gold-hue)),
    oklch(var(--wal-l-gold-dark)  var(--wal-gold-chroma) var(--wal-gold-hue)));
  --wal-gold-ink: light-dark(
    oklch(calc(var(--wal-l-gold-light) + var(--wal-ink-shift-light)) var(--wal-gold-chroma) var(--wal-gold-hue)),
    oklch(calc(var(--wal-l-gold-dark)  + var(--wal-ink-shift-dark))  var(--wal-gold-chroma) var(--wal-gold-hue)));
  --wal-gold-soft: color-mix(in oklch, var(--wal-gold) 15%, transparent);

  /* Cue 4 — olive. The botanical: the calm, receding cue. */
  --wal-olive: light-dark(
    oklch(var(--wal-l-olive-light) var(--wal-olive-chroma) var(--wal-olive-hue)),
    oklch(var(--wal-l-olive-dark)  var(--wal-olive-chroma) var(--wal-olive-hue)));
  --wal-olive-ink: light-dark(
    oklch(calc(var(--wal-l-olive-light) + var(--wal-ink-shift-light)) var(--wal-olive-chroma) var(--wal-olive-hue)),
    oklch(calc(var(--wal-l-olive-dark)  + var(--wal-ink-shift-dark))  var(--wal-olive-chroma) var(--wal-olive-hue)));
  --wal-olive-soft: color-mix(in oklch, var(--wal-olive) 12%, transparent);

  /* Cue 5 — cool. The split complement: the only cue that argues with the others,
     which is exactly why a catalogue needs one. */
  --wal-cool: light-dark(
    oklch(var(--wal-l-cool-light) var(--wal-cool-chroma) var(--wal-cool-hue)),
    oklch(var(--wal-l-cool-dark)  var(--wal-cool-chroma) var(--wal-cool-hue)));
  --wal-cool-ink: light-dark(
    oklch(calc(var(--wal-l-cool-light) + var(--wal-ink-shift-light)) var(--wal-cool-chroma) var(--wal-cool-hue)),
    oklch(calc(var(--wal-l-cool-dark)  + var(--wal-ink-shift-dark))  var(--wal-cool-chroma) var(--wal-cool-hue)));
  --wal-cool-soft: color-mix(in oklch, var(--wal-cool) 12%, transparent);

  /* Numbered aliases. The named cues are what you write; the numbers are what
     you *iterate* — a swatch strip, a chart series, an nth-child rule can walk
     1..5 without knowing whether a given script calls cue 3 "gold" or "brass". */
  --wal-cue-1: var(--wal-accent);
  --wal-cue-2: var(--wal-bloom);
  --wal-cue-3: var(--wal-gold);
  --wal-cue-4: var(--wal-olive);
  --wal-cue-5: var(--wal-cool);

  /* ── Functional ──
     Off-script by design: success and danger must survive any rotation of
     the cues, so they are anchored to fixed hues and only ride the shared cue
     lightness stops. Chroma is a touch lower than a cue's so they read as
     signal rather than as part of the palette. Danger is set as type (.wal-error)
     far more than as a fill, so it rides the accent's ink stop, not its fill. */
  --wal-success: light-dark(oklch(var(--wal-l-olive-light) 0.14 145), oklch(var(--wal-l-gold-dark) 0.16 145));
  --wal-danger:  light-dark(oklch(calc(var(--wal-l-accent-light) + var(--wal-ink-shift-light)) 0.18 25),
                            oklch(calc(var(--wal-l-accent-dark)  + var(--wal-ink-shift-dark))  0.20 25));

  /* ── Shadow colour ──
     light-dark() takes colours, not whole shadow values, so the mode split
     lives in the colour; the geometry is in the next rule. */
  --wal-shadow-ink:    light-dark(oklch(50% 0.03 var(--wal-hue) / 0.18), oklch(10% 0.02 var(--wal-hue) / 0.45));
  --wal-shadow-ink-sm: light-dark(oklch(50% 0.03 var(--wal-hue) / 0.12), oklch(10% 0.02 var(--wal-hue) / 0.30));
  --wal-shadow-ink-lg: light-dark(oklch(50% 0.03 var(--wal-hue) / 0.22), oklch(8%  0.02 var(--wal-hue) / 0.55));

  /* ── Foregrounds ON a cue ──
     Plain fallback; the nested @supports below replaces these with a computed
     contrast pick. Kept as a real value so a browser without relative colour
     syntax (Firefox before 128) still gets a legible button rather than an
     invalid one. Each is the pick the computed form makes for the stock stops
     in each mode — so accent, olive and cool flip from light to dark ink with
     the mode, as the computed form does. */
  --wal-accent-fg: light-dark(oklch(97% 0.01 var(--wal-accent-hue)), oklch(16% 0.02 var(--wal-accent-hue)));
  --wal-bloom-fg:  light-dark(oklch(97% 0.01 var(--wal-bloom-hue)),  oklch(16% 0.02 var(--wal-bloom-hue)));
  --wal-gold-fg:   oklch(16% 0.02 var(--wal-gold-hue));
  --wal-olive-fg:  light-dark(oklch(97% 0.01 var(--wal-olive-hue)),  oklch(16% 0.02 var(--wal-olive-hue)));
  --wal-cool-fg:   light-dark(oklch(97% 0.01 var(--wal-cool-hue)),   oklch(16% 0.02 var(--wal-cool-hue)));

  /* ── Automatic foreground contrast ──
     The single most tedious part of hand-theming: every time you rotate a cue
     you have to re-decide whether text on it should be black or white, and
     getting it wrong is an accessibility bug rather than a taste one.

     Relative colour syntax can decide it arithmetically. `l` is the cue's own
     lightness as a 0–1 number, so

         clamp(0.16, (0.57 - l) * 1000, 0.97)

     is a step function: below the 0.57 threshold the middle term is a large
     positive number and clamps to 0.97 (near-white ink); above it the term goes
     negative and clamps to 0.16 (near-black ink). The ×1000 makes the crossover
     effectively instant rather than a mushy grey band.

     Not pure white and black: `calc(c * 0.08)` keeps a trace of the cue's own
     chroma in its foreground, which is why a terracotta button reads as warm
     cream on terracotta rather than as a sticker.

     0.57 is where the two candidates swap places under WCAG 2 contrast: for
     every cue in the shipped palettes, in both modes, any threshold from 0.56
     to 0.58 picks whichever of the two reads better. (It used to be 0.63, which
     picked the worse one in 19 of those 50 cases — dark-mode primary buttons
     at 3.6:1.) Near the threshold neither candidate may reach 4.5:1, so the
     shipped cue stops keep clear of roughly 0.55–0.59; a cue you move into
     that band is the case to check.

     light-dark() goes on the OUTSIDE. The natural spelling,
     `oklch(from var(--wal-accent) …)`, hands relative colour a light-dark()
     origin, and Chrome from the 123 floor through at least 127 cannot resolve
     one. It parses, so the @supports test below passes; it fails only at
     computed-value time, where `color` silently inherits the page text —
     near-black type on a terracotta button. So light-dark() chooses between
     two finished relative colours, and each origin is the cue rebuilt from
     the same stops as --wal-<cue> above, never var(--wal-<cue>): the alias
     would carry the light-dark() back in. The cost is that -fg follows the
     rods and the ramp, as -ink and -hover already do, rather than a colour
     written straight into --wal-<cue>.

     Nested rather than a separate rule, so the selector list above is the
     only copy: a second copy that drifted would leave a scoped palette with
     the root's foregrounds. */
  @supports (color: oklch(from white l c h)) {
    --wal-accent-fg: light-dark(
      oklch(from oklch(var(--wal-l-accent-light) var(--wal-accent-chroma) var(--wal-accent-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h),
      oklch(from oklch(var(--wal-l-accent-dark)  var(--wal-accent-chroma) var(--wal-accent-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h));
    --wal-bloom-fg: light-dark(
      oklch(from oklch(var(--wal-l-bloom-light) var(--wal-bloom-chroma) var(--wal-bloom-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h),
      oklch(from oklch(var(--wal-l-bloom-dark)  var(--wal-bloom-chroma) var(--wal-bloom-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h));
    --wal-gold-fg: light-dark(
      oklch(from oklch(var(--wal-l-gold-light) var(--wal-gold-chroma) var(--wal-gold-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h),
      oklch(from oklch(var(--wal-l-gold-dark)  var(--wal-gold-chroma) var(--wal-gold-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h));
    --wal-olive-fg: light-dark(
      oklch(from oklch(var(--wal-l-olive-light) var(--wal-olive-chroma) var(--wal-olive-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h),
      oklch(from oklch(var(--wal-l-olive-dark)  var(--wal-olive-chroma) var(--wal-olive-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h));
    --wal-cool-fg: light-dark(
      oklch(from oklch(var(--wal-l-cool-light) var(--wal-cool-chroma) var(--wal-cool-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h),
      oklch(from oklch(var(--wal-l-cool-dark)  var(--wal-cool-chroma) var(--wal-cool-hue))
            clamp(0.16, (0.57 - l) * 1000, 0.97) calc(c * 0.08) h));
  }
}


/* ═══ WHAT A FINISH SCALES ═══
   Hairlines and shadows read --wal-line-boost and --wal-elevation, which a
   finish sets. A custom property substitutes its var()s where it is declared,
   so derived only where a palette is, these froze at that element's finish:
   `data-finish` on a subtree changed its radii and label voice but not its
   rules or its shadows. Hence [data-finish] in this list and not in the one
   above — a finish re-derives the geometry it owns and nothing else, so it
   can never move a colour that a palette set. */
:root,
:where(.wal-palette,
       .wal-cafe, .wal-forest, .wal-dusk, .wal-bone, .wal-press,
       [data-theme|="cafe"], [data-theme|="forest"], [data-theme|="dusk"],
       [data-theme|="bone"], [data-theme|="press"],
       [data-finish]) {
  /* ── Hairlines ──
     --wal-line-boost pushes the rule AWAY from the page ground: darker on
     light, lighter on dark. It is one knob rather than two overrides so a
     finish can ask for stronger rules on any palette without owning a colour
     — see 07-finish.css, where the catalogue finish turns it to 1.7. */
  --wal-line: light-dark(
    oklch(calc(var(--wal-l-line-light) - (var(--wal-line-boost) - 1) * 9%)
          calc(var(--wal-chroma) * var(--wal-cx-line-light)) var(--wal-hue)),
    oklch(calc(var(--wal-l-line-dark)  + (var(--wal-line-boost) - 1) * 9%)
          calc(var(--wal-chroma) * var(--wal-cx-line-dark))  var(--wal-hue)));
  --wal-line-soft: light-dark(
    oklch(calc(var(--wal-l-line-soft-light) - (var(--wal-line-boost) - 1) * 6%)
          calc(var(--wal-chroma) * var(--wal-cx-line-light)) var(--wal-hue)),
    oklch(calc(var(--wal-l-line-soft-dark)  + (var(--wal-line-boost) - 1) * 6%)
          calc(var(--wal-chroma) * var(--wal-cx-line-dark))  var(--wal-hue)));

  /* The standard rule, as one shorthand, so a component never states a
     width of its own and --wal-hairline reaches every border. Declared here
     rather than in :root because it has to pick up this scope's --wal-line. */
  --wal-border: var(--wal-hairline) solid var(--wal-line);

  /* ── Shadows ──
     --wal-elevation scales the offsets, which is how a flat finish stays flat
     without overriding three separate shadows. */
  --wal-shadow:    0 calc(20px * var(--wal-elevation)) calc(44px * var(--wal-elevation)) -18px var(--wal-shadow-ink);
  --wal-shadow-sm: 0 calc(4px  * var(--wal-elevation)) calc(12px * var(--wal-elevation)) -4px  var(--wal-shadow-ink-sm);
  --wal-shadow-lg: 0 calc(32px * var(--wal-elevation)) calc(64px * var(--wal-elevation)) -24px var(--wal-shadow-ink-lg);
}


/* ═══ TYPOGRAPHY ═══ */
:root {
  /* Font stacks — override these vars, not the rules that read them. */
  --wal-font-display: "Jost", Futura, system-ui, sans-serif;
  --wal-font-body:    "Inter", system-ui, sans-serif;
  --wal-font-mono:    "DM Mono", ui-monospace, "Cascadia Code", monospace;

  /* Typewriter — screenplay/call-sheet texture. This is what --wal-font-serif
     used to hold, which was simply a wrong name: Courier Prime is monospace,
     so `.wal-font-serif` set a typewriter face and anyone asking for a serif
     got one silently. The two are separate tokens as of v0.2.0. */
  --wal-font-typewriter: "Courier Prime", Courier, monospace;
  --wal-font-serif:      ui-serif, Georgia, "Iowan Old Style", "Times New Roman", serif;

  /* The face used for catalogue furniture — eyebrows, plate keys, captions,
     figure numbers. Its own token because a finish may want the label voice to
     differ from the data voice even though both default to mono. */
  --wal-font-label: var(--wal-font-mono);

  /* Type scale — minor third (1.2) */
  --wal-text-xs:   0.694rem;   /* ~11.1px */
  --wal-text-sm:   0.833rem;   /* ~13.3px */
  --wal-text-base: 1rem;       /* 16px */
  --wal-text-md:   1.2rem;     /* ~19.2px */
  --wal-text-lg:   1.44rem;    /* ~23px */
  --wal-text-xl:   1.728rem;   /* ~27.6px */
  --wal-text-2xl:  2.074rem;   /* ~33.2px */
  --wal-text-3xl:  2.488rem;   /* ~39.8px */
  --wal-text-4xl:  2.986rem;   /* ~47.8px */
  --wal-text-5xl:  3.583rem;   /* ~57.3px */
}

}

/* ── base ── */
@layer base {
/* ═══════════════════════════════════════════════════
   02 — BASE
   Element-level defaults. No classes — just
   well-styled raw HTML that feels warm and typeset.
═══════════════════════════════════════════════════ */

body {
  background-color: var(--wal-bg);
  color: var(--wal-text);
  font-family: var(--wal-font-body);
  font-size: var(--wal-text-base);
  position: relative;
}

/* Warm light wash — three cues lighting the ground from off-screen, so it
   never reads as flat. Scaled by --wal-wash rather than declared outright:
   a finish that wants printed paper sets --wal-wash: 0 and the gradients
   collapse to transparent, with no rule here to override and no second
   `background-image` to keep in sync.

   Rendered on an accelerated fixed underlay pseudo-element (::after at z-index: -2)
   rather than background-attachment: fixed on body, so the browser composites the wash
   on its own GPU layer, scrolling the document requires zero background repaint, and it
   never collides with ::before components such as .wal-desk. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background-image:
    radial-gradient(60% 45% at 12% 0%,
      color-mix(in oklch, var(--wal-gold) calc(10% * var(--wal-wash)), transparent), transparent 70%),
    radial-gradient(55% 50% at 100% 20%,
      color-mix(in oklch, var(--wal-olive) calc(10% * var(--wal-wash)), transparent), transparent 72%),
    radial-gradient(70% 60% at 80% 100%,
      color-mix(in oklch, var(--wal-bloom) calc(8% * var(--wal-wash)), transparent), transparent 70%);
  transform: translateZ(0);
}

/* ─── Headings ─── */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--wal-font-display);
  color: var(--wal-text);
  font-weight: 700;
}

h1 { font-size: clamp(var(--wal-text-3xl), 7vw, var(--wal-text-5xl)); letter-spacing: -0.03em; }
h2 { font-size: clamp(var(--wal-text-2xl), 4.5vw, var(--wal-text-4xl)); letter-spacing: -0.02em; }
h3 { font-size: var(--wal-text-xl); letter-spacing: -0.01em; }
h4 { font-size: var(--wal-text-lg); }
h5 { font-size: var(--wal-text-md); }
h6 { font-size: var(--wal-text-base); text-transform: uppercase; letter-spacing: 0.08em; }

/* ─── Prose ─── */
p {
  color: var(--wal-text-soft);
  max-width: 65ch;
  margin-bottom: 1em;

  &:last-child { margin-bottom: 0; }
}

strong, b { color: var(--wal-text); font-weight: 700; }
em, i { font-style: italic; }
small { font-size: var(--wal-text-sm); }

/* ─── Links ───
   --wal-accent-ink, not --wal-accent: a link is the accent used as *type* on
   the page ground, which needs more separation from that ground than the same
   accent needs as a button fill. This is the distinction the -ink forms exist
   to carry; see the cue block in 01-tokens.css. */
a {
  color: var(--wal-accent-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  transition: color var(--wal-duration) ease;

  &:hover {
    color: var(--wal-accent-hover);
  }
}

/* ─── Code ─── */
code, kbd, samp {
  font-family: var(--wal-font-mono);
  font-size: 0.88em;
  padding: 0.15em 0.4em;
  border-radius: var(--wal-radius-sm);
  background: var(--wal-surface-2);
  border: var(--wal-hairline) solid var(--wal-line-soft);
}

pre {
  font-family: var(--wal-font-mono);
  font-size: var(--wal-text-sm);
  padding: var(--wal-space-lg);
  border-radius: var(--wal-radius);
  background: var(--wal-surface);
  border: var(--wal-border);
  overflow-x: auto;
  line-height: 1.7;

  & code {
    padding: 0;
    background: none;
    border: none;
    font-size: inherit;
  }
}

/* ─── Blockquote ─── */
blockquote {
  padding-left: var(--wal-space-lg);
  border-left: 3px solid var(--wal-gold);
  color: var(--wal-text-soft);
  font-style: italic;
}

/* ─── Horizontal rule ─── */
hr {
  border: none;
  border-top: var(--wal-border);
  margin: var(--wal-space-xl) 0;
}

/* ─── Mark / highlight ─── */
mark {
  background: var(--wal-gold-soft);
  color: var(--wal-text);
  padding: 0.05em 0.3em;
  border-radius: 3px;
}

/* ─── Selection ─── */
::selection {
  background: color-mix(in oklch, var(--wal-accent) 30%, transparent);
  color: var(--wal-text);
}

/* ─── Focus ─── */
:focus-visible {
  outline: 2px solid var(--wal-accent);
  outline-offset: 3px;
}

/* ─── Scrollbar (warm) ───
   Scoped to `html`, which is where the document scrollbar takes its styling
   from. It used to be `*`, which meant walnut silently restyled every scroll
   container in a consuming app — and because Chromium ignores the
   ::-webkit-scrollbar-* pseudos entirely whenever `scrollbar-width` or
   `scrollbar-color` is non-auto, a consumer had to reset BOTH properties to
   get its own bar back.

   Two things this scoping does NOT do. A bare `::-webkit-scrollbar` means
   `*::-webkit-scrollbar`, so the pseudos below carry an explicit `html`, or
   Safari would still style every container. And `scrollbar-color` inherits,
   so inner scroll containers do take the warm colours (not the thin width)
   in browsers with the standard property. To opt one out, give it
   `scrollbar-color: auto`. */
:root {
  --wal-scrollbar-thumb: color-mix(in oklch, var(--wal-olive) 45%, var(--wal-line));
}

html::-webkit-scrollbar { width: 10px; }
html::-webkit-scrollbar-track { background: var(--wal-bg); }
html::-webkit-scrollbar-thumb {
  background: var(--wal-scrollbar-thumb);
  border-radius: 6px;
  border: 2px solid var(--wal-bg);
}
html::-webkit-scrollbar-thumb:hover { background: var(--wal-accent); }

/* Firefox scrollbar */
@supports (scrollbar-color: auto) {
  html {
    scrollbar-color: var(--wal-scrollbar-thumb) var(--wal-bg);
    scrollbar-width: thin;
  }
}

/* ─── Skip link ─── */
.wal-skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--wal-gold);
  /* The computed foreground, not the page ground: --wal-bg on light gold
     measured 3.0:1, on the one control whose whole job is to be read. */
  color: var(--wal-gold-fg);
  padding: 0.75rem 1.25rem;
  z-index: 200;
  font-weight: 700;
  font-family: var(--wal-font-display);

  &:focus {
    left: 12px;
    top: 12px;
  }
}

}

/* ── layout ── */
@layer layout {
/* ═══════════════════════════════════════════════════
   03 — LAYOUT
   Composable layout primitives that respond to the
   space they are given, not to the viewport: grids
   shed columns via auto-fit + minmax, flex rows wrap
   on their bases. .wal-wrap is an inline-size
   container for your own @container rules; nothing
   here queries it. Pure CSS, zero JavaScript.
═══════════════════════════════════════════════════ */


/* ─── .wal-wrap ─────────────────────────────────
   Max-width content container, horizontally centered.
   Acts as a container query context so children can
   respond to the wrapper width, not the viewport.
   ────────────────────────────────────────────── */
.wal-wrap {
  width: 100%;
  max-width: 1140px;
  margin-inline: auto;

  /* Responsive padding: 1rem at narrow → 3rem at wide */
  padding-inline: clamp(var(--wal-space-md), 5cqi, var(--wal-space-3xl));

  /* Establish container context */
  container-type: inline-size;
  container-name: wrap;
}


/* ─── .wal-grid ─────────────────────────────────
   Intrinsic CSS Grid with auto-fill columns by
   default.  Explicit column variants shed columns
   intrinsically (auto-fit + minmax), so they respond
   to their parent's width at any nesting depth.
   ────────────────────────────────────────────── */
.wal-grid {
  display: grid;
  gap: var(--wal-space-lg);

  /* Auto-fill: columns never narrower than 280px */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));

  /* NOTE: deliberately NOT a query container.
     `@container` resolves against the nearest *ancestor* container — an element
     can never query itself. The previous `container-type: inline-size` here plus
     `@container grid { .wal-grid.wal-cols-N { ... } }` below therefore never
     matched, and the column variants never collapsed at any width. The variants
     now collapse intrinsically via auto-fit + minmax, which needs no container
     at all and works at any nesting depth.
     Removing container-type also drops the implied `contain: layout`, which had
     made every grid a containing block for `position: fixed` descendants.

     Gaps are the .wal-gap-* utilities, which mean the same step on every
     primitive: .wal-gap-lg is --wal-space-lg on a grid, a stack, a flex row
     and a cluster alike. This file used to restate them per primitive with
     different steps (a stack's -lg was space-xl, a cluster's -sm space-xs).
     Those never applied: the utilities layer comes later, and layer order
     outranks specificity, so .wal-gap-lg beat .wal-stack.wal-gap-lg. They
     were deleted rather than revived, because reviving them would silently
     change the spacing of every page already built on what actually rendered. */


  /* Column variants. `auto-fit` + `minmax(min(100%, Xrem), 1fr)` caps the count
     at N on wide parents and sheds columns as the parent narrows — the intended
     behaviour, achieved without a container query. The min track is tuned per
     variant so columns never get uncomfortably narrow before they wrap. */
  &.wal-cols-2 {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
    max-width: 100%;
  }

  &.wal-cols-3 {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  }

  &.wal-cols-4 {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  }

  &.wal-cols-5 {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  }

  &.wal-cols-6 {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  }
}


/* ─── .wal-flex ──────────────────────────────────
   Flexbox row with gap. Composable modifiers for
   direction, wrapping, and alignment.
   ────────────────────────────────────────────── */
.wal-flex {
  display: flex;
  gap: var(--wal-space-md);

  /* Direction */
  &.wal-flex-col  { flex-direction: column; }

  /* Wrapping */
  &.wal-flex-wrap { flex-wrap: wrap; }

  /* Alignment presets */
  &.wal-flex-center {
    align-items: center;
    justify-content: center;
  }

  &.wal-flex-between {
    justify-content: space-between;
    align-items: center;
  }

  &.wal-flex-end {
    justify-content: flex-end;
    align-items: center;
  }
}


/* ─── .wal-stack ─────────────────────────────────
   Vertical rhythm primitive.  Every direct child
   is separated by a consistent gap — the
   "lobotomized owl" approach, modernized with gap.
   ────────────────────────────────────────────── */
.wal-stack {
  display: flex;
  flex-direction: column;
  gap: var(--wal-space-lg);
}


/* ─── .wal-cluster ───────────────────────────────
   Horizontal cluster that wraps naturally.
   Perfect for tag lists, pill groups, and inline
   action bars.
   ────────────────────────────────────────────── */
.wal-cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wal-space-sm);
  align-items: center;
}


/* ─── .wal-sidebar ───────────────────────────────
   Two-panel layout: one fixed-width sidebar, one
   fluid content area. Wraps to a stacked column
   once the content panel can no longer hold its
   55% floor — flex-basis + flex-grow, no query.
   ────────────────────────────────────────────── */
.wal-sidebar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wal-space-xl);

  /* Not a query container, for the same reason as .wal-grid: an element
     cannot query itself. */

  /* Sidebar panel (first child by convention) */
  & > :first-child {
    flex-basis: 260px;
    flex-grow: 0;
    flex-shrink: 0;
  }

  /* Content panel fills remaining space */
  & > :last-child {
    flex-basis: 0;
    flex-grow: 999;
    min-width: 55%;
  }
}


/* ─── .wal-contain ──────────────────────────────
   Opt-in rendering isolation and off-screen skipping
   for long pages. Uses content-visibility: auto so the
   browser skips layout and paint of offscreen scenes
   until scrolled near the viewport.
   ────────────────────────────────────────────── */
.wal-contain {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

.wal-section {
  padding-block: var(--wal-space-3xl);
  border-top: var(--wal-border);

  /* First section on the page needs no border */
  &:first-child,
  &:first-of-type {
    border-top: none;
  }
}

}

/* ── components ── */
@layer components {
/* Do NOT wrap this file in `@layer components { }` — build.js wraps every layer
   file uniformly. Wrapping here too produced a nested sublayer
   (`components.components`) in dist, which outranks the outer `components`
   layer and made consumer `@layer components { }` overrides silently lose. */

  /* ═══ 1. Navbar ═══ */
  .wal-nav {
    position: sticky;
    top: 0;
    z-index: 100;
    background: color-mix(in oklch, var(--wal-bg) 80%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    /* min-height, not height, and allowed to wrap. At narrow widths the links
       drop to a second row instead of overlapping the brand and running off
       the page — which is what a fixed height plus nowrap did. Nothing is
       hidden and no JavaScript is involved; .wal-drawer is still there if you
       want a real mobile menu instead. */
    min-height: var(--wal-nav-h);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--wal-space-sm);
    padding-block: var(--wal-space-sm);
    padding-inline: var(--wal-space-md);
    transition: box-shadow var(--wal-duration) var(--wal-ease), border-bottom var(--wal-duration) var(--wal-ease);
    border-bottom: var(--wal-hairline) solid transparent;

    &.is-scrolled {
      border-bottom: var(--wal-border);
      box-shadow: var(--wal-shadow-sm);
    }

    /* The same rule and shadow, from the browser: a scroll-driven
       animation (below) brings it in over the first 1.5rem the page
       scrolls, so a masthead gains its rule once the page passes under it,
       with no scroll listener. It is drawn on ::after and stands down when
       a script sets .is-scrolled, so the two never draw two rules. */
    &::after {
      content: "";
      position: absolute;
      inset: auto 0 0;
      block-size: var(--wal-hairline);
      pointer-events: none;
      background: var(--wal-line);
      box-shadow: 0 0.4rem 0.8rem -0.3rem var(--wal-shadow-ink);
      opacity: 0;
      transition: opacity var(--wal-duration) var(--wal-ease);
    }

    &.is-scrolled::after { opacity: 0; animation: none; }

    .wal-nav-brand {
      display: flex;
      flex-direction: column;
      font-family: var(--wal-font-display);
      font-weight: 700;
      color: var(--wal-text);
      line-height: 1.2;
      white-space: nowrap;
      /* A masthead's name, not a link in running text: no underline, as the
         nav's own links have none at rest. */
      text-decoration: none;

      small {
        font-family: var(--wal-font-label);
        font-size: var(--wal-text-xs);
        font-weight: 400;
        color: var(--wal-text-muted);
        text-transform: uppercase;
        letter-spacing: 0.05em;
      }
    }

    .wal-nav-links {
      display: flex;
      gap: var(--wal-space-md);
      align-items: center;

      /* Scroll rather than wrap once the row itself is too long: a nav whose
         links reflow into three ragged lines is harder to scan than one you
         swipe. The scrollbar is suppressed because the row is 30px tall and a
         visible bar would take a third of it. */
      max-inline-size: 100%;
      overflow-x: auto;
      scrollbar-width: none;
      &::-webkit-scrollbar { display: none; }

      /* Scroll-spy from the browser, with no script, is in
         walnut-scroll.css; .is-active is the script's way, and
         aria-current="page" marks the page you are on. */

      a { white-space: nowrap; }

      a {
        color: var(--wal-text);
        text-decoration: none;
        font-family: var(--wal-font-display);
        font-weight: 500;
        font-size: var(--wal-text-sm);
        position: relative;
        padding: var(--wal-space-xs) 0;

        &::after {
          content: "";
          position: absolute;
          bottom: 0;
          left: 0;
          width: 100%;
          height: 2px;
          background-color: var(--wal-accent);
          transform: scaleX(0);
          transform-origin: right;
          transition: transform var(--wal-duration) var(--wal-ease);
        }

        &:hover::after,
        &.is-active::after,
        &[aria-current="page"]::after {
          transform: scaleX(1);
          transform-origin: left;
        }
      }
    }

    .wal-nav-actions {
      display: flex;
      gap: var(--wal-space-sm);
      align-items: center;
    }
  }

  /* Not a scroll-state container query, which says "stuck" exactly:
     Lightning CSS (Turbopack, Vite, Parcel, Bun) cannot parse one and
     rejects the whole stylesheet. Guarded, because without scroll
     timelines the animation would run once, at once, and leave the rule
     on from the top of the page. */
  @supports (animation-timeline: scroll()) {
    .wal-nav:not(.is-scrolled)::after {
      animation: wal-nav-rule linear both;
      animation-timeline: scroll();
      animation-range: 0 1.5rem;
    }
  }

  @keyframes wal-nav-rule {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* The nav is sticky, so whatever an in-page link brings to the top would
     land under it. Keep the nav's height clear. Only for a masthead, a nav
     straight under <body>: a sticky box sticks only within its parent, so a
     nav inside a section or a demo never covers the page. A nav that wraps
     to two rows is taller than --wal-nav-h: set scroll-padding-block-start
     on :root at that width, and an unlayered rule wins. */
  :root:has(> body > .wal-nav) { scroll-padding-block-start: var(--wal-nav-h); }

  /* ═══ 2. Hero ═══ */
  .wal-hero {
    min-height: 100vh;
    display: grid;
    place-items: center;
    position: relative;
    overflow: hidden;
    padding: var(--wal-space-2xl) var(--wal-space-md);

    .wal-hero-glow {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 100vw;
      height: 100vw;
      max-width: 1200px;
      max-height: 1200px;
      background: radial-gradient(circle, color-mix(in oklch, var(--wal-gold) 15%, transparent) 0%, transparent 60%);
      pointer-events: none;
      z-index: -1;
    }

    .wal-hero-content {
      text-align: center;
      max-width: 800px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: var(--wal-space-lg);
    }

    .wal-hero-name {
      font-family: var(--wal-font-display);
      font-size: clamp(3rem, 8vw, 6rem);
      font-weight: 800;
      line-height: 1.1;
      color: var(--wal-text);
      letter-spacing: -0.02em;
      margin: 0;
    }

    .wal-hero-tagline {
      font-family: var(--wal-font-typewriter);
      font-size: var(--wal-text-xl);
      color: var(--wal-text-soft);
      max-width: 600px;
      line-height: 1.6;
      margin: 0;
    }

    .wal-hero-actions {
      display: flex;
      gap: var(--wal-space-md);
      flex-wrap: wrap;
      justify-content: center;
      margin-top: var(--wal-space-md);
    }
  }

  /* ═══ 3. Buttons ═══ */
  .wal-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--wal-space-sm);
    font-family: var(--wal-font-display);
    font-weight: 700;
    font-size: var(--wal-text-sm);
    /* Own token, not --wal-radius-full directly: a consumer with square
       geometry can flatten buttons without also flattening chips and badges. */
    border-radius: var(--wal-btn-radius, var(--wal-radius-full));
    padding: 0.65rem 1.3rem;
    cursor: pointer;
    text-decoration: none;
    border: var(--wal-hairline) solid transparent;
    /* Listed, not `all`. In Chrome, any restyle of <html> — a rod, a
       palette, even a custom property nothing reads — started a
       scrollbar-color transition on every button, card and input: the
       property inherits, and `all` caught it. The first line is what the
       states below animate; the second is the rest of what `all` visibly
       animated, the base layer's :focus-visible ring and a finish switch's
       radius and tracking. */
    transition-property:
      background-color, border-color, color, box-shadow, transform, opacity;
    transition-duration: var(--wal-duration);
    transition-timing-function: var(--wal-ease);
    line-height: 1.2;

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

    &.wal-btn-sm {
      padding: 0.4rem 1rem;
      font-size: var(--wal-text-xs);
    }

    &.wal-btn-lg {
      padding: 0.85rem 1.75rem;
      font-size: var(--wal-text-base);
    }

    &.wal-btn-primary {
      background: var(--wal-accent);
      color: var(--wal-accent-fg);
      
      &:hover {
        background: var(--wal-accent-hover);
        transform: translateY(-2px);
        box-shadow: var(--wal-shadow-sm);
      }
      
      &:active {
        transform: translateY(0);
      }
    }

    &.wal-btn-secondary {
      background: transparent;
      border-color: var(--wal-line);
      color: var(--wal-text);

      /* The label is the accent used as type, so it takes the -ink form, as
         links do. The plain accent measured 3.8:1 on a dark ground. */
      &:hover {
        border-color: var(--wal-accent);
        color: var(--wal-accent-ink);
        background: var(--wal-bg-subtle);
      }
    }

    &.wal-btn-ghost {
      background: transparent;
      color: var(--wal-text);

      &:hover {
        background: var(--wal-bg-subtle);
      }
    }
    
    &:disabled,
    &[disabled] {
      opacity: 0.5;
      cursor: not-allowed;
      pointer-events: none;
    }
  }

  /* ═══ 4. Card ═══ */
  .wal-card {
    background: var(--wal-card-bg);
    color: var(--wal-card-ink);
    border: var(--wal-border);
    border-radius: var(--wal-radius);
    padding: var(--wal-space-lg);
    position: relative;
    overflow: hidden;
    /* Listed, not `all`; see .wal-btn. Background and ink are here because a
       palette switch moves them; the ring is for a card that is a link. */
    transition-property:
      background-color, border-color, color, box-shadow, transform,
      outline-color, outline-offset, outline-width, border-radius;
    transition-duration: var(--wal-duration);
    transition-timing-function: var(--wal-ease);
    display: flex;
    flex-direction: column;
    gap: var(--wal-space-md);

    /* Rebind the inherited ink scale to the card's own, so every descendant
       that reads --wal-text* flips with the surface automatically. See the
       cycle warning on --wal-card-ink in 01-tokens.css. */
    --wal-text:       var(--wal-card-ink);
    --wal-text-soft:  var(--wal-card-ink-soft);
    --wal-text-muted: var(--wal-card-ink-muted);

    &:hover {
      box-shadow: var(--wal-shadow);
      border-color: color-mix(in oklch, var(--wal-accent) 40%, var(--wal-line));
      transform: translateY(-2px);
    }
    
    @media (prefers-reduced-motion: reduce) {
      &:hover {
        transform: none;
      }
    }

    &:has(> img:first-child) {
      padding: 0;
      
      > img:first-child {
        width: 100%;
        height: auto;
        aspect-ratio: 16 / 9;
        object-fit: cover;
        border-bottom: var(--wal-border);
      }
      
      > *:not(img:first-child) {
        padding: 0 var(--wal-space-lg);
      }
      
      > *:last-child {
        padding-bottom: var(--wal-space-lg);
      }
    }

    .wal-card-flora {
      position: absolute;
      bottom: 0;
      right: 0;
      width: 150px;
      height: 150px;
      background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%23000000" fill-opacity="0.03" d="M10,90 Q50,90 50,50 T90,10 Q50,10 50,50 T10,90 Z"/></svg>');
      background-size: cover;
      background-repeat: no-repeat;
      pointer-events: none;
      z-index: 0;
    }
    
    > * {
      position: relative;
      z-index: 1;
    }
  }

  /* ═══ 5. Plate ═══
     A spec strip: a row of dt/dd cells divided by hairlines.

     Cells shed intrinsically via auto-fit rather than by container query. The
     old version set `container-type` on .wal-plate and then queried
     `@container` from inside it — an element cannot query itself, so those
     rules never matched and the plate never collapsed.

     The dividers are hairline `gap`s filled by each cell's own outline, so the
     hairlines land correctly at *any* wrap configuration. That removes the
     whole nth-child border dance the container queries used to need.

     The outline is what fixes a ragged last row. The previous version painted
     the CONTAINER line-coloured and let it show through the gaps — which meant
     any grid area with no cell in it (three items across two columns, say)
     rendered as a solid line-coloured block that looked like a fourth, empty
     spec. Now the container carries the cell colour and the cells draw their
     own dividers, so unfilled areas are simply blank. `outline` rather than
     `border` because it is drawn outside the box and does not affect layout,
     so adjacent cells' outlines meet inside the gap. */
  .wal-plate {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--wal-plate-min, 10rem)), 1fr));
    gap: var(--wal-hairline);
    background: var(--wal-plate-bg, var(--wal-card-bg));
    border: var(--wal-border);
    border-radius: var(--wal-radius);
    overflow: hidden;

    > div {
      background: var(--wal-plate-bg, var(--wal-card-bg));
      color: var(--wal-card-ink);
      outline: var(--wal-border);
      padding: var(--wal-space-md);
      display: flex;
      flex-direction: column;
      gap: var(--wal-space-xs);
    }

    dt {
      font-family: var(--wal-font-label);
      font-size: var(--wal-text-xs);
      color: var(--wal-card-ink-muted);
      text-transform: uppercase;
      letter-spacing: var(--wal-tracking-label);
      margin: 0;
    }

    dd {
      font-family: var(--wal-font-display);
      font-size: var(--wal-text-lg);
      font-weight: 700;
      color: var(--wal-card-ink);
      margin: 0;
    }
  }

  /* ═══ 6. Metric ═══ */
  .wal-metric {
    display: flex;
    flex-direction: column;
    gap: var(--wal-space-xs);
    padding: var(--wal-space-md);
    background: var(--wal-card-bg);
    color: var(--wal-card-ink);
    border: var(--wal-border);
    border-radius: var(--wal-radius);
    border-top: var(--wal-metric-border-width, 3px) solid var(--wal-metric-color, var(--wal-accent));

    .wal-metric-value {
      font-family: var(--wal-font-display);
      font-size: var(--wal-text-4xl);
      font-weight: 800;
      color: var(--wal-card-ink);
      line-height: 1.1;
    }

    .wal-metric-label {
      font-family: var(--wal-font-label);
      font-size: var(--wal-text-sm);
      color: var(--wal-card-ink-muted);
      text-transform: uppercase;
      letter-spacing: var(--wal-tracking-label);
    }
  }

  /* ═══ 7. Chip ═══ */
  .wal-chip {
    display: inline-flex;
    align-items: center;
    font-family: var(--wal-font-label);
    font-size: var(--wal-text-xs);
    padding: 0.2rem 0.6rem;
    /* Own token so a consumer with square geometry can flatten chips
       independently of buttons and badges. */
    border-radius: var(--wal-chip-radius, var(--wal-radius-full));
    border: var(--wal-border);
    background: var(--wal-chip-bg, var(--wal-surface));
    color: var(--wal-chip-color, var(--wal-text-soft));
    white-space: nowrap;
  }

  /* ═══ 8. Swatch ═══ */
  .wal-swatch {
    display: flex;
    flex-direction: column;
    border-radius: var(--wal-radius);
    overflow: hidden;
    border: var(--wal-border);
    background: var(--wal-card-bg);
    color: var(--wal-card-ink);

    .wal-swatch-color {
      height: var(--wal-swatch-h, 100px);
      width: 100%;
      background: var(--wal-swatch-bg, var(--wal-bg));
    }

    .wal-swatch-info {
      padding: var(--wal-space-sm) var(--wal-space-md);
      display: flex;
      flex-direction: column;
      gap: var(--wal-space-xs);
      
      .wal-swatch-name {
        font-family: var(--wal-font-display);
        font-weight: 600;
        font-size: var(--wal-text-sm);
        color: var(--wal-card-ink);
      }

      .wal-swatch-hex {
        font-family: var(--wal-font-mono);
        font-size: var(--wal-text-xs);
        color: var(--wal-card-ink-muted);
      }
    }
  }

  /* ═══ 9. Timeline ═══ */
  .wal-timeline {
    display: flex;
    flex-direction: column;

    .wal-timeline-item {
      display: grid;
      grid-template-columns: 150px 1fr auto;
      gap: var(--wal-space-md);
      padding: var(--wal-space-lg) 0;
      border-bottom: var(--wal-border);
      
      &:last-child {
        border-bottom: none;
      }

      .wal-timeline-date {
        font-family: var(--wal-font-label);
        font-size: var(--wal-text-sm);
        color: var(--wal-gold-ink);
        padding-top: var(--wal-space-xs);
      }

      .wal-timeline-content {
        display: flex;
        flex-direction: column;
        gap: var(--wal-space-xs);

        h3 {
          margin: 0;
          font-family: var(--wal-font-display);
          font-size: var(--wal-text-lg);
          color: var(--wal-text);
        }

        p {
          margin: 0;
          color: var(--wal-text-soft);
          font-family: var(--wal-font-body);
          line-height: 1.5;
        }

        ul {
          margin: var(--wal-space-sm) 0 0 0;
          padding-left: var(--wal-space-lg);
          color: var(--wal-text-soft);
          
          li {
            margin-bottom: var(--wal-space-xs);
          }
        }
      }

      .wal-timeline-tags {
        display: flex;
        flex-direction: column;
        gap: var(--wal-space-xs);
        align-items: flex-end;
      }
    }

    @media (max-width: 768px) {
      .wal-timeline-item {
        grid-template-columns: 1fr;
        gap: var(--wal-space-sm);
        
        .wal-timeline-tags {
          flex-direction: row;
          flex-wrap: wrap;
          align-items: flex-start;
          margin-top: var(--wal-space-sm);
        }
      }
    }
  }

  /* ═══ 10. Table ═══ */
  .wal-table {
    width: 100%;
    border-collapse: collapse;
    text-align: left;
    
    th {
      font-family: var(--wal-font-label);
      font-size: var(--wal-text-xs);
      text-transform: uppercase;
      letter-spacing: var(--wal-tracking-label);
      color: var(--wal-text-muted);
      padding: var(--wal-space-md) var(--wal-space-lg);
      border-bottom: var(--wal-border);
      font-weight: 400;
    }

    td {
      font-family: var(--wal-font-body);
      font-size: var(--wal-text-sm);
      color: var(--wal-text);
      padding: var(--wal-space-md) var(--wal-space-lg);
      border-bottom: var(--wal-hairline) solid var(--wal-line-soft);
    }

    tbody tr:nth-child(even) {
      background: var(--wal-bg-subtle);
    }
    
    tbody tr:hover {
      background: color-mix(in oklch, var(--wal-bg-subtle) 50%, var(--wal-surface));
    }
  }

  /* ═══ 11. Divider ═══ */
  .wal-divider {
    display: flex;
    align-items: center;
    text-align: center;
    color: var(--wal-gold);
    margin: var(--wal-space-xl) 0;

    &::before,
    &::after {
      content: "";
      flex: 1;
      border-bottom: var(--wal-hairline) solid transparent;
    }

    &::before {
      background: linear-gradient(to left, var(--wal-gold), transparent);
      height: var(--wal-hairline);
      margin-right: var(--wal-space-md);
    }

    &::after {
      background: linear-gradient(to right, var(--wal-gold), transparent);
      height: var(--wal-hairline);
      margin-left: var(--wal-space-md);
    }
    
    .wal-divider-icon {
      font-size: var(--wal-text-xs);
    }
  }

  /* ═══ 12. Drawer ═══ */
  .wal-drawer-overlay {
    position: fixed;
    inset: 0;
    background: color-mix(in oklch, var(--wal-bg) 50%, transparent);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    z-index: 1000;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--wal-duration) var(--wal-ease);
    
    &.is-open {
      opacity: 1;
      pointer-events: auto;
    }
  }

  .wal-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 320px;
    background: var(--wal-surface);
    border-left: var(--wal-border);
    box-shadow: var(--wal-shadow-lg);
    z-index: 1001;
    transform: translateX(100%);
    /* Off-canvas is not closed: a drawer that is only translated away keeps
       every link in the tab order, so a keyboard user tabbed into a panel
       they could not see. `visibility` takes it out of both the tab order and
       the accessibility tree, and flips only after the slide has finished. */
    visibility: hidden;
    transition: transform var(--wal-duration) var(--wal-ease),
                visibility 0s linear var(--wal-duration);
    display: flex;
    flex-direction: column;
    padding: var(--wal-space-lg);

    &.is-open {
      transform: translateX(0);
      visibility: visible;
      transition-delay: 0s;
    }

    .wal-drawer-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: var(--wal-space-xl);
      
      h2 {
        margin: 0;
        font-family: var(--wal-font-display);
        font-size: var(--wal-text-xl);
      }
    }

    .wal-drawer-nav {
      display: flex;
      flex-direction: column;
      gap: var(--wal-space-md);
      
      a {
        color: var(--wal-text);
        text-decoration: none;
        font-family: var(--wal-font-display);
        font-size: var(--wal-text-lg);
        padding: var(--wal-space-sm) 0;
        border-bottom: var(--wal-hairline) solid var(--wal-line-soft);
        /* On the link, not on :hover, so the indent eases back out as well. */
        transition: padding var(--wal-duration) var(--wal-ease), color var(--wal-duration) var(--wal-ease);

        &:hover {
          color: var(--wal-accent-ink);
          padding-left: var(--wal-space-sm);
        }
      }
    }
  }

  /* ═══ 13. Input ═══ */
  .wal-input {
    width: 100%;
    background: var(--wal-surface);
    border: var(--wal-hairline) solid var(--wal-line-input);
    border-radius: var(--wal-radius-sm);
    padding: 0.65rem 1rem;
    font-family: var(--wal-font-body);
    font-size: var(--wal-text-sm);
    color: var(--wal-text);
    /* Listed, not `all`; see .wal-btn. The outline is left out on purpose:
       the :focus one below is transparent, so easing it only faded a dark
       ring out of the text colour each time the input took focus. */
    transition-property:
      background-color, border-color, color, box-shadow,
      border-radius;
    transition-duration: var(--wal-duration);
    transition-timing-function: var(--wal-ease);

    &::placeholder {
      color: var(--wal-text-muted);
    }
    
    /* A transparent outline, not `none`: forced-colors mode discards the
       box-shadow ring and paints outlines in a system colour, so this is the
       one focus indicator that survives there. */
    &:focus {
      outline: 2px solid transparent;
      border-color: var(--wal-accent);
      box-shadow: 0 0 0 3px color-mix(in oklch, var(--wal-accent) 20%, transparent);
    }
    
    /* background-color, not the shorthand, which would also reset the
       select arrow's background-image. */
    &:disabled {
      background-color: var(--wal-bg-subtle);
      color: var(--wal-text-muted);
      cursor: not-allowed;
    }
  }

  textarea.wal-input {
    min-height: 100px;
    resize: vertical;
  }

  /* The arrow is two gradients meeting in a point, because a gradient can
     read currentColor and an SVG background cannot. The data-URI SVG this
     replaces was its own document, so its `currentColor` was always black,
     and the arrow disappeared on every dark ground. */
  select.wal-input {
    appearance: none;
    background-image:
      linear-gradient(45deg,  transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-size: 5px 5px;
    background-repeat: no-repeat;
    background-position: right calc(1rem + 5px) center, right 1rem center;
    padding-right: 2.5rem;
  }

  /* ═══ 13b. Field, label, checkbox, radio ═══
     .wal-input shipped without the furniture around it, so every consumer was
     inventing its own label and help-text styling. */

  .wal-field {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
  }

  /* Also usable as a bare <label>; the flex row is what puts a checkbox or
     radio on the same line as its text without extra wrapper markup. */
  .wal-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: var(--wal-font-body);
    font-size: var(--wal-text-sm);
    font-weight: 500;
    color: var(--wal-text);

    /* A required marker, opt-in via [data-required] so it needs no extra span.
       The empty alt text keeps screen readers from announcing "star": the
       input's own `required` is what says it. */
    &[data-required]::after {
      content: "*" / "";
      color: var(--wal-accent);
      font-weight: 700;
    }
  }

  .wal-hint {
    font-size: var(--wal-text-xs);
    color: var(--wal-text-muted);
  }

  .wal-error {
    font-size: var(--wal-text-xs);
    color: var(--wal-danger);
  }

  /* accent-color themes the native control in one declaration and keeps the
     platform's own focus ring, keyboard behaviour and indeterminate state —
     all of which a re-drawn appearance:none control has to reimplement badly. */
  .wal-check,
  .wal-radio {
    accent-color: var(--wal-accent);
    width: 1rem;
    height: 1rem;
    flex: none;
    cursor: pointer;

    &:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }
  }

  /* ═══ 14. Badge ═══
     Every variant is a cue's -soft as the fill and the SAME cue's -ink as the
     type. Using the plain cue for both is the classic failure: goldenrod on a
     goldenrod wash is a badge you cannot read. See the cue block in
     01-tokens.css for why the two forms exist. */
  .wal-badge {
    display: inline-flex;
    align-items: center;
    font-family: var(--wal-font-label);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0.15rem 0.5rem;
    /* Own token so a consumer with square geometry can flatten badges
       independently of buttons and chips. */
    border-radius: var(--wal-badge-radius, var(--wal-radius-full));
    background: var(--wal-surface-2);
    color: var(--wal-text-soft);
    font-weight: 600;

    &.wal-badge-accent {
      background: var(--wal-accent-soft);
      color: var(--wal-accent-ink);
    }

    &.wal-badge-bloom {
      background: var(--wal-bloom-soft);
      color: var(--wal-bloom-ink);
    }

    &.wal-badge-olive {
      background: var(--wal-olive-soft);
      color: var(--wal-olive-ink);
    }

    &.wal-badge-gold {
      background: var(--wal-gold-soft);
      color: var(--wal-gold-ink);
    }

    &.wal-badge-cool {
      background: var(--wal-cool-soft);
      color: var(--wal-cool-ink);
    }
  }

  /* ═══ 15. Dialog ═══
     Opens like a sheet drawn from a drawer: it rises the last few pixels into
     place as it fades in, and sinks back the same way as it closes.

     A <dialog> and a [popover] both start from display: none, which a
     transition has no "before" for. @starting-style supplies one. `display`
     and `overlay` transition discretely, so a closing sheet stays rendered,
     and in the top layer, until its exit has played. A browser without
     @starting-style shows it at once, as before.

     The closed state is scoped to <dialog> and [popover], so a .wal-dialog
     used as a plain styled box is never hidden by it. */
  .wal-dialog {
    padding: var(--wal-space-xl);
    border: var(--wal-border);
    border-radius: var(--wal-radius-lg);
    background: var(--wal-surface);
    color: var(--wal-text);
    box-shadow: var(--wal-shadow-lg);
    max-width: 500px;
    width: 90vw;
    margin: auto;
    transition:
      opacity   var(--wal-duration-slow) var(--wal-ease),
      translate var(--wal-duration-slow) var(--wal-ease),
      display   var(--wal-duration-slow) allow-discrete,
      overlay   var(--wal-duration-slow) allow-discrete;

    &:is(dialog, [popover]):not([open], :popover-open) {
      opacity: 0;
      translate: 0 0.75rem;
    }

    @starting-style {
      &:is([open], :popover-open) {
        opacity: 0;
        translate: 0 0.75rem;
      }
    }

    &::backdrop {
      background: color-mix(in oklch, var(--wal-bg) 60%, transparent);
      -webkit-backdrop-filter: blur(8px);
      backdrop-filter: blur(8px);
      transition:
        opacity var(--wal-duration-slow) var(--wal-ease),
        display var(--wal-duration-slow) allow-discrete,
        overlay var(--wal-duration-slow) allow-discrete;
    }

    &:is(dialog, [popover]):not([open], :popover-open)::backdrop {
      opacity: 0;
    }

    @starting-style {
      &:is([open], :popover-open)::backdrop {
        opacity: 0;
      }
    }
  }

  /* ═══ 16. Tooltip ═══
     The tooltip goes INSIDE its trigger, and the trigger points at it:

         <button class="wal-tooltip-trigger" aria-describedby="save-tip">Save
           <span class="wal-tooltip" id="save-tip" role="tooltip"
                 aria-hidden="true">Saves a draft</span>
         </button>

     aria-hidden keeps the tip out of the button's NAME, which is computed
     from its content ("Save Saves a draft" otherwise); aria-describedby
     still reads it, as the description.

     The trigger is its containing block, so plain offsets centre it with no
     anchor positioning — which an earlier version used, sharing one
     anchor-name that resolved to the LAST trigger on the page. Anything with
     `overflow: hidden` between the two clips it, .wal-card included.

     WCAG 1.4.13 asks for three things. Hoverable: the tip takes the pointer
     (no `pointer-events: none`) and ::before bridges the gap, so moving onto
     it keeps the trigger hovered — it is a descendant. Persistent: it stays
     until hover and focus leave. Dismissible: Escape, which CSS cannot hear,
     so it takes two lines of script that add .is-dismissed — see the README. */
  .wal-tooltip-trigger {
    position: relative;
  }

  .wal-tooltip {
    position: absolute;
    top: 100%;
    left: 50%;
    /* Hidden a quarter-rem up, tucked toward its trigger; it drops the rest
       of the way as it fades in, so it reads as coming from the trigger
       rather than appearing beside it. */
    translate: -50% -0.25rem;
    margin-top: var(--wal-space-sm);
    /* Sized to its text, not to the space left after `left: 50%`, which would
       wrap a short tip inside half the width of a small button. */
    inline-size: max-content;
    max-inline-size: 16rem;

    padding: var(--wal-space-xs) var(--wal-space-sm);
    background: var(--wal-surface-2);
    color: var(--wal-text);
    font-family: var(--wal-font-body);
    font-size: var(--wal-text-xs);
    /* Inside a .wal-btn it would otherwise inherit the button's weight,
       tracking and (under a finish) its case. */
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
    text-align: center;
    border: var(--wal-border);
    border-radius: var(--wal-radius-sm);
    box-shadow: var(--wal-shadow-sm);
    z-index: 1000;
    /* `visibility`, not just opacity: an invisible tip that still took the
       pointer would open itself when the cursor crossed where it will be. It
       flips after the fade, so the fade still plays on the way out. */
    visibility: hidden;
    opacity: 0;
    transition: opacity var(--wal-duration) var(--wal-ease),
                translate var(--wal-duration) var(--wal-ease),
                visibility 0s linear var(--wal-duration);

    /* The hover bridge across the margin. */
    &::before {
      content: "";
      position: absolute;
      inset-inline: 0;
      bottom: 100%;
      block-size: var(--wal-space-sm);
    }

    /* The arrow: a square turned 45° with the tip's own fill and two edges. */
    &::after {
      content: "";
      position: absolute;
      top: 0;
      left: 50%;
      inline-size: 8px;
      block-size: 8px;
      translate: -50% -50%;
      rotate: 45deg;
      background: var(--wal-surface-2);
      border-top: var(--wal-border);
      border-left: var(--wal-border);
    }
  }

  .wal-tooltip-trigger:is(:hover, :focus-visible) > .wal-tooltip {
    visibility: visible;
    opacity: 1;
    translate: -50% 0;
    transition-delay: 0s;
  }

  .wal-tooltip-trigger.is-dismissed > .wal-tooltip {
    visibility: hidden;
    opacity: 0;
  }

  /* ═══ 16b. Tip ═══
     The tooltip with no script and no nesting: a hint popover the browser
     opens on hover and focus, and closes on Escape.

         <button interestfor="save-tip">Save</button>
         <span class="wal-tip" id="save-tip" popover="hint">Saves a draft</span>

     interestfor gives all three of WCAG 1.4.13's asks (hoverable,
     persistent, dismissible) and the description, so .is-dismissed and its
     two lines of script are not needed. The tip lives in the top layer, so
     no `overflow: hidden` between it and its button can clip it, and it is
     anchored to the button that showed it: above, or below when there is no
     room above.

     A browser without interestfor never opens it, so keep anything the
     reader needs in the button's own name. .wal-tooltip still works
     everywhere. */
  .wal-tip {
    position-area: top;
    position-try-fallbacks: flip-block;
    inset: auto;
    margin: 0 0 0.45rem;
    padding: 0.35rem 0.6rem;
    border: 0;
    border-radius: var(--wal-radius-sm);
    background: var(--wal-text);
    color: var(--wal-bg);
    font: 500 var(--wal-text-xs)/1.3 var(--wal-font-body);
    white-space: nowrap;
    opacity: 0;
    translate: 0 0.25rem;
    transition:
      opacity   var(--wal-duration) var(--wal-ease),
      translate var(--wal-duration) var(--wal-ease),
      display   var(--wal-duration) allow-discrete,
      overlay   var(--wal-duration) allow-discrete;
  }

  .wal-tip:popover-open {
    opacity: 1;
    translate: 0 0;
  }

  @starting-style {
    .wal-tip:popover-open {
      opacity: 0;
      translate: 0 0.25rem;
    }
  }

  /* A beat before it opens, so a pointer crossing the button does not
     flicker a tip; a shorter one before it closes, so the pointer can
     travel onto it. */
  [interestfor] { interest-delay: 0.3s 0.15s; }

  /* ═══ 17. Eyebrow ═══ */
  .wal-eyebrow {
    font-family: var(--wal-eyebrow-font, var(--wal-font-label));
    font-size: var(--wal-text-xs);
    text-transform: uppercase;
    letter-spacing: var(--wal-tracking-label);
    color: var(--wal-eyebrow-color, var(--wal-accent-ink));
    margin-bottom: var(--wal-space-xs);
    display: block;
    font-weight: 600;
  }

  /* ═══ 18. Availability ═══ */
  .wal-avail {
    display: inline-flex;
    align-items: center;
    gap: var(--wal-space-sm);
    font-family: var(--wal-font-body);
    font-size: var(--wal-text-sm);
    color: var(--wal-text-soft);

    .wal-avail-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background-color: var(--wal-success);
      box-shadow: 0 0 0 0 color-mix(in oklch, var(--wal-success) 70%, transparent);
      animation: wal-ping 2s infinite var(--wal-ease);
    }
  }

  @keyframes wal-ping {
    0% {
      box-shadow: 0 0 0 0 color-mix(in oklch, var(--wal-success) 70%, transparent);
    }
    70% {
      box-shadow: 0 0 0 6px color-mix(in oklch, var(--wal-success) 0%, transparent);
    }
    100% {
      box-shadow: 0 0 0 0 color-mix(in oklch, var(--wal-success) 0%, transparent);
    }
  }


  /* ═══ 19. Footer ═══
     End-credits colophon: a top rule and a row of role/value pairs. */
  .wal-footer {
    border-top: var(--wal-border);
    padding-block: var(--wal-space-xl) var(--wal-space-2xl);
  }

  .wal-footer-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--wal-footer-min, 11rem)), 1fr));
    gap: var(--wal-space-md);
    font-family: var(--wal-font-label);
    font-size: var(--wal-text-xs);
    color: var(--wal-text-soft);

    dt {
      text-transform: uppercase;
      letter-spacing: calc(var(--wal-tracking-label) * 1.3);
      font-size: var(--wal-text-xs);
      color: var(--wal-text-muted);
      margin: 0 0 var(--wal-space-xs);
    }

    dd {
      margin: 0;
      color: var(--wal-text);
    }
  }

  /* ═══ 20. Icon button ═══
     Circular icon-only control. Pairs with .wal-btn in an action cluster. */
  .wal-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--wal-iconbtn-size, 38px);
    height: var(--wal-iconbtn-size, 38px);
    border-radius: var(--wal-radius-full);
    border: var(--wal-border);
    background: transparent;
    color: var(--wal-text-soft);
    cursor: pointer;
    text-decoration: none;
    transition: color var(--wal-duration) var(--wal-ease),
                border-color var(--wal-duration) var(--wal-ease);

    &:hover {
      color: var(--wal-accent);
      border-color: var(--wal-accent);
    }
  }

  /* ═══ 21. Arrow link ═══
     Text link whose gap opens on hover, so the arrow steps away from the word. */
  .wal-link-arrow {
    display: inline-flex;
    align-items: center;
    gap: var(--wal-space-xs);
    font-family: var(--wal-font-display);
    font-weight: 600;
    font-size: var(--wal-text-sm);
    color: var(--wal-accent-ink);
    text-decoration: none;
    transition: gap var(--wal-duration) var(--wal-ease);

    &:hover { gap: var(--wal-space-sm); }
  }

  /* ═══ 22. Stat ═══
     A metric without the box — for stat rows inside an existing card. */
  .wal-stat {
    .wal-stat-value {
      font-family: var(--wal-font-display);
      font-weight: 600;
      font-size: var(--wal-text-3xl);
      line-height: 1;
      color: var(--wal-stat-color, var(--wal-accent-ink));
    }

    .wal-stat-label {
      font-family: var(--wal-font-label);
      font-size: var(--wal-text-xs);
      text-transform: uppercase;
      letter-spacing: var(--wal-tracking-label);
      color: var(--wal-text-muted);
      margin-top: var(--wal-space-xs);
    }
  }

  /* ═══ 23. Note ═══
     A titled block under a coloured top rule. No box, no fill. */
  .wal-note {
    border-top: 3px solid var(--wal-note-color, var(--wal-accent));
    padding-top: var(--wal-space-md);

    h4 {
      font-family: var(--wal-font-display);
      font-weight: 600;
      margin: 0 0 var(--wal-space-sm);
    }
  }

  /* ═══ 24. Dash list ═══
     Bulleted list whose marker is a short rule rather than a disc. */
  .wal-list-dash {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--wal-space-sm);

    li {
      position: relative;
      padding-left: 17px;
      color: var(--wal-text-soft);

      &::before {
        content: "";
        position: absolute;
        left: 0;
        top: 0.6em;
        width: 7px;
        height: 2px;
        background: var(--wal-list-marker, var(--wal-gold));
      }
    }
  }

  /* ═══ 25. Frame ═══
     A fixed-ratio letterboxed window with an optional caption rail — the
     "shot" device. Ratio is a token so a consumer can pick 2.39, 1.85, 1.43. */
  .wal-frame {
    margin: 0;

    .wal-frame-window {
      aspect-ratio: var(--wal-frame-ratio, 1.43);
      overflow: hidden;
      position: relative;
      background: var(--wal-frame-bg, var(--wal-bg-subtle));
      border: var(--wal-border);
    }

    figcaption {
      display: flex;
      justify-content: space-between;
      gap: var(--wal-space-md);
      padding-top: var(--wal-space-sm);
      font-family: var(--wal-font-label);
      font-size: var(--wal-text-xs);
      letter-spacing: calc(var(--wal-tracking-label) * 1.6);
      text-transform: uppercase;
      color: var(--wal-text-muted);
    }
  }

  /* ═══ 26. Colour script ═══
     The palette, printed. A catalogue shows its own ink chart, and a design
     system that renders its cues on the page treats them as content rather
     than as decoration — which is the fastest way to notice a cue has drifted.

     Zero-config form: five empty <li>s.

         <ol class="wal-script">
           <li></li><li></li><li></li><li></li><li></li>
         </ol>

     Each cell falls back to --wal-cue-N by position, so the strip is correct
     without a single inline style. Override any cell with `--wal-cue:` when you
     want a specific colour rather than the script in order, and put
     <b>name</b> / <small>value</small> inside for a caption. */
  .wal-script {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--wal-script-min, 8rem)), 1fr));
    gap: var(--wal-space-sm);
    counter-reset: wal-cue;

    > * {
      counter-increment: wal-cue;
      display: flex;
      flex-direction: column;
      gap: var(--wal-space-xs);
      margin: 0;

      /* The swatch itself. A hairline rather than a shadow, because a colour
         chart is printed, and a border in the cue's own colour would hide
         exactly the edge you are trying to judge. */
      &::before {
        content: "";
        display: block;
        block-size: var(--wal-script-h, 4.5rem);
        background: var(--wal-cue, var(--wal-surface-2));
        border: var(--wal-border);
        border-radius: var(--wal-radius-sm);
      }

      /* Cue number, from the counter — never hand-typed, so reordering the
         strip cannot leave the numbering behind. */
      &::after {
        content: counter(wal-cue, decimal-leading-zero);
        font-family: var(--wal-font-label);
        font-size: var(--wal-text-xs);
        letter-spacing: var(--wal-tracking-label);
        color: var(--wal-text-muted);
        order: -1;
      }

      b {
        font-family: var(--wal-font-display);
        font-weight: 600;
        font-size: var(--wal-text-sm);
        color: var(--wal-text);
      }

      small {
        font-family: var(--wal-font-mono);
        font-size: var(--wal-text-xs);
        color: var(--wal-text-muted);
        overflow-wrap: anywhere;
      }
    }

    /* Positional defaults. Inline `--wal-cue` beats these, because an inline
       style outranks any layered rule regardless of specificity. */
    > *:nth-child(1) { --wal-cue: var(--wal-cue-1); }
    > *:nth-child(2) { --wal-cue: var(--wal-cue-2); }
    > *:nth-child(3) { --wal-cue: var(--wal-cue-3); }
    > *:nth-child(4) { --wal-cue: var(--wal-cue-4); }
    > *:nth-child(5) { --wal-cue: var(--wal-cue-5); }
  }

  /* ═══ 27. Rule ═══
     A hairline with a label sitting in it — the catalogue's section divider.
     Distinct from .wal-divider, which is the gold flourish: this one is flat,
     square and structural.

         <div class="wal-rule"><span>Plates 1–8</span></div>

     With no child it is a plain full-width hairline. */
  .wal-rule {
    display: flex;
    align-items: center;
    gap: var(--wal-space-md);
    margin-block: var(--wal-space-lg);
    font-family: var(--wal-font-label);
    font-size: var(--wal-text-xs);
    text-transform: uppercase;
    letter-spacing: calc(var(--wal-tracking-label) * 1.6);
    color: var(--wal-rule-color, var(--wal-text-muted));

    &::before,
    &::after {
      content: "";
      flex: 1;
      block-size: var(--wal-hairline);
      background: var(--wal-line);
    }

    /* Label to the left by default — a catalogue reads down the left margin.
       .wal-rule-center puts it in the middle instead. */
    &::before { flex: 0 0 var(--wal-space-xl); }
    &.wal-rule-center::before { flex: 1; }
    &:empty::before { flex: 1; }
  }

  /* ═══ 28. Caption ═══
     The label voice: mono, uppercase, widely tracked. Figure captions, plate
     notes, table sources, the line under a photograph. */
  .wal-caption {
    font-family: var(--wal-font-label);
    font-size: var(--wal-text-xs);
    text-transform: uppercase;
    letter-spacing: calc(var(--wal-tracking-label) * 1.6);
    color: var(--wal-caption-color, var(--wal-text-muted));
    margin: 0;
    /* A caption is usually a <p>, and base caps <p> at 65ch — measured in
       the caption's own tiny, widely tracked face, which is under 30rem. A
       one-line standfirst broke after its fourth word and left the last two
       on a line of their own. A caption takes its container's measure. */
    max-width: none;
  }

  /* ═══ 29. Index ═══
     A plate number. Tabular figures so a column of them aligns on the digit
     rather than on the glyph, which is the difference between a catalogue and
     a list of numbers. */
  .wal-index {
    font-family: var(--wal-font-label);
    font-size: var(--wal-index-size, var(--wal-text-sm));
    font-variant-numeric: tabular-nums;
    letter-spacing: calc(var(--wal-tracking-label) * 1.6);
    color: var(--wal-index-color, var(--wal-accent-ink));
    margin: 0;
  }

  /* ═══ 30. Section head ═══
     Index, title and standfirst as one block, with the index in a left margin
     exactly as wide as it needs. No breakpoint: see the column note below.

         <header class="wal-section-head">
           <p class="wal-index">01</p>
           <h2>Colour</h2>
           <p class="wal-caption">Five cues and a ground</p>
         </header> */
  .wal-section-head {
    display: grid;
    /* `max-content minmax(0, 1fr)`: the index column is exactly as wide as
       "01" needs and the content column can shrink to zero, so this needs no
       breakpoint and cannot overflow at any width. A fixed floor on the
       content column (minmax(20rem, 1fr), say) would bring back both. */
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--wal-space-xs) var(--wal-space-lg);
    align-items: baseline;
    margin-bottom: var(--wal-space-xl);

    .wal-index { grid-column: 1; grid-row: 1; }
    > h2, > h3 { grid-column: 2; margin: 0; }
    .wal-caption { grid-column: 2; }
  }

  /* ═══ 31. Desk ═══
     The surface the page's papers lie on.

         <html class="wal-press wal-desk">

     .wal-desk lays a walnut grain under the page and lifts the papers on it —
     cards, plates, metrics — a resting shadow off it. The grain is the
     ground rod a few points darker (--wal-desk-ink), so it re-stains with
     the script.

     It holds still. A version that shifted the grain a few pixels with the
     pointer was built, measured and taken out: even at 3–10px, a whole
     window's worth of texture moving read as dizzying rather than as depth.
     The resting shadows carry the depth instead, and they never move.

     On <html> or <body> only. The grain is fixed to the window, so papers
     scroll across the desk rather than with it. */
  .wal-desk::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: var(--wal-desk-ink);
    mask: var(--wal-desk-grain) 0 0 / 640px repeat;
  }

  /* Papers rest a shadow above the desk. :where() holds this to one class's
     specificity, so .wal-card:hover still lifts to --wal-shadow and a
     .wal-shadow-none utility still wins; a paper inside another paper rests
     on that one, and casts nothing. Any other element joins the desk with
     the .wal-shadow-sm utility.

     `*:is`, not ` :is`: the minifier strips the space before a colon, which
     would turn this descendant selector into "a .wal-desk that is a card"
     (see build.js). */
  :where(.wal-desk) *:is(.wal-card, .wal-plate, .wal-metric):where(:not(:is(.wal-card, .wal-plate, .wal-metric) *)) {
    box-shadow: var(--wal-shadow-sm);
  }

  @media print {
    .wal-desk::before { display: none; }
  }

  /* ═══ 32. Stock ═══
     What the papers are printed on. A finish may give their stock a tooth —
     --wal-tooth, a grey texture multiplied into each paper's own colour, so
     it carries no colour of its own — and Catalogue does (07-finish.css).
     Unset, this is `none` and the papers are flat fills, as before.

     Only on a paper showing its own stock. The .wal-fill-* and .wal-bg-*
     utilities repaint a paper's colour, not its image, so they would keep a
     tooth multiplied into ink it was never measured against: gold's dark
     foreground on a toothed gold fill fell to 4.3:1. A repainted paper is
     a solid surface, and stays flat.

     The specificity (0,2,1) is above the papers' own `background`
     shorthands, so it outranks them for this one longhand. */
  :is(.wal-card, .wal-metric, .wal-plate > div):not([class*="wal-fill-"], [class*="wal-bg-"]) {
    background-image: var(--wal-tooth, none);
    background-blend-mode: multiply;
  }

  /* ═══ 33. Duotone ═══
     Any photograph, printed in two inks: a deep shade of one of the
     palette's cues, and paper.

         <figure class="wal-duotone" data-ink="cool">
           <img src="…" alt="…">
         </figure>

     data-ink is accent (the default), bloom, gold, olive or cool. A new
     palette or mode regrades every print with no work from the page, so a
     stock photograph is always on palette, and the same picture can sit in
     two sections graded two ways.

     A true gradient map, in every browser, from two blend modes. The image
     goes grey and is screened over the ink's deep shade, so its shadows take
     the ink and its highlights stay light; then the paper is multiplied over
     the lot, which tints the highlights and leaves the shadows alone. The
     paper is warm white by day and greyed by night, so a dark page is not
     lit by a white rectangle.

     The figure sizes to its image. Give it a block size or an aspect-ratio
     and the image covers it. `overflow: clip`, not hidden: hidden makes a
     scroll container, and a scroll-driven animation inside, such as
     .wal-reveal-develop, would read its timeline off the figure instead of
     the page. */
  .wal-duotone {
    --wal-duotone-ink: var(--wal-accent);
    position: relative;
    isolation: isolate;
    overflow: clip;
    margin: 0;
    background-color: color-mix(in oklch, var(--wal-duotone-ink) 58%, oklch(0.18 0.02 50));

    &[data-ink="bloom"] { --wal-duotone-ink: var(--wal-bloom); }
    &[data-ink="gold"]  { --wal-duotone-ink: var(--wal-gold); }
    &[data-ink="olive"] { --wal-duotone-ink: var(--wal-olive); }
    &[data-ink="cool"]  { --wal-duotone-ink: var(--wal-cool); }

    img {
      display: block;
      inline-size: 100%;
      block-size: 100%;
      object-fit: cover;
      filter: grayscale(1) contrast(1.08);
      mix-blend-mode: screen;
    }

    &::after {
      content: "";
      position: absolute;
      inset: 0;
      pointer-events: none;
      background-color: color-mix(in oklch, var(--wal-duotone-ink) 16%,
                                  light-dark(oklch(0.97 0.01 85), oklch(0.86 0.02 85)));
      mix-blend-mode: multiply;
    }
    /* No transition of its own: under .wal-retune the rods already carry
       the ink across, and a transition on top would restart every frame
       and trail behind the rest of the page. */
  }

  /* ═══ 34. Chest ═══
     A plan chest: drawers in the flow of the page. Each drawer is a
     <details>; its front is the summary, and the drawer slides out of the
     slot beneath it while the rows below make room. Its contents ride with
     it, so they come out of the slot rather than unrolling.

         <div class="wal-chest">
           <details name="colourways">
             <summary>Forest</summary>
             <div>Everything in the drawer.</div>
           </details>
           …
         </div>

     A shared `name` makes them one chest, where pulling out a drawer pushes
     the open one home; leave it off and any number can be out. The browser
     owns the rest: the keyboard, the open state, find-in-page reaching into
     a shut drawer.

     The drawer moves on the four opening tokens (01-tokens.css), so a
     material changes how it lands: data-wal-material="walnut" on the chest
     and a drawer hits its stop and knocks back before it settles. A browser
     without ::details-content opens the drawer at once, as a plain
     <details> does. */
  .wal-chest {
    display: grid;
    border-block-start: var(--wal-border);

    > details {
      interpolate-size: allow-keywords;
      border-block-end: var(--wal-border);
      background: var(--wal-surface);
      color: var(--wal-text);
    }

    summary {
      list-style: none;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: var(--wal-space-md);
      padding: var(--wal-space-md) var(--wal-space-lg);
      transition: background-color var(--wal-duration) var(--wal-ease);

      &::-webkit-details-marker { display: none; }
      &:hover { background: var(--wal-surface-2); }

      &:focus-visible {
        outline: 2px solid var(--wal-accent);
        outline-offset: -2px;
      }

      /* The pull: a finger slot routed into the front. It fills with the
         accent while its drawer is out. */
      &::after {
        content: "";
        flex: none;
        margin-inline-start: auto;
        inline-size: 2.75rem;
        block-size: 0.5rem;
        border-radius: var(--wal-radius-full);
        background: var(--wal-bg-subtle);
        box-shadow: inset 0 1px 2px var(--wal-shadow-ink),
                    0 1px 0 color-mix(in oklch, var(--wal-surface), white 40%);
        transition: background-color var(--wal-duration) var(--wal-ease);
      }
    }

    details[open] > summary::after { background: var(--wal-accent); }

    /* The drawer is the gap that opens under the front: the details'
       content box, from no height to its own. interpolate-size lets a
       transition reach `auto`, and content-visibility, the property that
       hides a shut drawer, flips at the end of the close instead of the
       start, so the contents stay drawn until the drawer is home. */
    details::details-content {
      block-size: 0;
      overflow-y: clip;
      transition:
        block-size         var(--wal-close-duration) var(--wal-close-ease),
        content-visibility var(--wal-close-duration) allow-discrete;
    }

    details[open]::details-content {
      block-size: auto;
      transition-duration: var(--wal-open-duration);
      transition-timing-function: var(--wal-open-ease);
    }

    /* Only where the drawer clips: without that, contents sliding down
       from 2.5rem up would pass over the front. */
    @supports selector(::details-content) {
      details > :not(summary) {
        background: var(--wal-bg-subtle);
        /* The cabinet's edge shades the top of the drawer; its back is a
           lip. */
        box-shadow: inset 0 1.1rem 1rem -1rem var(--wal-shadow-ink),
                    inset 0 -3px 0 color-mix(in oklch, var(--wal-surface), var(--wal-text) 12%);
        translate: 0 -2.5rem;
        transition: translate var(--wal-close-duration) var(--wal-close-ease);
      }

      details[open] > :not(summary) {
        translate: 0 0;
        transition-duration: var(--wal-open-duration);
        transition-timing-function: var(--wal-open-ease);
      }

      /* A shut drawer's contents are not styled at all, so there is no
         "before" for the slide. This is it. */
      @starting-style {
        details[open] > :not(summary) { translate: 0 -2.5rem; }
      }
    }
  }

  /* ═══ 35. Door ═══
     A sliding door: a side panel that is a <dialog>, so it needs no script.

         <button commandfor="cart" command="show-modal">Cart</button>
         <dialog class="wal-door" id="cart" closedby="any" aria-label="Cart">
           <button class="wal-door-pull" commandfor="cart" command="close"
                   aria-label="Close"></button>
           <div class="wal-door-body">…</div>
         </dialog>

     commandfor opens and closes it, and closedby="any" lets a click outside
     or Escape close it, so focus, the inert page behind and the Escape key
     are all the browser's. It runs in on a track from the right and throws
     its shadow across the page; on a phone it rises from below. The
     leading edge is its pull, which closes it. With walnut-motion.js the
     pull can be dragged as well: the door follows the hand, and let go a
     third of the way out, or flicked, it shuts.

     This is .wal-drawer without the script that drawer needs for its
     classes and its focus. Like the chest, it moves on the four opening
     tokens, so it takes a material. */
  .wal-door {
    position: fixed;
    inset: 0 0 0 auto;
    margin: 0;
    padding: 0;
    border: 0;
    inline-size: min(30rem, 92vw);
    max-inline-size: none;
    block-size: 100dvh;
    max-block-size: none;
    background: var(--wal-surface);
    color: var(--wal-text);
    box-shadow: -1px 0 0 var(--wal-line), -2.5rem 0 3rem -2rem var(--wal-shadow-ink-lg);
    /* The grid is set shut as well as open, so the door keeps its shape
       while it slides away. */
    display: none;
    grid-template-columns: 1.5rem minmax(0, 1fr);
    translate: 100% 0;
    transition:
      translate var(--wal-close-duration) var(--wal-close-ease),
      display   var(--wal-close-duration) allow-discrete,
      overlay   var(--wal-close-duration) allow-discrete;

    &[open] {
      display: grid;
      translate: 0 0;
      transition-duration: var(--wal-open-duration);
      transition-timing-function: var(--wal-open-ease);
    }

    /* The shadow the door throws, darkest at the door. A dim in the
       ground's hue rather than a wash of the page colour: the door is
       solid, and what it shades is the room. */
    &::backdrop {
      background: linear-gradient(to left,
        oklch(0.18 0.02 var(--wal-hue) / 0.38),
        oklch(0.18 0.02 var(--wal-hue) / 0.12) 55%,
        oklch(0.18 0.02 var(--wal-hue) / 0.05));
      opacity: 0;
      transition:
        opacity var(--wal-close-duration) var(--wal-ease),
        display var(--wal-close-duration) allow-discrete,
        overlay var(--wal-close-duration) allow-discrete;
    }

    &[open]::backdrop {
      opacity: 1;
      transition-duration: var(--wal-open-duration);
    }

    /* Taken in hand by its pull (walnut-motion.js): the door goes where
       the hand puts it, so nothing eases it, and its shadow thins with the
       share of it that is out (--wal-door-out, 0 to 1). */
    &[data-wal-held] {
      transition: none;
      user-select: none;
    }

    &[data-wal-held]::backdrop {
      transition: none;
      opacity: calc(1 - var(--wal-door-out, 0));
    }
  }

  @starting-style {
    .wal-door[open] { translate: 100% 0; }
    .wal-door[open]::backdrop { opacity: 0; }
  }

  .wal-door-pull {
    grid-row: 1 / -1;
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    cursor: pointer;
    /* A finger on the pull drags the door, not the page. */
    touch-action: none;
    background: color-mix(in oklch, var(--wal-surface), var(--wal-text) 9%);
    box-shadow: inset -1px 0 0 var(--wal-line);

    [data-wal-held] > & { cursor: grabbing; }

    /* The handle: a long slot, the height of a hand. */
    &::before {
      content: "";
      inline-size: 0.45rem;
      block-size: 4.5rem;
      border-radius: var(--wal-radius-full);
      background: color-mix(in oklch, var(--wal-surface), var(--wal-text) 24%);
      box-shadow: inset 1px 1px 3px var(--wal-shadow-ink);
      transition: background-color var(--wal-duration) var(--wal-ease);
    }

    &:hover::before { background: var(--wal-accent); }

    &:focus-visible {
      outline: 2px solid var(--wal-accent);
      outline-offset: -4px;
    }
  }

  .wal-door-body {
    overflow: auto;
    overscroll-behavior: contain;
    padding: var(--wal-space-xl) var(--wal-space-lg);
  }

  @media (max-width: 40rem) {
    .wal-door {
      inset: auto 0 0;
      inline-size: 100%;
      block-size: min(88dvh, 44rem);
      border-radius: var(--wal-radius-lg) var(--wal-radius-lg) 0 0;
      box-shadow: 0 -1px 0 var(--wal-line), 0 -2.5rem 3rem -2rem var(--wal-shadow-ink-lg);
      translate: 0 100%;
      grid-template-columns: none;
      grid-template-rows: 1.5rem minmax(0, 1fr);

      &::backdrop {
        background: linear-gradient(to top,
          oklch(0.18 0.02 var(--wal-hue) / 0.38),
          oklch(0.18 0.02 var(--wal-hue) / 0.08));
      }
    }

    @starting-style {
      .wal-door[open] { translate: 0 100%; }
    }

    .wal-door-pull {
      grid-row: auto;
      border-radius: inherit;
      box-shadow: inset 0 -1px 0 var(--wal-line);

      &::before {
        inline-size: 4.5rem;
        block-size: 0.45rem;
      }
    }
  }

  /* ═══ 36. Window ═══
     A page inside the page. The <dialog> is the room the window opens in:
     the whole viewport, transparent, so a movement can start from anywhere
     on screen. The window itself is the panel inside it.

         <dialog class="wal-window" id="dusk" closedby="any" aria-labelledby="dusk-title">
           <div class="wal-window-panel">…</div>
         </dialog>

     On its own it opens like .wal-dialog: the panel rises the last few
     pixels into place. walnut-motion.js gives it a movement from the
     button that opened it, the gatefold (37). */
  .wal-window {
    position: fixed;
    inset: 0;
    margin: 0;
    padding: var(--wal-space-lg);
    border: 0;
    box-sizing: border-box;
    inline-size: 100%;
    block-size: 100%;
    max-inline-size: none;
    max-block-size: none;
    background: none;
    color: var(--wal-text);
    overflow: auto;
    overscroll-behavior: contain;
    display: none;
    place-items: center;
    transition:
      display var(--wal-duration) allow-discrete,
      overlay var(--wal-duration) allow-discrete;

    &[open] { display: grid; }

    /* A movement has already played the exit, so the window goes at once. */
    &.is-closing { transition: none; }

    /* A dim, not a wash: the window may wear another palette from the page
       behind it, so the backdrop takes none of the window's colours. */
    &::backdrop {
      background: oklch(0.16 0.015 var(--wal-hue) / 0.36);
      -webkit-backdrop-filter: blur(5px) saturate(0.8);
      backdrop-filter: blur(5px) saturate(0.8);
      opacity: 0;
      transition:
        opacity var(--wal-close-duration) var(--wal-ease),
        display var(--wal-close-duration) allow-discrete,
        overlay var(--wal-close-duration) allow-discrete;
    }

    &[open]::backdrop {
      opacity: 1;
      transition-duration: var(--wal-open-duration);
    }

    /* walnut-motion.js sets --wal-closing to the length of its movement, so
       the dim lifts over the whole of it rather than before it is done,
       evenly, as the cover glides home. */
    &.is-closing::backdrop {
      opacity: 0;
      transition-duration: var(--wal-closing, calc(var(--wal-close-duration) * 0.8));
      transition-timing-function: var(--wal-ease-glide);
    }
  }

  @starting-style {
    .wal-window[open]::backdrop { opacity: 0; }
  }

  .wal-window-panel {
    position: relative;
    inline-size: min(40rem, 100%);
    box-sizing: border-box;
    background: var(--wal-surface);
    color: var(--wal-text);
    border-radius: var(--wal-radius-lg);
    box-shadow: var(--wal-shadow-lg);
  }

  /* The panel's own rise, when no movement is playing. */
  .wal-window:not([data-wal-moving]) .wal-window-panel {
    transition:
      opacity   var(--wal-duration-slow) var(--wal-ease),
      translate var(--wal-duration-slow) var(--wal-ease);
  }

  .wal-window:not([data-wal-moving]):not([open]) .wal-window-panel {
    opacity: 0;
    translate: 0 0.75rem;
  }

  @starting-style {
    .wal-window:not([data-wal-moving])[open] .wal-window-panel {
      opacity: 0;
      translate: 0 0.75rem;
    }
  }

  /* ═══ 37. Gatefold ═══
     A window that opens like a gatefold sleeve: its cover travels from the
     card that opened it, and the notes unfold from behind the cover on the
     spine.

         <button commandfor="dusk" command="show-modal">
           <span data-wal-art>…the card's picture…</span> Dusk
         </button>
         <dialog class="wal-window" data-wal-open="gatefold" id="dusk" closedby="any">
           <div class="wal-gatefold-spread">
             <figure class="wal-gatefold-cover">…the same picture, larger…</figure>
             <div class="wal-gatefold-notes">…</div>
           </div>
         </dialog>

     The movement is walnut-motion.js; without it, or under reduced motion,
     the spread opens as any .wal-window does. The cover's shape is
     --wal-cover-ratio, 10 / 11 unless you set it, and needn't be the card
     picture's ([data-wal-art]): the cover never stretches, but leaves at
     one scale cropped to the picture, and the crop opens as it lands.
     --wal-cover-focus (50% 50%, read as object-position is) says which part
     of the cover the picture is. The notes are the cover's size and scroll
     inside it, and fold out from behind the cover. On a phone the spread
     stacks and the notes fold down from behind it. */
  /* --wal-cover-ratio is read with its default rather than declared here,
     so it can be set on the dialog, or anywhere above it. */
  .wal-gatefold-spread {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* Two covers wide, and never taller than the screen. */
    inline-size: min(60rem, 100%, calc((100dvh - 3rem) * 2 * var(--wal-cover-ratio, 10 / 11)));
    perspective: 1800px;
    filter: drop-shadow(0 1.6rem 1.8rem var(--wal-shadow-ink-lg));
    will-change: filter;
  }

  /* The cover lies over the notes, which fold out from behind it. */
  .wal-gatefold-cover {
    position: relative;
    z-index: 1;
    margin: 0;
    aspect-ratio: var(--wal-cover-ratio, 10 / 11);
    transform-origin: 0 0;
    overflow: hidden;
    border-radius: var(--wal-radius-lg) 0 0 var(--wal-radius-lg);
    background: var(--wal-surface-2);
  }

  .wal-gatefold-notes {
    position: relative;
    transform-origin: 0 50%;
    backface-visibility: hidden;
    contain: size;
    background: var(--wal-surface);
    border-radius: 0 var(--wal-radius-lg) var(--wal-radius-lg) 0;
    box-shadow: inset 1px 0 0 var(--wal-line-soft);
    overflow: auto;
    overscroll-behavior: contain;

    /* The light across the page as it turns. The script fades it out as the
       notes come down flat. */
    &::after {
      content: "";
      position: absolute;
      inset: 0;
      pointer-events: none;
      opacity: 0;
      border-radius: inherit;
      background: linear-gradient(90deg, oklch(0.15 0.02 60 / 0.55), oklch(0.15 0.02 60 / 0.12) 45%, oklch(1 0 0 / 0.1));
    }
  }

  /* Each half moves as a transform on its own layer. Kept on that layer while
     the window is open: dropped the moment the notes land, their page would
     be painted again in that frame, and the landing is the frame where a
     stutter shows. */
  @media (prefers-reduced-motion: no-preference) {
    .wal-window[open] :is(.wal-gatefold-cover, .wal-gatefold-notes) {
      will-change: transform, clip-path;
    }
  }

  @media (max-width: 44rem) {
    .wal-gatefold-spread {
      grid-template-columns: minmax(0, 1fr);
      inline-size: 100%;
    }

    .wal-gatefold-cover {
      aspect-ratio: auto;
      block-size: min(30dvh, 14rem);
      border-radius: var(--wal-radius-lg) var(--wal-radius-lg) 0 0;
    }

    .wal-gatefold-notes {
      contain: none;
      transform-origin: 50% 0;
      border-radius: 0 0 var(--wal-radius-lg) var(--wal-radius-lg);
      box-shadow: inset 0 1px 0 var(--wal-line-soft);
    }
  }

  /* ═══ 38. Tint ═══
     Any colour at all, given the four forms every cue has, legible in both
     modes. For the colours a site owns that no palette does: a tag colour
     a user picked, a category, a brand's own.

         <span class="wal-chip wal-tint" style="--wal-tint: #2f7d6d">Garden</span>
         <span class="wal-badge wal-tint wal-fill-tint" style="--wal-tint: gold">New</span>

     --wal-tint-ink   the colour as text on the page
     --wal-tint-soft  a wash of it under text
     --wal-tint-fill  the colour as a solid fill
     --wal-tint-fg    text on that fill

     .wal-fill-tint (05-utilities.css, with the other fills) paints an
     element in the last two.

     The fill is the colour itself, nudged out of the middle band of
     lightness where neither near-black nor near-white text reaches 4.5:1.
     A light colour is lifted to at least 0.74 and takes dark text; a dark
     one is held to at most 0.5 and takes light. The step at 0.62 is
     `clamp(0, (l - 0.62) * 1000, 1)`: 0 or 1, never between, the same
     switch the cues' -fg tokens use. Every pair was measured at 4.5:1 or
     better for sixteen colours across the wheel, in both modes.

     Unset, the tint is the accent, rebuilt from its light stop rather than
     var(--wal-accent): that alias is a light-dark(), and a light-dark()
     origin is the one relative colour Chrome 123 to 127 cannot resolve
     (see the cues' -fg tokens in 01-tokens.css). The tint makes its own
     dark-mode ink from it. */
  .wal-tint {
    --wal-tint: oklch(var(--wal-l-accent-light) var(--wal-accent-chroma) var(--wal-accent-hue));
    --wal-tint-ink: light-dark(oklch(from var(--wal-tint) 0.44 min(c, 0.16) h),
                               oklch(from var(--wal-tint) 0.8 min(c, 0.13) h));
    --wal-tint-soft: color-mix(in oklch, var(--wal-tint) 18%, var(--wal-surface));
    --wal-tint-fill: oklch(from var(--wal-tint)
      calc(clamp(0, (l - 0.62) * 1000, 1) * max(l, 0.74) + (1 - clamp(0, (l - 0.62) * 1000, 1)) * min(l, 0.5)) c h);
    --wal-tint-fg: oklch(from var(--wal-tint) calc(0.98 - clamp(0, (l - 0.62) * 1000, 1) * 0.8) calc(c * 0.08) h);
  }

  :is(.wal-chip, .wal-badge).wal-tint {
    background: var(--wal-tint-soft);
    color: var(--wal-tint-ink);
    border-color: color-mix(in oklch, var(--wal-tint) 45%, transparent);
  }

  /* The same engine, for your own CSS: `color: --wal-ink(#2f7d6d)`. A
     browser without @function skips these, and a declaration that calls
     one is invalid there, so give it a fallback declaration first.
     The parameter is untyped: Lightning CSS reprints an at-rule it does
     not know, and turns `--c <color>` into `--c< color>`, which no
     browser will read. */
  @function --wal-ink(--c) {
    result: light-dark(oklch(from var(--c) 0.44 min(c, 0.16) h), oklch(from var(--c) 0.8 min(c, 0.13) h));
  }

  @function --wal-soft(--c) {
    result: color-mix(in oklch, var(--c) 18%, var(--wal-surface));
  }

  /* ═══ 39. Select ═══
     A <select> that can show what it chooses: a swatch, an icon, two lines.

         <select class="wal-select">
           <button><selectedcontent></selectedcontent></button>
           <option value="forest"><i class="…"></i> Forest</option>
         </select>

     appearance: base-select lets the options hold any markup, and
     <selectedcontent> copies the chosen one into the button. A browser
     without it shows the ordinary select, with the text of each option and
     the same border, so the form still works. */
  .wal-select,
  .wal-select::picker(select) {
    appearance: base-select;
  }

  .wal-select {
    /* A picker is small and close to hand: it keeps the quick pace whether
       or not a material is in play, unless one is named on it. */
    --wal-open-duration:  var(--wal-duration-slow);
    --wal-open-ease:      var(--wal-ease-glide);
    --wal-close-duration: var(--wal-duration);
    --wal-close-ease:     var(--wal-ease);
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    min-inline-size: 11rem;
    padding: 0.5rem 0.75rem;
    border: var(--wal-border);
    border-color: var(--wal-line-input);
    border-radius: var(--wal-radius);
    background: var(--wal-surface);
    color: var(--wal-text);
    font: 500 var(--wal-text-sm)/1.2 var(--wal-font-body);
    cursor: pointer;

    &:focus-visible {
      outline: 2px solid var(--wal-accent);
      outline-offset: 2px;
    }

    option {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      padding: 0.5rem 0.6rem;
      border-radius: calc(var(--wal-radius) - 0.2rem);
      font: 500 var(--wal-text-sm)/1.2 var(--wal-font-body);
      color: var(--wal-text);
      cursor: pointer;
    }

    option:is(:hover, :focus-visible) {
      background: var(--wal-surface-2);
      outline: none;
    }

    option:checked { background: var(--wal-accent-soft); }
  }

  .wal-select::picker-icon {
    margin-inline-start: auto;
    transition: rotate var(--wal-duration) var(--wal-ease);
  }

  .wal-select:open::picker-icon { rotate: 180deg; }

  .wal-select::picker(select) {
    margin-block: 0.35rem;
    padding: 0.3rem;
    border: var(--wal-border);
    border-radius: var(--wal-radius);
    background: var(--wal-surface);
    color: var(--wal-text);
    box-shadow: var(--wal-shadow-lg);
    opacity: 0;
    translate: 0 -0.35rem;
    transition:
      opacity   var(--wal-close-duration) var(--wal-ease),
      translate var(--wal-close-duration) var(--wal-close-ease),
      display   var(--wal-close-duration) allow-discrete,
      overlay   var(--wal-close-duration) allow-discrete;
  }

  /* :open on the select, not :popover-open on its picker: Lightning CSS
     allows only :hover-like states after a pseudo-element, and rejects the
     whole stylesheet over one. */
  .wal-select:open::picker(select) {
    opacity: 1;
    translate: 0 0;
    transition-duration: var(--wal-open-duration);
    transition-timing-function: var(--wal-ease), var(--wal-open-ease);
  }

  @starting-style {
    .wal-select:open::picker(select) {
      opacity: 0;
      translate: 0 -0.35rem;
    }
  }

  .wal-select option::checkmark {
    order: 1;
    margin-inline-start: auto;
    color: var(--wal-accent-ink);
  }

  /* ═══ 40. Menu ═══
     A menu that opens under its button, or above it, or to its other side,
     whichever has room. No script and no positioning library: the menu is
     a popover anchored to the button that opened it.

         <button popovertarget="file-menu">File</button>
         <div class="wal-menu" id="file-menu" popover>
           <button>New</button>
           <a href="…">Open…</a>
         </div>

     A popover closes on Escape and on a click outside, and the button keeps
     aria-expanded in step. */
  .wal-menu {
    --wal-open-duration:  var(--wal-duration-slow);
    --wal-open-ease:      var(--wal-ease-glide);
    --wal-close-duration: var(--wal-duration);
    --wal-close-ease:     var(--wal-ease);
    position-area: block-end span-inline-end;
    position-try-fallbacks: flip-block, flip-inline;
    inset: auto;
    margin: 0.35rem 0;
    padding: 0.3rem;
    min-inline-size: 12rem;
    border: var(--wal-border);
    border-radius: var(--wal-radius);
    background: var(--wal-surface);
    color: var(--wal-text);
    box-shadow: var(--wal-shadow-lg);
    opacity: 0;
    translate: 0 -0.35rem;
    transition:
      opacity   var(--wal-close-duration) var(--wal-ease),
      translate var(--wal-close-duration) var(--wal-close-ease),
      display   var(--wal-close-duration) allow-discrete,
      overlay   var(--wal-close-duration) allow-discrete;

    > :is(button, a) {
      display: flex;
      gap: 0.6rem;
      align-items: center;
      inline-size: 100%;
      padding: 0.5rem 0.65rem;
      border: 0;
      border-radius: calc(var(--wal-radius) - 0.2rem);
      background: none;
      color: inherit;
      text-align: start;
      text-decoration: none;
      font: 500 var(--wal-text-sm)/1.2 var(--wal-font-body);
      cursor: pointer;

      &:is(:hover, :focus-visible) {
        background: var(--wal-surface-2);
        outline: none;
      }
    }
  }

  /* The menu anchors to the button that opened it, which needs
     position-area and a popover that knows its button (Chrome 133,
     Safari 26). Without position-area, inset: auto would leave it where it
     sits in the page, often off screen, so it opens in the middle of the
     screen, as a popover does by default. Chrome 129 to 132 have
     position-area but not the button link, and CSS can't tell them apart. */
  @supports not (position-area: block-end) {
    .wal-menu { inset: 0; margin: auto; }
  }

  .wal-menu:popover-open {
    display: grid;
    opacity: 1;
    translate: 0 0;
    transition-duration: var(--wal-open-duration);
    transition-timing-function: var(--wal-ease), var(--wal-open-ease);
  }

  @starting-style {
    .wal-menu:popover-open {
      opacity: 0;
      translate: 0 -0.35rem;
    }
  }

  /* ═══ 41. Carousel ═══
     A row of slides with the browser's own buttons and dots: no script,
     and every part is focusable and announced.

         <ul class="wal-carousel" aria-label="Colourways">
           <li>…</li>
           <li>…</li>
         </ul>

     It is a scroll-snapping list. walnut-scroll.css gives it a dot per
     slide, which scrolls to that slide and lights while it is the one in
     view, and Previous and Next, which grey out at either end. Without
     that sheet, or in a browser without scroll markers, it is a list you
     swipe or scroll, which is still a carousel. */
  .wal-carousel {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 100%;
    gap: var(--wal-space-md);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
    anchor-name: --wal-carousel;
    anchor-scope: --wal-carousel;

    > li {
      scroll-snap-align: center;
      min-inline-size: 0;
    }
  }

  /* ═══ 42. Chart ═══
     Columns, a ring and a key, in the five cues, and made like furniture.
     The columns stand on a brass rail, each on a brass shoe like the feet of
     a sideboard's legs; the gridlines are stringing, the fine inlaid line
     of a paler wood, a groove with its lit edge; the ring sits in a brass
     bezel with a tick at each quarter. The data is the markup: no script,
     no canvas, nothing to load.

         <ol class="wal-chart" style="--max: 80" aria-label="Rooms painted, by season">
           <li><b>Spring</b><span style="--v: 42">42</span><span style="--v: 30">30</span></li>
           <li><b>Summer</b><span style="--v: 64">64</span><span style="--v: 51">51</span></li>
         </ol>
         <ul class="wal-key"><li>Walls</li><li>Trim</li></ul>

     Each <li> is a group on the axis, and each <span> a column in it: its
     value in --v and, as text, printed above it. --max is the top of the
     scale and --ticks how many gridlines divide it, 4 unless you say. The
     nth column of every group is cue n, as is the nth entry in .wal-key.

     A new --v moves the column on the opening tokens, so a material shows
     here as well: under data-motion="materials" a chart is brass, and a
     column strikes its new height and rings. So does the ring below. */
  .wal-chart {
    --max: 100;
    --ticks: 4;
    --wal-rail: 5px;
    --wal-shoe: 6px;
    position: relative;
    list-style: none;
    margin: 0 0 calc(var(--wal-rail) + 1.6rem);
    padding: 0;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    align-items: end;
    gap: var(--wal-space-md);
    block-size: var(--wal-chart-h, 12rem);
    /* Stringing: a groove in the line colour, and under it a hairline of
       light where its far edge catches. One per tick, down from the top of
       the scale; the rail is the last. */
    background:
      linear-gradient(var(--wal-line) 1px,
                      light-dark(oklch(1 0 0 / 0.75), oklch(1 0 0 / 0.07)) 1px 2px,
                      transparent 2px)
      0 0 / 100% calc(100% / var(--ticks));

    /* The rail the columns stand on. */
    &::after {
      content: "";
      position: absolute;
      inset: 100% 0 auto;
      block-size: var(--wal-rail);
      border-radius: 1px;
      background: linear-gradient(var(--wal-brass-hi), var(--wal-brass) 45%, var(--wal-brass-lo));
      box-shadow: 0 1px 2px var(--wal-shadow-ink);
    }

    > li {
      position: relative;
      display: flex;
      align-items: stretch;
      justify-content: center;
      gap: 3px;
      block-size: 100%;
    }

    /* The group's name, under the rail. */
    > li > b {
      position: absolute;
      inset: 100% 0 auto;
      padding-block-start: calc(var(--wal-rail) + 0.45rem);
      text-align: center;
      font: 500 var(--wal-text-xs)/1 var(--wal-font-body);
      color: var(--wal-text-muted);
    }

    /* A column: its value printed at its top, the leg below. The leg does
       not shrink for the label; a tall column's label stands above the
       scale instead. */
    > li > span {
      --wal-chart-cue: var(--wal-cue-1);
      flex: 1 1 0;
      max-inline-size: 2.75rem;
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      align-items: center;
      font: 500 var(--wal-text-xs)/1 var(--wal-font-body);
      font-variant-numeric: tabular-nums;
      color: var(--wal-text-soft);

      &::after {
        content: "";
        flex: none;
        align-self: stretch;
        margin-block-start: 0.3rem;
        block-size: calc(var(--v, 0) / var(--max) * 100%);
        border-radius: var(--wal-radius-sm) var(--wal-radius-sm) 0 0;
        corner-shape: var(--wal-corner-shape, round);
        /* Top to bottom: the light across a turned leg, the shoe and the
           seam above it, and the cue. */
        background:
          var(--wal-chart-sheen, linear-gradient(90deg, oklch(1 0 0 / 0.16), oklch(1 0 0 / 0) 38% 66%, oklch(0 0 0 / 0.1))),
          linear-gradient(to top,
            var(--wal-brass-lo) 0 1px,
            var(--wal-brass) 1px var(--wal-shoe),
            oklch(0 0 0 / 0.22) var(--wal-shoe) calc(var(--wal-shoe) + 1px),
            transparent 0),
          var(--wal-chart-cue);
        transition: block-size var(--wal-open-duration) var(--wal-open-ease);
      }
    }

    > li > span:nth-of-type(2) { --wal-chart-cue: var(--wal-cue-2); }
    > li > span:nth-of-type(3) { --wal-chart-cue: var(--wal-cue-3); }
    > li > span:nth-of-type(4) { --wal-chart-cue: var(--wal-cue-4); }
    > li > span:nth-of-type(5) { --wal-chart-cue: var(--wal-cue-5); }
  }

  /* The ring: shares out of a whole, cue by cue, as percentages. Four
     shares are named; the fifth is whatever is left. Its own text is the
     figure in the middle, set in the display face, with a <small> caption
     under it.

         <div class="wal-ring" style="--a: 38; --b: 24; --c: 18; --d: 12">
           92<small>rooms</small>
         </div>

     The shares are inlay: a hairline of the ground between each. New
     shares turn the ring on the opening tokens, as a new --v moves a
     column, so under brass the ring strikes and rings too. */
  /* The shares again, as numbers the browser can tween. They have names of
     their own because a registration is global, and --a is short enough
     that a site may already use it for something else. */
  @property --wal-ring-a { syntax: "<number>"; inherits: true; initial-value: 0; }
  @property --wal-ring-b { syntax: "<number>"; inherits: true; initial-value: 0; }
  @property --wal-ring-c { syntax: "<number>"; inherits: true; initial-value: 0; }
  @property --wal-ring-d { syntax: "<number>"; inherits: true; initial-value: 0; }

  .wal-ring {
    --a: 0;
    --b: 0;
    --c: 0;
    --d: 0;
    --wal-ring-a: var(--a);
    --wal-ring-b: var(--b);
    --wal-ring-c: var(--c);
    --wal-ring-d: var(--d);
    --wal-ring-gap: 0.5%;
    transition:
      --wal-ring-a var(--wal-open-duration) var(--wal-open-ease),
      --wal-ring-b var(--wal-open-duration) var(--wal-open-ease),
      --wal-ring-c var(--wal-open-duration) var(--wal-open-ease),
      --wal-ring-d var(--wal-open-duration) var(--wal-open-ease);
    position: relative;
    display: grid;
    place-content: center;
    isolation: isolate;
    inline-size: var(--wal-ring-size, 8rem);
    aspect-ratio: 1;
    border-radius: 50%;
    font: 700 1.6rem/1 var(--wal-font-display);
    font-variant-numeric: tabular-nums;
    text-align: center;
    color: var(--wal-text);

    small {
      display: block;
      margin-block-start: 0.2rem;
      font: 500 var(--wal-text-xs)/1.2 var(--wal-font-body);
      color: var(--wal-text-muted);
    }

    &::before {
      content: "";
      position: absolute;
      inset: 6px;
      z-index: -1;
      border-radius: 50%;
      background: conic-gradient(
        var(--wal-cue-1) 0 calc(var(--wal-ring-a) * 1% - var(--wal-ring-gap)),
        transparent 0 calc(var(--wal-ring-a) * 1%),
        var(--wal-cue-2) 0 calc((var(--wal-ring-a) + var(--wal-ring-b)) * 1% - var(--wal-ring-gap)),
        transparent 0 calc((var(--wal-ring-a) + var(--wal-ring-b)) * 1%),
        var(--wal-cue-3) 0 calc((var(--wal-ring-a) + var(--wal-ring-b) + var(--wal-ring-c)) * 1% - var(--wal-ring-gap)),
        transparent 0 calc((var(--wal-ring-a) + var(--wal-ring-b) + var(--wal-ring-c)) * 1%),
        var(--wal-cue-4) 0 calc((var(--wal-ring-a) + var(--wal-ring-b) + var(--wal-ring-c) + var(--wal-ring-d)) * 1% - var(--wal-ring-gap)),
        transparent 0 calc((var(--wal-ring-a) + var(--wal-ring-b) + var(--wal-ring-c) + var(--wal-ring-d)) * 1%),
        var(--wal-cue-5) 0 calc(100% - var(--wal-ring-gap)),
        transparent 0);
      mask: radial-gradient(closest-side, transparent 64%, #000 calc(64% + 1px));
    }

    /* The bezel: a brass ring round the shares, 6px out, and a tick on its
       inside edge at each quarter. --wal-ring-size includes it. */
    &::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      border-radius: 50%;
      border: 2px solid var(--wal-brass);
      background: repeating-conic-gradient(from -0.75deg, var(--wal-brass-lo) 0 1.5deg, transparent 0 90deg);
      mask: radial-gradient(closest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
    }
  }

  .wal-key {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1rem;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--wal-text-xs);
    color: var(--wal-text-soft);

    > li {
      --wal-chart-cue: var(--wal-cue-1);
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
    }

    > li::before {
      content: "";
      inline-size: 0.75rem;
      block-size: 0.75rem;
      border-radius: 2px;
      corner-shape: var(--wal-corner-shape, round);
      background: var(--wal-chart-cue);
      box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.12), inset 0 -2px 0 var(--wal-brass);
    }

    > li:nth-child(2) { --wal-chart-cue: var(--wal-cue-2); }
    > li:nth-child(3) { --wal-chart-cue: var(--wal-cue-3); }
    > li:nth-child(4) { --wal-chart-cue: var(--wal-cue-4); }
    > li:nth-child(5) { --wal-chart-cue: var(--wal-cue-5); }
  }

  /* ═══ 42b. Tabs ═══
     Sections you switch between. Write them one after another, each with
     the name its tab shows:

         <div class="wal-tabs" data-label="Install">
           <section data-tab="CDN" id="install-cdn">…</section>
           <section data-tab="Download" id="install-download">…</section>
         </div>

     walnut-tabs.js builds the row of tabs above them and shows one section
     at a time; see that file for the keys and links. Without the script
     the sections stack, each under its name, and print does the same.

     Two styles. The default is a row of names over a rule, with a bar
     under the chosen one. data-style="index" makes card tabs: the chosen
     one stands up and joins its panel, and its top edge is brass.

     The row works on its own, for an app that renders its own tabs:

         <div class="wal-tablist" role="tablist" aria-label="Scenes">
           <button role="tab" aria-selected="true" aria-controls="s1">…</button>
           …
         </div>

     data-numbered on the row numbers the tabs 01, 02, 03. data-running
     makes the chosen tab's bar fill over --wal-tab-run, for tabs that
     advance on their own; it starts over whenever the selection changes or
     data-running comes back.

     The colours, for a row on a ground of its own:
       --wal-tab-ink        the chosen tab           (--wal-text)
       --wal-tab-ink-soft   a tab under the pointer  (--wal-text-soft)
       --wal-tab-ink-muted  the others               (--wal-text-muted)
       --wal-tab-mark       the bar                  (--wal-accent; brass on index)
       --wal-tab-no         the numbers              (--wal-gold-ink)
       --wal-tab-rule       the rule under the row   (--wal-line-soft) */
  .wal-tabs:not(.is-enhanced) > [data-tab] + [data-tab] {
    margin-block-start: var(--wal-space-xl);
  }

  .wal-tabs:not(.is-enhanced) > [data-tab]::before {
    content: attr(data-tab);
    display: block;
    margin-block-end: var(--wal-space-sm);
    font: 500 var(--wal-text-xs)/1.3 var(--wal-font-label);
    letter-spacing: var(--wal-tracking-label);
    text-transform: uppercase;
    color: var(--wal-text-muted);
  }

  .wal-tablist {
    display: flex;
    gap: 0.25rem;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    counter-reset: wal-tab;
    /* A shadow, not a border, so the bar can sit on the rule. */
    box-shadow: inset 0 -1px 0 var(--wal-tab-rule, var(--wal-line-soft));

    &::-webkit-scrollbar { display: none; }

    [role="tab"] {
      position: relative;
      flex: none;
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      min-block-size: 2.75rem;
      padding: 0.6rem 0.8rem 0.7rem;
      border: 0;
      border-radius: var(--wal-radius-sm) var(--wal-radius-sm) 0 0;
      background: none;
      color: var(--wal-tab-ink-muted, var(--wal-text-muted));
      font: 500 var(--wal-text-xs)/1.2 var(--wal-font-label);
      letter-spacing: var(--wal-tracking-label);
      text-transform: uppercase;
      white-space: nowrap;
      cursor: pointer;
      counter-increment: wal-tab;
      transition: color var(--wal-duration) var(--wal-ease);

      &:hover { color: var(--wal-tab-ink-soft, var(--wal-text-soft)); }
      &[aria-selected="true"] { color: var(--wal-tab-ink, var(--wal-text)); }
      /* Inside the tab: the row clips anything outside it. */
      &:focus-visible { outline-offset: -2px; }

      /* The bar: a border, so forced colours still draw it. It grows from
         the start of the line, which is the right in right-to-left text. */
      &::after {
        content: "";
        position: absolute;
        inset-inline: 0.8rem;
        inset-block-end: 0;
        border-block-start: 2px solid var(--wal-tab-mark, var(--wal-accent));
        scale: 0 1;
        transform-origin: left;
        transition: scale var(--wal-duration) var(--wal-ease);
      }

      &:dir(rtl)::after { transform-origin: right; }
      &[aria-selected="true"]::after { scale: 1 1; }
    }

    &[data-numbered] [role="tab"]::before {
      content: counter(wal-tab, decimal-leading-zero);
      color: var(--wal-tab-no, var(--wal-gold-ink));
    }

    &[data-running] [role="tab"][aria-selected="true"]::after {
      animation: wal-tab-run var(--wal-tab-run, 6s) linear both;
    }
  }

  /* The numbers are for the eye; a screen reader says only the name. */
  @supports (content: "" / "") {
    .wal-tablist[data-numbered] [role="tab"]::before {
      content: counter(wal-tab, decimal-leading-zero) / "";
    }
  }

  @keyframes wal-tab-run { from { scale: 0 1; } }

  .wal-tabs.is-enhanced {
    > .wal-tablist { margin-block-end: var(--wal-space-lg); }

    /* A link to a panel lands with its row of tabs in view, below a
       walnut navbar. */
    > [role="tabpanel"] {
      scroll-margin-block-start: calc(var(--wal-nav-h) + 4.5rem);
    }

    > [role="tabpanel"]:focus-visible { outline-offset: 4px; }

    /* hidden="until-found" hides a panel's contents but keeps its box, so
       find-in-page can still reach in. The box goes flat. */
    > [role="tabpanel"][hidden] {
      margin: 0;
      padding: 0;
      border: 0;
      box-shadow: none;
    }
  }

  /* Only once a tab has been chosen, so the first panel does not move on
     load. */
  .wal-tabs[data-wal-switched] > [role="tabpanel"]:not([hidden]) {
    animation: wal-tab-in var(--wal-duration-slow) var(--wal-ease);
  }

  @keyframes wal-tab-in { from { opacity: 0; translate: 0 0.375rem; } }

  /* Index: card tabs, filed like a folder's. The others sit lower, their
     feet behind the panel, and the chosen one rises to meet it. Its bar is
     the brass top edge. Each tab tucks a little under the one before it
     and throws a soft shadow on the one after; the chosen one comes to the
     front. The shadow falls both ways, so only the one cast on a tab
     underneath shows, whichever way the text runs. */
  :is(.wal-tabs[data-style="index"] > .wal-tablist, .wal-tablist[data-style="index"]) {
    align-items: flex-end;
    gap: 0;
    padding-inline: 0.625rem;
    box-shadow: none;
    /* The tabs' stacking stays inside the row, so their feet still go
       behind the panel below. */
    isolation: isolate;

    [role="tab"] {
      /* Its edge, top and sides, as a card's catches the light, and the
         shadow it throws on its neighbours. */
      --wal-tab-edge: inset 0 1px 0 var(--wal-line-soft), inset 1px 0 0 var(--wal-line-soft), inset -1px 0 0 var(--wal-line-soft);
      --wal-tab-cast: -0.35rem 0 0.6rem -0.25rem var(--wal-shadow-ink), 0.35rem 0 0.6rem -0.25rem var(--wal-shadow-ink);
      /* Narrower on a phone, so a row that nearly fits does fit, rather
         than leaving its last tab half off the edge. */
      padding: 0.75rem min(1.1rem, 3.4vw) 0.7rem;
      border-radius: var(--wal-radius) var(--wal-radius) 0 0;
      background: var(--wal-surface-2);
      box-shadow: var(--wal-tab-edge), inset 0 -0.75rem 0.875rem -0.875rem var(--wal-shadow-ink), var(--wal-tab-cast);
      translate: 0 0.5rem;

      & + [role="tab"] { margin-inline-start: -0.375rem; }
      /* Left alone, a later tab paints over an earlier one; a folder's go
         the other way. Eight is more than a row of these holds. */
      &:nth-child(1) { z-index: 8; }
      &:nth-child(2) { z-index: 7; }
      &:nth-child(3) { z-index: 6; }
      &:nth-child(4) { z-index: 5; }
      &:nth-child(5) { z-index: 4; }
      &:nth-child(6) { z-index: 3; }
      &:nth-child(7) { z-index: 2; }
      &:nth-child(8) { z-index: 1; }
      transition:
        color            var(--wal-duration) var(--wal-ease),
        background-color var(--wal-duration) var(--wal-ease),
        translate        var(--wal-open-duration) var(--wal-open-ease);

      &:hover { background: color-mix(in oklch, var(--wal-surface-2) 60%, var(--wal-surface)); }

      &[aria-selected="true"] {
        z-index: 9;
        translate: 0 0;
        background: var(--wal-surface);
        box-shadow: var(--wal-tab-edge), var(--wal-tab-cast);
      }

      &::after {
        inset-inline: 0;
        inset-block: 0 auto;
        border-color: var(--wal-tab-mark, var(--wal-brass));
      }
    }
  }

  .wal-tabs.is-enhanced[data-style="index"] > .wal-tablist { margin-block-end: 0; }

  /* Each panel is a card, with the script or without: what is made for the
     card keeps its contrast when the panels stack on a ground of their own. */
  .wal-tabs[data-style="index"] > [data-tab]:not([hidden]) {
    position: relative;
    padding: clamp(1.25rem, 3.4vw, 2.75rem);
    border-radius: var(--wal-radius-sm) var(--wal-radius-lg) var(--wal-radius-lg);
    background: var(--wal-surface);
    color: var(--wal-text);
    box-shadow: var(--wal-shadow-lg), var(--wal-shadow-sm);
  }

  .wal-tabs:not(.is-enhanced)[data-style="index"] > [data-tab] {
    border-radius: var(--wal-radius-lg);
  }

  @media (forced-colors: active) {
    .wal-tablist { border-block-end: 1px solid CanvasText; }
  }

  @media print {
    .wal-tablist { display: none; }

    .wal-tabs.is-enhanced > [role="tabpanel"][hidden] {
      display: block;
      content-visibility: visible;
      margin-block-start: var(--wal-space-xl);
    }

    .wal-tabs.is-enhanced > [data-tab]::before {
      content: attr(data-tab);
      display: block;
      margin-block-end: var(--wal-space-sm);
      font: 500 var(--wal-text-xs)/1.3 var(--wal-font-label);
      letter-spacing: var(--wal-tracking-label);
      text-transform: uppercase;
    }
  }

  /* ═══ 43. Corners ═══
     Every rule here that draws a radius also reads its shape:
     --wal-corner-shape, round unless a finish or you say otherwise
     (01-tokens.css). Catalogue cuts them. squircle is Piet Hein's
     superellipse, the edge of the Superellipse table, between the circle's
     arc and the square. */
  :is(.wal-btn, .wal-card, .wal-plate, .wal-metric, .wal-chip, .wal-swatch,
      .wal-input, .wal-badge, .wal-dialog, .wal-tooltip, .wal-iconbtn, .wal-tip,
      .wal-select, .wal-menu, .wal-door, .wal-window-panel,
      .wal-gatefold-cover, .wal-gatefold-notes,
      .wal-tablist > [role="tab"], .wal-tabs > [data-tab],
      .wal-rounded, .wal-rounded-sm, .wal-rounded-lg, .wal-rounded-full) {
    corner-shape: var(--wal-corner-shape, round);
  }

  .wal-script > *::before { corner-shape: var(--wal-corner-shape, round); }
  .wal-select::picker(select) { corner-shape: var(--wal-corner-shape, round); }

}

/* ── finish ── */
@layer finish {
/* ═══════════════════════════════════════════════════
   07 — FINISH
   ───────────────────────────────────────────────────
   A finish is the THIRD theming axis. The three are deliberately independent
   and use three different syntaxes so they can never be confused:

       PALETTE   by class       .wal-press          which colours
       MODE      by attribute   data-theme="light"  light or dark
       FINISH    by attribute   data-finish="…"     the geometry and the voice

   Palette answers "what colour is this". Finish answers "what KIND of object
   is this page" — printed or lit, square or rounded, ruled or floated. They
   are orthogonal: every finish works with every palette in both modes.

   ───────────────────────────────────────────────────
   WHY THIS IS ALMOST ENTIRELY TOKENS

   Nearly all of a finish is retuning the geometry and atmosphere tokens that
   01-tokens.css already routes every component through — radius, hairline,
   elevation, wash, label tracking, label face. That is the point of those
   tokens existing: a finish should not have to know the selector of every
   component it affects, or it will fall out of date the first time a component
   is added.

   The handful of real rules below are the cases where the catalogue look needs
   a different *structure*, not a different value: a card that is a printed
   plate rather than a floating panel, a nav that is a ruled masthead rather
   than a frosted bar.

   ───────────────────────────────────────────────────
   A PALETTE NEVER SETS WHAT A FINISH SETS

   Every token declared in this file belongs to the finish axis alone. No
   palette file sets elevation, wash, radius, rule strength, the stock's
   tooth or the label voice, however much its mood seems to ask for it —
   "print does not cast shadows" is the catalogue finish's sentence, not
   Press's.

   The `finish` layer cannot protect these tokens on its own, for two reasons:

     1. dist/themes/*.css ship unlayered, and unlayered beats every layer. A
        palette that set --wal-elevation won on <html class="wal-press"
        data-finish="soft">, so switching finishes changed nothing.
     2. Even inside @layer tokens, a layer only arbitrates between
        declarations on the SAME element. <section class="wal-bone"> on a
        catalogue page declares its own value there, and an element's own
        declaration beats an inherited one from any layer.

   So orthogonality is kept by ownership, not by order: the two axes compose
   in any combination and at any depth because they never write the same
   token. It also keeps `soft` honest — it resets to the stock values, which
   is only the same thing as "no finish" if no palette has moved them.
═══════════════════════════════════════════════════ */


/* ═══ CATALOGUE ═══
   A 1960s parts catalogue: square corners, visible rules, tabular figures,
   type set in the label voice, no glow and almost no shadow. Everything is
   printed on stock — nothing floats.

       <html class="wal-press" data-finish="catalogue"> */

[data-finish="catalogue"] {
  /* ── Geometry ──
     3px rather than 0: a true square corner reads as an unstyled box at small
     sizes, while a hair of radius reads as a trimmed edge. The component-level
     radius tokens are set separately because pills are the single strongest
     signal of a *web app*, and a catalogue has none of them. */
  --wal-radius:      3px;
  --wal-radius-sm:   2px;
  --wal-radius-lg:   3px;
  --wal-btn-radius:  2px;
  --wal-chip-radius: 2px;
  --wal-badge-radius: 2px;
  /* --wal-radius-full is deliberately untouched: it still has to make the
     availability dot a circle. */
  /* The corners are cut, not rounded: a card trimmed on the guillotine
     with its corners clipped, as a parts sheet's are. */
  --wal-corner-shape: bevel;

  /* ── Atmosphere ──
     Print does not glow and barely casts. Both are scalars rather than
     overrides, so no shadow rule and no background-image rule is restated. */
  --wal-elevation: 0.35;
  --wal-wash:      0;

  /* ── Voice ──
     Wider tracking on labels; the typewriter face carries the catalogue's
     furniture, which is what separates it from a modern dashboard using the
     same mono for code. Only a little wider: Courier Prime is monospaced, so
     its advance is already generous, and captions multiply this by 1.6.
     At 0.16em a caption set at 0.26em and read as spaced-out letters rather
     than as words. */
  --wal-tracking-label: 0.12em;
  --wal-font-label: var(--wal-font-typewriter);

  /* ── Rules ──
     Hairlines are the catalogue's entire visual language, so they have to
     read on any palette, including the soft ones. This turns the knob rather
     than overriding --wal-line: the finish must not own a colour, or it stops
     being orthogonal to the palette and a light rule ends up hard-coded onto
     a dark theme. --wal-line-boost is consumed by the rule in 01-tokens.css
     that lists [data-finish], so it re-derives on this element. */
  --wal-line-boost: 1.7;

  /* ── Stock ──
     Printed matter has a tooth. Fine fibres over a soft mottle, as grey that
     every paper multiplies into its own colour (04-components.css, 32. Stock),
     so it owns no colour either: it darkens light stock by four percent at
     its darkest speck and all but vanishes on dark. Muted text on the darkest
     speck still clears 4.8:1 in every shipped palette. */
  --wal-tooth: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='t' x='0' y='0' width='100%' height='100%' color-interpolation-filters='sRGB'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' seed='3' stitchTiles='stitch' result='f'/><feTurbulence type='fractalNoise' baseFrequency='0.018' numOctaves='2' seed='8' stitchTiles='stitch' result='m'/><feComposite in='f' in2='m' operator='arithmetic' k1='0' k2='0.7' k3='0.3' k4='0'/><feColorMatrix values='-0.14 0 0 0 1.07 -0.14 0 0 0 1.07 -0.14 0 0 0 1.07 0 0 0 0 1'/></filter><rect width='100%' height='100%' filter='url(%23t)'/></svg>");

  /* ─── Headings ───
     Tighter and quieter. A catalogue's hierarchy comes from rules and
     numbering, not from typographic shouting, so headings lose a weight step
     and gain balanced wrapping. */
  h1, h2, h3, h4 {
    font-weight: 600;
    text-wrap: balance;
  }

  h2 { letter-spacing: -0.015em; }

  /* Tabular figures everywhere numbers are read in a column. */
  .wal-metric-value,
  .wal-stat-value,
  .wal-plate dd,
  .wal-table td {
    font-variant-numeric: tabular-nums;
  }
}

/* ─── Masthead ───
   A ruled bar, not a frosted floating one. The blur is removed rather than
   reduced: backdrop-filter over a paper ground reads as a smudge. */
[data-finish="catalogue"] .wal-nav {
  background: var(--wal-bg);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom: var(--wal-border);

  &.is-scrolled {
    box-shadow: none;
    border-bottom-color: var(--wal-text);
  }

  /* The rule a stuck masthead draws for itself (04-components.css, 1). */
  &::after {
    background: var(--wal-text);
    box-shadow: none;
  }
}

/* ─── Plates ───
   A card becomes a printed plate: it sits ON the page rather than above it, so
   it does not lift, and the hover response moves to the rule instead of to the
   geometry. Keeping a translate on hover is what makes an otherwise correct
   catalogue still feel like a SaaS dashboard. */
[data-finish="catalogue"] .wal-card {
  &:hover {
    transform: none;
    box-shadow: none;
    border-color: var(--wal-accent);
  }
}

/* ─── Tables ───
   No zebra. A catalogue's table is ruled, and the stripes fight the rules for
   the reader's sense of where a row ends. */
[data-finish="catalogue"] .wal-table {
  tbody tr:nth-child(even) { background: none; }
  tbody tr:hover { background: var(--wal-accent-soft); }
  th { border-bottom-color: var(--wal-text); }
}

/* ─── Buttons ───
   A secondary button becomes a boxed control with a real rule; the primary
   keeps the accent fill, which is the one place the catalogue lets a colour
   carry weight. */
[data-finish="catalogue"] .wal-btn {
  letter-spacing: 0.04em;

  &.wal-btn-secondary:hover { background: var(--wal-accent-soft); }

  &.wal-btn-primary:hover {
    transform: none;
    box-shadow: none;
  }
}

/* ─── Hero ───
   No radial glow: the catalogue's cover is a ruled sheet. The tagline keeps
   the typewriter face, which now matches the rest of the furniture rather than
   being the one screenplay-flavoured element on the page. */
[data-finish="catalogue"] .wal-hero .wal-hero-glow {
  display: none;
}


/* ═══ SOFT ═══
   The stock finish, named so it can be asked for explicitly — useful when a
   subtree should opt out of an inherited catalogue finish. It cannot simply
   `unset` them: these tokens inherit, so unset means inherit, which is the
   catalogue it is opting out of. `initial` is the next best thing. On a
   registered token it is the registered stock value (01-tokens.css), so the
   numbers are not restated here; on the unregistered per-component radii it
   is "no value", and each component falls back to its pill. The two tokens
   that are neither are stated. */
[data-finish="soft"] {
  --wal-radius:       initial;
  --wal-radius-sm:    initial;
  --wal-radius-lg:    initial;
  --wal-btn-radius:   initial;
  --wal-chip-radius:  initial;
  --wal-badge-radius: initial;
  --wal-corner-shape: initial;
  --wal-elevation:    initial;
  --wal-wash:         initial;
  --wal-line-boost:   initial;
  --wal-tooth:        initial;
  --wal-tracking-label: 0.1em;
  --wal-font-label:   var(--wal-font-mono);
}

}

/* ── utilities ── */
@layer utilities {
/* ═══════════════════════════════════════════════════════════════════════
   05-utilities.css
   Minimal utility classes for layout, typography, color, and more.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Spacing Utilities ───────────────────────────────────────────────── */
/* Margins */
.wal-m-xs { margin: var(--wal-space-xs); }
.wal-m-sm { margin: var(--wal-space-sm); }
.wal-m-md { margin: var(--wal-space-md); }
.wal-m-lg { margin: var(--wal-space-lg); }
.wal-m-xl { margin: var(--wal-space-xl); }
.wal-m-2xl { margin: var(--wal-space-2xl); }
.wal-m-3xl { margin: var(--wal-space-3xl); }
.wal-m-0 { margin: 0; }

.wal-mt-xs { margin-top: var(--wal-space-xs); }
.wal-mt-sm { margin-top: var(--wal-space-sm); }
.wal-mt-md { margin-top: var(--wal-space-md); }
.wal-mt-lg { margin-top: var(--wal-space-lg); }
.wal-mt-xl { margin-top: var(--wal-space-xl); }
.wal-mt-2xl { margin-top: var(--wal-space-2xl); }
.wal-mt-3xl { margin-top: var(--wal-space-3xl); }
.wal-mt-0 { margin-top: 0; }

.wal-mb-xs { margin-bottom: var(--wal-space-xs); }
.wal-mb-sm { margin-bottom: var(--wal-space-sm); }
.wal-mb-md { margin-bottom: var(--wal-space-md); }
.wal-mb-lg { margin-bottom: var(--wal-space-lg); }
.wal-mb-xl { margin-bottom: var(--wal-space-xl); }
.wal-mb-2xl { margin-bottom: var(--wal-space-2xl); }
.wal-mb-3xl { margin-bottom: var(--wal-space-3xl); }
.wal-mb-0 { margin-bottom: 0; }

.wal-ml-xs { margin-left: var(--wal-space-xs); }
.wal-ml-sm { margin-left: var(--wal-space-sm); }
.wal-ml-md { margin-left: var(--wal-space-md); }
.wal-ml-lg { margin-left: var(--wal-space-lg); }
.wal-ml-xl { margin-left: var(--wal-space-xl); }
.wal-ml-2xl { margin-left: var(--wal-space-2xl); }
.wal-ml-3xl { margin-left: var(--wal-space-3xl); }
.wal-ml-0 { margin-left: 0; }

.wal-mr-xs { margin-right: var(--wal-space-xs); }
.wal-mr-sm { margin-right: var(--wal-space-sm); }
.wal-mr-md { margin-right: var(--wal-space-md); }
.wal-mr-lg { margin-right: var(--wal-space-lg); }
.wal-mr-xl { margin-right: var(--wal-space-xl); }
.wal-mr-2xl { margin-right: var(--wal-space-2xl); }
.wal-mr-3xl { margin-right: var(--wal-space-3xl); }
.wal-mr-0 { margin-right: 0; }

.wal-mx-xs { margin-inline: var(--wal-space-xs); }
.wal-mx-sm { margin-inline: var(--wal-space-sm); }
.wal-mx-md { margin-inline: var(--wal-space-md); }
.wal-mx-lg { margin-inline: var(--wal-space-lg); }
.wal-mx-xl { margin-inline: var(--wal-space-xl); }
.wal-mx-2xl { margin-inline: var(--wal-space-2xl); }
.wal-mx-3xl { margin-inline: var(--wal-space-3xl); }
.wal-mx-0 { margin-inline: 0; }

.wal-my-xs { margin-block: var(--wal-space-xs); }
.wal-my-sm { margin-block: var(--wal-space-sm); }
.wal-my-md { margin-block: var(--wal-space-md); }
.wal-my-lg { margin-block: var(--wal-space-lg); }
.wal-my-xl { margin-block: var(--wal-space-xl); }
.wal-my-2xl { margin-block: var(--wal-space-2xl); }
.wal-my-3xl { margin-block: var(--wal-space-3xl); }
.wal-my-0 { margin-block: 0; }

/* Padding */
.wal-p-xs { padding: var(--wal-space-xs); }
.wal-p-sm { padding: var(--wal-space-sm); }
.wal-p-md { padding: var(--wal-space-md); }
.wal-p-lg { padding: var(--wal-space-lg); }
.wal-p-xl { padding: var(--wal-space-xl); }
.wal-p-2xl { padding: var(--wal-space-2xl); }
.wal-p-3xl { padding: var(--wal-space-3xl); }
.wal-p-0 { padding: 0; }

.wal-pt-xs { padding-top: var(--wal-space-xs); }
.wal-pt-sm { padding-top: var(--wal-space-sm); }
.wal-pt-md { padding-top: var(--wal-space-md); }
.wal-pt-lg { padding-top: var(--wal-space-lg); }
.wal-pt-xl { padding-top: var(--wal-space-xl); }
.wal-pt-2xl { padding-top: var(--wal-space-2xl); }
.wal-pt-3xl { padding-top: var(--wal-space-3xl); }
.wal-pt-0 { padding-top: 0; }

.wal-pb-xs { padding-bottom: var(--wal-space-xs); }
.wal-pb-sm { padding-bottom: var(--wal-space-sm); }
.wal-pb-md { padding-bottom: var(--wal-space-md); }
.wal-pb-lg { padding-bottom: var(--wal-space-lg); }
.wal-pb-xl { padding-bottom: var(--wal-space-xl); }
.wal-pb-2xl { padding-bottom: var(--wal-space-2xl); }
.wal-pb-3xl { padding-bottom: var(--wal-space-3xl); }
.wal-pb-0 { padding-bottom: 0; }

.wal-pl-xs { padding-left: var(--wal-space-xs); }
.wal-pl-sm { padding-left: var(--wal-space-sm); }
.wal-pl-md { padding-left: var(--wal-space-md); }
.wal-pl-lg { padding-left: var(--wal-space-lg); }
.wal-pl-xl { padding-left: var(--wal-space-xl); }
.wal-pl-2xl { padding-left: var(--wal-space-2xl); }
.wal-pl-3xl { padding-left: var(--wal-space-3xl); }
.wal-pl-0 { padding-left: 0; }

.wal-pr-xs { padding-right: var(--wal-space-xs); }
.wal-pr-sm { padding-right: var(--wal-space-sm); }
.wal-pr-md { padding-right: var(--wal-space-md); }
.wal-pr-lg { padding-right: var(--wal-space-lg); }
.wal-pr-xl { padding-right: var(--wal-space-xl); }
.wal-pr-2xl { padding-right: var(--wal-space-2xl); }
.wal-pr-3xl { padding-right: var(--wal-space-3xl); }
.wal-pr-0 { padding-right: 0; }

.wal-px-xs { padding-inline: var(--wal-space-xs); }
.wal-px-sm { padding-inline: var(--wal-space-sm); }
.wal-px-md { padding-inline: var(--wal-space-md); }
.wal-px-lg { padding-inline: var(--wal-space-lg); }
.wal-px-xl { padding-inline: var(--wal-space-xl); }
.wal-px-2xl { padding-inline: var(--wal-space-2xl); }
.wal-px-3xl { padding-inline: var(--wal-space-3xl); }
.wal-px-0 { padding-inline: 0; }

.wal-py-xs { padding-block: var(--wal-space-xs); }
.wal-py-sm { padding-block: var(--wal-space-sm); }
.wal-py-md { padding-block: var(--wal-space-md); }
.wal-py-lg { padding-block: var(--wal-space-lg); }
.wal-py-xl { padding-block: var(--wal-space-xl); }
.wal-py-2xl { padding-block: var(--wal-space-2xl); }
.wal-py-3xl { padding-block: var(--wal-space-3xl); }
.wal-py-0 { padding-block: 0; }

/* Gap */
.wal-gap-xs { gap: var(--wal-space-xs); }
.wal-gap-sm { gap: var(--wal-space-sm); }
.wal-gap-md { gap: var(--wal-space-md); }
.wal-gap-lg { gap: var(--wal-space-lg); }
.wal-gap-xl { gap: var(--wal-space-xl); }
.wal-gap-2xl { gap: var(--wal-space-2xl); }
.wal-gap-3xl { gap: var(--wal-space-3xl); }
.wal-gap-0 { gap: 0; }

/* ── Typography Utilities ────────────────────────────────────────────── */
/* Font Families */
.wal-font-display { font-family: var(--wal-font-display); }
.wal-font-body { font-family: var(--wal-font-body); }
.wal-font-mono { font-family: var(--wal-font-mono); }
.wal-font-serif { font-family: var(--wal-font-serif); }
.wal-font-typewriter { font-family: var(--wal-font-typewriter); }
.wal-font-label { font-family: var(--wal-font-label); }

/* Sizes */
.wal-text-xs { font-size: var(--wal-text-xs); }
.wal-text-sm { font-size: var(--wal-text-sm); }
.wal-text-base { font-size: var(--wal-text-base); }
.wal-text-md { font-size: var(--wal-text-md); }
.wal-text-lg { font-size: var(--wal-text-lg); }
.wal-text-xl { font-size: var(--wal-text-xl); }
.wal-text-2xl { font-size: var(--wal-text-2xl); }
.wal-text-3xl { font-size: var(--wal-text-3xl); }
.wal-text-4xl { font-size: var(--wal-text-4xl); }
.wal-text-5xl { font-size: var(--wal-text-5xl); }

/* Weights */
.wal-weight-300 { font-weight: 300; }
.wal-weight-400 { font-weight: 400; }
.wal-weight-500 { font-weight: 500; }
.wal-weight-600 { font-weight: 600; }
.wal-weight-700 { font-weight: 700; }
.wal-weight-800 { font-weight: 800; }

/* Alignment */
.wal-text-center { text-align: center; }
.wal-text-left { text-align: left; }
.wal-text-right { text-align: right; }

/* Transform */
.wal-uppercase { text-transform: uppercase; }
.wal-lowercase { text-transform: lowercase; }
.wal-capitalize { text-transform: capitalize; }

/* Tracking */
.wal-tracking-tight { letter-spacing: -0.025em; }
.wal-tracking-normal { letter-spacing: 0; }
.wal-tracking-wide { letter-spacing: 0.05em; }

/* ── Color Utilities ─────────────────────────────────────────────────── */
/* Text Colors.
   These set `color`, so they resolve to the -ink form of each cue — the same
   cue shifted to stay legible as type on the page ground. .wal-color-gold with
   the plain --wal-gold was a paragraph you had to squint at on a light theme.
   Reach for --wal-gold directly when you want the cue as a FILL. */
.wal-color-accent { color: var(--wal-accent-ink); }
.wal-color-bloom { color: var(--wal-bloom-ink); }
.wal-color-gold { color: var(--wal-gold-ink); }
.wal-color-olive { color: var(--wal-olive-ink); }
.wal-color-cool { color: var(--wal-cool-ink); }
.wal-color-soft { color: var(--wal-text-soft); }
.wal-color-muted { color: var(--wal-text-muted); }

/* Backgrounds */
.wal-bg-surface { background-color: var(--wal-surface); }
.wal-bg-surface-2 { background-color: var(--wal-surface-2); }
.wal-bg-accent-soft { background-color: var(--wal-accent-soft); }
.wal-bg-bloom-soft { background-color: var(--wal-bloom-soft); }
.wal-bg-gold-soft { background-color: var(--wal-gold-soft); }
.wal-bg-olive-soft { background-color: var(--wal-olive-soft); }
.wal-bg-cool-soft { background-color: var(--wal-cool-soft); }

/* Fills — the cue itself, with a foreground that is computed to stay legible
   on it. Handy for a callout or a filled cell without writing both colours. */
.wal-fill-accent { background-color: var(--wal-accent); color: var(--wal-accent-fg); }
.wal-fill-bloom { background-color: var(--wal-bloom); color: var(--wal-bloom-fg); }
.wal-fill-gold { background-color: var(--wal-gold); color: var(--wal-gold-fg); }
.wal-fill-olive { background-color: var(--wal-olive); color: var(--wal-olive-fg); }
.wal-fill-cool { background-color: var(--wal-cool); color: var(--wal-cool-fg); }
/* Any colour as a fill, with .wal-tint naming it (04-components.css, 38. Tint). */
.wal-fill-tint { background-color: var(--wal-tint-fill); color: var(--wal-tint-fg); }

/* ── Display & Visibility ────────────────────────────────────────────── */

.wal-hidden { display: none; }
.wal-block { display: block; }
.wal-inline { display: inline; }
.wal-inline-block { display: inline-block; }
.wal-flex { display: flex; }
.wal-grid { display: grid; }

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

/* ── Border & Radius ─────────────────────────────────────────────────── */
.wal-border { border: var(--wal-border); }
.wal-border-top { border-top: var(--wal-border); }
.wal-border-bottom { border-bottom: var(--wal-border); }

.wal-rounded { border-radius: var(--wal-radius); }
.wal-rounded-sm { border-radius: var(--wal-radius-sm); }
.wal-rounded-lg { border-radius: var(--wal-radius-lg); }
.wal-rounded-full { border-radius: var(--wal-radius-full); }
.wal-rounded-none { border-radius: 0; }

/* ── Misc ────────────────────────────────────────────────────────────── */
.wal-shadow { box-shadow: var(--wal-shadow); }
.wal-shadow-sm { box-shadow: var(--wal-shadow-sm); }
.wal-shadow-lg { box-shadow: var(--wal-shadow-lg); }
.wal-shadow-none { box-shadow: none; }

.wal-glass {
  background-color: color-mix(in oklch, var(--wal-surface) 70%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

.wal-gradient-text {
  background: linear-gradient(135deg, var(--wal-accent), var(--wal-gold));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ── Print: a catalogue sheet ────────────────────────────────────────────
   Print a walnut page and it comes out as a sheet from a catalogue: A4,
   light, flat, numbered "Sheet 2 of 5" at the foot, with the furniture
   of a screen left off and every link's address printed after it, since
   paper cannot be clicked.

   What is left off: the masthead, the progress bar, the drawers and
   doors, and anything you mark data-wal-print="skip". A chest prints with
   every drawer out. Charts, rings, keys, colour scripts and swatches print
   in colour, because their colours are what they say; everything else
   prints as the browser decides. Headings stay with what follows them and
   a card, plate, metric or chart is never cut across two sheets.

   The !important is on purpose: a print sheet hides the nav even where a
   site's own unlayered CSS sets its display. */
@media print {
  @page {
    size: A4;
    margin: 18mm 16mm 20mm;

    @bottom-left {
      content: "Sheet " counter(page) " of " counter(pages);
      font: 8pt var(--wal-font-body);
      color: #666;
    }
  }

  :root,
  [data-theme],
  .wal-dark {
    color-scheme: light;
  }

  :root,
  [data-finish] {
    --wal-elevation: 0;
    --wal-wash: 0;
    --wal-tooth: none;
  }

  .wal-nav,
  .wal-progress-bar,
  .wal-drawer,
  .wal-drawer-overlay,
  .wal-door,
  [data-wal-print="skip"] {
    display: none !important;
  }

  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.8em;
    color: var(--wal-text-muted);
    overflow-wrap: anywhere;
  }

  h1, h2, h3, h4 { break-after: avoid; }

  figure, table, .wal-card, .wal-plate, .wal-metric, .wal-chart, .wal-ring {
    break-inside: avoid;
  }

  .wal-chart, .wal-ring, .wal-key, .wal-script, .wal-swatch, .wal-duotone {
    print-color-adjust: exact;
  }

  /* Out at once: with its transition left on, the change to print media
     slides every drawer out, and the sheet is set mid-slide. */
  .wal-chest details::details-content {
    block-size: auto;
    content-visibility: visible;
    transition: none;
  }

  .wal-chest details > *:not(summary) {
    translate: none;
    transition: none;
  }
}

}

/* ── cinema ── */
@layer cinema {
/* ═══════════════════════════════════════════════════════════════════════
   06-cinema.css
   Motion. The first two blocks move the colour script itself; the rest are
   general-purpose scroll and view-transition helpers.
   ═══════════════════════════════════════════════════════════════════════ */

/* The radius of the recolour circle (see "From the swatch" below). Declared
   out here because a registration is not a conditional rule. */
@property --wal-spread { syntax: "<length>"; inherits: false; initial-value: 0px; }

@media (prefers-reduced-motion: no-preference) {

  /* ── Retune ────────────────────────────────────────────────────────────
     When the rods move, the page travels with them instead of cutting.

         <html class="wal-press wal-retune">

     Every colour in walnut is derived from registered numbers, and a
     registered number interpolates. So a palette class, a finish, or a rod
     written from script tweens the *inputs*, and every surface, hairline,
     ink and shadow re-derives on each frame from wherever the inputs have
     got to. That is not a crossfade between two pictures of the page: a hue
     travels along the wheel, so every intermediate frame is still a coherent
     script rather than a muddy mix of two.

     Along, not round: a registered <number> interpolates numerically, with
     no notion of the short way round. Café's ground (55) to Dusk's (280)
     passes through green and blue. A palette that wants the short path to
     a neighbour writes its hue within 180 of it (-80 is the same hue as 280).

     Derived hues stay locked to their source. With the default
     `--wal-bloom-hue: calc(var(--wal-accent-hue) + 27)`, bloom's own
     transition runs on the same timing as the accent's, from a value 27°
     away to a value 27° away, so the offset holds on every frame.

     Opt-in, because a tween is wrong for direct manipulation: a slider bound
     to a rod should move the page under the thumb, not 800ms behind it.
     Take the class off while a control is being dragged.

     Mode is not included and cannot be. color-scheme is discrete, and
     light-dark() picks a branch rather than blending two. A root view
     transition is the tool for that; see the crossfade further down. */
  .wal-retune {
    transition-property:
      --wal-hue, --wal-chroma,
      --wal-accent-hue, --wal-accent-chroma,
      --wal-bloom-hue,  --wal-bloom-chroma,
      --wal-gold-hue,   --wal-gold-chroma,
      --wal-olive-hue,  --wal-olive-chroma,
      --wal-cool-hue,   --wal-cool-chroma,
      --wal-l-bg-light, --wal-l-bg-dark,
      --wal-l-subtle-light, --wal-l-subtle-dark,
      --wal-l-surface-light, --wal-l-surface-dark,
      --wal-l-surface-2-light, --wal-l-surface-2-dark,
      --wal-l-line-light, --wal-l-line-dark,
      --wal-l-line-soft-light, --wal-l-line-soft-dark,
      --wal-l-line-input-light, --wal-l-line-input-dark,
      --wal-l-text-light, --wal-l-text-dark,
      --wal-l-text-soft-light, --wal-l-text-soft-dark,
      --wal-l-text-muted-light, --wal-l-text-muted-dark,
      --wal-l-accent-light, --wal-l-accent-dark,
      --wal-l-bloom-light, --wal-l-bloom-dark,
      --wal-l-gold-light, --wal-l-gold-dark,
      --wal-l-olive-light, --wal-l-olive-dark,
      --wal-l-cool-light, --wal-l-cool-dark,
      --wal-hover-shift-light, --wal-hover-shift-dark,
      --wal-ink-shift-light, --wal-ink-shift-dark,
      --wal-cx-ground-light, --wal-cx-ground-dark,
      --wal-cx-surface-light, --wal-cx-surface-dark,
      --wal-cx-line-light, --wal-cx-line-dark,
      --wal-cx-ink-light, --wal-cx-ink-dark,
      --wal-radius, --wal-radius-sm, --wal-radius-lg,
      --wal-elevation, --wal-wash, --wal-line-boost;
    transition-duration: var(--wal-retune-duration, 0.3s);
    /* Symmetric, unlike --wal-ease. Expo-out suits a thing arriving; a rod
       being turned from one setting to another should start and stop like a
       dial under a hand. */
    transition-timing-function: var(--wal-ease-travel, cubic-bezier(0.65, 0, 0.35, 1));
  }

  /* ── Cue in ────────────────────────────────────────────────────────────
     The page prints itself on load: the paper first, then the five cues in
     script order, each rising from zero chroma — a grey proof taking its
     colour one plate at a time.

         <html class="wal-press wal-cue-in">

     Only the chroma rods move. Lightness never does, so contrast is final
     from the first frame and nothing is unreadable while it runs. Each
     keyframe states only `from`, which makes the end of every pass the
     element's own value — the same six keyframes print any palette,
     including one written inline.

     Runs once, on first style resolution. A pass is --wal-cue-in-pass long
     and each cue starts --wal-cue-in-stagger after the one before it; the
     ground leads the first cue by twice that. `backwards` holds every cue
     at zero until its turn. It uses animation-name as a list, so put it on
     an element that has no other animation — <html> is the intended one. */
  @keyframes wal-cue-in-ground { from { --wal-chroma: 0; } }
  @keyframes wal-cue-in-accent { from { --wal-accent-chroma: 0; } }
  @keyframes wal-cue-in-bloom  { from { --wal-bloom-chroma: 0; } }
  @keyframes wal-cue-in-gold   { from { --wal-gold-chroma: 0; } }
  @keyframes wal-cue-in-olive  { from { --wal-olive-chroma: 0; } }
  @keyframes wal-cue-in-cool   { from { --wal-cool-chroma: 0; } }

  .wal-cue-in {
    animation-name: wal-cue-in-ground, wal-cue-in-accent, wal-cue-in-bloom,
                    wal-cue-in-gold, wal-cue-in-olive, wal-cue-in-cool;
    animation-duration: var(--wal-cue-in-pass, 0.7s);
    animation-delay:
      0s,
      calc(var(--wal-cue-in-stagger, 0.14s) * 2),
      calc(var(--wal-cue-in-stagger, 0.14s) * 3),
      calc(var(--wal-cue-in-stagger, 0.14s) * 4),
      calc(var(--wal-cue-in-stagger, 0.14s) * 5),
      calc(var(--wal-cue-in-stagger, 0.14s) * 6);
    animation-timing-function: var(--wal-ease);
    animation-fill-mode: backwards;
  }

  /* ── Scroll-driven Reveals ─────────────────────────────────────────── */
  
  /* Shared keyframes for reveals */
  @keyframes wal-reveal-fade-up {
    from {
      opacity: 0;
      transform: translateY(30px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  @keyframes wal-reveal-slide-left {
    from {
      opacity: 0;
      transform: translateX(-50px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }

  @keyframes wal-reveal-slide-right {
    from {
      opacity: 0;
      transform: translateX(50px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }

  @keyframes wal-reveal-scale-up {
    from {
      opacity: 0;
      transform: scale(0.9);
    }
    to {
      opacity: 1;
      transform: scale(1);
    }
  }

  .wal-reveal,
  .wal-reveal-left,
  .wal-reveal-right,
  .wal-reveal-scale {
    will-change: transform, opacity;
    animation-timing-function: var(--wal-ease);
    animation-fill-mode: both;
    /* Use view timeline for scroll-driven animations */
    animation-timeline: view();
    /* Natural entrance feel */
    animation-range: entry 10% cover 35%;
  }

  .wal-reveal {
    animation-name: wal-reveal-fade-up;
  }

  .wal-reveal-left {
    animation-name: wal-reveal-slide-left;
  }

  .wal-reveal-right {
    animation-name: wal-reveal-slide-right;
  }

  .wal-reveal-scale {
    animation-name: wal-reveal-scale-up;
  }

  /* Develop: a photograph comes up out of blank paper as it rises into
     view, the way a print does in the developer tray.

         <figure class="wal-duotone wal-reveal-develop"><img …></figure>

     Brightness pushes every grey to white; as it falls back, the darkest
     greys drop below white first, so the shadows come up first and the
     highlights last. On the image, not the figure: the figure's ink and
     paper stay put while the picture develops in them. Made for
     .wal-duotone, whose filter it ends on. Like the other reveals it is
     tied to scrolling, so scrolling back up takes the print back to paper. */
  @keyframes wal-develop { from { filter: grayscale(1) contrast(1.08) brightness(7); } }
  .wal-reveal-develop img {
    will-change: filter;
    transform: translateZ(0);
    animation: wal-develop linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 50%;
    animation-timing-function: cubic-bezier(0.3, 0.7, 0.4, 1);
  }

  /* ── Scroll Progress ───────────────────────────────────────────────── */
  
  @keyframes wal-progress-grow {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
  }

  .wal-progress-bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 3px;
    background: var(--wal-accent);
    transform-origin: 0 50%;
    animation: wal-progress-grow linear;
    animation-timeline: scroll();
    z-index: 1000;
  }

  /* Without scroll timelines the declaration above is dropped, the animation
     runs on the clock at its default 0s and leaves no fill — so the bar sat
     at full width across the top of every page. Better absent than wrong.
     The reveals need no guard: `fill-mode: both` leaves them visible. */
  @supports not (animation-timeline: scroll()) {
    .wal-progress-bar { display: none; }
  }

  /* ── Parallax ──────────────────────────────────────────────────────── */

  @keyframes wal-parallax-move {
    from { transform: translateY(-10%); }
    to { transform: translateY(10%); }
  }

  .wal-parallax {
    animation: wal-parallax-move linear;
    animation-timeline: scroll();
  }

  /* ── View Transitions ──────────────────────────────────────────────── */

  /* Between pages. A same-origin navigation from one walnut page to another
     crossfades instead of cutting: it becomes a view transition, and plays
     the root crossfade below. Whatever the two pages share — the masthead,
     the desk, the ground — is the same pixels on both sides of the fade, so
     it holds still while only the papers change. Nothing slides.

     Name an element the same on both pages and it travels between them
     instead of fading: a card on one page and the heading of its detail page,
     say, and the card grows into the page. Name it only for the navigation —
     in the old page's `pageswap` event and the new page's `pagereveal` —
     if the page also runs same-document transitions, or they will catch it
     too. The groups take walnut's easing and duration below.

     Both pages need this rule, which any walnut page has. Opt out with your
     own `@view-transition { navigation: none; }`, which outranks this one. A
     browser without cross-document transitions (Firefox; Safari before 18.2)
     simply navigates, and so does reduced motion. */
  @view-transition { navigation: auto; }

  ::view-transition-group(*) {
    animation-duration: var(--wal-duration-slow);
    animation-timing-function: var(--wal-ease);
  }

  /* View transition name helpers */
  .wal-transition-name-hero { view-transition-name: wal-hero; }
  .wal-transition-name-header { view-transition-name: wal-header; }
  .wal-transition-name-image { view-transition-name: wal-image; }
  .wal-transition-name-title { view-transition-name: wal-title; }

  /* Cinematic cross-fade for old/new view transitions */
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: var(--wal-duration-slow);
    animation-timing-function: var(--wal-ease);
  }

  ::view-transition-old(*) {
    animation-name: wal-breathe-out;
  }
  
  ::view-transition-new(*) {
    animation-name: wal-breathe-in;
  }

  @keyframes wal-breathe-out {
    to { opacity: 0; transform: scale(0.98); }
  }

  @keyframes wal-breathe-in {
    from { opacity: 0; transform: scale(1.02); }
  }

  /* The root snapshot is the whole page, and scaling the whole page reads as
     the browser zooming rather than as a cut. Named elements keep the breathe
     above; the root only crossfades — which is what a mode switch needs,
     since color-scheme cannot tween (see Retune). */
  ::view-transition-old(root) { animation-name: wal-fade-out; }
  ::view-transition-new(root) { animation-name: wal-fade-in; }

  @keyframes wal-fade-out { to   { opacity: 0; } }
  @keyframes wal-fade-in  { from { opacity: 0; } }

  /* ── From the swatch ───────────────────────────────────────────────────
     The recolour walnut-recolour.js runs for data-wal-recolour="swatch": the
     new palette opens out of the button that asked for it, in a circle with
     a soft edge, until it covers the page or the section being recoloured.

         <html class="wal-forest" data-wal-recolour="swatch">
         <button data-wal-palette="wal-dusk">Dusk</button>

     The script names the recoloured element wal-recolour for the length of
     the change, and leaves the circle's centre and final radius in
     --wal-recolour-x, -y and -r on <html>. Everything else is here: the
     "after" picture is masked by a radial gradient whose radius is a
     registered length, so the browser can tween it.

     A view transition, not a tween of the rods, and that is the point:
     every frame shows either the old palette or the finished new one, never
     a mix, so contrast is final from the first frame. A mask is not a
     clip-path: the edge is a 96px fade, light spreading rather than a
     shape being cut.

     While it runs the rest of the page holds still and nothing in the new
     picture replays a colour transition of its own. An element with a
     permanent view-transition-name of its own is pictured separately and
     crossfades instead; name such things only for their own transitions.
     Lasts --wal-recolour-duration. */
  :root:active-view-transition-type(wal-recolour)::view-transition-group(root),
  :root:active-view-transition-type(wal-recolour)::view-transition-group(wal-recolour) { animation: none; }
  :root:active-view-transition-type(wal-recolour)::view-transition-old(root),
  :root:active-view-transition-type(wal-recolour)::view-transition-new(root),
  :root:active-view-transition-type(wal-recolour)::view-transition-old(wal-recolour),
  :root:active-view-transition-type(wal-recolour)::view-transition-new(wal-recolour) {
    animation: none;
    mix-blend-mode: normal;
  }
  :root:active-view-transition-type(wal-recolour),
  :root:active-view-transition-type(wal-recolour) *,
  :root:active-view-transition-type(wal-recolour) *::before,
  :root:active-view-transition-type(wal-recolour) *::after { transition: none !important; }

  :root:active-view-transition-type(wal-swatch)::view-transition-new(wal-recolour) {
    mask-image: radial-gradient(circle at var(--wal-recolour-x, 50%) var(--wal-recolour-y, 50%),
                                #000 calc(var(--wal-spread) - 96px), transparent var(--wal-spread));
    animation: wal-spread var(--wal-recolour-duration, 340ms) var(--wal-ease, cubic-bezier(0.16, 1, 0.3, 1)) both;
  }
  @keyframes wal-spread { to { --wal-spread: var(--wal-recolour-r, 150vmax); } }

  /* ── Shuffle ───────────────────────────────────────────────────────────
     A grid that is filtered or sorted, with every item travelling to its
     new place instead of jumping there. walnut.shuffle (walnut-motion.js)
     runs the change as a view transition scoped to the list:

         <ul class="wal-shuffle" id="tiles">…</ul>
         walnut.shuffle(tiles, () => { …hide, show or reorder items… });

     While it runs, .is-shuffling gives each item a view-transition name of
     its own, made by the browser from the element (match-element), so
     nothing needs an id. The names are on only for the change, so no other
     transition catches them. An item that stays travels; one that leaves
     shrinks and fades; one that arrives grows in once the others are on
     their way. A browser without scoped transitions runs it on the
     document, with the names given by the script, since it may not know
     match-element either. Without any transitions, the change simply
     happens. */
  .wal-shuffle.is-shuffling > * {
    view-transition-name: match-element;
    view-transition-class: wal-shuffle;
  }

  ::view-transition-group(*.wal-shuffle) {
    animation-duration: 0.52s;
    animation-timing-function: var(--wal-ease-glide);
  }

  ::view-transition-old(*.wal-shuffle):only-child {
    animation: wal-shuffle-out 0.26s cubic-bezier(0.4, 0, 1, 1) both;
  }

  ::view-transition-new(*.wal-shuffle):only-child {
    animation: wal-shuffle-in 0.42s 0.14s var(--wal-ease-glide) both;
  }

  @keyframes wal-shuffle-out { to { opacity: 0; scale: 0.9; translate: 0 0.4rem; } }
  @keyframes wal-shuffle-in { from { opacity: 0; scale: 0.92; translate: 0 0.6rem; } }

  /* ── Materials ─────────────────────────────────────────────────────────
     Weight, felt in how a thing comes to a stop.

         <html data-motion="materials">
         <div class="wal-chest" data-wal-material="stone">…</div>

     walnut  For drawers and doors. Slow to get going and heavy when it
             lands: a drawer hits its stop, knocks back, and settles. A
             second and a bit out, 0.8s home.
     brass   For hardware: menus, pickers, a chart's columns and ring. Quick
             to its stop, a quarter of a second, and then it rings like a
             spring, three knocks each smaller and quicker than the last.
     stone   For big, slow surfaces. No bounce at all; it grinds to a
             dead stop. Over a second each way.

     data-motion="materials" on <html> gives every part its own: walnut for
     .wal-chest, .wal-door and .wal-window, brass for .wal-menu, .wal-select,
     .wal-chart and .wal-ring. data-wal-material names one for an element
     and for everything inside it. Without either, openings glide and
     settle as they always have.

     A material is no more than the four opening tokens (01-tokens.css),
     each curve a small model of the thing sampled into linear(). A part of
     your own takes a material by moving on them:

         transition: translate var(--wal-open-duration) var(--wal-open-ease);

     Like everything in this layer it is off under reduced motion, where
     the reset makes every opening instant. */
  [data-wal-material="walnut"],
  :where(:is([data-motion="materials"], [data-motion="materials"] *):is(.wal-chest, .wal-door, .wal-window):not([data-wal-material] *)) {
    --wal-open-duration:  360ms;
    --wal-open-ease:      var(--wal-ease-walnut);
    --wal-close-duration: 260ms;
    --wal-close-ease:     var(--wal-ease-walnut-shut);
  }

  [data-wal-material="brass"],
  :where(:is([data-motion="materials"], [data-motion="materials"] *):is(.wal-menu, .wal-select, .wal-chart, .wal-ring):not([data-wal-material] *)),
  :where([data-wal-material="brass"] *:is(.wal-menu, .wal-select)) {
    --wal-open-duration:  240ms;
    --wal-open-ease:      var(--wal-ease-brass);
    --wal-close-duration: 180ms;
    --wal-close-ease:     var(--wal-ease-brass);
  }

  [data-wal-material="stone"],
  :where([data-wal-material="stone"] *:is(.wal-menu, .wal-select)) {
    --wal-open-duration:  420ms;
    --wal-open-ease:      var(--wal-ease-stone);
    --wal-close-duration: 300ms;
    --wal-close-ease:     var(--wal-ease-stone);
  }

  /* Menus and pickers set a quick pace on themselves (04-components.css),
     which an ancestor's material would not reach by inheritance; these
     carry it down to them. */
  :where([data-wal-material="walnut"] *:is(.wal-menu, .wal-select)) {
    --wal-open-duration:  360ms;
    --wal-open-ease:      var(--wal-ease-walnut);
    --wal-close-duration: 260ms;
    --wal-close-ease:     var(--wal-ease-walnut-shut);
  }

  /* ── Cinematic Keyframes ───────────────────────────────────────────── */

  @keyframes wal-drift {
    0% { transform: translateY(0); }
    50% { transform: translateY(-10px); }
    100% { transform: translateY(0); }
  }

  @keyframes wal-pulse {
    0% { transform: scale(1); }
    50% { transform: scale(1.03); }
    100% { transform: scale(1); }
  }

  @keyframes wal-shimmer {
    0% { background-position: -200% center; }
    100% { background-position: 200% center; }
  }

  @keyframes wal-breathe {
    0% { opacity: 1; }
    50% { opacity: 0.7; }
    100% { opacity: 1; }
  }

  @keyframes wal-letterbox-open {
    0% { transform: scaleY(1); }
    100% { transform: scaleY(0); }
  }

  /* ── Utility Animation Classes ─────────────────────────────────────── */

  .wal-animate-drift {
    animation: wal-drift 4s var(--wal-ease) infinite;
  }

  .wal-animate-pulse {
    animation: wal-pulse 3s var(--wal-ease) infinite;
  }

  .wal-animate-breathe {
    animation: wal-breathe 5s var(--wal-ease) infinite;
  }

  /* Animation delays */
  .wal-delay-100 { animation-delay: 100ms; }
  .wal-delay-200 { animation-delay: 200ms; }
  .wal-delay-300 { animation-delay: 300ms; }
  .wal-delay-400 { animation-delay: 400ms; }
  .wal-delay-500 { animation-delay: 500ms; }
  .wal-delay-600 { animation-delay: 600ms; }
  .wal-delay-700 { animation-delay: 700ms; }
  .wal-delay-800 { animation-delay: 800ms; }
  .wal-delay-900 { animation-delay: 900ms; }
  .wal-delay-1000 { animation-delay: 1000ms; }

}

/* ── Forced colours ────────────────────────────────────────────────────
   Windows High Contrast and friends replace every colour with a handful of
   system ones and drop gradient backgrounds. Anything walnut draws as a
   filled box or a gradient rather than a border — rules, markers, the nav
   underline, the progress bar — would vanish, so those are redrawn as
   borders or in system colours. It lives in this, the last layer, so it
   wins over whichever layer drew the original. */
@media (forced-colors: active) {
  .wal-rule::before,
  .wal-rule::after,
  .wal-divider::before,
  .wal-divider::after {
    background: none;
    block-size: 0;
    border-block-start: var(--wal-hairline) solid CanvasText;
  }

  .wal-nav .wal-nav-links a::after {
    background: none;
    border-block-end: 2px solid LinkText;
  }

  .wal-list-dash li::before,
  .wal-avail .wal-avail-dot {
    forced-color-adjust: none;
    background: CanvasText;
  }

  .wal-progress-bar {
    forced-color-adjust: none;
    background: Highlight;
  }

  /* The gradient arrow is gone here; let the platform draw its own. */
  select.wal-input {
    appearance: auto;
    background-image: none;
  }

  /* A colour chart's colours are its content. */
  .wal-script > *::before,
  .wal-swatch .wal-swatch-color {
    forced-color-adjust: none;
  }

  /* So are a chart's, and its columns and shares are backgrounds. The
     stringing and the rail go to the system's text colour. */
  .wal-chart > li > span::after,
  .wal-ring::before,
  .wal-key > li::before {
    forced-color-adjust: none;
  }

  .wal-chart {
    background: none;
    border-block-end: 2px solid CanvasText;
  }

  .wal-chart::after { display: none; }

  /* A stuck masthead's rule is a filled box. */
  .wal-nav::after {
    background: none;
    border-block-end: var(--wal-hairline) solid CanvasText;
  }

  /* The grain is texture, not content, and would only be forced to Canvas
     on a Canvas ground. */
  .wal-desk::before { display: none; }

  /* A duotone's ink is a background, which is forced to Canvas, and a grey
     photograph screened over Canvas is a white rectangle. The photograph
     is the content: show it plainly. */
  .wal-duotone img { mix-blend-mode: normal; }
  .wal-duotone::after { display: none; }
}

}

