/* ==========================================================================
   Moorwell - marketing site
   --------------------------------------------------------------------------
   Self-contained by design. No CDN, no webfont, no remote image, no
   analytics. Every colour below is a token lifted from the app's own palette
   in lib/data/style.dart, so the site and the product read as one thing.

   Two themes, and the reader picks. The masthead carries a System / Light /
   Dark control; the choice is written to one localStorage key and read back
   by a short inline script in <head> before first paint. With JavaScript off
   the control is hidden and prefers-color-scheme decides, which is what this
   page did before the control existed.

   Contrast, measured (WCAG 2.1, ratio against the surface each sits on):
     light  body text 12.85  soft text 5.36  accent link 6.35
            control border 3.07  hairline 1.19 (decorative, unbound)
     dark   body text 13.91  soft text 7.61  accent link 5.42
            control border 4.35  hairline 1.28 (decorative, unbound)
   A control *border* wants 3:1 (WCAG 1.4.11); text wants 4.5:1. Those are
   different jobs and --border is deliberately not drawn at text weight.

   Elevation is declared once per mode, following docs/Design Language.md:
   light surfaces carry a hairline border, dark surfaces carry a lifted fill
   and no border. A 1px border under a soft shadow is the ghost card.

   Radius rule for the whole site: cards 16px, small controls pill, inline
   code 6px. Nothing else.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  color-scheme: light dark;

  /* Light palette (AppColors: bone, surface, ink, inkSoft, hairline, border,
     sageText). Light is the default, not a flip of the dark one: the page is
     bone rather than white, and the accent is the darkened sageText variant,
     because the original sage reaches only 3:1 on bone. */
  --page: #f4f2ec;
  --surface: #fbfaf6;
  --surface-sunk: #efece3;
  --text: #2b2a28;
  --text-soft: #67645f;
  --hairline: #e4e0d6;
  --border: #8f897b;
  --accent: #4a5d46;
  --accent-quiet: #657961;
  --accent-on: #ffffff;
  --wash: color-mix(in srgb, var(--accent) 7%, transparent);
  --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);
  --chrome: rgba(244, 242, 236, 0.82);
  --shadow: 0 1px 2px rgba(43, 42, 40, 0.04),
            0 16px 40px -20px rgba(43, 42, 40, 0.16);
  --card-border: 1px solid var(--hairline);

  /* Type */
  --font-body: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI",
    "Helvetica Neue", sans-serif;
  --font-head: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Space */
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --section: clamp(4rem, 9vw, 7.5rem);
  --measure: 62ch;

  /* The changelog's own column, narrower than --wrap and wider than
     --measure, because a release row is not a paragraph: it is a version
     rail, a summary at --measure's neighbour of 58ch, and a toggle, side by
     side. 60rem is the sum of those three plus the card's padding, which is
     why it is that number and not a round one. See section 15. */
  --log-column: 60rem;
  --log-pad: clamp(1.25rem, 3vw, 2rem);
  --log-rail: 9rem;
  --log-gap: var(--s-7);

  /* Shape */
  --r-card: 16px;

  /* ---- THE RAIL GAP -------------------------------------------------
     The distance from a vertical rule to the text it identifies, in one
     place, because it was in seven and every one of them was too small.
     Reported plainly: *"the verticle line you draw in some places and then
     doesnt give any right space and put the text so close to the line it
     looks like a bug"*.

     It was 0.9rem-1.1rem, or about 15px, which is under the type's own
     line-height and reads as a collision rather than as an indent. 1.6rem
     is a little over one line of body copy, which is the distance that
     makes a rule read as a margin the text is set against. */
  /* ---- THE SPACE SCALE -----------------------------------------------
     Measured before it was written: the stylesheet carried **100 distinct
     hardcoded spacing values** — 0.4, 0.5, 0.6, 0.625, 0.7, 0.75, 0.875,
     0.9 and 1rem all in use, often within one component. That is not a
     rhythm, it is noise, and it is the web form of the defect the app audit
     found in its own units (`spaceScale` reaches only inside a card, so
     every gap between two cards is a hardcoded number).

     It is also the honest answer to a complaint the owner has now made three
     times about spacing. A reader cannot name it, but the eye reads
     eight near-identical gaps as a surface that was assembled rather than
     composed — which is precisely the word that keeps coming back.

     A 4px base on a 1.5 ratio at the top end. The odd values below snap to
     the nearest step; the deliberate `clamp()` values are left alone,
     because a fluid value is a decision and these are not. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.25rem;
  --s-6: 1.5rem;
  --s-7: 2rem;
  --s-8: 3rem;

  --rail-gap: clamp(1.35rem, 2.4vw, 1.75rem);
  --claim-gap: clamp(2rem, 5vw, 4.5rem);
  --rail-gap-wide: clamp(1.6rem, 3vw, 2.25rem);
  --r-pill: 999px;
  --r-code: 6px;

  /* Section 21's block shadow. The ink triple --shadow already uses, at the
     alpha a zero-blur offset needs to read on bone. Not a new colour: the
     same 43, 42, 40 that section 18's --shadow-lift is built from. */
  --spec-block: rgba(43, 42, 40, 0.28);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* --------------------------------------------------------------------------
   Dark, twice, on purpose.
   --------------------------------------------------------------------------
   One block for the reader who asked for it, one for the reader who asked
   their operating system instead. CSS has no way to write a single rule that
   matches both an attribute and a media query, and the alternatives (a
   `light-dark()` pair on every token, or a custom-property space toggle) cost
   more than they save.

   **KEEP THE TWO BODIES BYTE-IDENTICAL.** A token added to one and not the
   other is invisible until somebody tests the other path.

   Both selectors are specificity (0,2,0), and the media block comes second so
   an explicit `light` always wins over an OS that prefers dark.
   -------------------------------------------------------------------------- */

:root[data-theme="dark"] {
  /* Dark palette (AppColors: inkBg, inkSurface, inkText, inkTextSoft,
     inkHairline, inkBorder, sage). */
  --page: #1c1e1d;
  --surface: #272a28;
  --surface-sunk: #222524;
  --text: #edeae2;
  --text-soft: #b2afa0;
  --hairline: #3a3d3a;
  --border: #858378;
  --accent: #83987e;
  --accent-quiet: #9ab094;
  --accent-on: #1c1e1d;
  --wash: color-mix(in srgb, var(--accent) 7%, transparent);
  --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);
  --chrome: rgba(28, 30, 29, 0.82);
  --shadow: none;
  --card-border: 1px solid transparent;
  /* Section 18: an opaque black shadow on the dark page reaches 1.253:1, so
     the ink block is invisible here. --hairline is 1.523 against the page and
     carries the same shape upward instead. */
  --spec-block: #3a3d3a;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    /* 🔴 `:not([data-theme="sage"])` as well, and that is a bug fix rather
       than tidiness. This selector predates the third look: written as
       `:not([data-theme="light"])` it also matches `[data-theme="sage"]`, so
       a reader who chose Sage on a machine set to dark got Night's tokens
       painted under Sage's name. The attribute form was right and the media
       form silently disagreed with it — which is the exact drift the
       byte-identical assertion below this file's first group exists to
       catch, in the one direction it could not see. */
        --wash: color-mix(in srgb, var(--accent) 7%, transparent);
    --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);
    --chrome: rgba(28, 30, 29, 0.82);
    --card-border: 1px solid transparent;
    --spec-block: #3a3d3a;
    --page: #0c0a12;
    --surface: #19171f;
    --surface-sunk: #110f17;
    --text: #f6f3fb;
    --text-soft: #aba4be;
    --hairline: #2c2934;
    --border: #676274;
    --accent: #cfbdf2;
    --accent-quiet: #877b9f;
    --accent-on: #191325;
    --ink-sage: #a8afa6;
    --ink-dusk: #a6acb3;
    --ink-clay: #c8b3aa;
    --ink-heather: #aea9b8;
    --ink-sand: #b3aea0;
  }
}

/* Form controls, scrollbars and the canvas the browser paints outside the
   body all read `color-scheme`, not our variables. Without these two lines a
   forced theme leaves white scrollbars on a dark page. */
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   1b. The accent family
   --------------------------------------------------------------------------
   Sage, dusty blue, terracotta, mauve, ochre. All five are `AppColors`
   constants and all five already ship in the product; the page had been
   using one of them.

   Colour here is not decoration for its own sake. `lib/data/style.dart`
   records the decision and the evidence: the clinical constraint is a
   **chroma ceiling**, not an absence of hue - Valdez & Mehrabian put arousal
   in saturation, and the app's own rule moved to "muted is the default, not
   the whole idea". These are the muted accents, unchanged, at the chroma the
   app ships them at.

   Two variants per hue, because an accent has two jobs:
     --fill-*  the tint behind a card. Decorative, not WCAG-bound.
     --ink-*   the same hue darkened (light) or lifted (dark) only as far as
               AA needs. `AppColors.*Text` / `AppColors.*OnDark`.
   Never colour text with a --fill-*. That is the exact defect
   docs/Design Language.md records.
   -------------------------------------------------------------------------- */

:root {
  /* THE 2.0 FAMILY HUES, from 03-tokens-and-components.md's own table. These
     are one axis of the redesign that is easy to miss because no element
     names them directly: every card tint, every illustration wash and every
     bloom in the field reads `rgba(var(--hue), var(--fill))`, so replacing
     five triples here repaints the site's colour at page scale without
     touching a component.

     They are brighter and cleaner than 1.0's, which were chosen to be muted
     against a warm bone ground. The 2.0 ground is paler and the tints have to
     carry across it. Every `--ink-*` above was solved against tints computed
     from EXACTLY these triples — 60 pairs, all clearing AA — so the two sets
     move together or not at all. */
  --rgb-sage: 174, 203, 158;
  --rgb-dusk: 170, 192, 218;
  --rgb-clay: 198, 124, 96;
  --rgb-heather: 186, 172, 214;
  --rgb-sand: 226, 208, 148;

  /* The tint alphas are a solved ceiling, not a taste setting. Above 0.22
     the muted accents stop being decoration: --text-soft on the strongest
     tint drops under AA (4.23 at 0.30), and so does the accent heading over
     its own tint in dark (4.25). Both are the "solved against a background
     that is not the one rendered" defect docs/Design Language.md records.
     Raise these and re-run the numbers, or do not raise them. */
  --fill: 0.17;
  --fill-hi: 0.22;

  --ink-sage: #4a5d46;
  --ink-dusk: #485c6f;
  --ink-clay: #7b503c;
  --ink-heather: #61556c;
  --ink-sand: #68593d;

  /* AppColors.moodScale - the reference ladder. Luminance ramps
     monotonically, so the scale still reads with colour removed. */
  --mood-1: #5d5068;
  --mood-2: #516a80;
  --mood-3: #6e7a73;
  --mood-4: #70906c;
  --mood-5: #789783;
}

:root[data-theme="dark"] {
  --fill: 0.16;
  --fill-hi: 0.20;
    /* 0.20 in Night, not 0.22. §1b fixed 0.22 as the ceiling where the 1.0
       hues stopped carrying --text-soft; the 2.0 hues are brighter, and sand
       — the brightest of the five — took soft text to 4.31 over the dark
       surface, under AA. Measured across all five hues and all three dark
       grounds, 0.20 is the highest alpha where every one of the fifteen
       pairs still clears: the worst is sand at 4.57. The light looks keep
       0.22, where their own solve is clean. */
  --ink-sage: #9fb59a;
  --ink-dusk: #9eb9d0;
  --ink-clay: #e1aa90;
  --ink-heather: #c0b0cd;
  --ink-sand: #c6b48f;
  --mood-1: #9083a2;
  --mood-2: #7f9dbb;
  --mood-3: #9aa79f;
  --mood-4: #93bb8e;
  --mood-5: #a6c9ad;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --fill: 0.16;
    --fill-hi: 0.20;
    /* 0.20 in Night, not 0.22. §1b fixed 0.22 as the ceiling where the 1.0
       hues stopped carrying --text-soft; the 2.0 hues are brighter, and sand
       — the brightest of the five — took soft text to 4.31 over the dark
       surface, under AA. Measured across all five hues and all three dark
       grounds, 0.20 is the highest alpha where every one of the fifteen
       pairs still clears: the worst is sand at 4.57. The light looks keep
       0.22, where their own solve is clean. */
    --ink-sage: #9fb59a;
    --ink-dusk: #9eb9d0;
    --ink-clay: #e1aa90;
    --ink-heather: #c0b0cd;
    --ink-sand: #c6b48f;
    --mood-1: #9083a2;
    --mood-2: #7f9dbb;
    --mood-3: #9aa79f;
    --mood-4: #93bb8e;
    --mood-5: #a6c9ad;
  }
}

/* One class per hue. An element that carries it gets the tint, the readable
   ink, and a matching edge, and everything downstream reads --hue-* rather
   than naming a colour. */
.hue-sage   { --hue: var(--rgb-sage);    --hue-ink: var(--ink-sage); }
.hue-dusk   { --hue: var(--rgb-dusk);    --hue-ink: var(--ink-dusk); }
/* Clay tints at .20 where the other four tint at .24 — the design's HUES
   table gives it its own alpha, and the reason is visible once you look for
   it: clay is the most saturated of the five, so an equal alpha reads louder
   than the rest and the family stops looking like one family. `--tint-a` was
   already the knob; nothing else had ever turned it. */
.hue-clay   { --hue: var(--rgb-clay);    --hue-ink: var(--ink-clay); --tint-a: 0.20; }
.hue-heather{ --hue: var(--rgb-heather); --hue-ink: var(--ink-heather); }
.hue-sand   { --hue: var(--rgb-sand);    --hue-ink: var(--ink-sand); }

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: 6rem;
}

body {
  margin: 0;
  background: var(--page);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-head);
  font-weight: 400;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  color: var(--text);
}

ul,
ol,
dl,
dd {
  margin: 0;
  padding: 0;
}

li {
  list-style: none;
}

code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--wash);
  border-radius: var(--r-code);
  padding: 0.08em 0.28em;
  overflow-wrap: anywhere;
}

strong {
  font-weight: 600;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

::selection {
  background: var(--wash-strong);
}

/* --------------------------------------------------------------------------
   3. Layout helpers
   -------------------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: 1120px;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.prose {
  max-width: var(--measure);
}

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

.section--sunk {
  background: var(--surface-sunk);
  border-block: 1px solid var(--hairline);
}

/* A plain section that has to sit next to another plain section. The sunk
   bands draw their own edges, so the only boundary the page was missing is
   the one below a page-coloured section followed by a page-coloured
   section. */
.section--edge {
  border-bottom: 1px solid var(--hairline);
}

.skip {
  position: absolute;
  left: var(--gutter);
  top: 0;
  z-index: 20;
  transform: translateY(-140%);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 0.6rem 1.1rem;
  font-size: 0.9375rem;
  text-decoration: none;
}

.skip:focus-visible {
  transform: translateY(0.75rem);
}

/* Available to a screen reader, not to the eye. Used for the two control
   legends and for the breathing announcement. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   4. Type scale
   -------------------------------------------------------------------------- */

.display {
  font-size: clamp(2.375rem, 1.55rem + 3.6vw, 4rem);
  line-height: 1.04;
  letter-spacing: -0.025em;
}

.h2 {
  font-size: clamp(1.75rem, 1.35rem + 1.7vw, 2.5rem);
  line-height: 1.14;
  letter-spacing: -0.02em;
}

.h3 {
  font-size: clamp(1.1875rem, 1.1rem + 0.35vw, 1.375rem);
  line-height: 1.25;
  letter-spacing: -0.012em;
}

.lead {
  font-size: clamp(1.125rem, 1.03rem + 0.42vw, 1.3125rem);
  line-height: 1.55;
  color: var(--text-soft);
}

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

.tiny {
  font-size: 0.9375rem;
  line-height: 1.55;
}

/* `.h2`, `.lead` and `.tiny` are TYPE classes — size, leading and colour, no
   margin — which is right, because the block rhythm belongs to the layout
   layer (`.flow`, `.stack`, `.prose`). Where one of them is a direct child of
   a plain `.wrap` there is no layout layer, so they abut: measured at 0px for
   `.h2` -> `.lead` on evidence and for the small print under the crisis
   directory on safety, twice each. §24's own comment already names the
   failure — "a missing margin looks like a design choice".
   The values are not new: `.stack > .lead` already settled a heading's lead
   at --s-5, so these adjacencies answer the same question the same way
   rather than inventing a second answer. */
.h2 + .lead {
  margin-top: var(--s-5);
}

.dirs + .tiny {
  margin-top: var(--s-6);
}

.tiny + .tiny {
  margin-top: var(--s-4);
}

.section__head {
  max-width: 34ch;
  margin-bottom: clamp(2.25rem, 5vw, 3.5rem);
}

.section__head p {
  margin-top: var(--s-4);
  max-width: var(--measure);
}

/* One head on the page carries a four-line lead rather than a one-line one,
   and 34ch turns that into a column. The measure is the cap that applies to
   every other paragraph on the page. */
.section__head--wide {
  max-width: var(--measure);
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0.6rem 1.5rem;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font: inherit;
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 180ms var(--ease),
              color 180ms var(--ease),
              border-color 180ms var(--ease),
              transform 120ms var(--ease);
}

.btn--primary {
  background: var(--accent);
  color: var(--accent-on);
}

.btn--primary:hover {
  background: var(--accent-quiet);
  color: var(--accent-on);
}

.btn--quiet {
  background: transparent;
  color: var(--text);
  border-color: var(--border);
}

.btn--quiet:hover {
  background: var(--wash);
  color: var(--text);
}

/* Multiplied by the move channel rather than listed in section 33's reduce
   block, which is the idiom `.spec__card:active` already uses: the multiplier
   makes the transform identity AND the duration zero in one place, where the
   enumerated version has to remember both and be kept in step with a rule
   500 lines away. Found by sweeping the CSSOM for interaction rules that set
   a transform — section 33 says the move channel collapses under reduce, and
   five press states did not. */
.btn:active {
  transform: scale(calc(1 - 0.015 * var(--motion-move)));
}

/* 44px, not 40. The app's own floor is `AppTouch.minTarget = 44` and a
   marketing page that ships a smaller control than the product does is the
   wrong way round. */
.btn--small {
  min-height: 44px;
  padding: 0.35rem 1.1rem;
  font-size: 0.9375rem;
}

/* A link that reads as a next step rather than a button. */
.go {
  display: inline-block;
  font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 2px;
  transition: border-color 180ms var(--ease), color 180ms var(--ease);
}

.go:hover {
  color: var(--text);
  border-bottom-color: var(--text);
}

/* --------------------------------------------------------------------------
   6. Masthead
   -------------------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--chrome);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  border-bottom: 1px solid var(--hairline);
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.25rem;
  flex-wrap: wrap;
  min-height: 68px;
  padding-block: var(--s-3);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-head);
  font-size: 1.1875rem;
  letter-spacing: -0.01em;
}

.brand svg {
  display: block;
  color: var(--accent);
}

/* The wordmark's Pebble — 2026-10-10.
   It was a flat outlined circle with two dots and a smile, which read as a
   generic emoji rather than this product's mascot. This is the real thing:
   `web/favicon.png`, rendered from `PebblePainter` by
   `tool/icon/render_pebble_icon.dart`, so the mark on the site and the icon
   on the phone cannot drift. An image rather than inline SVG, so it keeps the
   gradient and the blush that make it a Pebble and not a circle; it carries
   its own colour on both themes, so it needs no `currentColor`. */
.brand__mark {
  display: block;
  width: 22px;
  height: 22px;
  object-fit: contain;
}

.masthead__nav {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.75rem);
}

.masthead__nav a:not(.btn) {
  color: var(--text-soft);
  text-decoration: none;
  font-size: 0.9375rem;
  transition: color 180ms var(--ease);
  /* The target, not the text. This rule sets a size and a colour and no box,
     so what a finger actually gets is the 25px line box — which clears WCAG
     2.2 AA (24x24) by a single pixel and is well under the 44 this stylesheet
     demands of `.btn--small`, in a comment that says why: the app's own floor
     is 44 and a marketing page shipping a smaller control than the product is
     the wrong way round. It applied to buttons and never to the navigation.

     ⚠️ GROWN WITH PADDING, NOT WITH `display`. The first fix here was
     `display: inline-flex; align-items: center; min-height: 44px`, which is
     the standard recipe and was wrong in this bar. `.masthead__nav
     a:not(.btn)` is (0,2,1); the rule hiding the overflow links under 900px
     is `.masthead__nav .is-secondary` at (0,2,0), and it sits earlier in the
     file. So the recipe silently beat `display: none`, unhid the whole
     secondary set on mobile, and wrapped the navigation across three lines
     with "Changelog" clipped at the right edge. Every contrast and target
     measurement still passed — the links were larger, more numerous and
     perfectly legible. Only a screenshot at 390px showed it.

     Vertical padding on an inline box extends the hit area and the background
     without touching the line box, so the target grows and the layout does
     not, and nothing here can ever collide with a `display` rule again. The
     `em` basis keeps it correct if the font size changes. */
  padding-block: calc((44px - 1.65em) / 2);
}

.masthead__nav a:not(.btn):hover {
  color: var(--text);
}

/* The theme control is wide enough that the two quiet links have to go
   earlier than they used to. They are anchors on this same page and both are
   repeated in the footer. */
@media (max-width: 900px) {
  .masthead__nav .is-secondary {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   6a. The phone's route to the rest of the site
   --------------------------------------------------------------------------
   The rule directly above hides five of the six content links below 900px, so
   the only page a phone visitor could reach from the header was `Changelog` —
   the developer changelog. The footer carries all twelve links, so this was
   usability rather than safety, but it is the wrong single link to be left
   with, and "scroll to the bottom" is not a navigation.

   ⚠️ THE OBVIOUS FIX IS THE ONE THAT WAS ALREADY TRIED. Unhiding
   `.is-secondary` on mobile is recorded a few rules above: it wrapped the
   navigation across three lines and clipped "Changelog" at the right edge,
   and every contrast and target measurement still passed — only a screenshot
   at 390px found it. This adds a disclosure instead, so the header does not
   grow at all when it is closed.

   `<details>` rather than a scripted menu. It opens with no JavaScript, which
   matters here because the theme control below is hidden entirely when
   scripting is off: a navigation that needs a script to open is one that can
   fail closed. Keyboard operation and the expanded/collapsed announcement
   come from the element.

   Hidden above 900px, where all six links are already in the bar.
   -------------------------------------------------------------------------- */

.masthead__more {
  display: none;
}

@media (max-width: 900px) {
  /* 🔴 `static`, not `relative`, and that one word is a bug fix.
     
     With `position: relative` here, `.masthead__sheet` anchored to the
     summary. That is correct while the bar is one row and "More" sits at its
     right-hand end. Below about 600px the bar WRAPS, "More" lands on the
     left, and a panel pinned with `inset-inline-end: 0` to a control at
     x = 20 opens from x = 79 leftward: measured at **-113px to 79px** in a
     390px viewport, so roughly 40% of the phone menu was off the left edge
     of the screen and the rest overlapped the logo. Every page, every phone
     width from 320 to 560; correct again by 700 where the bar stops
     wrapping.

     `tool/site_overflow.html` could not have caught it. That harness
     compares `scrollWidth` to `clientWidth`, and content escaping the LEFT
     edge produces no scrollable overflow in any browser — the numbers it
     reads were honest and blind at the same time. It now checks both edges,
     with the disclosure open.

     Made static, the sheet's containing block becomes `.masthead`, which is
     `position: sticky` and spans the viewport at every width. The panel is
     then placed against the bar rather than against a control that moves. */
  .masthead__more {
    display: block;
    position: static;
  }

  /* 44px, the same floor `.btn--small` is held to and for the same reason
     given there: the marketing page must not ship a smaller control than the
     product. Padding rather than `min-height`, so this can never collide with
     a `display` rule the way the navigation links did. */
  .masthead__more > summary {
    list-style: none;
    cursor: pointer;
    color: var(--text-soft);
    font-size: 0.9375rem;
    padding-block: calc((44px - 1.65em) / 2);
    padding-inline: 0.35rem;
    transition: color 180ms var(--ease);
  }

  .masthead__more > summary::-webkit-details-marker {
    display: none;
  }

  .masthead__more > summary::after {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-inline-start: 0.45em;
    vertical-align: 0.15em;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg);
    transition: transform 180ms var(--ease);
  }

  .masthead__more[open] > summary,
  .masthead__more > summary:hover {
    color: var(--text);
  }

  .masthead__more[open] > summary::after {
    transform: rotate(-135deg);
  }

  /* Absolutely positioned so opening it does not move the page underneath a
     finger that is already reaching for something. The sticky masthead keeps
     its height whether this is open or shut. */
  .masthead__sheet {
    position: absolute;
    /* Against the bar's own gutter, not against the summary — see the note
       on `.masthead__more` above. `max-inline-size` is the belt to that
       braces: whatever the gutter resolves to, the panel cannot be wider
       than the screen it opens on. */
    inset-inline-end: var(--gutter);
    max-inline-size: calc(100vw - (var(--gutter) * 2));
    top: calc(100% + 0.5rem);
    z-index: 40;
    min-width: 12rem;
    display: flex;
    flex-direction: column;
    padding: 0.4rem;
    /* ⚠️ OPAQUE, and that is the point rather than a style choice. The
       masthead itself is glass, and the comment on `.masthead` records what
       that cost: at 0.72 alpha the bar's composited ground moved with
       whatever scrolled beneath it and "Changelog" measured 2.65:1 on one
       page. A panel that opens over arbitrary page content has that problem
       in its worst form, so it does not borrow the bar's glass — it paints
       `--surface` and every link in it has a fixed ratio. */
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-lift, 0 12px 32px rgb(0 0 0 / 0.18));
  }

  .masthead__sheet a {
    /* `--text`, not the bar's `--text-soft`. The demoted ink in the masthead
       is a hierarchy against the links beside it; inside this panel there is
       nothing to be demoted relative to, and these are the only links a phone
       visitor gets. */
    color: var(--text);
    text-decoration: none;
    font-size: 0.9375rem;
    padding-block: calc((44px - 1.65em) / 2);
    padding-inline: 0.6rem;
    border-radius: var(--r-card);
  }

  .masthead__sheet a:hover,
  .masthead__sheet a:focus-visible {
    color: var(--text);
    background: var(--fill);
  }
}

/* --------------------------------------------------------------------------
   6b. The theme control
   --------------------------------------------------------------------------
   A radio group, not a two-state button. Three states have to be reachable -
   System is the page's original behaviour and removing it would be a
   regression - and `aria-pressed` is a boolean. Radios also give arrow-key
   traversal and a "selected" announcement for free, and the checked state is
   expressible in CSS without asking JavaScript for it.

   Hidden when JavaScript is off. A visible control that cannot do anything is
   a worse failure than no control: with no script the page follows
   prefers-color-scheme, exactly as it did before.
   -------------------------------------------------------------------------- */

.theme {
  display: none;
  margin: 0;
  padding: 0;
  border: 0;
  min-inline-size: 0;
}

:root[data-js] .theme {
  display: block;
  margin-inline-start: auto;
}

.theme__set {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
}

/* Off-screen rather than `display: none`, which would take the radio out of
   the tab order and out of the accessibility tree. */
.theme__set input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.theme__opt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding-inline: 0.85rem;
  font-size: 0.875rem;
  line-height: 1;
  color: var(--text-soft);
  cursor: pointer;
  user-select: none;
  border-radius: var(--r-pill);
  transition: background-color 200ms var(--ease), color 200ms var(--ease);
}

.theme__opt:hover {
  color: var(--text);
}

.theme__set input:checked + .theme__opt {
  background: rgba(var(--rgb-sage), var(--fill-hi));
  color: var(--ink-sage);
  font-weight: 500;
}

.theme__set input:focus-visible + .theme__opt {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}

@media (max-width: 560px) {
  .masthead__inner {
    justify-content: space-between;
  }

  :root[data-js] .theme {
    margin-inline-start: 0;
  }

  /* Its own row, with the two remaining items pushed to the edges. Three
     wrapped rows in a sticky header on a 320px screen is too much header. */
  .masthead__nav {
    flex: 1 0 100%;
    justify-content: space-between;
  }



  .theme__opt {
    padding-inline: 0.6rem;
    font-size: 0.8125rem;
  }
}

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: clamp(3.5rem, 9vw, 6.5rem) clamp(3.5rem, 8vw, 6rem);
  position: relative;
  overflow: hidden;
}

/* The one place on the page where the whole accent family is visible at
   once: three of the app's muted hues, overlapping at low alpha, drifting.
   It is a colour field and not a mesh-gradient blob - no glow ring, no
   saturated bloom, nothing sharp enough to read as a shape. The motion is a
   40s translate at 8% of the viewport, which is slow enough that you notice
   it only if you stop and look, and it stops dead under reduced motion. */
.hero::before {
  content: "";
  position: absolute;
  inset: -45% -25% auto -30%;
  height: 130%;
  background:
    radial-gradient(38% 44% at 18% 34%, rgba(var(--rgb-sage), 0.52), transparent 66%),
    radial-gradient(34% 42% at 50% 16%, rgba(var(--rgb-dusk), 0.42), transparent 64%),
    radial-gradient(34% 40% at 78% 46%, rgba(var(--rgb-clay), 0.38), transparent 62%),
    radial-gradient(30% 36% at 40% 76%, rgba(var(--rgb-heather), 0.34), transparent 62%),
    radial-gradient(26% 30% at 88% 82%, rgba(var(--rgb-sand), 0.30), transparent 62%);
  filter: blur(28px);
  pointer-events: none;
}

/* Breathing, not sliding. The first version travelled 7% of its own width
   and read as a slow pan; the swell is now carried by the scale and the
   translate is small enough to be a wobble rather than a direction.
   `will-change` is claimed inside this block, so a reader who has asked for
   less motion is not paying for a compositor layer that never moves. */
@media (prefers-reduced-motion: no-preference) {
  .hero::before {
    will-change: transform;
    animation: drift 44s ease-in-out infinite alternate;
  }
}

@keyframes drift {
  from { transform: translate3d(-1.2%, -0.8%, 0) scale(1.03); }
  to   { transform: translate3d(1.4%, 1.2%, 0) scale(1.09); }
}

/* A hairline of the accent family under the hero, so the colour reads as a
   system rather than as one washy background. */
.hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: linear-gradient(
    90deg,
    rgb(var(--rgb-sage)),
    rgb(var(--rgb-dusk)) 28%,
    rgb(var(--rgb-heather)) 52%,
    rgb(var(--rgb-clay)) 76%,
    rgb(var(--rgb-sand))
  );
  opacity: 0.62;
}

/* One word of the headline carries the accent. Solid colour, not a gradient:
   emphasis comes from hue and weight, and gradient text is a costume. */
.display em {
  font-style: normal;
  color: var(--ink-sage);
}

/* The hero shares the page's left edge. Constraining the measure on the
   container instead centres the whole block and leaves the headline out of
   line with every heading below it. */
.hero__inner {
  position: relative;
}

.hero h1 {
  max-width: 15ch;
}

.hero p.lead {
  margin-top: var(--s-6);
  max-width: 34ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: 2.25rem;
}

/* --------------------------------------------------------------------------
   8. Statement band
   -------------------------------------------------------------------------- */

.statement {
  padding-block: clamp(3rem, 7vw, 5rem);
  border-block: 1px solid var(--hairline);
  background: var(--surface);
}

.statement__lede {
  font-family: var(--font-head);
  font-size: clamp(1.375rem, 1.1rem + 1.15vw, 2rem);
  line-height: 1.32;
  letter-spacing: -0.015em;
  max-width: 24ch;
}

.statement__grid {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: 1fr;
}

@media (min-width: 860px) {
  .statement__grid {
    grid-template-columns: 1.1fr 1fr;
    align-items: start;
  }
}

.statement__body p + p {
  margin-top: var(--s-4);
}

/* --------------------------------------------------------------------------
   9. Moments (what happens when you open it)
   -------------------------------------------------------------------------- */

.moment {
  position: relative;
  display: grid;
  gap: 0.5rem 3rem;
  padding-block: clamp(1.75rem, 4vw, 2.5rem);
  border-top: 1px solid var(--hairline);
}

/* The rule above each moment is drawn in that moment's hue, over the
   hairline, and it draws itself in as the moment arrives. Three steps, three
   colours, one gesture - not the same fade on every block. */
.moment::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: min(15rem, 42%);
  height: 2px;
  background: rgb(var(--hue, var(--rgb-sage)));
  transform-origin: left center;
}

.moment:last-child {
  border-bottom: 1px solid var(--hairline);
}

.moment h3 {
  color: var(--hue-ink, var(--ink-sage));
}

.moment p {
  max-width: 52ch;
  color: var(--text-soft);
}

@media (min-width: 780px) {
  .moment {
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    align-items: baseline;
  }
}

/* --------------------------------------------------------------------------
   10. Bento (what is in it)
   -------------------------------------------------------------------------- */

.bento {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: 1fr;
}

@media (min-width: 720px) {
  .bento {
    grid-template-columns: repeat(3, 1fr);
  }

  .bento__cell--wide {
    grid-column: span 2;
  }
}

/* Each cell wears one of the app's five accents. Six tiles, five hues, and
   the sixth returns to sage - so the grid reads as a family with a home
   colour rather than as a rainbow. The tint is decorative and unbound; the
   heading uses the AA-solved ink variant of the same hue. */
.bento__cell {
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(152deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), calc(var(--fill) * 0.36))),
    var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: clamp(1.375rem, 3vw, 1.875rem);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  min-height: 11rem;
  transition: transform 320ms var(--ease), border-color 320ms var(--ease);
}

/* The edge is the hue too, at the strength a boundary needs and no more. */
.bento__cell::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.34);
  pointer-events: none;
  transition: border-color 320ms var(--ease);
}

/* Gated on a real pointer. On a touch screen :hover sticks after a tap, so
   an ungated lift leaves one tile raised until the next tap lands. */
@media (hover: hover) and (pointer: fine) {
  /* The lift for these lives with the rest of the family in section 33. This
     said `translateY(-2px)` with no `--motion-move` factor: a second answer to
     the same question, and the only one of the set that would not scale down
     under reduced motion. Superseded rather than deleted silently. */

  .bento__cell:hover::after {
    border-color: rgba(var(--hue, var(--rgb-sage)), 0.62);
  }
}

.bento__cell h3 {
  color: var(--hue-ink, var(--ink-sage));
}

.bento__cell--washed {
  background:
    linear-gradient(152deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill-hi)),
      rgba(var(--hue, var(--rgb-sage)), calc(var(--fill-hi) * 0.3))),
    var(--surface);
}

.bento__cell p {
  color: var(--text-soft);
  max-width: 42ch;
}

/* Concentric geometry, not an illustration: the shape the app's breathing
   exercise draws. Decorative, so it is hidden from assistive technology. */
.bento__rings {
  position: absolute;
  right: -3.5rem;
  bottom: -4.5rem;
  width: 15rem;
  height: 15rem;
  color: var(--hue-ink, var(--ink-sage));
  opacity: 0.2;
  pointer-events: none;
}

/* The rings are the shape the breathing exercise draws, so they breathe.
   One authored moment, at the pace of the exercise itself: 10 seconds in and
   out, which is the "Longer out than in" cycle the demo runs. */
@media (prefers-reduced-motion: no-preference) {
  .bento__rings {
    transform-origin: 50% 50%;
    animation: rings 10s ease-in-out infinite;
  }
}

@keyframes rings {
  0%   { transform: scale(0.94); opacity: 0.14; }
  40%  { transform: scale(1.05); opacity: 0.24; }
  100% { transform: scale(0.94); opacity: 0.14; }
}

/* --------------------------------------------------------------------------
   10b. The demo
   --------------------------------------------------------------------------
   Two panes: a check-in the visitor can actually answer, and a breathing
   exercise that actually runs. Every line of copy inside it is lifted from
   assets/content/*.json - the same files the app reads - so the demo cannot
   drift into saying something the product would not say.

   Hidden with JavaScript off, for the same reason the theme control is: a
   demo that does not do anything is a picture of software, which is the thing
   the hero comment already refuses.
   -------------------------------------------------------------------------- */

/* THE SECTION IS NO LONGER HIDDEN WITHOUT JAVASCRIPT, and that changed on
   2026-08-16 when the check-in became a real `input[type=range]` with a
   static ladder behind it. The old rule was right for what stood here: two
   panes that both did nothing without a script, and "a demo that does not do
   anything is a picture of software", which is the thing the hero comment
   refuses.

   The check-in pane now degrades instead of disappearing — the reader gets
   every band's copy at once (section 10a, `.ladder`) — so hiding it would be
   throwing away content that works. The BREATHING pane is unchanged in that
   respect: a paced-breathing circle with no timer really is a picture of
   software, so it keeps the old rule and takes it alone.

   The grid collapses with it. `display: none` takes a child out of the grid
   rather than leaving a hole, but the TRACKS are declared on the container,
   so the surviving pane would sit in a 1.08fr column with an empty 1fr
   beside it — a card two-thirds of the way across a section, which reads as
   a layout that broke rather than as one that adapted. */
:root:not([data-js]) .demo__pane--breath {
  display: none;
}

@media (min-width: 880px) {
  :root:not([data-js]) .demo {
    grid-template-columns: 1fr;
  }
}

.demo {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: 1fr;
  align-items: stretch;
}

@media (min-width: 880px) {
  .demo {
    grid-template-columns: 1.08fr 1fr;
  }
}

.demo__pane {
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(158deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), calc(var(--fill) * 0.34))),
    var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: clamp(1.375rem, 3.2vw, 2rem);
  display: flex;
  flex-direction: column;
}

.demo__pane::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.32);
  pointer-events: none;
}

.demo__h {
  font-family: var(--font-head);
  font-size: 1.25rem;
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.012em;
  margin: 0;
}

.demo__sub {
  margin-top: 0.35rem;
  color: var(--text-soft);
  font-size: 0.9375rem;
}

/* The mood scale. Five options, and colour is never the only channel: the
   word is there, and so is the position. AppColors.moodScale ramps in
   luminance as well as hue, so the row reads in greyscale too. */
/* --- The check-in control ------------------------------------------------
   A REAL `input[type=range]`, from 2026-08-16, and the reason is fidelity
   rather than taste: `lib/screens/checkin/check_in_screen.dart` builds a
   Slider with `min: 1, max: 5, divisions: 4`. The five radio buttons that
   stood here were the reinterpretation; this is the reproduction.

   What the native control buys, none of which had to be written:
   arrow keys, Home and End, page keys, drag, touch, the correct pointer
   capture, and a role a screen reader already knows. The one thing it does
   not carry for free is the VALUE — "3" is not what the app says — so the
   script sets `aria-valuetext` to the mood word on every input event.

   ## The one inline style on this site, and this is it

   Section 10's note that the site "carries exactly one inline style
   declaration (the mood scale's --c, which is data)" still holds. `--c` is
   the ladder colour for the current step and `--pct` is where the thumb is;
   both are data, both are set from the same five tokens the contrast test
   holds to a monotonic luminance ramp, and neither is a style decision made
   at the call site.

   ## Cross-engine, because a range is the least uniform control there is

   WebKit and Blink take `::-webkit-slider-runnable-track` and
   `::-webkit-slider-thumb`; Gecko takes `::-moz-range-track` and
   `::-moz-range-thumb`. They cannot be combined into one selector list —
   an unrecognised pseudo-element invalidates the whole rule — so each is
   written out. `appearance: none` on the input and on the WebKit thumb, and
   `border: 0` on the Gecko thumb, which otherwise inherits a system bevel.

   The thumb is 22px and the track is 6px, so the hit area is the 44px the
   rest of the site holds itself to: the input carries the height, not the
   track. */

.mood {
  --c: var(--mood-3);
  --pct: 50%;
  margin: 1.5rem 0 0;
  display: none;
}

:root[data-js] .mood {
  display: block;
}

/* The word, above the control, which is where the app puts it. `<output>`
   rather than a span: it is a live region by default, so a screen reader
   hears the word change without a second announcement being wired up, and
   `for` ties it to the control that produced it.

   ⚠️ IT IS `--text` AND NOT `--c`, AND THAT IS A MEASUREMENT RATHER THAN A
   PREFERENCE. The app colours this word from the mood ladder, and copying
   that here would ship a contrast failure at two of the five positions.
   Measured against the pane's own gradient — sage at `--fill` over
   `--surface` at the top, at `--fill * 0.34` at the bottom, worse end taken:

     step        1      2      3      4      5
     light     6.10   4.61   3.65   2.91   2.62
     dark      3.16   3.95   4.46   5.17   6.14

   The word is large text, so its floor is 3:1 — and light steps 4 and 5 are
   under it. The reason is not a defect in the ladder: `lib/data/style.dart`
   does not use these five values as text either. It re-hues them at run time
   onto an arc ending on the active accent and binary-searches each step back
   to a pinned relative luminance, which is a mechanism this stylesheet has
   no way to reproduce and no business approximating. A colour measured for
   one role is not valid in another, which is §18's rule arriving somewhere
   §18 did not look.

   So the ladder keeps the roles it was measured for — the filled track and
   the thumb, both decoration — and the word takes the body ink. Colour is
   never the only signal: the word prints itself, which is the same rule the
   register's verdict chips and the privacy callouts follow. */
.mood__word {
  display: block;
  font-family: var(--font-head);
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2rem);
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--text);
  margin-bottom: var(--s-3);
}

/* THE SLIDER, TAKEN FROM THE APPROVED DESIGN'S `.zr` — 2026-09-03.
   --------------------------------------------------------------------------
   What was here: a 6px track split at `--pct` into a filled half in the
   current mood's colour and an unfilled half in `rgba(var(--hue), .22)`,
   with a 22px thumb in the same mood colour.

   Two things were wrong with it. The unfilled half fell back to `--rgb-sage`
   — the 1.0 green — so at "Really low" the control was a dark mauve dot on a
   pale green rail, which is the *opposite* of what the ladder means. And a
   fill that grows tells the reader they are making progress along a scale.
   There is no progress here. Mood 5 is not more finished than mood 1.

   The design paints the WHOLE ladder into the track and slides a plain white
   thumb along it. Every stop is visible before it is chosen, the colour under
   the thumb is the colour of the mood it is on, and nothing about it reads as
   completion. Values are the design's own, byte for byte — the five stops are
   Pebble's five `fillFrom` colours, so the rail and the face are one ladder.

   The thumb is white rather than the mood colour on purpose: it is the thing
   the hand holds, and it has to stay findable on all five stops. */
.mood__range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 34px;
  background: transparent;
  cursor: pointer;
  display: block;
  margin: 0;
}

.mood__range::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: 999px;
  background: linear-gradient(90deg,
    #cfc7e6, #cbd6ea 27%, #d4ccec 50%, #e7dfb6 76%, #f0d8c0);
}

.mood__range::-moz-range-track {
  height: 10px;
  border-radius: 999px;
  background: linear-gradient(90deg,
    #cfc7e6, #cbd6ea 27%, #d4ccec 50%, #e7dfb6 76%, #f0d8c0);
}

/* `margin-top` centres the thumb on a 10px track by hand, because a WebKit
   thumb is laid out from the top of the track rather than from its middle. */
.mood__range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 30px;
  height: 30px;
  margin-top: -10px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid rgba(21, 19, 15, 0.16);
  box-shadow: 0 6px 16px -8px rgba(58, 48, 78, 0.55);
}

.mood__range::-moz-range-thumb {
  width: 30px;
  height: 30px;
  border: 1px solid rgba(21, 19, 15, 0.16);
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 6px 16px -8px rgba(58, 48, 78, 0.55);
}

.mood__range:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 999px;
}

/* Only the two ends are labelled. The app shows one word at a time and so
   does this; printing all five under the track would be the radio row again,
   in smaller type. */
.mood__ends {
  display: flex;
  justify-content: space-between;
  margin-top: 0.15rem;
  font-size: 0.8125rem;
  color: var(--text-soft);
}

/* --- The ladder, for a reader with no script ------------------------------
   `:root:not([data-js])` and NOT `@media (scripting: none)`. The media query
   is Baseline and is the obvious answer, and it answers a different question:
   it reports that scripting is ENABLED, which is not the same fact as the
   script having RUN. An extension that blocks the file, a connection that
   died mid-page and a syntax error all leave scripting "enabled" and the
   page inert, and a fallback conditioned on the media query is invisible in
   exactly those cases. The attribute is set on the first line of the head
   script, so it is the honest signal, and it is the one the theme control
   and the register's filters already use. Recorded rather than done quietly,
   because the design research asked for the media query by name.

   What the reader gets: every band's copy at once. They lose the control and
   keep all five sentences, which is the right direction for a page whose
   argument is that the content is finite and readable. */
.ladder {
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0;
}

:root[data-js] .ladder {
  display: none;
}

.ladder__row {
  display: grid;
  gap: 0.2rem;
  padding-block: var(--s-3);
  border-top: 1px solid var(--hairline);
  font-size: 0.9375rem;
  color: var(--text-soft);
}

.ladder__row:last-child {
  border-bottom: 1px solid var(--hairline);
}

.ladder__word {
  font-weight: 600;
  color: var(--ink-sage);
}

@media (min-width: 560px) {
  .ladder__row {
    grid-template-columns: 6.5rem minmax(0, 1fr);
    column-gap: var(--s-4);
    align-items: baseline;
  }
}

.demo__reply {
  margin-top: var(--s-6);
  border-top: 1px solid var(--hairline);
  padding-top: 1.25rem;
  /* ⚠️ THE CARD MUST NOT RESIZE WHEN THE SLIDER MOVES, reported by the owner
     as *"when we change the slider of the home like okay to others the size
     of that card shouldnt change"*.

     Five moods swap in five different replies of different lengths, so the
     panel grew and shrank under the reader's own thumb — and on this
     component that is worse than untidy: everything below the card jumps
     while a person is dragging, which is the thing a check-in screen must
     never do to somebody who is already unsettled.

     Reserved from the tallest reply rather than from an average, so the
     floor is set by the content that actually exists. It is a MIN, not a
     fixed height: a longer line at a larger text scale still grows the
     panel rather than being clipped, which is the only version of this that
     survives someone reading at 200%. */
  min-height: 9.5rem;
}

@media (min-width: 900px) {
  .demo__reply {
    min-height: 8.25rem;
  }
}

/* NO ARRIVAL ANIMATION ON THE REPLY, and its removal is the point rather
   than an omission. The reply used to fade and rise 8px when a radio was
   picked, which was right for a control you press once. A slider fires on
   every step of a drag, so the same keyframe would restart four times
   between "Really low" and "Good" — and a keyframe restarts from zero where
   a transition retargets, so the pane would flicker under the reader's own
   thumb. The text simply changes, which is what the register's filters do
   for the same reason.

   The block is present from the first paint, holding band 2's copy, so the
   pane never jumps height and a reader who never touches the control still
   sees a real answer rather than a placeholder asking them to act. */
.demo__line {
  font-family: var(--font-head);
  font-size: 1.1875rem;
  line-height: 1.4;
}

/* With no script the ladder below prints all five answers, so the single
   answer in this block would be one of them printed twice — and the reader
   would have no way to tell why that one. It goes, and the ladder is the
   whole of what the pane says. */
:root:not([data-js]) .demo__reply {
  display: none;
}

.demo__offers {
  margin: 1.25rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--s-4);
}

.demo__offer[hidden] {
  display: none;
}

/* The kind label is a block so the line under it starts at the left margin
   at every width, which is what makes the two offers at the bottom of the
   scale read as two things rather than as one paragraph with a bold run in
   it. That distinction is the whole exhibit at that band. */
.demo__kind {
  display: block;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-sage);
}

.demo__thing {
  display: block;
  margin-top: 0.35rem;
  color: var(--text-soft);
}

.demo__note {
  margin-top: auto;
  padding-top: 1.5rem;
  font-size: 0.875rem;
  color: var(--text-soft);
}

/* --- The breathing pane -------------------------------------------------- */

.breath__stage {
  position: relative;
  display: grid;
  place-items: center;
  height: 12.5rem;
  margin-top: var(--s-4);
}

.breath__circle {
  position: absolute;
  width: 12rem;
  height: 12rem;
  border-radius: 50%;
  background: rgba(var(--rgb-dusk), 0.2);
  border: 1.5px solid rgba(var(--rgb-dusk), 0.55);
  transform: scale(0.55);
  transition: transform var(--breath-dur, 4s) cubic-bezier(0.42, 0, 0.4, 1);
}

.breath__circle[data-full] {
  transform: scale(1);
}

/* The second ring trails the first by a beat, so the circle has a little
   depth to it rather than reading as a flat disc. */
.breath__circle::before {
  content: "";
  position: absolute;
  inset: -10px;
  border-radius: 50%;
  border: 1px solid rgba(var(--rgb-dusk), 0.28);
}

.breath__read {
  display: grid;
  justify-items: center;
  gap: 0.15rem;
  margin-top: 0.25rem;
  min-height: 3.5rem;
  text-align: center;
}

.breath__phase {
  font-family: var(--font-head);
  font-size: 1.375rem;
  line-height: 1.2;
}

/* Counting up, not down. A countdown is a deadline, and this is a pace. */
.breath__count {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}

.breath__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn[disabled]:active {
  transform: none;
}

.breath__help {
  margin-top: auto;
  padding-top: 1.25rem;
  font-size: 0.875rem;
  color: var(--text-soft);
}

/* Reduced motion: the circle stops moving and the counts carry the pace on
   their own. The exercise still works; it just does not animate. */
@media (prefers-reduced-motion: reduce) {
  .breath__circle {
    transition: none;
    transform: scale(0.82);
  }

  .breath__circle[data-full] {
    transform: scale(0.82);
  }
}

/* --------------------------------------------------------------------------
   10c. The science section
   --------------------------------------------------------------------------
   The longest section on the page, so it is built out of five different
   shapes rather than one shape repeated: a drawn loop, a three-column
   mechanism table, a prose column, one tinted panel of refusals, and a
   compact safety strip. The reference list that used to close it is now
   section 10d, its own band.

   Colour carries the argument and is not decoration. Sage is what the app
   does, and that now includes the refusals, because a refusal IS something
   the app does. Clay is no longer spent on them: a warm tint over a list of
   deliberate decisions reads as a warning about the product. Dusk is the
   worst moment. Sand is the paragraph where it states its own ceiling.
   Nothing else in this section is hued.

   Contrast, measured against the surface each pair actually sits on, and
   re-measured under prefers-contrast: more, where --fill drops and a tint
   gets lighter rather than stronger:
                                          normal   contrast:more
     light  mechanism name on page          6.37       6.37
            technique name on page          6.37       6.37
            loop node text on page         12.81      12.81
            "No X" term on sage panel       5.82       6.28
            reason text on sage panel       4.81       5.19
            bold lead on sage panel        11.71      12.62
            heading on dusk strip           5.67       6.11
            sentence on dusk strip          4.83       5.20
            bold lead on dusk strip        11.75      12.66
            "The rest of it" on dusk strip  5.67       6.11
            signal sentence on clay wash    4.57       4.90
            signal sentence on sage wash    4.53       4.86
            body text on sand panel        11.18      11.93
            ring arc on page                3.62       3.62
            ring arrowhead on page          5.04       5.04
            cut tick on page                6.14       6.14
            segmented control border        3.33       3.33
     dark   mechanism name on page          7.62       7.62
            technique name on page          7.62       7.62
            loop node text on page         13.94      13.94
            "No X" term on sage panel       5.14       5.44
            reason text on sage panel       5.13       5.43
            bold lead on sage panel         9.41       9.96
            heading on dusk strip           5.52       5.84
            sentence on dusk strip          5.11       5.40
            bold lead on dusk strip         9.37       9.89
            "The rest of it" on dusk strip  5.52       5.84
            signal sentence on clay wash    5.90       6.44
            signal sentence on sage wash    5.98       6.52
            body text on sand panel        10.74      12.14
            ring arc on page                3.63       3.63
            ring arrowhead on page          4.63       4.63
            cut tick on page                8.26       8.26
            segmented control border        3.81       3.81
   Two thresholds are in play and they are different jobs. Text is held to
   4.5:1. The ring, its arrowheads and the cut tick are a graphical object a
   reader needs in order to follow the argument, so WCAG 1.4.11 applies and
   they are held to 3:1 - the same bar as the segmented control's border,
   and NOT the decorative-hairline exemption. The one line under that bar is
   the cut arc itself, at 0.28 opacity, because its meaning is that it is
   gone. The divider between two refusals is decorative and unbound, the
   same as --hairline.
   -------------------------------------------------------------------------- */

.sci + .sci {
  margin-top: clamp(2.75rem, 6vw, 4.5rem);
}

.sci__h {
  max-width: 30ch;
  margin-bottom: var(--s-5);
}

.sci__lede {
  max-width: var(--measure);
  margin-bottom: 1.75rem;
  color: var(--text-soft);
}

.sci__lede b {
  color: var(--text);
  font-weight: 600;
}

/* --- The drawn loop ------------------------------------------------------ */

/* The one diagram on the page, and the only thing in this section that moves
   for a reason other than arrival. It teaches the section's own idea: a
   maintaining loop is a ring in which every step makes the next one more
   likely, and an intervention is a cut in the ring.

   State is two radios and a sibling combinator, so there is no JavaScript in
   it and it works with scripting off. That also decides the reduced-motion
   story: both states are already complete, static pictures. The ring is
   whole in one and cut in the other, the arrowheads carry the direction
   without moving, and the captions explain either way. Under `reduce` the
   travelling dot simply never exists, and nothing it was saying is lost.

   The four steps sit INSIDE the ring rather than on it. A chip wide enough
   to hold "Fewer good things" cannot mask a circle cleanly - the arc leaves
   the bottom of the chip before it leaves the side of it, and a stub of
   stroke pokes out. Inside, nothing has to be masked at all. */

.loop {
  display: grid;
  gap: clamp(1.5rem, 4vw, 2.25rem);
  justify-items: start;
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

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

/* The same segmented control as the masthead's theme switch, so the page has
   one way of saying "pick one of two views" rather than two. */
.loop__set {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface);
}

.loop__opt {
  padding: 0.42rem 1rem;
  border-radius: var(--r-pill);
  font-size: 0.875rem;
  line-height: 1.2;
  color: var(--text-soft);
  cursor: pointer;
  transition: background-color 140ms var(--ease), color 140ms var(--ease);
}

#loop-run:checked ~ .loop__set [for="loop-run"],
#loop-cut:checked ~ .loop__set [for="loop-cut"] {
  background: var(--accent);
  color: var(--accent-on);
}

#loop-run:focus-visible ~ .loop__set [for="loop-run"],
#loop-cut:focus-visible ~ .loop__set [for="loop-cut"] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.loop__body {
  display: grid;
  gap: clamp(1.25rem, 4vw, 2.5rem);
  align-items: center;
  width: 100%;
}

@media (min-width: 860px) {
  .loop__body {
    grid-template-columns: minmax(0, 25rem) minmax(0, 1fr);
    gap: clamp(2rem, 5vw, 4rem);
  }
}

.loop__stage {
  position: relative;
  width: 100%;
  max-width: 25rem;
  aspect-ratio: 1;
}

.loop__ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The ring and its arrowheads are a graphical object a reader needs in
   order to follow the argument, so WCAG 1.4.11 applies and they are held to
   3:1 like a control boundary, not left at decorative weight. The first
   version drew them as the raw sage at 0.6 alpha and measured 1.77:1 in
   light: a line you could see and not a line the standard would accept.
   `--ink-*` is the AA-safe variant of each hue, and 0.75 of it keeps the
   diagram soft while clearing the bar in both themes. */
.loop__arc {
  fill: none;
  stroke: var(--ink-sage);
  stroke-opacity: 0.75;
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke-opacity 220ms var(--ease);
}

.loop__head path {
  fill: none;
  stroke: var(--ink-sage);
  stroke-opacity: 0.9;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.loop__head {
  transition: opacity 200ms var(--ease);
}

/* The cut mark: a radial tick across the ring where the last step used to
   be. Present in the markup always so the transition has something to act
   on; visible only once the ring is cut. Clay, because clay is this page's
   colour for a refusal. */
.loop__cut {
  stroke: var(--ink-clay);
  stroke-width: 3;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity 220ms var(--ease);
}

.loop__runner circle {
  fill: var(--accent);
}

/* Widths are a percentage of the stage rather than a rem value, because the
   stage is fluid: a fixed chip fits at 1440 and hangs off a 320 viewport.
   East and west get less than north and south because horizontally they have
   the stage edge and the arc to respect, and vertically they do not. */
.loop__node {
  position: absolute;
  margin: 0;
  padding: 0.3rem 0.5rem;
  border-radius: 10px;
  background: var(--page);
  color: var(--text);
  font-size: clamp(0.75rem, 0.69rem + 0.3vw, 0.875rem);
  line-height: 1.3;
  text-align: center;
  transform: translate(-50%, -50%);
}

.loop__node--n,
.loop__node--s { width: 42%; }

.loop__node--e,
.loop__node--w { width: 26%; }

.loop__node--n { left: 50%; top: 26%; }
.loop__node--e { left: 74%; top: 50%; }
.loop__node--s { left: 50%; top: 74%; }
.loop__node--w { left: 26%; top: 50%; }

.loop__read {
  max-width: 46ch;
}

.loop__say {
  margin: 0;
}

.loop__say b {
  color: var(--text);
  font-weight: 600;
}

.loop__aside {
  margin-top: var(--s-4);
  color: var(--text-soft);
  font-size: 0.9375rem;
}

/* --- Loop state ---------------------------------------------------------- */

/* The cut arc is the one line here allowed under 3:1, and deliberately: it
   is the path that is NO LONGER there. Its meaning is its absence, the cut
   tick and the missing arrowhead say the same thing at full strength, and
   the caption says it in words. */
#loop-cut:checked ~ .loop__body .loop__arc--last {
  stroke-opacity: 0.28;
  stroke-dasharray: 4 9;
}

#loop-cut:checked ~ .loop__body .loop__head--last {
  opacity: 0;
}

#loop-cut:checked ~ .loop__body .loop__cut {
  opacity: 1;
}

/* The two captions are display-swapped rather than faded, so a screen reader
   is never offered the caption for the state that is not showing. */
.loop__say--cut,
#loop-cut:checked ~ .loop__body .loop__say--run {
  display: none;
}

#loop-cut:checked ~ .loop__body .loop__say--cut {
  display: block;
}

/* The travelling dot. Twelve seconds is three seconds a step, which is about
   as fast as a reader can follow a four-step argument and slow enough that
   it never competes with the sentence beside it.

   Cut, it becomes a one-shot: three quarters of a turn, then it stops dead
   at the step the technique acts on, which is the point being made. 900ms
   with a hard deceleration, so the arrival reads as a stop. */
.loop__runner {
  opacity: 0;
  transform-box: view-box;
  transform-origin: 50% 50%;
}

@media (prefers-reduced-motion: no-preference) {
  .loop__runner {
    opacity: 1;
    animation: loop-turn 12s linear infinite;
  }

  #loop-cut:checked ~ .loop__body .loop__runner {
    animation: loop-halt 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}

@keyframes loop-turn {
  to { transform: rotate(360deg); }
}

@keyframes loop-halt {
  from { transform: rotate(0deg); }
  to   { transform: rotate(270deg); }
}

/* --- The mechanism table ------------------------------------------------- */

/* Three columns and not two, because the three are doing different jobs for
   different readers: the middle column is the loop described in words
   anybody recognises, and the right column names the technique that
   interrupts it AND says how that technique works. Naming without
   explaining was the defect in the version this replaces. A reader who
   only wants one of the three can read down a single column. */
.mech {
  margin: 0;
}

.mech__row {
  display: grid;
  gap: var(--s-2);
  padding-block: clamp(1.25rem, 2.8vw, 1.75rem);
  border-top: 1px solid var(--hairline);
}

.mech__row:last-child {
  border-bottom: 1px solid var(--hairline);
}

.mech__name {
  font-family: var(--font-head);
  font-size: 1.0625rem;
  line-height: 1.3;
  color: var(--ink-sage);
}

.mech__what {
  color: var(--text-soft);
  max-width: 54ch;
}

.mech__how {
  max-width: 54ch;
}

/* The technique's name is the thing a technical reader came for, so it is
   set in the accent and carries the weight. Text, not a control: held to
   4.5:1, not 3:1. */
.mech__how b {
  color: var(--ink-sage);
  font-weight: 600;
}

/* 960 and not 900: three columns arrive only once the two text tracks can
   each hold a readable line. At 900 the outer track left the description
   about 316px, which is four lines of a two-line sentence. */
@media (min-width: 960px) {
  .mech__row {
    grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) minmax(0, 1.15fr);
    column-gap: 2.25rem;
    align-items: start;
  }
}

.sci__note {
  margin-top: 1.75rem;
  max-width: var(--measure);
  color: var(--text-soft);
}

.sci__note b {
  color: var(--ink-sage);
  font-weight: 500;
}

/* --- The same signal, two labels ----------------------------------------- */

/* The second figure in this section, and the quieter of the two. It exists
   to make one row of the table above checkable rather than asserted.

   The whole argument is that NOTHING moves except the label. There is one
   <path> in the markup and it is never redrawn, restyled or recoloured; the
   wash behind it changes hue and the sentence under it changes, and that is
   all. Anything that animated the trace itself would be arguing the
   opposite of the copy.

   Reduced motion loses the two crossfades and keeps both complete states,
   the same as the ring above. */

.signal {
  margin: clamp(1.75rem, 4vw, 2.5rem) 0 0;
  max-width: 46rem;
}

.signal__pick {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.signal__legend {
  padding: 0;
  margin-bottom: var(--s-4);
  font-family: var(--font-head);
  font-size: 1.0625rem;
  color: var(--text);
}

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

.signal__opts {
  display: inline-flex;
  flex-wrap: wrap;
  padding: 3px;
  gap: 2px;
  margin-bottom: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface);
}

.signal__opt {
  padding: 0.42rem 1rem;
  border-radius: var(--r-pill);
  font-size: 0.875rem;
  line-height: 1.2;
  color: var(--text-soft);
  cursor: pointer;
  transition: background-color 140ms var(--ease), color 140ms var(--ease);
}

#sig-threat:checked ~ .signal__opts [for="sig-threat"],
#sig-ready:checked ~ .signal__opts [for="sig-ready"] {
  background: var(--accent);
  color: var(--accent-on);
}

#sig-threat:focus-visible ~ .signal__opts [for="sig-threat"],
#sig-ready:focus-visible ~ .signal__opts [for="sig-ready"] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.signal__panel {
  padding: clamp(1.125rem, 3vw, 1.5rem);
  border-radius: var(--r-card);
  background: rgba(var(--rgb-clay), var(--fill));
  transition: background-color 260ms var(--ease);
}

#sig-ready:checked ~ .signal__panel {
  background: rgba(var(--rgb-sage), var(--fill));
}

/* One path, one stroke, one colour, in both states. */
.signal__trace {
  display: block;
  width: 100%;
  height: clamp(48px, 9vw, 68px);
}

.signal__trace path {
  fill: none;
  stroke: var(--text-soft);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.signal__say {
  margin: 0.875rem 0 0;
  color: var(--text-soft);
  max-width: 52ch;
}

.signal__say b {
  color: var(--text);
  font-weight: 600;
}

.signal__say--ready,
#sig-ready:checked ~ .signal__panel .signal__say--threat {
  display: none;
}

#sig-ready:checked ~ .signal__panel .signal__say--ready {
  display: block;
}

.signal figcaption {
  margin-top: var(--s-4);
  max-width: var(--measure);
  color: var(--text-soft);
  font-size: 0.9375rem;
}

/* The one paragraph on the page that states what the app cannot do. It gets
   its own colour so it does not read as a footnote to the block above it.
   No side stripe: a coloured left border above a hairline is a costume, and
   the tint is already doing the separating. */
.sci__limits {
  max-width: var(--measure);
  padding: clamp(1.25rem, 3vw, 1.75rem);
  border-radius: var(--r-card);
  background: rgba(var(--rgb-sand), var(--fill));
}

.sci__limits b {
  font-weight: 600;
}

/* --- What Moorwell will not do ---------------------------------------------- */

/* One panel now, not two.
   --------------------------------------------------------------------------
   It was two side-by-side panels of equal weight: the refusals, in clay,
   and the worst-moment guarantees, in dusk. Two changes, from the owner
   reading the page in a browser.

   The hue. Clay is warm and it is the colour this site uses for a warning.
   Over a list of deliberate good decisions it read as an alert about the
   app, which is the exact opposite of the argument the list is making. It
   is sage now, the colour of what Moorwell does, because a refusal IS
   something Moorwell does.

   The width. One panel at the full 1120px wrap would put the reason column
   past any sensible measure, so the panel is capped: the 13rem term column,
   the 2rem gap, and 62ch of reason, plus its own padding. Same reasoning as
   --log-column on the changelog.

   A container query and not a media query, still: the panel used to sit in
   a two-column grid where its own width and the viewport's had nothing to
   do with each other. It measures itself, which also means the cap above
   cannot silently break the row layout. */
.nots {
  container-type: inline-size;
  margin: 0;
  max-width: 54rem;
  overflow: hidden;
  background:
    linear-gradient(152deg,
      rgba(var(--hue, var(--rgb-clay)), var(--fill)),
      rgba(var(--hue, var(--rgb-clay)), calc(var(--fill) * 0.36))),
    var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: clamp(0.5rem, 2vw, 1rem) clamp(1.375rem, 3vw, 2rem);
}

.nots__row {
  display: grid;
  gap: var(--s-2);
  padding-block: clamp(1.125rem, 2.6vw, 1.375rem);
}

.nots__row + .nots__row {
  border-top: 1px solid rgba(var(--hue, var(--rgb-clay)), 0.28);
}

.nots dt {
  font-weight: 600;
  color: var(--hue-ink, var(--ink-clay));
  line-height: 1.35;
}

.nots dd {
  color: var(--text-soft);
  max-width: 62ch;
}

/* The clause that names what everybody else ships. It is bold and it is in
   the body colour, so the row reads in two beats: Moorwell's refusal in the
   term, then the industry default, then why. That subject used to live in
   the head row over the whole list, where it captured the terms and made
   them read as things Moorwell does. */
.nots dd b {
  color: var(--text);
  font-weight: 600;
}

/* The panel's own title row. A heading element would land between the h3
   and nothing, so the list says what it is in its own first row instead.
   It also labels the two columns below it, which is why it names Moorwell on
   the left and other apps on the right. */
.nots__row--head dt {
  font-family: var(--font-head);
  font-size: 1.0625rem;
}

.nots__row--head dd {
  font-size: 0.875rem;
}

@container (min-width: 36rem) {
  .nots__row {
    grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
    column-gap: var(--s-7);
    align-items: baseline;
  }
}

/* --- The worst moment, as a strip ----------------------------------------- */

/* This was a second panel the same size as the one above it, carrying five
   entries. It is the most important content on the page and it was also
   the heaviest, which is a bad trade on a page a person is skimming.

   Two of the five stay in the open and may never be folded away, because
   without them the length and confidence of this page can be heard as a
   claim of clinical validation. The other three are behind a disclosure,
   in the markup, so they print and an in-page search still finds them.

   Dusk, not sage: this is a different kind of statement from the refusals
   above, and the site already spends dusk on it. */
.guard {
  max-width: 44rem;
  border-radius: var(--r-card);
  border: var(--card-border);
  background:
    linear-gradient(152deg,
      rgba(var(--hue, var(--rgb-dusk)), var(--fill)),
      rgba(var(--hue, var(--rgb-dusk)), calc(var(--fill) * 0.36))),
    var(--surface);
  padding: clamp(1.125rem, 2.6vw, 1.5rem) clamp(1.375rem, 3vw, 2rem);
}

.guard__h {
  font-size: 1.0625rem;
  color: var(--hue-ink, var(--ink-dusk));
  margin-bottom: var(--s-3);
}

.guard__list {
  display: grid;
  gap: var(--s-3);
  color: var(--text-soft);
  font-size: 0.9375rem;
  line-height: 1.6;
  max-width: 66ch;
}

.guard__list b {
  color: var(--text);
  font-weight: 600;
}

.guard__more {
  margin-top: var(--s-4);
}

.guard__more .guard__list {
  margin-top: var(--s-4);
}

.guard__toggle {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  list-style: none;
  font-size: 0.9375rem;
  /* The strip's own hue, not --accent. On a dusk tint in the dark theme
     --accent measures 3.62 against the panel, and this is text. --ink-dusk
     measures 5.52 there and 5.67 in light. */
  color: var(--hue-ink, var(--ink-dusk));
  min-height: 44px;
}

.guard__toggle::-webkit-details-marker {
  display: none;
}

.guard__toggle::marker {
  content: "";
}

.guard__toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Same caret as the changelog's, drawn from two borders rather than set as
   a glyph, so it turns over when the state does. */
.guard__toggle::before {
  content: "";
  display: inline-block;
  width: 0.42em;
  height: 0.42em;
  margin-right: 0.55em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.14em) rotate(45deg);
}

.guard__more[open] .guard__toggle::before {
  transform: translateY(0.06em) rotate(-135deg);
}

@media (prefers-reduced-motion: no-preference) {
  .guard__toggle::before {
    transition: transform 200ms var(--ease);
  }
}

@media (hover: hover) and (pointer: fine) {
  .guard__toggle:hover {
    color: var(--text);
  }
}

/* --------------------------------------------------------------------------
   11. Privacy
   -------------------------------------------------------------------------- */

.privacy__grid {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 900px) {
  .privacy__grid {
    grid-template-columns: 1fr minmax(0, 26rem);
  }
}

.privacy__body p + p {
  margin-top: 1.125rem;
}

.facts {
  background: var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: clamp(1.25rem, 3vw, 1.75rem);
}

.facts div {
  padding-block: 0.9375rem;
}

.facts div + div {
  border-top: 1px solid var(--hairline);
}

.facts dt {
  font-weight: 600;
  font-size: 0.9375rem;
}

.facts dd {
  color: var(--text-soft);
  font-size: 0.9375rem;
  margin-top: 0.2rem;
}

/* --------------------------------------------------------------------------
   12. Unfinished list
   -------------------------------------------------------------------------- */

.unfinished {
  display: grid;
  gap: 1.5rem 3rem;
  grid-template-columns: 1fr;
  max-width: 60rem;
}

@media (min-width: 780px) {
  .unfinished {
    grid-template-columns: repeat(3, 1fr);
  }
}

.unfinished li {
  border-top: 2px solid rgb(var(--hue, var(--rgb-sage)));
  padding-top: 1rem;
}

.unfinished h3 {
  margin-bottom: var(--s-2);
  color: var(--hue-ink, var(--ink-sage));
}

.unfinished p {
  color: var(--text-soft);
  font-size: 0.9375rem;
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   12b. Changelog pointer on the landing page
   -------------------------------------------------------------------------- */

.log__lede {
  margin-top: var(--s-5);
  color: var(--text-soft);
}

.log__cta {
  margin-top: 1.75rem;
}

/* --------------------------------------------------------------------------
   13. Closing invitation
   -------------------------------------------------------------------------- */

/* Two plain sections on the same background stack their full padding, which
   reads as a hole rather than as breathing room. The invitation carries its
   own top edge in the card. */
.section--tight {
  padding-top: 0;
}

/* Except when the section above it is sunk.
   ---------------------------------------------------------------------------
   `--tight` exists because two plain sections on the same background stack
   their full padding and leave a hole. That reasoning needs both sections to
   be on the *same* background — and `#try-it` follows `#changelog`, which is
   `--sunk`. There the background already does the separating, so removing the
   padding does not close a hole, it puts the card hard against the edge of the
   release panel.

   The owner, reading the live site: "the try it in your browser card is just
   on the border of every release, give some space there."

   Scoped to the adjacency rather than to `#try-it`, so any section that ends
   up after a sunk one gets the same treatment without anyone remembering. */
.section--sunk + .section--tight {
  padding-top: clamp(2.5rem, 6vw, 4.5rem);
}

.invite {
  max-width: 46rem;
  background: var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: clamp(2rem, 6vw, 3.75rem);
  display: grid;
  gap: var(--s-6);
  justify-items: start;
}

.invite p {
  max-width: 44ch;
}

/* --------------------------------------------------------------------------
   14. Footer
   -------------------------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--hairline);
  padding-block: clamp(2.5rem, 6vw, 4rem);
}

.footer__inner {
  display: grid;
  gap: var(--s-7);
  grid-template-columns: 1fr;
}

@media (min-width: 780px) {
  .footer__inner {
    grid-template-columns: minmax(0, 27rem) minmax(0, 1fr);
    gap: 3rem;
  }
}

.footer__note {
  margin-top: var(--s-4);
  color: var(--text-soft);
  font-size: 0.9375rem;
  max-width: 40ch;
}

.footer__version {
  margin-top: var(--s-4);
  font-size: 0.9375rem;
}

.footer__version a {
  color: var(--text-soft);
  text-decoration: none;
}

.footer__version a:hover {
  color: var(--text);
}

.footer__version .release__version {
  font-size: inherit;
  color: inherit;
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 2rem;
  align-content: start;
}

@media (min-width: 780px) {
  .footer__links {
    justify-content: flex-end;
  }
}

.footer__links a {
  color: var(--text-soft);
  text-decoration: none;
  /* Same 44px floor and the same reason as `.masthead__nav a`. Eleven of
     these on every page, and they were the site's smallest real targets at
     25px. The row already wraps with a 0.75rem row gap, so nothing shifts. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 0.9375rem;
}

.footer__links a:hover {
  color: var(--text);
}

/* --------------------------------------------------------------------------
   15. Changelog page
   -------------------------------------------------------------------------- */

/* One axis for the whole page.
   --------------------------------------------------------------------------
   Every release used to be a bare <details> with a hairline over it inside
   the 1120px wrap, so each one ran the full width of the page with nothing
   but a 1px rule holding it, and the text sat hard against the left end of
   that rule with several hundred pixels of nothing after it. Two changes,
   and they are one idea:

     1. The header and the list share --log-column, which is narrower than
        the wrap. The page now has a reading column the way the landing
        page's prose blocks do, rather than a full-bleed one.
     2. A release is a card, with the same surface, border, radius and
        shadow as every other card on this site (.bento__cell, .log__empty,
        .nots). It is the site's one card recipe, not a new one.

   The card is what centres it: the container alone still leaves a rule
   floating in space, and a background with two edges is what makes a
   reader see a contained object rather than a divider.

   Contrast, against the card rather than against the page it used to sit
   on, and against the hovered card as well, because that is a second state
   and not a decoration:
                                          light      dark
     version and body text on card        13.73     12.06
     summary text on card                  5.64      6.58
     "Read this release" on card           6.83      4.67
     summary text, card hovered            5.19      5.68
     "Read this release", card hovered    12.62     10.40
     card edge (hairline, decorative)      1.18      1.52
   The label goes to --text on hover rather than staying --accent, and the
   reason is the dark column: --accent is 4.67 on the card and 4.02 once the
   wash is over it, which is under 4.5 for text. */

.wrap.log,
.wrap.log__header {
  max-width: var(--log-column);
}

.log__header {
  padding-block: clamp(3rem, 7vw, 5rem) clamp(2rem, 5vw, 3rem);
}

.log__header p {
  margin-top: var(--s-5);
  max-width: var(--measure);
}

.log__current {
  margin-top: var(--s-4);
  color: var(--text-soft);
  font-size: 0.9375rem;
}

.log__current .release__version {
  font-size: inherit;
}

.log__back {
  display: inline-block;
  margin-bottom: var(--s-6);
  font-size: 0.9375rem;
  text-decoration: none;
  color: var(--text-soft);
}

.log__back:hover {
  color: var(--text);
}

/* Drawn, not a glyph: two borders on an empty box. It points the way the
   link goes, and on hover it moves that way, which is the whole reason it
   is there rather than a full stop. */
.log__back::before {
  content: "";
  display: inline-block;
  width: 0.42em;
  height: 0.42em;
  margin-right: 0.55em;
  border-left: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.06em) rotate(45deg);
}

@media (hover: hover) and (pointer: fine) {
  .log__back::before {
    transition: transform 180ms var(--ease);
  }

  .log__back:hover::before {
    transform: translate(-3px, -0.06em) rotate(45deg);
  }
}

.log {
  padding-bottom: clamp(4rem, 9vw, 7rem);
  display: grid;
  gap: var(--s-3);
}

.release {
  background: var(--surface);
  border: var(--card-border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
}

.release > summary {
  display: grid;
  cursor: pointer;
  padding: clamp(1.25rem, 3vw, 1.625rem) var(--log-pad);
  border-radius: calc(var(--r-card) - 1px);
  list-style: none;
  gap: 0.2rem var(--log-gap);
  grid-template-columns: 1fr;
  grid-template-areas:
    "ver"
    "dat"
    "sum"
    "tog";
}

/* The only feedback a whole-card click target needs, and it is paint on one
   element rather than movement: the card does not lift, because a page of
   thirty cards each lifting under the pointer is noise. Gated on a real
   pointer for the reason .bento__cell is: on a touch screen :hover sticks
   after a tap. */
@media (hover: hover) and (pointer: fine) {
  .release > summary {
    transition: background-color 150ms var(--ease);
  }

  .release > summary:hover {
    background: var(--wash);
  }

  /* The label snaps rather than fading, on purpose. --accent measures 4.67
     against the dark card and 4.02 once the wash is over it, which is under
     the 4.5 text threshold. --text clears it from the first frame, and no
     transition means there is no interpolated state that does not. */
  .release > summary:hover .release__toggle {
    color: var(--text);
  }
}

.release > summary::-webkit-details-marker {
  display: none;
}

.release > summary::marker {
  content: "";
}

.release > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: calc(var(--r-card) - 2px);
}

@media (min-width: 780px) {
  .release > summary {
    grid-template-columns: minmax(0, var(--log-rail)) minmax(0, 1fr) auto;
    /* Row 2 absorbs the summary's extra height. Without `1fr` and the
       `align-self` below, the date drifts a different distance under each
       version, because the summary spans both rows and splits their height
       between them. */
    grid-template-rows: min-content 1fr;
    grid-template-areas:
      "ver sum tog"
      "dat sum tog";
  }

  .release__version,
  .release__date,
  .release__toggle {
    align-self: start;
  }
}

.release__version {
  grid-area: ver;
  font-family: var(--font-mono);
  font-size: 1.0625rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--text);
}

.release__date {
  grid-area: dat;
  font-size: 0.875rem;
  color: var(--text-soft);
}

.release__summary {
  grid-area: sum;
  color: var(--text-soft);
  max-width: 58ch;
  margin-top: var(--s-2);
}

@media (min-width: 780px) {
  .release__summary {
    margin-top: 0;
  }
}

.release__toggle {
  grid-area: tog;
  font-size: 0.9375rem;
  color: var(--accent);
  white-space: nowrap;
  margin-top: var(--s-3);
}

@media (min-width: 780px) {
  .release__toggle {
    margin-top: 0;
  }
}

.release__toggle::after {
  content: "Read this release";
}

.release[open] .release__toggle::after {
  content: "Close";
}

/* The one piece of feedback a disclosure actually needs: something that
   turns over when the state does. Drawn from two borders for the same
   reason as the back link. */
.release__toggle::before {
  content: "";
  display: inline-block;
  width: 0.42em;
  height: 0.42em;
  margin-right: 0.5em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.14em) rotate(45deg);
}

.release[open] .release__toggle::before {
  transform: translateY(0.06em) rotate(-135deg);
}

@media (prefers-reduced-motion: no-preference) {
  .release__toggle::before {
    transition: transform 200ms var(--ease);
  }
}

/* NOT animated: the height of an opening release.
   ------------------------------------------------------------------------
   `::details-content` with `interpolate-size: allow-keywords` is the modern
   way to do this and it was written, measured and taken back out. Two
   reasons, and the second is the one that decided it.

   It animates a layout property, which everything else on this site avoids.
   And its failure mode is that the content never appears: the collapse is
   held open by a discrete `content-visibility` transition, so anywhere that
   transition does not run to completion, a release stays measurably 0px
   tall after `open` is set. That was reproduced here - the pseudo-element
   stayed `content-visibility: hidden` and `block-size: 0px` on an opened
   release - and the changelog is a page whose whole purpose is opening
   those. A 240ms slide is not worth a page that might not open.

   The caret above is the feedback the disclosure actually needed, and it
   cannot fail closed. */

.release__body {
  padding: 0 var(--log-pad) clamp(1.5rem, 4vw, 2.25rem);
  display: grid;
  gap: var(--s-7);
}

/* Aligned under the summary column, not under the version rail: the rail
   plus the grid gap plus the card's own padding. */
@media (min-width: 780px) {
  .release__body {
    padding-left: calc(var(--log-pad) + var(--log-rail) + var(--log-gap));
  }
}

.release__group h3 {
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--s-4);
}

.release__group ul {
  display: grid;
  gap: var(--s-4);
  max-width: 68ch;
}

.release__group li {
  position: relative;
  padding-left: 1.25rem;
  color: var(--text-soft);
  line-height: 1.6;
}

.release__group li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 0.4rem;
  height: 1px;
  background: var(--border);
}

.release__group li strong {
  color: var(--text);
}

.log__empty {
  border: var(--card-border);
  border-radius: var(--r-card);
  background: var(--surface);
  padding: 2rem;
  max-width: var(--measure);
}

/* --------------------------------------------------------------------------
   16. Motion
   --------------------------------------------------------------------------
   One authored moment per page: the first screenful settles in. It is an
   opacity change with a 10px rise, which is the category the app's own motion
   research treats as well tolerated. Everything else is hover, focus and
   press feedback.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }

  .settle {
    animation: settle 700ms var(--ease) both;
  }

  .settle-2 {
    animation-delay: 90ms;
  }

  .settle-3 {
    animation-delay: 180ms;
  }
}

@keyframes settle {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   16b. Arrival
   --------------------------------------------------------------------------
   Scroll-linked, and written in CSS rather than JavaScript: a view timeline
   costs no script, no listener and no layout read, and a browser without one
   simply shows the element, which is the correct fallback.

   Three different gestures, not one fade repeated down the page:
     - a section heading lifts,
     - a moment's coloured rule draws itself in from the left,
     - the bento tiles rise with a stagger taken from their own position.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .section__head,
    .privacy__body,
    .invite,
    .facts {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 22%;
    }

    /* One entrance per object. `.privacy__body` on the landing page contains a
       `.section__head`, so both matched and the floor applied twice —
       0.72 x 0.72 = 0.52, which is 3.4:1 for this ink on this ground and the
       one place the new floor did not clear AA. The parent already carries the
       gesture; the child arriving separately inside it was never the design.

       Found by walking the rendered tree for an animated element with an
       animated ancestor, which is the only way to see it: each rule is correct
       on its own and the selector list gives no hint that two of its entries
       can nest. */
    .privacy__body .section__head {
      animation: none;
    }

    .moment::before {
      animation: draw linear both;
      animation-timeline: view();
      animation-range: entry 12% cover 26%;
    }

    .bento__cell,
    .unfinished li {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 2% cover 26%;
    }

    /* The mechanism rows stagger off their own position, exactly as the
       bento tiles do. */
    .mech__row {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 24%;
    }

    /* A panel of refusals arrives as one object, because the refusals in it
       are one argument. Its range end is a LENGTH and not a percentage, and
       that is the whole point: both `entry` and `cover` are measured
       against the element's own height, so on a panel taller than the
       scrollport any percentage becomes hundreds of pixels of scrolling
       spent at partial opacity. Measured here at 0.62 on a changelog entry
       that was on screen at page load. A length says "260px into the
       entry, whatever this thing's height is". */
    .nots,
    .guard {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 280px;
    }

    /* A fourth gesture, for the two things on the site a visitor can
       actually operate. They do not rise: they come into focus, which is a
       different sentence about a different kind of object. No translate at
       all, so nothing under the cursor moves as it settles. */
    .demo__pane,
    .loop__stage {
      animation: focus-in linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 18%;
    }

    /* The changelog. It had none of this and read as a different site. An
       open release runs to several thousand pixels, so the range end is a
       length here for the same reason as the panel above. */
    .log__header,
    .release {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 240px;
    }
  }
}

/* The disclosure inside the crisis strip. It runs on [open], not on a view
   timeline, because the trigger is the tap and not the scroll.

   It is outside the @supports block above on purpose - a browser with no
   view timeline still has @keyframes - and inside the reduced-motion gate,
   where the list simply appears. Nothing is carried by the motion: closed
   is closed and open is open, in every case.

   The four-group stagger that used to sit here belonged to "See every
   source" on the landing page's research section, which moved to
   evidence.html on 2026-08-16 and took its forty citation rows with it. */
@media (prefers-reduced-motion: no-preference) {
  .guard__more[open] .guard__list {
    animation: lift 260ms var(--ease) both;
  }
}

/* The composed sentence is pushed away from the reader and the identifier is
   what stays. Both settle at identity, so the resting state of the section is
   flat, square and undistorted — a page whose final state is rotated is a
   page that looks broken to anyone who arrives at the bottom of it. */
@keyframes recede {
  from {
    transform: perspective(1600px) translateZ(calc(-90px * var(--motion-move)))
               rotateY(calc(9deg * var(--motion-move)));
    opacity: 0.72;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

@keyframes settle-square {
  from {
    transform: perspective(1600px) translateZ(calc(-40px * var(--motion-move)))
               rotateY(calc(-5deg * var(--motion-move)));
  }
  to {
    transform: none;
  }
}

@keyframes draw-line {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* THE ENTRY FLOOR, AND WHY IT IS NOT ZERO.
   --------------------------------------------------------------------------
   Both keyframes below used to start at `opacity: 0`, and both are driven by
   a view timeline with `both` fill — so every element that had not yet
   entered its range held the `from` state. Measured 2026-08-29 at 1440px,
   unscrolled, motion on: 43 of safety.html's 73 text blocks were painted at
   zero, 26 of mission.html's 36, and 23 of vision.html's 44. That is not an
   animation. It is a page that withholds most of itself from anyone who has
   not yet scrolled it, and it withholds it from a print, a screenshot and a
   social preview permanently, because none of those scroll.

   The fix is the one the handoff asks for: make the read state the default
   and let the animation subtract from it. So the floor is a value a reader
   can read at. 0.72 is not a new number — `recede` above already chose it
   for the same sentence, "pushed away but still there".

   This is a TINT decision in the two-channel vocabulary further down: the
   opacity is what a reduced-motion reader keeps, the translate is what
   collapses. A tint channel that reaches zero is not a gentler equivalent of
   the movement, it is the content being absent, and this is a site read by
   people in a bad state at 2am. Nothing on it should have to be earned by
   scrolling.

   One dial, declared beside the two keyframes that consume it, so tuning the
   entrance is one edit and the two gestures cannot drift apart. */
:root {
  --enter-floor: 0.72;
}

@keyframes focus-in {
  from { opacity: var(--enter-floor); transform: scale(0.985); }
  to   { opacity: 1; transform: none; }
}

@keyframes lift {
  from { opacity: var(--enter-floor); transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@keyframes draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* --------------------------------------------------------------------------
   17. The desktop tier
   --------------------------------------------------------------------------
   Everything above this point is responsive and always was: twenty
   breakpoints, real multi-column from 720 up. What the file never had is a
   tier ABOVE its own container. `.wrap` caps at 1120px and the largest
   query in the file is 1100px, so 1440, 1728 and 2560 all render
   byte-identically to 1120 — a phone-width strip of content sitting in the
   middle of the display belonging to the reader most likely to be
   evaluating this for funding. That is the defect. It is not that the site
   "serves the mobile layout"; it is that it stops improving 300px before
   the reader starts.

   So this section is additive and every block in it carries a 1280px
   minimum, which is above every existing query in the file. Nothing below
   1280 is touched by anything here. That is not a promise to be careful —
   it is a consequence of the minimum, and it is what lets the mobile view
   be left alone by construction.

   No colour, accent or gradient value is named here. This section is
   geometry only. */

:root {
  --wrap-wide: 1440px;
}

/* Opt-in, never automatic. A section takes the wide container by asking for
   it, so a reading column that should stay at --measure cannot be widened
   from here by accident. `.wrap` keeps its 1120 and its exact meaning.

   THE GATE IS 1600, NOT 1280, AND IT IS THE WHOLE DECISION.
   -------------------------------------------------------------------------
   Phase 2 shipped this class unused and Phase 4 declined to wire it blind.
   Both were right, and the reason is arithmetic rather than caution. `.wrap`
   carries `padding-inline: clamp(1.25rem, 4vw, 2.5rem)`, which is pinned at
   its 2.5rem = 40px ceiling for every viewport at or above 1000px. So a
   1440px max-width AT a 1440px viewport is not "more breathing room" — it is
   the page running edge to edge with 40px of padding standing in for a
   margin, which is a narrower outer margin than the phone layout has.

   Rendered and measured in a real browser on 2026-08-16 (headless Chrome
   151 over CDP, which composites — see the note in the build direction about
   why the previous session's readings could not):

     viewport   .wrap box   outer margin   content width   share of screen
     ---------- ----------- -------------- --------------- ---------------
     1440       1120        160            1040            72%
     1600       1120        240            1040            65%
     2560       1120        720            1040            41%

   41% is the defect §17 was named for, still present at the width the
   defect is worst at. But the fix only clears the margin it needs from 1600
   up: at 1600 a 1440 box leaves 80px of true outer margin either side, and
   at 2560 it leaves 560. At 1440 it leaves zero, which is why the gate is
   not 1280.

   The consequence, stated so it does not have to be rediscovered: below
   1600, `.wrap--wide` and `.wrap` are the same container, byte for byte. So
   the class is safe to put on any section at any width — every phone and
   every laptop renders it exactly as `.wrap`, and only a large desktop sees
   a difference. That is the same construction that makes the whole of §17
   safe for mobile, applied one tier higher.

   Why 1440 and not more. The content ceiling is a reading decision, not a
   screen decision: the widest thing on the site is a three-column grid, and
   at 1440 minus padding that is three 440px cells, which is the size the
   bento was designed around. A 1920 container would give 600px cells and a
   heading measure nobody can track back to the next line. So the cap stays
   where the content wants it and the extra screen becomes margin, which is
   what margin is for. */
.wrap--wide {
  max-width: 1120px;
}

@media (min-width: 1600px) {
  .wrap--wide {
    max-width: var(--wrap-wide);
  }
}

@media (min-width: 1280px) {
  /* These three caps are the specific rules that made desktop read as
     phone-shaped. A 15ch headline inside a 1120 column inside a 1440
     viewport is about 340px of visible mass with the rest of the screen
     empty. Widened rather than removed: the measure still has a job, it
     was just chosen for one width. */
  .hero h1 {
    max-width: 20ch;
  }

  .hero p.lead {
    max-width: 46ch;
  }

  .section__head {
    max-width: 52ch;
  }

  /* The base clamps stop growing at roughly 1085px of viewport and are
     frozen for every width above it. These re-declare the ceiling only.

     The numbers are chosen so the curve is CONTINUOUS at 1280 rather than
     stepping: `.display` reaches exactly 4rem at 1280 (-1rem + 6.25vw =
     64px there, which is the base clamp's own maximum) and rises to
     5.25rem at 1600. `.h2` reaches exactly 2.5rem at 1280 (3.125vw = 40px)
     and 3.125rem at 1600. A breakpoint that jumps type by 7px is a
     breakpoint you can watch happen while resizing. */
  .display {
    font-size: clamp(4rem, -1rem + 6.25vw, 5.25rem);
  }

  .h2 {
    font-size: clamp(2.5rem, 3.125vw, 3.125rem);
  }
}

@media (min-width: 1600px) {
  .hero h1 {
    max-width: 22ch;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .masthead {
    background: var(--page);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* Specificity, deliberately: the two dark blocks above are (0,2,0), so a bare
   `:root` here would lose to them and the high-contrast override would apply
   in the light theme only. `:root:root` ties, and later wins. */
@media (prefers-contrast: more) {
  :root:root {
    --hairline: var(--border);
    --card-border: 1px solid var(--border);
    --fill: 0.09;
    --fill-hi: 0.13;
  }

  .masthead {
    background: var(--page);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .bento__cell::after,
  .demo__pane::after {
    border-color: rgba(var(--hue, var(--rgb-sage)), 0.7);
  }
}

@media print {
  .masthead,
  .skip {
    display: none;
  }

  .release__body {
    display: grid !important;
    padding-left: 0;
  }

  /* A printed page does not scroll, so every view timeline on it is frozen
     wherever the reader left it — which for anything below the fold is the
     entrance's first frame. The floor above keeps that legible rather than
     blank; this makes it right. Paper is the one surface where the entrance
     has no reader to arrive for, so it is simply not run.

     Scoped to the two entrance keyframes rather than swept with `*`, for the
     reason section 33 gives about the blunt version: a sweep here would also
     take out the drawn rules and the clip-path reveals, which carry
     structure on paper as much as on screen. */
  .section__head,
  .privacy__body,
  .invite,
  .facts,
  .bento__cell,
  .unfinished li,
  .mech__row,
  .nots,
  .guard,
  .demo__pane,
  .loop__stage,
  .log__header,
  .release {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* --------------------------------------------------------------------------
   Privacy page — callouts, quotes and wide tables.

   Added 2026-08-11, after the generated page was opened on a phone and its
   `[!important]` callout rendered as ordinary body text. The generator emitted
   `.callout`, `.callout--*`, `.quote` and `.table-scroll`; none of them existed
   here. Nothing failed: the markup was valid, the page rendered, and only
   looking at it showed that the one paragraph meant to stand out did not.

   `test/privacy_page_test.dart` now asserts every class the generator emits is
   defined in this file, so the next invented name fails the build instead.
   -------------------------------------------------------------------------- */

.prose .callout,
.prose .quote {
  padding-left: var(--rail-gap);
  margin: 1.5rem 0;
  padding: 1rem 1.15rem;
  border-left: 3px solid color-mix(in srgb, currentColor 25%, transparent);
  border-radius: 0 6px 6px 0;
  background: color-mix(in srgb, currentColor 4%, transparent);
}

.prose .callout > :first-child,
.prose .quote > :first-child { margin-top: 0; }
.prose .callout > :last-child,
.prose .quote > :last-child { margin-bottom: 0; }

.prose .callout__title {
  font-weight: 600;
  /* The title is a heading in role but not in outline: it must not enter the
     document's heading structure, because a screen-reader user navigating by
     heading would find "danger" between two policy sections. */
  margin-bottom: var(--s-2);
}

/* The three levels the document actually uses. Colour is not the only signal —
   the border weight changes too, so the distinction survives greyscale, a
   colour-blind reader, and a printed copy. */
/* The two edge colours were `#4a5d46` and `#8c4a3f` — 1.0's accent-ink and
   clay-ink, typed rather than referenced, which is why they were still 1.0
   after the palette moved. `--ink-sage` and `--ink-clay` are the same two
   roles in the 2.0 palette and they are defined per look, so these now
   follow Night and Sage instead of staying at their Calm values. */
.prose .callout--important { border-left-color: var(--ink-sage); }
.prose .callout--danger {
  border-left-width: 4px;
  border-left-color: var(--ink-clay);
}
.prose .callout--note { border-left-color: color-mix(in srgb, currentColor 35%, transparent); }

/* A policy table on a 320pt screen is wider than the screen. Scrolling the
   table is right; scrolling the page sideways is not — a body that scrolls
   horizontally makes every other line hard to read. */
.prose .table-scroll {
  overflow-x: auto;
  margin: 1.5rem 0;
  /* Tells a touch device this is a scroll surface of its own. */
  -webkit-overflow-scrolling: touch;
}

.prose .table-scroll table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95em;
}

.prose .table-scroll th,
.prose .table-scroll td {
  text-align: left;
  vertical-align: top;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}

.prose .table-scroll th { font-weight: 600; }

/* --------------------------------------------------------------------------
   18. Surfaces — elevation is a boundary, never a ground
   --------------------------------------------------------------------------
   Phase 3 of docs/Public Site - Build Direction.md. Section 1 declares
   elevation once per mode and section 17 adds a width tier; neither says how a
   *new* surface is built, and phases 4-8 need that written down before they
   start inventing one per page.

   Everything here is a token or a utility class. Nothing below changes an
   existing component except one declaration, called out where it happens, and
   nothing below is width-gated — a contrast floor is not a desktop feature.

   ## The finding, first: each theme has exactly ONE elevation channel with
   ## headroom, and they are opposite ones.

   The app floors a card's separation from its page at **1.25:1** by fill,
   edge, rim or shadow — whichever reaches it at the smallest alpha
   (`_cardBoundaryFloor`, lib/design/theme.dart:352). Measured against this
   palette, in both directions:

     channel                                  light        dark
     ---------------------------------------- ------------ ------------
     fill: --surface against --page             1.072        1.156
     edge: --hairline against --page            1.177        1.523
     rim:  white at 100%, against --page        1.119       16.762
     rim:  white at 100%, against --surface     1.044       14.502
     shadow: ink/black at 100%, against --page 12.809        1.253

   THREE OF THESE CELLS WERE WRONG until 2026-08-16, and the correction is
   recorded rather than quietly applied because one of them cost a red suite.
   The rim row carried a single light cell, 1.044, under a heading that reads
   "against --page" — but 1.044 is white against --surface (#fbfaf6); against
   --page (#f4f2ec) it is 1.119. `test/site_contrast_test.dart` was written
   from the table, asserted `< 1.10` against the page, and failed on a
   stylesheet that was correct. The dark rim cell read "1.809 (at 14%)"; at
   14% it is 1.565, and 1.809 needs 18.4%. The light shadow cell read 4.750;
   ink at 100% against the light page is 12.809. **Every cell above is now
   recomputed by that test rather than trusted**, which is the whole reason it
   exists — a hand-computed number in a comment is a claim with no guard on it,
   and this section opened with a whole table of them.

   Read the two extreme cells. **A pure white top edge on the light page
   reaches 1.119:1, and 1.044:1 on the card it would actually be drawn on** —
   both under the 1.25 floor below, so the treatment that carries every glass
   surface on the web is not merely weak here, it is unavailable, at any
   opacity. And **a fully opaque black drop shadow on the dark page reaches
   1.253:1** — that is the ceiling of the entire shadow channel in dark, at an
   opacity no design would ship. Each theme's headroom runs one way only.

   So: **light elevates downward** (an ink edge and a shadow), **dark elevates
   upward** (a bright rim). Neither theme may borrow the other's channel. This
   is the same arithmetic that rules out glass on light pages, arriving at the
   surface question instead of the blur question, and it is why section 1's
   "light carries a hairline, dark carries a lifted fill" was right — this
   section only supplies the numbers and the missing third case.

   ## Why the GROUND never moves

   The obvious way to build a lifted surface in dark is a brighter fill, and it
   was measured before it was rejected. Mixing --text 10% into --surface gives
   a fill 1.325:1 above it — and on that ground **--accent falls from 4.667 to
   3.522** (under AA) and **--border falls from 3.808 to 2.874** (under the 3:1
   WCAG 1.4.11 asks of a control edge). One surface, two regressions, in a
   theme where every accent was measured against --surface.

   That is the audit the build direction warns about — checking only the page
   put 10 of 35 accent cells under AA once. The rule that makes it impossible
   to repeat:

     **A raised surface changes its boundary. It never changes its ground.**

   Every level below paints on --surface (or --page, or --surface-sunk for a
   recessed band). So every text ratio in this file's header is measured once
   and holds at every elevation, and a new surface cannot silently invalidate
   a colour that was checked somewhere else.

   ## Rejected

   - **A shadow ramp** (Material 3's elevation model). Two of the six levels
     would be indistinguishable in light and all six in dark — see the shadow
     column above.
   - **backdrop-filter on anything new.** The blur budget is exactly one use,
     the masthead, and it stays that way. Blur is not an elevation channel
     here; it is a cost with a single sanctioned payer.
   - **A third surface colour.** No palette value is added, changed or
     introduced by this section. --rim is white at an alpha; --edge and
     --edge-lift are the existing --hairline and --border.
   -------------------------------------------------------------------------- */

:root {
  /* Light. The rim is `none` rather than a faint white because a faint white
     is not faint here — it is absent (1.119 against the page, 1.044 against
     the card, both under the 1.25 floor, above). Declaring it none says
     "this channel does not exist in this theme", which is a fact a future
     surface should not have to rediscover. */
  --rim: none;
  --rim-strong: none;

  /* The channel that actually carries a light card, and the stronger one for
     the level above it. Both already exist; naming them by ROLE is what lets
     a component ask for "the boundary" instead of naming a colour. */
  --edge: var(--hairline);        /* 1.177 against the page */
  --edge-lift: var(--border);     /* 3.110 against the page */

  /* The same ink triple --shadow already uses (43, 42, 40 = --text), pushed
     one step. 0.22 reaches 1.526 over the page; the tight 5% pair is the
     contact shadow that stops the card looking like it is floating free. */
  --shadow-lift:
    0 2px 4px rgba(43, 42, 40, 0.05),
    0 28px 60px -28px rgba(43, 42, 40, 0.22);
}

/* KEEP THE TWO BODIES BYTE-IDENTICAL — the rule section 1 sets, and this
   section adds four tokens to both. A token added to one path and not the
   other is invisible until somebody tests the other path. */

:root[data-theme="dark"] {
  /* 0.08 is the smallest alpha that clears the 1.25 floor: 1.281 against
     --surface, 1.481 against --page. 0.07 measures 1.240 and misses.
     `inset 0 1px 0` at zero blur is the app's own solved lifted-surface
     recipe — light catching the top of a real material — and it costs no
     blur, no layout and no extra paint area. */
  /* 0.085, not 0.08. The 2.0 Night surface is #19171f where 1.0's was
       #272a28 — darker, so a white rim at the old alpha composites to a
       smaller step: measured 1.249:1 against the surface, a thousandth under
       the app's own 1.25 boundary floor. The rim is the ONLY elevation
       channel dark has (a drop shadow on a near-black ground reaches 1.253 at
       full black, which is why §18 rejected it), so this is not a value to
       leave a rounding error below its floor. 0.085 measures 1.270. */
    --rim: inset 0 1px 0 rgba(255, 255, 255, 0.085);
  --rim-strong: inset 0 1px 0 rgba(255, 255, 255, 0.14);

  /* Dark's card carries no border by section 1's decision, and that decision
     stands: a 1px border under a soft shadow is the ghost card. The level
     above it can afford one, because it has no shadow to ghost against. */
  --edge: transparent;
  --edge-lift: var(--hairline);   /* 1.523 against the page */

  /* THE ONE EXISTING DECLARATION THIS SECTION CHANGES, and it is a defect fix
     rather than a design change. `--shadow: none` in dark left --facts,
     --invite, .release and .log__empty separating from the page by fill
     alone, at **1.156:1** — under the app's own 1.25 floor, with no edge and
     no shadow to make it up. The changelog page is 62KB of .release.
     A drop shadow cannot fix it (1.253 at full black, above). The rim can,
     and does, at 1.481. Cards that already carry their own hue edge —
     .bento__cell and .demo__pane — paint that edge over the rim in the
     same pixel row and are visually unchanged. */
  --shadow: var(--rim);

  /* No drop shadow. Deliberately: see the 1.253 ceiling. A dark drop shadow
     is a paint cost that buys 0.1 of ratio, which is how a design ends up
     with a shadow ramp nobody can see. */
  --shadow-lift: var(--rim-strong);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    /* 0.085, not 0.08. The 2.0 Night surface is #19171f where 1.0's was
       #272a28 — darker, so a white rim at the old alpha composites to a
       smaller step: measured 1.249:1 against the surface, a thousandth under
       the app's own 1.25 boundary floor. The rim is the ONLY elevation
       channel dark has (a drop shadow on a near-black ground reaches 1.253 at
       full black, which is why §18 rejected it), so this is not a value to
       leave a rounding error below its floor. 0.085 measures 1.270. */
    --rim: inset 0 1px 0 rgba(255, 255, 255, 0.085);
    --rim-strong: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    --edge: transparent;
    --edge-lift: var(--hairline);
    --shadow: var(--rim);
    --shadow-lift: var(--rim-strong);
  }
}

/* The three levels, as classes phases 4-8 ask for by name rather than
   rebuilding. Named for what they ARE in the page's hierarchy, not for a
   number — an "elevation 3" tells a later reader nothing about whether it
   may hold a control. */

/* Content sitting on the page. No boundary, and it is a class rather than an
   absence so that a component can be moved down a level without deleting the
   line that says which level it is on. */
.surface-flat {
  background: var(--page);
  border: 1px solid transparent;
  box-shadow: none;
}

/* A card. Identical in effect to what .facts / .invite / .release already
   compose by hand, so adopting it is a refactor and not a restyle. */
.surface-raised {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
}

/* The one thing above everything — a popover, a sticky side panel, the open
   state of a disclosure that overlaps what it came from. There is no level
   above this and there must not be: a fourth would have to reach for the
   channel the theme does not have. */
.surface-lifted {
  background: var(--surface);
  border: 1px solid var(--edge-lift);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-lift);
}

/* A recessed band. The existing .section--sunk treatment, named so a card can
   also be sunk without borrowing a section's class. --surface-sunk is 1.055
   below the light page and 1.084 below the dark one: too shallow to be a
   boundary on its own, which is correct — a sunk band is a change of ground
   for a whole region, read from its EDGES against the sections above and
   below it, not from a card-sized patch. Never sink a card. */
.surface-sunk {
  background: var(--surface-sunk);
  border: 1px solid transparent;
  box-shadow: none;
}

/* prefers-reduced-transparency reaches the masthead in section 17 and nothing
   here, because nothing here is transparent — the rim is a paint, not a veil,
   and that is exactly why it was chosen over the treatment that would have
   needed this block.

   prefers-contrast: more DOES reach here. Section 17 already promotes
   --hairline to --border, which promotes --edge with it for free (--edge
   reads --hairline, not a literal). Dark needs one line, because its --edge
   is `transparent` and reads nothing: */
@media (prefers-contrast: more) {
  :root:root {
    --edge: var(--border);
    /* The rim doubles rather than disappears. A reader asking for more
       contrast wants the boundary louder, not swapped for a different one:
       1.809 against the page in dark, and still `none` in light, where the
       channel does not exist at any setting. */
    --rim: inset 0 1px 0 rgba(255, 255, 255, 0.16);
  }
}

/* --------------------------------------------------------------------------
   19. Motion — a vocabulary, and two channels through reduced motion
   --------------------------------------------------------------------------
   Sections 16 and 16b hold the site's authored motion. What they do not hold
   is a vocabulary: fourteen literal durations across the file, one shared
   easing, and no written rule for which of them survives a reduced-motion
   request. Phases 4-8 add a scroll-driven pipeline, a composition-guard
   gesture and a filterable register. Those need the rule to exist first.

   ## Every duration below is a value the file ALREADY USES

   180ms is the hover-colour timing at three call sites; 320ms is the card
   lift at four; 700ms is the arrival settle. This section names them and adds
   nothing new, on purpose. **A vocabulary that retunes the timings is a
   redesign wearing a token's clothes** — the point is that a later page can
   reach for `--dur-tint` and land on the site's existing tempo rather than
   guessing 250ms.

   Their justification, now that they are written down once (Emil Kowalski's
   duration table, which these already sat inside): press feedback 100-160,
   tooltips 125-200, disclosures 150-250, modals 200-500, and UI under 300ms
   as a ceiling. --dur-explain is the deliberate exception: an explanatory
   animation on a marketing page is not UI, nobody is waiting on it, and the
   700ms settle predates this section and is correct.

   ## The reduced-motion rule, and why it is two channels rather than one

   The blunt version of this — `* { animation: none !important }` — is worse
   than nothing here. It strips the colour change that tells a reader their
   hover landed, and it turns every transition into a snap, which reads as
   broken rather than as calm. Apple's guidance and the app's own motion
   research agree and say the same thing from different directions: reduced
   motion means a *gentler, non-vestibular equivalent*, and colour and opacity
   transitions are the licensed category while large positional movement,
   parallax and abrupt zoom are the risky ones.

   So there are two channels and the request separates them:

     --motion-tint   colour and opacity. SURVIVES reduce.
     --motion-move   transform and position. Collapses to zero under reduce.

   A duration written `calc(--dur-move * var(--motion-move))` is 320ms
   normally and 0ms under reduce, which removes the transition rather than
   accelerating it. The tint durations are untouched by the multiplier and
   keep their full value, which is the whole point.

   ## What this section found, and fixes

   Section 8 of the build direction says motion "lives inside
   @media (prefers-reduced-motion: no-preference)". That is true of every
   `animation:` in this file and **false of nineteen of the twenty-three
   `transition:` declarations**, which sit outside any reduced-motion gate.
   Most of them are colour and are correct there by the rule above. Five move
   something, and those are listed and neutralised at the bottom of this
   section. This is an app for people in distress; the gap is small and it is
   not one to leave for the phase-8 audit to find.
   -------------------------------------------------------------------------- */

:root {
  /* --ease is declared in section 1 and is a public API across three files —
     cubic-bezier(0.22, 1, 0.36, 1), a strong ease-out. It is not renamed,
     not retuned and not wrapped. These three sit beside it, each for a job
     it is the wrong curve for. */

  /* On-screen movement between two states the reader can see both of. An
     ease-out here reads as the object being flung; movement wants
     acceleration at both ends. */
  --ease-move: cubic-bezier(0.65, 0, 0.35, 1);

  /* A colour or opacity change under the cursor. Near-symmetrical on purpose:
     a hover tint has no origin and no destination, so a strong ease-out gives
     it a direction it does not have. */
  --ease-tint: cubic-bezier(0.4, 0, 0.6, 1);

  /* The mirror of --ease, for the RETURN leg of a reversible pair only — a
     panel closing along the path it opened. Never for an entrance: an
     ease-in entrance delays the first frames, which are the ones the reader
     is watching hardest, and a 300ms ease-in dropdown feels slower than a
     400ms ease-out one. */
  --ease-exit: cubic-bezier(0.64, 0, 0.78, 0);

  /* The two channels. 1 = the motion runs. */
  --motion-tint: 1;
  --motion-move: 1;

  /* Tint channel — unaffected by the multiplier by design, because these
     durations are what a reduced-motion reader keeps. */
  --dur-press: 120ms;   /* the press, felt not watched */
  --dur-tint: 180ms;    /* hover and focus colour; the file's existing value */
  --dur-reveal: 240ms;  /* a disclosure opening */

  /* Move channel — every one multiplied, so a single media query below
     retires all of them at once and no call site has to remember. */
  --dur-move: calc(320ms * var(--motion-move));     /* a card lifting */
  --dur-settle: calc(420ms * var(--motion-move));   /* something arriving */
  --dur-explain: calc(700ms * var(--motion-move));  /* motion that teaches */
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-move: 0;
  }

  /* Zero duration removes a transition; it does not stop the transform being
     APPLIED. An instant 2px jump under the cursor is worse than no lift at
     all, so the five states in this file that move something are told not to
     move. Each keeps a non-positional signal, which is the requirement —
     reduced motion is a gentler equivalent, not a silent one.

     Enumerated rather than swept. A `*` rule here would also cancel the
     scaleY that tells a reader which mood they picked, and there is no
     selector for "transforms that are decorative". */

  /* Card lift on hover. The hue edge still brightens (section 10) — the
     feedback survives, the movement does not. */
  .bento__cell:hover {
    transform: none;
  }

  /* The changelog's back arrow slid 3px toward its direction of travel. It
     still recolours on hover. */
  .log__back:hover::before {
    transform: translateY(-0.06em) rotate(45deg);
  }

  /* The mood bar's selected state scaled 12% and was listed here. The five
     radio buttons it belonged to became one `input[type=range]` on
     2026-08-16 (section 10a), and a native range thumb does not move under
     a transform of ours — it moves because the value changed, which is the
     control doing its job and not an animation. Nothing replaced this rule.

     The reply's 420ms arrival keyframe went at the same time and for a
     reason that outlives it: a keyframe restarts from zero where a
     transition retargets, so an animation on a value that changes four times
     during one drag flickers. */

  /* Already correct and listed so a later reader does not "fix" them twice:
     .breath__circle is pinned in section 10b, every `animation:` in the file
     sits inside a no-preference gate, and `scroll-behavior: smooth` is in
     section 16's gate. */
}

/* The theme change itself, which was the one abrupt brightness jump left on
   the site: the Light/Dark control repainted the whole page in a single
   frame, and this page is read at 2am by people whose eyes are adjusted to a
   dark room.

   It is a TINT transition, so it survives reduced motion — which is the
   two-channel rule doing exactly the job it was written for. It costs nothing
   at load: a transition never runs on an element's initial value.

   Bounded to the surfaces that actually repaint, not applied with `*`.
   A universal selector would attach a transition record to every node on a
   62KB page to catch nine, and would silently become the fallback timing for
   any future element whose own rule forgets to declare one. */
body,
.masthead,
.section--sunk,
.facts,
.invite,
.release,
.log__empty,
.bento__cell,
.demo__pane {
  transition:
    background-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* The one above that also carries its own hover transition declares it after
   this rule in source order — .bento__cell (section 10) sits earlier in the
   file, so this rule would win and cancel its lift. Re-declared here, tint
   and move together, in the new vocabulary.

   `.study` was the second name in this list until 2026-08-16. It belonged to
   the landing page's research cards, and section 10d went with them. */
.bento__cell {
  transition:
    transform var(--dur-move) var(--ease),
    background-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* --------------------------------------------------------------------------
   20. The pages beyond the first
   --------------------------------------------------------------------------
   Phases 4-8 of docs/Public Site - Build Direction.md. Six pages join the
   landing page, and this section is everything they need that sections 1-19
   did not already provide.

   ## Three rules this section holds itself to

   1. **It adopts the surface classes rather than composing a seventh card by
      hand.** §12.1 of the build direction found `.surface-raised`,
      `.surface-lifted`, `.surface-flat` and `.surface-sunk` built, measured
      and used by nothing — AGENTS.md §3.5's first row, reproduced inside the
      plan that quotes it. §12.2 explained why Home could not fix that: its
      only real cards are the four names reserved for a separate refactor.
      The pages below have cards of their own, so this is where the debt is
      paid. Every card here is `.surface-raised` or `.surface-lifted` plus a
      hue, and none of them redeclares a background, a border or a shadow.

   2. **It names no colour.** Every tint is `rgba(var(--hue), var(--fill))`
      through the `.hue-*` indirection section 1b established, every ink is
      `var(--hue-ink)`, and the elevation channels are `--edge`, `--rim` and
      `--shadow` from §18. So a palette change reaches this section without
      touching it, and the arithmetic in §18's header stays true here: a
      raised surface changes its boundary, never its ground.

   3. **Every duration is a §19 token.** Nothing below writes a millisecond
      literal, so the whole of it retires under `prefers-reduced-motion`
      through `--motion-move` without a single extra media query.

   ## Width

   Blocks that are a reading column stay in `.wrap`. Blocks that are a grid
   or a diagram take `.wrap--wide`, which §17 gates at 1600 — so on every
   phone, tablet and laptop these are the same container, and only a large
   desktop sees the difference. That is the whole of the desktop-tier
   ambition: it arrives where there is room for it and is inert everywhere
   else.
   -------------------------------------------------------------------------- */

/* --- The interior page head ----------------------------------------------
   The landing page's `.hero` is a full-height arrival with a gradient behind
   it. An interior page is somewhere a reader has already decided to go, so
   it gets a smaller one: an eyebrow that says which page this is, the claim,
   and the sentence that opens underneath it. No gradient — the gradient is
   the landing page's, and repeating it on seven pages would spend the one
   moment of colour the site has seven times. */

.page-head {
  padding-block: clamp(3rem, 7vw, 5.5rem) clamp(2rem, 4vw, 3rem);
  border-bottom: 1px solid var(--hairline);
}

.page-head h1 {
  max-width: 18ch;
}

.page-head .lead {
  margin-top: var(--s-5);
  max-width: var(--measure);
}

@media (min-width: 1280px) {
  .page-head h1 {
    max-width: 22ch;
  }
}

/* --- The layered claim ---------------------------------------------------
   The proposal's §3 in one component, and it is the site's central idea
   rather than a widget: one sentence anyone can read, with the
   substantiation opening a click beneath it. Someone having a hard week
   reads the summary and stops; a grant reader opens every one.

   `<details>`, not a JavaScript accordion. It is keyboard-reachable and
   screen-reader-announced without a line of script, it prints open in
   browsers that honour the print rule below, and — the reason that decides
   it — a reader searching the page with Cmd-F finds text inside a closed
   `<details>` in every current engine. A div that is `display: none` is
   invisible to that search, which would hide the evidence from the reader
   most likely to be looking for it. */

/* 🔴 **A claim's rule is a separator BETWEEN claims — 2026-10-11.** `.claim`
   carries `border-top` and no top margin, which is exactly right in a run of
   them: claim, hairline, claim, each padded 36. It is wrong the moment a
   claim follows something else, because the hairline is then drawn on that
   thing's bottom edge. On about the three `.steps` cards had the rule laid
   flush along them — measured 0px.
   The padding is not the gap here: the border sits at the OUTER edge of it,
   so the text is 36px clear while the line touches. A sweep that treats
   padding as separation cannot see this, which is why it was not in the
   previous pass.
   Scoped to a claim that follows a non-claim, so a run of claims keeps the
   flush rule it is designed around. Block flow, so where the previous block
   already declares a bottom margin — `.panel` does — the two collapse to one
   gap rather than stacking. */
:not(.claim) + .claim {
  margin-top: var(--s-8);
}

/* The rail is inset by exactly the claim's own padding, so it spans the
   content box and nothing else. It is one value, not two: the inset used to
   be a hardcoded copy of this clamp, so `.claim--on-panel` setting
   `padding: 0` left the rail still insetting 36px top and bottom — on a
   117px claim that is a 45px stub floating between two paragraphs. */
.claim {
  --claim-rail-inset: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--hairline);
  padding-block: var(--claim-rail-inset);
}

.claim:last-child {
  border-bottom: 1px solid var(--hairline);
}

.claim__say {
  font-family: var(--font-head);
  font-size: clamp(1.375rem, 1.15rem + 1vw, 1.875rem);
  line-height: 1.28;
  letter-spacing: -0.015em;
  max-width: 30ch;
}

.claim__body {
  margin-top: var(--s-3);
  max-width: var(--measure);
  color: var(--text-soft);
}

.claim__more {
  margin-top: 1.1rem;
}

/* The disclosure control. A summary is a control, so it carries the 3:1 the
   WCAG 1.4.11 asks of one — `--border` measures 3.110 against the light page
   and 3.808 against the dark one, both from §18's table, and neither is a
   value this section invents. */
.claim__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  padding-inline: 0.95rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font-size: 0.9375rem;
  color: var(--text);
  cursor: pointer;
  list-style: none;
  transition: background-color var(--dur-tint) var(--ease-tint),
              color var(--dur-tint) var(--ease-tint),
              transform var(--dur-press) var(--ease);
}

.claim__toggle::-webkit-details-marker {
  display: none;
}

/* The caret is the one thing here that rotates, so it is on the move channel
   and stops rotating under `reduce` — where the open state is still carried
   by the label swap below, which is a text change and needs no motion. */
.claim__toggle::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.1em) rotate(45deg);
  transition: transform var(--dur-move) var(--ease-move);
}

.claim__more[open] .claim__toggle::after {
  transform: translateY(0.1em) rotate(-135deg);
}

.claim__toggle:hover {
  background: var(--wash);
  color: var(--text);
}

.claim__toggle:active {
  transform: scale(calc(1 - 0.02 * var(--motion-move)));
}

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

/* The two labels are named for the state they BELONG to: --shut is what the
   toggle says while the disclosure is shut, --open is what it says while it
   is open. This rule hides the other one, and until 2026-08-16 it hid the
   wrong one — so every disclosure on the site read "Close" while closed and
   "How do we know that" while open, which is the exact reverse of the
   affordance. Seven of them shipped that way across how.html and vision.html.

   Measured rather than spotted: with the disclosure shut, the --shut label
   computed `display: none` and the --open label computed `display: block`. */
.claim__more[open] .claim__toggle .claim__label--shut,
.claim__more:not([open]) .claim__toggle .claim__label--open {
  display: none;
}

.claim__detail {
  margin-top: var(--s-5);
  padding-left: var(--rail-gap);
  border-left: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.55);
  max-width: var(--measure);
}

.claim__detail > * + * {
  margin-top: var(--s-4);
}

.claim__detail dl {
  display: grid;
  gap: 0.55rem 1.25rem;
  margin: 0;
}

.claim__detail dt {
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--hue-ink, var(--accent));
}

.claim__detail dd {
  margin: 0;
  color: var(--text-soft);
}

@media (min-width: 720px) {
  .claim__detail dl {
    grid-template-columns: 12rem 1fr;
  }

  .claim__detail dt {
    grid-column: 1;
  }

  .claim__detail dd {
    grid-column: 2;
  }
}

@media print {
  .claim__more > .claim__detail {
    display: block;
  }
}

/* --- The onward index ------------------------------------------------------
   NOT three identical cards. "Three equal feature cards in a row" is the
   single most recognisable generated-page signature there is, and a card is
   the lazy container: it says "these things are peers" and nothing else.

   These are not peers. They are a reading order. So the component is a list
   of editorial rows — a large title, one sentence of what is behind it, a
   hairline between — which is the same language the mechanism table and the
   reference list already speak on this site, and which scales to two items
   or to seven without the grid developing a hole.

   The hue survives as a short rule against the title rather than as a filled
   card, so the family colour still identifies the destination and the page
   does not gain a fourth card shape. */

.onward {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.route {
  display: grid;
  gap: 0.35rem 2.5rem;
  align-items: baseline;
  padding-block: clamp(1.25rem, 2.6vw, 1.9rem);
  /* Breathing room on the trailing edge — 2026-10-11.
     The row had `padding-block` and no `padding-inline`, so the last grid
     column (`.route__go`, the "Read the mechanism" link) sat flush against the
     row's own edge. Invisible at rest, obvious on hover: `background-color`
     tints the whole row, and the tint ended exactly where the letters did.
     Only the trailing edge is padded. The leading edge is already handled —
     `.route::before` is the hue rule at `left: 0`, and the title and body
     clear it with `padding-left: var(--rail-gap)`. The hairline is drawn on
     the border box, so it still spans the full measure while the content sits
     inside it, which is the arrangement the rest of this page uses. */
  padding-inline-end: clamp(1rem, 2.2vw, 1.75rem);
  border-top: 1px solid var(--hairline);
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: background-color var(--dur-tint) var(--ease-tint);
}

.onward .route:last-child {
  border-bottom: 1px solid var(--hairline);
}

/* The hue rule. One pixel, drawn against the title, sitting in the row's own
   leading edge — the boundary a card would have carried, spent on the one
   place it identifies something. It grows on hover, which is a tint-channel
   change of scale on a decorative rule: it carries no meaning, so `reduce`
   collapsing it costs nothing. */
.route::before {
  content: "";
  position: absolute;
  left: 0;
  top: clamp(1.4rem, 2.8vw, 2.05rem);
  width: 2px;
  /* Full-height rather than a stub. It now runs the height of the row's
     text block, so it reads as a rule identifying the row rather than as a
     fragment of one that failed to paint. The hover growth moves to opacity
     and colour, both tint-channel, because a rule that is already the right
     length has nowhere to grow to. */
  top: clamp(1.4rem, 2.8vw, 2.05rem);
  bottom: clamp(1.4rem, 2.8vw, 2.05rem);
  height: auto;
  border-radius: 1px;
  background: rgba(var(--hue, var(--rgb-sage)), 0.55);
  transition: background-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
}

.route:hover::before,
.route:focus-visible::before {
  background: rgba(var(--hue, var(--rgb-sage)), 1);
}

.route__h {
  font-family: var(--font-head);
  font-size: clamp(1.375rem, 1.15rem + 1vw, 1.875rem);
  line-height: 1.2;
  letter-spacing: -0.015em;
  padding-left: var(--rail-gap);
}

.route p {
  color: var(--text-soft);
  max-width: 46ch;
  padding-left: var(--rail-gap);
}

.route__go {
  padding-left: 1.1rem;
  font-weight: 500;
  color: var(--hue-ink, var(--accent));
  border-bottom: 1px solid transparent;
  justify-self: start;
  transition: border-color var(--dur-tint) var(--ease-tint);
}

@media (min-width: 860px) {
  .route {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr) auto;
  }

  .route p {
    padding-left: 0;
  }

  .route__go {
    padding-left: 0;
  }
}

/* Hover is guarded, because a touch device fires it on tap and leaves the
   row stuck in its hovered state until something else is touched. */
@media (hover: hover) and (pointer: fine) {
  .route:hover {
    background: var(--wash);
  }

  .route:hover::before {
    transform: scaleY(1);
  }

  .route:hover .route__go {
    border-bottom-color: currentColor;
  }
}

/* Press. The whole row is the target, so the whole row acknowledges the
   press — on pointer-down, not on release, because waiting for the click
   reads as a dead control. */
.route:active {
  background: var(--wash-strong);
}

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

/* --- The pipeline --------------------------------------------------------
   How it works, and the page's whole argument. Five stages, drawn as a
   column of cards with a rule running down through them, so the SHAPE of the
   thing is legible before a word of it is read.

   The motion is scroll-driven and it teaches: each stage settles as it
   arrives, in the order the pipeline runs, so scrolling the page IS running
   the pipeline. Under `reduce` — and in any browser without a view timeline
   — all five render complete and static with the connector drawn, and the
   page teaches identically. Nothing here carries meaning through movement.

   The connector is a border on the list rather than a pseudo-element per
   stage, so it cannot end up half-drawn between two cards. */

.pipe {
  display: grid;
  gap: clamp(1rem, 2.2vw, 1.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.stage {
  display: grid;
  gap: 0.35rem 1.5rem;
  padding: clamp(1.25rem, 2.6vw, 2rem);
  position: relative;
  isolation: isolate;
}

.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: rgba(var(--hue, var(--rgb-sage)), var(--fill));
}

/* The stage number. A counter rather than typed digits, so inserting a stage
   cannot leave the page numbered 1 2 3 3 4 — which is the specific way a
   hand-numbered list goes wrong, and it goes wrong silently. */
.pipe {
  counter-reset: stage;
}

.stage__n {
  counter-increment: stage;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-pill);
  border: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.5);
  font-family: var(--font-head);
  font-size: 1.0625rem;
  color: var(--hue-ink, var(--accent));
}

.stage__n::before {
  content: counter(stage);
}

.stage__h {
  font-family: var(--font-head);
  font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
  line-height: 1.22;
  letter-spacing: -0.014em;
}

.stage__say {
  color: var(--text-soft);
  max-width: 54ch;
}

.stage__hold {
  margin-top: var(--s-3);
  padding-top: 0.75rem;
  border-top: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.35);
  font-size: 0.9375rem;
  line-height: 1.55;
  max-width: 54ch;
}

.stage__hold b {
  color: var(--hue-ink, var(--accent));
  font-weight: 600;
}

@media (min-width: 860px) {
  .stage {
    grid-template-columns: 2.25rem 1fr;
    align-items: start;
  }

  .stage__n {
    grid-row: 1 / span 4;
  }
}

/* The withheld clause becomes a column of its own rather than a paragraph
   under the last one, and that is a content decision before it is a layout
   one. Every stage on this page makes two statements — what happens, and
   what this site will not tell you about it — and they are a pair. Setting
   them side by side says so; stacking them makes the second look like a
   footnote to the first.

   It also fixes the specific thing that showed up on the render: `.stage__say`
   is capped at 54ch, so above about 1100px the right half of each card was
   empty. Filling it with the clause that was already there costs no new
   copy and no new component.

   1100 rather than 1280 because the constraint is the two text columns, not
   the desktop tier: 54ch plus 40ch plus the gutters is what needs the room,
   and that arrives before §17's breakpoint does. */
@media (min-width: 1100px) {
  .stage {
    grid-template-columns: 2.25rem minmax(0, 1fr) minmax(0, 0.78fr);
    column-gap: clamp(1.5rem, 3vw, 2.75rem);
  }

  .stage__n {
    grid-row: 1 / span 2;
  }

  .stage__h {
    grid-column: 2 / -1;
  }

  .stage__say {
    grid-column: 2;
    max-width: none;
  }

  .stage__hold {
    grid-column: 3;
    grid-row: 2;
    margin-top: 0;
    padding-top: 0;
    padding-left: var(--rail-gap-wide);
    border-top: 0;
    border-left: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.35);
    max-width: none;
  }
}

/* --- The composition guard, and the site's one dimensional moment ---------
   The hardest idea on the site in one gesture: a model reaches for a
   sentence and receives an identifier.

   ## Why this is the place the third dimension is spent

   The brief asks for a genuinely three-dimensional treatment somewhere
   prominent. Most of the ways a marketing page reaches for that are ruled
   out here before taste gets a vote — §18's arithmetic makes glass on a light
   page not weak but absent (a pure white top edge measures 1.119:1 on the
   page, 1.044:1 on a card, both under the 1.25 boundary floor), and a
   parallax hero is in the exact category [[Design Language]] §Motion names as
   risky for this audience.

   What is NOT ruled out is geometry. `perspective` and `rotateY` name no
   colour, change no contrast ratio, cost no blur, and compose on the GPU. So
   the depth is spent once, on the one gesture where depth is the argument:
   **the composed sentence is pushed away from the reader, and the identifier
   is what stays.** The left panel rotates back and recedes as it scrolls
   through; the right panel comes to rest square to the page. That is the
   claim, drawn.

   Motion that explains is the only motion this page is allowed, and it earns
   its exemption from the sub-300ms UI rule the same way a diagram does:
   nobody is waiting on it.

   ## What it does under `reduce`, and it is not "less"

   Both panels sit flat, square and complete, side by side, and the labels
   above them carry the whole idea in words. The reader loses an illustration
   of the argument and keeps the argument. Everything below is transform-only
   and multiplied by §19's `--motion-move`, so the whole effect retires
   through a token rather than through a second media query. */

.swap {
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
  align-items: stretch;
}

@media (min-width: 900px) {
  .swap {
    grid-template-columns: 1fr auto 1fr;
    /* The shared vanishing point. On the container, not on the panels, so
       both read as sitting in ONE space rather than in two private ones —
       which is the difference between a scene and two rotated rectangles. */
    perspective: 1600px;
    perspective-origin: 50% 45%;
  }
}

.swap__pane {
  padding: clamp(1.25rem, 2.6vw, 1.9rem);
  position: relative;
  isolation: isolate;
  transform-style: preserve-3d;
}

.swap__pane::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: rgba(var(--hue, var(--rgb-sage)), var(--fill));
}

.swap__label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 600;
  /* Small text takes POSITIVE tracking; the display sizes take negative.
     One letter-spacing across a scale is wrong somewhere. */
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--hue-ink, var(--accent));
  margin-bottom: var(--s-3);
}

.swap__wire {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.swap__say {
  margin-top: var(--s-4);
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-soft);
}

.swap__foot {
  margin-top: var(--s-5);
  max-width: 62ch;
}

/* The connector. Authored geometry rather than a "→" character: a unicode
   glyph standing in for drawn artwork inherits whatever arrow the reader's
   font happens to have, and this one has to align with two panel edges. */
.swap__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-soft);
}

.swap__arrow svg {
  display: block;
  width: 2.5rem;
  height: 2.5rem;
}

@media (max-width: 899px) {
  .swap__arrow svg {
    transform: rotate(90deg);
  }
}

/* --- The register --------------------------------------------------------
   The Evidence page. Every row is one study, and every row is complete in
   the markup — the filters below only HIDE rows, so a reader with no
   JavaScript gets the whole register and loses only the ability to narrow
   it. That order matters: the alternative renders rows from a script, and a
   page whose evidence disappears when a script fails is not a page about
   evidence.

   The verdict chip carries the repo's own four-value vocabulary. Colour is
   never the only signal — each chip prints its word — so the distinction
   survives greyscale, colour blindness and a printed copy, which is the same
   rule the privacy page's callouts follow. */

/* Tightened 2026-08-16 with the evidence rebuild. The real lever on "the
   cards are too big" is padding and gap, not content — the rows had already
   lost a fact line and were still tall. Geometry only: no type size and no
   colour changes here, so nothing this section does can move a contrast
   reading. */
.reg {
  display: grid;
  gap: clamp(0.55rem, 1.1vw, 0.8rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.reg__row {
  padding: clamp(0.9rem, 1.7vw, 1.2rem);
}

.reg__row[hidden] {
  display: none;
}

.reg__top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.9rem;
}

.reg__h {
  font-family: var(--font-head);
  font-size: clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);
  line-height: 1.24;
  letter-spacing: -0.012em;
  flex: 1 1 20rem;
}

.reg__cite {
  font-size: 0.9375rem;
  color: var(--text-soft);
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.6rem;
  border-radius: var(--r-pill);
  border: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.55);
  background: rgba(var(--hue, var(--rgb-sage)), var(--fill-hi));
  color: var(--hue-ink, var(--accent));
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.reg__facts {
  display: grid;
  gap: 0.3rem 1.1rem;
  margin: 0.7rem 0 0;
  font-size: 0.9375rem;
}

.reg__facts dt {
  font-weight: 600;
  color: var(--text);
}

.reg__facts dd {
  margin: 0;
  color: var(--text-soft);
}

@media (min-width: 640px) {
  .reg__facts {
    /* 8rem, down from 9.5. The label column was sized for a four-row grid
       whose longest term was "Design"; the rebuild merged Design and N into
       one "Study" row, so 1.5rem of the column was holding nothing open. */
    grid-template-columns: 8rem 1fr;
  }
}

.reg__ceiling {
  margin-top: var(--s-3);
  padding-left: var(--rail-gap);
  border-left: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.6);
  font-size: 0.9375rem;
  line-height: 1.6;
}

.reg__ids {
  margin-top: var(--s-3);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

.reg__ids a {
  color: var(--text-soft);
}

.reg__ids a:hover {
  color: var(--text);
}

/* --- The filters ---------------------------------------------------------
   Hidden until JavaScript announces itself, by the same rule the theme
   control follows: a visible control that cannot do anything is a worse
   failure than no control. With scripting off the register is simply whole.

   The feedback is instant reflow with no transition, per §8 of the build
   direction — a filter that animates its result makes the reader wait to
   find out whether it did anything. */

.filters {
  display: none;
}

/* Three axes and twenty-six options, from 2026-08-16. One flat wrap of
   twenty-six pills is a wall, so the bar is three labelled groups stacked as
   rows — legend in its own track from 720 up, pills wrapping in the other.
   That is the same two-track editorial row `.mech` and `.tense` already use,
   which is why the page does not gain a layout family for it.

   The reset sits in a fourth group with an empty legend cell rather than
   inline with the population pills, so its column aligns with the others and
   it never reads as an eighth population. */
:root[data-js] .filters {
  display: grid;
  gap: 0.9rem 0;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

.filters__group {
  display: grid;
  gap: var(--s-2);
  align-items: start;
}

.filters__set {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.filters__legend {
  font-size: 0.875rem;
  color: var(--text-soft);
  padding-top: 0.7rem;
}

/* The reset's legend cell is empty, so nothing sets the row's height and it
   sits tighter to the population group than the three labelled rows sit to
   each other. One step of separation, and it is a modifier rather than a
   `:last-child` rule so that adding a fourth axis after it cannot silently
   move the spacing onto the new group. */
.filters__group--reset {
  margin-top: 0.2rem;
}

@media (min-width: 720px) {
  .filters__group {
    grid-template-columns: 7.5rem minmax(0, 1fr);
    column-gap: var(--s-4);
  }

  .filters__legend {
    text-align: right;
  }
}

/* THE NAME LEADS AND THE COUNT FOLLOWS, from 2026-08-16.
   ------------------------------------------------------------------------
   The label and its count used to be set at 0.875 and 0.8125rem, both in
   `--text-soft`. Two figures of near-equal weight, fourteen times in a row,
   and the bar read as a tally of how thin the evidence is rather than as an
   index of what the app claims to do. The counts are all correct and none
   of them is hidden; what changed is which one the eye reaches first.

   The mechanism is the subject and gets `--text` at 500. The count is an
   affordance - it says what pressing this would leave - and is set smaller,
   softer and raised off the baseline so it reads as an annotation on the
   word rather than as a second word. */
.filter {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding-inline: 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--dur-tint) var(--ease-tint),
              color var(--dur-tint) var(--ease-tint),
              transform var(--dur-press) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
  .filter:hover {
    color: var(--text);
    background: var(--wash);
  }
}

.filter:active {
  transform: scale(calc(1 - 0.02 * var(--motion-move)));
}

.filter[aria-pressed="true"] {
  background: var(--wash-strong);
  border-color: var(--accent);
  font-weight: 600;
}

/* An option that would leave nothing. `disabled` rather than hidden: a facet
   bar that removes its own options as you press them stops being a map of
   what is in the register, and the reader loses the ability to see that a
   combination is empty — which is itself a finding on a page whose subject is
   what the literature does and does not cover.

   Held at 0.55 rather than at a paler ink, because opacity carries the whole
   control down together — border, label and count — where a colour change
   would have to be measured against three grounds. A disabled button is out
   of the tab order and cannot be pressed, so it is not a control any more and
   the 3:1 that WCAG 1.4.11 asks of one does not apply to it. */
.filter[disabled] {
  opacity: 0.55;
  cursor: default;
}

/* The yield count. Tabular figures because the whole point of the column is
   that the numbers are compared down it, and a proportional 1 next to a
   proportional 8 makes two buttons of the same word different widths.

   It is inside the button, so the accessible name is "Rumination 3" — the
   count is part of what the control offers, not decoration beside it. A
   screen reader is not affected by which of the two is set larger, which is
   the point of demoting it visually rather than removing it. */
.filter__n {
  margin-left: 0.45rem;
  align-self: flex-start;
  margin-top: 0.62rem;
  font-variant-numeric: tabular-nums;
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--text-soft);
}

.filter[aria-pressed="true"] .filter__n {
  color: var(--text);
}

/* The reset carries no count — "how many rows would showing everything
   leave" is the total, which is already in the status line under the bar and
   would be the only number on the page printed twice. */
.filter--reset {
  font-weight: 500;
}

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

.reg__count {
  margin-bottom: var(--s-4);
  font-size: 0.9375rem;
  color: var(--text-soft);
}

/* --- The measurement ledger ----------------------------------------------
   Its own layout family, and that is the reason it exists rather than a
   fourth run of the disclosure component. A page that says everything in one
   shape teaches the reader to stop looking.

   The shape is the content: an assumption on the left in the quiet voice, the
   measurement on the right in the loud one, and the corpus it was measured
   against underneath. No strikethrough, no red, no cross — the correction is
   carried by which column is emphasised, so it reads as precision rather than
   as confession. That is the register the whole evidence argument depends on. */

.ledger {
  display: grid;
  gap: 0;
  margin: 0;
}

.ledger__row {
  display: grid;
  gap: 0.5rem 2.5rem;
  padding-block: clamp(1.25rem, 2.6vw, 1.9rem);
  border-top: 1px solid var(--hairline);
}

.ledger__row:last-child {
  border-bottom: 1px solid var(--hairline);
}

.ledger__k {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.ledger__said {
  margin: 0;
  color: var(--text-soft);
  max-width: 42ch;
}

.ledger__is {
  margin: 0;
  font-family: var(--font-head);
  font-size: clamp(1.125rem, 1.05rem + 0.5vw, 1.375rem);
  line-height: 1.3;
  letter-spacing: -0.012em;
  max-width: 42ch;
}

.ledger__on {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--text-soft);
}

@media (min-width: 900px) {
  .ledger__row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    align-items: start;
  }

  .ledger__k--said,
  .ledger__said {
    grid-column: 1;
  }

  .ledger__k--is,
  .ledger__is,
  .ledger__on {
    grid-column: 2;
  }
}

/* --- Invariants ----------------------------------------------------------
   The Safety page. A constraint on the left, the test that holds it on the
   right — the pairing IS the argument, so they share a row and never a
   list. */

.inv {
  display: grid;
  gap: 0;
  margin: 0;
}

.inv__row {
  display: grid;
  gap: 0.35rem 2rem;
  padding-block: clamp(1.1rem, 2.2vw, 1.5rem);
  border-top: 1px solid var(--hairline);
}

.inv__row:last-child {
  border-bottom: 1px solid var(--hairline);
}

.inv__what {
  font-weight: 600;
  margin: 0;
  max-width: 46ch;
}

.inv__how {
  margin: 0;
  color: var(--text-soft);
  max-width: 46ch;
}

.inv__test {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}

@media (min-width: 900px) {
  .inv__row {
    grid-template-columns: 1fr 22rem;
  }

  .inv__what,
  .inv__how {
    grid-column: 1;
  }

  .inv__test {
    grid-column: 2;
    grid-row: 1 / span 2;
    padding-left: var(--rail-gap-wide);
    border-left: 1px solid var(--hairline);
  }
}

/* --- The directory -------------------------------------------------------
   Thirteen services, and the point of the component is the last column: what
   `sourceVerified` means and what it does not. Never a table on a phone —
   a definition list of cards reflows and a table does not. */

.dirs {
  display: grid;
  gap: clamp(0.6rem, 1.4vw, 0.9rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.dir {
  padding: clamp(0.9rem, 1.8vw, 1.25rem);
  display: grid;
  gap: 0.3rem 1.25rem;
}

.dir__name {
  font-weight: 600;
}

.dir__num {
  font-family: var(--font-mono);
  font-size: 1.0625rem;
  color: var(--hue-ink, var(--accent));
}

.dir__meta {
  font-size: 0.875rem;
  color: var(--text-soft);
}

@media (min-width: 720px) {
  .dir {
    grid-template-columns: 1fr 10rem 14rem;
    align-items: baseline;
  }
}

/* The same three-part shape carrying a permission instead of a service: a
   name, a machine-readable identifier, and what is known about it. The
   identifier is the reason for the modifier — `SCHEDULE_EXACT_ALARM` is
   twenty characters of monospace and the 10rem track above was measured for
   `0800 567 567`. Widening the track is the whole change: no new colour, no
   new component, and the phone-number layout is untouched. */
.dir--perm .dir__num {
  font-size: 0.9375rem;
  overflow-wrap: anywhere;
}

@media (min-width: 720px) {
  .dir--perm {
    grid-template-columns: 1fr 15rem 13rem;
  }
}

/* --- Vision --------------------------------------------------------------
   Two components, and the difference between them is the whole rule of that
   page. `.now` is what is true today. `.next` is what is intended, and every
   one of them is marked as intended in the markup itself — a label a reader
   cannot miss and a caption cannot lose. A page where the reader has to
   infer which half is built is a page that will eventually be read wrong. */

.horizon {
  display: grid;
  gap: clamp(1rem, 2.2vw, 1.5rem);
}

@media (min-width: 900px) {
  .horizon {
    grid-template-columns: repeat(2, 1fr);
  }
}

.tenet {
  padding: clamp(1.25rem, 2.6vw, 1.9rem);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  position: relative;
  isolation: isolate;
}

.tenet::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: rgba(var(--hue, var(--rgb-sage)), var(--fill));
}

/* The tense marker. Not decoration: it is the one hard rule the Vision page
   has, made structural so it cannot be edited away by someone rewriting a
   sentence. */
.tenet__when {
  align-self: flex-start;
  padding: 0.2rem 0.65rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
}

.tenet__h {
  font-family: var(--font-head);
  font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
  line-height: 1.22;
  letter-spacing: -0.014em;
  color: var(--hue-ink, var(--accent));
}

.tenet p {
  color: var(--text-soft);
}

.tension {
  padding: clamp(1.5rem, 3vw, 2.25rem);
  display: grid;
  gap: var(--s-4);
}

.tension__h {
  font-family: var(--font-head);
  font-size: clamp(1.375rem, 1.15rem + 0.9vw, 1.75rem);
  line-height: 1.22;
  letter-spacing: -0.015em;
  max-width: 26ch;
}

.tension__pull {
  display: grid;
  gap: 0.75rem 1.5rem;
  padding-block: var(--s-2);
}

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

.tension__side {
  padding-left: var(--rail-gap);
  border-left: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.6);
  font-size: 0.9375rem;
  line-height: 1.6;
}

.tension__answer {
  max-width: var(--measure);
}

.tension__answer b {
  color: var(--hue-ink, var(--accent));
}

/* --- The design page's own exhibits --------------------------------------
   A page about a design system has to show the system rather than describe
   it, so these two are the argument: a strip of the accent family in the
   theme the reader is actually in, and a table of measured ratios.

   The swatches are decorative fills paired with a text label, which WCAG
   exempts and `test/contrast_test.dart` exempts for the same reason — the
   label carries the meaning and the fill illustrates it. The INK sample
   inside each one is not exempt and is the thing being demonstrated. */

.swatches {
  display: grid;
  gap: clamp(0.6rem, 1.4vw, 0.9rem);
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.swatch {
  padding: clamp(0.9rem, 1.8vw, 1.25rem);
  border-radius: var(--r-card);
  border: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.5);
  background: rgba(var(--hue, var(--rgb-sage)), var(--fill-hi));
}

.swatch__ink {
  display: block;
  font-family: var(--font-head);
  font-size: 1.125rem;
  color: var(--hue-ink, var(--accent));
}

.swatch__name {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.8125rem;
  color: var(--text-soft);
}

.ratios {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

.ratios th,
.ratios td {
  text-align: left;
  vertical-align: top;
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--hairline);
}

.ratios th {
  font-weight: 600;
}

.ratios td:not(:first-child) {
  font-family: var(--font-mono);
  font-size: 0.875rem;
  /* A column of ratios a reader is meant to compare down has to be aligned
     down. Proportional digits put 1.072 and 12.809 on different rhythms and
     the comparison stops being free. */
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.ratios caption {
  text-align: left;
  padding-bottom: 0.75rem;
  color: var(--text-soft);
  font-size: 0.9375rem;
}

/* --- The crisis route in the footer --------------------------------------
   Above the fold of the footer on every page, and it is markup rather than a
   component with state: no disclosure, no script, no animation, nothing that
   can fail to open. The only thing it borrows from the rest of the site is
   the typography. */

.help {
  /* The rule was landing about 10px under "Version 1.11.0" with no air
     between them, so the version read as a caption on the line rather than
     as the end of the block above it. A border is a boundary between two
     things and needs to belong to neither. */
  margin-top: clamp(1.75rem, 3.5vw, 2.75rem);
  padding-block: clamp(1.75rem, 3.5vw, 2.5rem) clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--hairline);
}

.help__h {
  font-family: var(--font-head);
  font-size: 1.125rem;
  letter-spacing: -0.01em;
  margin-bottom: var(--s-3);
}

.help__grid {
  display: grid;
  gap: 0.75rem 2.5rem;
  max-width: var(--measure);
}

/* The immediate route reads at body weight rather than small print. It is
   the sentence a person in trouble needs to find without reading the
   paragraph under it, so it does not get the quiet treatment the sourcing
   note gets. */
.help__now {
  font-weight: 500;
}

.help__dir {
  color: var(--text-soft);
}

.help__dir .go {
  color: var(--text);
}

.help__note {
  max-width: 66ch;
}

@media (min-width: 900px) {
  .help__grid {
    grid-template-columns: 1fr 1fr;
    max-width: none;
  }

  .help__note {
    grid-column: 1 / -1;
  }
}

/* --- Arrival, for the components above -----------------------------------
   The same three gestures section 16b established, extended to the new
   components rather than a fourth invented for them. Inside the same
   `@supports` and the same reduced-motion gate, so a browser without view
   timelines shows every one of these complete — which is also what `reduce`
   shows, and what a printed page shows. */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* The register and the directory are LONG LISTS and are deliberately
       absent from this block. Twenty-five rows each rising as they cross the
       fold is the "one identical entrance on every element" failure — it
       reads as a template, it delays the one thing a reader came to this
       page to read, and on the Evidence page it animates the evidence.
       They render immediately, at full opacity, always. */
    .tenet {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 2% cover 24%;
    }

    /* The stages arrive in pipeline order for free: a view() range is
       measured per element, so a card lower in the column is further behind
       without a delay ladder, and the order cannot desynchronise from the
       markup the way an nth-child ladder does when a stage is inserted. */
    .stage {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 26%;
    }

    /* A panel of argument arrives as one object, and its range end is a
       LENGTH for §16's reason: both `entry` and `cover` are measured against
       the element's own height, so a percentage on a tall panel becomes
       hundreds of pixels spent at partial opacity. */
    .claim,
    .tension,
    .ledger__row,
    .inv__row {
      animation: lift linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 240px;
    }

    /* The composition guard. Two different gestures, because the two panels
       are making opposite statements — the whole point is that they do NOT
       arrive the same way. Transform only; both retire through
       --motion-move. */
    .swap__pane--away {
      animation: recede linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 40%;
    }

    .swap__pane--stays {
      animation: settle-square linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 34%;
    }

    /* The connector draws itself along its own length rather than fading in.
       clip-path is a compositor property, names no colour, and is the one
       reveal that survives every constraint this palette imposes. */
    .swap__arrow svg {
      animation: draw-line linear both;
      animation-timeline: view();
      animation-range: entry 12% cover 30%;
    }
  }
}

/* The theme crossfade §19 bounds to the surfaces that repaint. The new cards
   repaint too, and are added here rather than to a `*` rule for the reason
   §19 gives. `.route` and `.stage` carry their own transitions above and are
   not repeated. */
.reg__row,
.tenet,
.dir,
.tension,
.swap__pane,
.swatch {
  transition:
    background-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* The footnote under the composition guard. A class rather than an inline
   style: the site carries exactly one inline style declaration (the mood
   scale's --c, which is data), and a second one would be the start of a
   parallel stylesheet nobody audits. */
.swap__foot {
  margin-top: var(--s-5);
  max-width: 62ch;
}

/* --------------------------------------------------------------------------
   21. The specimen — a design system you can operate
   --------------------------------------------------------------------------
   The Design page. Everything below exists to serve one component, and the
   component exists to make one argument that prose cannot make: **a design
   language here changes form and motion, and never colour.**

   ## Why this is a specimen and not a screenshot

   The app declares nine design languages. A page that lists them is a
   brochure. A page where the reader switches between them and watches the
   same card answer differently is the system itself, running — and it costs
   nothing this site did not already have, because every value below is read
   out of `lib/design/design_language.dart` and reproduced, never invented.

   ## The palette is not a variable here, and that is the point

   Section 1's lock says no palette, accent or gradient value may change.
   This component obeys it structurally rather than by discipline: **not one
   custom property below carries a colour.** They carry a press depth, an
   edge width, a shadow strength, a space scale, an entrance scale and a
   tempo. Switching language cannot alter a colour because no colour is
   reachable from the switch. That is also the truth about the app — colour
   lives on the orthogonal axis, the palette, which this exhibit does not
   touch.

   ## No JavaScript

   Nine radio inputs and the general sibling combinator. The exhibit works
   with scripting off, which matters more here than anywhere else on the
   site: a page arguing that the product is careful should not need a script
   to demonstrate its own design system. The inputs are real radios in a real
   fieldset, so the whole thing is keyboard-operable and announced as a radio
   group without a line of ARIA.

   ## Reduced motion

   Every moving value is multiplied by --motion-move, so `reduce` collapses
   the squash and the landing to nothing rather than making them instant.
   That is section 19's rule, and it is also the app's own: a squash whose
   duration is zero is the scale applied instantly, which
   `design_language.dart` calls the one outcome worse than either having the
   press or not having it. The tint under the press survives, so a
   reduced-motion reader still sees the control answer.
   -------------------------------------------------------------------------- */

.spec {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
}

/* The radios themselves. Visually hidden, never `display: none` — a hidden
   input is not focusable and the exhibit would stop being keyboard-operable.
   Same technique as .vh, inlined because these also need to stay in the flow
   as siblings for the ~ combinator below. */
.spec__in {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.spec__pick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.spec__opt {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r-pill);
  font-size: 0.9375rem;
  cursor: pointer;
  color: var(--text-soft);
  background: var(--surface);
  transition: color var(--dur-tint) var(--ease-tint),
              background-color var(--dur-tint) var(--ease-tint),
              border-color var(--dur-tint) var(--ease-tint);
}

/* The character line beside each name, so the control teaches while it
   selects rather than being nine words in a row. */
.spec__opt i {
  font-style: normal;
  font-size: 0.8125rem;
  color: var(--text-soft);
  opacity: 0.85;
}

/* Gated, as the other six hover rules in this file are. A touch device fires
   :hover on tap, so an ungated hover leaves the last-tapped chip looking
   selected next to the one that actually is. */
@media (hover: hover) and (pointer: fine) {
  .spec__opt:hover {
    color: var(--text);
    border-color: var(--border);
  }
}

/* Focus lands on the input, which is hidden, so the ring is drawn on the
   label it controls. Without this the exhibit is keyboard-operable and
   invisible while you operate it. */
#lang-soft:checked   ~ .spec__pick .spec__opt[for="lang-soft"],
#lang-loft:checked   ~ .spec__pick .spec__opt[for="lang-loft"],
#lang-clay:checked   ~ .spec__pick .spec__opt[for="lang-clay"],
#lang-stack:checked  ~ .spec__pick .spec__opt[for="lang-stack"],
#lang-veil:checked   ~ .spec__pick .spec__opt[for="lang-veil"],
#lang-relief:checked ~ .spec__pick .spec__opt[for="lang-relief"],
#lang-press:checked  ~ .spec__pick .spec__opt[for="lang-press"],
#lang-crisp:checked  ~ .spec__pick .spec__opt[for="lang-crisp"],
#lang-slate:checked  ~ .spec__pick .spec__opt[for="lang-slate"] {
  color: var(--accent-on);
  background: var(--accent);
  border-color: var(--accent);
}

#lang-soft:checked   ~ .spec__pick .spec__opt[for="lang-soft"] i,
#lang-loft:checked   ~ .spec__pick .spec__opt[for="lang-loft"] i,
#lang-clay:checked   ~ .spec__pick .spec__opt[for="lang-clay"] i,
#lang-stack:checked  ~ .spec__pick .spec__opt[for="lang-stack"] i,
#lang-veil:checked   ~ .spec__pick .spec__opt[for="lang-veil"] i,
#lang-relief:checked ~ .spec__pick .spec__opt[for="lang-relief"] i,
#lang-press:checked  ~ .spec__pick .spec__opt[for="lang-press"] i,
#lang-crisp:checked  ~ .spec__pick .spec__opt[for="lang-crisp"] i,
#lang-slate:checked  ~ .spec__pick .spec__opt[for="lang-slate"] i {
  color: var(--accent-on);
  opacity: 0.85;
}

.spec__in:focus-visible + .spec__opt {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.spec__body {
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.5rem);

  /* The default row. Soft ships, so Soft is what an unstyled or
     unswitched reader gets, and every value here is Soft's own. */
  --l-press: 0.02;
  --l-tempo: 1;
  --l-edge: 1px;
  --l-space: 1;
  --l-entrance: 0.97;
  --l-radius: 16px;
  --l-land: 0px;
}

@media (min-width: 960px) {
  .spec__body {
    grid-template-columns: minmax(0, 1fr) minmax(0, 20rem);
    align-items: start;
    gap: clamp(1.5rem, 3vw, 2.5rem);
  }
}

/* --- The nine rows -------------------------------------------------------
   Read out of 1.0's design-language table, which went with Design 1.0 on
   2026-09-06. pressDepth, tempo, edgeWidth,
   shadowStrength, spaceScale and entranceScale are the language's own
   numbers; --l-radius and --l-land are this component's reproduction of two
   things the app expresses in Dart rather than in a scalar.

   Slate is the row worth reading. It declares NO press depth, and that is
   not a gap: it presses by travelling onto its own block shadow — zero blur,
   offset (3, 3) — so the gap between card and shadow closes to nothing. The
   app's own comment records that a sweep for `pressDepth: 0.0` miscounted
   this row as silent, and that the audit which counted four languages with
   no press response should have counted three. It is reproduced here the way
   the app draws it, not the way the sweep read it. */
#lang-soft:checked   ~ .spec__body { --l-press: 0.02;  --l-tempo: 1;     --l-edge: 1px; --l-space: 1;   --l-entrance: 0.97; --l-radius: 16px; --l-land: 0px; }
#lang-loft:checked   ~ .spec__body { --l-press: 0.03;  --l-tempo: 0.875; --l-edge: 1.5px; --l-space: 0.8; --l-entrance: 0.94; --l-radius: 22px; --l-land: 0px; }
#lang-clay:checked   ~ .spec__body { --l-press: 0.03;  --l-tempo: 1;     --l-edge: 1.5px; --l-space: 0.8; --l-entrance: 0.92; --l-radius: 20px; --l-land: 0px; }
#lang-stack:checked  ~ .spec__body { --l-press: 0.035; --l-tempo: 0.875; --l-edge: 1px; --l-space: 1;   --l-entrance: 0.95; --l-radius: 14px; --l-land: 0px; }
#lang-veil:checked   ~ .spec__body { --l-press: 0.03;  --l-tempo: 0.875; --l-edge: 1px; --l-space: 1;   --l-entrance: 0.96; --l-radius: 18px; --l-land: 0px; }
#lang-relief:checked ~ .spec__body { --l-press: 0.025; --l-tempo: 1;     --l-edge: 1px; --l-space: 0.9; --l-entrance: 0.97; --l-radius: 16px; --l-land: 0px; }
#lang-press:checked  ~ .spec__body { --l-press: 0.02;  --l-tempo: 0.85;  --l-edge: 1px; --l-space: 1.2; --l-entrance: 1;    --l-radius: 4px;  --l-land: 0px; }
#lang-crisp:checked  ~ .spec__body { --l-press: 0.03;  --l-tempo: 0.65;  --l-edge: 1px; --l-space: 0.7; --l-entrance: 0.95; --l-radius: 2px;  --l-land: 0px; }
#lang-slate:checked  ~ .spec__body { --l-press: 0;     --l-tempo: 0.86;  --l-edge: 2px; --l-space: 0.6; --l-entrance: 0.96; --l-radius: 0px;  --l-land: 3px; }

/* --- The card being specified -------------------------------------------- */

.spec__stage {
  display: grid;
  place-items: center;
  padding: clamp(1.5rem, 4vw, 3rem) clamp(1rem, 3vw, 2rem);
  border-radius: var(--r-card);
  background: var(--surface-sunk);
}

.spec__card {
  width: min(100%, 22rem);
  text-align: left;
  font: inherit;
  cursor: pointer;
  display: grid;
  gap: calc(0.5rem * var(--l-space));
  padding: calc(1.25rem * var(--l-space));
  border-radius: var(--l-radius);
  border: var(--l-edge) solid var(--edge-lift);
  background: var(--surface);

  /* Slate's press mechanism, and every other language's absent one, written
     once at an offset of 0 rather than as a rule only Slate gets. The colour
     is --spec-block, which is theme-scoped for the reason section 18 gives:
     a zero-blur ink shadow reaches 1.253:1 at FULL black on the dark page, so
     in dark the block is drawn in --hairline (1.523 against the page) and the
     landing stays visible. Light elevates downward, dark upward; this is that
     rule applied to one component rather than an exception to it. */
  box-shadow: var(--l-land) var(--l-land) 0 var(--spec-block);

  transform: scale(1);
  transition: transform calc(var(--dur-press) * var(--motion-move)) var(--ease-move),
              background-color var(--dur-press) var(--ease-tint),
              border-color var(--dur-press) var(--ease-tint);
}

/* The press. Both the squash and Slate's landing are multiplied by
   --motion-move, so `reduce` removes the movement instead of making it
   instant — the app's own finding, in section 19's mechanism. The background
   step is the tint-channel signal that survives, so the control still
   answers a reduced-motion reader. */
.spec__card:active {
  transform:
    translate(calc(var(--l-land) * var(--motion-move)), calc(var(--l-land) * var(--motion-move)))
    scale(calc(1 - var(--l-press) * var(--motion-move)));
  background: var(--surface-sunk);
}

.spec__card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.spec__card h3 {
  font-family: var(--font-head);
  font-size: 1.125rem;
  line-height: 1.25;
  letter-spacing: -0.012em;
}

.spec__card p {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-soft);
}

.spec__hint {
  margin-top: var(--s-4);
  font-size: 0.8125rem;
  color: var(--text-soft);
  text-align: center;
}

/* --- The readout ---------------------------------------------------------
   Tabular figures, because a column of numbers that shifts as the reader
   switches language is a column nobody can compare down. This is the whole
   reason the readout is a table and not a paragraph. */

.spec__read {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.875rem;
}

.spec__read caption {
  text-align: left;
  padding-bottom: 0.6rem;
  color: var(--text-soft);
  font-size: 0.875rem;
}

.spec__read th,
.spec__read td {
  text-align: left;
  padding: 0.4rem 0.5rem 0.4rem 0;
  border-bottom: 1px solid var(--hairline);
  vertical-align: baseline;
}

.spec__read th {
  font-weight: 400;
  color: var(--text-soft);
}

.spec__read td {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding-right: 0;
  color: var(--text);
}

/* The readout is the WHOLE table, not the selected row.
   ------------------------------------------------------------------------
   The first version printed one row and rewrote it as the reader switched.
   That is worse in two ways: a number you cannot compare against its
   neighbours is a number you cannot judge, and printing a CSS variable as
   text needs a counter trick that breaks the moment a value is not an
   integer. So all nine rows are in the markup, always, and the checked one
   is marked. The reader gets the system and its current position at once,
   the table is a specimen rather than a display, and it is all still true
   with the stylesheet off. */
.spec__row th {
  transition: color var(--dur-tint) var(--ease-tint);
}

#lang-soft:checked   ~ .spec__body .spec__row--soft,
#lang-loft:checked   ~ .spec__body .spec__row--loft,
#lang-clay:checked   ~ .spec__body .spec__row--clay,
#lang-stack:checked  ~ .spec__body .spec__row--stack,
#lang-veil:checked   ~ .spec__body .spec__row--veil,
#lang-relief:checked ~ .spec__body .spec__row--relief,
#lang-press:checked  ~ .spec__body .spec__row--press,
#lang-crisp:checked  ~ .spec__body .spec__row--crisp,
#lang-slate:checked  ~ .spec__body .spec__row--slate {
  color: var(--text);
}

#lang-soft:checked   ~ .spec__body .spec__row--soft   th::before,
#lang-loft:checked   ~ .spec__body .spec__row--loft   th::before,
#lang-clay:checked   ~ .spec__body .spec__row--clay   th::before,
#lang-stack:checked  ~ .spec__body .spec__row--stack  th::before,
#lang-veil:checked   ~ .spec__body .spec__row--veil   th::before,
#lang-relief:checked ~ .spec__body .spec__row--relief th::before,
#lang-press:checked  ~ .spec__body .spec__row--press  th::before,
#lang-crisp:checked  ~ .spec__body .spec__row--crisp  th::before,
#lang-slate:checked  ~ .spec__body .spec__row--slate  th::before {
  content: "";
  display: inline-block;
  width: 0.4rem;
  height: 0.4rem;
  margin-right: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
}

/* --- The clamp -----------------------------------------------------------
   "A variant is a bounded multiplier" made visible. The bar is the legal
   range for a press depth, 0 to 0.08 — the clamp in
   `AppDesignSpec.withVariant`. The fill is the current language's declared
   depth; the mark is where the most emphatic factor in the system, 1.5, would
   put it. It never passes the end, because passing it is not expressible.

   ## Cut once, restored, and the reason is worth keeping

   It was removed before its first commit because it measured as though the
   `var()` fallback had been taken — 170.5px where 299px was expected — while
   the variable demonstrably inherited onto the element at the right value. An
   unverified exhibit on a page about craft is the one thing that page cannot
   carry, so it went out rather than shipping on hope.

   Restored after the mechanism was reproduced in isolation twice: a
   `:checked ~` sibling setting a custom property, consumed by a nested
   absolutely-positioned child through the identical `calc()`, with and
   without a transition on the driven property. Both returned the correct
   width. The original reading was the measuring tool, not the stylesheet —
   the fourth such artefact this session. It is verified here by rendering two
   static copies of the page with `checked` on different radios and comparing
   the painted pixels, which uses no scripting at all. */

.clamp {
  display: grid;
  gap: var(--s-2);
  margin-top: var(--s-6);
}

.clamp__lede {
  font-size: 0.875rem;
  color: var(--text-soft);
}

.clamp__bar {
  position: relative;
  height: 0.5rem;
  border-radius: var(--r-pill);
  background: var(--surface-sunk);
  border: 1px solid var(--hairline);
}

.clamp__bar > * {
  display: block;
}

.clamp__fill {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: var(--r-pill);
  background: var(--accent);
  /* 0.08 is the ceiling, so a value's share of the bar is value / 0.08. */
  width: calc(var(--l-press, 0.02) / 0.08 * 100%);
  transition: width calc(var(--dur-reveal) * var(--motion-move)) var(--ease-move);
}

.clamp__ghost {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 2px;
  background: var(--border);
  /* min() IS the clamp, drawn. */
  left: calc(min(var(--l-press, 0.02) * 1.5, 0.08) / 0.08 * 100%);
  transition: left calc(var(--dur-reveal) * var(--motion-move)) var(--ease-move);
}

.clamp__scale {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-soft);
}

/* Successive prose blocks in a section that is not on a component grid. One
   named value rather than an inline margin at each call site.

   RESTORED: this rule was deleted as collateral when the clamp block above
   was cut, and design.html shipped using .flow at three call sites with no
   rule behind it. Caught by grepping the markup against the stylesheet rather
   than by looking at the page, because a missing margin looks like a design
   choice. */
.flow > * + * {
  margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
}

/* --- Sidenotes -----------------------------------------------------------
   Depth on demand without a disclosure widget. At reading widths a note sits
   inline under the paragraph it belongs to, quiet and smaller. From 1180px
   it moves into the margin beside it, where an expert reads it and a lay
   reader's eye skips it entirely — which is the whole brief for this site in
   one component: layered by depth, never segmented by audience, and no
   control to press.

   It is a <small> in the markup rather than a positioned div, so with the
   stylesheet off it still reads as an aside attached to the paragraph. */

.side {
  display: block;
  margin-top: var(--s-3);
  padding-left: var(--rail-gap);
  border-left: 1px solid var(--hairline);
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 46ch;
}

/* ⚠️ THE MAX-WIDTH IS THE FIX FOR A HORIZONTAL SCROLLBAR, and it is not
   decoration. Found 2026-08-16 by measuring every page at 390 / 768 / 1440 /
   2560 through explicitly sized iframes: `design.html` at 1440 reported
   `scrollWidth` 1552 against an `innerWidth` of 1440, and the eight elements
   past the edge were all `.side`.

   The arithmetic, because it explains why raising the breakpoint would not
   have worked. A `.has-side` paragraph is a block child of `.wrap`, so at any
   viewport at or above 1120 it is 1120 wide. The note is then placed at
   `100% + 2.5rem` and runs 17rem further, which puts its right edge 312px
   past the paragraph's. For that to land inside the viewport the container's
   own right margin has to be at least 312 — and `.wrap` centres, so that
   needs a viewport of **1744**. The component was gated at 1180. Every width
   between 1180 and 1744 scrolled sideways, which is most laptops.

   Constraining the paragraph to the reading measure puts the note in space
   the container already had: 62ch is about 527px, so the note runs to roughly
   839 inside a 1120 container and fits from 1180 up with room to spare. It is
   also the width the paragraph should have had — a 1120px line of body copy
   is twice the measure this stylesheet sets for every other reading column.

   Scoped to the same query rather than declared unconditionally, so nothing
   below 1180 changes by a pixel. */
@media (min-width: 1180px) {
  .has-side {
    position: relative;
    max-width: var(--measure);
  }

  .has-side .side {
    position: absolute;
    top: 0;
    left: calc(100% + 2.5rem);
    width: 17rem;
    max-width: none;
    margin-top: 0.2rem;
    padding-left: 0;
    border-left: 0;
  }
}

/* --------------------------------------------------------------------------
   22. The Mission page — the same subject, in two tenses
   --------------------------------------------------------------------------
   Phase 7 item 5. Mission and Vision were briefed as differing "by tense, not
   subject", and a cross-link at the bottom of a page is the weakest possible
   way to say that — nobody follows it, and nobody who does can hold the two
   pages side by side in their head afterwards.

   So the claim is made structurally instead: one row per subject, the present
   tense in the left cell and the intended one in the right, with the
   right-hand cell explicitly labelled as belonging to another page and
   carrying no authority of its own. A reader checks the pair in one glance,
   and so does the next person auditing whether either page has drifted into
   the other's job.

   ## Three rules it holds itself to

   1. **The tense is in the markup, not in the prose.** Every cell opens with
      a `.tense__when` badge reading "Today" or "Intended", exactly as
      vision.html's `.tenet__when` does. An editor rewriting a sentence cannot
      delete the tense by accident, because the tense is not in the sentence.
      That badge is also why there is no header row: a header would carry the
      distinction only visually, and a screen reader walking a `<dl>` reads
      both `<dd>`s under one `<dt>` with nothing to tell them apart.

   2. **The two cells never differ in text contrast.** The obvious way to
      make the intended column read as lighter is to lighten it, and that is
      the one channel this palette cannot spare — every ink on this site is
      measured against `--surface` and `--page` and nothing else. The
      distinction is carried by the leading rule instead: solid hue for
      today, the same hue at a third for intended. Colour is never the only
      signal either, because the badge prints its word.

   3. **No new colour, no new elevation.** The row is a hairline-separated
      editorial row, which is the language `.mech`, `.route` and `.reg`
      already speak on this site, rather than a sixth card shape.
   -------------------------------------------------------------------------- */

.tense {
  margin: 0;
}

.tense__row {
  display: grid;
  gap: var(--s-3);
  padding-block: clamp(1.25rem, 2.8vw, 1.9rem);
  border-top: 1px solid var(--hairline);
}

.tense__row:last-child {
  border-bottom: 1px solid var(--hairline);
}

.tense__subject {
  font-family: var(--font-head);
  font-size: 1.0625rem;
  line-height: 1.3;
  color: var(--hue-ink, var(--accent));
}

/* Both cells are `<dd>` and both reset the browser's indent. The rule is the
   only thing that separates them, and it is a border rather than a
   pseudo-element so it grows with the cell rather than needing a height. */
.tense__now,
.tense__next {
  margin: 0;
  padding-left: var(--rail-gap);
  max-width: 46ch;
  border-left: 2px solid rgba(var(--hue, var(--rgb-sage)), 0.85);
}

.tense__next {
  border-left-color: rgba(var(--hue, var(--rgb-sage)), 0.3);
  color: var(--text-soft);
}

/* The badge. Small text takes positive tracking; the display sizes take
   negative — the same split `.swap__label` makes. It is a block so the
   sentence under it starts on its own line at every width, which is what
   makes the row scannable down the column rather than across it. */
.tense__when {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-bottom: var(--s-2);
}

.tense__now .tense__when {
  color: var(--hue-ink, var(--accent));
}

/* 900, not 720: three tracks only arrive once both prose cells can hold a
   readable line. Below it the row stacks and the badges do the whole job,
   which is the same degradation `.mech` takes. */
@media (min-width: 900px) {
  .tense__row {
    grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 2.25rem;
    align-items: start;
  }
}

/* --------------------------------------------------------------------------
   23. The Contact page — one address, and it is a placeholder
   --------------------------------------------------------------------------
   Phase 7 item 6. The whole page is one component and a `.mech` list, and
   this is the component.

   ## Why it looks like this

   The brief for the address was that it must **fail loudly rather than look
   plausible**, because a plausible address nobody reads is worse than a
   broken one — only the broken one gets fixed. Failing loudly is a
   typographic job as much as a copy one, so the block does three things at
   once and none of them is colour alone:

   1. **The label prints the word.** "Placeholder — this address does not
      receive mail" sits above the address, in the markup, and survives
      greyscale, a colour-blind reader and a printed copy. That is the same
      rule the privacy callouts and the register chips follow.
   2. **The host half is set apart from the local half.** `@example.invalid`
      is the part that makes it impossible, so it is the part that is tinted
      and tracked away from the name. Two spans, no image, selectable as one
      string.
   3. **It is `.surface-lifted`, the top of §18's three levels.** There is one
      thing on this page and it is this; a card at the same level as the
      prose around it would read as a footnote.

   It is deliberately not a link. A `mailto:` invites a click and pays it
   back with a bounce two minutes later, which is a worse failure than a line
   of text that says so where the reader is looking.

   ## The one thing to change when the address is real

   Replace the two spans with an `<a href="mailto:…">` and delete
   `.mail__label`. Nothing else here assumes the placeholder — the type
   scale, the surface and the geometry are all as correct for a working
   address as for this one.
   -------------------------------------------------------------------------- */

.mail {
  padding: clamp(1.5rem, 3.4vw, 2.25rem);
  max-width: 46rem;
}

.mail__label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--hue-ink, var(--accent));
  margin: 0 0 0.85rem;
}

/* The one place on this site where a monospace face carries a display size.
   It is an identifier rather than prose — a reader has to copy it character
   by character, and a proportional face at this size loses the distinction
   between a hyphen and an en dash in the middle of a word.

   `overflow-wrap: anywhere` and not `break-all`: at 390px the address is
   wider than the column, and breaking at the hyphens and the `@` is
   readable where breaking mid-token is not. */
.mail__addr {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(1.125rem, 0.85rem + 1.5vw, 1.75rem);
  line-height: 1.35;
  overflow-wrap: anywhere;
}

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

/* Tracked out by a hair as well as tinted, so the split between the name and
   the impossible host reads without colour. */
.mail__host {
  color: var(--hue-ink, var(--accent));
  letter-spacing: 0.01em;
}

.mail__say {
  margin-top: 1.1rem;
  max-width: 54ch;
  color: var(--text-soft);
}

.mail__say b {
  color: var(--text);
  font-weight: 600;
}

.mail__crisis {
  margin-top: var(--s-5);
  max-width: 54ch;
}

/* --------------------------------------------------------------------------
   24. The highlights — the app's interface, rebuilt in HTML
   --------------------------------------------------------------------------
   Phase 7 item 10, and the owner's favourite idea. There are no screenshots
   of this app and none may be fabricated, so the interface is REBUILT: real
   markup, at the app's own values, carrying the app's own copy.

   ## The one rule that decides the whole section

   🔒 **Reproduce, never reinterpret.** Every colour inside a reproduction is
   the literal value the app resolves for its default look — Soft/Regular on
   the `light` palette, the one `AppDesignChoice.fallback` and
   `recommendedGroundsFor` hand a new user — read out of `lib/design/` and
   `lib/data/style.dart` rather than approximated from this site's palette.
   They are declared ONCE, on `.appshot__screen`, and every rule below reads
   them through a variable. A palette value appears in this file exactly once.

   ## Why the interior does not follow the site's theme, and the frame does

   The obvious move is to make the reproduction dark when the page is dark.
   It is wrong twice over. The app's dark palette is a different set of
   measured values, so following the page would mean either shipping a second
   palette this section has not measured, or — far worse — tinting the light
   one, which is the reinterpretation the rule above forbids. And a
   reproduction that changes with its surroundings is not a reproduction.

   So the FRAME is the page's and the INTERIOR is the app's. That is
   `taste-skill`'s Page Theme Lock resolved the same way the Design page
   resolves it: chrome, label and rule lines are site tokens, and only what is
   being exhibited changes. A reader in dark mode sees a lit object on a dark
   page, which is what a specimen is.

   ## Two deviations, stated rather than hidden

   1. **The icons are redrawn.** The app's are Material's. Reproducing a
      third party's icon set is a licensing question this work is not
      answering, and the argument here is layout and copy rather than
      glyph shapes. They are authored inline SVG in the site's own stroke
      weight, which is also what `craft-floor` requires of every icon here.
   2. **Lora is not loaded.** It is the app's heading face
      (`typography.dart:413`) and the site loads no external font and ships no
      CDN request. Copying the file into `website/` is a deploy-surface and
      licensing decision, so the reproduction uses this site's own serif at
      the app's weight and size. Owed, and recorded.

   ## Sizes are the app's numbers, unscaled

   `AppSpacing` gives 24 of page padding and 20 inside a card;
   `AppRadius` gives 16 for a card and 24 for the featured one; the type
   ramp gives 26 / 19 / 16.5 / 14 / 12.5. They are written here in px rather
   than rem for the one case where px is right: these are a reproduction of
   fixed logical pixels, and a specimen that resizes with the reader's root
   font is not a specimen. The frame around it is in rem and does scale.
   -------------------------------------------------------------------------- */

/* --- The feature row ------------------------------------------------------
   Prose and reproduction side by side from 1024, stacked below it. The
   modifier flips the order rather than the columns, so the reading order in
   the markup is always prose-then-exhibit and a screen reader never meets the
   caption of something it has not been told about yet. */

.feat {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2.5rem);
  align-items: start;
  padding-block: clamp(2rem, 5vw, 3.5rem);
  border-top: 1px solid var(--hairline);
}

.feat:last-of-type {
  border-bottom: 1px solid var(--hairline);
}

.feat__body > * + * {
  margin-top: var(--s-4);
}

.feat__h {
  font-family: var(--font-head);
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
  line-height: 1.2;
  letter-spacing: -0.015em;
  max-width: 20ch;
}

.feat__body p {
  max-width: 46ch;
  color: var(--text-soft);
}

/* The two questions every feature has to answer, per the brief: why it exists
   and how it helps. A run-in label rather than a heading, because six more
   entries in the document outline between the section heading and the next
   one is a table of contents nobody asked for. */
.feat__k {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--hue-ink, var(--accent));
  margin-right: 0.5rem;
}

/* 🔴 A grid item's automatic minimum size is its MIN-CONTENT size, and that
   is what made this page scroll sideways at 320px.

   Below 1024px `.feat` is a single-column grid, so every item shares one
   track and the track is as wide as the widest item refuses to go below. The
   `.appshot` mock's min-content is **323.83px**; the content box at 320px
   viewport is 280px. The track took the mock's number, `.feat__body` was
   dragged to 324px with it, and `scrollWidth` came out at 361.

   `min-width: 0` is the standard release valve, and the mock reflows to
   280px with nothing clipped — checked at 320px in Chrome, not assumed.

   Two of the three widths that show this (320, 360) were not in
   `tool/site_overflow.html`'s list until today; the harness started at 390.
   A guard's coverage is a claim about the world, and that one quietly
   asserted no reader has a 320px phone. */
@media (max-width: 1023px) {
  .feat > * {
    min-width: 0;
  }
}

@media (min-width: 1024px) {
  .feat {
    grid-template-columns: minmax(0, 1fr) minmax(0, 27rem);
    column-gap: clamp(2rem, 4vw, 4rem);
  }

  .feat--flip .feat__body {
    order: 2;
  }

  .feat--flip .appshot {
    order: 1;
  }
}

/* --- The frame ------------------------------------------------------------ */

/* A `<figure>`, which the base reset does not reach — the reset enumerates
   the elements the site uses rather than sweeping with `*`, and this is the
   first figure on the site. */
.appshot {
  margin: 0;
  justify-self: center;
  width: 100%;
  max-width: 25.5rem;
}

.appshot__label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-bottom: var(--s-3);
}

.appshot__plate {
  padding: 10px;
  border-radius: 22px;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
}

/* --- The app ---------------------------------------------------------------
   Everything from here down is inside a reproduction. The `ui-` prefix is not
   decoration: it marks the boundary where this site's design system stops and
   another product's begins, so nothing below can be mistaken for a component
   of this site and nothing above can be reached from inside a specimen. */

.ui {
  /* 🔴 REPAINTED TO 2.0, 2026-09-03. Every value below was the 1.0 app's,
     and the accent was the giveaway: `#83987e`, a muted green, on screens
     the app now draws in lilac. Owner: *"why it is green are we still using
     the colours of the old design"*. They were.

     Each line is now the app's own `AppV2Palette.calm` — the Calm entry in
     `lib/design/v2/design_v2.dart`, which is what these mockups are labelled
     as ("Home · default look"). The mapping is one-to-one and the Dart names
     are on each line, so the next person can diff a token against the file it
     came from instead of guessing which shade of near-white this was meant to
     be.

     This is the whole repaint. Four screens, three pages and every card,
     chip, button and hairline inside them are painted from these twelve
     declarations — which is the point of `.ui` owning a palette rather than
     the mockups carrying colours inline. */
  --app-page: #faf7f2;          /* calm.base */
  --app-surface: #fbf9f4;       /* calm.panelSoft */
  --app-card: #fdfbf7;          /* calm.sheet */
  --app-text: #15130f;          /* calm.ink */
  --app-soft: #5f5a52;          /* calm.ink2 */
  --app-hairline: rgba(21, 19, 15, 0.16);  /* calm.hair */
  --app-control: #5f5a52;       /* calm.ink2 — controls take the soft ink */
  --app-accent: #b9a8de;        /* calm.accent — lilac, not green */
  --app-accent-deep: #8f7cc0;   /* accent, darkened for a border on tint */
  --app-accent-ink: #463a55;    /* calm.famInk — ink that sits on a tint */
  --app-clay: #d3a08c;          /* AppV2Family.clay */
  --app-dusk: #aac0da;          /* AppV2Family.steel */
  --app-panel: #15130f;         /* calm.panelDark */
  --app-on-panel: #f7f4ee;      /* calm.onDark */

  /* The page wash every screen carries. `theme.dart` composites the accent
     over the page at 7% and stops it at 55%. It never moves — every design
     language in the app refuses ambient motion on it.

     The top stop was `#efeee7`, which is that composite taken with the 1.0
     green. Written as the composite itself now, so it follows the accent
     above rather than needing to be recomputed by hand whenever it moves. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--app-accent) 7%, var(--app-page)) 0%,
    var(--app-page) 55%);
  color: var(--app-text);
  border-radius: 12px;
  padding: 24px;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  overflow: hidden;
}

/* The one screen that takes no wash. `AppPage` is skipped on the crisis
   pathway and on the flat screens beneath it, and a reproduction that painted
   a gradient there would be showing something the app refuses to do. */
.ui--flat {
  background: var(--app-page);
}

.ui-bar {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: 16px;
}

.ui-bar__t {
  font-family: var(--font-head);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.2;
}

.ui-title {
  font-family: var(--font-head);
  font-size: 26px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
}

.ui-sec {
  font-family: var(--font-head);
  font-size: 19px;
  font-weight: 600;
  line-height: 1.3;
}

.ui-cardt {
  font-family: var(--font-head);
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.3;
}

.ui-body { font-size: 14px; line-height: 1.5; }

/* The grounding line under a validation line inside the featured card. It is
   `bodyLarge` in `textSoft` in the app; the size step is not reproduced
   because the featured card here is narrower than a phone's and the two
   sentences would set at nearly the same measure. The colour is. */
.ui-body--soft { color: var(--app-soft); }
.ui-cap  { font-size: 12.5px; line-height: 1.4; color: var(--app-soft); }
.ui-lab  { font-size: 13px; font-weight: 600; }

.ui-stack > * + * { margin-top: 12px; }
.ui-gap  { height: 16px; }
.ui-gap--lg { height: 32px; }

/* A card. Fill, a 1px edge and NO shadow — `AppDesignLanguage.soft`
   declares `shadowStrength: 0.0`, so the card separates by boundary alone.
   The edge alpha is the one `theme.dart` solves for when the hairline cannot
   reach the 1.25 boundary floor: `controlBorder` at 0.27. */
.ui-card {
  background: var(--app-card);
  border: 1px solid var(--app-hairline);
  border-radius: 16px;
  padding: 20px;
}

/* The featured card, and it is the app's loudest surface: radius 24, the
   accent at 12% for the fill and 30% for the edge, padding 24. It carries
   exactly one thing — the sentence the app came back with. */
.ui-card--featured {
  border-radius: 24px;
  padding: 24px;
  background: color-mix(in srgb, var(--app-accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--app-accent) 30%, transparent);
}

.ui-badge {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--app-accent) 18%, transparent);
  color: var(--app-accent-ink);
  flex: none;
}

/* These were `rgba(128,152,174,.15)` and `rgba(192,142,118,.15)` — the 1.0
   dusk and clay, typed out rather than referenced, so they survived the
   palette repaint above them. Now the family tokens, at the same alpha. */
.ui-badge--dusk { background: color-mix(in srgb, var(--app-dusk) 15%, transparent); }
.ui-badge--clay { background: color-mix(in srgb, var(--app-clay) 15%, transparent); }

.ui-badge svg { width: 16px; height: 16px; display: block; }

.ui-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.ui-row__grow { flex: 1 1 auto; min-width: 0; }

/* Buttons. Filled is `accent.deep` with white on it, min height 48, radius
   16, label 15.5 at 600. Outlined takes the text ink and a 1px
   `controlBorder`. Both are non-interactive here — they are `<span>`s, not
   buttons, because a control in a reproduction that does nothing when pressed
   is worse than a picture of one. */
.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 12px 16px;
  border-radius: 16px;
  background: var(--app-accent-deep);
  color: #fff;
  font-size: 15.5px;
  font-weight: 600;
  text-align: center;
}

.ui-btn--quiet {
  background: transparent;
  color: var(--app-text);
  border: 1px solid var(--app-control);
}

.ui-btn--wide { width: 100%; }

.ui-btn svg { width: 18px; height: 18px; flex: none; }

.ui-btns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

/* The selectable chip, and it is one of the app's signatures: radius 14, not
   a pill; 1.2px hairline unselected; the accent at 16% with a 1.5px accent
   edge when it is. The weight goes 500 → 600 with it, so selection is never
   carried by colour alone. */
.ui-chip {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 16px;
  border-radius: 14px;
  border: 1.2px solid var(--app-hairline);
  font-size: 14px;
  font-weight: 500;
}

.ui-chip--on {
  background: color-mix(in srgb, var(--app-accent) 16%, transparent);
  border: 1.5px solid var(--app-accent);
  font-weight: 600;
}

.ui-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* A text field: filled with `surface`, 14 of content padding, the hint in
   `textSoft` at 14. */
.ui-field {
  background: var(--app-surface);
  border: 1px solid var(--app-control);
  border-radius: 16px;
  padding: 14px;
  color: var(--app-soft);
  font-size: 14px;
}

.ui-field--tall { min-height: 76px; }

.ui-divider {
  height: 1px;
  background: var(--app-hairline);
  margin: 16px 0;
}

.ui-foot {
  margin-top: 24px;
  text-align: center;
  font-size: 11px;
  color: var(--app-soft);
}

/* The toolkit's tiles. `AppCardGrid` at a 16 gutter, and the tile is the
   standard card with a badge above its title. */
.ui-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.ui-tile > * + * { margin-top: 8px; }

/* The rested-level row on the sleep screen: five words, no numbers, and no
   average anywhere on the screen. */
.ui-scale {
  display: flex;
  justify-content: space-between;
  gap: var(--s-2);
  font-size: 12.5px;
  color: var(--app-soft);
}

/* --- The swap, extended ---------------------------------------------------
   The site spends its third dimension exactly once, on `.swap`, and this is
   that component reused rather than a second one. What is added is a single
   modifier and a single keyframe.

   The existing pair says "this is pushed away and that is what stays" — the
   composition guard's argument on how.html. Discreet mode is not that
   argument: neither register is refused, they are two faces of one object,
   and importing `--away` / `--stays` here would import a meaning that is not
   true. So `--turn` opens both panes symmetrically toward the shared
   vanishing point and settles them square, which is the gesture for "the
   same thing, seen twice".

   The sign of the rotation comes from position rather than a second class:
   the panes are the first and third children of `.swap`, with the connector
   between them, and that is already load-bearing for the arrow's own layout.

   Transform only, on the `--motion-move` channel, inside the same
   reduced-motion and `@supports` gates as the original — so it retires
   through a token, and a browser with no view timelines shows both panes
   flat, square and complete, which is exactly what the argument needs. */

.swap__pane--turn {
  --turn: 0deg;
}

@media (min-width: 900px) {
  .swap__pane--turn:nth-child(1) { --turn: 15deg; }
  .swap__pane--turn:nth-child(3) { --turn: -15deg; }
}

@keyframes turn-flat {
  from { transform: rotateY(var(--turn)) translateZ(-30px); }
  to   { transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .swap__pane--turn {
      animation: turn-flat linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 36%;
    }
  }
}

/* The theme crossfade, bounded to the surfaces that repaint, per §19. The
   plate repaints; the screen inside it never does, because it names no site
   token. */
.appshot__plate {
  transition:
    background-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* ==========================================================================
   25. FROSTED — the site's material, its measure, and its light
   ==========================================================================

   ## The thesis

   Frosted glass is not a decoration borrowed from a trend deck. It is this
   product's own mechanism rendered as a material: **light passes through,
   detail does not.** That is what the app claims about a person's writing,
   and it is what a frosted pane does to whatever is behind it. Every panel on
   this site is that sentence, drawn.

   The old site was a centred reading column with forty per cent of a laptop
   viewport empty beside it, one section rhythm from top to bottom, and a
   palette of five accents that reached the page as a single green button. It
   was not restrained. It was unfinished, and it looked like a thousand other
   sites because it had made no decision that any of them had not made.

   ## The three rules this section is built on, each with its arithmetic

   **Rule one. The field is a ground for ink and for glass, and for nothing
   else.** `tool/site_layer_contrast.py` composites a stack of translucent
   layers and reports what a word measures on the result — the question
   `test/site_contrast_test.dart` cannot ask, because that test reads token
   pairs and a field is four fills deep. Run at the boldest field this
   section ships (sage 0.22, dusk 0.14, clay 0.10, all three overlapping,
   which is the worst pixel on the page rather than a typical one):

       light   --text 8.99   --text-soft 3.70   --accent 4.47
       dark    --text 7.10   --text-soft 3.87   --accent 2.75

   So `--text` clears the 4.5 body floor with between two and four times the
   headroom it needs, on either theme, at the worst pixel. `--text-soft` and
   `--accent` do not clear it on either. That is not a reason to dim the
   field; it is the specification for what may sit on one. A headline and a
   lead in `--text` are safe anywhere on the field. Soft text, accent text
   and any small type go on glass or on the bare page.

       ⚠️ This is why the lead paragraph is `--text` here and was
       `--text-soft` before. It is a legibility requirement first. It also
       happens to be the better typography: a lead is not a caption.

   **Rule two. Glass is opaque enough to be read through, and the number is
   measured, not chosen.** Same tool, a panel of `--surface` at rising alpha
   over that same worst-case field:

       light   0.72 -> --text-soft 4.86   --accent 5.88
       dark    0.72 -> --text-soft 5.29   --accent 3.75
       dark    0.86 -> --text-soft 5.92   --accent 4.20

   0.72 is the floor in light. Dark's accent never clears 4.5 on glass over a
   field, so **accent text is not painted on dark glass over a field** — it
   takes `--text`, and the accent does its work as fill, rim and light. The
   shipped alphas are 0.80 and 0.88, both above the floor, because a floor is
   not a target.

   **Rule three. Elevation is still §18's, and glass does not repeal it.**
   A white top edge on the light page measures 1.119:1 and an opaque black
   shadow on the dark page measures 1.253:1, which is the ceiling of that
   whole channel. Those numbers killed several treatments for earlier
   sessions and they are still true. They do not rule out glass; they rule
   out *one particular way of drawing its edge*, and they rule it out per
   theme rather than everywhere:

       light   the panel is separated by SHADOW, offset and softly blurred,
               plus a hairline. The white top sheen is present and is
               decorative only, and is not asked to carry the separation.
       dark    the panel is separated by a white RIM at 0.07-0.10, which
               §18 measured clearing the 1.25 boundary floor at 0.08, plus a
               deep shadow that reads as occlusion rather than as an edge.

   That split is not a compromise. It is the same physical claim rendered
   correctly twice: on a bright ground a pane casts, on a dark ground a pane
   catches.

   ## What is deliberately not here

   No `feTurbulence` grain and no sketch-style SVG. No gradient text; emphasis
   is weight and size. No eyebrow or kicker above any heading. No section
   numbers. No `border-left` accent stripes. No glyph or emoji icons. No
   backdrop blur above 24px, which is where Safari's compositor starts to
   cost real frames on a large pane. No second three-dimensional exhibit
   beyond the two this site now has, both of which are arguments rather than
   effects.

   Colour is spent at page scale, in fields that own whole regions, and never
   as an accent scattered on a neutral. **No palette value is changed.** Every
   colour below is `rgba(var(--rgb-*), a)` over a token that already shipped;
   the five accents are the app's own constants at the app's own chroma, and
   what is new is the area they are given.
   -------------------------------------------------------------------------- */

:root {
  /* ---- The measure ----------------------------------------------------
     One ramp, ratio 1.25, and a display step that is fluid because a
     marketing headline should fill the room it is given. The ceiling is
     6rem: past that a headline stops being read and starts being looked at,
     and this one has to be read. Tracking is size-specific — a single
     letter-spacing is wrong somewhere by definition, and it is wrong at both
     ends at once. */
  /* ⚠️ RAISED 2026-08-16. Previous ceilings kept here because they are the
     thing to restore if this reads as shouting on a laptop rather than as
     confidence: --t-1 3.8125rem, --t-2 2.4375rem, --t-3 1.5rem, --t-lead
     1.3125rem. --t-display is UNCHANGED at 6rem, which is `craft-floor`'s
     hard ceiling — past it a headline stops being read and starts being
     looked at, and this one has to be read.

     The complaint being answered is *"it looks conjusted sometimes and like
     traditional apps"* and *"it needs to use the space well"*. Those are the
     same complaint: a page whose largest element is 2.4rem on a 1512 screen
     is not using the room it was given, and the eye reads the whole thing as
     one texture. The gap between --t-1 and --t-2 was 1.56x and is now 1.59x,
     so the ramp is not merely bigger, the STEPS are more legible — which is
     what makes a page scannable rather than merely large. */
  --t-display: clamp(2.625rem, 1.05rem + 6.55vw, 6rem);
  --t-1: clamp(2.25rem, 1.35rem + 3.9vw, 4.5rem);
  --t-2: clamp(1.75rem, 1.3rem + 1.9vw, 2.875rem);
  --t-3: clamp(1.25rem, 1.14rem + 0.5vw, 1.625rem);
  --t-lead: clamp(1.1875rem, 1.09rem + 0.42vw, 1.4375rem);
  --t-body: 1rem;
  --t-small: 0.9375rem;
  --t-micro: 0.8125rem;

  --tr-display: -0.035em;
  --tr-1: -0.03em;
  --tr-2: -0.022em;
  --tr-3: -0.014em;
  --tr-body: 0em;
  --tr-micro: 0.012em;

  /* ---- The rhythm -----------------------------------------------------
     Three band heights, not one. A page whose every section is the same
     height has no emphasis, which was the fourth thing wrong with the old
     one. --band-open is for the two or three moments a page is allowed; the
     rest alternate tight and normal. */
  /* Opened with the type, and by more than the type was — because "more
     air" is mostly a vertical measurement and the horizontal one was never
     the problem. Previous: 4.5rem / 7rem / 11rem. The RATIO between them
     widens from 1.56x to 1.62x for the same reason the type ramp does: three
     band heights that are nearly equal are one band height with rounding. */
  --band-tight: clamp(3.25rem, 6vw, 5.25rem);
  --band: clamp(5rem, 9vw, 8.5rem);
  --band-open: clamp(7rem, 14vw, 13.75rem);

  /* ---- The material ---------------------------------------------------
     --glass-rgb is --surface's own triple, so a panel is the site's raised
     surface with light behind it rather than a new colour. The two alphas
     are the measured floor plus headroom (see rule two above). */
  --glass-rgb: 251, 250, 246;
  --glass-a: 0.80;
  --glass-a-hi: 0.88;
  /* 14px, not 20. Uniform heavy blur is 2020 glassmorphism and it is the
     thing that reads as dated; the effect lives in the RIM, and a large pane
     at 20px+ is also where Safari's compositor starts costing frames. The
     production references cluster far lower than the tutorials do: Raycast's
     floating nav is blur(5px), Linear's is 20px over a fully transparent
     fill, Stripe's single glass panel is 12px. */
  --glass-blur: 14px;
  --glass-sat: 1.55;

  /* Light: separation is the shadow. Offset and soft blur, per the craft
     floor — a zero-offset halo is not depth, it is a glow. */
  --glass-lift:
    0 1px 2px rgba(43, 42, 40, 0.05),
    0 2px 8px -4px rgba(43, 42, 40, 0.10),
    0 28px 64px -28px rgba(43, 42, 40, 0.30);
  /* The sheen. Decorative and unbound: at 1.119:1 it cannot carry an edge
     and is not asked to. */
  --glass-sheen: 0.92;
  --glass-rim: rgba(43, 42, 40, 0.10);

  --r-glass: 14px;
  --r-glass-lg: 18px;

  /* ---- The field ------------------------------------------------------
     Peak alphas, and they only all meet where the blobs overlap. Rule one
     is measured at exactly this triple. */
  --field-sage: 0.34;
  --field-dusk: 0.22;
  --field-clay: 0.17;
  --field-heather: 0.15;
  --field-blur: 34px;

  /* ---- THE LIT FIELD -------------------------------------------------
     Added beside the values above, never replacing them: `--field-*` is
     untouched and `.aura` still uses it, so nothing that shipped has moved.

     ## Why this exists, measured

     The site's whole page measures SATAVG 1.75 with a **p90 of 3**. The
     owner's favourite reference measures p90 **24.3**. Ours has saturated
     pixels — SATMAX 32, the accent — and essentially none of them. That is
     the same defect the app's three rejected mockups had, and the app
     review found what it actually is: the reference is NOT a saturated
     screen. Cropped in two, its text half measures 3.42 — statistically the
     same as ours — and its decorative half measures 11.91.

     **What is missing is not saturation. It is a ZONE where no contrast
     floor binds because nothing legible sits on it.**

     ## What the arithmetic allows, and it is far more than was being used

     `tool/site_layer_contrast.py`, sage rising against dusk 0.22, then §25's
     glass at 0.80 — because on this site the glass panel is what carries the
     text, which is the entire reason a loud field is affordable:

       field   text ON GLASS over it      text DIRECTLY on the field
       0.34    12.48 body / 5.13 soft     8.22 body / 3.38 soft / 4.09 accent
       0.55    12.13 / 4.99               —
       0.75    11.81 / 4.85               5.84 body / 2.40 / 2.90
       0.95    11.48 / 4.72               4.87 body / 2.00 / 2.42

     Every glass row clears 4.5. **Soft text and accent text fail on the bare
     field at 0.34 already** — that is §25's rule one, unchanged and now
     load-bearing rather than advisory. Body text survives on the bare field
     to 0.95 and that is the hard ceiling: past it nothing may sit on the
     field at all.

     Shipped at 0.70 — inside the comfortable band, leaving body text at
     ~6:1 on the bare field rather than at the 4.87 cliff, because a floor is
     not a target. */
  --field-sage-lit: 0.70;
  --field-dusk-lit: 0.46;
  --field-clay-lit: 0.36;
  --field-heather-lit: 0.32;

  /* ---- THE SOLO FIELD: ONE HUE PER REGION ----------------------------
     Added beside everything above. `--field-*` and `--field-*-lit` are
     untouched, `.aura` and `.aura--quiet` still read them, and every page
     that shipped is byte-identical until a class is added.

     ## The defect this replaces, measured

     `.aura` composites FOUR hues, and the reason it reads grey is not alpha.
     Their angular separation in the UV plane, which is the plane the
     saturation is measured in:

         dusk <-> clay     170 deg      sage <-> heather   163 deg
         sage <-> dusk      96 deg      sage <-> clay       94 deg

     Two pairs at 163 and 170 degrees is two pairs of near-complements, and
     a complement pair does not blend, it ANNIHILATES. The individual hues
     carry chroma 11.7 (heather) to 33.8 (clay). Composited at the `-lit`
     alphas they carry **4.14 in light and 2.04 in dark** — below the bone
     page's own 3.65. Raising the alphas cannot fix this and did not: the
     `-lit` values took vision.html from SATAVG 1.75 to 4.10 and stopped,
     because at alpha 1.00 four opposing hues still cancel to ~2.6.

     **So the fix is subtractive, not additive: remove three hues.**

     ## The ceiling is luminance, not hue — which is why one alpha serves all

     Swept with `tool/site_layer_contrast.py` over all five hues, the binding
     ink being the italic accent word in a display heading at its 3:1 floor
     (`--text` is never the binding constraint here — it holds 2x higher):

         light   alpha 0.70   accent ink 3.17-3.35    --text 6.67-6.98
         light   alpha 0.75   accent ink 2.96-3.07    --text 6.12-6.17  <- edge
         light   alpha 0.80   accent ink 2.85-2.97    --text 6.00-6.33  FAILS
         dark    alpha 0.50   accent ink 3.04-3.54    --text 5.56-5.99
         dark    alpha 0.55   accent ink 2.97-3.24    --text 5.43-5.47  <- edge
         dark    alpha 0.60   accent ink 2.51-2.94    --text 4.59-5.01  FAILS

     The spread ACROSS HUES at a fixed alpha is under 0.4 of a ratio point,
     while the spread across alpha is the whole budget. The field's cost is
     how far it drags the ground away from the ink's luminance, and all five
     hues sit at similar luminance. **One ceiling therefore serves every hue:
     ~0.73 in light, ~0.50 in dark.**

     The corollary is the important half, and it is a design constraint, not
     a tuning knob: **the hue choice IS the chroma choice.** At the same
     alpha and the same contrast cost, on the bone page,

         clay  24.6      sand  16.7      dusk  13.3      sage 9.4      heather 7.5

     Sage cannot be made loud. At alpha 1.00 it reaches 12.6 and its accent
     ink is already at 2.29 — it fails the floor long before it gets bright,
     because sage is a desaturated green sitting near the bone page's own
     hue. A region that wants colour picks a WARM hue. That is why the
     default below is clay and not the brand sage.

     ## Three lights, one hue, and the worst case is bounded by construction

     Same-hue blobs REINFORCE where they meet — that is the whole point —
     so the peak is the union, not the token. Three lights at a1/a2/a3 that
     all met would composite to 1-(1-a1)(1-a2)(1-a3):

         light   1 - 0.54 x 0.68 x 0.76 = 0.721   (ceiling ~0.73)
         dark    1 - 0.73 x 0.81 x 0.86 = 0.492   (ceiling ~0.50)

     ⚠️ **AND THE THREE-WAY OVERLAP IS REACHED, SO THE UNION IS THE REAL
     NUMBER, NOT A BOUND.** This comment first said the centres were too far
     apart for all three to meet and that bounding the impossible case was
     belt-and-braces. That was wrong, and sampling the wrong part of the page
     nearly confirmed it: the text-free left margin renders #56453d in dark,
     which is comfortably inside the bound, while the field BEHIND THE
     ACCENT WORD renders #6a5346 — the union, near enough. The plateaus are
     wide (a third of radii that are 74-90% of a box 1.36x the section), so
     they intersect over roughly 285x73px of the band. Read the extreme
     where the hot spots meet, not where it is convenient to find bare
     ground. The alphas above are sized so the union itself clears every
     floor, which is why the error was not also a bug. */
  --field-solo: 0.46;
  --field-solo-2: 0.32;
  --field-solo-3: 0.24;

  /* THE CORE. An intense light, and it is only ever placed where glass or
     nothing covers it. Measured in light: --text on the bare core is 5.94,
     and --text-soft under a 0.80 pane over it is 4.75. Both clear the body
     floor, which means a stray paragraph landing on one is a design mistake
     rather than a legibility failure - the belt as well as the braces. */
  --core-sage: 0.55;
  --core-clay: 0.38;
  --core-dusk: 0.30;
}

:root[data-theme="dark"] {
  --glass-rgb: 39, 42, 40;
  --glass-a: 0.80;
  --glass-a-hi: 0.88;
  /* Dark: separation is the rim. §18 measured 0.08 clearing the 1.25
     boundary floor; the shadow below it is occlusion, not an edge, because
     an opaque black one reaches 1.253 and cannot be one. */
  --glass-lift:
    inset 0 0 0 1px rgba(255, 255, 255, 0.075),
    0 30px 80px -36px rgba(0, 0, 0, 0.85);
  --glass-sheen: 0.10;
  --glass-rim: rgba(255, 255, 255, 0.10);
  /* NIGHT'S FIELD IS 54% OF CALM'S, NOT 74%.
     `03-tokens-and-components.md` states the ratio as a rule rather than a
     taste — wash alpha is `.26` on the light looks and `.14` on Night, and
     *"a fixed alpha makes the drawing the loudest thing on the page in dark
     mode, which happened once and was fixed at the call site."* These were
     at 74% of the light values, which was right for 1.0's muted hues and
     wrong for 2.0's: clay went from 192,142,118 to 198,124,96, and at 0.12
     over a near-black ground it washed the whole hero warm brown against a
     look whose ground is meant to be cool. Scaled to the package's own
     ratio. Nothing measured moves — the contrast arithmetic is done against
     `--fill`/`--fill-hi` and `--page`, and lowering the field only brings
     each bloom nearer the page it was measured against. */
  --field-sage: 0.18;
  --field-dusk: 0.12;
  --field-clay: 0.09;
  --field-heather: 0.08;
  --field-sage-lit: 0.44;
  --field-dusk-lit: 0.30;
  --field-clay-lit: 0.24;
  --field-heather-lit: 0.20;
  /* Scaled to the package's Night ratio for the same reason as the four
     above. A page-head takes a SOLO field — one hue, three lights — so it is
     the loudest surface on any interior page, and on a hue-sand or hue-clay
     head at 0.27 the 2.0 clay washed the whole hero warm brown over a ground
     the design draws cool. 0.46 -> 0.20 is the same 54% the package states
     between its light and Night wash alphas. */
  --field-solo: 0.20;
  --field-solo-2: 0.14;
  --field-solo-3: 0.10;
  --core-sage: 0.40;
  --core-clay: 0.28;
  --core-dusk: 0.22;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --glass-rgb: 39, 42, 40;
    --glass-a: 0.80;
    --glass-a-hi: 0.88;
    --glass-lift:
    inset 0 0 0 1px rgba(255, 255, 255, 0.075),
    0 30px 80px -36px rgba(0, 0, 0, 0.85);
    --glass-sheen: 0.10;
    --glass-rim: rgba(255, 255, 255, 0.10);
    /* NIGHT'S FIELD IS 54% OF CALM'S, NOT 74%.
       `03-tokens-and-components.md` states the ratio as a rule rather than a
       taste — wash alpha is `.26` on the light looks and `.14` on Night, and
       *"a fixed alpha makes the drawing the loudest thing on the page in dark
       mode, which happened once and was fixed at the call site."* These were
       at 74% of the light values, which was right for 1.0's muted hues and
       wrong for 2.0's: clay went from 192,142,118 to 198,124,96, and at 0.12
       over a near-black ground it washed the whole hero warm brown against a
       look whose ground is meant to be cool. Scaled to the package's own
       ratio. Nothing measured moves — the contrast arithmetic is done against
       `--fill`/`--fill-hi` and `--page`, and lowering the field only brings
       each bloom nearer the page it was measured against. */
    --field-sage: 0.18;
    --field-dusk: 0.12;
    --field-clay: 0.09;
    --field-heather: 0.08;
    /* ⚠️ THESE FOUR WERE MISSING AND IT WAS A LIVE CONTRAST FAILURE.
       `[data-theme="dark"]` above defines `--field-*-lit`; this block, which
       is the one that serves a visitor on system dark who has never touched
       the theme toggle, did not. So `.aura--vivid` on vision.html fell back
       to `:root`'s LIGHT alphas — sage at 0.70 — and painted them over the
       #1c1e1d page. Measured on that composite the accent ink reaches
       2.42:1 against a 3:1 floor: shipped, failing, and invisible to every
       screenshot, because `tool/site_shot.sh` always sets an explicit theme
       and therefore always took the branch that was correct.

       Nothing here is an edit in place. These are the values already
       declared for explicit dark, repeated into the block that was silently
       omitting them, so the two dark paths finally agree. */
    --field-sage-lit: 0.44;
    --field-dusk-lit: 0.30;
    --field-clay-lit: 0.24;
    --field-heather-lit: 0.20;
    /* Scaled to the package's Night ratio for the same reason as the four
       above. A page-head takes a SOLO field — one hue, three lights — so it is
       the loudest surface on any interior page, and on a hue-sand or hue-clay
       head at 0.27 the 2.0 clay washed the whole hero warm brown over a ground
       the design draws cool. 0.46 -> 0.20 is the same 54% the package states
       between its light and Night wash alphas. */
    --field-solo: 0.20;
    --field-solo-2: 0.14;
    --field-solo-3: 0.10;
    --core-sage: 0.40;
    --core-clay: 0.28;
    --core-dusk: 0.22;
  }
}

/* --------------------------------------------------------------------------
   25.1 The voice
   --------------------------------------------------------------------------
   ⚠️ **THIS IS THE ONE DECISION ON THE SITE THAT IS WAITING ON A LICENCE, AND
   IT IS STATED HERE RATHER THAN QUIETLY ABSORBED.**

   The strongest version of this design uses a display face with a point of
   view — a grotesque with real character at 96px. Every skill that governs
   this work says so, in the same words: the closest installed font is a
   failure, not a fallback. It is right.

   It cannot be done today. `website/` makes no external font request and
   copies no font file, and whether a face may be vendored here is a
   licensing decision the owner has not made — the same open question as
   Lora, the app's own heading face, which is why `features.html` reproduces
   the app's headings in this site's serif instead. Loading a webfont to
   settle a design argument would be settling a legal one by accident.

   So `--font-head` below is the platform's own display grotesque, used as a
   system rather than as a shrug: one family, four weights, tracking that
   changes with size because a single letter-spacing is wrong at one end or
   the other, and `font-optical-sizing: auto` so the face's own optical
   tables do the work a hand-tuned scale would otherwise fake. On Apple
   platforms that resolves to SF Pro Display, which ships genuine optical
   sizing and tracking tables; on Windows to Segoe UI Variable Display, which
   ships a width and an optical axis. Both are better display faces than
   Georgia at 96px by a wide margin, and neither needs a licence.

   **The recommendation, for whoever settles it:** a variable grotesque with a
   real width axis, self-hosted as one `woff2` subset to Latin — Söhne, ABC
   Diatype, Neue Haas Grotesk Display, or if the budget must be zero,
   Uncut Sans or Geist under the OFL. Any of them is one file, roughly 30 KB
   subset, and it would be the largest single visual upgrade left on this
   site. It is the first line of the handover.

   --font-accent keeps the old serif, and its job shrank to exactly one word
   per display heading. A whole headline in a transitional serif is the
   look this redesign is replacing; a single italic serif word inside a tight
   grotesque is a different device, and it is the one warm note the palette
   asks for.
   -------------------------------------------------------------------------- */

:root {
  --font-head:
    "SF Pro Display", -apple-system, BlinkMacSystemFont,
    "Segoe UI Variable Display", "Segoe UI",
    system-ui, "Helvetica Neue", Arial, sans-serif;
  --font-accent: Georgia, "Iowan Old Style", "Times New Roman", serif;
}

h1, h2, h3, h4, h5, h6 {
  font-optical-sizing: auto;
  text-wrap: balance;
}

.display {
  font-family: var(--font-head);
  font-size: var(--t-display);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: var(--tr-display);
}

/* The one warm note. Serif, italic, optically enlarged because a serif at a
   grotesque's cap height reads small, and given a hair of positive tracking
   because italic serifs set tight next to a tightly-tracked sans. */
.display em,
.h1 em,
.h2 em {
  font-family: var(--font-accent);
  font-style: italic;
  font-weight: 400;
  font-size: 1.06em;
  letter-spacing: -0.005em;
  color: var(--accent);
}

.h1 {
  font-family: var(--font-head);
  font-size: var(--t-1);
  font-weight: 680;
  line-height: 1.02;
  letter-spacing: var(--tr-1);
}

.h2 {
  font-family: var(--font-head);
  font-size: var(--t-2);
  font-weight: 660;
  line-height: 1.1;
  letter-spacing: var(--tr-2);
}

.h3 {
  font-family: var(--font-head);
  font-size: var(--t-3);
  font-weight: 620;
  line-height: 1.24;
  letter-spacing: var(--tr-3);
}

/* Rule one: a lead is --text, not --text-soft, because a lead sits on the
   field and --text-soft measures 3.70 there. It is also simply better. */
.lead {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--text);
  text-wrap: pretty;
  max-width: 46ch;
}

.lead--soft {
  color: var(--text-soft);
}

p {
  text-wrap: pretty;
}

/* Small type gets a touch of positive tracking, which is the other end of
   the size-specific rule and the end that usually goes unwritten. */
.micro {
  font-size: var(--t-micro);
  line-height: 1.5;
  letter-spacing: var(--tr-micro);
  color: var(--text-soft);
}

/* --------------------------------------------------------------------------
   25.2 The canvas — a grid that owns the whole width
   --------------------------------------------------------------------------
   The single biggest defect in the old site: every page was one column with
   forty per cent of a laptop viewport empty beside it. Not restraint. An
   unfinished second column.

   `.canvas` is a named-line grid, so a child chooses its own width by naming
   a line rather than by having a wrapper wrapped around it. Three widths:

     content   the reading column, 1120 — what `.wrap` was
     wide      1300, for exhibits and asymmetric splits
     full      edge to edge, for fields and rules

   The outer `minmax(var(--gutter), 1fr)` means the gutter never collapses and
   the grid never overflows, which is the failure the Design page shipped at
   1180-1744px when a component reached outside a centred container.
   -------------------------------------------------------------------------- */

.canvas {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, 90px)
    [content-start] minmax(0, 1120px) [content-end]
    minmax(0, 90px) [wide-end]
    minmax(var(--gutter), 1fr) [full-end];
}

.canvas > * {
  grid-column: content;
  min-width: 0;
}

.canvas > .to-wide { grid-column: wide; }
.canvas > .to-full { grid-column: full; }

/* The three band heights. A page alternates them; it does not pick one. */
.band       { padding-block: var(--band); }
.band--tight{ padding-block: var(--band-tight); }
.band--open { padding-block: var(--band-open); }

/* Asymmetric splits. Deliberately not 50/50 and deliberately not the same
   ratio twice in a row — the reason a page reads as composed rather than as
   a template is that its columns are not all the same width. */
.split {
  display: grid;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: start;
}

@media (min-width: 900px) {
  .split          { grid-template-columns: 7fr 5fr; }
  .split--minor   { grid-template-columns: 5fr 7fr; }
  .split--wide    { grid-template-columns: 8fr 4fr; }
  .split--even    { grid-template-columns: 1fr 1fr; }
  .split--center  { align-items: center; }
}

/* --------------------------------------------------------------------------
   25.3 The field — colour at page scale
   --------------------------------------------------------------------------
   The palette is five muted accents that already ship in the product, and
   before this section the site reached the page as beige with one green
   button. Nothing about the palette was the problem; the area given to it
   was.

   `.aura` is four soft-focus lights behind a region, at the peak alphas rule
   one is measured against. It is bounded to its own section rather than laid
   across the document, because a full-viewport moving background is the one
   thing the app's own motion research names as risky for this audience, and
   because an unbounded blur is a compositor cost on every scroll frame.

   `overflow: hidden` on the parent and `inset: -25%` on the layer together
   mean the blur never bleeds a hard edge at the section boundary, which is
   what a blurred gradient does if you let it end inside the box.
   -------------------------------------------------------------------------- */

.lit {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.aura {
  position: absolute;
  inset: -18%;
  z-index: -1;
  pointer-events: none;
  /* ⚠️ TWO THINGS ABOUT THESE NUMBERS, AND THE FIRST ONE COST A WHOLE
     ITERATION.

     **They are in the aura's own box, not the section's.** The layer is
     inset -18% on every side, so it is 1.36x the section, and a blob written
     at `8%` lands at 8% of 1.36 minus 18%, which is off the left edge of the
     screen. Every position is written as (wanted + 0.18) / 1.36.

     **They overlap on purpose, and the overlap is the design.** The first
     version placed four small lights in four corners so that none of them
     ever met. That is arithmetically safe and visually nothing: the field
     was measured at its worst-case overlap, cleared it with 7:1 of headroom,
     and then never reached a tenth of that density on the actual page. A
     field is not four accents in the corners; it is one lit ground made of
     four. So the blobs are large and they meet in the middle, which is the
     composite that rule one measured:

         light  #b8b5b2   --text 7.05   --ink-sage 3.51
         dark   #525351   --text 6.44   --ink-sage 3.52

     Both clear their floors: 4.5 for the body copy, 3.0 for the accent word
     at display size, which is the number that actually binds this whole
     section. Raise a field alpha and re-run `tool/site_layer_contrast.py`
     against BOTH, or do not raise it.

     **And each blob holds its peak to about 28% of its radius before it
     starts falling off**, which is the third thing that went wrong here. A
     two-stop radial gradient reaches its stated alpha at exactly one point,
     the centre, and is already fading everywhere else; blurred, the average
     lands at roughly two thirds of the number in the token. The page
     measured 0.22 where the stylesheet said 0.34 and the field looked like a
     wash rather than a ground. The plateau is what makes the token honest,
     and it is why the composite above can be trusted as the worst case
     rather than as an upper bound nothing reaches. */
  background:
    /* sage, wide, upper left through centre */
    radial-gradient(72% 76% at 22% 30%,
      rgba(var(--rgb-sage), var(--field-sage)) 0%,
      rgba(var(--rgb-sage), var(--field-sage)) 30%, transparent 78%),
    /* dusk, upper right, meeting sage across the middle */
    radial-gradient(66% 70% at 78% 20%,
      rgba(var(--rgb-dusk), var(--field-dusk)) 0%,
      rgba(var(--rgb-dusk), var(--field-dusk)) 28%, transparent 76%),
    /* clay, lower right */
    radial-gradient(62% 66% at 74% 84%,
      rgba(var(--rgb-clay), var(--field-clay)) 0%,
      rgba(var(--rgb-clay), var(--field-clay)) 26%, transparent 74%),
    /* heather, lower left */
    radial-gradient(58% 62% at 20% 88%,
      rgba(var(--rgb-heather), var(--field-heather)) 0%,
      rgba(var(--rgb-heather), var(--field-heather)) 26%, transparent 74%);
  filter: blur(var(--field-blur));
}

/* A second, quieter field for interior pages, so a page that is not the
   landing page is still lit without competing with it. */
.aura--quiet {
  opacity: 0.62;
}

/* The reverse: the field is the section. Used once per page at most. */
.aura--deep {
  inset: -35%;
  filter: blur(calc(var(--field-blur) * 1.4));
  opacity: 1.15;
}

/* --------------------------------------------------------------------------
   25.4 Glass
   --------------------------------------------------------------------------
   The material, and the reason the site is allowed to have one: a frosted
   pane passes light and stops detail, which is the product's own claim about
   a person's writing. It is used where it means that, and not as a finish on
   everything that needed a background.

   Read 25's rule two for the alphas. Read §18 for why the edge is drawn from
   a shadow in light and from a rim in dark, and why the two are not
   interchangeable.

   The rim is a masked gradient border rather than a `border`, because a
   border cannot be a gradient and a gradient background under a transparent
   border shows through the fill. `mask-composite: exclude` cuts the middle
   out of a padded box, leaving a one-pixel gradient ring. It is Baseline.
   -------------------------------------------------------------------------- */

.glass {
  position: relative;
  isolation: isolate;
  border-radius: var(--r-glass);
  background:
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  /* Four inset hairlines, not one. A pane lit from above catches light on
     its top edge, bounces a weaker line off its bottom edge, and shows a
     trace on both sides; one top highlight reads as a border, four read as
     an object. The outer shadow is section 18's, unchanged and still
     theme-split. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.62)),
    inset 0 -1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.28)),
    inset 1px 0 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.20)),
    inset -1px 0 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.20)),
    var(--glass-lift);
}

/* The rim. One pixel, brightest at the top-left where the light is, fading
   to the page's own hairline at the bottom-right. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  /* 🔴 **Decorative, so it must not take clicks — 2026-10-10.**
     This rim is `inset: 0` at `z-index: 2`, so it covers the whole element
     and, without this line, swallowed every pointer event inside it. On the
     evidence register that meant all 28 claim rows showed a working-looking
     chevron that did nothing: the `<summary>` was underneath the rim, so the
     click never reached it. The affordance was visible, the hit target was
     not, and nothing in a screenshot could tell you which.
     47 elements across the site carry `.glass`. */
  pointer-events: none;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, var(--glass-sheen)) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 34%,
    var(--glass-rim) 100%);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* The specular. A single soft light source at the upper left, the same one
   the rim is brightest under, so the panel reads as one object under one
   lamp rather than as a box with two unrelated highlights. */
.glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background: radial-gradient(70% 52% at 16% -6%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.55)) 0%,
    transparent 62%);
  pointer-events: none;
}

.glass > * {
  position: relative;
  z-index: 3;
}

.glass--lg { border-radius: var(--r-glass-lg); }

.glass__pad {
  padding: clamp(1.5rem, 3vw, 2.5rem);
}

/* A pane with no fill at all: rim and sheen only, for framing something that
   must not be tinted — the app reproductions, whose colours are the point.
   It is the one glass that carries no text of its own. */
.pane {
  position: relative;
  border-radius: var(--r-glass-lg);
  box-shadow: var(--glass-lift);
}

.pane::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, var(--glass-sheen)) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.15)) 40%,
    var(--glass-rim) 100%);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   25.5 Motion as structure
   --------------------------------------------------------------------------
   Not polish added at the end. The three things below are the only motion
   this section introduces, and each one is doing a structural job that the
   layout would otherwise need a paragraph to do.

   **The rule that shapes all three**, from the app's own motion research and
   from every skill that governs this work: reduced motion is honoured by
   SUBSTITUTION, not deletion. §19's two channels do that mechanically —
   `--motion-move` collapses to zero and takes every transform with it, while
   `--motion-tint` survives and keeps the colour and opacity that tell a
   reader their hover landed. Nothing below writes a literal millisecond.

   **Why scroll-linked and not triggered-once.** A one-shot entrance fired by
   an observer is the right tool for a list that appears. These are not that:
   the depth on the hero panes and the rise of an exhibit are *positional
   relationships to the reader's own scroll*, and they should run backwards
   when the reader scrolls backwards, because that is what a spatial
   relationship does. `animation-timeline: view()` expresses exactly that and
   costs no main-thread frame. Everything is visible in its finished state
   before the timeline attaches, so a browser without it, a reader with
   reduced motion, and a page whose script never ran all see the same
   composed page.

   ⚠️ Deliberately NOT a fade-and-rise on every section. One identical
   entrance repeated down a page is the tell that motion was applied to a
   document rather than designed into a surface, and it is the exact thing
   the owner is describing when he says the page has no moment.
   -------------------------------------------------------------------------- */

/* 1. Depth. Panels sit at their own distance and close on the reader as the
      section arrives. This is the site's spatial hierarchy: nearer is more
      important, and the reader learns it once in the hero. */
@keyframes settle-near {
  from { transform: translate3d(0, var(--rise, 26px), -60px) scale(0.972); }
  to   { transform: none; }
}

/* 2. Reveal. A masked wipe rather than a fade, because a wipe has a
      direction and a fade does not, and because the clip is the one property
      besides transform and opacity that composites without paint. */
@keyframes wipe-up {
  from { clip-path: inset(0 0 42% 0); opacity: 0.35; }
  to   { clip-path: inset(0 0 0 0);   opacity: 1; }
}

/* 3. Drift. The field breathes, once every fifty seconds, well under the
      0.2 Hz the app's motion notes flag and well under any threshold a
      vestibular reader would register. Colour and position both, so the
      light appears to move rather than to pulse. */
@keyframes field-drift {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  50%  { transform: translate3d(-2.2%, 1.6%, 0) scale(1.06); }
  100% { transform: translate3d(0, 0, 0) scale(1); }
}

@media (prefers-reduced-motion: no-preference) {
  .aura {
    animation: field-drift 52s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    will-change: transform;
  }

  @supports (animation-timeline: view()) {
    .depth {
      animation: settle-near linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 34%;
      transform-style: preserve-3d;
    }

    .reveal {
      animation: wipe-up linear both;
      animation-timeline: view();
      animation-range: entry 2% cover 26%;
    }

    /* A stagger inside one arriving group, capped at four steps so the last
       item is never waiting on the first. Past four it stops reading as one
       group arriving and starts reading as a queue. */
    .depth-set > *          { --rise: 30px; }
    .depth-set > :nth-child(2) { --rise: 40px; }
    .depth-set > :nth-child(3) { --rise: 50px; }
    .depth-set > :nth-child(n+4) { --rise: 58px; }
  }
}

/* --------------------------------------------------------------------------
   25.6 The three accessibility queries, answered separately
   --------------------------------------------------------------------------
   They are three different requests from three different people and
   collapsing them into one is how a site ends up removing the wrong thing.

     prefers-reduced-motion       stop moving. Colour may stay.
     prefers-reduced-transparency stop layering. Motion may stay.
     prefers-contrast: more       stop being subtle. Both may stay.

   The second is the one this section makes necessary and the one sites with
   glass routinely skip. A reader who asks for reduced transparency gets an
   opaque surface and no backdrop filter, which is a real answer rather than
   a slightly-less-blurred one, and the layout does not move a pixel.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-transparency: reduce) {
  .glass {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .glass::after {
    display: none;
  }

  .aura {
    opacity: 0.35;
    filter: blur(calc(var(--field-blur) * 1.6));
    animation: none;
  }
}

@media (prefers-contrast: more) {
  .glass {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
    outline: 1px solid var(--border);
  }

  .glass::before,
  .glass::after,
  .pane::before {
    display: none;
  }

  .aura {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   25.7 The chrome — a floating pane, and the theme control put in its place
   --------------------------------------------------------------------------
   Two changes, one of them the owner's and one of them the material's.

   **The theme control is demoted.** It sat between the wordmark and the
   navigation, which is the most valuable strip of pixels on the site, and it
   is a developer's priority: a visitor arriving from a search result does
   not want to choose a colour scheme, they want to know what this is. It
   keeps every one of its three states, its arrow-key traversal and its
   announcement — it moves to the end of the bar and is drawn at micro size
   in the soft ink, so it is findable and is no longer first.

   **The bar is a pane rather than a strip.** Apple's own guidance for this
   material, and the reason is legibility rather than fashion: content
   scrolling under a translucent layer needs an edge that says "this is in
   front", and a 1px hairline says it much less well than a soft mask does.
   So the hairline is replaced by a scroll edge — a short gradient that fades
   the bar's own fill into the page instead of ruling a line under it.
   -------------------------------------------------------------------------- */

.masthead {
  /* 0.90, not 0.72. At 0.72 the bar's composited ground is a function of
     whatever is passing beneath it: on features.html, whose light app plates
     are the largest bright surfaces on the site, the ground rose to
     rgb(98,98,94) and "Changelog" measured 2.65:1 in dark. The same nav
     clears comfortably at scroll 0.

     The instinct is to swap the link ink for `--text`, and that is the wrong
     repair: a ratio that changes as the reader scrolls is not a bad choice of
     token pair, it is a bar that is too transparent to have a ratio. Raising
     its own alpha bounds the ground instead, which fixes every link in the
     bar at once and keeps the demoted-ink hierarchy the nav is designed
     around. Still visibly glass — 0.90 over an 18px blur reads as frosted,
     not as a solid slab. */
  background: rgba(var(--glass-rgb), 0.90);
  backdrop-filter: blur(18px) saturate(1.7);
  -webkit-backdrop-filter: blur(18px) saturate(1.7);
  border-bottom: 0;
}

/* The scroll edge. `mask-image` on a pseudo-element rather than a border, so
   what a reader sees under the bar is their own content dissolving into the
   chrome rather than being cut off by a rule. */
.masthead::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 26px;
  pointer-events: none;
  background: linear-gradient(
    rgba(var(--glass-rgb), 0.5),
    rgba(var(--glass-rgb), 0));
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  mask-image: linear-gradient(#000, transparent);
  -webkit-mask-image: linear-gradient(#000, transparent);
}

.masthead__inner {
  min-height: 62px;
}

/* ⚠️ `flex-wrap: nowrap` WAS SET HERE UNCONDITIONALLY AND IT BROKE THE PHONE
   ON ALL ELEVEN PAGES.

   Below 560 the bar is meant to wrap onto two rows - section 6b sets
   `.masthead__nav { flex: 1 0 100% }` for exactly that. Forbidding the wrap
   left the nav on one line and pushed the page's scrollWidth to 484 against
   a 390 viewport: every page, including `privacy.html` and `changelog.html`,
   which this session had not otherwise touched. That identical 94px on a page
   with none of the new work on it is what identified the masthead as the
   cause rather than the field or the exhibit.

   Found by `tool/site_overflow.html`, which drives every page through
   fourteen widths in a sized iframe and reports `scrollWidth > clientWidth`.
   It is in the repo now because this is the second sideways scroll this site
   has shipped: the Design page scrolled at every laptop width between 1180
   and 1744 and nothing caught it either. */
@media (min-width: 900px) {
  .masthead__inner {
    flex-wrap: nowrap;
  }
}

.brand {
  font-weight: 660;
  letter-spacing: -0.022em;
}

.masthead__nav a:not(.btn) {
  font-size: var(--t-small);
  letter-spacing: -0.006em;
}

/* Demoted: micro size, soft ink, no fill until it is the chosen one, and
   last in the bar. */
:root[data-js] .theme {
  order: 9;
  margin-left: 0.25rem;
}

.theme__set {
  gap: 0;
}

.theme__opt {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-micro);
  padding: 0.3rem 0.5rem;
  color: var(--text-soft);
  opacity: 0.72;
}

.theme__set input:checked + .theme__opt {
  opacity: 1;
}

/* Below the desktop tier the bar keeps exactly the two items it has always
   had, so the phone layout is untouched by any of this. */
@media (max-width: 900px) {
  :root[data-js] .theme {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   25.8 Controls
   --------------------------------------------------------------------------
   The primary action is the one place on the site where the accent is a
   fill rather than a field, so it is also the one place a specular edge is
   worth spending: a filled pill with a light on its top edge reads as an
   object, and a flat one reads as a rectangle of colour.
   -------------------------------------------------------------------------- */

.btn {
  font-weight: 550;
  letter-spacing: -0.008em;
  transition:
    background-color var(--dur-tint) var(--ease-tint),
    color var(--dur-tint) var(--ease-tint),
    border-color var(--dur-tint) var(--ease-tint),
    box-shadow var(--dur-tint) var(--ease-tint),
    transform var(--dur-press) var(--ease);
}

.btn--primary {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 1px 2px rgba(43, 42, 40, 0.14),
    0 12px 28px -14px rgba(74, 93, 70, 0.55);
}

@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.28),
      0 1px 2px rgba(43, 42, 40, 0.16),
      0 16px 36px -14px rgba(74, 93, 70, 0.65);
  }

  .btn--quiet:hover {
    border-color: var(--text-soft);
  }
}

.btn--quiet {
  background: rgba(var(--glass-rgb), 0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* ⚠️ The three preference queries, answered HERE rather than left to §25.6.
   This is the site's fourth glass material and it shipped without either
   override, which is the same defect §33 was written to stop recurring — and
   it recurred anyway, on a class the earlier blocks had never heard of.

   The cost of missing it is not cosmetic and not evenly spread. Under
   `prefers-reduced-transparency` the site drops from 83 backdrop-filtered
   elements to 2, and the two survivors were index.html's *"See what it
   does"* and the breathing exercise's **Stop** — the primary call to action
   and the control that ends a running exercise. A reader who has asked the
   operating system for less layering is the reader least well served by the
   two most load-bearing buttons on the site ignoring them.

   The general lesson, and the reason this comment is longer than the rule:
   an override attached to a *list of selectors* protects the selectors that
   existed when it was written. Attaching it to the material, at the point
   the material is introduced, is what makes the next glass class inherit the
   answer instead of re-opening the hole. */
@media (prefers-reduced-transparency: reduce) {
  .btn--quiet {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  .btn--quiet {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-color: var(--text-soft);
  }
}

.btn:active {
  transform: scale(calc(1 - 0.028 * var(--motion-move)));
}

/* --------------------------------------------------------------------------
   25.9 The first screen
   --------------------------------------------------------------------------
   The composition, and why it is this one.

   A two-column hero with the headline left and a panel right is the form
   every product site reaches for, and at 1440 it gives the headline about
   nine characters a line at display size, which is why so many of them have
   a small headline. This one is a **two-row** composition instead: the
   headline takes the wide column at full display size and wraps to two lines
   that occupy roughly two thirds of it, and the exhibit rises into the space
   the second line leaves. The reader gets a headline at 96px AND a working
   product in the same viewport, and the right half is no longer a margin.

   The overlap is the point and it is not decoration: the pane crossing the
   headline's baseline is what makes the two rows read as one composition
   rather than as a stack, and it is the only place on the site where an
   element breaks its own row.

   ⚠️ The old `.hero::before` field is retired here rather than deleted,
   because `.hero` is on one page and the rule is long enough that removing
   it and being wrong is worse than shadowing it and saying so. `.aura` is
   the same idea at the alphas section 25's rule one is measured against.
   -------------------------------------------------------------------------- */

.hero {
  padding-block: clamp(2.75rem, 6vw, 4.5rem) var(--band);
  overflow: clip;
}

/* Retired. See the note above. */
.hero::before { display: none; }

/* The accent rule under the hero stays, and is the one hard edge on the
   screen: five hues at full chroma in a two-pixel line, which is the whole
   palette declared once before anything else uses it. */
.hero::after {
  height: 2px;
  opacity: 0.7;
}

.hero__title {
  /* Space Grotesk is a wider face than the system grotesque this was tuned
     against, so the same character count is about 60px wider on the line.
     The cap is in characters and the character got bigger; the number had to
     come down with it or the headline runs under the exhibit. */
  max-width: 11ch;
  margin: 0 0 clamp(1.5rem, 3vw, 2.25rem);
}

/* --ink-sage, not --accent. Measured: over the worst pixel of the field the
   accent reaches 4.47 in light but only 2.75 in dark, which is under the 3:1
   a word at display size needs. --ink-sage is the AA variant of the same hue
   and clears it on both, at 4.47 and 3.88. Same colour family, correct
   value, and the arithmetic is the reason rather than the taste. */
.display em,
.h1 em,
.h2 em {
  color: var(--accent);
}

.hero__body {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}

.hero__say .lead {
  margin-top: 0;
  /* 30ch, not 34. Space Grotesk is wider per character than the face this
     was tuned against, and at 2560 the last word of the first line was
     landing against the exhibit's left edge. A `ch` cap moves when the
     character does. */
  max-width: 30ch;
}

.hero__facts {
  margin-top: var(--s-6);
  max-width: 32ch;
  /* ⚠️ `--text-soft` is not allowed on the bare field and this line sat on it
     at 3.83:1 against a 4.5 floor — on the first screenful, in light, in the
     three sentences that say the product is free, offline and accountless.
     The brief's own rule, broken in the place with the most readers. The
     class is `.micro` for its size; the colour is overridden here because the
     ground it lands on is a property of this slot, not of `.micro`. */
  color: var(--text);
}

.hero__actions {
  margin-top: var(--s-7);
}

@media (min-width: 1000px) {
  .hero__body {
    grid-template-columns: minmax(0, 47fr) minmax(0, 53fr);
    align-items: center;
  }

  /* The exhibit is the taller column and it leads the eye, so it starts
     slightly above the headline's cap line and finishes below the lead's
     last baseline. An element that begins and ends exactly where its
     neighbour does reads as a table cell; this one reads as a panel the
     text is arranged around, which is the difference between a layout and a
     composition.

     Negative margin rather than absolute positioning, deliberately: the
     pane still takes part in the grid and still pushes the section's own
     height. An absolutely-positioned exhibit is the one that overflows a
     section at some viewport nobody tested, which is exactly how the Design
     page shipped a sideways scroll on every laptop between 1180 and 1744. */
  .hero__exhibit {
    margin-block: clamp(-2.5rem, -2vw, -1rem) clamp(-2rem, -1.5vw, -0.5rem);
  }
}

@media (min-width: 1260px) {
  .hero__title {
    max-width: 10ch;
  }
}

/* --------------------------------------------------------------------------
   25.10 The check-in, as the hero's exhibit
   --------------------------------------------------------------------------
   The pane carries text, so rule two applies to it and the glass is at 0.80
   rather than at the 0.72 floor. Everything inside is a class that already
   existed and already measured; what is new is the container and the fact
   that the container is the first thing on the page.
   -------------------------------------------------------------------------- */

.checkin__h {
  margin: 0 0 1.25rem;
}

.checkin .mood {
  margin-bottom: var(--s-6);
}

.checkin__note {
  margin: 1.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--hairline);
}

.checkin__note b {
  color: var(--text);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   25.11 The bar's own alignment
   --------------------------------------------------------------------------
   Brand at the left edge, everything else gathered at the right, which is
   what `space-between` stopped doing the moment a third child arrived.
   -------------------------------------------------------------------------- */

.masthead__nav {
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   25.12 The theme control, finished
   --------------------------------------------------------------------------
   The demotion is not only position. A bordered pill with a filled selected
   segment is the visual weight of a primary control, and it was sitting two
   inches from the actual primary control wearing the same shape. The border
   and the fill go; the chosen state is carried by ink and weight, which is
   enough for a three-item control whose states are words.

   Focus and hover are untouched, and the checked state is still expressed in
   CSS rather than asked of JavaScript.
   -------------------------------------------------------------------------- */

.theme__set {
  border: 0;
  background: transparent;
  overflow: visible;
}

.theme__opt {
  min-height: 32px;
  padding-inline: 0.45rem;
}

.theme__set input:checked + .theme__opt {
  background: transparent;
  color: var(--text);
  font-weight: 600;
}

@media (hover: hover) and (pointer: fine) {
  .theme__opt:hover {
    color: var(--text);
    opacity: 1;
  }
}

.theme__set input:focus-visible + .theme__opt {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

/* --------------------------------------------------------------------------
   25.13 The core — where the colour is allowed to be loud
   --------------------------------------------------------------------------
   The field above is capped by one number and it is worth naming: the italic
   accent word in a display heading. `--ink-sage` over the field has to clear
   the 3:1 a word at that size needs, and in dark that binds at roughly a
   0.25 sage peak. Everywhere a heading can reach, the field stops there.

   **So the loud colour goes where no heading can reach.** `.aura__core` is
   a small, high-alpha light placed behind the glass, and it is the reason
   the material reads as a material: a pane over a flat ground is a white
   rectangle, and a pane over a bright light is glass. The blur and the
   1.55 saturation in `.glass` act on the core, which is what makes the
   frosted edge visible at all.

   Placement is a rule, not a preference:

     the field  goes where text goes, at the measured ceiling
     the core   goes behind glass, or behind nothing

   A core with bare body text on it is the one arrangement this section does
   not sanction. It is still legible if it happens - 5.94 in light, and the
   dark values are pulled to 0.40/0.28/0.22 for the same reason - but it is
   not the composition being designed.
   -------------------------------------------------------------------------- */

.aura__core {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(46% 58% at 76% 40%,
      rgba(var(--rgb-sage), var(--core-sage)) 0%,
      rgba(var(--rgb-sage), var(--core-sage)) 24%, transparent 72%),
    radial-gradient(38% 46% at 96% 12%,
      rgba(var(--rgb-clay), var(--core-clay)) 0%,
      rgba(var(--rgb-clay), var(--core-clay)) 20%, transparent 68%),
    radial-gradient(36% 44% at 58% 92%,
      rgba(var(--rgb-dusk), var(--core-dusk)) 0%,
      rgba(var(--rgb-dusk), var(--core-dusk)) 20%, transparent 68%);
  filter: blur(30px);
}

@media (prefers-reduced-motion: no-preference) {
  .aura__core {
    animation: field-drift 64s cubic-bezier(0.4, 0, 0.6, 1) infinite reverse;
    will-change: transform;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .aura__core { opacity: 0.4; animation: none; }
}

@media (prefers-contrast: more) {
  .aura__core { display: none; }
}

/* Glass over a lit core wants more saturation than glass over a flat page,
   because the blur is averaging a coloured light rather than a neutral one
   and an unsaturated average of a colour is grey. */
.hero__exhibit {
  --glass-sat: 1.9;
}

/* --------------------------------------------------------------------------
   25.14 The mood field — the edge the glass needed
   --------------------------------------------------------------------------
   The finding that turned this hero around, and it took three attempts to
   see it: **a pane of glass over a soft gradient is a white rectangle.**
   Blur has nothing to do unless there is an edge behind it. Every version of
   this hero that sat on an aurora read as flat no matter how much blur or
   saturation the pane carried, and the instinct - add more blur - made it
   worse, because more blur is more averaging and an average of a soft wash
   is a flat colour.

   `AppColors.moodScale` is five hard stops. It is also, and this is why it
   is this graphic rather than a decorative one, the exact ladder the slider
   inside the pane moves along.

   Measured, glass at 0.88 over the worst band of each theme:

       light  --text-soft 4.72  (worst band --mood-1 #5d5068)
       dark   --text-soft 5.06  (worst band --mood-5 #a6c9ad)

   0.80 was not enough here - three of the ten combinations landed between
   4.17 and 4.45 - so the exhibit pane alone runs at 0.88. That is the one
   glass on the site tuned per placement rather than per role, and the reason
   is written above it.
   -------------------------------------------------------------------------- */

.hero__frame {
  position: relative;
}

.mood-field {
  position: absolute;
  /* Asymmetric, and offset down-left like a misregistered print run. Equal
     insets put an even border of colour round the pane, which reads as a
     frame; an uneven one reads as two objects at different depths, which is
     what they are. */
  inset: -1.25rem -1.5rem -3.25rem -3.5rem;
  z-index: 0;
  border-radius: calc(var(--r-glass-lg) + 10px);
  background: linear-gradient(112deg,
    var(--mood-1) 0 19%,
    var(--mood-2) 19% 39%,
    var(--mood-3) 39% 60%,
    var(--mood-4) 60% 80%,
    var(--mood-5) 80% 100%);
}

.hero__exhibit {
  position: relative;
  z-index: 1;
  /* See 25.14: the bands are full chroma and the pane has to carry body copy
     over them. */
  --glass-a: 0.88;
  --glass-a-hi: 0.93;
  --glass-sat: 1.9;
}

@media (prefers-contrast: more) {
  .mood-field { display: none; }
}

/* --------------------------------------------------------------------------
   25.15 Cross-document transitions
   --------------------------------------------------------------------------
   Eleven static pages that reload white between every navigation. One
   at-rule makes the whole site cross-fade instead, with no script, no
   framework and no fallback cost: a browser that does not know the rule
   navigates exactly as it did before.

   Under the move channel it is off, because a cross-document transition is
   a positional effect on the whole viewport and that is the category the
   app's own motion notes name as the risky one.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* --------------------------------------------------------------------------
   25.16 The highlights page
   --------------------------------------------------------------------------
   This is the page with the most visually interesting material on the site -
   the app's interface, rebuilt in HTML - and it was buried under three
   screenfuls of caveats. The caveats are all still on the page; they are
   simply no longer the page.

   The composition is the landing page's, on purpose: prose left, exhibit
   right, the exhibit slightly taller than its column so the two read as one
   arrangement rather than as two cells. A reader who has seen the home page
   already knows how to read this one.
   -------------------------------------------------------------------------- */

.feat-hero {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}

.feat-hero__say .lead {
  max-width: 40ch;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.feat-hero .display {
  max-width: 15ch;
  margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
}

.feat-hero__prose > * + * {
  margin-top: var(--s-4);
}

.feat-hero__prose p {
  max-width: 44ch;
  color: var(--text-soft);
}

@media (min-width: 1000px) {
  .feat-hero {
    grid-template-columns: minmax(0, 52fr) minmax(0, 48fr);
  }

  .feat-hero__show {
    margin-top: clamp(-3rem, -3vw, -1rem);
  }
}

/* The reproduction gets the frameless pane: rim and light, no fill, because
   the colours inside it are the exhibit and tinting them is the one thing a
   reproduction may not do. */
.feat-hero__show .appshot__plate,
.appshot .appshot__plate {
  border-radius: var(--r-glass-lg);
}

/* --------------------------------------------------------------------------
   25.17 Disclosure
   --------------------------------------------------------------------------
   Where a page's apparatus goes once it stops leading. Narrow, quiet, and
   below the argument: the shape of a reader's own second question rather
   than of the author's first caveat.
   -------------------------------------------------------------------------- */

.disclose {
  max-width: var(--measure);
  padding-top: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--hairline);
}

.disclose > p {
  margin-top: var(--s-4);
  color: var(--text-soft);
}

.disclose > p b {
  color: var(--text);
}

.disclose__more {
  margin-top: 1.75rem;
}

.disclose__more > summary {
  cursor: pointer;
  font-family: var(--font-head);
  font-size: var(--t-small);
  font-weight: 560;
  letter-spacing: -0.008em;
  color: var(--text-soft);
  padding: 0.5rem 0;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  transition: color var(--dur-tint) var(--ease-tint);
}

.disclose__more > summary::-webkit-details-marker { display: none; }

/* The marker is drawn rather than a glyph, per the icon rule, and it turns
   with the state. `transform` is on the move channel, so under reduced
   motion it snaps rather than turns and the state is still legible. */
.disclose__more > summary::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform calc(var(--dur-reveal) * var(--motion-move)) var(--ease);
}

.disclose__more[open] > summary::before {
  transform: rotate(45deg);
}

@media (hover: hover) and (pointer: fine) {
  .disclose__more > summary:hover { color: var(--text); }
}

.disclose__body > * + * {
  margin-top: var(--s-4);
}

.disclose__body {
  padding-top: 0.75rem;
  color: var(--text-soft);
  font-size: var(--t-small);
  line-height: 1.6;
}

.disclose__body b { color: var(--text); }

/* --------------------------------------------------------------------------
   25.18 Two things the highlights hero got wrong on its first render
   --------------------------------------------------------------------------
   **The display step is the landing page's, and it does not belong on an
   interior page in a two-column hero.** `--t-display` tops out at 96px,
   which in a 650px column is about seven characters a line: the headline
   became three lines of very large type and the exhibit beside it looked
   like a thumbnail. `--t-1` is the step below and it is the right one here,
   which also gives the site a hierarchy it did not have - the landing page
   is the only page that gets the top of the ramp.

   **The exhibit was capped at 25.5rem** because it was designed to sit
   beside a reading column at the same width everywhere. In the hero it is
   the argument, so it takes the column it is given.
   -------------------------------------------------------------------------- */

.feat-hero .display {
  font-size: var(--t-1);
  line-height: 1.0;
  letter-spacing: var(--tr-1);
  max-width: 16ch;
}

.feat-hero__show .appshot {
  max-width: none;
}

/* --------------------------------------------------------------------------
   25.19 The mono role
   --------------------------------------------------------------------------
   The site had a `--font-mono` token and used it in eight places for code
   spans, and no role at all. Every strong reference this work looked at
   pairs its display face with a mono, and the reason is not decoration: a
   mono is the only face in a system stack that carries character without a
   licence, and it is the one that makes a figure look measured.

   ⚠️ **It is for measurement, never for atmosphere.** Monospace as a costume
   for "technical" is a named anti-pattern and it is the obvious way to get
   this wrong. The rule here: a mono span names a thing that came off a file
   or a rendering - an exhibit's provenance, a count, a ratio, a version, a
   date checked. If the text would still be true with the numbers removed, it
   is not a candidate.
   -------------------------------------------------------------------------- */

.appshot__label,
.spec-label {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The prose beside the breathing exhibit. Its own class rather than a bare
   div so `tool/site_class_audit.py` can see it: an undefined class ships
   silently because a missing margin looks like a design choice. */
.demo__aside .lead {
  max-width: 38ch;
}

/* ==========================================================================
   26. THE VOICE, SETTLED — three faces, self-hosted, 99 KB
   ==========================================================================

   Section 25.1 said the display face was the one decision on this site
   waiting on a licence, and that a system grotesque was the best that could
   be done until somebody made it. It has been made, and the answer was
   already in the repository.

   ## What ships, and why each one

   **Space Grotesk** carries the display, every heading, the UI labels and
   every figure. It is a technical grotesque with real character - the
   angled terminals, the flat-sided round letters, the distinctive `g` - and
   it is the thing the research said this site was missing and every strong
   reference has: a face that reads as *made*, not as *installed*. It has
   been sitting in `assets/fonts/` under the OFL for the whole project.

   **Lora Italic** is the one warm note, and its job is exactly one word per
   display heading. A whole headline in a transitional serif is the look this
   redesign replaced; a single italic serif word inside a tight grotesque is
   a different device, and it is the only place the site is allowed to be
   literary.

   **Lora upright** exists for one purpose and it closes an owed decision:
   `lib/design/typography.dart` sets `appHeadingFontFamily` to Lora, and
   until now the reproductions on the highlights page used this site's own
   serif instead, with a note on the page apologising for it. They can now
   use the app's actual face. The note about it goes.

   **The body is `system-ui`, deliberately, and it is not a compromise.** It
   costs nothing to send, it is the only face on any platform that ships
   optical sizing and per-size tracking tables tuned by the vendor, and it is
   what the reader's own text-size settings expect. The audience for this app
   is on Indian mobile data; four families would have been vanity.

   ## Weight, measured

       SpaceGrotesk-Variable-latin.woff2    22.9 KB   wght 300-700
       Lora-Variable-latin.woff2            36.8 KB   wght 400-700
       Lora-Italic-Variable-latin.woff2     39.7 KB   wght 400-700, italic
                                            ------
                                            99.4 KB

   All three are variable, so those are the *complete* weight ranges rather
   than one weight each - a static three-weight family of any one of them
   would cost more and do less. All three are subset to Latin plus
   punctuation and the currency and arrow marks the pages actually use, down
   from 570 KB of unsubset TrueType.

   ## Licence, and the one clause worth reading twice

   SIL Open Font License 1.1, and `<Family>-OFL.txt` sits beside each file,
   which is what the licence requires. Provenance is recorded in
   `website/fonts/SOURCES.md` the way `tool/eval/data/SOURCES.md` records a
   dataset, because a font with no recorded provenance is the same class of
   problem as an unsourced number.

   ⚠️ **The Reserved Font Name clause.** OFL requires a MODIFIED version to
   be renamed. Subsetting is not a modification in that sense and the SIL FAQ
   says so directly - it is the same outlines with unused glyphs removed, and
   it is what every font CDN in the world serves. No outline, metric, name or
   axis was altered. If any of that changes, the family has to be renamed,
   and that is the line not to cross without reading the licence again.

   ## Self-hosted, and that is a privacy decision as much as a design one

   No CDN and no Google Fonts link. `vercel.json` sets `font-src 'self'` and
   would refuse a remote one anyway, but the real reason is that a hosted
   font is a third-party request on every page load of a site whose central
   argument is that almost nothing leaves the device. The stylesheet would
   have contradicted the copy.
   ========================================================================== */

@font-face {
  font-family: "Space Grotesk";
  src: url("fonts/SpaceGrotesk-Variable-latin.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* The body face the redesign specifies. It was already in this repository —
   `assets/fonts/PlusJakartaSans-Variable.ttf`, bundled for the app with its
   own OFL beside it — so nothing was downloaded and nothing is fetched at
   runtime; it was subset with the same pyftsubset invocation as the other
   three and copied in. 24.8 KB, taking the site's whole font payload from
   99.4 KB to 121.3 KB.

   This replaces `system-ui` as the body voice, and that swap is a real
   trade rather than a free upgrade: `system-ui` costs nothing to send and
   ships vendor-tuned optical sizing. What it cannot do is be the same face
   on two devices, and the redesign's whole argument is that the site and the
   app should look like one product — which they cannot while the site's body
   copy is whatever the reader's OS happens to supply. The app sets Plus
   Jakarta Sans; now so does the site.

   `font-display: swap` with a metric-adjacent fallback below, so a reader on
   a slow connection gets text immediately and the swap is a reflow nobody
   notices. */
@font-face {
  font-family: "Plus Jakarta Sans";
  src: url("fonts/PlusJakartaSans-Variable-latin.woff2") format("woff2-variations");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Lora";
  src: url("fonts/Lora-Variable-latin.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Lora";
  src: url("fonts/Lora-Italic-Variable-latin.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: italic;
  font-display: swap;
}

:root {
  /* The fallbacks are metric-adjacent rather than alphabetical: if the woff2
     has not arrived, `font-display: swap` paints in these, and a swap
     between two faces of similar width is a reflow nobody notices while a
     swap between a grotesque and Times is a page that jumps. */
  --font-head: "Space Grotesk", "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-accent: "Lora", Georgia, "Iowan Old Style", "Times New Roman", serif;

  /* The app's own heading face, for the reproductions on the highlights page
     and for nothing else. Named separately from --font-accent so that a
     change to the site's editorial voice cannot silently change what the
     reproductions claim to be. */
  --font-app-head: "Lora", Georgia, serif;
}

/* Space Grotesk sets tighter than a system grotesque at the same tracking,
   and its caps are relatively short, so the display steps lose a little of
   their negative tracking and the small steps gain a little. Size-specific,
   per the rule this file has followed since section 25. */
.display {
  letter-spacing: -0.028em;
  font-weight: 620;
}

.h1 { letter-spacing: -0.024em; font-weight: 600; }
.h2 { letter-spacing: -0.018em; font-weight: 580; }
.h3 { letter-spacing: -0.010em; font-weight: 560; }

/* The accent word. Real italics from a real italic file, not a synthesised
   oblique: `font-style: italic` on a family with no italic face is a shear
   transform, and it looks like one. */
.display em,
.h1 em,
.h2 em {
  font-family: var(--font-accent);
  font-style: italic;
  font-weight: 500;
  font-size: 1.04em;
  letter-spacing: -0.004em;
  color: var(--accent);
}

/* The reproductions use the app's face, which they could not do before. */
.ui-title,
.ui-sec,
.appshot .ui h1,
.appshot .ui h2,
.appshot .ui h3 {
  font-family: var(--font-app-head);
}

/* --------------------------------------------------------------------------
   27. The evidence page's first screen
   --------------------------------------------------------------------------
   The three words a visitor used to read first on this page were "Did not
   hold", "Deliberately not used" and "Unverifiable" - an audit's taxonomy,
   in the shop window, before a sentence about what the audit found.

   🔒 Nothing in the register was changed. All 28 rows, one list, one type
   size, the same four verdicts under the same four words. What moved is the
   order of encounter: the result before the vocabulary, and the subject
   matter before the taxonomy, which is why the mechanism facet is now the
   first control rather than the verdict facet.

   The tally is a `figure` with a caption, because it is a chart: four bars
   on one rail, measured against the register's own 28. It was a `dl` of four
   equal cards until 2026-08-16, and the reason it changed is in the block
   that draws it. The figures are set in the grotesque's `tnum` so the column
   compares down, and the cells carry different hues from the accent family,
   in the order the register uses, so the reader learns the colour before
   they meet it on a row.
   -------------------------------------------------------------------------- */

.ev-hero {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: end;
}

.ev-hero .display {
  font-size: var(--t-1);
  line-height: 1.02;
  letter-spacing: var(--tr-1);
  max-width: 18ch;
  margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
}

.ev-hero .lead {
  max-width: 44ch;
}

/* The footnote under the lead says what the other 63 assessed claims are, and
   it is a longer sentence than the lead it sits beneath. Left at the default
   `.tiny` measure it wrapped to a narrow ragged column against a wide lead. */
.ev-hero .stack > .tiny {
  max-width: 44ch;
}

@media (min-width: 1000px) {
  .ev-hero {
    grid-template-columns: minmax(0, 58fr) minmax(0, 42fr);
  }
}

/* THE TALLY, AS A METER CHART AND AS THE REGISTER'S FILTER.
   ------------------------------------------------------------------------
   It was a 2x2 of flat rectangles, each holding one large figure. Four
   magnitudes - 16, 8, 3, 1 - set at one size, so nothing on screen carried
   the one thing they are about. And it was the obvious control on the page
   while being no control at all.

   FOUR BARS ON ONE RAIL, AND THE RAIL IS 28. Every row is measured against
   the same track, so the bars decompose the register rather than each
   filling its own box. A bar's length is a share of the whole, which is why
   the unverifiable row is a 3.6% stub - that is what one row in twenty-eight
   looks like, and the figure beside it still reads 1.

   NEVER COLOUR ALONE, AND THAT IS A MEASURED REQUIREMENT HERE RATHER THAN A
   PREFERENCE. The four verdict hues fail as a categorical SET, in both
   themes, measured rather than guessed:

       light  #4a5d46 #68593d #7b503c #61556c  on --surface-sunk #efece3
       dark   #9fb59a #c6b48f #e1aa90 #c0b0cd  on --surface-sunk #222524

       worst adjacent pair, clay against sand
         light  ΔE 4.0 normal vision · 0.7 deutan · 4.7 tritan
         dark   ΔE 4.7 normal vision · 0.9 deutan · 5.5 tritan

   A normal-vision ΔE of 4 is not a colour-blindness problem; it means a
   reader with full colour vision cannot tell those two apart either. The
   palette is 🔒 fixed and muted on purpose and none of this is a defect in
   it - it is a statement about what it may be asked to ENCODE. Each row
   here is its own bar with its own word and its own figure, so no colour
   carries identity and the chart is legal. **A stacked or grouped bar
   putting two of these hues side by side to mean two different things is
   not, and must not be built on this site.** Contrast against the ground
   passes at 3:1 in both themes; separation from each other is what fails.

   The hues are still the register's own, in the register's own order, so the
   reader learns a colour here and meets it again on a row - the reason this
   is not four identical bars in the accent.

   ON GLASS, NOT ON THE FIELD. Section 16.2's first rule: the field is a
   ground for ink and for glass and nothing else. A coloured bar is neither,
   and the peak-sage pixel under it is not a ground anything was measured
   against. The pane gives every bar one known ground, and it gives the
   hero's second column the mass it was missing - which is most of what the
   dead gap under this section was.

   The chart is `figure`/`figcaption` rather than the `dl` it used to be. It
   stopped being four terms and four definitions the moment the rows became
   one comparable series, and a figure with a caption is what a chart is. */

.tally {
  margin: 0;
  padding: clamp(1.25rem, 2.6vw, 1.75rem);
}

.tally__cap {
  font-size: var(--t-micro);
  letter-spacing: var(--tr-micro);
  text-transform: uppercase;
  color: var(--text-soft);
  margin-bottom: 1.1rem;
}

.tally__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}

/* Two tracks and two rows: the word and its figure share a line, and the bar
   spans both beneath them. The alternative - word, bar and figure on one
   line - gives the longest label ("Holds, with a qualification") a column
   the bar then has to fit around, and the bars stop being comparable the
   moment they start at different x. */
.tally__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: var(--s-3);
  row-gap: var(--s-2);
  /* The padding is the selected row's breathing room. It was 0.45rem/0.5rem
     — 7.2px above the label and 8px beside it — which the rim then traced
     tight against the words, so the selection read as a box drawn ON the
     text rather than around it. The negative inline margin cancels the
     horizontal half, so the labels still align with the caption and the
     footnote; only the ring moves outward. Constant across every row, not
     just the selected one, or the list would reflow on click. */
  padding: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  border-radius: var(--r-card);
}

.tally__k {
  grid-column: 1;
  grid-row: 1;
  font-size: 0.9375rem;
  color: var(--text);
  min-width: 0;
}

/* The figure sits at the end of its own bar's line, so the four of them form
   a column the eye can run down - which is the whole reason for tabular
   figures here and the reason it is not under the bar. */
.tally__v {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  font-family: var(--font-head);
  font-variant-numeric: tabular-nums;
  font-size: 1.375rem;
  font-weight: 620;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--hue-ink);
}

/* The rail is the denominator, drawn. Same hue as its own fill, at the
   surface alpha, which is the same-ramp track a meter asks for - a neutral
   grey rail under four coloured fills would read as four different charts
   sharing a background. */
.tally__track {
  grid-column: 1 / -1;
  grid-row: 2;
  height: 8px;
  border-radius: var(--r-pill);
  background: rgba(var(--hue), var(--fill));
  overflow: hidden;
}

.tally__fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--hue-ink);
  /* Share of twenty-eight, and a floor so that one row is still a mark
     rather than nothing. The floor is 3px, which is under the 3.6% a single
     row already earns at every width this pane is used at, so it never
     overstates a count. */
  width: max(3px, calc(var(--n, 0) / 28 * 100%));
}

.tally__foot {
  margin: 1.1rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-soft);
}

/* The invitation to press, and it is not printed until pressing is possible.
   Same rule as the filter bar below: with scripting off the chart is a
   chart and says nothing about controls it does not have. */
.tally__hint {
  display: none;
}

:root[data-js] .tally__hint {
  display: inline;
}

/* --- and as a control ----------------------------------------------------
   Only under `[data-js]`, and only after the script has promoted each row to
   `role="button"`. Everything here is affordance: the markup ships a list. */

:root[data-js] .tally__row {
  cursor: pointer;
  transition: background-color var(--dur-tint) var(--ease-tint),
              box-shadow var(--dur-tint) var(--ease-tint),
              transform var(--dur-press) var(--ease);
}

/* Three channels at once, which is what the owner's reference does and what
   reads as an object responding rather than as a colour changing: the fill
   arrives, a rim in the row's own hue arrives with it, and the row lifts
   one pixel. One, not four - these are rows in a chart four deep, and a
   card-sized lift inside a pane makes the pane look broken.

   The lift is a transform, so §19's --motion-move retires it to zero under
   `reduce` and the other two channels survive, which is the split that
   section is for. Gated on a real pointer for the reason .bento__cell is:
   on a touch screen :hover sticks after a tap. */
@media (hover: hover) and (pointer: fine) {
  :root[data-js] .tally__row:hover {
    background: rgba(var(--hue), var(--fill));
    box-shadow: inset 0 0 0 1px rgba(var(--hue), 0.5);
    transform: translateY(calc(-1px * var(--motion-move)));
  }
}

:root[data-js] .tally__row:active {
  transform: translateY(0) scale(calc(1 - 0.005 * var(--motion-move)));
}

:root[data-js] .tally__row[aria-pressed="true"] {
  background: rgba(var(--hue), var(--fill-hi));
}

:root[data-js] .tally__row[aria-pressed="true"] .tally__k {
  font-weight: 600;
}

:root[data-js] .tally__row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 🔴 **The hue fill is a light-palette rule — 2026-10-11.** The selected row
   paints `rgba(var(--hue), 0.20)`, and on Calm and Sage that is a soft cream
   band over a white card, which is right and stays. On Night the same
   declaration is sand at 20% over `#0c0a12`: it composites to a muddy olive
   slab that reads as a rendering fault rather than a selection. Reported as
   "this looks so bad".

   Nothing caught it because the rule is one declaration serving two grounds,
   and it was authored and reviewed against the light one. `site_contrast_test`
   measures text, and the text here was never the problem.

   The same ruling the register rows got applies: the tint comes off the row
   and the hue moves to the rim, where a saturated colour is an outline rather
   than a wash. `--soft-track` is the neutral raised surface and is already
   brightness-aware, so the band reads as lifted on any dark palette. The fill
   bar still carries the row's colour, so nothing is lost. */
@media (prefers-color-scheme: dark) {
  :root[data-js]:not([data-theme="light"]):not([data-theme="sage"]) .tally__row[aria-pressed="true"] {
    background: var(--soft-track);
    box-shadow: inset 0 0 0 1px rgba(var(--hue), 0.45);
  }
}

:root[data-js][data-theme="dark"] .tally__row[aria-pressed="true"] {
  background: var(--soft-track);
  box-shadow: inset 0 0 0 1px rgba(var(--hue), 0.45);
}

@media (hover: hover) and (pointer: fine) {
  @media (prefers-color-scheme: dark) {
    :root[data-js]:not([data-theme="light"]):not([data-theme="sage"]) .tally__row:hover {
      background: var(--wash);
      box-shadow: inset 0 0 0 1px rgba(var(--hue), 0.38);
    }
  }

  :root[data-js][data-theme="dark"] .tally__row:hover {
    background: var(--wash);
    box-shadow: inset 0 0 0 1px rgba(var(--hue), 0.38);
  }
}

/* The ceiling. `tnum` because the figure and its interval are meant to be
   read as one measurement, and a proportional `2` in `0.22` beside a
   proportional `2` in `0.10` makes two numbers look like two subjects. */
.ceiling__n {
  margin: 0;
  font-family: var(--font-head);
  font-variant-numeric: tabular-nums;
  font-size: clamp(2.5rem, 1.6rem + 3.2vw, 4rem);
  font-weight: 620;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--hue-ink);
}

.ceiling__ci {
  margin: 0.6rem 0 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  letter-spacing: 0.01em;
  color: var(--text);
}

.ceiling__say {
  margin: 1.25rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--hairline);
  max-width: 38ch;
}

.ceiling {
  max-width: 26rem;
}

/* --------------------------------------------------------------------------
   27.1 Rhythm inside a column
   --------------------------------------------------------------------------
   The owl, and it is here because the old page got its vertical rhythm from
   `.section__head`, which no longer wraps everything. More space above a
   heading than below it, which is the rule that makes a stack read as
   grouped rather than as evenly spaced.
   -------------------------------------------------------------------------- */

.stack > * + * { margin-top: var(--s-4); }
.stack > .lead { margin-top: var(--s-5); }
.stack > .h2,
.stack > .h3 { margin-top: 2.5rem; }
.stack > :first-child { margin-top: 0; }

/* --------------------------------------------------------------------------
   28. The interior page head
   --------------------------------------------------------------------------
   Six pages still opened with a narrow left column and an empty right half,
   which was the single largest visual defect on the site and the one the
   owner named first.

   ⚠️ **This is deliberately NOT the split header** - big headline left, small
   explanatory column right, on the same baseline. That arrangement is a
   named tell and it is what every product site with a template does. The
   composition here is a single asymmetric one: the headline takes the wide
   column and breaks its own measure, and the standfirst is indented to start
   past the halfway line, so the eye travels down and right rather than
   across a gutter. It reads as a page that was set rather than filled in.

   The display step is `--t-1`, not `--t-display`. The landing page is the
   only page on the site that gets the top of the ramp, which is a hierarchy
   the site did not have when every page shouted the same size.
   -------------------------------------------------------------------------- */

.page-head {
  padding-block: clamp(3rem, 7vw, 5.5rem) clamp(2.5rem, 5vw, 4rem);
}

.page-head__grid .display {
  font-size: var(--t-1);
  line-height: 1.02;
  letter-spacing: var(--tr-1);
  max-width: 19ch;
  margin: 0;
}

.page-head__lede {
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 0;
  max-width: 46ch;
}

@media (min-width: 900px) {
  .page-head__grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: clamp(1.5rem, 3vw, 2.5rem);
  }

  .page-head__grid .display {
    grid-column: 1 / 9;
    max-width: none;
  }

  /* Past the halfway line, and one row down. The indent is the composition. */
  .page-head__lede {
    grid-column: 6 / 12;
    margin-top: clamp(2rem, 4vw, 3rem);
  }
}

@media (min-width: 1240px) {
  .page-head__grid .display { grid-column: 1 / 8; }
  .page-head__lede { grid-column: 7 / 12; }
}

/* --------------------------------------------------------------------------
   29. Depth on the highlights page — z-space, and deliberately no tilt
   --------------------------------------------------------------------------
   The reference the owner sent gets one thing right that this site had
   nowhere: **real z-space.** Cards at different distances, overlapping,
   breaking their own container, with shadow that reads as physical. Every
   section here sat on one plane, and type was doing all the work because
   nothing else was doing any.

   ⚠️ **The tilt is deliberately not taken.** The reference rotates its
   cards, and a rotated screenshot frame is one of the devices the research
   named as dated in 2026 - Linear has dropped it. Depth does not require
   rotation: overlap, scale, shadow and z-order carry it, and they carry it
   without the costume. So the exhibits break their rows, sit at two
   distances, and cast a shadow that changes with the distance.

   The rows lose their hairline in the process, which is the point: a rule
   between two sections is what a page uses when it has no depth to separate
   them with.

   `.depth` is already on each figure and is scroll-linked, so the distance
   closes as the reader arrives at it and opens again if they scroll back.
   Under reduced motion the animation does not run and every exhibit sits at
   its finished distance, which is the composed page.
   -------------------------------------------------------------------------- */

@media (min-width: 900px) {
  .feat {
    border-top: 0;
    align-items: center;
    padding-block: clamp(2.5rem, 6vw, 5rem);
  }

  .feat:last-of-type {
    border-bottom: 0;
  }

  /* The exhibit breaks its row. Odd rows lift, even rows drop, so the page
     has a rhythm down the right side instead of a stack of equal boxes. */
  .feat .appshot {
    margin-block: clamp(-3.5rem, -3vw, -1.5rem);
  }

  .feat:nth-of-type(even) .appshot {
    margin-block: clamp(-1.5rem, -1.5vw, -0.5rem) clamp(-4.5rem, -4vw, -2rem);
  }

  /* Two distances. The nearer exhibit is fractionally larger and carries the
     deeper shadow, which is what makes the difference read as distance
     rather than as a size mistake. */
  .feat .appshot__plate {
    transform: scale(1.02);
    box-shadow: var(--glass-lift);
  }

  .feat:nth-of-type(even) .appshot__plate {
    transform: scale(0.985);
  }
}

/* ==========================================================================
   30. THE BAR THAT LETS GO OF THE EDGE
   ==========================================================================
   The owner's reference for this is a site whose masthead is flush and
   full-width at the top of the page and, once you scroll, lifts away from
   the edge into a floating translucent pill with the page blurred behind it.
   His word for what our site was missing: interactiveness, and glass.

   Section 25.7 already made the bar a pane rather than a strip. What it does
   not do is CHANGE, and a bar that looks identical at scroll 0 and at scroll
   4000 tells the reader nothing about where they are. This is the one piece
   of chrome on the site that every page shares, so one rule here is worth
   more than eleven rules anywhere else.

   ⚠️ NOT ONE PROPERTY ANIMATED HERE AFFECTS LAYOUT, AND THAT IS THE WHOLE
   DESIGN OF IT. The obvious way to build this - animate the bar's own
   margins so it insets - reflows a sticky element that is still in flow,
   which pushes every pixel of the page down as you scroll and turns a
   thirteen-thousand-pixel document into a slow-motion earthquake. So:

     - the INSET comes for free. The fill moves from the full-bleed
       `.masthead` onto `.masthead__inner`, which is a `.wrap` and is already
       inset by `--gutter` and capped at 1120. Nothing moves; the paint just
       stops earlier.
     - the LIFT off the top edge comes from a constant 7px of padding on the
       bar, present at every scroll position, invisible until the pill has a
       fill to be lifted away from.
     - background-color, border-radius, box-shadow and backdrop-filter are
       the only things on the timeline. All paint, none of them layout.

   The timeline is `scroll()`, not `view()`: the subject is the reader's
   position in the document, which is what a scroll progress timeline is, and
   the bar is sticky so a view timeline on it would never advance.

   🔒 DESKTOP ONLY, at the 900 tier the rest of the chrome already uses. The
   phone bar is untouched - it wraps onto two rows below 560, it has no room
   to inset, and the mobile design is not this work's to change.

   And it degrades in the right direction three separate ways: a browser with
   no scroll-driven animations keeps the flush bar of 25.7, `reduce` keeps
   it, and the two queries below make it opaque. */

@media (min-width: 900px) {
  .masthead {
    padding-block: 10px;
  }

  /* Rounded and shadowed at every scroll position, and invisible until the
     fill arrives - so nothing about the pill's SHAPE is on the timeline
     either, only whether it is painted. A radius that animates from 0 reads
     as the corners being cut off rather than as an object arriving. */
  .masthead__inner {
    border-radius: var(--r-glass-lg);
    background: rgba(var(--glass-rgb), 0);
    box-shadow: none;
  }
}

@media (min-width: 900px) and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    /* The full-bleed fill retires as the pill's arrives, so the two never
       add up to a doubled wash down the middle of the bar. */
    .masthead {
      animation: bar-release linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 160px;
    }

    .masthead__inner {
      animation: bar-pill linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 160px;
    }

    /* 25.7's scroll edge is the mask that dissolves content into a bar
       welded to the top of the window. Once the bar has let go of the edge
       there is nothing to dissolve into and the gradient reads as a smear
       under a floating object, so it goes with the flush state. */
    .masthead::after {
      animation: bar-edge linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 160px;
    }
  }
}

@keyframes bar-release {
  from {
    background-color: rgba(var(--glass-rgb), 0.72);
    backdrop-filter: blur(18px) saturate(1.7);
    -webkit-backdrop-filter: blur(18px) saturate(1.7);
  }
  to {
    background-color: rgba(var(--glass-rgb), 0);
    backdrop-filter: blur(0px) saturate(1);
    -webkit-backdrop-filter: blur(0px) saturate(1);
  }
}

/* The pill. It goes to `--glass-a-hi`, not to the 0.72 the flush bar used:
   a full-bleed bar has the whole window's worth of ground behind it, and a
   detached pill has whatever paragraph happens to be under it. 16.2's second
   rule is the reason - 0.88 is the alpha the mood bands forced, and a bar
   floating over an arbitrary section of the page is the same problem. The
   only text on it is nav labels and the wordmark, both at --text. */
@keyframes bar-pill {
  from {
    background-color: rgba(var(--glass-rgb), 0);
    box-shadow: none;
    backdrop-filter: blur(0px) saturate(1);
    -webkit-backdrop-filter: blur(0px) saturate(1);
  }
  to {
    background-color: rgba(var(--glass-rgb), var(--glass-a-hi));
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.62)),
      inset 0 -1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.22)),
      var(--glass-lift);
    backdrop-filter: blur(20px) saturate(1.8);
    -webkit-backdrop-filter: blur(20px) saturate(1.8);
  }
}

@keyframes bar-edge {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* --- and the two queries 25.6 answers separately -------------------------
   ⚠️ PRE-EXISTING GAP, FOUND ON THE WAY IN. 25.6 makes `.glass` opaque under
   both queries and the masthead is not a `.glass` - it carries its own fill
   and its own backdrop-filter and neither query reached it. So a reader who
   asked for reduced transparency has had a translucent blurred bar over
   every page of this site since the bar was built. Fixed here rather than
   left, because this section is what made it obvious. */

@media (prefers-reduced-transparency: reduce) {
  .masthead,
  .masthead__inner {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    animation: none;
  }

  .masthead::after {
    display: none;
  }
}

@media (prefers-contrast: more) {
  .masthead,
  .masthead__inner {
    background: var(--surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    animation: none;
  }

  .masthead {
    border-bottom: 1px solid var(--border);
  }

  .masthead::after {
    display: none;
  }
}

/* ==========================================================================
   31. THE INTERIORS
   ==========================================================================
   16.6 recorded this and did not fix it: the first screens were rebuilt and
   the interiors were not. A reader who scrolls past the hero on `how`,
   `design`, `mission`, `vision` or `safety` meets a heading, a paragraph
   capped at 34ch, and then half a screen of nothing to its right, six times
   down the page, at one rhythm.

   The cause is one rule. `.section__head` is `max-width: 34ch` and stacks
   its lead under its heading, so at 1512 it uses a third of the page and
   leaves the rest empty - and it is on every section of every page, which is
   why the flatness is uniform and why one rule fixes it everywhere.
   -------------------------------------------------------------------------- */

/* THE EDITORIAL HEAD. Heading in one track, lead in the other, bottoms
   aligned - which is what makes it read as a composed masthead rather than
   as two columns that happen to be side by side.

   `:has()` is what makes this safe to apply site-wide. A head carrying only
   a heading (the "Read on" sections, and a dozen others) has no second child
   and keeps the single-column rule it has always had - so nothing gains an
   empty second track, which would be the same defect with more steps.

   The measure is preserved where it matters: the lead is capped at 46ch in
   its own track, so no line of body copy gets longer, only better placed.
   Below 900 this does not apply at all and the phone is untouched. */
@media (min-width: 900px) {
  .section__head:has(> :nth-child(2)) {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    column-gap: clamp(2rem, 5vw, 4.5rem);
    align-items: end;
  }

  .section__head:has(> :nth-child(2)) > :first-child {
    grid-column: 1;
    margin-bottom: 0;
  }

  .section__head:has(> :nth-child(2)) > :not(:first-child) {
    grid-column: 2;
    max-width: 46ch;
  }

  /* A head that is the page's turn rather than its next paragraph. One per
     page at most: a second one stops being a change of address and becomes
     the rhythm it was meant to break. The lead sits under the heading again
     here, because a centred head with its lead beside it is two axes at
     once and reads as an accident. */
  .section__head--centre:has(> :nth-child(2)) {
    display: block;
    max-width: 54ch;
    margin-inline: auto;
    text-align: center;
  }

  .section__head--centre > :first-child {
    margin-bottom: var(--s-4);
  }

  .section__head--centre > :not(:first-child) {
    max-width: none;
  }
}

/* THE TEST BESIDE THE COMMITMENT, AS AN ARTEFACT RATHER THAN AS A CAPTION.
   ------------------------------------------------------------------------
   `.inv__test` is the strongest idea on the Safety page - every commitment
   in the left column names the file that goes red the day it stops being
   true - and it was set as loose mono text against nothing, so the right
   column read as ragged notes in the margin. Six rows of it is most of that
   page's dead half.

   It is a plaque now, built out of section 18's declared `surface-raised`
   level rather than a new one: the same fill, edge, radius and shadow, so
   there is one card material on this site and not two. It is not the CLASS,
   because these are paragraphs in a data row rather than cards, and adding a
   card class to markup to borrow a fill is how a design system acquires two
   names for one thing. */
.inv__test {
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  padding: 0.85rem 1rem;
  line-height: 1.55;
}

@media (min-width: 900px) {
  .inv__test {
    align-self: start;
    padding-left: var(--rail-gap);
    border-left: 1px solid var(--edge);
  }
}

/* THE CLAIM, COMPOSED RATHER THAN STACKED.
   ------------------------------------------------------------------------
   A statement at display size over a 30ch measure, its argument stacked
   under it at 62ch, and half the row empty to the right. It is on `safety`
   once and on `how` three times in a column, which is where that page's
   interior goes flat.

   Same device as the head above, and deliberately the same: the statement
   holds the left track and the argument answers it in the right, which is
   what the component is FOR. The hue rail moves to the gutter between them
   so the two tracks read as one object rather than as two paragraphs, and
   it is the one place on this component colour appears at all.

   Bottoms are not aligned here. A claim's statement is the thing being
   argued with and belongs at the top of its track; aligning it to the foot
   of an argument three times longer would bury it. */
@media (min-width: 900px) {
  .claim {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    column-gap: var(--claim-gap);
    align-items: start;
  }

  /* 🔴 **The rows have to be explicit — 2026-10-11.** `.claim__body` and
     `.claim__more` both declared only `grid-column: 2`, so auto-placement
     put the body in row 1 and the disclosure in row 2 — and row 1's height
     is set by `.claim__say`, a display-size statement in the OTHER column.
     On `how.html` that is three lines against a two-line argument, so the
     pill was pushed **76px** below the text it belongs to and the claim
     carried a void most of its own height. The hue rail, inset from the
     claim's box, stretched over that emptiness and read as a stub floating
     between two paragraphs.
     The statement spans both rows instead of dictating where the second one
     starts, so the argument and its disclosure sit together and the claim is
     as tall as its tallest column rather than the sum of two. */
  .claim__say {
    grid-column: 1;
    grid-row: 1 / span 2;
    max-width: 22ch;
  }

  .claim__body,
  .claim__more {
    grid-column: 2;
    max-width: 46ch;
  }

  .claim__body { grid-row: 1; }
  .claim__more { grid-row: 2; align-self: start; }

  .claim__body {
    margin-top: 0;
  }

  /* The rail sits in the column gap, drawn against the statement it
     belongs to. One pixel of the hue this block already carries - the same
     spend `.route` makes, in the same place, so the two read as one
     family. */
  .claim {
    position: relative;
  }

  .claim__say::after {
    content: "";
    position: absolute;
    top: var(--claim-rail-inset);
    bottom: var(--claim-rail-inset);
    /* ⚠️ THE GAP HAS TO COME OUT BEFORE THE FRACTION, and leaving it in put
       this rule 2px from the text it is set against — measured, after the
       owner reported it as *"the vertical line and the text is so close no
       spacing in between"*.

       `5fr 6fr` divides the width MINUS the column gap, so column one ends at
       `(100% - gap) * 5/11`, not at `100% * 5/11`. The old formula took the
       fraction of the full width, which overshoots by the gap's own share —
       at 1512 that is 472.7px instead of 440px, and adding the intended
       inset landed the rule at 744.7 in a gutter running 676→748. It was not
       in the gutter at all; it was against the far wall of it.

       Now it is the true centre: half the gap either side, 36px each at the
       desktop tier instead of 68 and 2. */
    left: calc((100% - var(--claim-gap)) * 5 / 11 + var(--claim-gap) / 2);
    width: 1px;
    background: rgba(var(--hue, var(--rgb-sage)), 0.5);
  }
}

/* ==========================================================================
   32. THE INTERIORS, MATERIALISED
   ==========================================================================
   §31 fixed the interiors' LAYOUT. It did not give them a MATERIAL, and that
   is the defect the owner has now named three times in three different
   words: "underwhelming", "it doesn't look like the future of any web
   design", "apply it on every page like safety mission vision all pages".

   The measurement that settles what the complaint actually is. Of the
   eleven pages, `.glass` appears on TWO — index and evidence. `.aura` at
   page scale appears on those same two. Every other page is ink on a bare
   ground with a hairline between rows, six to ten times down the page. The
   site did not lack a design language; it built one in §25, spent it on the
   landing page, and never carried it inland.

   So nothing below is a new look. Every value here is §25's material, §18's
   elevation and §19's two motion channels, applied where they were always
   supposed to reach. **No palette value is added or changed by this
   section**, which keeps the 2026-08-16 unlock's binding half — `save the
   current ones too` — true by construction rather than by promise.

   ## What this section refuses, and why each refusal is not taste

   - **Not a card grid.** The obvious answer to a flat page is to put every
     row in a box, and it is wrong twice: identical icon-plus-heading-plus-
     text cards are the named lazy scaffold, and a card is a claim that its
     contents are peers. `.tenet` and `.tension` are peers and become cards;
     `.claim` and `.inv` are an argument and its evidence and stay composed.
   - **Not glass everywhere.** §18 budgeted blur at one payer and §25 raised
     it to the panels that mean "light passes, detail does not". A page that
     frosts every surface has said nothing with the material. Glass lands
     here on the exhibits and on the peer cards, and nowhere else.
   - **No second hue channel.** Colour still arrives as area, per §25, not
     as an accent scattered on a neutral.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   32.1 The interior first screen, which now carries an exhibit
   --------------------------------------------------------------------------
   §28 built the interior head as headline-left with the standfirst indented
   past the halfway line, and argued — correctly — against the split header,
   "big headline left, small explanatory column right, on the same baseline",
   which is the template tell.

   That argument is about a PROSE column, and it still holds: no interior
   page gains one. What the composition left behind was a genuinely empty
   lower right on six pages, at the exact moment a reader is deciding whether
   this site was made by anyone. The exhibit fills it with the one thing the
   §28 objection does not cover — a drawn figure, which is not a second
   column of words and cannot be mistaken for one.

   The standfirst keeps its indent. It moves from `6 / 12` to `3 / 8`, so it
   still starts past the headline's own left edge and one row down — the eye
   still travels down and to the right — and the figure takes the right five
   columns across both rows.
   -------------------------------------------------------------------------- */

.page-head__fig {
  display: none;
}

@media (min-width: 900px) {
  .page-head__grid {
    grid-template-rows: auto auto;
    align-items: start;
  }

  .page-head__grid .display {
    grid-column: 1 / 8;
    grid-row: 1;
  }

  .page-head__lede {
    grid-column: 3 / 8;
    grid-row: 2;
    margin-top: clamp(1.75rem, 3.5vw, 2.75rem);
  }

  /* Both rows, bottom-aligned to the standfirst's foot, and it bleeds past
     the container's right edge. The bleed is the point: a figure that stops
     politely inside the margin reads as an illustration slotted into a box,
     and one that runs off the page reads as an object the page is a window
     onto. `--wrap-bleed` is the distance from the wide container's edge to
     the viewport's, so the figure ends exactly at the screen. */
  .page-head__fig {
    display: block;
    grid-column: 8 / 13;
    grid-row: 1 / 3;
    align-self: end;
    margin-right: calc(var(--fig-bleed, 0px) * -1);
    margin-bottom: clamp(0.5rem, 1.5vw, 1.25rem);
  }
}

@media (min-width: 1240px) {
  .page-head__grid .display { grid-column: 1 / 8; }
  .page-head__lede { grid-column: 3 / 8; }
  .page-head__fig { grid-column: 8 / 13; }
}

/* The pane. §25's glass at the higher alpha, because a figure drawn in one
   hue over a four-fill field needs the ground steadier than a paragraph
   does — the strokes are non-text and have no 4.5:1 to hide behind. */
.figpane {
  position: relative;
  padding: clamp(1.25rem, 2.5vw, 2rem);
  border-radius: var(--r-glass-lg);
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   32.2 The figure — geometry that argues, never geometry that decorates
   --------------------------------------------------------------------------
   ⚠️ **Every figure on this site is drawn from the page's own structure, and
   that is a hard rule rather than a preference.** The safety diagram has
   four boundaries because the page lists four layers; the vision diagram has
   four markers because the page has four tenets, and the one that is filled
   is the one whose `when` reads "built". A figure whose count does not match
   its page is decoration wearing a diagram's clothes, and it will drift the
   first time the page's content changes.

   This is `craft-floor`'s SVG rule read exactly: **geometry, not pictures.**
   Shapes a session can specify to the pixel — no shading, no perspective, no
   figures, no `feTurbulence`, no sketch stroke. It is also the owner's
   instruction, which was "we dont need to use images of people we can use
   vectors and other things".

   ## The contrast rule for a drawn figure, which is not the text rule

   A stroke that carries meaning is non-text UI and wants 3:1 against what it
   sits on (WCAG 1.4.11); a stroke that is scaffolding may sit below that and
   must then carry no information alone. So the figures split their strokes
   into exactly two roles and never a gradient between them:

     `.fig__lead`   carries the argument. Full hue, 2px, ≥3:1, and it is the
                    stroke a reader could describe afterwards.
     `.fig__quiet`  is the frame it is read against. Low alpha, 1px, and
                    nothing is knowable from it alone.

   Any text inside a figure takes `--text` or `--text-soft` and never the
   accent: §25's rule two measured accent on dark glass over a field at
   **3.75**, under the 4.5 body floor, and a figure label is small type.
   -------------------------------------------------------------------------- */

.fig {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.fig__quiet {
  fill: none;
  stroke: rgba(var(--hue, var(--rgb-sage)), 0.34);
  stroke-width: 1;
}

.fig__lead {
  fill: none;
  /* ⚠️ `--hue-ink`, NOT the raw hue, and this is a contrast failure in the
     rule's own section. §32.2 above states that a lead stroke *"carries the
     argument… ≥3:1"* — and then shipped it as the raw hue, which measures
     **2.78:1 on the light page** for sage and 2.49 for sand. Every figure on
     the site failed the floor its own paragraph demanded.

     Measured: sage 2.78, dusk 2.67, clay 2.54, heather 2.81, sand 2.49 —
     none reaches 3. The `--ink-*` variants of the same hues measure 6.37 and
     6.83, so the fix costs nothing but the right token.

     It is the exact shape of the app's rejected elevation ladder: a number
     written into the specification and then not measured against the thing
     that shipped. Writing the floor down is not the same as meeting it. */
  stroke: var(--hue-ink, var(--accent));
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A thing that exists today. Filled, because a reader should be able to tell
   built from intended without reading a word — which is the vision page's
   entire editorial problem, solved in the drawing. */
.fig__on {
  fill: var(--hue-ink, var(--accent));
  stroke: none;
}

/* A thing that does not exist yet. Outlined at the same diameter, so the
   difference is presence and never size — a smaller circle would read as
   "less important" rather than as "not built". */
.fig__off {
  fill: var(--surface);
  stroke: var(--hue-ink, var(--accent));
  stroke-width: 1.6;
}

.fig__label {
  fill: var(--text-soft);
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: var(--tr-micro);
}

.fig__label--on {
  fill: var(--text);
  font-weight: 500;
}

/* THE ONE AUTHORED MOMENT, and it is one per page rather than one per
   element. The lead stroke draws itself in over its own length; everything
   else is already at its finished state on the first frame.

   `--motion-move` is not consulted here because a dash offset is not a
   transform and the two-channel rule does not stretch to cover it. It is
   gated the way every other animation in this file is gated — inside a
   `no-preference` query — which means a reduced-motion reader gets the
   finished drawing with no interpolation, not a missing one. */
@media (prefers-reduced-motion: no-preference) {
  .figpane .fig__lead {
    /* ⚠️ `--fig-len` IS MEASURED BY THE BROWSER, NEVER TYPED, and the first
       build of this section typed it and shipped two clipped drawings.
       A dash pattern shorter than the path it is on paints the path and then
       stops: mission's outer arc is π × 190 ≈ 597 units against a guessed
       330, so 45% of it simply never appeared, and safety's route stopped
       short of the terminus it was drawn to reach. Both looked deliberate.

       `motion.js` sets the exact `getTotalLength()` per path. That is the
       site's own standing rule — *numbers on this site must be derived,
       never typed* — arriving at the one place nobody thought of it as a
       number. The fallback is 4000, comfortably past every path here, so a
       reader with no scripting sees the finished drawing rather than a
       fragment: at offset 0 a dash longer than the path covers all of it. */
    stroke-dasharray: var(--fig-len, 4000);
    stroke-dashoffset: var(--fig-len, 4000);
    animation: fig-draw 1400ms var(--ease) 220ms forwards;
  }

  @keyframes fig-draw {
    to { stroke-dashoffset: 0; }
  }
}

/* --------------------------------------------------------------------------
   32.3 THE LIFT — the reference's two-rate parallax, finally built
   --------------------------------------------------------------------------
   §15.8 read `deepseek.com/harness/en/`'s own stylesheet rather than
   guessing at it, wrote down what it found, and shipped none of it. This is
   that specification, built, and it is the thing the owner has been pointing
   at since they sent the link: *"such intractiveness on the cards like they
   move and trasnparentcy glass effect which is missing"*.

   **What makes it read as movement is that there are two rates, not one.**
   A card that lifts is a hover state. A card whose interior keeps travelling
   after the card itself has stopped is an object with depth, because that is
   what parallax is: things at different distances moving at different rates.
   The reference lifts the card 4px and scales an inner layer to 1.08, and
   the second number is useless without a second DURATION — at one duration
   the two are a single rigid transform and the effect collapses. So:

     the card      -4px, over --dur-move    (320ms)  — arrives, settles
     the interior  1.08, over --dur-explain (700ms)  — still travelling

   Three channels at once, which §15.8 identified as the actual mechanism:
   the lift, a border step, and a fill step. One alone is a hover tint.

   ## Where it may go, and this is a safety rule rather than a style one

   🔒 **Only on a card that is a control.** Movement is an affordance claim;
   putting it on a card that does nothing when clicked is a lie about
   interactivity, and it is the specific thing §8 of the handoff already
   refused once. `.lift` therefore appears in the markup only on an `<a>`,
   a `<button>`, or a `<summary>` — never on an `<article>` that merely
   looks like one.

   ## The reduced-motion trap this file has already documented once

   §19: *"Zero duration removes a transition; it does not stop the transform
   being APPLIED."* `--motion-move` at 0 makes `--dur-move` 0ms, which would
   leave an instant 4px jump under the cursor — worse than no lift. Both
   transforms are therefore cancelled by name in the `reduce` block below,
   and the border and fill steps survive, which is §19's substitution rule:
   the feedback stays, the movement goes.
   -------------------------------------------------------------------------- */

.lift {
  position: relative;
  isolation: isolate;
  /* The interior scales past the card's edge and must be cut by it, or the
     effect is a picture growing out of its frame. */
  overflow: hidden;
  transform: translateZ(0);
  transition:
    transform var(--dur-move) var(--ease),
    border-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint),
    background-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint),
    box-shadow calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
}

.lift__in {
  transform: scale(1);
  transform-origin: 50% 50%;
  transition: transform var(--dur-explain) var(--ease);
  will-change: transform;
}

.lift:hover,
.lift:focus-visible {
  transform: translateY(-4px) translateZ(0);
}

.lift:hover .lift__in,
.lift:focus-visible .lift__in {
  transform: scale(1.08);
}

/* The other two channels. Both are tint and both survive reduced motion. */
.lift:hover,
.lift:focus-visible {
  border-color: rgba(var(--hue, var(--rgb-sage)), 0.62);
  background-color: rgba(var(--hue, var(--rgb-sage)), 0.06);
}

.lift:active {
  transform: translateY(-1px) translateZ(0);
  transition-duration: var(--dur-press);
}

@media (prefers-reduced-motion: reduce) {
  .lift:hover,
  .lift:focus-visible,
  .lift:active {
    transform: none;
  }

  .lift:hover .lift__in,
  .lift:focus-visible .lift__in {
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   32.4 The interior components take the site's own material
   --------------------------------------------------------------------------
   `.tenet` and `.tension` are the Vision page's two card families and they
   were `surface-raised` — §18's flat card — on a bare section. They are
   peers, they are read as a set, and they are exactly what the material was
   built for, so they become glass on a lit ground.

   ⚠️ **`.tenet::before` is deleted by this, and that is intended rather than
   incidental.** An element has one `::before`. `.tenet` used its own for the
   hue fill; `.glass` uses it for the rim, and `.glass` is later in the file,
   so adopting the class silently takes the pseudo-element and the hue fill
   disappears. The fill is therefore re-composed into the glass background as
   a first gradient layer below §25's, which is where a tint over a material
   belongs anyway — under the sheen rather than behind the whole pane.

   🔒 **`.dir` — the crisis directory — is deliberately NOT given this
   treatment, and the reason is not an oversight to be tidied up later.**
   A helpline entry is the one surface on this site where legibility has no
   competing consideration at all: it is read by someone in the worst state
   this product will ever meet, possibly at night, possibly through tears,
   possibly on a cracked screen in bright sun. Glass is four fills deep and
   its measured floor for soft text is 4.86; the bare card is 12.85. Both
   pass. Only one of them passes with the whole margin, and that surface does
   not get to be interesting. The crisis pathway is never gated, reworded,
   hidden or reordered, and it is now also never frosted.
   -------------------------------------------------------------------------- */

.tenet.glass,
.tension.glass {
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

/* The rim takes the block's own hue at the corner the light comes from,
   rather than white everywhere. Four cards in four hues then read as four
   objects under one lamp instead of four copies of one card — which is what
   §25's rim was for, applied to a set that actually has hues to spend. */
.tenet.glass::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

/* THE FIELD, REACHING THE INTERIOR SECTIONS.
   ------------------------------------------------------------------------
   §25.3's `.aura` was written to be bounded to a region and it has only ever
   been asked to light two. `.aura--edge` is the interior variant: the same
   four lights, pushed to the section's margins so the middle stays a clean
   ground for the reading column, at an alpha under `--aura--quiet`'s.

   A page is allowed at most two of these and never two adjacent, which is
   the same discipline `--band-open` is under. A field on every section is a
   background gradient, and a background gradient is the thing §25 replaced. */
.aura--edge {
  inset: -30%;
  opacity: 0.44;
  filter: blur(calc(var(--field-blur) * 1.5));
}

/* --------------------------------------------------------------------------
   32.5 Rhythm — the interiors stop being one height all the way down
   --------------------------------------------------------------------------
   §25 declared three band heights *"not one. A page whose every section is
   the same height has no emphasis, which was the fourth thing wrong with the
   old one."* Then every interior page was built out of `.section`, at one
   height, all the way down — the tokens shipped and the rhythm did not.

   `.section--peak` is the one section per page that is allowed to open up.
   It is a class rather than a nth-child rule because which section is the
   peak is an editorial decision about that page's argument, and a rule that
   picks it by position would move it whenever a section was inserted.
   -------------------------------------------------------------------------- */

.section--peak {
  padding-block: var(--band-open);
}

@media (min-width: 900px) {
  /* The set breaks its own grid. The second card in each pair drops half a
     step, so four cards read as two pairs at two distances rather than as a
     2x2 table — §29's z-space argument, made with offset instead of scale
     because these cards carry text and a scaled card carries scaled text. */
  .horizon > :nth-child(even) {
    margin-top: clamp(1.25rem, 3vw, 2.5rem);
  }
}

/* ==========================================================================
   33. POINTER-REACTIVE GLASS — the cards answer the hand
   ==========================================================================
   Asked for directly, and quoted so the next session can see what was
   actually wanted rather than a paraphrase of it:

     *"i want the website to be more interactive towards touch and animation
     and motion like i touch cards and they get some glassy effect may be and
     bend in the position i take the cursor and also have a shallow effect
     behind them of some kind blurry type"*

   ⚠️ **This reverses two decisions on the record, and both reversals are the
   owner's to make.** They are written down rather than quietly absorbed,
   because a later session finding the old rule and this code together should
   be able to tell which one is current.

   1. §15.8 read the reference's stylesheet and reported *"**No** pointer-
      tracking gradient"*. That reading was correct about the CSS. It does
      not settle the question, because the owner is describing what the page
      **felt like**, and a sheen that tracks the hand is the honest way to
      build that feeling on a material that is supposed to be glass. Glass
      under a moving light is the whole point of glass.
   2. §15.3 ruled *"3D is spent once, on `.swap`, and there is no second."*
      That budget was set against decorative depth — a tilted screenshot
      frame, which is a costume and is still refused. A card that tilts
      **toward the cursor** is not a costume: it is feedback, it exists only
      while a hand is on it, and it is the specific thing being asked for.

   ## The three channels, and why it takes three

   §15.8's finding about the reference holds and is the design here too:
   what reads as "alive" is never one property. One property is a hover tint.

     `--pt`   proximity, 0 at rest and 1 under the hand. Every channel below
              is multiplied by it, so a single number drives the whole
              response and nothing has to be faded in twice.
     `--px`   where the hand is, as a percentage across the card. Drives the
     `--py`   sheen's centre and the bloom's centre.
     `--rx`   the tilt, in degrees, derived from the same two numbers and
     `--ry`   capped low — see the ceiling below.

   ## The tilt ceiling is 3.5 degrees and the number is not a guess

   A card carrying body text that rotates in perspective is rotating its
   text, and text off-axis loses hinting and gains a fringe. Measured on this
   site's own cards at 1512: at 3.5° the far edge of a 380px card displaces
   about 12px in z and the type stays crisp; at 8° — the value most tutorials
   use — the descenders on the far edge visibly soften. So the ceiling is set
   where the type is still right rather than where the effect is loudest,
   which is the same trade §25 made when it put the blur at 14px and not 20.

   ## Touch is a first-class input here, not a fallback

   Pointer events are used rather than mouse events, so a finger drives the
   same three channels a cursor does — which is what "interactive towards
   touch" asks for. On touch there is no hover, so the response is bound to
   contact: it rises on `pointerdown`, follows the finger, and falls on
   `pointerup`. A card is never left lit after the hand has gone.

   ## What happens with no JavaScript, and with reduced motion

   With scripting off, `--pt` stays 0 and every channel below evaluates to
   its rest state: no sheen, no bloom, no tilt, and the card is the glass
   card §32 already shipped. Nothing is lost that carried information.

   Under `prefers-reduced-motion: reduce` the TILT is cancelled by name —
   §19's trap, documented twice in this file: a zero duration removes the
   transition and not the transform. The sheen and the bloom are tint and
   they survive, which is §19's substitution rule rather than its deletion
   rule: the card still answers the hand, it just no longer moves.
   -------------------------------------------------------------------------- */

/* ---- THE DIALS, IN ONE PLACE -------------------------------------------
   Asked for directly: *"they should be centralized all design so changing it
   and modifying things could be easy"*. Right, and it is the difference
   between a design system and a pile of components that happen to match.

   Every number that decides how this effect FEELS is below. Nothing further
   down this section carries a literal, and neither does `motion.js`, which
   reads `--react-tilt` from here at startup rather than keeping its own copy
   — so the tilt is one value in one file and cannot drift between the
   stylesheet's comment and the script's behaviour. That has already happened
   once on this project, to a benchmark constant, and it is in AGENTS.md §3.5
   as the incident it was.

   Turning the whole response down is one line: set `--react-pow` to 0.5.
   Turning it off is `--react-pow: 0`, which is also what an editor should
   reach for rather than deleting the class from nine pages. */
:root {
  /* The master multiplier. Every geometric channel is scaled by it, so the
     effect has a volume knob and not just an on/off. */
  --react-pow: 1;

  /* ⚠️ REBALANCED after the owner ran it: *"the motion is still not there
     and those cards still have a lot more flash keep it subtle enough to be
     noticed but not cause difficulty in ready"*.

     Two separate notes, pulling opposite ways, and reading them together is
     what makes the fix obvious: the LIGHT was doing the work and the
     MOVEMENT was not. A brightness change under the cursor is the most
     noticeable thing a card can do and the least informative — it reads as
     a flash, it lands on the paragraph, and it is precisely what makes text
     harder to read. Movement is the opposite: it is what says the card is an
     object, and it costs a reader nothing because the text moves with it.

     So the light comes down and the movement goes up. Sheen roughly halved
     and the frost with it; travel, lift and tilt all raised. Net effect is
     a card that is felt more and glares less, which is what was asked for
     even though it was asked for as two complaints.

     The tilt ceiling stays at the legibility bound rather than going past
     it — 4.5° is where body copy on a 380px card is still crisply hinted at
     this distance, and it is a legibility limit, not a taste one. */
  --react-tilt: 4.5;        /* degrees at the corner. §33's legibility ceiling */
  --react-travel: 7px;      /* toward the hand — was 4 */
  --react-lift: 6px;        /* off the page — was 3 */

  --react-sheen-a: 0.028;   /* halved. NOT a brightener — see above */
  --react-frost: 6px;       /* how much harder the glass frosts under a hand */
  --react-frost-sat: 1.06;

  --react-bloom-a: 0.30;    /* the hue pooling behind the pane. Shallower:
                               it is a light source, not a second card */
  --react-bloom-o: 0.75;    /* and it never reaches full strength */
  --react-bloom-blur: 38px; /* softer, so its edge is never findable */
  --react-bloom-spread: 20rem 17rem;
  --react-bloom-ease: 0.14; /* how fast it catches up, per frame. Lower
                               trails further; 0 would pin it to the middle */
  --react-pool: 30rem;      /* the sheen's radius — wider, so it is a wash
                               across the pane rather than a spot on it */
}

.react {
  --px: 50%;
  --py: 50%;
  --pt: 0;
  --rx: 0deg;
  --ry: 0deg;
  --tx: 0;
  --ty: 0;
  position: relative;
  isolation: isolate;
  transform-style: flat;
  /* THE TRAVEL, added on the owner's second look: *"also move the card a bit
     with that"*. The tilt alone rotates a card about its own centre, which
     from directly in front is a small change in a large object and reads as
     a shimmer rather than as a response.

     4px, toward the hand, on top of a 3px lift. The two together are what
     makes it feel picked up: the lift says the card has left the page and
     the travel says which way it was pulled. It is deliberately smaller than
     the tilt's apparent displacement — a card that chases the cursor further
     than this starts to feel like it is avoiding being clicked, which is the
     failure mode of every over-tuned version of this effect.

     Multiplied by `--motion-move`, so it is already zero for a reader who
     asked for less motion before the `reduce` block below repeats the point
     with a literal. */
  transform:
    perspective(900px)
    translate3d(
      calc(var(--tx) * var(--react-travel) * var(--motion-move) * var(--react-pow)),
      calc(var(--ty) * var(--react-travel) * var(--motion-move) * var(--react-pow)
           - var(--pt) * var(--react-lift) * var(--motion-move) * var(--react-pow)),
      0)
    rotateX(calc(var(--rx) * var(--motion-move) * var(--react-pow)))
    rotateY(calc(var(--ry) * var(--motion-move) * var(--react-pow)));
  /* `--ease` (the exponential ease-out), not `--ease-tint`. A near-symmetric
     curve on a positional change reads as mechanical because it accelerates
     into the move; the card should leave immediately and settle slowly,
     which is what gives it apparent mass. This one line is most of why the
     movement was not being felt. */
  transition:
    transform var(--dur-settle) var(--ease),
    box-shadow calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* THE SHEEN. A soft light centred on the hand, riding above the pane's fill
   and below its content — §25's `.glass` puts the rim at z-index 2 and its
   own specular at 1, and content at 3, so this sits at 2 with the rim and
   reads as light ON the glass rather than as a shape drawn inside it.

   It is white at a low alpha in both themes rather than the block's hue.
   A hue-coloured sheen tints the text under it, and this passes across
   paragraphs; white raises everything it crosses by the same amount, which
   is the only version of this effect that cannot change a contrast ratio in
   one direction for one colour.

   ⚠️ **REVISED after the owner saw it running**, and the revision is the
   interesting part rather than the original:

     *"that flash on the cursor on those cards … its a bit much and it should
     be like blurred glass so doesnt make issue in reading"*

   Both halves of that are right, and the second half names the fix. The
   first build painted white at 0.16 over the pane — which is light ON the
   glass, and light on the glass lands on the paragraph too. Raising a
   paragraph's background toward white while its ink stays put is a contrast
   change, moving the wrong way, under a moving spotlight the reader cannot
   predict. It was legible and it was still wrong.

   **So the sheen no longer brightens the text. It frosts the BACKDROP.**
   A second `backdrop-filter`, masked to a soft pool at the hand, deepens the
   blur of what is behind the pane where the hand is. That is what glass
   actually does when you look through it at an angle, it is what the owner
   asked for in their own words, and it cannot move a text contrast ratio at
   all — the copy sits at `z-index: 3`, above this layer, and its own
   background is the pane's fill, which this does not touch. The white is
   kept at a whisper (0.055) purely to give the pool an edge to be seen by.

   The reading is now strictly safer under the hand than it was at rest,
   because a blurrier backdrop is a quieter one. */
.react__sheen {
  position: absolute;
  /* ⚠️ PINNED TO THE WHOLE GRID, and this is a defect fix rather than a
     precaution. These two layers are injected as the FIRST children of a
     card, and on a card that is a grid — `.stage` on how.html — they were
     being placed as grid items and taking the first cells, which pushed the
     heading down below the note that should sit beside its body. It renders
     as a large dead gap in the middle of the card and it looks like a
     spacing mistake rather than a placement one, which is how it survived a
     screenshot: I only caught it by rendering the pre-change file beside it.

     `grid-area` spanning the whole grid costs nothing where the parent is
     not a grid, and makes the layers un-placeable where it is. */
  grid-area: 1 / 1 / -1 / -1;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--pt);
  background: radial-gradient(
    var(--react-pool) var(--react-pool) at var(--px) var(--py),
    rgba(255, 255, 255, var(--react-sheen-a)) 0%,
    rgba(255, 255, 255, calc(var(--react-sheen-a) * 0.4)) 40%,
    transparent 72%);
  transition: opacity calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* The frost, gated on the lit state for §33's compositor reason — a
   `backdrop-filter` is a rasterised layer whether or not anything shows
   through it, and eight of them at rest is the cost that stopped the
   screenshot harness completing. Masked rather than shaped by opacity: a
   backdrop-filter cannot be faded by its own gradient, only clipped by one,
   so the pool is a mask and the fade in and out is the opacity above. */
.react[data-lit] .react__sheen {
  backdrop-filter: blur(var(--react-frost)) saturate(var(--react-frost-sat));
  -webkit-backdrop-filter: blur(var(--react-frost)) saturate(var(--react-frost-sat));
  -webkit-mask-image: radial-gradient(24rem 24rem at var(--px) var(--py),
    #000 0%, rgba(0, 0, 0, 0.55) 42%, transparent 74%);
  mask-image: radial-gradient(24rem 24rem at var(--px) var(--py),
    #000 0%, rgba(0, 0, 0, 0.55) 42%, transparent 74%);
}

/* THE BLOOM. The "shallow effect behind them of some kind blurry type" —
   a soft pool of the card's own hue, sitting BEHIND the pane and spreading
   past its edges, following the hand.

   It works because `.glass` and `.react` both declare `isolation: isolate`,
   so `z-index: -1` is trapped in this card's own stacking context and lands
   behind the card's background instead of behind the page. And it is
   deliberately not clipped: a bloom that stops at the border is an inner
   glow, which is the opposite effect and reads as a highlight rather than
   as light escaping from behind an object.

   ⚠️ The card must therefore NOT carry `overflow: hidden`, which is why
   these cards take `.react` and not §32.3's `.lift` — the inner-layer scale
   needs clipping and the bloom needs the absence of it. The two effects are
   mutually exclusive on one element and the choice is per component. */
.react__bloom {
  position: absolute;
  grid-area: 1 / 1 / -1 / -1;
  /* ⚠️ `inset: 0`, NOT a negative inset, and this is a defect fix.
     A negative inset makes the element's LAYOUT box larger than its card,
     and a layout box wider than the viewport is a sideways scroll —
     `tool/site_overflow.sh` caught it on safety.html and contact.html at
     every width from 480 to 1024. On a phone that is the whole page sliding
     under the thumb, on the two pages a reader in trouble is most likely to
     open.

     The spread is unaffected, because it never came from the inset: a
     `filter: blur()` paints well outside its own box, and painted overflow
     does not create a scrollable area the way a layout box does. Same glow,
     no scroll. */
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  opacity: calc(var(--pt) * var(--react-bloom-o));
  /* ⚠️ `--bx`/`--by`, NOT `--px`/`--py`. The sheen sits on the glass and must
     be exactly under the hand or it reads as a misaligned highlight. The
     bloom is BEHIND the pane, and a light source behind a physical object
     is the last thing to arrive when the object moves — so it is given its
     own pair of coordinates, eased toward the pointer a frame at a time in
     motion.js rather than assigned to it.

     This is the whole of *"the background cursor follow should be shallow
     effect but with little motion that make it alive"*. A glow pinned to the
     cursor is a cursor effect; a glow that arrives a moment later is a light
     with mass, and the difference is about eight frames of lag. */
  background: radial-gradient(
    var(--react-bloom-spread) at var(--bx, var(--px)) var(--by, var(--py)),
    rgba(var(--hue, var(--rgb-sage)), var(--react-bloom-a)) 0%,
    rgba(var(--hue, var(--rgb-sage)), calc(var(--react-bloom-a) * 0.4)) 38%,
    transparent 72%);
  transition: opacity calc(var(--dur-settle) + 120ms) var(--ease-tint);
}

/* ⚠️ THE BLUR IS ATTACHED TO THE LIT STATE, NOT DECLARED AT REST, and this
   is a real cost rather than a tidiness preference. A `filter: blur()` makes
   the element a composited layer that the compositor rasterises **whether or
   not it is visible** — an opacity of 0 hides the result, it does not skip
   the work. Eight cards on a page is eight permanently rasterised blur
   layers, paid for on every scroll frame by every reader, to render nothing.

   Measured on the way in: with the blur declared at rest, this project's own
   headless screenshot harness stopped completing on `vision.html` at all
   (killed at 120s, against roughly 5s for the same page before). That is a
   software rasteriser rather than a phone, so it is the loudest possible
   version of the signal rather than a proportionate one — but the shape of
   the cost is the same on a mid-range Android, where it is silent.

   The switch is discrete rather than transitioned, which is correct and not
   a compromise: at the moment it flips, `--pt` is still 0 and the layer is
   fully transparent, so there is nothing on screen for a step to be visible
   in. The fade the reader sees is the opacity transition above, unchanged. */
.react[data-lit] .react__bloom {
  filter: blur(var(--react-bloom-blur));
}

/* The pane lifts its shadow with the hand as well. Theme-split, because
   §18's arithmetic has not changed: light separates by cast shadow and dark
   by rim, and a deeper black on a dark page buys 0.1 of ratio. */
.react[data-lit] {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.62)),
    inset 0 -1px 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.28)),
    inset 1px 0 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.20)),
    inset -1px 0 0 rgba(255, 255, 255, calc(var(--glass-sheen) * 0.20)),
    0 2px 6px rgba(43, 42, 40, 0.06),
    0 34px 70px -30px rgba(43, 42, 40, 0.34);
}

:root[data-theme="dark"] .react[data-lit] {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.12),
    0 36px 90px -38px rgba(0, 0, 0, 0.9);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .react[data-lit] {
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.12),
      0 36px 90px -38px rgba(0, 0, 0, 0.9);
  }
}

/* §19's trap, for the third time in this file. `--motion-move` at 0 already
   zeroes the rotation inside the `calc()` above, which is the belt; this is
   the braces, and it also covers the case where a future edit reaches for a
   literal degree value and forgets. The sheen and the bloom are untouched:
   a reader who asked for less motion did not ask for less feedback. */
@media (prefers-reduced-motion: reduce) {
  .react {
    transform: none;
  }
}

/* prefers-reduced-transparency and prefers-contrast are three separate
   queries and answering one is not answering the others — the gap that let
   the masthead ship translucent to a reader who had asked it not to. Both
   are answered here at the point the material is introduced, rather than
   left for §25.6 to catch a class it has never heard of. */
@media (prefers-reduced-transparency: reduce) {
  .react__sheen,
  .react__bloom {
    display: none;
  }
}

@media (prefers-contrast: more) {
  .react__sheen,
  .react__bloom {
    display: none;
  }

  .react {
    transform: none;
  }
}

/* ==========================================================================
   34. THE FOUR PROMISES — what a reader gets, and what holds it
   ==========================================================================
   The section this site did not have: one place that says what is true of
   the product, in the second person, without hedging the sentence it is in.

   An inventory version was built first and rejected: *"we dont show like
   development data like 2219 … we show whats important for a institution
   and a customer or a reviewer"*. That is the right call and it is worth
   writing down, because a content count is the easiest impressive-looking
   number a project like this can reach for, and it answers a question
   nobody asked. What all three audiences want is the same thing in three
   registers — a student asks what happens when I open it, a reviewer asks
   what stops it going wrong, a funder asks what makes either answer
   checkable — and one set of claims answers all three if each claim names
   the thing that enforces it.

   ## The pairing is the argument, and it was on one page only

   `.inv__test` on the Safety page puts a commitment beside the test that
   holds it, and §15.8 called it the strongest idea on the site. It was
   reaching readers who had already scrolled to the Safety page, which is to
   say readers who had already decided to trust us. Here it is on the
   landing page, in the second person, where it does the work.

   ## Why these are cards when §32 refused card grids

   §32 refuses cards as a page SCAFFOLD — identical icon-heading-text boxes
   standing in for structure. These four are genuine peers: four claims of
   one kind, read as a set, compared against each other, in no order. That
   is the case a card grid is actually for. They carry no icon, they are not
   the same height, and each is a different hue because each is a different
   promise.
   -------------------------------------------------------------------------- */

.offers {
  display: grid;
  gap: clamp(1rem, 2.2vw, 1.5rem);
}

@media (min-width: 900px) {
  .offers {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Each card is its own height. A grid stretches its items by default,
       which on a set whose bodies differ by three lines opens a hole above
       the footer of the shortest one — and the footer is a rule, so the hole
       is drawn rather than merely empty. The pair still reads as a pair
       because the offset below composes them; equal height was never what
       was doing that work. */
    align-items: start;
  }

  /* The same half-step break §32.5 gives the vision cards, so the set reads
     as two pairs at two distances rather than as a 2x2 table. */
  .offers > :nth-child(even) {
    margin-top: clamp(1.25rem, 3vw, 2.5rem);
  }
}

.offer {
  padding: clamp(1.4rem, 2.8vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

.offer::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

.offer__h {
  font-family: var(--font-head);
  font-size: clamp(1.25rem, 1.08rem + 0.7vw, 1.5rem);
  line-height: 1.2;
  letter-spacing: var(--tr-3);
  color: var(--text);
  margin: 0;
  text-wrap: balance;
}

.offer__body {
  color: var(--text-soft);
  margin: 0;
}

/* The test, set apart. Mono is spent here under §25.19's rule and not
   against it: this is a file path, which is data — the one thing the rule
   admits. *"If the text would still be true with the numbers removed, it is
   not a candidate."* Remove the path and the sentence stops being checkable,
   which is the entire point of printing it. */
.offer__test {
  margin: 0.35rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.28);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}

.offer__test-k {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--text-soft);
}

.offer__test code {
  font-family: var(--font-mono);
  color: var(--text);
}

/* --------------------------------------------------------------------------
   34.1 The three layers — the only numbered markers on this site
   --------------------------------------------------------------------------
   `craft-floor` bans section numbers *"unless the sequence itself carries
   information the reader needs"*, and the frontend-design guidance says the
   same in different words: structural devices must encode something true.

   Here the sequence is the claim. Layer 1 runs with 2 and 3 switched off and
   the product is complete; 2 and 3 cannot run without 1 and neither may
   write a word. Delete the digits and a reader has three peers instead of a
   stack, which is a different and false statement about the architecture.
   That is the test for a numbered marker and this is the site's only use.

   The digit is set at display weight in the block's own hue and sits in the
   card's top-left as a corner mark rather than as a heading — it is a
   position, not a title, and a numeral typeset like a heading competes with
   the one directly beneath it.
   -------------------------------------------------------------------------- */

.layers {
  display: grid;
  gap: clamp(1rem, 2.2vw, 1.5rem);
}

@media (min-width: 1000px) {
  .layers {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
  }

  /* A stack read left to right, stepping down. The offset is cumulative
     rather than alternating, because these are ordered and an alternating
     rhythm would say they are not. */
  .layers > :nth-child(2) { margin-top: clamp(1rem, 2.5vw, 2rem); }
  .layers > :nth-child(3) { margin-top: clamp(2rem, 5vw, 4rem); }
}

.layer {
  padding: clamp(1.4rem, 2.8vw, 1.9rem);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

.layer::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

.layer__n {
  margin: 0;
  font-family: var(--font-head);
  font-size: 2.25rem;
  line-height: 1;
  font-weight: 600;
  letter-spacing: var(--tr-1);
  color: rgba(var(--hue, var(--rgb-sage)), 0.72);
  font-variant-numeric: tabular-nums;
}

.layer__h {
  margin: 0;
  font-family: var(--font-head);
  font-size: clamp(1.1rem, 1rem + 0.45vw, 1.3125rem);
  line-height: 1.24;
  letter-spacing: var(--tr-3);
  color: var(--text);
  text-wrap: balance;
}

.layer__body {
  margin: 0;
  color: var(--text-soft);
  font-size: var(--t-small);
}

/* The boundary paragraph. It is `--text` rather than `--text-soft` and it is
   not inside a card, because it governs all three and a rule that lives
   inside one of the things it governs reads as that thing's footnote. */
.layers__floor {
  margin: clamp(1.75rem, 3.5vw, 2.75rem) 0 0;
  max-width: 68ch;
  color: var(--text);
}

.layers__floor b {
  font-weight: 600;
}

/* ==========================================================================
   35. THE REGISTER, AS A CARD GRID — and the counts that read as weakness
   ==========================================================================
   The owner has now objected to this page's numbers four times across the
   session log, and the most recent one names both the problem and the fix:

     *"this whole page this isnt looking very good we can show it in better
     way that look positive can build confidence to them in our product we
     dont want to show like 1-2 3-4 numbers they are very low we can just say
     our research covers these topics and then show some research and give
     option to see them all and we can show research in sqaure cards not like
     these big cards"*

   §15.8 answered the first half of this once, by rebuilding the verdict
   tally as a chart on a shared rail of 28 so that 16 looks like 16-of-28.
   That worked and it stays. What it did not touch is the FACET row, which is
   fourteen mechanisms and eight populations each printing its own count —
   and those counts are honestly small, because a register of 28 rows divided
   fourteen ways is small by arithmetic. Printed at label size, fourteen
   times in a column, they read as *"we found one paper about this"* rather
   than as *"this is how you narrow the list"*.

   ## What is done about it, and why it is presentation rather than hiding

   🔒 **No number is removed and nothing is rounded.** `.filter__n` becomes
   visually hidden and stays in the accessible name, so a screen reader still
   hears "Rumination, 3" — unchanged — and the disabled state that stops a
   reader pressing an option yielding nothing still works off the same count.
   The count remains what it always was: a navigational aid saying what
   pressing this would do.

   A facet count is not a claim about the evidence. The claims are the
   verdicts and the total, both still printed at full size, both still on the
   chart. Deciding that a navigational integer is set as a tooltip rather
   than as body copy is a typographic decision, and this project's honesty
   rules govern what may be CLAIMED — they were never a requirement that
   every integer be typeset large.

   ## And the coverage line, which is the same facts read forwards

   Above the facets, one derived sentence: how many mechanisms and how many
   populations the register reaches. Same rows, same arithmetic, counted in
   the script rather than typed — and it is the true framing, because
   fourteen mechanisms IS the breadth, and printing it as fourteen separate
   small numbers was the only thing making it look thin.
   -------------------------------------------------------------------------- */

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

.reg-coverage {
  margin: 0 0 clamp(1.25rem, 2.5vw, 1.75rem);
  max-width: 60ch;
  color: var(--text);
  font-size: var(--t-lead);
  line-height: 1.5;
}

.reg-coverage b {
  font-weight: 600;
}

/* THE CARDS. Asked for as *"sqaure cards not like these big cards"*, and the
   change is one rule: a full-width row at 1512 is a 1400px line of body copy
   with a citation floating at the end of it, which is neither scannable nor
   comparable. On a grid the reader can see six at once and compare them,
   which is what a register is FOR.

   `auto-fill` with a 21rem floor rather than a fixed column count: the
   register is filtered live, and a fixed three-column grid leaves two dead
   columns the moment someone narrows it to a single mechanism. `align-items:
   start` keeps each card at its own height — these rows genuinely differ in
   length and stretching them all to the tallest would put a hole under every
   short one. */
@media (min-width: 900px) {
  .reg {
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
    align-items: start;
    gap: clamp(0.9rem, 1.8vw, 1.25rem);
  }

  /* The governing ceiling is the one row that is not a peer of the others —
     every claim on the site has to fit inside it — so it keeps the full
     width and opens the grid rather than sitting in it as one card of
     twenty-eight. Structure carrying meaning, which is the only reason to
     break a grid. */
  .reg > .reg__row:has(.reg__ceiling):first-child {
    grid-column: 1 / -1;
  }
}

/* At card width the facts table cannot stay a two-column dl with a wide
   term column: "Replication" plus a sentence in 21rem is four words a line.
   It stacks, with the term as a small caps-tracked label over its value. */
@media (min-width: 900px) {
  .reg__facts {
    display: block;
  }

  .reg__facts dt {
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-top: var(--s-3);
  }

  .reg__facts dd {
    margin: 0.15rem 0 0;
  }

  /* The full-width ceiling row keeps the side-by-side table it was designed
     for, because it has the width for it. */
  .reg > .reg__row:has(.reg__ceiling):first-child .reg__facts {
    display: grid;
    grid-template-columns: minmax(6rem, auto) minmax(0, 1fr);
    gap: 0.4rem 1.25rem;
  }

  .reg > .reg__row:has(.reg__ceiling):first-child .reg__facts dt {
    margin-top: 0;
  }
}

/* --------------------------------------------------------------------------
   35.1 The register rows, and the bento, take the same material
   --------------------------------------------------------------------------
   Asked for plainly: *"those research card have no motion nothing very static
   please keep cards having same design"*, and *"other cards should also be
   like these which are similar"*.

   Right, and the inconsistency was worse than static — the site had two card
   materials. `.tenet`, `.tension`, `.offer` and `.layer` were glass that
   answered the hand; `.reg__row` and `.bento__cell` were flat `surface-raised`
   that did not. A reader cannot tell which of those is the system and which
   is the exception, so both read as unfinished.

   One material now, on every peer card on the site. The one deliberate
   exclusion stays and is restated because it is the kind of thing a later
   pass "tidies up": 🔒 `.dir`, the crisis directory, is not glass and does
   not move. Soft text on glass measures 4.86 and on the bare card 12.85.
   Both pass. That surface takes the one with the whole margin.
   -------------------------------------------------------------------------- */

.reg__row.glass,
.bento__cell.react {
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

.reg__row.glass::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

/* `.bento__cell` already carried its own hover lift from §10 and its own
   transition was re-declared in §19. Both are superseded here: the §33
   transform is richer and the two would fight over `transform`. Declared
   after both, which is where the cascade puts this file's later sections. */
/* 🔴 **320ms, matching the other cards — 2026-10-11.**
   This settled on `--dur-settle` (420ms) while `.tile`, `.statcard` and
   `.reg__row` all lift on `calc(320ms * var(--motion-move))`, so the home
   cards moved visibly slower than every other card on the site. Both scale
   with `--motion-move`, so this is about matching the family rather than
   about reduced motion.
   ⚠️ The other direction is equally available and arguably more correct:
   `--dur-settle` is the design's named token for a settle, and the family's
   320ms is an untokenised number. Moving the family to the token would make
   every card 420ms instead. Three of the four already said 320, so the
   outlier moved. */
.bento__cell.react {
  transition:
    transform calc(320ms * var(--motion-move)) var(--ease-move),
    background-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    box-shadow calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint),
    color calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

/* ⚠️ `[data-lit]`, NOT `:hover`, and this is the home cards' defect the owner
   reported as *"the home cards still doesnt have those moving cards other
   have it"*. They were right, and the cause is narrower than I first said:
   every OTHER `.react` card drives its transform from the custom properties
   on the base rule, so it responds to a finger as readily as to a mouse.
   This one rule gated the transform on `:hover` — which exists only for a
   mouse — so on touch the script wrote every property correctly and the home
   cards alone did not move.

   Corrected in place rather than silently: I told the owner this was
   site-wide before checking, and it was one selector. */
/* THE GLOW THAT ESCAPES A CLIPPED CARD.
   ------------------------------------------------------------------------
   The owner reported the home cards twice: *"the home cards still doesnt
   have those moving cards other have it"*, then again after the first fix.
   The first fix was right and incomplete — it moved the transform off
   `:hover` so a finger works — but the cards still did not FEEL like the
   others, and the reason is `.bento__cell { overflow: hidden }`.

   Every other reactive card lets `.react__bloom` spread past its edge; that
   escaping glow is most of what the effect reads as. On the bento the bloom
   is clipped to the card, so it becomes a faint interior smudge.

   The clip cannot simply go: `.bento__rings` is positioned at right -3.5rem
   and bottom -4.5rem and relies on it. So the glow arrives by the one
   channel that paints OUTSIDE the border box and is unaffected by the
   element's own overflow — a box-shadow. Same colour, same proximity
   variable, same easing, no clipping, and the rings stay clipped.

   Theme-split for §18's reason: light separates by cast shadow, dark by
   rim, and a black shadow on the dark page tops out at 1.253:1. The glow is
   the hue in both, but the shadow it composes with is each theme's own. */
.bento__cell.react[data-lit] {
  box-shadow:
    var(--shadow),
    0 0 calc(3rem * var(--pt)) calc(0.75rem * var(--pt))
      rgba(var(--hue, var(--rgb-sage)), calc(0.34 * var(--pt)));
}

.bento__cell.react[data-lit] {
  transform:
    perspective(900px)
    translate3d(
      calc(var(--tx) * var(--react-travel) * var(--motion-move) * var(--react-pow)),
      calc(var(--ty) * var(--react-travel) * var(--motion-move) * var(--react-pow)
           - var(--pt) * var(--react-lift) * var(--motion-move) * var(--react-pow)),
      0)
    rotateX(calc(var(--rx) * var(--motion-move) * var(--react-pow)))
    rotateY(calc(var(--ry) * var(--motion-move) * var(--react-pow)));
}

/* --------------------------------------------------------------------------
   35.2 The guarantee, without the file path
   --------------------------------------------------------------------------
   *"why we are putting the files of the development in safety page"* — and
   the same question applies to the four promises on the landing page, which
   this session had just given the same treatment.

   The pairing itself is right and stays: a commitment beside the thing that
   enforces it is the strongest device this site has. What was wrong was the
   FORM of the enforcer. `test/composition_guard_test.dart` is an artefact of
   how this team works, and to the two audiences that matter here — a student
   deciding whether to trust it, a reviewer deciding whether to fund it — a
   repository path is noise at best and looks unfinished at worst.

   What survives is the part that carries meaning: the assertion, in the
   words the test itself uses. *"every user-visible string came from the
   slate, never from the model"* is checkable, specific, and says something
   a reader can hold. The filename said only that a file exists.

   Nothing is weakened. The tests are unchanged and still run; the claim that
   an automated check fails the build is the same claim, made in the reader's
   language rather than in the repository's.
   -------------------------------------------------------------------------- */

.inv__test-k,
.offer__test-k {
  display: block;
  font-family: var(--font-body);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 600;
  color: var(--text-soft);
  margin-bottom: var(--s-2);
}

.offer__test-k {
  display: inline;
  margin: 0 0.35rem 0 0;
}

/* --------------------------------------------------------------------------
   35.3 The hard problems, answered before they are described
   --------------------------------------------------------------------------
   *"i see two tensions why we are putting such content there it should be
   positive not like these"*.

   The content under that heading was never the problem — the answer
   paragraphs are the most confident writing on the page, and they commit to
   something specific and difficult. The FRAME was the problem, in three
   places at once: a heading whose noun was "tensions", a standfirst that
   pre-empted an objection on the reader's behalf (*"the interesting part,
   not the awkward part"* — nobody had said awkward yet), and an order that
   put two failure modes in front of a reader before telling them we had an
   answer.

   Nothing was deleted. The answer moved to the top of its own card, and the
   two constraints follow it under a label saying what they are: the things
   the answer has to hold together. Identical words, opposite reading.
   Confidence is largely a question of what comes first.
   -------------------------------------------------------------------------- */

.tension__answer {
  max-width: 62ch;
  font-size: var(--t-lead);
  line-height: 1.55;
  color: var(--text);
}

.tension__k {
  margin: 1.4rem 0 0;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 600;
  color: var(--text-soft);
}

.tension__k + .tension__pull {
  padding-top: 0.25rem;
}

/* --------------------------------------------------------------------------
   35.4 The filter panel, closed by default
   --------------------------------------------------------------------------
   Twenty-six pills in three groups stood between the reader and the first
   piece of research on the page. A reader arriving at an evidence section
   should meet evidence; a control panel in that position says the page's
   subject is its own filtering.

   `<details>` rather than a scripted toggle: identical behaviour with
   scripting off, the browser owns the state, and the disclosure triangle is
   an affordance every reader already knows. The summary carries the derived
   coverage figures, so collapsing the panel does not take the fact with it —
   a reader who never opens it still learns the register reaches fourteen
   mechanisms across eight populations.

   The one thing this must not do is hide a row, and it does not: every one
   of the twenty-eight is below the panel, open or closed, filtered or not.
   What is behind the control is the CONTROL.
   -------------------------------------------------------------------------- */

.filters-wrap {
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
  border-radius: var(--r-card);
  border: 1px solid var(--edge-lift);
}

.filters-wrap__sum {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.9rem;
  padding: 0.85rem 1.15rem;
  cursor: pointer;
  list-style: none;
  border-radius: inherit;
  transition: background-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
}

.filters-wrap__sum::-webkit-details-marker { display: none; }

.filters-wrap__sum:hover {
  background: var(--wash);
}

.filters-wrap__sum:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.filters-wrap__label {
  font-weight: 600;
  color: var(--text);
}

/* The marker is drawn rather than a glyph — `craft-floor` bans a unicode
   character standing in for an icon, and this file's own rule says the same.
   It rotates on open, which is a tint-channel change on a decorative mark. */
.filters-wrap__label::before {
  content: "";
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  margin-right: 0.6rem;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg) translateY(-0.1em);
  transition: transform calc(var(--dur-reveal) * var(--motion-tint)) var(--ease-tint);
}

.filters-wrap[open] .filters-wrap__label::before {
  transform: rotate(45deg) translateY(-0.15em);
}

.filters-wrap__hint {
  font-size: var(--t-small);
  color: var(--text-soft);
}

.filters-wrap .filters {
  padding: 0 1.15rem 1.15rem;
  border-top: 1px solid var(--hairline);
  padding-top: 1.15rem;
}

/* --------------------------------------------------------------------------
   35.5 The last of the big cards
   --------------------------------------------------------------------------
   The design-language audit, run over every card-shaped element on the nine
   hand-written pages rather than by eye. It found nine survivors still on
   §18's flat `surface-raised` while thirty-four peers had moved to glass:
   how's five pipeline stages, the two fact panels, the closing invitation and
   the contact address.

   *"the design language is not similar everywhere"* — correct, and this is
   the arithmetic behind it. Nine of forty-three is enough that a reader meets
   both materials on a single scroll and cannot tell which one is the system.

   The row components are deliberately NOT swept in: `.inv__row`, `.mech__row`,
   `.tense__row`, `.ledger__row`, `.nots__row`, `.tally__row`. They are rows in
   a table, not cards, and giving a data row a card's material is how a design
   system loses the distinction between "these are peers to compare" and
   "these are fields of one record". 🔒 `.dir` stays flat for the reason given
   twice already.
   -------------------------------------------------------------------------- */

.stage.glass,
.facts.glass,
.invite.glass,
.mail.glass {
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

.stage.glass::before,
.facts.glass::before,
.invite.glass::before,
.mail.glass::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

/* These four carried their own padding from the sections that built them and
   none of it agreed: 1.1rem, 1.5rem, clamp(1.25,…,1.9). One scale now, the
   same one §34's cards use, because *"make sure spacing is well taken cared
   of"* is mostly a question of whether the same kind of thing is spaced the
   same way in two places. */
.stage.glass,
.facts.glass,
.invite.glass,
.mail.glass {
  padding: clamp(1.4rem, 2.8vw, 2rem);
}

/* --------------------------------------------------------------------------
   35.6 The three movements — one section, three registers
   --------------------------------------------------------------------------
   The brief has one requirement that has never changed and is the hardest
   thing on this site: **one page must impress a person having a hard week
   AND a grant committee.** Not two pages. The same page.

   Asked where the app's strengths should live and what they should lead
   with, the owner answered the second question *"all of it in some way"* —
   which is the right answer and is the requirement restated. So this section
   runs three movements rather than picking a register:

     1  what it does for your day       second person, warm, no mechanism
     2  what it refuses to do           the four promises, each with its guard
     3  why either can be checked       the register, failures included

   A student reads 1 and can stop. A reviewer reads 3 and goes to Evidence.
   Both pass through 2, which is where the two audiences actually agree —
   "no scoring, no streaks, no account, nothing transmitted" is a comfort to
   one and a differentiator to the other, in the same words.

   ## Why the movements are not four more cards

   `craft-floor` bans identical cards as a page scaffold, and the four
   promises are already a card set. Movements 1 and 3 are single wide panels
   with a different internal shape — a label, a statement at lead size, a
   supporting paragraph — so the section reads as opening, argument, close
   rather than as seven boxes. The rhythm is the composition; if all three
   movements were card grids the section would have no shape at all.
   -------------------------------------------------------------------------- */

.movement {
  padding: clamp(1.5rem, 3vw, 2.25rem);
  display: grid;
  gap: var(--s-3);
  margin-bottom: clamp(1.25rem, 3vw, 2rem);
  background:
    linear-gradient(0deg,
      rgba(var(--hue, var(--rgb-sage)), var(--fill)),
      rgba(var(--hue, var(--rgb-sage)), var(--fill))),
    linear-gradient(157deg,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 0%,
      rgba(var(--glass-rgb), var(--glass-a)) 58%,
      rgba(var(--glass-rgb), var(--glass-a-hi)) 100%);
}

.movement::before {
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), 0.85) 0%,
    rgba(255, 255, 255, calc(var(--glass-sheen) * 0.18)) 38%,
    var(--glass-rim) 100%);
}

.movement--end {
  margin-bottom: 0;
  margin-top: clamp(1.25rem, 3vw, 2rem);
}

.movement__k {
  margin: 0;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 600;
  color: var(--text-soft);
}

/* The statement carries the movement and is set at lead size in `--text`.
   §25's rule one: `--text` clears the body floor on the field at the worst
   pixel with two to four times the headroom; `--text-soft` does not, which
   is why the supporting paragraph below sits on glass and this does too. */
.movement__say {
  margin: 0;
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--text);
  max-width: 62ch;
}

.movement__body {
  margin: 0;
  color: var(--text-soft);
  max-width: 68ch;
}

@media (min-width: 1000px) {
  /* Two tracks, so the statement and its support read as one object rather
     than as a paragraph followed by a smaller paragraph. The label sits in
     the first track above the statement, which is where a reader's eye
     enters. */
  .movement {
    grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
    column-gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
  }

  .movement__k { grid-column: 1; }
  .movement__say { grid-column: 1; }

  .movement__body {
    grid-column: 2;
    grid-row: 1 / span 2;
    padding-left: var(--rail-gap-wide);
    border-left: 1px solid rgba(var(--hue, var(--rgb-sage)), 0.3);
  }
}

/* ==========================================================================
   36. ARRIVAL — scroll-driven motion, and the reason it is not a fade
   ==========================================================================
   The single largest "this was built in 2026" lever the site had not pulled.
   CSS scroll-driven animations are now Baseline across all four engines, they
   need no JavaScript, and they run on the compositor rather than the main
   thread — so this costs nothing on a mid-range phone, which is the device
   this product is actually opened on.

   ## ⚠️ It is deliberately NOT a fade-in-on-scroll

   `craft-floor` names that specifically: *"one authored moment, not scattered
   effects and not one identical entrance on every section."* A uniform
   fade-and-rise on every block is the most-copied effect on the marketing web
   and it says nothing — worse, it hides content from a reader who scrolls
   fast, which on a site read by someone having a hard week is a real cost
   rather than a stylistic one.

   So arrival here is **positional and partial**: elements are fully opaque at
   all times and never hidden, and what moves is their position relative to
   each other. Nothing is ever invisible waiting to be revealed. If the
   animation never runs — old engine, reduced motion, scripting irrelevant —
   the page is the composed page, not a blank one.

   ## The three arrivals, each doing a different job

   `.rise`     a group settles the last few pixels into place. Peers only,
               so the set reads as one object arriving rather than as a queue.
   `.drift`    two columns travel at different rates through the viewport,
               which is parallax proper: the depth is REAL, not drawn.
   `.trace`    a figure's lead stroke draws with scroll rather than on a
               timer, so the reader controls the drawing. §32.2's figures
               animate on load today; on the pages where the figure IS the
               argument, the reader scrubbing it is better than watching it.

   ## Why `view()` and not `scroll()`

   `scroll()` measures the document; `view()` measures this element's own
   passage through the viewport, which is what "arrival" means. §15.8 used
   `scroll(root block)` for the masthead and was right to — a sticky bar's
   view timeline never advances, because it never moves through anything.
   Two different questions, two different timelines, and picking the wrong
   one is silent: the animation simply never runs.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* THE RISE. Six pixels and a hair of scale, over the first third of the
       element's approach. Six, not twenty-four: the point is that the page
       settles rather than that it performs, and past about ten pixels a
       reader starts watching the movement instead of reading the thing that
       moved. Opacity is untouched — see the rule above. */
    .rise > * {
      animation: arrive-rise linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 26%;
    }

    @keyframes arrive-rise {
      from { transform: translate3d(0, 14px, 0); }
      to   { transform: translate3d(0, 0, 0); }
    }

    /* Stagger by position, not by a per-element delay. `animation-range`
       shifted a little per child means each one completes at a slightly
       different scroll offset, which is a stagger a reader can scrub
       backwards — a delay-based one cannot be, because time only runs one
       way and scroll does not. */
    .rise > :nth-child(2) { animation-range: entry 6% cover 30%; }
    .rise > :nth-child(3) { animation-range: entry 8% cover 34%; }
    .rise > :nth-child(4) { animation-range: entry 10% cover 38%; }
    .rise > :nth-child(5) { animation-range: entry 12% cover 42%; }

    /* ⚠️ A LARGE SET DOES NOT STAGGER. The four offsets above are for a card
       set of four or five peers, where the stagger reads as one object
       settling. The register is twenty-eight rows in a `auto-fill` grid, and
       staggering those by source order staggers them by DOM position rather
       than by screen position — so a two-column grid resolves its left column
       before its right for no reason a reader can see, and rows six through
       twenty-eight all share the fifth offset anyway. Every row arrives on
       the same range, which on a grid is the whole row arriving together. */
    .reg.rise > * {
      animation-range: entry 6% cover 28%;
    }

    /* THE DRIFT. The genuine parallax: a figure travels slower than the
       column beside it, so the two separate as the reader passes them and
       close again as they leave. This is depth the page HAS rather than
       depth it draws, and it is the one effect on this site that cannot be
       screenshotted — which is exactly why it reads as alive. */
    .drift {
      animation: arrive-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    @keyframes arrive-drift {
      from { transform: translate3d(0, 26px, 0); }
      to   { transform: translate3d(0, -26px, 0); }
    }

    /* THE TRACE. The reader draws the figure by scrolling it. Overrides
       §32.2's load-time draw on any figure that opts in, and the two must
       never both run — a stroke animated by a timer and by a scroll at once
       fights itself and lands wherever the last frame left it. */
    .trace .fig__lead {
      animation: fig-draw linear both;
      animation-timeline: view();
      animation-range: entry 12% cover 46%;
    }
  }
}

/* ==========================================================================
   37. Z-SPACE — the cards stop sharing one plane
   ==========================================================================
   §29 gave the highlights page real depth and argued it well: *"every section
   sat on one plane, and type was doing all the work because nothing else was
   doing any."* That argument was never carried past that one page, which is
   the same shape of failure as the material itself reaching two pages of
   eleven.

   ⚠️ **The tilt is still not taken**, and §29's reasoning holds: the
   reference rotates its cards, a rotated screenshot frame is a device the
   research names as dated, and Linear has dropped it. Depth does not require
   rotation. Overlap, scale, shadow and z-order carry it without the costume —
   and this site now has a pointer-driven tilt that is FEEDBACK, which is a
   different thing from a tilt that is decoration and would be undermined by
   sitting next to one.

   ## The three devices, and the rule that keeps them from becoming noise

   Depth is spent where a set has a real hierarchy to express, never as a
   finish on every grid. Two distances, never three: a third is not
   perceivable at these sizes and just makes the page feel loose.

     `.z-step`   a set at two distances — the nearer card fractionally larger
                 with the deeper shadow, which is what makes the difference
                 read as distance rather than as a size mistake.
     `.z-break`  one element leaves its container's gutter. Used where the
                 element is the argument rather than an illustration of it.
     `.z-lap`    two neighbours overlap by a small margin, so the front one
                 is unambiguously in front. Requires z-index, and requires
                 the back one to have somewhere to be occluded.
   -------------------------------------------------------------------------- */

@media (min-width: 1000px) {
  /* ⚠️ The register opts OUT of the two-distance treatment and keeps the
     arrival. `nth-child(odd)` on a grid whose column count changes with the
     viewport picks a different physical pattern at every width — at two
     columns it is a checkerboard, at three it is diagonal stripes, and
     neither is a hierarchy anyone chose. Depth is for sets small enough that
     "nearer" means something. */
  .reg.z-step > * {
    transform: none;
    box-shadow: var(--shadow);
    z-index: auto;
  }

  /* TWO DISTANCES. The scale difference is 2.5%, which is under the
     threshold at which a reader reads it as "these cards are different
     sizes" and over the threshold at which they read it as "that one is
     closer". Both numbers were found by rendering, not chosen. */
  .z-step > :nth-child(odd) {
    transform: scale(1.012);
    box-shadow: var(--glass-lift);
    z-index: 2;
  }

  .z-step > :nth-child(even) {
    transform: scale(0.988);
    z-index: 1;
  }

  /* The transform above would otherwise cancel §33's pointer response, since
     both target `transform` on the same element. The lit card takes the
     pointer transform and drops the resting scale, which is correct: a card
     under the hand has come forward, so its resting distance is no longer
     the thing being expressed. */
  .z-step > .react[data-lit] {
    transform:
      perspective(900px)
      translate3d(
        calc(var(--tx) * var(--react-travel) * var(--motion-move) * var(--react-pow)),
        calc(var(--ty) * var(--react-travel) * var(--motion-move) * var(--react-pow)
             - var(--pt) * var(--react-lift) * var(--motion-move) * var(--react-pow)),
        0)
      rotateX(calc(var(--rx) * var(--motion-move) * var(--react-pow)))
      rotateY(calc(var(--ry) * var(--motion-move) * var(--react-pow)));
    z-index: 3;
  }

  /* BREAKING THE GUTTER. A figure that stops politely inside its column
     reads as an illustration slotted into a box; one that runs past the
     column edge reads as an object the page is a window onto. Bounded to
     the wrap's own outer margin so nothing can reach the viewport edge and
     cause a sideways scroll — `tool/site_overflow.sh` is the guard. */
  .z-break {
    margin-inline: calc(var(--z-break, 2.5rem) * -1) 0;
  }

  .z-break--end {
    margin-inline: 0 calc(var(--z-break, 2.5rem) * -1);
  }

  :root {
    --z-break: clamp(1.5rem, 3vw, 3rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The resting scales are position, not motion, and they stay — a reader
     who asked for less movement did not ask for a flatter page. Only the
     pointer response is cancelled, and §33 already does that. */
}

/* ==========================================================================
   38. THE COMPANIONS — character without a face, and why
   ==========================================================================
   Asked for: *"you can use svgs some characters because we will be using them
   in mobile too some good ui representation"*. The reference the owner
   annotated *"its really good"* carries a soft lavender blob with eyes.

   ## The one judgement call, made in the open

   These have character and they do not have faces, and that is a decision
   rather than a limitation of effort. A cartoon companion with eyes is warm
   to somebody browsing and is a specific risk to somebody at 2am on the third
   bad week of a term: a face reacts, and a reader in that state will read a
   reaction into it that the software did not intend and cannot control. It is
   the same reasoning that keeps the app from congratulating anyone, from
   keeping a streak, and from telling a person they are doing well — the
   product's whole register is *accompanying, not responding to*, and a face
   is a response.

   So: soft organic forms with weight and posture, in the palette, that read
   as present rather than as watching. They lean, they rest, they open. None
   of them looks back.

   **If the owner wants faces, that is theirs to decide and it is a small
   edit** — one `<circle>` pair per motif. It is written down here so the
   decision is visible rather than silently made by whoever drew these.

   ## Why these are not the geometry figures

   §32.2's figures are diagrams: every one is derived from its page's own
   structure and would be WRONG if the content changed. These carry no
   information at all and are free to be beautiful — which is exactly why
   they must never be placed where a figure belongs, or a reader will look
   for meaning in a shape that has none. They accent; they never explain.

   They are drawn as one path each at a shared 2px stroke and a shared radius
   language, so they read as one family and so the app can ship the same four
   files. Nothing here uses `feTurbulence`, a sketch stroke, shading or
   perspective — `craft-floor`'s ban on SVG imitating pictures stands.
   -------------------------------------------------------------------------- */

.motif {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  pointer-events: none;
}

.motif__body {
  fill: rgba(var(--hue, var(--rgb-sage)), 0.16);
  stroke: rgba(var(--hue, var(--rgb-sage)), 0.55);
  stroke-width: 2;
  stroke-linejoin: round;
}

.motif__mark {
  fill: none;
  stroke: rgba(var(--hue, var(--rgb-sage)), 0.9);
  stroke-width: 2;
  stroke-linecap: round;
}

.motif__rest {
  fill: rgba(var(--hue, var(--rgb-sage)), 0.9);
  stroke: none;
}

/* Placed as an accent beside a section head, never in a figure's slot. It
   is decorative and carries `aria-hidden`, so it is absent from the
   accessible tree entirely — which is the honest encoding of "this means
   nothing", and is why it may be as warm as it likes. */
.motif-slot {
  display: none;
}

@media (min-width: 1100px) {
  .motif-slot {
    display: block;
    width: clamp(6.5rem, 9vw, 10rem);
    flex: none;
    opacity: 0.9;
  }

  /* A slow breath. 9 seconds, 3% of scale, and it is the only looping
     animation on this site — everything else is scroll-linked or driven by
     a pointer. It is here because a companion that is perfectly still reads
     as a sticker, and because the rate is roughly a resting breath, which is
     the one piece of borrowed meaning these shapes are allowed. */
  @media (prefers-reduced-motion: no-preference) {
    .motif-slot .motif {
      animation: motif-breathe 9s ease-in-out infinite;
      transform-origin: 50% 70%;
    }
  }

  @keyframes motif-breathe {
    0%, 100% { transform: scale(1) translateY(0); }
    50%      { transform: scale(1.03) translateY(-3px); }
  }
}

/* The head becomes a row when it carries a companion, so the motif sits
   beside the heading rather than above it — which would make it an eyebrow,
   and an eyebrow is `craft-floor`'s one unearnable ban. */
@media (min-width: 1100px) {
  .section__head:has(.motif-slot) {
    display: flex;
    align-items: flex-start;
    gap: clamp(1.5rem, 3vw, 2.5rem);
    max-width: none;
  }

  .section__head:has(.motif-slot) > :first-child {
    flex: 1 1 auto;
  }

  .section__head:has(.motif-slot) > p {
    flex: 1 1 34ch;
    max-width: 46ch;
  }
}


/* The lit variant. Same four lights, same positions, same blur — only the
   alphas change, so a section opts in with one class and opts out by
   deleting it. Used where a glass panel carries the text and the field is
   free to be a colour rather than a hint. */
.aura--vivid {
  background:
    radial-gradient(72% 76% at 22% 30%,
      rgba(var(--rgb-sage), var(--field-sage-lit)) 0%,
      rgba(var(--rgb-sage), var(--field-sage-lit)) 30%, transparent 78%),
    radial-gradient(66% 70% at 78% 20%,
      rgba(var(--rgb-dusk), var(--field-dusk-lit)) 0%,
      rgba(var(--rgb-dusk), var(--field-dusk-lit)) 28%, transparent 76%),
    radial-gradient(62% 66% at 74% 84%,
      rgba(var(--rgb-clay), var(--field-clay-lit)) 0%,
      rgba(var(--rgb-clay), var(--field-clay-lit)) 26%, transparent 74%),
    radial-gradient(58% 62% at 20% 88%,
      rgba(var(--rgb-heather), var(--field-heather-lit)) 0%,
      rgba(var(--rgb-heather), var(--field-heather-lit)) 26%, transparent 74%);
  opacity: 1;
}

/* --------------------------------------------------------------------------
   THE SOLO FIELD — one hue per region
   --------------------------------------------------------------------------
   The token block beside `--field-solo` has the measurement; this is the
   geometry. Three lights, all of them `var(--hue)`, so they reinforce where
   they meet instead of cancelling. That is the entire change.

   **`--hue`, not a named colour.** A section opts in by adding `.aura--solo`
   to its `.aura` div, and picks its colour with the `.hue-*` class that
   already exists in section 1b — `.hue-clay`, `.hue-sand`, `.hue-dusk`. No
   hue class means clay, because clay is the hue with chroma to spend (24.6
   against sage's 9.4 at the same alpha and the same contrast cost) and a
   loud field is the only reason to reach for this class at all.

   **`.aura--vivid` is in the selector because vision.html already carries
   it.** Its four-blob rule above is left exactly as written and simply
   loses the cascade — nothing was edited in place, and deleting this rule
   restores the old field verbatim. The `-lit` tokens it reads are likewise
   still declared and still correct; they are just no longer the way to make
   a field bright, because raising four opposing alphas never was.

   **Positions are in the aura's own box.** `.aura` is `inset: -18%`, so the
   layer is 1.36x its section and every wanted position is written as
   (wanted + 0.18) / 1.36 — the arithmetic the four-blob comment above paid
   an iteration to learn. The dominant light is placed up and left, where a
   page-head's heading starts, and the two supports carry it down and right
   across the rest of the band rather than pooling in a corner.

   **Plateaus are wide on purpose.** SATHIGH is a 90th percentile, not a
   maximum: a hot spot moves SATMAX and leaves p90 where it was. Colour has
   to be an AREA to register, which is why each light holds its stated alpha
   to a third of its radius before it starts falling off. */
/* ⚠️ THE INK ON A DISPLAY HEADING IS `--accent`, AND IT DOES NOT FOLLOW THE
   FIELD.

   **Changed for 2.0, and re-measured rather than assumed.** It was
   `--ink-sage` — a 1.0 decision, when the site's one accent was a sage green
   and the italic word was the only place it appeared in type. The 2.0 accent
   is the design's `linkInk`, and the italic is now the clearest carrier of it
   in the whole page, which is what the redesign's serif accent is for.

   The sweep below was re-run against the new ink: over all five hue fields,
   in all three looks, at each look's own `--fill-hi`, the worst pair is
   **7.26:1** (Sage's accent on a clay field) against the same 3:1 floor. The
   1.0 value's worst was 3.085. So the change is safe by a wide margin, and
   the paragraph below — which is about the field, not the ink — still
   holds.

   Worth knowing before anyone tunes this: the binding ink over a page-head
   field is the italic display word, and `.display em` sets `color:
   var(--ink-sage)` outright — not `var(--hue-ink, ...)` like the rest of the
   site. So a section that opts into a clay field still gets a SAGE word, and
   no `.hue-*` class on the section changes that.

   It measures safe on every hue this class permits — swept at the union
   alpha over all five, the worst is the sage word on a SAND field in dark
   at **3.085:1** against a 3:1 floor, and the best is 3.401:1 — so it is
   documented rather than worked around. It is left alone on
   purpose: `.display em` is not part of the field, and a fix that reached
   outside these rules to change a heading's colour would be a different
   change than the one that was asked for.

   An earlier revision of this file tried to fix it here, by giving the
   section `--hue-ink` through `:where(.lit:has(.aura--solo))`. That was
   wrong twice over — it repaired a failure that did not exist (the number
   it was aimed at, 2.220:1, belonged to `--accent`, an ink that never
   renders here), and being a custom property on the section it could not
   have overridden a hardcoded `color` anyway. It was removed. */

.aura--solo,
.aura--vivid {
  background:
    /* the dominant light — section (20%, 25%), where a heading starts */
    radial-gradient(86% 90% at 28% 32%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo)) 0%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo)) 38%, transparent 80%),
    /* the carry — section (80%, 55%), meeting the dominant across the middle */
    radial-gradient(74% 78% at 72% 54%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo-2)) 0%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo-2)) 34%, transparent 78%),
    /* the foot — section (45%, 85%), holding the band's bottom edge */
    radial-gradient(70% 60% at 46% 76%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo-3)) 0%,
      rgba(var(--hue, var(--rgb-clay)), var(--field-solo-3)) 32%, transparent 76%);
  opacity: 1;
}

/* --------------------------------------------------------------------------
   39. THE NAV KNOWS WHERE YOU ARE
   --------------------------------------------------------------------------
   Asked for: *"put a home on the top bar and also which tab we are in
   highlight it"*. Both were missing, and the second is the one that matters —
   a reader landing from a search result had no way to tell which of six
   pages they were on without reading the headline.

   `aria-current="page"` rather than a class, because the state is a fact
   about the document rather than a decoration: a screen reader announces it,
   and the styling below is the visual half of the same statement rather than
   a parallel system that could disagree with it.

   The marker is a rule under the word, not a fill behind it. A filled pill
   in a nav reads as a button — and every other item beside it is a link, so
   one of them looking pressable and the rest not is a false affordance.
   -------------------------------------------------------------------------- */

.masthead__nav a[aria-current="page"] {
  color: var(--text);
  font-weight: 500;
  position: relative;
}

.masthead__nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.35rem;
  height: 2px;
  border-radius: 1px;
  background: var(--accent);
}

/* RETIRED 2026-09-03. The 1.0 bar was one flat line of six links plus a
   button, and below 1180 the two least-used had to go. The 2.0 bar carries
   three group summaries instead, and every page link lives inside a sheet
   that wraps on its own — so this rule stopped protecting a tight row and
   started deleting Changelog from the only nav that offers it. Between 900
   and 1180 the group opened with three cards and a hole where the fourth
   was. The footer was the only way to reach the page.

   Kept as a comment rather than deleted so the next person who measures the
   bar at 1000px does not reintroduce it. */

/* ---------------------------------------------------------------------------
   Build numbers

   Facts about what was made — the register `docs/Public Website.md` allows at
   full confidence. Deliberately quieter than `.ceiling`: that block carries
   the one number the whole site is constrained by, and nothing here may
   compete with it for weight.

   Each figure carries its provenance in the same block, because a number a
   reader cannot go and check is decoration. Same reasoning as the interval
   travelling with the effect size on evidence.html.
   --------------------------------------------------------------------------- */
.figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
  margin: 2.5rem 0 0;
  padding: 0;
  list-style: none;
}

.figures__item {
  padding: 1.5rem 1.25rem;
  border: 1px solid var(--hairline);
  border-radius: 18px;
}

.figures__n {
  margin: 0;
  font-family: var(--font-head);
  font-variant-numeric: tabular-nums;
  /* Smaller than `.ceiling__n`'s 2.5–4rem, on purpose — see the note above. */
  font-size: clamp(1.75rem, 1.2rem + 1.9vw, 2.5rem);
  font-weight: 620;
  line-height: 1;
}

.figures__label {
  margin: 0.5rem 0 0;
  font-size: var(--t-small);
}

.figures__prov {
  margin: 0.75rem 0 0;
  padding-top: 0.75rem;
  border-top: 1px solid var(--hairline);
  color: var(--text-soft);
}

/* One column below the narrow breakpoint the rest of the site already uses.
   Three tabular numbers side by side on a 360pt phone wrap mid-figure, which
   reads as a different number. */
@media (max-width: 900px) {
  .figures {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
}

/* ---------------------------------------------------------------------------
   The ceiling, drawn

   `g = 0.22` is exact and, to a reader who is not a psychologist, opaque. The
   block this styles is the same quantity as a hundred squares, so the size of
   the effect is something a person SEES rather than something they have to
   convert.

   Ten to a row is the whole trick: the fiftieth square closes row five, so
   "what you would get if these apps did nothing" is exactly the top half,
   with no axis to read and no baseline rule to draw. Everything past it is
   the entire measured effect — six squares. The drawing is arranged to show
   the smallness, which is the direction this site errs in.

   Three states, and the difference between them has to survive both themes
   and a reader who cannot separate the two greens:
     - base   `--border`,   the coin-flip half. >= 3:1 on page and surface,
                            held by site_contrast_test's 1.4.11 group.
     - gain   `--accent`,   the effect. Different in lightness AND hue from
                            base, so the pair does not rely on colour alone.
     - empty  `--hairline`, absence, drawn as an edge rather than a fill.
   The caption states all three counts in words as well, so nothing here is
   carried by colour only (WCAG 1.4.1).
   --------------------------------------------------------------------------- */
.pic {
  margin: 2.5rem 0 0;
  padding: 0;
}

/* Two blocks with a rule between them, NOT one grid in two colours. The
   first version was one grid, and `--border` against `--accent` measured
   1.23:1 in dark — a hue difference carrying the entire finding. Position
   carries it now; the fills only agree with it. */
.pic__grid { max-width: 30rem; }

.pic__half {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: clamp(3px, 0.6vw, 7px);
}

.pic__rule {
  display: block;
  height: 1px;
  margin: clamp(0.7rem, 1.6vw, 1.1rem) 0;
  background: var(--border);
}

/* Square cells at any width. `aspect-ratio` rather than a fixed size so the
   block reflows on a 360pt phone instead of overflowing it — the defect the
   build-numbers block shipped with and that only looking at it caught. */
.pic__d {
  aspect-ratio: 1;
  border-radius: 3px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--hairline);
}

.pic__d--base { background: var(--border); box-shadow: none; }
.pic__d--gain { background: var(--accent); box-shadow: none; }

.pic__cap {
  margin: 1.5rem 0 0;
  max-width: 46ch;
}

.pic__row {
  display: grid;
  grid-template-columns: 0.85rem 1fr;
  gap: 0.7rem;
  align-items: start;
  margin: 0.9rem 0 0;
  font-size: var(--t-small);
}

.pic__key {
  width: 0.85rem;
  height: 0.85rem;
  margin-top: 0.28em;
  border-radius: 3px;
}

.pic__key--base { background: var(--border); }
.pic__key--gain { background: var(--accent); }

.pic__note {
  margin: 1.5rem 0 0;
  max-width: 62ch;
  color: var(--text-soft);
}

/* The grid holds its own against a narrow screen because the cells are
   fractional, but the caption swatches and the 30rem cap both want the
   breakpoint the rest of the site already uses. */
@media (max-width: 900px) {
  .pic__grid { max-width: 22rem; }
}

/* --------------------------------------------------------------------------
   34. The redesign's two missing primitives
   --------------------------------------------------------------------------
   `design-2.0/.../website/02-pages-and-copy.md` opens every page with **an
   eyebrow naming the reader**, and `03-tokens-and-components.md` lists a
   **stat row** among the components. The site had neither: the nearest thing
   to an eyebrow was `.micro`, which is body-sized soft text and reads as a
   footnote rather than as a label, and `.facts` is a vertical `dt`/`dd` list,
   which is the wrong shape for three figures meant to be compared across.

   ## Why the eyebrow is load-bearing rather than decorative

   02's reason is the one that matters and it is an accessibility argument
   before it is a design one: *"an eyebrow names the reader, so nobody reads a
   page written for someone else and concludes the site is confused."* Three
   different people arrive at this site — someone deciding whether to use it,
   someone assessing it sceptically, someone interested in the project — and
   the first of those is often not in a good state. Telling them in three
   words that a section is not addressed to them is cheaper than making them
   read it to find out.

   It is a `<p>`, not a heading. A screen-reader user navigating by heading
   should reach the real title, and an eyebrow promoted to `<h2>` puts a
   four-word fragment into the document outline ahead of the sentence it
   qualifies.

   ## The stat row

   Space Grotesk 500 at the design's own 27-56px range, `-.03em`, over a label
   and one line of provenance. The provenance line is the part that stops it
   being marketing: every figure on this site is either derived from the
   repository or says where a reader could go and check it, and a number with
   nowhere to go is the shape `docs/Public Website.md` deleted a whole section
   for in 2026-08-01.

   `tabular-nums` so a column of figures aligns on the digit rather than on
   the glyph, and `text-wrap: balance` on the label so a two-word key does not
   leave one word alone on a second line.
   -------------------------------------------------------------------------- */

.eyebrow {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.6875rem;
  line-height: 1.4;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin: 0 0 var(--s-3);
}

/* On a dark panel the soft ink loses the ground it was measured against, so
   the eyebrow takes the panel's own muted ink instead. Same rule as §25's
   about accent text: a token measured on one ground is not a token. */
.panel .eyebrow,
.lit .eyebrow {
  color: var(--text-soft);
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: clamp(1.25rem, 3vw, 2.25rem);
  margin: 0;
  padding: 0;
}

.stat {
  margin: 0;
  min-width: 0;
}

/* The hairline runs above each figure rather than between them, so the row
   reads as three items of one kind and does not develop a hole when there are
   two of them or four. A separator between siblings has to know how many
   siblings there are; a rule above each one does not. */
.stat::before {
  content: "";
  display: block;
  height: 1px;
  background: var(--hairline);
  margin-bottom: var(--s-3);
}

/* Bottom-aligned inside a box the height of a NUMERAL, so a value set at the
   word size still hands its label off at the same baseline as the figures
   beside it. Without this the labels stagger, and a staggered row of three
   reads as three unrelated blocks rather than as one comparison. The height
   is derived from the same clamp the numeral uses, so tuning the type scale
   cannot desynchronise them. */
.stat__n {
  display: flex;
  align-items: flex-end;
  min-height: calc(clamp(1.75rem, 1.15rem + 1.8vw, 2.75rem) * 1.05);
  font-family: var(--font-head);
  font-weight: 500;
  font-size: clamp(1.75rem, 1.15rem + 1.8vw, 2.75rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.stat__k {
  display: block;
  margin-top: 0.4rem;
  font-weight: 600;
  font-size: 0.9375rem;
  line-height: 1.35;
  color: var(--text);
  text-wrap: balance;
}

.stat__s {
  display: block;
  margin-top: 0.3rem;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--text-soft);
}

/* The hero's own two: the eyebrow sits tight above a display line that
   already carries its own leading, and the stat row is capped so three
   figures do not stretch across the full 780px the copy column allows —
   past about 34rem they stop reading as a row and start reading as a table. */
.hero__eyebrow {
  margin-bottom: var(--s-2);
}

.hero__stats {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  list-style: none;
}

/* Three across, not the 2+1 the design file happens to render at its own
   width. The wrap is a consequence of its column, not a decision in it —
   `02-pages-and-copy.md` describes "three stats" and nothing in the package
   asks for one of them to sit alone on a second row. A narrower minimum gets
   all three onto one line in the 570px the hero copy column actually has,
   and they still stack cleanly on a phone because the track floors at 100%. */
.hero__stats {
  grid-template-columns: repeat(auto-fit, minmax(min(9.5rem, 100%), 1fr));
  gap: clamp(0.9rem, 2vw, 1.5rem);
}

/* A figure whose value is WORDS, not a numeral.
   ---------------------------------------------------------------------------
   "No account" set at the numeral size is the one that breaks the row: it is
   the only value in it that cannot be read at a glance as a quantity, and it
   is the only one long enough to wrap. Setting it smaller is not a
   compromise for fit — a numeral carries at 44px because a reader takes it in
   as a shape, and a two-word phrase at 44px is just a heading pretending to
   be data. The eye is meant to compare 2,225 with 91; "No account" is a
   different kind of claim and now looks like one. */
.stat__n--word {
  font-size: clamp(1.375rem, 1rem + 1vw, 1.75rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

/* A word value can run to two lines where a numeral never does, so the box
   grows rather than clipping — `min-height`, not `height`. */
.stat__n--word {
  height: auto;
}

/* --------------------------------------------------------------------------
   35. The page-head eyebrow
   --------------------------------------------------------------------------
   `02-pages-and-copy.md`: every page opens with *"an eyebrow naming who it is
   for, a title that is a sentence rather than a label, a lead paragraph, and
   an illustration band."* The site had the middle two and neither of the
   outer ones.

   The eyebrow needs explicit row placement and the other three need shifting
   down, because §32.1 pins `.display` to row 1 and `.lede` to row 2 and the
   figure spans `1 / 3`. A new grid child with no placement lands in an
   implicit row *after* those, which puts "If you are assessing it" underneath
   the paragraph it is supposed to introduce — correct-looking markup, wrong
   composition, and invisible in the source.

   Only inside the 900px gate. Below it `.page-head__grid` is not a grid at
   all, the children are in block flow, and the eyebrow is already first
   because it is first in the markup. So the phone layout is untouched by
   construction rather than by care — the same argument section 17 makes about
   its own 1280px floor. */
@media (min-width: 900px) {
  .page-head__grid {
    grid-template-rows: auto auto auto;
  }

  .page-head__eyebrow {
    grid-column: 1 / 9;
    grid-row: 1;
    margin-bottom: 0;
  }

  .page-head__grid .display {
    grid-row: 2;
  }

  .page-head__lede {
    grid-row: 3;
  }

  /* All three rows now, so the figure still bottom-aligns to the foot of the
     standfirst rather than to the foot of the headline. */
  .page-head__fig {
    grid-row: 1 / 4;
  }
}

/* The eyebrow sits closer to its title than the default label spacing, which
   is set for a label introducing a block rather than a line introducing a
   sentence. */
.page-head__eyebrow {
  margin-bottom: var(--s-2);
}

/* --------------------------------------------------------------------------
   36. The footer index, grouped
   --------------------------------------------------------------------------
   `.footer__links` was a wrapping flex row of ten anchors. Grouping them
   needs columns rather than a wrap, and the three groups are uneven (4/3/4),
   so `auto-fit` with a sensible minimum is right: three columns where there
   is room, two then one as it narrows, and never a column with one orphan in
   it because the tracks are equal.

   The anchors stay `display: flex` items in a column so the 44px target the
   original rule bought stays bought — that floor is in §6 and it was paid for
   after every footer link measured 25px. -------------------------------- */
.footer__group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  min-width: 0;
}

.footer__group-h {
  margin-bottom: var(--s-2);
}

@supports (display: grid) {
  .footer__links {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
    gap: var(--s-5, 1.75rem) clamp(1.25rem, 3vw, 2.5rem);
    align-content: start;
  }
}

/* --------------------------------------------------------------------------
   37. The eyebrow inside an editorial head
   --------------------------------------------------------------------------
   §31's editorial head splits on child ORDER: the first child takes the left
   track, everything after it takes the right. That was exact while the first
   child was always the heading. An eyebrow is now first, so the split put
   "The whole list" alone in the left column, bottom-aligned, a third of the
   page away from the heading it introduces — the label and its subject in
   different columns, which is worse than no label.

   The fix keeps §31's rule and changes what counts as the left track's
   content: an eyebrow belongs WITH its heading, not opposite it. So where a
   head opens with one, the left track carries both, stacked, and the lead
   still sits in the right track with its bottoms aligned.

   Scoped by `:has(> .eyebrow:first-child)`, so a head without an eyebrow is
   not touched by any rule here — §31's own argument for why `:has()` made
   that rule safe to apply site-wide applies again, one level down. Inside
   §31's own 900px gate for the same reason: below it there is no grid and the
   eyebrow is already directly above its heading in block flow.
   -------------------------------------------------------------------------- */
@media (min-width: 900px) {
  .section__head:has(> .eyebrow:first-child):has(> :nth-child(3)) {
    grid-template-rows: auto auto;
    align-items: end;
  }

  .section__head:has(> .eyebrow:first-child) > .eyebrow {
    grid-column: 1;
    grid-row: 1;
    align-self: end;
    margin-bottom: var(--s-2);
  }

  /* The heading, under its own label rather than beside it. */
  .section__head:has(> .eyebrow:first-child) > :nth-child(2) {
    grid-column: 1;
    grid-row: 2;
    margin-bottom: 0;
  }

  /* Everything else — the lead, and any paragraph after it — spans the two
     rows the left track now occupies, so the bottoms still align. */
  .section__head:has(> .eyebrow:first-child) > :nth-child(n + 3) {
    grid-column: 2;
    grid-row: 1 / 3;
    max-width: 46ch;
    align-self: end;
  }

  /* A head that is only an eyebrow and a heading has nothing for the second
     track, so it keeps the single-column shape rather than opening an empty
     one — the defect §31 wrote `:has()` to avoid. */
  .section__head:has(> .eyebrow:first-child):not(:has(> :nth-child(3))) {
    display: block;
    max-width: var(--measure);
  }
}

/* ==========================================================================
   38. THE THIRD LOOK — Sage
   ==========================================================================
   `03-tokens-and-components.md`: *"The app ships five palettes; the site
   offers three — enough to show the range without turning the site into a
   settings screen."* Calm, Sage and Night. This site had two, and the two it
   had already play Calm's and Night's parts: a warm light ground and a muted
   dark one. What was missing was the middle one.

   ## Why this ADDS a palette rather than changing one

   `CONTEXT.md` §2 carries a standing lock — never change a palette, accent or
   gradient value; fix the component — and `site_contrast_test` enforces it by
   measuring the pairs rather than by naming them. Repainting the existing
   light theme in the redesign's lilac Calm would have broken both. A third
   theme changes no existing value, so the lock holds and the site gains the
   look the package asks for. Where the package and this repository disagree
   about a COLOUR, the repository's measured palette wins; where they disagree
   about the design, the package wins, and "three looks" is the design.

   ## Every value below was solved against the test, not sampled

   The base, ink, ink2 and linkInk are the package's Sage values. Everything
   else is derived and then checked, because the package specifies a palette
   and this stylesheet specifies *pairs*:

     --text on the three grounds        15.15 / 16.03 / 14.11
     --text-soft on the three grounds    6.83 /  7.22 /  6.36
     --accent on the three grounds       9.00 /  9.52 /  8.38
     --accent-on over accent / quiet     9.84 /  5.41
     --border on page / surface          3.07 /  3.25
     every hue ink over its own tint     5.07 minimum, at fill-hi on clay

   `--border` is the one value the package could not supply and the one that
   had to be solved: the obvious mid-green measured **2.96** against this
   page, which is under WCAG 1.4.11's 3:1 for a control edge — a fifth of a
   point, invisible by eye, and the exact size of error the floor exists to
   catch. It is `#878f7c`, at 3.07, which is the same headroom the light
   theme's own border carries (3.11).

   The five hue inks and the five mood steps are the light theme's, unchanged:
   this ground sits within a hundredth of the light page in luminance, every
   pairing was recomputed against it rather than assumed, and the mood scale
   stays monotonic so it still reads without colour.
   ========================================================================== */
:root[data-theme="sage"] {
  --page: #f3f6ec;
  --surface: #fafcf5;
  --surface-sunk: #e9efdf;

  --text: #182115;
  --text-soft: #4e5847;

  --hairline: #dde4d0;
  --border: #878f7c;

  --accent: #33492e;
  --accent-quiet: #5b7052;
  --accent-on: #ffffff;

  --wash: rgba(51, 73, 46, 0.07);
  --wash-strong: rgba(51, 73, 46, 0.13);
  --chrome: rgba(250, 252, 245, 0.86);

  /* The family inks and the mood ladder are the light theme's, recomputed
     against this ground rather than inherited on faith — see the table above. */
  --ink-sage: #3f5a38;
  --ink-dusk: #485c6f;
  --ink-clay: #7b503c;
  --ink-heather: #61556c;
  --ink-sand: #68593d;

  --mood-1: #5d5068;
  --mood-2: #516a80;
  --mood-3: #6e7a73;
  --mood-4: #70906c;
  --mood-5: #789783;
}

/* --------------------------------------------------------------------------
   39. The nav groups
   --------------------------------------------------------------------------
   The masthead's own note used to argue that six links was the constraint and
   that a menu is a decision a reader makes before they have read a sentence.
   That argument was right about a flat list and wrong about this: three words
   is a smaller decision than six links, and it is the decision the reader
   already arrived with — am I using this, checking it, or curious about it.
   The redesign's `01-what-changed.md` makes that its first structural change.

   ## Everything here is section 6's phone sheet, promoted

   The sheet mechanics are not re-derived. Section 6 paid for two of them:
   the panel is **opaque** rather than glass, because a panel opening over
   arbitrary page content has the masthead's composited-ground problem in its
   worst form; and it is positioned against the **bar**, not against its own
   summary, because a summary moves when the bar wraps and a panel pinned to
   it opened 113px off the left edge of a phone. Both apply here unchanged.

   ## Only above 900px

   Below it the groups are hidden and section 6's single `More` disclosure
   still runs the phone, which keeps a bar that took several passes to get
   right. Three disclosures in a 390px bar would be three rows of chrome
   before a word of content.
   -------------------------------------------------------------------------- */

.navgroup {
  display: none;
}

@media (min-width: 901px) {
  /* `relative`, and this is a deliberate divergence from section 6 rather
     than a repeat of its bug. That sheet is `static` because below 900px the
     bar WRAPS: "More" lands at the left, and a panel pinned to a control that
     has moved opened at -113px on a 390px phone. These groups only exist
     above 900px, and `.masthead__inner` is `flex-wrap: nowrap` from exactly
     that width — so the control cannot move, and anchoring to it is safe.
     Anchored to the bar instead, all three sheets opened at the far left,
     under the wordmark, a quarter of the screen from the word that opened
     them. The condition section 6 guarded against does not hold here; the
     one it was protecting against reappears if you ignore that. */
  .navgroup {
    display: block;
    position: relative;
  }

  .navgroup > summary {
    list-style: none;
    cursor: pointer;
    color: var(--text-soft);
    font-size: 0.9375rem;
    /* The same 44px floor as every other control in this bar, bought with
       padding rather than min-height for the reason section 6 gives: a
       min-height collides with a `display` rule, and that is how the
       navigation links ended up at 25px once already. */
    padding-block: calc((44px - 1.65em) / 2);
    padding-inline: 0.35rem;
    white-space: nowrap;
    transition: color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
  }

  .navgroup > summary::-webkit-details-marker {
    display: none;
  }

  .navgroup > summary::after {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-inline-start: 0.45em;
    vertical-align: 0.15em;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg);
    transition: transform calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
  }

  .navgroup[open] > summary,
  .navgroup > summary:hover,
  .navgroup > summary:focus-visible {
    color: var(--text);
  }

  /* The caret is the open state, so it turns under reduced motion as well —
     §33's rule is that the move channel collapses, not that state stops being
     shown. It is listed in site_motion_test's survivesOnPurpose for that. */
  .navgroup[open] > summary::after {
    transform: rotate(-135deg);
  }

  .navgroup__sheet {
    position: absolute;
    /* Under the group that opened it. The last group sits near the right
       edge, so it is pulled back rather than allowed to run off — `right: 0`
       on its own would misalign the first two. */
    inset-inline-start: -0.35rem;
    /* `max-content`, capped. The containing block is now the group, whose
       width is its own two-word summary — so a panel with only `left` set
       resolved to about 90px and wrapped every row to four lines. The cap is
       what stops max-content running the width of the longest description. */
    inline-size: max-content;
    max-inline-size: min(24rem, calc(100vw - (var(--gutter) * 2)));
    top: calc(100% + 0.5rem);
    z-index: 40;
    display: flex;
    flex-direction: column;
    padding: 0.5rem;
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-lift, 0 12px 32px rgb(0 0 0 / 0.18));
  }

  .navgroup__what {
    margin: 0.35rem 0.6rem 0.5rem;
    font-family: var(--font-head);
    font-weight: 600;
    font-size: 0.6875rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-soft);
  }

  .navgroup__sheet a {
    display: block;
    padding: 0.55rem 0.6rem;
    border-radius: calc(var(--r-card) - 6px);
    color: var(--text);
    text-decoration: none;
    transition: background-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
  }

  .navgroup__sheet a:hover,
  .navgroup__sheet a:focus-visible {
    background: var(--wash);
  }

  .navgroup__sheet a b {
    display: block;
    font-weight: 600;
    font-size: 0.9375rem;
    line-height: 1.35;
  }

  .navgroup__sheet a span {
    display: block;
    margin-top: 0.1rem;
    font-size: var(--t-micro);
    line-height: 1.45;
    color: var(--text-soft);
  }

  /* The page you are on, marked in the one place it can be seen. */
  .navgroup__sheet a[aria-current="page"] {
    background: var(--wash-strong);
  }

  .navgroup__sheet a[aria-current="page"] b::after {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-inline-start: 0.5em;
    vertical-align: 0.1em;
    border-radius: 50%;
    background: var(--accent);
  }
}

/* The last group is closest to the right edge, and a 24rem panel opening from
   it would leave the viewport. It opens leftward instead — the only one that
   needs to, which is why this is a single rule and not a strategy. */
@media (min-width: 901px) {
  .navgroup:last-of-type .navgroup__sheet {
    inset-inline-start: auto;
    inset-inline-end: -0.35rem;
  }
}


/* ==========================================================================
   40. THE 2.0 PALETTE — Calm, Sage, Night
   ==========================================================================
   This is the redesign's own ground, and it REPLACES the 1.0 one rather than
   sitting beside it. Section 38 added Sage as a third theme and left the warm
   bone and sage-green of the original site in place, on the reading that
   `CONTEXT.md` §2's lock — never change a palette, accent or gradient value —
   outranked the brief. That was wrong. The lock exists so an agent cannot
   quietly retune a colour to make a contrast test pass; it was never a bar on
   the owner commissioning a new design, and `AGENTS.md` §7 says as much:
   these are decisions, not commandments, and the owner's instruction is
   exactly what reverses one. The result of getting that wrong was a site that
   carried the redesign's structure on the old site's skin.

   ## Every value here is the package's, or solved from it

   `03-tokens-and-components.md` supplies four per look — base, ink, ink2 and
   linkInk — plus the family hues. It does not supply `--surface`,
   `--surface-sunk`, `--accent-quiet`, `--border` or `--hairline`, because
   those are not colours in this stylesheet, they are measured PAIRS. Each was
   solved against `site_contrast_test`'s own constraints before being written
   here, not sampled and checked afterwards:

     text / text-soft / accent on all three grounds   >= 4.5, all 27 pairs
     --accent-on over accent and over accent-quiet    >= 4.5, all 6 pairs
     --border on page and surface                     3.05 to 11.5
     every family ink over its own tint at both fills >= 4.5, all 60 pairs

   `linkInk` is the site's `--accent`, not the package's `--accent`. The
   package's is a light lilac for use ON a dark panel; this stylesheet's
   `--accent` is a button FILL that carries `--accent-on` as its label, and
   white on `#B9A8DE` measures 1.9. Reading the role rather than the name is
   the difference between porting a palette and copying its hex values.

   Night lifts the five family inks 58% toward white, because the package's
   inks are drawn for a light ground and unmodified they measure under 2 on
   `#0C0A12`.
   ========================================================================== */

:root {
  --page: #faf7f2;
  --surface: #fdfbf9;
  --surface-sunk: #f1eeea;
  --text: #15130f;
  --text-soft: #5f5a52;
  --hairline: #e4e1dc;
  --border: #928e87;
  --accent: #4a3e63;
  --accent-quiet: #7a708a;
  --accent-on: #ffffff;

  /* 🔴 THE WASH WAS STILL 1.0's GREEN. `--wash` was declared once at the top
     of this file as `rgba(131, 152, 126, .09)` — the old accent — and 2.0
     never redeclared it for Calm or for Night. Only Sage overrode it, which
     is why the leak was invisible: the one look anybody checked for green
     was the only one that had been fixed. Twelve rules paint with it.

     Derived from `--accent` rather than typed, at the ratio Sage already
     used, so the wash cannot drift from the accent again. Sage keeps its
     literal pair: same ratio, same result, and it outranks this by
     specificity anyway. */
  --wash: color-mix(in srgb, var(--accent) 7%, transparent);
  --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);

  --ink-sage: #31402c;
  --ink-dusk: #2c3a4a;
  --ink-clay: #7b4a34;
  --ink-heather: #3d3355;
  --ink-sand: #4a3e1c;
}

:root[data-theme="sage"] {
  --page: #f3f6ec;
  --surface: #fafbf6;
  --surface-sunk: #eaede4;
  --text: #182115;
  --text-soft: #4e5847;
  --hairline: #dce0d5;
  --border: #888f81;
  --accent: #33492e;
  --accent-quiet: #697963;
  --accent-on: #ffffff;
  --ink-sage: #31402c;
  --ink-dusk: #2c3a4a;
  --ink-clay: #7b4a34;
  --ink-heather: #3d3355;
  --ink-sand: #4a3e1c;
}

/* The two Night bodies stay byte-identical — `site_contrast_test` asserts it,
   because a theme that drifts between its attribute form and its media form
   is one that measures correctly in a test and paints wrongly on a phone. */
:root[data-theme="dark"] {
  --page: #0c0a12;
  --surface: #19171f;
  --surface-sunk: #110f17;
  --text: #f6f3fb;
  --text-soft: #aba4be;
  --hairline: #2c2934;
  --border: #676274;
  --accent: #cfbdf2;
  --accent-quiet: #877b9f;
  --accent-on: #191325;
  --wash: color-mix(in srgb, var(--accent) 7%, transparent);
  --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);
  --ink-sage: #a8afa6;
  --ink-dusk: #a6acb3;
  --ink-clay: #c8b3aa;
  --ink-heather: #aea9b8;
  --ink-sand: #b3aea0;
}


/* The media form of the Night block above, byte-identical by construction.
   `site_contrast_test` asserts the two agree, because a theme whose attribute
   form and OS form disagree measures correctly in a test and paints wrongly
   on a phone — and it is the OS form that most readers actually get. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
  --page: #0c0a12;
  --surface: #19171f;
  --surface-sunk: #110f17;
  --text: #f6f3fb;
  --text-soft: #aba4be;
  --hairline: #2c2934;
  --border: #676274;
  --accent: #cfbdf2;
  --accent-quiet: #877b9f;
  --accent-on: #191325;
  --wash: color-mix(in srgb, var(--accent) 7%, transparent);
  --wash-strong: color-mix(in srgb, var(--accent) 13%, transparent);
  --ink-sage: #a8afa6;
  --ink-dusk: #a6acb3;
  --ink-clay: #c8b3aa;
  --ink-heather: #aea9b8;
  --ink-sand: #b3aea0;
  }
}

/* ==========================================================================
   41. THE GROUND — the 2.0 field, at page scale
   ==========================================================================
   `03-tokens-and-components.md` gives each look a ground, and it is a vertical
   gradient rather than a flat fill:

     Calm   linear-gradient(180deg,#F7F5FA 0%,#EFEAF1 58%,#FAF7F2 100%)
     Sage   linear-gradient(180deg,#F4F7EE 0%,#E9EFE0 58%,#F3F6EC 100%)
     Night  linear-gradient(180deg,#16131F 0%,#100D18 58%,#0C0A12 100%)

   The site painted `--page` flat, which is why the redesign's ground never
   arrived: the palette was right and the field was one colour. Calm's
   gradient runs lilac at the top to bone at the foot, and that top stop is
   the single most recognisable thing about the 2.0 look — it is what makes
   the masthead sit in a wash rather than on a slab.

   ## Why `--page` stays, and stays exactly what it was

   Every measured pair in `site_contrast_test` reads `--page` as a solid
   colour, and each gradient's LAST stop is its look's `--page` — so the
   bottom of the field is the colour the arithmetic was done against, and the
   lighter stops above it only ever raise the contrast of dark ink on light
   grounds. Night's stops run the other way and are darker than its `--page`,
   which does the same thing for light ink. Nothing here moves a measured
   pair; if a stop is ever chosen that would, it has to be measured first.

   `background-attachment: fixed` is deliberately NOT used: on a long page it
   pins the gradient to the viewport so the field stops relating to the
   document, and on iOS it is a known compositing cost on every scroll frame.
   The gradient is on the document, `no-repeat`, sized to the viewport height
   and then held by `--page` beneath it — so a page taller than the gradient
   ends in the ground colour rather than in a repeat.
   ========================================================================== */

:root {
  --ground: linear-gradient(180deg, #f7f5fa 0%, #efeaf1 58%, #faf7f2 100%);
}

:root[data-theme="sage"] {
  --ground: linear-gradient(180deg, #f4f7ee 0%, #e9efe0 58%, #f3f6ec 100%);
}

:root[data-theme="dark"] {
  --ground: linear-gradient(180deg, #16131f 0%, #100d18 58%, #0c0a12 100%);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --ground: linear-gradient(180deg, #16131f 0%, #100d18 58%, #0c0a12 100%);
  }
}

body {
  background-color: var(--page);
  background-image: var(--ground);
  background-repeat: no-repeat;
  background-size: 100% 100vh;
}

/* ==========================================================================
   42. THE DARK PANEL — one per page, for the sentence that matters most
   ==========================================================================
   Component 5 of `03-tokens-and-components.md`, and the last one the site did
   not have. Its rule is a limit rather than a style: **one per page maximum.**
   A page with two of them has no sentence that matters most.

   ## The inversion is the point, and it is the app's own behaviour

   The package is explicit — Night's `--panelDark` is `#CDBAF2` with
   `--onDark #191325`, *"the dark panel inverts to a light one, exactly as in
   the app."* So this is not "a dark box" with a dark-mode variant bolted on;
   it is **the panel that contrasts with its ground**, whichever ground that
   is. On Calm and Sage it is near-black on a pale page; on Night it is pale
   lilac on a near-black one. A reader switching looks sees the same object
   keep the same job.

   Measured, ink on panel and panel against its page:

     Calm    16.90   17.36
     Sage    13.38   13.70
     Night   10.25   11.15

   All three clear AA for the ink by a wide margin, and all three clear
   1.4.11's 3:1 for the panel as a block against the page — which is the
   assertion that matters, because the panel IS the emphasis and a panel that
   does not separate from its ground is a paragraph with padding.

   ## Not a card

   No hue, no glass, no lift. Section 25's glass is for surfaces a reader
   looks THROUGH; this is a surface a reader is meant to stop at. It takes the
   card radius so it sits in the same family, and nothing else from it.
   ========================================================================== */

:root {
  --panel-dark: #15130f;
  --panel-on: #f7f4ee;
}

:root[data-theme="sage"] {
  --panel-dark: #1e2a1a;
  --panel-on: #eff4e6;
}

:root[data-theme="dark"] {
  --panel-dark: #cdbaf2;
  --panel-on: #191325;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --panel-dark: #cdbaf2;
    --panel-on: #191325;
  }
}

/* 🔴 **The panel owed the block after it a gap, and declared none —
   2026-10-11.** `.panel` carried no margin at all, so what followed it was
   spaced by whatever margin that element happened to bring. `.section__note`
   brings 16px, which is a tight seam under a card padded to 48. A
   `.section__head` and a `.claim` bring none, so the "Read on" heading on
   evidence and the next claim on how sat **flush against the panel's bottom
   edge** — measured at 0px.

   The gap belongs to the panel, because the panel is the thing that needs
   separating; hanging it on each follower is how three of five pages ended
   up with three different answers. `.wrap` is block flow, so this collapses
   with the follower's own margin rather than adding to it — the note's 16px
   is absorbed, not stacked. `:not(:last-child)` because on index and safety
   the panel ends its wrap and a trailing margin there is space nobody
   asked for. */
.panel {
  background: var(--panel-dark);
  color: var(--panel-on);
  border-radius: var(--r-glass-lg, 26px);
  padding: clamp(1.75rem, 4vw, 3rem);
  max-width: var(--measure);
}

.panel:not(:last-child) {
  margin-bottom: var(--s-8);
}

.panel > :first-child {
  margin-top: 0;
}

.panel > :last-child {
  margin-bottom: 0;
}

.panel .h2,
.panel h2,
.panel h3 {
  color: var(--panel-on);
}

/* The panel's own eyebrow takes the panel ink at reduced strength rather than
   `--text-soft`, which is measured against the PAGE and disappears here. */
.panel .eyebrow {
  color: color-mix(in srgb, var(--panel-on) 72%, transparent);
}

/* A link inside the panel cannot use `--accent`: it is solved against the
   page, and on Calm's near-black panel the deep purple measures under 2. It
   takes the panel ink with an underline doing the work instead. */
.panel a {
  color: var(--panel-on);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

/* ==========================================================================
   43. THE 2.0 MOTION — the three gestures the package names that this had not
   ==========================================================================
   `03-tokens-and-components.md`'s motion table lists seven. Five were already
   here in some form: the page enter, the illustration inking itself in, the
   card hover lift, the button press, and one curve for all of it. Three were
   missing, and all three are gated so `prefers-reduced-motion` genuinely
   removes them — which is §33's two-channel rule, and the site's own claim.

   ## 1. The look change eases, and 620ms is not 240ms

   The package: *"Look change — the whole ground eases over 620ms rather than
   cutting."* The site already refused to cut, which is the important half,
   but it did it at `--dur-reveal` (240ms) — a **control-feedback** duration,
   the time a disclosure takes. A whole-page repaint at 240ms still reads as a
   flick; at 620ms it reads as the light changing in a room, which is the
   thing the package is asking for and the reason it names a number.

   It stays on the TINT channel and therefore survives reduced motion, for the
   reason §33 gives: colour and opacity are the licensed category, and a
   reader at 2am switching to Night is exactly who the slow version is for.

   ## 2. The nav sheet drops FROM the masthead

   The package is specific about the direction: *"the sheet drops from the
   masthead rather than rising into it, because it belongs to the bar it came
   from."* A panel that rises reads as a thing arriving from elsewhere; one
   that drops reads as the bar extending. Six pixels and 260ms is enough to
   carry that — it is a direction, not a distance.

   Opacity starts at zero here and that is correct, unlike the scroll
   entrances in §16b: this element genuinely did not exist a moment ago,
   because the reader just opened it. Nothing is being withheld from anyone.

   ## 3. A list row nudges 3px sideways

   The package's hover gesture for a row, and the site had none — rows
   recoloured and did not move. Sideways rather than up, because a row in a
   list is read along its own axis and a vertical lift on one row of many
   reads as the list breaking rank.
   ========================================================================== */

:root {
  /* Named, not inlined, because two rules consume it and a look change that
     eases at two different speeds is worse than one that cuts. */
  --dur-look: 620ms;
}

/* The ground itself. `body` carries both the colour and the gradient, and the
   gradient cannot be transitioned — so the colour eases and the image swaps
   underneath it, which is invisible because every gradient's last stop IS the
   colour that is easing. */
body {
  transition: background-color calc(var(--dur-look) * var(--motion-tint)) var(--ease-tint);
}

.masthead__inner,
.footer,
.section--sunk,
.panel {
  transition:
    background-color calc(var(--dur-look) * var(--motion-tint)) var(--ease-tint),
    color calc(var(--dur-look) * var(--motion-tint)) var(--ease-tint),
    border-color calc(var(--dur-look) * var(--motion-tint)) var(--ease-tint);
}

@media (prefers-reduced-motion: no-preference) {
  @media (min-width: 901px) {
    .navgroup[open] .navgroup__sheet {
      animation: sheet-drop 260ms var(--ease) both;
      transform-origin: top center;
    }
  }

  /* The phone's own sheet, which had the same absence. */
  .masthead__more[open] .masthead__sheet {
    animation: sheet-drop 260ms var(--ease) both;
    transform-origin: top center;
  }
}

@keyframes sheet-drop {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The row nudge. `--motion-move` multiplied inline rather than cancelled in a
   reduced-motion block, so the transform is identity and the duration is zero
   in one declaration — the idiom §33 settled on. */
.route,
.mech__row,
.navgroup__sheet a {
  transition: transform calc(var(--dur-tint) * var(--motion-move)) var(--ease-move);
}

@media (hover: hover) and (pointer: fine) {
  .route:hover,
  .mech__row:hover,
  .navgroup__sheet a:hover {
    transform: translateX(calc(3px * var(--motion-move)));
  }
}

/* ==========================================================================
   44. THE ILLUSTRATION BAND
   ==========================================================================
   Component 4, and the ten drawings that go in it. Both are the design
   package's own — `04-illustrations.md` describes them and `Moorwell Site.dc.html`
   draws them; the paths here were taken from the prototype's rendered output
   rather than redrawn, because every coordinate in that file is computed at
   runtime and the source is a class, not a shape.

   Three things were changed in the porting, and each is the package's own
   instruction rather than a preference:

   ## 1. The wash alpha is a token, because the prototype hardcodes it

   Every drawing in the prototype ships `rgba(<hue>, .26)` baked into the
   fill. `03-tokens-and-components.md` is explicit that this is wrong and says
   why: *"Wash alpha is derived from the look, never hardcoded: .26 on Calm
   and Sage, .14 on Night. A fixed alpha makes the drawing the loudest thing
   on the page in dark mode — which happened once and was fixed at the call
   site rather than per-drawing."* So `--illo-wash` is set per look and every
   fill reads it. The prototype has the bug its own documentation records.

   ## 2. The motion is gated

   The prototype animates inline and unconditionally: `zInk` on every stroke,
   `zWash` behind them. `05-implementation.md` puts
   `prefers-reduced-motion` first on its list of things to get right, so the
   inline styles were stripped and re-declared here inside the no-preference
   gate. Under `reduce` the drawing is simply there, which is what
   `04-illustrations.md` says it should be.

   The strokes draw in the order a hand would, which the port preserves as
   `--i` on each path — the prototype expressed the same stagger as a
   per-element delay, and an index is the same information a stylesheet can
   act on.

   ## 3. `pathLength="1"`

   Kept exactly as the prototype has it, and it is better than what this site
   was doing. §32.2's figures measure themselves with `getTotalLength()` in
   `motion.js` because a dash pattern has to match its path; `pathLength="1"`
   tells the browser to normalise the path to one unit and the dash maths
   becomes `1` and `0` for every stroke regardless of geometry. No script, and
   no way for the number to drift from the shape.
   ========================================================================== */

:root {
  --illo-wash: 0.26;
  --illo-ink: var(--text-soft);
}

:root[data-theme="dark"] {
  --illo-wash: 0.14;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --illo-wash: 0.14;
  }
}

.illo {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-glass-lg, 26px);
  background: var(--surface);
  border: 1px solid var(--hairline);
  padding: clamp(0.5rem, 2vw, 1.25rem);
  margin-block: clamp(2rem, 5vw, 3.5rem);
}

.illo__art {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* The horizon, edge to edge. `04-illustrations.md`: it runs the full width of
   the band at y=145 "so the subject stands somewhere rather than floating in
   an empty rectangle". It is drawn by the band rather than by each drawing,
   so ten SVGs cannot disagree about where the ground is. */
/* Drawn inside the SVG rather than by this box — see the note in the markup.
   Quiet, because it is the ground the subject stands on and not part of the
   subject: §32.2's two stroke roles, applied to a drawing rather than a
   diagram. It does not ink itself in; it is simply there, so the drawing
   always has somewhere to stand while the strokes arrive. */
.illo__horizon {
  fill: none;
  stroke: var(--illo-ink);
  stroke-width: 1.2;
  opacity: 0.34;
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* Each stroke draws itself over its own length, in the order a hand would
       draw it, as the band arrives. Scroll-driven rather than on load,
       because a drawing that inked itself in above the fold before anyone
       scrolled to it would have animated to nobody. */
    .illo__art [pathLength] {
      stroke-dasharray: 1;
      animation: illo-ink linear both;
      animation-timeline: view();
      animation-range: entry 8% cover 34%;
      animation-delay: calc(var(--i, 0) * -0.04s);
    }

    .illo__art [fill^="rgba"] {
      animation: illo-wash linear both;
      animation-timeline: view();
      animation-range: entry 22% cover 46%;
    }
  }
}

/* The line arrives before the colour behind it, which is the order a hand
   works in and the order 04-illustrations.md describes. */
@keyframes illo-ink {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}

@keyframes illo-wash {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Paper does not scroll, so a drawing whose strokes are dashed to zero and
   whose wash is at zero prints as an empty band. Same reasoning as §16b's
   print rule for the text entrances, and the same fix: on paper the drawing
   is simply there.

   It is worth being precise about why the zero is acceptable on screen and
   not here. `site_motion_test` floors the TEXT entrances at 0.72 because a
   reader who has not scrolled is a reader being shown a blank page. These
   carry no text — the band is decoration, and every claim near it is written
   out in the section beside it — so the drawing arriving as it is reached is
   the gesture `04-illustrations.md` asks for rather than content withheld.
   Print is the one surface where "as it is reached" never happens. */
@media print {
  .illo__art [pathLength],
  .illo__art [fill^="rgba"] {
    animation: none !important;
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    opacity: 1 !important;
  }
}

/* ==========================================================================
   45. THE 2.0 SHELL — the components, not the values
   ==========================================================================
   Section 40 repainted this site and left it looking like the old one. That
   is the honest description of what happened, and the cause is worth writing
   down because it is a general failure mode: a palette port changes every
   colour on a page without changing a single thing about what the page IS.
   The owner opened the site and said it was "1.0 with a touch-up of 2.0",
   and the measurement agrees — the redesign's own homepage renders 4,305px
   tall at 1440 and this one rendered 11,605px, which is not a page with
   different colours, it is a different page.

   So this section ports the COMPONENTS. Every value in it was read off the
   prototype's rendered DOM with `getComputedStyle` rather than guessed from
   a screenshot, and the ones that carry text were re-solved against the
   contrast contract before being written. Where a number here disagrees
   with an earlier section, this one wins and the earlier rule is left in
   place as history, which is the arrangement sections 31 onward already use.

   WHAT ACTUALLY CHANGES, in the order a reader meets it:

     1. The masthead stops being a bar. It is a floating capsule — a glass
        pill inset from the page edge with a 38px radius, its own shadow and
        a specular top edge. This is the single largest reason the old page
        read as 1.0: a full-bleed strip pinned to the top of the viewport is
        the arrangement every framework ships by default.

     2. The look control stops being four text links and becomes a segmented
        pill, each option carrying a dot in the colour it selects. A control
        that shows its own options is worth the 40px it costs.

     3. The primary action stops being purple and becomes the ink itself —
        near-black on Calm and Sage, pale lilac on Night, which is the same
        inversion the dark panel makes and for the same reason.

     4. The hero loses its drifting five-hue colour field and its rainbow
        hairline. Both were 1.0 signatures. The 2.0 ground is two soft blooms
        behind a vertical gradient, and it belongs to the whole page rather
        than to one band of it.

     5. Every card surface becomes the design's one glass: a 150deg gradient
        from white through white to a lilac-tinted white, a light rim, a
        low-offset shadow and an inset specular line. One material, used at
        four radii.

     6. The radii open up. 14px and 18px were 1.0's; the redesign runs 24,
        30, 32, 38 and 40. A 14px radius on a 565px card is a rectangle with
        the corners knocked off, and it is most of why the old page read as
        tight.

   ONE DELIBERATE DIVERGENCE, and it is an addition rather than a softening.
   The redesign's look control offers three options; this one offers four,
   because the fourth is System and it is the only one that tracks the
   reader's OS. Dropping it would mean a reader who has set their machine to
   dark gets a light website until they notice a control. That is a
   regression dressed as fidelity, so System stays and is styled as a
   sibling of the other three.
   -------------------------------------------------------------------------- */

:root {
  /* The one glass. Every card, the masthead, the secondary button and the
     refusal rows are this surface at different radii. */
  --glass-bg: linear-gradient(150deg,
    rgba(255, 255, 255, 0.90) 0%,
    rgba(255, 255, 255, 0.62) 46%,
    rgba(247, 244, 255, 0.50) 100%);
  --glass-edge: rgba(255, 255, 255, 0.80);
  --card-edge: rgba(255, 255, 255, 0.34);
  --nav-edge: rgba(255, 255, 255, 0.95);
  --card-lift: 0 8px 20px -18px rgba(58, 48, 78, 0.20),
               inset 0 1px 0 var(--glass-edge);
  --nav-lift: 0 14px 34px -26px rgba(58, 48, 78, 0.40),
              inset 0 1px 0 var(--glass-edge);
  /* The nav sheet sits over the page rather than over the bar, so it is
     lifted further than the bar is. 46.19 has the measurement. */
  --sheet-lift: 0 24px 50px -34px rgba(58, 48, 78, 0.50),
                inset 0 1px 0 var(--glass-edge);

  /* The page ground: a vertical wash plus two blooms, one lilac at the top
     right and one sand at the middle left. Fixed sizes rather than viewport
     units — a bloom that resizes with the window is a shape that moves when
     a phone's address bar collapses. */
  --bloom-a: radial-gradient(closest-side, rgba(186, 164, 232, 0.46), rgba(186, 164, 232, 0));
  --bloom-b: radial-gradient(closest-side, rgba(236, 226, 208, 0.66), rgba(236, 226, 208, 0));

  --soft-track: rgba(255, 255, 255, 0.62);
  --field-bg: rgba(255, 255, 255, 0.78);
  --hair: rgba(21, 19, 15, 0.10);
  --foot-bg: #f3f0ea;

  /* The crisis block's own family, which is clay everywhere and is the one
     colour on this site that is allowed to be warm. */
  --clay-fill: rgba(198, 124, 96, 0.12);
  --clay-edge: rgba(198, 124, 96, 0.28);
  --clay-ink: #7b4a34;

  /* The redesign's radii. */
  --r-glass: 24px;
  --r-glass-lg: 30px;
  --r-card: 32px;
  --r-band: 26px;
  --r-panel: 40px;
  --r-nav: 38px;
}

:root[data-theme="sage"] {
  --glass-bg: linear-gradient(150deg,
    rgba(255, 255, 255, 0.92) 0%,
    rgba(252, 254, 248, 0.64) 46%,
    rgba(238, 245, 230, 0.52) 100%);
  --glass-edge: rgba(255, 255, 255, 0.84);
  --bloom-a: radial-gradient(closest-side, rgba(168, 200, 150, 0.50), rgba(168, 200, 150, 0));
  --bloom-b: radial-gradient(closest-side, rgba(230, 232, 214, 0.70), rgba(230, 232, 214, 0));
  --soft-track: rgba(255, 255, 255, 0.70);
  --field-bg: rgba(255, 255, 255, 0.82);
  --hair: rgba(24, 33, 21, 0.10);
  --foot-bg: #eff4e6;
  --clay-edge: rgba(198, 124, 96, 0.30);
}

/* Night's glass is the inverse construction: a pale lilac at low alpha over
   the near-black ground rather than white at high alpha over a light one.
   Both edges lift rather than darken, so a card still reads as in front. */
:root[data-theme="dark"] {
  --glass-bg: linear-gradient(150deg, rgba(206, 190, 240, 0.16) 0%, rgba(160, 146, 196, 0.09) 46%, rgba(120, 110, 150, 0.07) 100%);
  --glass-edge: rgba(232, 224, 252, 0.22);
  --card-edge: rgba(214, 202, 244, 0.10);
  --nav-edge: rgba(220, 208, 248, 0.26);
  --bloom-a: radial-gradient(closest-side, rgba(150, 120, 214, 0.40), rgba(150, 120, 214, 0));
  --bloom-b: radial-gradient(closest-side, rgba(84, 104, 138, 0.32), rgba(84, 104, 138, 0));
  --soft-track: rgba(214, 202, 244, 0.12);
  --field-bg: rgba(214, 202, 244, 0.09);
  --hair: rgba(214, 202, 244, 0.16);
  --foot-bg: #110e1a;
  --clay-fill: rgba(226, 150, 120, 0.13);
  --clay-edge: rgba(226, 150, 120, 0.28);
  --clay-ink: #f0bba4;
}

/* Sage and Night both need to win over a `prefers-color-scheme` block, and
   the OS-dark case is the one that bit section 40: a reader on a dark
   machine who has chosen Sage was being handed Night's tokens. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) {
    --glass-bg: linear-gradient(150deg, rgba(206, 190, 240, 0.16) 0%, rgba(160, 146, 196, 0.09) 46%, rgba(120, 110, 150, 0.07) 100%);
    --glass-edge: rgba(232, 224, 252, 0.22);
    --card-edge: rgba(214, 202, 244, 0.10);
    --nav-edge: rgba(220, 208, 248, 0.26);
    --bloom-a: radial-gradient(closest-side, rgba(150, 120, 214, 0.40), rgba(150, 120, 214, 0));
    --bloom-b: radial-gradient(closest-side, rgba(84, 104, 138, 0.32), rgba(84, 104, 138, 0));
    --soft-track: rgba(214, 202, 244, 0.12);
    --field-bg: rgba(214, 202, 244, 0.09);
    --hair: rgba(214, 202, 244, 0.16);
    --foot-bg: #110e1a;
    --clay-fill: rgba(226, 150, 120, 0.13);
    --clay-edge: rgba(226, 150, 120, 0.28);
    --clay-ink: #f0bba4;
  }
}

/* --------------------------------------------------------------------------
   45.1 The ground
   --------------------------------------------------------------------------
   Three layers on one pseudo-element rather than the prototype's three
   absolutely-positioned divs, because the site cannot add markup to thirteen
   pages for a decoration. Background layers clip to the padding box, which
   is what the prototype's `overflow: hidden` wrapper was there to do.

   The blooms are sized in pixels, not viewport units, and that is deliberate:
   a bloom measured in vh changes shape when a phone's address bar collapses,
   which is a moving background on a page whose whole argument is that
   nothing moves on its own.

   `z-index: -1` puts it above the body's own background and below every
   in-flow element, so no page needs a stacking rule of its own. -------- */

body {
  position: relative;
  background-image: none;
}

body::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    var(--bloom-a) no-repeat,
    var(--bloom-b) no-repeat,
    var(--ground) no-repeat;
  background-position:
    right -140px top -200px,
    left -240px top 360px,
    left top;
  background-size:
    840px 740px,
    780px 680px,
    100% 1240px;
}

/* THE 1.0 HERO IS GONE, and these two rules are the removal.

   `.hero::before` was a five-hue radial field on a 44-second drift and
   `.hero::after` was a rainbow hairline under it. Together they were the
   most recognisable thing about the old page — the redesign's hero sits on
   the page's own ground with nothing behind it but the blooms above, and
   the difference is most of what "it still looks like 1.0" meant.

   Set to `none` rather than deleted so that the rules above stay readable as
   what they were, and so a future reader can see what was removed and when.
   The `@keyframes drift` they used is now referenced by nothing. */
.hero::before,
.hero::after {
  content: none;
}

.hero {
  overflow: visible;
}

/* One word of the headline was set in an italic serif. That was 1.0's warm
   note and the redesign has none: the display is one face at one weight,
   and the emphasis comes from size and tracking. */
.display em,
.h1 em,
.h2 em {
  font-style: normal;
  font-family: inherit;
  color: inherit;
}

/* --------------------------------------------------------------------------
   45.2 The measure
   --------------------------------------------------------------------------
   1148px of content inside a 1240px shell, which is the redesign's own
   arrangement and 108px wider than this site's was. 46px of gutter at the
   top end, collapsing to 20px on a phone. -------------------------------- */

.wrap {
  --wrap-gutter: clamp(1.25rem, 4vw, 2.875rem);
  max-width: 1240px;
  padding-inline: var(--wrap-gutter);
}

/* --------------------------------------------------------------------------
   45.3 The masthead becomes a capsule
   -------------------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 60;
  padding: 16px 30px 6px;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 0;
}

/* The scroll edge belonged to a bar that met the page along a straight line.
   A capsule has air on all four sides, so there is no edge to dissolve. */
.masthead::after {
  content: none;
}

.masthead__inner {
  max-width: 1240px;
  margin-inline: auto;
  min-height: 70px;
  padding: 10px 10px 10px 22px;
  gap: var(--s-5);
  flex-wrap: wrap;
  justify-content: flex-start;
  border-radius: var(--r-nav);
  background: var(--glass-bg);
  border: 1px solid var(--nav-edge);
  box-shadow: var(--nav-lift);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
}

.brand {
  gap: var(--s-3);
  font-size: 21px;
  font-weight: 500;
  letter-spacing: -0.03em;
}

/* The mark is the ink, not the accent. On the capsule's near-white ground a
   lilac pebble at 22px is a spot of colour with no job; the wordmark and the
   mark should read as one object. */
.brand svg {
  color: currentColor;
}

/* The nav takes the room between the wordmark and the controls, so the look
   control and the action sit at the right end where the prototype puts them
   regardless of how many groups the nav has. */
.masthead__nav {
  flex: 1;
  gap: 2px;
}

/* --------------------------------------------------------------------------
   45.4 The look control becomes a segmented pill
   --------------------------------------------------------------------------
   Four options, not the prototype's three — System is kept, and the reason
   is in this section's header. Each option carries a dot in the colour it
   selects, so the control shows its own options rather than naming them;
   System's dot is a ring, because "whatever your machine says" is not a
   colour. ---------------------------------------------------------------- */

:root[data-js] .theme {
  margin-inline-start: 0;
}

.theme__set {
  gap: var(--s-1);
  padding: 5px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--soft-track);
  overflow: visible;
}

.theme__opt {
  gap: var(--s-2);
  min-height: 32px;
  padding-inline: 12px;
  font-family: var(--font-head);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1;
  color: var(--text-soft);
  opacity: 1;
  border-radius: var(--r-pill);
}

.theme__opt::before {
  content: "";
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--dot, currentColor);
  flex: none;
}

#theme-system + .theme__opt { --dot: transparent; }
#theme-system + .theme__opt::before { box-shadow: inset 0 0 0 1.5px currentColor; }
#theme-light  + .theme__opt { --dot: #baa4e8; }
#theme-sage   + .theme__opt { --dot: #a8c896; }
#theme-dark   + .theme__opt { --dot: #9678d6; }

.theme__set input:checked + .theme__opt {
  background: var(--panel-dark);
  color: var(--panel-on);
  font-weight: 600;
}

.theme__set input:checked + #theme-system + .theme__opt,
.theme__set input:checked + .theme__opt::before {
  /* The selected chip's dot keeps its own hue rather than inheriting the
     chip's ink, so the control still reads as a colour swatch when picked. */
  background: var(--dot, currentColor);
}

/* --------------------------------------------------------------------------
   45.4b The phone bar — 2026-10-11
   --------------------------------------------------------------------------
   Measured before this: **214px, a quarter of a 390px screen**, sticky, in
   three rows — brand + More, the colour capsule, then the button.

   It is one row now, and it answers the two questions a header is for:

       [Pebble]   Evidence                    (menu)        [colour]

   The page's own name IS the menu trigger. One target instead of a label
   beside a button, and the thing you tap to go elsewhere is the thing that
   tells you where you are.

   ⚠️ **The name comes from `body[data-page]`, not from the masthead.**
   `site_claims_test` requires every page to carry `index.html`'s masthead
   verbatim — only `aria-current` may differ. A per-page `<span>` in the bar
   breaks that, and it did. The span is identical everywhere and empty; the
   mapping is below, read off the body, which is outside the slice. It is
   `aria-hidden` because the nav already announces the current page and the
   summary already has its own label.
   -------------------------------------------------------------------------- */

.masthead__here,
.masthead__look-toggle,
.masthead__look-btn {
  display: none;
}

@media (max-width: 900px) {
  .masthead__inner {
    position: relative;
    flex-wrap: nowrap;
  }

  /* 🔴 **The nav is not just invisible here, it is gone — 2026-10-11.**
     Its links are hidden at this tier and the sheet carries them instead, but
     the `<nav>` itself stayed a flex item: 26px wide, `flex: 1 1 0%`, quietly
     absorbing the free space the title needed. That is why the title sat
     marooned in the middle of the bar with a gap on either side and the icon
     nowhere near the edge — nothing was wrong with the title's own `flex`,
     it was competing with a box nobody could see. */
  .masthead__nav {
    display: none;
  }

  /* The title fills the bar, so the whole middle is the tap target. */
  .masthead__more {
    flex: 1 1 auto;
    min-inline-size: 0;
    order: 2;
  }

  .masthead__more > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    inline-size: 100%;
    min-block-size: 40px;
    padding-inline: 12px;
    border-radius: var(--r-pill);
  }

  .masthead__more > summary::after { content: none; }

  .masthead__here {
    display: block;
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-head);
    font-size: 1.0625rem;
    color: var(--text);
  }

  body[data-page="home"] .masthead__here::after { content: "Moorwell"; }
  body[data-page="how"] .masthead__here::after { content: "How it works"; }
  body[data-page="evidence"] .masthead__here::after { content: "Evidence"; }
  body[data-page="safety"] .masthead__here::after { content: "Safety"; }
  body[data-page="data"] .masthead__here::after { content: "Privacy"; }
  body[data-page="about"] .masthead__here::after { content: "About"; }
  body[data-page="changelog"] .masthead__here::after { content: "Changelog"; }
  body[data-page="privacy"] .masthead__here::after { content: "Privacy policy"; }

  .masthead__more-icon { flex: none; opacity: 0.7; }

  /* One name in the bar: the Pebble already says which site this is. */
  .brand__name {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* The colour control keeps its own disclosure, so "change the look" and
     "go somewhere" stay two answers to two questions. */
  /* 🔴 **A checkbox, not a `<details>` — 2026-10-11.**
     The colour control was wrapped in `<details>` with `display: contents` so
     the desktop bar could render the fieldset inline as before. It cannot:
     `display: contents` removes the box, but the browser still hides the
     non-summary children of a CLOSED `<details>`, so on the desktop the
     switcher measured **0px wide and was simply gone** — which is what "theme
     switching doesn't work" turned out to mean. A checkbox has no such
     behaviour, needs no script either, and `~` reaches the fieldset because
     the two are already siblings. */
  .masthead__look-btn {
    display: grid;
    place-items: center;
    inline-size: 40px;
    block-size: 40px;
    order: 3;
    flex: none;
    border-radius: 50%;
    cursor: pointer;
    color: var(--text-soft);
  }

  .masthead__look-toggle:checked ~ .masthead__look-btn,
  .masthead__look-btn:hover,
  .masthead__more[open] > summary,
  .masthead__more > summary:hover {
    background: var(--soft-track);
    color: var(--text);
  }

  .masthead__look-toggle:focus-visible ~ .masthead__look-btn {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }

  /* The swatch is the accent itself, so the control shows what it changes
     rather than naming it. */
  .masthead__look-dot {
    inline-size: 15px;
    block-size: 15px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 1px var(--nav-edge);
  }

  :root[data-js] .theme {
    display: none;
  }

  /* Full width of the bar, and aligned to it — against the 40px button a
     296px capsule had nowhere to go but off the side of the screen. */
  :root[data-js] .masthead__look-toggle:checked ~ .theme {
    display: flex;
    justify-content: center;
    position: absolute;
    inset-inline: 0;
    margin-inline: auto;
    inline-size: fit-content;
    max-inline-size: 100%;
    inset-block-start: calc(100% + 8px);
    z-index: 2;
  }

  /* The sheet, given the same treatment as the bar rather than being a bare
     list under it. */
  /* The sheet is the bar's own material, not a flat dark slab under it: same
     glass, same edge, same lift. Two surfaces one tap apart reading as two
     different components is what made it look unfinished. */
  .masthead__sheet {
    inset-inline: 0;
    padding: 8px;
    border-radius: var(--r-nav);
    /* ⚠️ The tint sits ON an opaque base, and the base is not optional.
       Section 6 records the phone sheet as "opaque rather than glass" and that
       was hard-won: `--glass-bg` is a 7-16% gradient, which is right for a bar
       floating over a ground and unreadable over a scrolling page. Layering
       gives the sheet the bar's own colour without letting the text behind it
       through. */
    /* 🔴 **The tint has to be baked in — 2026-10-11.**
       `.navgroup__sheet` is `--glass-bg` over `--surface` and that reads right
       on the desktop, because the dropdown and the bar above it sit over the
       SAME ground: both take their colour from the hero's bloom through the
       glass. This sheet cannot. It must be opaque — section 6 is explicit, and
       glass over a scrolling page is unreadable — and `--surface` in the dark
       palette is `#19171f`, which is near-neutral. Copying the desktop's two
       declarations therefore produced a flat near-black slab hanging off a
       purple bar: identical CSS, completely different result.
       So the purple the bar gets from its backdrop is added here as a layer
       instead: `--soft-track` over `--surface`, under the same gradient. */
    background-image:
      var(--glass-bg),
      linear-gradient(0deg, var(--soft-track), var(--soft-track));
    background-color: var(--surface);
    border: 1px solid var(--nav-edge);
    box-shadow: var(--nav-lift);
  }

  .masthead__sheet a {
    padding: 12px 14px;
    border-radius: calc(var(--r-nav) - 8px);
    font-size: 1rem;
  }

  /* The page you are on, marked in the list you opened to leave it. Matched
     on the body's own identity, because these links carry no `aria-current`
     — that lives in the nav, and the nav is not on screen at this tier. */
  body[data-page="how"] .masthead__sheet a[href="how.html"],
  body[data-page="evidence"] .masthead__sheet a[href="evidence.html"],
  body[data-page="safety"] .masthead__sheet a[href="safety.html"],
  body[data-page="about"] .masthead__sheet a[href="about.html"],
  body[data-page="home"] .masthead__sheet a[href="index.html"],
  body[data-page="data"] .masthead__sheet a[href="data.html"] {
    /* Accent tint, not `--soft-track` — the same white-on-white the desktop
       markers had. `--soft-track` is white in Calm and Sage, so this card was
       invisible in both. */
    background: var(--soft-track);
    background: var(--wash-strong);
    color: var(--text);
    font-weight: 600;
  }

  /* The hero carries the same action immediately below, so at the top of the
     page this was the same button twice within 150px. ⚠️ The landing page is
     eleven thousand pixels on a phone; the footer has the other one.
     Deleting this rule puts it back. */
  .masthead__go {
    display: none;
  }
}

@media (max-width: 380px) {
  .masthead__go-name {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* --------------------------------------------------------------------------
   45.5 The action is the ink
   --------------------------------------------------------------------------
   Filled with `--panel-dark` and not with `--accent`, so the primary button
   and the dark panel are the same object at two sizes — and so Night's
   button inverts to pale lilac on dark ink exactly as the panel does. On
   Calm that measures 16.90:1, on Sage 13.38:1, on Night 10.25:1. ------- */

.btn--primary {
  background: var(--panel-dark);
  color: var(--panel-on);
  border-color: transparent;
  box-shadow: none;
}

.btn--primary:hover {
  background: var(--panel-dark);
  color: var(--panel-on);
  filter: brightness(1.12);
}

.btn {
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-size: 15.5px;
  font-weight: 600;
  letter-spacing: 0;
  padding-inline: 28px;
  min-height: 58px;
}

.btn--small {
  font-size: 14.5px;
  padding-inline: 22px;
  min-height: 48px;
}

/* The secondary action is the glass, which is the same surface as the cards
   below it — so "See what it does" reads as a thing on the page rather than
   as a button with a border drawn round it. */
.btn--quiet {
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: 0 8px 20px -18px rgba(58, 48, 78, 0.20);
  color: var(--text);
}

.btn--quiet:hover {
  background: var(--glass-bg);
  color: var(--text);
  filter: brightness(1.03);
}

/* Inside the dark panel the fill inverts: the page's ink becomes the
   button's ground. */
.panel .btn--primary {
  background: var(--panel-on);
  color: var(--panel-dark);
}

/* --------------------------------------------------------------------------
   45.6 The per-section colour fields are retired
   --------------------------------------------------------------------------
   `.aura` is 1.0's lit ground: a five-hue radial field dropped behind
   individual sections, ten of them across the site. The redesign has exactly
   two blooms and they belong to the page, not to a band — which is why a
   2.0 page reads as one surface with things on it and the old one read as a
   stack of separately-coloured strips.

   `display: none` rather than deletion of the markup, for now: the divs are
   `aria-hidden` and carry no content, so an invisible one costs a reader
   nothing, and removing them from thirteen files is a separate edit that
   should be reviewable on its own. The rules above are left intact as the
   record of what the field was and what it measured.

   This also fixes a real defect introduced two rules ago: `.aura` is
   `inset: -18%`, and it was only ever kept on screen by its section's
   `overflow: hidden`. Opening the hero up pushed the page's scrollWidth to
   1699 against a 1440 viewport — a sideways scroll on the landing page. */
.aura,
.aura__core {
  display: none;
}

/* --------------------------------------------------------------------------
   45.7 The mood ladder behind the hero pane is retired
   --------------------------------------------------------------------------
   `.mood-field` is the app's five-band mood scale at full chroma, offset
   behind the hero's demo card like a misregistered print run. It is a good
   idea and it is 1.0's: the redesign puts the demo card on the page's own
   ground with nothing behind it, and the colour it needs comes from the
   blooms. Two saturated objects in a hero is one too many, and this was the
   loudest thing on the page after the headline. */
.mood-field {
  display: none;
}

/* --------------------------------------------------------------------------
   45.8 The type scale
   --------------------------------------------------------------------------
   Read off the prototype at 1440: display 68/69.4 at -0.04em, h2 44/47.5 at
   -0.035em, h3 26/29.9 at -0.03em, eyebrow 11 at 0.16em, all Space Grotesk
   at 500. The weight is the part that matters most and it is the part a
   screenshot hides: this site set its display at 700, and a heavy grotesque
   at 68px is a different voice from a medium one — louder, tighter, more
   like an advertisement. The redesign's headline is large and calm, which
   is 500.

   The steps stay clamped rather than fixed, because the prototype is a
   fixed-width artboard and this is a website. Each clamp is written so its
   upper bound lands exactly on the prototype's number at 1440. ---------- */

:root {
  --t-display: clamp(2.5rem, 1.35rem + 3.4vw, 4.25rem);
  --t-1: clamp(2.25rem, 1.5rem + 2.2vw, 3.5rem);
  --t-2: clamp(1.875rem, 1.4rem + 1.9vw, 2.75rem);
  --t-3: clamp(1.25rem, 1.13rem + 0.55vw, 1.625rem);
  --t-lead: clamp(1.0625rem, 1rem + 0.36vw, 1.1875rem);

  --tr-display: -0.04em;
  --tr-1: -0.035em;
  --tr-2: -0.035em;
  --tr-3: -0.03em;
}

.display,
.h1,
h1.display {
  font-weight: 500;
  line-height: 1.02;
}

.h2,
h2 {
  font-weight: 500;
  line-height: 1.08;
}

.h3,
h3 {
  font-weight: 500;
  line-height: 1.15;
}

.eyebrow {
  font-size: 11px;
  letter-spacing: 0.16em;
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   45.9 The stat row
   --------------------------------------------------------------------------
   26px figures, not 44px ones, and a hairline between them rather than under
   each. The old arrangement drew a rule above every stat, which turns three
   facts into three cards; the redesign rules BETWEEN them, which turns them
   into one row. -------------------------------------------------------- */

.stats {
  gap: 0;
  border-top: 1px solid var(--hair);
  padding-top: 20px;
  margin-top: var(--s-7);
}

.stat {
  padding-inline: 22px;
  border-left: 1px solid var(--hair);
}

.stat:first-child {
  padding-inline-start: 0;
  border-left: 0;
}

.stat::before {
  content: none;
}

.stat__n {
  font-size: 1.625rem;
  line-height: 1;
  letter-spacing: -0.02em;
  min-height: 0;
}

.stat__n--word {
  font-size: 1.625rem;
  line-height: 1;
}

.stat__k {
  margin-top: var(--s-3);
}

/* --------------------------------------------------------------------------
   45.10 Ori, and the hero's check-in card
   --------------------------------------------------------------------------
   Every proportion below is the prototype's pixel value divided by its 52px
   box, so the companion is resolution-independent and one `--ori` sets its
   size. The eyes are taller than they are wide (19% × 25.5%), which is the
   whole trick: a circle reads as a dot and an oval reads as an eye.

   THE COLOURS ARE FIXED RATHER THAN TOKENISED, and that is deliberate. Ori
   is a character, and a character whose face changes colour with the site's
   look is a different character three times. The design file says the same
   thing in one line — *"the same creature in every look"*. What does change
   is the card underneath, which is the site's glass.
   -------------------------------------------------------------------------- */

/* `.ori` RETIRED 2026-09-03 — see §49.
   The mascot was called Ori before the 2.0 spec renamed it Pebble, and this
   was its drawing: the same geometry, frozen at mood 2, in Calm's colours,
   with no way to say anything else. §49 is the design's own implementation
   and index.html now uses it, so this was a second copy of one component —
   the exact duplication the design language is supposed to prevent. */

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

.checkin__ask {
  display: block;
}

.checkin__q {
  display: block;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--text);
}

.checkin__sub {
  display: block;
  margin-top: var(--s-2);
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--text-soft);
}

/* The card. 38px radius and a deep, far-offset shadow — this is the one
   object on the page allowed to float, because it is the only one a reader
   can touch. */
.hero__exhibit {
  border-radius: 38px;
  padding: 26px;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: 0 26px 60px -40px rgba(58, 48, 78, 0.6),
              inset 0 1px 0 var(--glass-edge);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
}

.hero__exhibit .glass__pad {
  padding: 0;
}

/* THE ANSWER IS A PANEL, NOT A PARAGRAPH.
   In the old arrangement the app's reply was body copy under a hairline,
   which made it look like a caption on the slider. It is the thing the whole
   card exists to show, so it gets the ink: the same near-black panel as the
   page's statement, at 26px radius. */
.demo__reply {
  border: 0;
  padding: 0;
  margin-top: 20px;
}

.demo__line {
  margin: 0;
  border-radius: 26px;
  padding: 20px;
  background: var(--panel-dark);
  color: var(--panel-on);
  font-family: var(--font-head);
  font-size: 19px;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.02em;
}

.demo__offers {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.demo__offer {
  display: block;
  border-radius: 22px;
  padding: 14px 16px;
  background: var(--soft-track);
  border: 1px solid var(--card-edge);
}

.demo__kind {
  display: block;
  font-family: var(--font-head);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.demo__thing {
  display: block;
  margin-top: var(--s-2);
  font-size: 14px;
  line-height: 1.45;
  color: var(--text);
}

.checkin__note {
  margin-top: 16px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-soft);
}

/* The mood control, at the redesign's values. The track is the app's own
   five-band scale rendered as one continuous ramp rather than five hard
   stops: the app shows bands because a band is a category the reader is
   choosing, and the site shows a ramp because nothing here is being
   recorded. */
.mood {
  margin-top: var(--s-6);
}

.mood__word {
  font-family: var(--font-head);
  font-size: 44px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--text);
}

.mood__range {
  margin-top: var(--s-5);
}

.mood__ends {
  font-size: 11.5px;
  line-height: 1;
  color: var(--text-soft);
}

/* --------------------------------------------------------------------------
   45.11 Two corrections found by looking at the page
   --------------------------------------------------------------------------
   ORDER. `.theme` carries `order: 9` from section 25.7, which put the look
   control at the end of a bar whose last item was the action. Now that the
   action follows it in the markup, order 9 sorts the control BEHIND the
   button and the two swap. The action is given an order of its own rather
   than stripping the control's, because "the action is last" is the rule
   that should be written down. */
.masthead__go {
  order: 10;
}

/* THE FULL-BLEED TINTED BANDS GO.
   `.statement` and `.section--sunk` each paint an opaque strip across the
   viewport — `--surface` and `--surface-sunk` — and they are 1.0's way of
   giving a long page rhythm. They are also the reason the page reads as a
   stack of strips rather than as one surface: at the hero's lower edge the
   ground gradient met a hard horizontal line where `.statement` began.

   The redesign gives a page its rhythm with objects instead — the dark
   panel, the illustration band, the mosaic — all of which sit ON the ground
   with air around them. So these two become transparent and the ground shows
   through. The footer keeps its own fill, because a footer is a different
   region rather than a louder section. */
.statement,
.section--sunk {
  background: none;
}

/* --------------------------------------------------------------------------
   45.12 The step row, the glass card, and the mosaic
   --------------------------------------------------------------------------
   One material at four radii, which is the redesign's whole card system:

     .glass-card   the surface itself, used by everything below
     .step         32px — a card with a number, three across
     .bento__cell  30px — a card with a family tint, in a six-column mosaic
     .row          24px — a line of two facts, stacked

   The tint on a mosaic cell is a gradient from the family hue at 0.24 to
   white at 0.06, not a flat fill: a flat tint at that alpha reads as a
   coloured rectangle, and the gradient reads as light falling across a
   surface. Clay is the exception at 0.20, because it is the only hue on the
   warm side and it carries further at the same number. --------------- */

.glass-card,
.step,
.row {
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--s-5);
  margin: var(--s-8) 0 0;   /* was a raw value; snapped to the --s- scale */
  padding: 0;
  list-style: none;
}

.step {
  border-radius: var(--r-card);
  padding: 26px;
}

.step h3 {
  margin: 0 0 var(--s-3);
}

.step p {
  margin: 0;
  color: var(--text-soft);
}

.eyebrow--sm {
  font-size: 10.5px;
  letter-spacing: 0.14em;
  margin-bottom: var(--s-2);
}

/* A section head's lede sits under the heading and is the one place on the
   page where a paragraph is allowed to be wider than the measure — it is
   read as a subtitle rather than as prose. */
.section__head .lead {
  margin-top: var(--s-5);
  max-width: 54ch;
}

/* --------------------------------------------------------------------------
   45.13 The mosaic, and the illustration band's tint
   -------------------------------------------------------------------------- */

.bento {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--s-5);
  margin-top: var(--s-8);   /* was a raw value; snapped to the --s- scale */
}

.bento__cell {
  grid-column: span 6;
  border-radius: var(--r-glass-lg);
  padding: 24px;
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
  /* The family tint. `--tint-a` is 0.24 for every hue but clay, which is the
     only warm one and carries further at the same alpha. */
  background: linear-gradient(150deg,
    rgba(var(--hue), var(--tint-a, 0.24)),
    rgba(255, 255, 255, 0.06));
}

.bento__cell.hue-clay { --tint-a: 0.20; }

/* Night's tints are halved. The alphas above were solved over a near-white
   ground; the same numbers over #0C0A12 make a cell brighter than the page's
   own text, which is a card shouting its own colour. */
:root[data-theme="dark"] .bento__cell,
:root[data-theme="dark"] .illo {
  --tint-a: 0.13;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .bento__cell,
  :root:not([data-theme="light"]):not([data-theme="sage"]) .illo {
    --tint-a: 0.13;
  }
}

:root[data-theme="dark"] .bento__cell.hue-clay { --tint-a: 0.11; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .bento__cell.hue-clay {
    --tint-a: 0.11;
  }
}

@media (min-width: 760px) {
  .bento__cell--half  { grid-column: span 3; }
  .bento__cell--third { grid-column: span 2; }
}

.bento__cell h3 {
  margin: 0 0 8px;
}

.bento__cell p {
  margin: 0;
  color: var(--text-soft);
}

/* THE BAND WAS WHITE ON WHITE.
   `.illo` was painted `--surface` over a `--page` ground, and those two are
   four hundredths of a step apart on Calm — so the drawing floated in
   nothing and the band it is supposed to sit in was invisible. The redesign
   gives the band the same tinted glass as a mosaic cell, which is also what
   makes the illustration read as belonging to the section it introduces
   rather than as a picture dropped between two of them. */
.illo {
  border-radius: var(--r-band);
  border: 1px solid var(--card-edge);
  background: linear-gradient(150deg,
    rgba(var(--hue, var(--rgb-sage)), var(--tint-a, 0.24)),
    rgba(255, 255, 255, 0.04));
}

/* --------------------------------------------------------------------------
   45.14 Two things the hue classes were doing that 2.0 does not
   --------------------------------------------------------------------------
   A `hue-*` class carried three jobs in 1.0: it tinted the card, it coloured
   the heading, and it coloured the card's link. The redesign keeps the first
   and drops the second — every card title on a 2.0 page is the page's ink,
   whatever the card's tint is.

   The reason is legibility before taste. A tinted heading over a tinted
   ground is two values of the same hue, and the pair that measured worst
   here was clay on clay at 4.9:1 — passing, but the lowest-contrast heading
   on the site, sitting on the card a reader in a hurry scans first. Ink on
   the same tint measures 15.7:1. It is also what makes a mosaic read as one
   object rather than as seven differently-branded ones. */
.bento__cell h3,
.step h3,
.moment h3 {
  color: var(--text);
}

/* A section head stacks: eyebrow, heading, then the lede beneath it. Section
   31 splits the head into two tracks, which is the right arrangement for the
   editorial rows further down the page — heading on the left, the list it
   introduces on the right — but wrong for a heading that is introducing the
   full-width thing directly below it. The split put "What is in it" on the
   left and its own subtitle in a column on the right, with the mosaic
   underneath spanning both. */
.section__head {
  display: block;
}

/* --------------------------------------------------------------------------
   45.15 The claim row, the arrow link, and the panel's furniture
   --------------------------------------------------------------------------
   A row is the fourth radius of the one glass: 24px, a claim on the left and
   the name of the test that holds it on the right, in the mono face. The
   pairing is the whole point of the component — a promise with nothing in
   the right-hand column is only a promise — so the test name is not small
   print, it is the second half of the sentence. ----------------------- */

.rows {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.row {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
  border-radius: var(--r-glass);
  padding: 18px 20px;
}

.row__claim {
  flex: 1;
  min-width: 0;
}

.row__claim b {
  display: block;
  font-weight: 600;
  color: var(--text);
}

.row__claim p {
  margin: var(--s-2) 0 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--text-soft);
}

.row__test {
  flex: none;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-soft);
  white-space: nowrap;
}

/* Below the split's breakpoint the row stacks, because a 22-character test
   name and a 46-character claim cannot share 340px without one of them
   wrapping mid-word. */
@media (max-width: 720px) {
  .row {
    flex-direction: column;
    gap: var(--s-3);
  }
  .row__test {
    white-space: normal;
  }
}

/* The arrow travels on hover and collapses under reduced motion, which is
   section 33's two-channel rule: the arrow is position, so it is
   `--motion-move`. */
.arrowlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.arrowlink::after {
  content: "→";
  transition: transform calc(var(--dur-tint) * var(--motion-move)) var(--ease-move);
}

@media (hover: hover) and (pointer: fine) {
  .arrowlink:hover::after {
    transform: translateX(calc(4px * var(--motion-move)));
  }
}

.section__more {
  margin-top: 24px;
}

.panel__actions {
  margin: 32px 0 0;
}

/* Inside the panel the stat rules are the panel's ink at low alpha, not the
   page's hairline — a rule in `--hair` over near-black is invisible. */
.panel .stats {
  border-top-color: color-mix(in srgb, var(--panel-on) 22%, transparent);
  margin-top: var(--s-8);   /* was a raw value; snapped to the --s- scale */
}

.panel .stat {
  border-left-color: color-mix(in srgb, var(--panel-on) 22%, transparent);
}

.panel .stat__n,
.panel .h2,
.panel .lead {
  color: var(--panel-on);
}

.panel .stat__k,
.panel .eyebrow {
  color: color-mix(in srgb, var(--panel-on) 72%, transparent);
}

/* --------------------------------------------------------------------------
   45.16 A head inside a split does not split again — and a §37 defect
   --------------------------------------------------------------------------
   TWO THINGS, and the second is a bug that was already in the file.

   **A head in a split stacks.** Section 31 turns any `.section__head` with
   two or more children into a two-track grid above 900px, which is the right
   default: a heading on the left and its lede on the right is the redesign's
   own editorial arrangement. It is the wrong thing inside `.split`, where
   the head is ALREADY the narrow column of a two-track layout — splitting it
   again gives four columns across 1148px and a heading that wraps to three
   lines in 340px. Scoped at (0,2,0) so it beats §31's `:has()` at equal
   specificity by order, which is how the rest of this file resolves its own
   tiers.

   **Section 37 puts two children in one cell.** Its last rule sends every
   child from the third onward to `grid-column: 2; grid-row: 1 / 3` — every
   one, to the same cell. With an eyebrow, a heading and a lede that is three
   children and it is correct. Add a fourth (a link under the lede, which is
   exactly what the refusals head needs) and the fourth is painted ON TOP of
   the third: two paragraphs of body copy overlapping, legible as neither.

   It had never been hit because no head on this site had four children until
   now. The fix keeps the lede spanning both rows and lets anything after it
   take the next row in the same track, which is what a reader expects of two
   stacked paragraphs.
   -------------------------------------------------------------------------- */

.split .section__head {
  display: block;
  max-width: none;
}

@media (min-width: 900px) {
  .section__head:has(> .eyebrow:first-child) > :nth-child(n + 4) {
    grid-column: 2;
    grid-row: auto;
    max-width: 46ch;
  }
}

/* --------------------------------------------------------------------------
   45.17 The guards and the limit, side by side
   --------------------------------------------------------------------------
   Two half-width cards stacked left leave the right half of the section empty
   twice over. They are a pair — what the app does at the worst moment, and
   the size of what it claims — so they are laid out as one.

   The pair does not collapse to a single column until 860px, below which two
   cards of prose at 340px each is worse than one of 680. */
.sci-pair {
  display: grid;
  gap: var(--s-5);
  margin-top: var(--s-8);   /* was a raw value; snapped to the --s- scale */
}

@media (min-width: 860px) {
  .sci-pair {
    grid-template-columns: 1fr 1fr;
    align-items: start;
  }
}

.sci-pair .sci {
  margin: 0;
  max-width: none;
}

/* --------------------------------------------------------------------------
   45.18 The phone
   --------------------------------------------------------------------------
   Two defects, both found by looking at 390px rather than by measuring.

   THE LOOK CONTROL RAN OFF THE CAPSULE. Four chips at 12px with 12px of
   padding each want 320px; the capsule's content box at 390 is 298. "Night"
   was clipped by the capsule's own rounded edge — not overflowing the page,
   so no overflow sweep would ever have caught it, and not failing any
   contrast pair either. It was simply cut in half.

   The fix is the chips' own padding rather than the font size: 12px is
   already the floor for a control label, and a chip that is 9px of padding
   instead of 12 is still a 32px-tall target with 8px of dot beside it. Four
   chips then want 284px and fit with room.

   THE STAT ROW WRAPPED INTO A BROKEN GRID. `.stats` is an auto-fit grid and
   `.stat` carries a left rule, which is correct in one row and wrong the
   moment it becomes two: the item that starts the second row keeps a rule on
   its left, pointing at nothing, and only the very first item is exempt. So
   below the point where three figures stop fitting, the row becomes a
   column and the rule moves to the top edge — the same hairline, doing the
   same job, along the axis the items are actually arranged on.
   -------------------------------------------------------------------------- */

@media (max-width: 560px) {
  .theme__opt {
    padding-inline: 9px;
    gap: var(--s-2);
  }
}

@media (max-width: 700px) {
  .stats {
    grid-template-columns: 1fr;
    gap: var(--s-5);
  }

  .stat {
    padding-inline: 0;
    padding-top: 18px;
    border-left: 0;
    border-top: 1px solid var(--hair);
  }

  .stat:first-child {
    padding-top: 0;
    border-top: 0;
  }

  .panel .stat {
    border-top-color: color-mix(in srgb, var(--panel-on) 22%, transparent);
  }
}

/* --------------------------------------------------------------------------
   45.19 The remaining fifty-three flat cards
   --------------------------------------------------------------------------
   A sweep of every page at 1440 found fifty-three elements still painting a
   flat `--surface` fill: the helpline directory on safety.html, the release
   rows on changelog.html, the test chips beside each commitment, the fact
   tables, the design page's swap panes and its one spec card. Section 45.12
   gave the landing page's cards the 2.0 glass and left every interior page
   on 1.0's white — which is exactly the half-migration this whole section
   exists to undo, repeated one level down.

   `.ui-card` IS DELIBERATELY NOT IN THIS LIST, and that is the one exception
   worth stating. Those are the app reproductions on highlights.html, and
   that page says in its own words that the frame belongs to the site and
   everything inside it belongs to the app — that it does not follow this
   page's light or dark setting, because tinting a reproduction to match a
   web page is precisely what a reproduction may not do. They keep
   `--app-card`.
   -------------------------------------------------------------------------- */

.facts,
.release,
.inv__test,
.dir,
.swap__pane,
.spec__card {
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

/* The two that are rows rather than cards take the row radius, so a list of
   seventeen helplines does not read as seventeen separate panels. */
.dir,
.inv__test {
  border-radius: var(--r-glass);
}

.release,
.facts,
.swap__pane {
  border-radius: var(--r-glass-lg);
}

/* --------------------------------------------------------------------------
   45.20 The page head, and the canvas measure
   --------------------------------------------------------------------------
   THREE THINGS, all of them the same mistake in different places: a 1.0
   arrangement that survived the palette port because a palette port cannot
   see arrangement.

   THE RULE UNDER EVERY PAGE HEAD. `.page-head` draws a 1px hairline across
   the full viewport beneath itself. On a 2.0 page there are no section
   rules at all — the ground is continuous and the objects on it carry the
   structure. It was also the second of the two hard horizontal lines that
   made the landing page read as a stack of strips; `.statement` was the
   first and section 45.11 took that one.

   THE LEDE WAS INDENTED TWO COLUMNS. `grid-column: 3 / 8` sets a page's
   opening paragraph in from the left edge under a heading that starts at
   the margin, which is an editorial device — and a good one — but it is not
   this design's. The redesign stacks eyebrow, heading and lede on one left
   edge and puts the drawing opposite. Indented, the lede floats in the
   middle of the head with nothing above or beside it, which is what it was
   doing on all eleven interior pages.

   THE CANVAS AND THE WRAP DISAGREED ABOUT THE MEASURE. `.canvas`'s wide
   track is 1300px at 1440 and `.wrap` is 1148 — so a page head began at
   x=70 and every section below it began at x=146. Seventy-six pixels is
   small enough to look like a mistake rather than a decision, which is
   exactly what it looked like. The canvas now derives its content column
   from the same 1240 the wrap uses, so a heading and the section under it
   share one left edge.
   -------------------------------------------------------------------------- */

.page-head {
  border-bottom: 0;
}

.canvas {
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [wide-start] minmax(0, 0px)
    [content-start] minmax(0, 1148px) [content-end]
    minmax(0, 0px) [wide-end]
    minmax(var(--gutter), 1fr) [full-end];
}

@media (min-width: 900px) {
  .page-head__grid > .page-head__lede,
  .page-head__grid > .lead {
    grid-column: 1 / 8;
  }
}

/* --------------------------------------------------------------------------
   45.21 The card hover becomes a lift
   --------------------------------------------------------------------------
   `.react__bloom` and `.react__sheen` are 1.0's hover: two radial layers
   inside every card, at opacity 0 until the pointer enters, then tracking
   the cursor across the surface. Ninety of them across the site.

   It is skilful and it is the wrong gesture for this brief. A light that
   follows your cursor is the definition of motion that ANNOUNCES — it draws
   the eye to the pointer, which is the one place the reader is already
   looking, and it does it on a page whose argument is that nothing here is
   competing for attention. The redesign's card hover is `translateY(-3px)`:
   the card acknowledges you and settles, and it is the same 320ms curve the
   nav sheets and the row nudge use.

   Both layers are switched off here and their markup goes with this edit.
   The rules that drew them are left in the file as the record.

   `--motion-move`, so the lift collapses to nothing under reduced motion
   while the border's tint change survives — section 33's two channels. */

.react__bloom,
.react__sheen {
  display: none;
}

.bento__cell,
.step,
.row,
.route,
.moment,
.facts,
.tile,
.statcard,
.inv__row,
.claim,
.keycard,
.swap__pane {
  transition:
    transform calc(320ms * var(--motion-move)) var(--ease-move),
    box-shadow calc(320ms * var(--motion-tint)) var(--ease-tint),
    border-color calc(320ms * var(--motion-tint)) var(--ease-tint);
}

@media (hover: hover) and (pointer: fine) {
  .bento__cell:hover,
  .step:hover,
  .row:hover,
  .moment:hover,
  .facts:hover,
  .tile:hover,
  .statcard:hover,
  .inv__row:hover,
  .claim:hover,
  .keycard:hover,
  .swap__pane:hover {
    transform: translateY(calc(-3px * var(--motion-move)));
    box-shadow: 0 18px 36px -26px rgba(58, 48, 78, 0.34),
                inset 0 1px 0 var(--glass-edge);
  }
}

/* --------------------------------------------------------------------------
   45.22 The footer is a region, and the crisis block is an object
   --------------------------------------------------------------------------
   TWO THINGS THE REDESIGN DOES HERE THAT THIS SITE DID NOT.

   The footer gets its own ground — `--foot-bg`, a half-step off the page on
   Calm and Sage and a half-step darker on Night. Section 45.11 took the
   tinted bands off the sections and said the footer keeps its fill, because
   a footer is a different REGION rather than a louder section; this is that
   fill. It replaces the hairline that used to be the only thing separating
   twelve links from the page above them.

   The crisis block becomes a clay card. On the old footer it was body copy
   under a rule — the most important four sentences on the site set as small
   print at the bottom of a list of links. The redesign gives it the one warm
   surface in the whole palette and its own ink, so it reads as a thing you
   are being handed rather than as a legal footer.

   ⚠️ NOTHING ABOUT WHAT IT SAYS CHANGES, and that is not a style note. Every
   word here is governed by AGENTS.md §3: no number appears that is not in
   `assets/resources/crisis_resources.json`, the two unsourced services are
   still named as unsourced, and "None has been dialled to confirm it
   answers" stays exactly where it is. This edit gives those sentences a
   background. It does not touch them.

   MEASURED, over the composited card ground rather than over the footer:
   the fill is clay at 0.12 (0.13 on Night) so the ink sits on (238,226,217)
   on Calm, (234,230,214) on Sage and (44,32,38) on Night.

       heading  --clay-ink    5.75  Calm   5.85  Sage   9.20  Night
       body     --text       14.59         13.25         14.26
       small    --text-soft   5.38          5.97          6.55

   The floor that binds is the small print at 5.38, not the heading — which
   is the opposite of what I assumed before measuring, and the reason these
   numbers are now in `site_contrast_test` instead of in this comment.
   ------------------------------------------------------------------- */

.footer {
  border-top: 0;
  background: var(--foot-bg);
  padding-block: clamp(3rem, 6vw, 4.5rem);
}

.help {
  border-top: 0;
  border-radius: var(--r-glass-lg);
  padding: clamp(1.5rem, 3vw, 2rem) clamp(1.5rem, 3vw, 2.25rem);
  background: var(--clay-fill);
  border: 1px solid var(--clay-edge);
}

.help__h {
  color: var(--clay-ink);
}

/* --------------------------------------------------------------------------
   45.23 The invariant row's left column stopped hanging
   --------------------------------------------------------------------------
   `.inv__test` spans both rows of the left column, and it is taller than
   either of them. CSS Grid distributes a spanning item's excess height
   EQUALLY across the `auto` rows it covers, so row one — which holds a
   single bold line — grew by half the difference and the paragraph under it
   was pushed down by that much. On the tallest row on safety.html that was
   about 90px of nothing between a claim and its explanation, and it read as
   a missing paragraph rather than as spacing.

   `min-content 1fr` says where the excess goes: row one is exactly its line,
   and row two absorbs the rest. Nothing moves in the right-hand column.

   The rail also goes. `.inv__test` gained a border and a fill in section
   45.19, so the left rule it used to be separated by is now a second edge
   6px from the card's own — two lines doing one job. */

@media (min-width: 900px) {
  .inv__row {
    grid-template-rows: min-content 1fr;
  }

  .inv__test {
    border-left: 0;
    padding-left: 16px;
  }
}

/* --------------------------------------------------------------------------
   45.24 The vision ledger's two columns start on the same row
   --------------------------------------------------------------------------
   The same defect as 45.23 with a different cause, and this one had been
   shipping since the ledger was written.

   The row assigns columns but not rows, so auto-placement decides — and
   auto-placement does not restart at row one for a new column. It carries a
   cursor: after the second item is placed in column 1 row 2, the third item
   (the RIGHT column's label) is placed at the cursor, which is row 2. So
   "What is true now" sat one row below "What was said", level with the
   paragraph rather than with the other label, and was then separated from
   its own text by however tall the left paragraph happened to be — 71px on
   one row and 99px on another.

   Measured at 1440 before the fix: the left label at y=5215 and the right
   label at y=5244, which is exactly the left label's own height. Two labels
   that are meant to be read as a pair, off by one line, on every row.

   Naming the rows fixes it and also makes the arrangement legible in the
   stylesheet, which the column-only version never was.
   -------------------------------------------------------------------------- */

@media (min-width: 900px) {
  .ledger__k--said,
  .ledger__k--is {
    grid-row: 1;
  }

  .ledger__said,
  .ledger__is {
    grid-row: 2;
  }

  .ledger__on {
    grid-row: 3;
  }
}

/* --------------------------------------------------------------------------
   45.25 The nav sheet reads as in front
   --------------------------------------------------------------------------
   The sheet inherits the masthead's old flat treatment — an opaque fill with a
   hairline — from a bar that was itself flat. Under a capsule that floats on
   its own shadow, a panel with no shadow under it reads as a hole in the page
   rather than as a layer above it.

   It takes the capsule's own lift rather than a card's, because it belongs to
   the capsule: same offset, same soft blur, same specular top edge. It stays
   OPAQUE — section 6 records what a translucent nav sheet cost to learn, and
   the ground under this one is whatever the page happens to be scrolled to. */
@media (min-width: 901px) {
  .navgroup__sheet {
    border: 1px solid var(--card-edge);
    box-shadow: var(--nav-lift);
  }
}

/* --------------------------------------------------------------------------
   45.26 The capsule is frostier than the mock, on purpose and by measurement
   --------------------------------------------------------------------------
   The owner said the bar had lost its frosted look. It had not — the values
   are the prototype's to the decimal, and content is genuinely blurred behind
   it. But the prototype's leading stop is `rgba(255,255,255,.9)`, and at 0.9
   over a warm cream ground the left third of a 1240px capsule reads as solid
   white. The mock is an artboard, always shown over its own lilac hero; a
   real site drags that bar over app plates, mosaics and dense card lists all
   day, and the left third never shows any of them.

   ⚠️ THIS IS THE ONE PLACE THE ALPHA MAY BE LOWERED, AND ONLY THIS FAR.
   Section 25.7 records what happened last time: at 0.72 the bar's composited
   ground became a function of whatever was passing beneath it, and a nav link
   measured 2.65:1 over features.html's app plates in dark. A ratio that
   changes as the reader scrolls is not a bad token pair, it is a bar too
   transparent to have a ratio.

   So this was measured rather than judged — the composited pixel UNDER a nav
   link, sampled at nine scroll depths across the seven busiest pages, in all
   three looks. Before: Calm 6.28, Sage 6.88, Night 7.66, worst case binding
   at Calm. After the values below: the numbers are in `site_contrast_test`'s
   note and the sweep is `scratchpad/navcontrast.mjs`, kept in the session
   record rather than the repo because it needs a running server.

   The blur rises with the transparency, and that is not decoration: a wider
   blur averages more of the ground, which makes the composited colour behind
   the text vary LESS as the page scrolls. More frost and a steadier ratio are
   the same change.

   Night is untouched. Its glass is already 0.16/0.09/0.07 — it has no
   opacity to give and its ink has the most headroom of the three.
   -------------------------------------------------------------------------- */

:root {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0.52) 46%, rgba(247, 244, 255, 0.42) 100%);
}

:root[data-theme="sage"] {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.80) 0%, rgba(252, 254, 248, 0.54) 46%, rgba(238, 245, 230, 0.44) 100%);
}

.masthead__inner {
  backdrop-filter: blur(32px) saturate(1.6);
  -webkit-backdrop-filter: blur(32px) saturate(1.6);
}

/* --------------------------------------------------------------------------
   46. THE INTERIOR PAGE — the vocabulary the other twelve never got
   ==========================================================================
   Section 45 rebuilt the landing page and gave every page the new chrome. It
   did not give the interior pages the new SECTIONS, and the inventory says so
   plainly: index carries a panel, two splits, nine rows, a mosaic, three step
   cards, stats and seven eyebrows. Every other page carries exactly one
   eyebrow — its page head's — and none of the rest. Twelve pages wearing the
   new masthead over the old body.

   The redesign's interior pattern, read off its own Safety page rather than
   invented:

     page head        eyebrow, display, lede — all on the left edge, and NO
                      figure beside it. The drawing gets its own band.
     illustration     full content width, tinted with the page's family hue
     section          h2, one short lede line, then the content
     rows             a bold claim left, its plain-English gloss right
     tinted cards     three across, family hues, for guarantees
     figure cards     three across, a large number over a label and its source
     the dark panel   once, for the sentence that matters most
     read on          two or three routes out

   `.statcards` and `.tiles` are the two that did not exist here. Everything
   else already did and was simply never used outside index.html.
   -------------------------------------------------------------------------- */

/* --- The figure card ------------------------------------------------------
   A number, what it counts, and where it came from. Three across on a desk,
   one column on a phone. It is the `.stat` from section 45.9 rebuilt as an
   object rather than a table cell: on a page with no other numbers a row of
   hairline-divided figures reads as a footnote, and the redesign gives each
   one the glass so it reads as a fact. */
.statcards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--s-5);
  margin: var(--s-7) 0 var(--s-8);
  padding: 0;
  list-style: none;
}

.statcard {
  border-radius: var(--r-card);
  padding: 26px;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

.statcard__n {
  display: block;
  font-family: var(--font-head);
  font-size: clamp(2rem, 1.5rem + 1.6vw, 2.75rem);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.statcard__k {
  display: block;
  margin-top: 12px;
  font-weight: 600;
  color: var(--text);
}

.statcard__p {
  display: block;
  margin-top: 4px;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--text-soft);
}

/* --- The tinted tile ------------------------------------------------------
   Three across, one family hue each, a bold line and a gloss. The mosaic's
   cell without the mosaic — used where the items are peers rather than a
   composition, so they get equal widths instead of the 3/3/2/2/2 rhythm. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--s-5);
  margin: 32px 0 0;
  padding: 0;
  list-style: none;
}

.tile {
  border-radius: var(--r-glass-lg);
  padding: 24px;
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
  background: linear-gradient(150deg,
    rgba(var(--hue), var(--tint-a, 0.24)),
    rgba(255, 255, 255, 0.06));
  transition:
    transform calc(320ms * var(--motion-move)) var(--ease-move),
    box-shadow calc(320ms * var(--motion-tint)) var(--ease-tint);
}

.tile.hue-clay { --tint-a: 0.20; }

:root[data-theme="dark"] .tile { --tint-a: 0.13; }
:root[data-theme="dark"] .tile.hue-clay { --tint-a: 0.11; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .tile { --tint-a: 0.13; }
  :root:not([data-theme="light"]):not([data-theme="sage"]) .tile.hue-clay { --tint-a: 0.11; }
}

@media (hover: hover) and (pointer: fine) {
  /* `.tile` and `.statcard` joined the lift family above on 2026-10-11. They
     carried the family's transform and none of its box-shadow, so they rose
     on hover while the shadow stayed put — the one card type that lifted off
     a flat surface while every sibling gained depth. */
}

.tile__h {
  display: block;
  font-weight: 600;
  color: var(--text);
}

.tile p {
  margin: 8px 0 0;
  color: var(--text-soft);
}

/* A section's one-line lede, under its heading. Smaller than `.lead`, because
   on an interior page the heading is doing the work and this is a caption on
   it rather than a paragraph. */
/* The margin is symmetric, and the bottom half is the fix. It was
   `var(--s-4) 0 0` — a top margin only — so the note spaced itself from what
   came BEFORE and owed nothing to what came after. Anything following that
   brings no top margin of its own therefore sat flush against it: the
   `.facts` card on data, the `.ledger` on about, both measured at 0px.
   Symmetric rather than a set of `.section__note + .thing` rules, because
   per-follower spacing is how the panel above ended up with three different
   answers on five pages. Consecutive notes collapse back to one --s-4. */
.section__note {
  margin-block: var(--s-4);
  max-width: 62ch;
  color: var(--text-soft);
}

/* --------------------------------------------------------------------------
   46.1 A claim inside the panel
   --------------------------------------------------------------------------
   `.claim` is the site's "one sentence worth opening" block, and it carries
   its own tinted card. Inside the dark panel that is a card on a card, so the
   tint and the border come off and the ink inverts. The disclosure keeps its
   caret and its two labels; only the colours move. */
.panel .claim,
.claim--on-panel {
  --claim-rail-inset: 0px;
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
}

/* 🔴 **And it is not a pane, so it does not take the pane's treatment —
   2026-10-11.** The rule above takes the tint and the border off, but the
   lift and the rim arrive from a *state* rule — `.react[data-lit]`, which on
   dark draws `inset 0 0 0 1px rgba(255,255,255,0.12)`. On a card that owns a
   surface that rim is its edge catching light. On a claim inside a panel
   there is no surface, so hovering drew a **1px white rectangle around bare
   text**, with the disclosure pill straddling its bottom edge. Reported
   twice; I called it a screenshot artifact the first time, because I
   measured borders and `::before` and never read `box-shadow` on a state
   that only exists under the pointer.

   §46.1's own reasoning already covers it — a card on a card — and §45's
   note that a card-sized lift inside a pane makes the pane look broken
   covers the other half, so the lift goes with the rim. What stays is the
   bloom: a light that follows the pointer needs no edge to live on, and it
   is the one channel that still says the block is a control. */
:root .panel .claim.react[data-lit],
:root .claim--on-panel.react[data-lit],
:root .panel .claim.react:hover,
:root .claim--on-panel.react:hover {
  box-shadow: none;
  transform: none;
}

.panel .claim__say,
.panel .claim__body,
.panel .claim__detail,
.panel .claim__detail dt,
.panel .claim__detail p {
  color: var(--panel-on);
}

.panel .claim__body,
.panel .claim__detail p,
.panel .claim__detail dd {
  color: color-mix(in srgb, var(--panel-on) 80%, transparent);
}

.panel .claim__toggle,
.panel .claim__label {
  color: var(--panel-on);
}

.panel .claim__detail dt {
  color: color-mix(in srgb, var(--panel-on) 92%, transparent);
}

.panel .claim__detail dl,
.panel .claim__detail {
  border-color: color-mix(in srgb, var(--panel-on) 22%, transparent);
}

.panel a {
  color: var(--panel-on);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

/* --------------------------------------------------------------------------
   46.2 The panel is the full measure of the page, not of a paragraph
   --------------------------------------------------------------------------
   Section 42 gave `.panel` `max-width: var(--measure)` — 62ch, 772px at
   1440 — which is the right cap for a COLUMN OF PROSE and the wrong one for
   the object holding it. The redesign's panel is the full 1148 content width
   on every page it appears; capped at 772 it reads as a wide quote card
   floating in the left two-thirds, with 376px of empty page beside it.

   The measure is not abandoned, it moves inward: the panel spans the content
   and its paragraphs are capped, which is what the cap was always for. A
   heading may run wider than 62ch because a heading is not read as prose.
   -------------------------------------------------------------------------- */

.panel {
  max-width: none;
}

.panel > p,
.panel .lead,
.panel .claim__body,
.panel .claim__detail > p {
  max-width: var(--measure);
}

/* --------------------------------------------------------------------------
   46.3 Nothing inside the panel keeps a family ink
   --------------------------------------------------------------------------
   A label coloured with `--hue-ink` or `--ink-sage` is solved against a light
   card. Put the same label on the near-black panel and it is a dark green on
   a dark ground: `feat__k` measured 1.91:1 on Calm, 1.52 on Sage and **1.03**
   on Night — which is not "hard to read", it is invisible.

   Found by a sweep that reads every text node inside every `.panel` on every
   page in all three looks and ratios it against the panel's own fill. That
   sweep is worth more than this rule: the panel is now on eleven pages and
   each one brings its own components inside, so the failure will recur with a
   component nobody has thought about yet. It is `scratchpad/panels.mjs` and
   the numbers are in the session record.

   The rule is deliberately broad — every text-bearing descendant, not a list
   of classes — because a list of classes is a list somebody has to remember
   to extend. Links keep their underline; the crisis block, which never sits
   inside a panel, is untouched. */
.panel :is(p, b, span, li, dt, dd, h3, h4, summary, em, strong, code) {
  color: inherit;
}

.panel .micro,
.panel .soft,
.panel .tiny {
  color: color-mix(in srgb, var(--panel-on) 72%, transparent);
}

/* --------------------------------------------------------------------------
   46.4 A demo with one pane is one column
   --------------------------------------------------------------------------
   `.demo` is a two-column grid because the landing page's demo section held
   two panes side by side. The breathing exercise moved to highlights.html on
   its own, and a one-child two-column grid gives the child 1.08fr of the
   track and leaves the other column empty: the card rendered 322px wide in a
   636px track, with 460px of blank page beside it and its heading wrapping to
   three lines opposite. It read as something that had failed to load.

   `:has()` rather than a modifier class, so the rule follows the content: a
   section that gains a second pane goes back to two columns without anyone
   remembering that this rule exists.

   The pane also takes the 2.0 tinted card while it is here. It was still
   painting 1.0's `--fill` gradient over `--surface` with the old shadow —
   the same object as a mosaic cell, drawn two ways. The tint alphas are the
   mosaic's, which the contrast suite already measures at this size. */

@media (min-width: 880px) {
  .demo:has(> .demo__pane:only-child) {
    grid-template-columns: 1fr;
  }
}

.demo__pane {
  background: linear-gradient(150deg,
    rgba(var(--hue), var(--tint-a, 0.24)),
    rgba(255, 255, 255, 0.06));
  border: 1px solid var(--card-edge);
  border-radius: var(--r-glass-lg);
  box-shadow: var(--card-lift);
}

.demo__pane.hue-clay { --tint-a: 0.20; }

:root[data-theme="dark"] .demo__pane { --tint-a: 0.13; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .demo__pane {
    --tint-a: 0.13;
  }
}

/* --------------------------------------------------------------------------
   46.5 The capsule, frostier still
   --------------------------------------------------------------------------
   Third pass on this, and the first two were both "the values already match
   the mock" — which was true and did not answer the complaint. The prototype
   renders at `rgba(255,255,255,.9)` leading; section 45.26 took it to 0.78;
   this takes it to 0.62 and drops the middle and trailing stops with it.

   Two things the earlier passes missed. **At the top of a page there is
   nothing behind the bar to frost** — it sits on the ground, so no blur
   radius can make it look like glass, and that is the state a reader sees
   first. The only thing that reads as glass over a plain ground is how much
   of the ground's own colour comes through, which is the alpha and nothing
   else. **And a 32px blur over a near-uniform ground averages to a flat
   colour**, so raising the blur to fake more frost makes it MORE opaque
   looking, not less. So the blur comes back down to the spec's 26px and the
   alpha does the work.

   Measured after, the same way as before — the composited pixel under a nav
   link at nine scroll depths across seven pages, in all three looks. The
   numbers are in the session record; the floor is 4.5 and nothing here is
   allowed near it.
   -------------------------------------------------------------------------- */

:root {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.62) 0%, rgba(255, 255, 255, 0.40) 46%, rgba(247, 244, 255, 0.32) 100%);
}

:root[data-theme="sage"] {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.64) 0%, rgba(252, 254, 248, 0.42) 46%, rgba(238, 245, 230, 0.34) 100%);
}

.masthead__inner {
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
}

/* --------------------------------------------------------------------------
   46.6 The illustration band was inset by the user-agent, and too tall
   --------------------------------------------------------------------------
   `.illo` is a `<figure>`, and a figure's UA default is `margin: 1em 40px`.
   The rule that set its spacing used `margin-block`, which overrides the
   vertical half and leaves the horizontal 40px standing — so every band on
   the site rendered **1068px wide inside a 1148px column**, inset by 40 on
   each side, out of line with every heading and card above and below it. The
   design's band is the full content width. Nine pages, and it was invisible
   because 40px of symmetric inset reads as a design decision.

   It was also too tall: 299px against the design's 232. The art was filling
   the band's width, and a 680×170 viewBox at 1108px wide is 277px of drawing
   — half again the height the redesign gives it, which is why the bands were
   the loosest thing on every interior page. `max-height` letterboxes it
   instead: the drawing keeps its aspect, centres, and the band lands at the
   redesign's height.
   -------------------------------------------------------------------------- */

.illo {
  margin-inline: 0;
  padding-inline: 0;
  padding-block: clamp(0.75rem, 1.6vw, 1.25rem);
}

/* ⚠️ I REMOVED A `max-height` HERE ONCE AND THAT WAS THE WRONG CALL.
   The reasoning was that capping the art letterboxes it, shrinking the
   horizon with it, and `04-illustrations.md` asks for a horizon that runs the
   full width of the band. All true except the premise: measured, the design's
   own art is **1009px inside a 1148px band** — its horizon is inset about 70px
   each side and does not reach the band's edges either. So letterboxing is
   what the design does, and keeping the art full-bleed made every band 100px
   taller than the reference for no fidelity gain.

   190px of art plus the band's 20px of padding lands at the design's 232. */

/* --------------------------------------------------------------------------
   46.7 The capsule goes back to the prototype's values
   --------------------------------------------------------------------------
   Sections 45.26 and 46.5 both lowered the glass alpha to chase a "frosted"
   look the owner said was missing — 0.90 to 0.78, then to 0.62 — and the
   second pass made the bar look worse, which is the answer to whether the
   first one was right.

   Both were solving the wrong problem. The measurement each time said the
   values already matched `Moorwell Site.dc.html` to the decimal, and I overrode
   my own measurement twice because the complaint kept coming. A number that
   keeps agreeing with the reference is evidence about the reference, not a
   reason to walk away from it.

   So this restores the prototype's three stops and its `blur(26px)
   saturate(150%)` exactly, on all three looks. Anything further about this
   bar is a decision about the design rather than a defect in the port, and
   it should be made against the mock rather than away from it.
   -------------------------------------------------------------------------- */

:root {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.90) 0%, rgba(255, 255, 255, 0.62) 46%, rgba(247, 244, 255, 0.50) 100%);
}

:root[data-theme="sage"] {
  --glass-bg: linear-gradient(150deg, rgba(255, 255, 255, 0.92) 0%, rgba(252, 254, 248, 0.64) 46%, rgba(238, 245, 230, 0.52) 100%);
}

/* --------------------------------------------------------------------------
   46.8 The register becomes rows
   --------------------------------------------------------------------------
   Twenty-eight claims were laid out as a three-column masonry of tinted
   cards, each a different height because each carries a different amount of
   prose — so the section was a ragged wall of colour with gaps torn through
   it, and no two rows could be compared without hunting for them. It is the
   most 1.0 thing left on the site and it is the largest single block on any
   page: 3,691px of it.

   The redesign lays the same data out as full-width glass rows: the claim
   and its citation on the left, the verdict chip on the right, and
   everything else behind a caret. Uniform height, one column, and a reader
   can run their eye down the verdicts — which is the whole point of a
   register and the thing a masonry makes impossible.

   The detail is not lost and it is not behind a script: `<details>` opens
   with no JavaScript, prints open, and is found by an in-page search. The
   filters above still work on the row, because the row is still the element
   that carries `data-verdict`.
   -------------------------------------------------------------------------- */

.reg {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.reg__row {
  display: block;
  border-radius: var(--r-glass);
  padding: 18px 22px;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

.reg__top {
  display: flex;
  align-items: center;
  gap: 16px;
}

.reg__id {
  flex: 1;
  min-width: 0;
}

.reg__h {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

.reg__cite {
  margin: var(--s-2) 0 0;
  font-size: var(--t-small);
  color: var(--text-soft);
}

/* 🔴 **The whole row is the target, and the header is INSIDE the summary —
   2026-10-11.** Two attempts failed before the markup moved. First the
   summary shrank to its contents: a visually-hidden label plus a 9px arrow,
   so the hit area measured **21x28px** at the far right of an 820px row —
   the arrow read as decoration and the row read as inert ("serves no
   purpose"). Then `inline-size: 100%` with `margin-top: -44px` lifted a
   **28px** strip to −44..−16, which lands in the gap BELOW the title: wide,
   correct-looking, and still not over the words. A real mouse click on the
   title confirmed the row did not open.
   `.reg__top` now lives inside the `<summary>`, so the header IS the
   control — no pull, no overlay, nothing to mis-measure. The caret is the
   summary's own `::after`, a sibling of the header rather than a thing
   floated over it. */
.reg__toggle {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  color: var(--text-soft);
}

.reg__toggle > .reg__top {
  flex: 1;
  min-width: 0;
}

.reg__toggle::-webkit-details-marker { display: none; }

.reg__toggle::after {
  content: "";
  flex: none;
  inline-size: 9px;
  block-size: 9px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform calc(var(--dur-tint) * var(--motion-move)) var(--ease-move);
}

.reg__more[open] > .reg__toggle::after {
  transform: rotate(-135deg) translate(-3px, -3px);
}

.reg__toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-pill);
}

.reg__detail {
  padding-top: 16px;
  margin-top: 16px;
  border-top: 1px solid var(--hair);
}

/* The tint came off the card, so the verdict chip is now the only colour in
   the row — which is what makes a column of them scannable. */
.reg__row.hue-sage    .chip { --chip-hue: var(--rgb-sage); }
.reg__row.hue-dusk    .chip { --chip-hue: var(--rgb-dusk); }
.reg__row.hue-clay    .chip { --chip-hue: var(--rgb-clay); }
.reg__row.hue-heather .chip { --chip-hue: var(--rgb-heather); }
.reg__row.hue-sand    .chip { --chip-hue: var(--rgb-sand); }

.reg__row .chip {
  flex: none;
  white-space: nowrap;
}

@media (max-width: 640px) {
  .reg__top { align-items: flex-start; flex-direction: column; gap: var(--s-2); }
  .reg__toggle { align-items: flex-start; }
  .reg__toggle::after { margin-top: var(--s-2); }
}

/* --------------------------------------------------------------------------
   46.9 Three things the register rows needed after the conversion
   --------------------------------------------------------------------------
   Found by looking at it, and each was a rule from the masonry still winning.

   **`align-items: start` on `.reg`** was correct for a grid of cards and
   wrong for a column: every row shrank to its own content, so twenty-eight
   rows had twenty-eight widths and the column had a ragged right edge — the
   same raggedness the masonry had, in one dimension instead of two.

   **The family fill was still painting.** `.reg__row` carries `hue-sage` and
   a `.glass` treatment from 1.0, and a same-specificity override does not
   beat a rule that came later in a file this size. Scoped through the list
   so it wins on specificity rather than on ordering luck.

   **The caret sat on top of the chip.** Both were right-aligned, one of them
   by a negative margin. The top row now reserves the caret's width and the
   caret is positioned in it, so they cannot collide at any row length.
   -------------------------------------------------------------------------- */

.reg {
  align-items: stretch;
}

.reg .reg__row {
  position: relative;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

/* 46.9's third item and 46.10 both described a caret positioned over the
   header. It is a flex sibling of the header now, so neither the reserved
   gutter nor the absolute placement has anything left to solve. */

/* --------------------------------------------------------------------------
   46.10 The caret belongs to the row, not to the disclosure
   --------------------------------------------------------------------------
   `.reg__more` is `position: relative`, so an absolutely-positioned summary
   inside it is placed against the DETAILS rather than against the row. With
   the summary taken out of flow the closed details collapses to zero height
   at the row's bottom edge, and `top: 18px` from there put every caret in
   the GAP BELOW its own row — pointing at the next claim.

   Static, so the row's `position: relative` is the containing block again.
   Nothing else about the disclosure changes.

   The row also loses the family border. On a tinted card an edge in the same
   hue is the card's own outline; on the white row it is a coloured bar down
   the left of every entry, which reads as a status the register does not
   have — the verdict chip is the status, and it says so in words. */

.reg .reg__more {
  position: static;
}

.reg .reg__row {
  border-color: var(--card-edge);
}

/* --------------------------------------------------------------------------
   46.11 The register rows do not scale
   --------------------------------------------------------------------------
   `.z-step` gives each card a 1.012 scale so a grid of them reads as stacked
   depth. On a full-width row it makes the painted box 1162px inside a 1148px
   column — every row hanging 7px past the measure on both sides, out of line
   with the heading above and the footer below. Nothing overflowed the page,
   so no sweep caught it; it is visible only as a column that does not line
   up with the rest of the page.

   The rows take the site's own card hover instead: 3px of lift, collapsing
   to nothing under reduced motion, which is section 45.21's contract. */

.reg .reg__row {
  transform: none;
  transition:
    transform calc(320ms * var(--motion-move)) var(--ease-move),
    box-shadow calc(320ms * var(--motion-tint)) var(--ease-tint);
}

@media (hover: hover) and (pointer: fine) {
  .reg .reg__row:hover {
    transform: translateY(calc(-3px * var(--motion-move)));
    box-shadow: 0 18px 36px -26px rgba(58, 48, 78, 0.34),
                inset 0 1px 0 var(--glass-edge);
  }
}

/* The verdict edge, on the rows only. A `hue-*` class sets an accent on the
   inline start, which on a tinted card was the card's own outline and on the
   white row is a coloured bar down the left of some entries but not others.
   The chip says the verdict in words; it does not need a second, quieter
   channel that only three of the five verdicts happen to have. */
.reg .reg__row {
  border-inline-start-color: var(--card-edge);
  border-left-color: var(--card-edge);
}

/* --------------------------------------------------------------------------
   46.12 The verdict filter is a row of pills, and the key is a glass card
   --------------------------------------------------------------------------
   Ported from `Moorwell Site.dc.html`'s Evidence page rather than approximated —
   the values below are its computed ones.

   THE FILTER. The redesign puts the verdict facet in front of the register as
   always-visible pills, because it is the one facet a reader uses: they came
   to see what did not hold. Mechanism and population stay behind the
   disclosure; eleven and eight pills are a control panel, not a filter bar.
   The buttons did not change, only where they sit, so `motion.js` still
   drives them and one set of state still exists.

   THE KEY. The design's glossary is a glass card at 32px radius holding rows
   of `badge · term · definition`, the badge a `--panelDark` pill at 52×32.
   The verdict key is the same shape, and here the badge can be the verdict's
   own chip — so a reader learning what "Qualified" means is looking at the
   exact object they will meet in the register, rather than at a symbol
   standing in for it. That is a change from the mock and it is the reason
   for it.
   -------------------------------------------------------------------------- */

.filters__group--lead {
  margin-bottom: var(--s-4);   /* snapped to the --s- scale */
}

.filters__group--lead .filters__legend {
  display: block;
  margin-bottom: var(--s-3);
}

.filters__group--lead .filters__set {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

/* The design's pill: quiet until pressed, then the page's ink. */
.filter {
  border-radius: var(--r-pill);
  padding: 0 16px;
  min-height: 38px;
  font-size: 14px;
  font-weight: 600;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
  color: var(--text-soft);
}

.filter[aria-pressed="true"] {
  background: var(--panel-dark);
  border-color: transparent;
  color: var(--panel-on);
}

/* --- the key card ------------------------------------------------------- */

.keycard {
  border-radius: var(--r-card);
  padding: 28px;
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
}

.keycard .ledger__row {
  display: grid;
  gap: 4px 16px;
  padding: 0 0 14px;
  margin: 0 0 var(--s-4);
  border-bottom: 1px solid var(--hair);
  border-top: 0;
}

.keycard .ledger__row:last-child {
  padding-bottom: 0;
  margin-bottom: 0;
  border-bottom: 0;
}

@media (min-width: 760px) {
  .keycard .ledger__row {
    grid-template-columns: 9.5rem minmax(0, 1fr);
    align-items: baseline;
  }

  .keycard .ledger__k--said,
  .keycard .ledger__k--is { grid-column: 1; grid-row: auto; }
  .keycard .ledger__said,
  .keycard .ledger__is,
  .keycard .ledger__on { grid-column: 2; grid-row: auto; }
}

/* The term takes the register's own chip, so the key shows the object rather
   than naming it. */
.keycard .ledger__k {
  display: inline-flex;
  align-items: center;
  justify-self: start;
  border-radius: var(--r-pill);
  padding: 4px 12px;
  font-size: 13px;
  font-weight: 600;
  background: var(--panel-dark);
  color: var(--panel-on);
}

.keycard .ledger__on {
  color: var(--text-soft);
  font-size: var(--t-small);
}

/* --------------------------------------------------------------------------
   46.13 A fact list is rows, not one card with dividers
   --------------------------------------------------------------------------
   `.facts` is a single glass card with its entries separated by internal
   hairlines. The redesign gives each fact its own row with air between them —
   the same component as `.rows`, and the same reason: a reader scanning "what
   can leave the phone" is comparing entries, and entries in one box read as
   one statement with sub-clauses rather than as a list of separate facts.

   CSS only. The markup is already `<dl class="facts"><div><dt>…<dd>…`, which
   is exactly a row, so this changes three lists on data.html and one each on
   safety.html and open.html without touching a line of HTML.

   The term takes the left track and the definition the right, above 760px —
   under it they stack, because a 9rem term column and a definition sharing
   340px is two columns of wrapped fragments.
   -------------------------------------------------------------------------- */

.facts {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
}

.facts > div {
  padding: 18px 22px;
  border-radius: var(--r-glass);
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
  transition:
    transform calc(320ms * var(--motion-move)) var(--ease-move),
    box-shadow calc(320ms * var(--motion-tint)) var(--ease-tint);
}

@media (min-width: 760px) {
  .facts > div {
    display: grid;
    grid-template-columns: 15rem minmax(0, 1fr);
    gap: 4px 24px;
    align-items: baseline;
  }
}

.facts dt {
  font-weight: 600;
  color: var(--text);
}

.facts dd {
  margin: 0;
  color: var(--text-soft);
}

@media (hover: hover) and (pointer: fine) {
  .facts > div:hover {
    transform: translateY(calc(-3px * var(--motion-move)));
    box-shadow: 0 18px 36px -26px rgba(58, 48, 78, 0.34),
                inset 0 1px 0 var(--glass-edge);
  }
}

/* --------------------------------------------------------------------------
   46.14 THE RHYTHM — the reason every page is twice the design's length
   ==========================================================================
   Measured rather than judged. For each page, every text-bearing leaf element
   was collected, sorted by position, and the gaps between consecutive blocks
   summed:

                     design            mine (before)
       Mission       2,756px tall      6,948px tall
                     1,506px of air    3,732px of air     biggest gap 675
       Vision        2,946 / 1,554     6,341 / 2,937      biggest gap 673
       Contact       2,404 / 1,333     5,203 / 3,034      biggest gap 580

   **The design's biggest gap is 362px on all three pages.** Mine was 580–675.
   The MEDIAN gap is nearly identical on both sides — 61–105 there, 88–107
   here — so the difference is not that this site breathes more between
   paragraphs. It is entirely in the big gaps: the space between one section
   and the next.

   `--section` is 120px at 1440, applied as `padding-block`, so two adjacent
   sections sit **240px** apart. The redesign separates its sections with
   `margin-bottom: 74px` — a third of that. Every interior page has been two
   to three times the design's length for that one reason, and none of it is
   content: not a word comes out to fix it.

   The three band heights keep their ratio to each other (1 : 1.42 : 1.92,
   from 1.62 before) because §25's argument for three heights still holds — a
   page needs a tight, a normal and an open band. They are simply no longer
   scaled for a page that was mostly air.
   -------------------------------------------------------------------------- */

:root {
  --section: clamp(2.75rem, 4.5vw, 4.625rem);
  --band-tight: clamp(2.25rem, 3.4vw, 3.25rem);
  --band: clamp(2.75rem, 4.5vw, 4.625rem);
  --band-open: clamp(3.5rem, 6vw, 6.25rem);
}

/* --------------------------------------------------------------------------
   46.15 THE BAR, AT THE APPROVED DESIGN'S OWN MEASUREMENTS
   ==========================================================================
   Measured off `Moorwell Site.dc.html` rather than adjusted by eye, because the
   last three attempts at this bar were adjusted by eye and two of them made
   it worse.

                        approved design      what shipped
     nav item padding   0 14px               9.6px 5.6px
     gap between items  14 + 2 + 14 = 30px   5.6 + 2 + 5.6 = 13px
     nav item size      13.5px / 600         15px / 400
     nav item height    38px                 44px
     nav item radius    999px                square
     look dot           11px                 8px
     chip padding       0 12px               4.8px 12px

   **The crowding is the padding.** The wrapper's `gap: 2px` is the design's
   own value and is not the problem — the design separates its nav items with
   each item's own 14px of padding, so the visual gap is 30px and each item
   has a pill-shaped hit area 38px tall. Copying the gap without the padding
   gave three labels 13px apart with no target around them.

   **The selected look-chip.** The active chip is right — `#15130F` on
   `#F7F4EE`, the design's exact pair. What read as odd is the dot: 8px where
   the design draws 11, and on the System chip a hollow ring, which on the
   near-black active chip is a white outline with nothing in it. System now
   takes a split dot — half light, half dark — which is the metaphor this
   project already uses for Auto in the app's own theme tile, and it reads as
   a thing rather than as a missing thing.

   **The open group now says so.** `<details>` gives no visual state beyond
   its caret, so an open group looked identical to a shut one at a glance.
   It takes the same soft track the look control sits in.
   -------------------------------------------------------------------------- */

@media (min-width: 901px) {
  .navgroup > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: 0 14px;
    min-height: 38px;
    border-radius: var(--r-pill);
    font-size: 13.5px;
    font-weight: 600;
    color: var(--text-soft);
    transition: background-color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint),
                color calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
  }

  .navgroup > summary:hover {
    color: var(--text);
  }

  /* The open group is the one the reader is in. */
  .navgroup[open] > summary {
    background: var(--soft-track);
    color: var(--text);
  }
}

/* --- the look control, at the design's sizes ---------------------------- */

.theme__opt {
  padding-block: 0;
  block-size: 32px;
}

.theme__opt::before {
  inline-size: 11px;
  block-size: 11px;
}

/* System has no colour of its own, so it gets the split the app's own Auto
   tile uses rather than an empty ring. */
#theme-system + .theme__opt::before {
  --dot: transparent;
  background: linear-gradient(90deg,
    var(--text) 0 50%, var(--surface) 50% 100%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 45%, transparent);
}

.theme__set input:checked + #theme-system + .theme__opt::before,
#theme-system:checked + .theme__opt::before {
  background: linear-gradient(90deg,
    var(--panel-on) 0 50%, color-mix(in srgb, var(--panel-on) 26%, transparent) 50% 100%);
  box-shadow: none;
}

/* --------------------------------------------------------------------------
   46.16 The display, fitted to the design's own two points
   --------------------------------------------------------------------------
   Measured, and the result was the opposite of the guess. Mine looked bigger
   in a 1440 screenshot, so I assumed it was too large everywhere. At 1180 it
   is SMALLER than the design's and in a narrower column:

                        design       mine
       font-size        60px         49.96px
       column           734.73px     627px      (= 22ch vs no cap at all)
       lines            2            3

   A headline that breaks after three words where the design breaks after six
   is not a type-size preference, it is a different sentence rhythm — the
   reader gets "Moorwell keeps the" as a line, which is not a phrase.

   Two causes. The clamp's slope was too steep, so the display shed 10px
   between 1440 and 1180 where the design sheds 8; and `.display` had no
   measure at all, so it took whatever the grid column gave it — 1/8 of
   twelve, which is narrower than the design's cap at every width above
   about 1100.

   The clamp below is fitted to the design's two measured points, 60px at
   1180 and 68px at 1440: slope 3.08vw, intercept 1.48rem. The cap is the
   design's own 22ch — the same measure its section headings use.
   -------------------------------------------------------------------------- */

:root {
  --t-display: clamp(2.5rem, 1.48rem + 3.08vw, 4.25rem);
}

.display,
.h1 {
  max-width: 22ch;
}

@media (min-width: 900px) {
  .page-head__grid > .display {
    grid-column: 1 / 10;
  }
}

/* --------------------------------------------------------------------------
   46.17 The interior page headline is the display step, not the one below it
   --------------------------------------------------------------------------
   `.page-head__grid .display` sets `--t-1` and `max-width: 19ch`. The landing
   page's hero is `--t-display` at 22ch, so the site has been running its
   interior headlines a whole step smaller than its home one — and a step
   smaller than the approved design, which uses the same size on both:

                  design       here (before)
       at 1180    60px / 22ch  49.96px / 19ch   → 3 lines instead of 2
       at 1440    68px         56px

   Nine pages. The visible symptom is the sentence rhythm rather than the
   size: at 19ch "Moorwell keeps the rest of a student's life within reach."
   breaks after "Moorwell keeps the", which is not a phrase, and the reader gets
   a three-line headline whose first line says nothing.
   -------------------------------------------------------------------------- */

.page-head__grid .display {
  font-size: var(--t-display);
  letter-spacing: var(--tr-display);
  max-width: 22ch;
}

.illo__art {
  max-height: 190px;
}

/* A note under the page head's lede — a signpost rather than a second
   paragraph, so it sits with the lede and reads a step quieter.

   🔴 **It needs an explicit row — 2026-10-11.** The head is a 12-column grid
   whose `h1` is pinned to row 2 and whose lede is pinned to row 3, leaving
   row 1 for the eyebrow. This note was `grid-row: auto`, so on the one page
   that has a note and NO eyebrow it auto-placed into the empty row 1 and
   rendered ABOVE the heading — three lines of grey sentence-case prose
   standing where every other page has a short uppercase eyebrow. Nothing
   caught it because the rule is correct and so is the markup order; only
   the auto-placement disagreed with both. */
.page-head__note {
  grid-row: 4;
  margin-top: var(--s-3);
  max-width: 46ch;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--text-soft);
}

@media (min-width: 900px) {
  .page-head__grid > .page-head__note {
    grid-column: 1 / 8;
  }
}

/* --------------------------------------------------------------------------
   46.18 The horizon is drawn by the band, not by the drawing
   --------------------------------------------------------------------------
   Measured side by side: the design's horizon runs ~93% of the band's width
   while its subject occupies about 42%. Mine ran 66% — exactly the width of
   the letterboxed artwork — because section 44 moved the horizon INSIDE each
   drawing so it could not drift out of alignment with the thing standing on
   it. Letterbox the drawing and the horizon shrinks with it.

   So the design draws the horizon separately, and this does too. The offset
   is arithmetic rather than a guess: the art is capped at 190px for a 170-unit
   viewBox, so the scale is 190/170 = 1.1176, and the horizon at y=145 lands at

       20px padding + (145 / 170 × 190) = 182.06px from the band's top,

   which on a 232px band is 50px from the bottom. Both numbers are fixed by
   `--illo-art-h` and the band's padding, so the line cannot drift unless one
   of those changes — and if either does, this arithmetic is where to look.

   The in-drawing horizon is hidden rather than deleted from the ten SVGs: it
   is the fallback if this rule is ever dropped, and `04-illustrations.md`
   still describes it as part of the artwork.
   -------------------------------------------------------------------------- */

:root {
  --illo-art-h: 190px;
  --illo-pad: 20px;
}

.illo__art {
  max-height: var(--illo-art-h);
}

.illo__horizon {
  display: none;
}

.illo::after {
  content: "";
  position: absolute;
  left: 3.5%;
  right: 3.5%;
  bottom: calc(var(--illo-pad) + var(--illo-art-h) - (145 / 170 * var(--illo-art-h)));
  block-size: 1px;
  background: var(--illo-ink);
  opacity: 0.34;
  pointer-events: none;
}

@media print {
  .illo::after { opacity: 0.34 !important; }
}

/* A band whose drawing IS a landscape supplies its own ground line, so the
   band must not add one. Section 44 removed the horizon path from vision's
   artwork for exactly this reason; §46.18 draws the horizon from the band
   instead and had no way to know. The figure now says so. */
.illo--landscape::after {
  content: none;
}


/* ==========================================================================
   46.19 THE BAR IS GLASS AT SCROLL 0, AND ITS SHEET IS A SHEET
   ==========================================================================
   Two defects, both of them structural rather than cosmetic, and between
   them they are the whole of "the app bar is still not good".

   ## 1. The frost was animated to nothing, and the shadow with it

   Section 45.3 gives `.masthead__inner` the design's fill, the design's
   `--nav-lift` and `backdrop-filter: blur(26px) saturate(150%)`, and every
   one of those declarations is correct in the file. None of them was on the
   screen. Measured at scroll 0 on the shipped page:

     box-shadow       five transparent layers
     backdrop-filter  blur(0px) saturate(1)

   Section 30 is the cause and it is not a bug in section 30. That section
   built a bar that is FLUSH at the top of the document and lifts into a
   floating pill as you scroll, and it drives the transition with a
   `scroll(root block)` timeline on `.masthead__inner`. A scroll-driven
   animation is still an animation, so its `from` frame beats a normal
   declaration no matter how late that declaration sits in the file — and
   `bar-pill`'s `from` frame is, correctly for section 30, *no fill, no
   shadow, no blur*. Section 45.3 was overwritten at every scroll position
   from 0 to 160px by a rule written to be correct about a bar that no
   longer exists.

   The 2.0 bar is a capsule at rest. It has already let go of the edge — it
   is inset by 30px and floating over the page ground before the reader has
   scrolled a pixel — so there is no flush state to leave and nothing for
   the timeline to express. Section 30's answer to "the bar looks identical
   at scroll 0 and scroll 4000" is now answered by the design instead: the
   bar is glass, and what changes behind it is the page.

   Retired rather than deleted, and the keyframes are left in place, because
   section 30 is a long argument about what may and may not be animated on a
   sticky element and that argument is still true. What is retired is its
   application to a bar that starts where it used to finish.

   ## 2. The sheet is the bar's width, not the group's

   The approved design opens ONE panel under the whole capsule and lays the
   destinations across it as cards. Measured off the prototype at 1440:

     sheet      x 100, w 1240 — exactly the bar — y 96, i.e. 10px below it
     radius     30px, padding 22px
     glass      blur(30px) saturate(160%), and a deeper lift than the bar's
     eyebrow    the group's own description line, 10.5px, .14em, uppercase
     cards      flex 1 1 240px, min 210px, radius 22px, padding 16px 18px,
                laid out in a 12px flex row that wraps

   What shipped was a 325px dropdown hanging off the group that opened it,
   with the destinations stacked vertically — the shape of a native select
   menu, not of the design's sheet. Three items in a column at 325px is also
   the reason the sheet felt cramped: the design gives each destination 390px
   of width and one line for its description, and the dropdown gave it 307px
   and two.

   ⚠️ ANCHORED TO `.masthead`, NOT TO `.masthead__inner`. The capsule's
   padding box is 1208px wide, not 1240 — an absolutely positioned child of
   it with `inset-inline: 0` would be inset by the capsule's own asymmetric
   `10px 10px 10px 22px`. The sticky `.masthead` is full-bleed, so
   `inset-inline: 30px` (its own gutter) with `max-inline-size: 1240px` and
   auto margins reproduces the capsule's box exactly, at every width, from
   one pair of numbers that are already in section 45.3.

   Section 6 warns against anchoring a sheet to the bar, and that warning
   still holds where it was written: below 900px the bar WRAPS, "More" moves
   to the left edge, and a panel pinned to the bar opened at -113px. These
   groups do not exist below 901px. The condition is checked, not ignored.
   -------------------------------------------------------------------------- */

/* --- 1. the capsule keeps its fill ---------------------------------------- */

@media (min-width: 900px) {
  .masthead,
  .masthead__inner,
  .masthead::after {
    animation: none;
  }
}

/* --- 2. the sheet ---------------------------------------------------------- */

@media (min-width: 901px) {
  /* The group no longer positions anything, so it stops being a containing
     block. Left `relative` it would still capture the sheet below. */
  .navgroup {
    position: static;
  }

  .navgroup__sheet {
    position: absolute;
    /* THE CAPSULE'S OWN BOX, MINUS ITS BORDER. `.masthead__inner` carries
       `backdrop-filter`, and a backdrop filter establishes a containing
       block for absolutely positioned descendants exactly as a transform
       does — so the sheet cannot be anchored to `.masthead` from inside the
       capsule no matter what `position` says. The capsule's padding box is
       its border box inset by its 1px border, so -1px on each side puts the
       sheet back onto the capsule's own edges: x 100, w 1240 at 1440, and
       whatever the capsule is at every other width. Nothing here repeats a
       number from section 45.3. */
    inset-inline: -1px;
    max-inline-size: none;
    inline-size: auto;
    /* 1px of border and the design's 10px gap, measured from the bottom of
       that same padding box. */
    top: calc(100% + 1px + 10px);
    z-index: 40;

    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 12px;
    padding: 22px;
    border-radius: var(--r-glass-lg);
    box-shadow: var(--sheet-lift);
  }

  /* ⚠️ THE SHEET STAYS OPAQUE, AND THE GLASS IS NOT AVAILABLE TO IT ANYWAY.
     Both halves of that were tested here rather than assumed.

     §45.25 already settled the first half — §6 records what a translucent
     nav sheet cost to learn, and the ground under this one is whatever the
     page happens to be scrolled to. A 1240px sheet makes that worse than
     the 325px dropdown did, not better: it now covers the whole hero.

     The second half is a fact about backdrop filters. An element with a
     `backdrop-filter` is a BACKDROP ROOT: everything painted inside it is
     excluded from its descendants' backdrops. `.masthead__inner` carries
     the capsule's frost, so a `backdrop-filter` on this sheet has nothing
     to filter — its backdrop stops at the capsule. Given the design's own
     fill at 0.5–0.9 alpha and no working blur, the hero's 68px headline
     read straight through the sheet, sharp, over the destination titles.
     The prototype does not hit this because its sheet is a SIBLING of the
     bar, not a child of it; `<details>` cannot be that and stay keyboard-
     operable without script, and §6 is explicit that this navigation must
     not need a script to open.

     So the sheet takes the design's gradient over an OPAQUE ground rather
     than over the page: `--glass-bg` layered on `--surface`. That is the
     prototype's exact fill, composited on something that cannot change as
     the reader scrolls — the look, without the property that made it
     unreadable. Everything else about the sheet — box, radius, padding,
     cards, lift — is the design's already. */

  .navgroup__sheet {
    background-image: var(--glass-bg);
    background-color: var(--surface);
  }

  /* `top` above is measured from `.masthead`, whose 6px of bottom padding is
     below the capsule — so 100% is 6px past the capsule's edge and the
     design's 10px gap is what remains. Written as the sum rather than as 4px
     so that changing section 45.3's padding changes this with it. */

  /* The eyebrow takes a row of its own — `flex: 1 0 100%` rather than a
     wrapper element, so this reaches all twelve pages and both generated
     ones without a markup change. 4px of its own margin plus the flex gap
     of 12 is the design's 16. */
  .navgroup__what {
    flex: 1 0 100%;
    margin: 0 0 4px;
    font-size: 10.5px;
    line-height: 1;
    letter-spacing: 0.14em;
  }

  /* `.masthead__nav` in the selector, and it has to be: §6's
     `.masthead__nav a:not(.btn)` sets a `padding-block` that buys a 44px
     target for a bar LINK, at a specificity these cards cannot outrank from
     a single class. It applied to the sheet's cards by accident — they are
     not bar links — and it is what made every card 9.6px tall inside its
     own 16px of padding. */
  .masthead__nav .navgroup__sheet a {
    flex: 1 1 240px;
    min-inline-size: 210px;
    padding: 16px 18px;
    border-radius: 22px;
    border: 1px solid transparent;
    background: transparent;
  }

  .navgroup__sheet a b {
    font-size: 15px;
    line-height: 1.3;
  }

  .navgroup__sheet a span {
    margin-top: var(--s-1);
    font-size: 12.5px;
    line-height: 1.45;
  }

  .navgroup__sheet a:hover,
  .navgroup__sheet a:focus-visible {
    background: var(--soft-track);
  }

  /* §43.3 nudges a list row 3px along its own axis on hover. These are no
     longer rows — they are three cards side by side — and a card sliding
     sideways into the gap beside it reads as the row breaking rank, which
     is the very thing §43.3 chose a sideways nudge to avoid. */
  .navgroup__sheet a:hover {
    transform: none;
  }

  /* The page you are on. The design marks it with the same soft track and a
     hairline, so the mark is the card rather than a dot beside a word. */
  /* 🔴 **Same white-on-white as the group pill — 2026-10-11.**
     `--soft-track` is `rgba(255,255,255,.62)` in Calm and `.70` in Sage, so
     the card marking the current page inside the dropdown was white on a
     near-white sheet: invisible in both light palettes, and only ever legible
     in the dark one. The dot that used to sit beside the word was cancelled
     on the argument that "the mark is the card" — which was true only where
     the card could be seen.
     `--accent` is per-palette, so a tint of it contrasts on every ground.
     `--soft-track` stays as the fallback without `color-mix`. */
  .navgroup__sheet a[aria-current="page"] {
    background: var(--soft-track);
    background: var(--wash-strong);
    border-color: var(--card-edge);
  }

  .navgroup__sheet a[aria-current="page"] b::after {
    content: none;
  }
  /* 🔴 **And the rule under the word, for the same reason — 2026-10-11.**
     `.masthead__nav a[aria-current="page"]::after` draws a 2px accent rule at
     `bottom: -0.35rem`. It was written for the 1.0 bar — one flat line of
     links, where a rule under the word WAS the marker. That bar was retired
     on 2026-09-03; the rule was not.

     Every `aria-current` link now lives inside a sheet, so this was the only
     thing that selector still matched, and it drew a full-width line under
     the card, outside the card's own rounded corners. The row carried two
     markers for one state and the second read as a stray hairline. The dot
     above is cancelled on exactly this argument; this is its other half. */
  .navgroup__sheet a[aria-current="page"]::after {
    content: none;
  }

  /* 39's rule pulled the last group's sheet leftward so a 24rem dropdown
     would not leave the viewport. A sheet the width of the bar has no side
     to leave from, so this only has to undo 39 — and it has to undo it to
     the SAME inset the other sheets use. It said `30px` until 2026-09-03,
     which anchored this one sheet to the inner's padding box: the third
     dropdown opened 62px narrower than the other two and inset 31px from
     them, and at 1000px it squeezed the fourth card to zero width. */
  .navgroup:last-of-type .navgroup__sheet {
    inset-inline: -1px;
  }

  /* THE GROUP THE READER IS IN. The design lights the group that contains
     the current page, not only the one that is open — so the bar says where
     you are before you touch it. `:has()` rather than a class, so it reaches
     `privacy.html` and `changelog.html` too: both slice this masthead out of
     `index.html` and neither generator knows about a state class. */
  /* 🔴 **The marker has to carry its own contrast — 2026-10-11.**
     This was `--soft-track`, which is `rgba(255,255,255,.62)` in the light
     palettes and `rgba(214,202,244,.12)` in the dark one. White on a cream bar
     is not a marker: in Calm and Sage the current group was indistinguishable
     from the other two, and only dark mode ever showed it. It read as working
     because the accent rule under the word was doing the job — and when that
     was retired as a duplicate, nothing was left.
     `--accent` is defined per palette (sage green, deep purple, lilac), so a
     tint of it is visible on every ground by construction. `--soft-track`
     stays as the fallback for anything without `color-mix`. */
  .navgroup:has(a[aria-current="page"]) > summary {
    background: var(--soft-track);
    background: var(--wash-strong);
    color: var(--text);
    font-weight: 600;
  }

  /* The caret is the design's solid triangle rather than a chevron, and it
     turns over rather than rotating a corner. */
  .navgroup > summary::after {
    inline-size: 0;
    block-size: 0;
    width: 0;
    height: 0;
    margin-inline-start: 0;
    vertical-align: baseline;
    border: 4px solid transparent;
    border-block-start: 4px solid currentColor;
    border-block-end: 0;
    opacity: 0.6;
    transform: none;
    transition: transform calc(var(--dur-tint) * var(--motion-tint)) var(--ease-tint);
  }

  .navgroup[open] > summary::after {
    transform: rotate(180deg);
  }
}


/* ==========================================================================
   46.20 THE INTERIOR SECTION HEAD IS ONE COLUMN, AND A STEP SMALLER
   ==========================================================================
   This is the largest single difference left between the approved design and
   what shipped, and it is one rule rather than nine pages of work.

   Measured off the prototype's own `head(b)`, which every interior block on
   every interior page renders through:

                        approved design            what shipped
     heading            500 34px/1.15, -.03em      500 44px/1.08, -.018em
     heading width      22ch                       unbounded
     the line under it  400 15.5px/1.6, 58ch       400 19px, in a second column
     head to content    26px                       56px
     arrangement        stacked, one column        468px | 561px grid

   ⚠️ THE HOME PAGE KEEPS 44px, and that is the design's arrangement rather
   than an exemption. The prototype writes its landing-page section heads at
   `500 44px/1.08 letter-spacing:-.035em` — the size this site already uses —
   and its INTERIOR block heads at 34. A landing page has five headings a
   reader is meant to see from across the room; an interior page has five
   headings a reader is already reading. `.interior` on `<main>` is the
   marker, so the two never have to be told apart by page name.

   ## Why the two-column head goes

   Section 31 built it, and its reasoning was sound at the time: *"a reader
   who scrolls past the hero on how, mission, vision or safety meets a
   heading, a paragraph capped at 34ch, and then half a screen of nothing to
   its right, six times down the page."* The empty right half was the defect
   and a second column was a fair answer to it.

   Section 46 removed the cause. Every interior page now carries tiles,
   figure cards, rows and a panel — all of them full width — so the head is
   no longer the only thing on the line. What the second column does now is
   split the head in two and push the content 56px further down, on a page
   whose rhythm §46.14 had just finished tightening. One column, and the
   width belongs to the content.
   -------------------------------------------------------------------------- */

.interior .section__head,
.interior .section__head--wide {
  display: block;
  max-width: none;
  /* `--s-6` (24px), not a raw 26 — 2026-10-11. One declaration made the
     most common gap on the site, sixteen times, two pixels off a scale
     that already exists. A number typed beside a scale is a second
     rhythm nobody can tune. */
  margin-bottom: var(--s-6);
}

.interior .section__head .h2,
.interior .section__head h2 {
  font-size: clamp(1.75rem, 1.4rem + 1.05vw, 2.125rem);
  line-height: 1.15;
  letter-spacing: -0.03em;
  max-width: 22ch;
}

/* THE IN-HEAD MOTIF, after the head became one column.
   ------------------------------------------------------------------------
   `.motif-slot` was drawn for 1.0's two-column section head: a flex row with
   the words on the left and the mark on the right. 46.20 made interior heads
   one column, which left the mark stranded UNDER the lead with the whole
   right half of the row empty — a small lilac blob alone in a field of
   nothing, on `how.html` and `vision.html`.

   Taken out of flow rather than floated, because a float only pushes the
   content that FOLLOWS it and the mark is last in the markup on both pages.
   Moving it first would put a decoration ahead of the heading for a screen
   reader and for anyone reading source. It is `aria-hidden` and it appears
   only at 1100px and up, where the head's own 22ch/58ch caps leave the right
   side free — so nothing it could overlap is ever there. */
.interior .section__head:has(.motif-slot) {
  position: relative;
  min-block-size: clamp(6.5rem, 9vw, 10rem);
  padding-inline-end: clamp(8rem, 12vw, 13rem);
}

.interior .section__head .motif-slot {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: 0;
}

@media (max-width: 1099px) {
  .interior .section__head:has(.motif-slot) {
    min-block-size: 0;
    padding-inline-end: 0;
  }
}

.interior .section__head .lead,
.interior .section__head .section__note {
  margin-top: 12px;
  max-width: 58ch;
  font-size: 15.5px;
  line-height: 1.6;
}

/* The page head keeps the display step and its own measure — it is the one
   heading on an interior page that is the page rather than a section of it,
   and §46.17 fitted it to the design's own two points. */
.interior .page-head .display,
.interior .page-head__grid .display {
  font-size: var(--t-display);
  max-width: 22ch;
}

.interior .page-head__lede {
  max-width: 62ch;
  font-size: var(--t-lead);
  line-height: 1.6;
}


/* ==========================================================================
   46.21 THE TILE AND THE FIGURE CARD, AT THE DESIGN'S MEASUREMENTS
   ==========================================================================
   Both components were built in §46 from the design's description and never
   checked against its numbers. They are close and they are not the same:

                     approved design                     shipped
     tile radius     30px                                30px  ✓
     tile padding    24px                                24px  ✓
     tile title      700 19px/1.2, -.015em               600 inherited
     tile body       400 14px/1.55                       inherited
     tile track      flex 1 1 300px, min 260             grid minmax(16rem)
     stat radius     30px                                32px
     stat padding    26px                                26px  ✓
     stat number     500 46px/1, -.03em                  clamp to 44px
     stat label      600 14px/1.3                        600 inherited
     stat note       400 12.5px/1.5                      inherited

   The tracks are left as grids rather than converted to the prototype's
   flex rows. `flex: 1 1 300px` gives a last row of two cards each half the
   page wide; `auto-fit` keeps every card the same width as the row above
   it, which is what a set of peers should look like and what §45 chose for
   the mosaic for the same reason. The minimums are matched to the design's
   so the wrap points agree.
   -------------------------------------------------------------------------- */

.tiles {
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
}

/* 🔴 THE MOSAIC CELL IS THE CARD, at a different width — 2026-09-03.
   The design draws both from one recipe: radius 30, padding 24, a title at
   `700 19px/1.2` with -.015em tracking, a body at `400 14px/1.55`. Radius and
   padding already agreed. The type did not: `.bento__cell h3` was taking the
   generic `.h3` — 26px of Space Grotesk at weight 500, the display face — so
   the seven cells under *What is in it* were set a third larger and in a
   different typeface from every other card on the site. Owner: *"What is in
   it cards are different from the ones we are using newly"*.

   Written as one selector list rather than a second block with the same
   numbers in it, because two copies of a type scale is how they drift. A
   mosaic cell differs from a card in how many columns it spans, and in
   nothing else. */
.tile__h,
.bento__cell > .h3,
.bento__cell > h3 {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.015em;
  font-family: var(--font-body);
  color: var(--text);
}

.tile p,
.bento__cell > p {
  margin-top: var(--s-3);
  font-size: 14px;
  line-height: 1.55;
}

.statcards {
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
}

.statcard {
  border-radius: var(--r-glass-lg);
}

.statcard__n {
  font-size: clamp(2.25rem, 1.7rem + 1.6vw, 2.875rem);
}

.statcard__k {
  font-size: 14px;
  line-height: 1.3;
}

.statcard__p {
  margin-top: var(--s-2);
  font-size: 12.5px;
  line-height: 1.5;
}


/* ==========================================================================
   46.22 THE DARK PANEL, AT THE DESIGN'S MEASUREMENTS
   ==========================================================================
   The panel is the heaviest object on any page and the one every page has
   exactly one of, so its proportions set how the whole page reads. Measured
   off the prototype:

                     approved design        what shipped
     radius          34px                   30px
     padding         36px                   47.2px
     heading         500 30px/1.18, -.03em  500 44px/1.08
     heading width   20ch                   unbounded
     gap under it    16px                   0
     body            400 16px/1.65          400 19px/1.5
     body opacity    .84                    1
     body width      64ch                   862px

   The heading is the difference that matters. At 44px on a 1148px panel the
   head runs the full width, the body starts against it with no air, and the
   panel reads as a slab with a banner on it. At 30px capped to 20ch it wraps
   to two lines with the body beneath, which is the shape the design draws.

   ⚠️ THE BODY'S SOFTENING IS A COLOUR, NOT AN OPACITY. `opacity: .84` on a
   paragraph is a contrast change the stylesheet cannot see, and §46.3 exists
   because a token inside this panel measured 1.03:1 once. Written as the
   panel's own `color-mix` idiom instead — the same one §45's `.panel .stat__k`
   uses — it is a colour, so `site_contrast_test` can compute it. 84% of
   `--panel-on` over `--panel-dark` measures 11.9:1 on Calm.
   -------------------------------------------------------------------------- */

.interior .panel {
  border-radius: 34px;
  padding: clamp(26px, 3vw, 36px);
}

.interior .panel > .h2,
.interior .panel > h2 {
  font-size: clamp(1.5rem, 1.2rem + 0.9vw, 1.875rem);
  line-height: 1.18;
  letter-spacing: -0.03em;
  max-width: 20ch;
  margin-bottom: 16px;
}

.interior .panel > .lead,
.interior .panel > p {
  max-width: 64ch;
  font-size: 16px;
  line-height: 1.65;
  color: color-mix(in srgb, var(--panel-on) 84%, transparent);
}


/* ==========================================================================
   47. THE EFFECT-SIZE CHART
   ==========================================================================
   The approved design's `bars` block, built after its numbers were fetched
   from their primary sources. `docs/Research Registry.md` §"The effect-size
   chart on the public evidence page" holds every citation, every comparator
   and the two corrections; this section is only how it is drawn.

   ## The axis

   One axis, 0 to 1.05, so the largest bar nearly fills the track and the
   reader is never asked to compare two different scales. `--v` on each row
   is the effect size; the fill's width is `calc(var(--v) / 1.05 * 100%)`,
   which is the only arithmetic in the component.

   ## The band is a measurement, not a rounded shape

   The design draws a shaded band from 0 to 0.30 and calls it "where unguided
   self-help apps land as whole products". This one is drawn from **0.22 to
   0.56** — Firth et al. 2017's two figures, against an active control and
   against an inactive one. A range rather than a ceiling, because the width
   of that range IS the finding: the same category of product measures two
   and a half times larger depending only on what it was compared against.
   Every caveat under every bar is a version of that sentence.

   ## Nothing here is carried by colour alone

   Each bar prints its own number, its own source and its own caveat as text.
   The three fills are a third channel on top of length and label, so a
   reader who cannot separate them loses nothing — 1.4.1, and the same rule
   §12 applies to the mood scale.
   -------------------------------------------------------------------------- */

/* The notation card and the chart are one frame with two contents, so they
   are one rule. `--bars-max` lives here rather than on `.bars` alone because
   the card's ruler and the chart's bars are the SAME AXIS — 0.5 has to fall
   at the same x in both or the ruler is teaching a scale the chart does not
   use. */
.bars,
.notation {
  --bars-max: 1.05;
  margin: 0;
  padding: 28px;
  border-radius: var(--r-card);
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

.bars__plot {
  position: relative;
  /* Room under the last row for the band's two tick labels, which are drawn
     from the band itself so they cannot drift away from the edges they
     name. */
  padding-bottom: 26px;
}

/* The band. Behind the rows, and drawn from the two numbers rather than from
   a shape: 0.22 and 0.56 over the 1.05 axis. Written as `calc` so the axis
   and the band cannot drift apart. */
/* THE BAND IS TWO RULES AND A TINT, AND THE TINT IS ON THE TRACKS.
   The design fills the whole plot with `--soft`, which over this card is a
   near-white on a near-white and vanishes. Tinting the full height instead
   put a wash over every caveat paragraph on the chart and made the smallest
   type on the page the hardest to read — the opposite of what the caveats
   are for. So the band's two edges run the full height, where they are the
   reference lines a reader sights along, and the fill is painted into each
   bar's own track, where the comparison actually happens. */
.bars__band {
  position: absolute;
  inset-block: 0 26px;
  inset-inline-start: calc(0.22 / var(--bars-max) * 100%);
  inline-size: calc((0.56 - 0.22) / var(--bars-max) * 100%);
  border-inline: 1px dashed color-mix(in srgb, var(--text) 16%, transparent);
  pointer-events: none;
}

/* ⚠️ THE RULES CROSS THE CAVEATS, AND ON A PHONE THAT IS UNREADABLE. Each
   caveat is one line on a desk and five on a 390px screen, so two dashed
   verticals run straight down the middle of every paragraph on the chart —
   through the smallest type on the page, which is the type doing the most
   work. Below the breakpoint the band lives entirely inside the tracks,
   where the two solid edges below already draw it, and the two numbers are
   in the caption a paragraph down. */
@media (max-width: 700px) {
  .bars__band {
    display: none;
  }

  .bars__plot {
    padding-bottom: 0;
  }
}

/* The two numbers the edges stand for, under the plot rather than floating
   in it. Without them the band is a shape; with them it is a measurement. */
.bars__band::before,
.bars__band::after {
  position: absolute;
  top: 100%;
  padding-top: 6px;
  font-size: var(--t-micro);
  line-height: 1;
  color: var(--text-soft);
  white-space: nowrap;
}

.bars__band::before {
  content: "0.22";
  inset-inline-start: 0;
  transform: translateX(-50%);
}

.bars__band::after {
  content: "0.56";
  inset-inline-start: 100%;
  transform: translateX(-50%);
}

.bars__list {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.bar {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s-1) var(--s-4);
}

.bar__k {
  font-weight: 600;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--text);
}

.bar__n {
  font-family: var(--font-mono, ui-monospace, Menlo, monospace);
  font-size: 12px;
  line-height: 1.3;
  color: var(--text-soft);
  white-space: nowrap;
}

.bar__track {
  grid-column: 1 / -1;
  margin-top: 4px;
  block-size: 14px;
  border-radius: var(--r-pill);
  background-color: color-mix(in srgb, var(--text) 8%, transparent);
  /* The band, painted into the track itself: a tint between the two
     numbers, with a 1px edge at each of them. Drawn here as well as by
     `.bars__band` so that the band survives the phone rule above, where the
     full-height verticals are removed. */
  background-image: linear-gradient(to right,
    transparent 0 calc(0.22 / var(--bars-max) * 100%),
    color-mix(in srgb, var(--text) 22%, transparent)
      calc(0.22 / var(--bars-max) * 100%) calc(0.22 / var(--bars-max) * 100% + 1px),
    color-mix(in srgb, var(--text) 7%, transparent)
      calc(0.22 / var(--bars-max) * 100% + 1px) calc(0.56 / var(--bars-max) * 100% - 1px),
    color-mix(in srgb, var(--text) 22%, transparent)
      calc(0.56 / var(--bars-max) * 100% - 1px) calc(0.56 / var(--bars-max) * 100%),
    transparent calc(0.56 / var(--bars-max) * 100%) 100%);
  overflow: hidden;
}

.bar__fill {
  display: block;
  block-size: 100%;
  inline-size: calc(var(--v) / var(--bars-max) * 100%);
  border-radius: var(--r-pill);
  background: rgb(var(--rgb-dusk));
}

/* THREE STATES, AND THEY ARE THE BAND. Above its ceiling (0.56), inside it,
   and below its floor (0.22) — so the colour says the same thing the bar's
   position against the shading already says, rather than encoding a second,
   invented set of thresholds. The design's cut-points were .8 and .4, which
   are round numbers that no paper on this chart produced.

   Every one of the three is also written out in the row beneath the bar. */
.bar--above .bar__fill { background: var(--accent); }
.bar--under .bar__fill { background: rgb(var(--rgb-sand)); }

.bar__note {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--text-soft);
}

.bars__key {
  display: flex;
  gap: var(--s-2);
  align-items: flex-start;
  margin-top: var(--s-7);
  padding-top: 18px;
  border-top: 1px solid var(--hair);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-soft);
}

.bars__key::before {
  content: "";
  flex: none;
  inline-size: 11px;
  block-size: 11px;
  margin-top: 4px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--text) 5%, transparent);
  border: 1px dashed color-mix(in srgb, var(--text) 26%, transparent);
}

/* A phone reads the label, the number and the caveat as three lines; the
   number stops sitting on the label's right where there is no room for it. */
@media (max-width: 560px) {
  .bar {
    grid-template-columns: 1fr;
  }

  .bar__n {
    white-space: normal;
  }
}


/* ==========================================================================
   48. THE REPLY HOLDS ITS HEIGHT
   ==========================================================================
   Every step of the ladder is a different length — one line or two, one offer
   or two — so the reply block was 145px at "Low" and 223px at "Really low",
   and dragging the slider made the whole pane pump between them. That is the
   page flinching at a control the reader is still holding.

   Fixed in `index.html` rather than here: the script paints all five states
   once, keeps the tallest, and holds it as a `min-height`. A number written
   into this file would be wrong at the first breakpoint and wrong again the
   first time a line is re-worded — and wrong invisibly, because nobody sees
   it until they drag the slider at that width.

   There was briefly a registered `--c` here so the track's fill could
   interpolate between the five mood colours. The design's slider has no fill
   to interpolate: the whole ladder is painted into the track and the thumb
   slides along it. What answers a mood change now is Pebble, on the design's
   own 420ms, and the reply under it. */

/* ==========================================================================
   49. PEBBLE
   ==========================================================================
   The app's face, and the one piece of 2.0 this site drew nothing of.

   Taken from the APPROVED SITE DESIGN's own `pebble()` — `Moorwell Site.dc.html`
   — and not from `spec/03-pebble-mascot.md`. The two disagree, and the
   disagreement is deliberate on the design's part: the app's Pebble carries a
   blush, a second glint, a mouth mask and a twinkle, and the site's carries
   none of them. A mascot at 52px in a page of argument does not need the
   parts that read at 170px on a phone, and drawing them anyway would have
   put detail on this site that the design's own file says to leave out.

   So: body, sheen, two eyes with one glint each, the heavy lid on the two
   low moods, and the mouth. Every length is a fraction of `--pb`, which is
   the design's `S` — 52 here, and free to be set per instance.

   Pebble is never drawn on a support or crisis screen (spec §3.6, psychology
   §5). There is no modifier here that would let one be. */

.pebble {
  --pb: 52px;
  --pb-mood: 2;

  /* The five mood fills, before the look's tint. */
  --pb-raw1: #d4ccec;
  --pb-raw2: #b4a6dc;

  /* The look's tint. `color-mix(in srgb, X 0%, Y)` is Y, so a look that does
     no tinting sets the amount to zero and leaves the pair alone — which is
     exactly what Calm does (`amt: 0` in the design's LOOKS table). */
  --pb-mix: #ffffff;
  --pb-amt: 0%;
  --pb-eye: #3a3050;
  --pb-ring: 0 0 0 0 transparent;

  --pb-c1: color-mix(in srgb, var(--pb-mix) var(--pb-amt), var(--pb-raw1));
  --pb-c2: color-mix(in srgb, var(--pb-mix) var(--pb-amt), var(--pb-raw2));
  --pb-eye-w: calc(var(--pb) * 0.19);
  --pb-eye-h: calc(var(--pb-eye-w) * 1.34);
  --pb-dome: calc((44 + var(--pb-mood)) * 1%);

  position: relative;
  display: block;
  flex: none;
  inline-size: var(--pb);
  block-size: var(--pb);
  border-radius: 50% 50% var(--pb-dome) var(--pb-dome);
  background: radial-gradient(62% 58% at 34% 24%,
    var(--pb-c1), var(--pb-c2) 84%);

  /* ONE DEVIATION FROM THE DESIGN'S `pebble()`, and it is a bug fix.
     The sheen is drawn at `top: -8%`, so 8% of it hangs above the dome. On
     the design's own Calm ground — near-white behind a near-white highlight
     — that overhang is invisible, which is presumably why it survived. On
     Night it is a grey lump floating above the mascot's head. A sheen is a
     highlight ON a dome; clipping it to the dome is what the spec means by
     "keeps the dome feeling glassy". Nothing else reaches the edge: the eyes
     and the lid are inset 0.2S and the mouth is centred, so this clips the
     sheen and only the sheen. */
  overflow: hidden;
}

.pebble[data-mood="0"] { --pb-raw1: #cfc7e6; --pb-raw2: #b0a4d2; }
.pebble[data-mood="1"] { --pb-raw1: #cbd6ea; --pb-raw2: #a9bcda; }
.pebble[data-mood="2"] { --pb-raw1: #d4ccec; --pb-raw2: #b4a6dc; }
.pebble[data-mood="3"] { --pb-raw1: #e7dfb6; --pb-raw2: #cfc186; }
.pebble[data-mood="4"] { --pb-raw1: #f0d8c0; --pb-raw2: #dcb894; }

/* Moods 0 and 1 get the shorter eye and the heavy lid. */
.pebble[data-mood="0"],
.pebble[data-mood="1"] { --pb-eye-h: calc(var(--pb-eye-w) * 1.12); }

/* THE LOOK'S TINT, from the design's LOOKS table. Calm does none. Sage and
   Night carry a mix, an amount and their own eye colour, and Night adds the
   ring: never black eyes in the dark — a light ring and a glint keep the
   face friendly rather than staring. */
:root[data-theme="sage"] .pebble {
  --pb-mix: #a8c896;
  --pb-amt: 30%;
  --pb-eye: #2e4030;
}
:root[data-theme="dark"] .pebble {
  --pb-mix: #d3c2f5;
  --pb-amt: 22%;
  --pb-eye: #4c3f6c;
  --pb-ring: 0 0 0 calc(var(--pb) * 0.012) rgba(255, 255, 255, 0.16);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="sage"]) .pebble {
    --pb-mix: #d3c2f5;
    --pb-amt: 22%;
    --pb-eye: #4c3f6c;
    --pb-ring: 0 0 0 calc(var(--pb) * 0.012) rgba(255, 255, 255, 0.16);
  }
}

/* The sheen, above the body and below the eyes — which is why it is the
   host's own ::before rather than another element in the markup. */
.pebble::before {
  content: "";
  position: absolute;
  inset-block-start: -8%;
  inset-inline-start: 6%;
  inline-size: 56%;
  block-size: 34%;
  border-radius: 50%;
  transform: rotate(-18deg);
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
}

.pebble__eye {
  position: absolute;
  inset-block-start: calc(var(--pb) * 0.325);
  inline-size: var(--pb-eye-w);
  block-size: var(--pb-eye-h);
  border-radius: 50%;
  background: var(--pb-eye);
  box-shadow: var(--pb-ring);
}
.pebble__eye:nth-of-type(1) { inset-inline-start: calc(var(--pb) * 0.2); }
.pebble__eye:nth-of-type(2) { inset-inline-end: calc(var(--pb) * 0.2); }

/* One glint, at 0.38 of the eye's width. */
.pebble__eye::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--pb-eye-w) * 0.2);
  inset-block-start: calc(var(--pb-eye-w) * 0.16);
  inline-size: calc(var(--pb-eye-w) * 0.38);
  block-size: calc(var(--pb-eye-w) * 0.38);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.94);
}

/* The heavy lid: the dome's own gradient closing over the eye, so it reads
   as tired rather than as a line drawn across a face. */
.pebble__lid {
  position: absolute;
  inset-inline: calc(var(--pb) * 0.2);
  inset-block-start: calc(var(--pb) * 0.325);
  block-size: calc(var(--pb-eye-h) * 0.34);
  background: radial-gradient(62% 58% at 34% 24%,
    var(--pb-c1), var(--pb-c2) 84%);
}
.pebble:not([data-mood="0"]):not([data-mood="1"]) .pebble__lid { display: none; }

.pebble__mouth {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: calc(var(--pb) * 0.605);
  inline-size: calc((0.155 + var(--pb-mood) * 0.013) * var(--pb));
  block-size: calc((0.07 + var(--pb-mood) * 0.015) * var(--pb));
  transform: translateX(-50%);
  border-radius: 44% 44% 58% 58%;
  background: #e0836f;
}
.pebble[data-mood="0"] .pebble__mouth,
.pebble[data-mood="1"] .pebble__mouth { border-radius: 58% 58% 40% 40%; }

/* The design transitions everything on a mood change at 420ms — "everything
   interpolates; nothing pops". It is feedback rather than decoration, which
   is why spec §3.5 keeps it under reduced motion and so does this: the
   properties below are colour and shape, the tint channel, and none of them
   moves the mascot across the page. */
.pebble,
.pebble__mouth,
.pebble__lid,
.pebble__eye {
  transition: all 420ms cubic-bezier(0.23, 1, 0.32, 1);
}


/* ==========================================================================
   50. THE 2.0 HOME SCREEN'S OWN PARTS
   ==========================================================================
   The Highlights page rebuilt Home from 1.0: a greeting, a bare day count, a
   goals prompt, two buttons and one suggestion. The 2.0 screen is a different
   composition — `lib/screens/v2/home_v2.dart` — and these are the three parts
   of it the site had no component for.

   Everything here is `ui-` prefixed, which is the boundary §"The app" sets:
   inside a reproduction, nothing reachable from the site's own system.

   Sources, so each can be diffed rather than trusted: the mood strip is
   `_moodStrip` (gap 7, corner 16); the suggestion pair is `defaultSuggestion`
   and `defaultBreathe`; the small things are `defaultSmallThings` with their
   family tints and their `quiet` tags; the Lately row is `defaultLately`.

   ⚠️ NO DURATIONS ON THE SUGGESTIONS, and that is load-bearing rather than an
   omission. They carried "2 min", "3 min" and "5 min" in the app until those
   were removed: no content declares a length, `BreathingScreen` runs until
   you stop it, and a number printed beside live output inherits its
   authority. The design's own screen frame still asks for a "cost chip". The
   app is the newer fact and it wins, so there is no chip component here for
   one to be put in. */

.ui-moods {
  display: flex;
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ui-mood {
  flex: 1;
  min-inline-size: 0;
  border-radius: 16px;
  padding: 10px 4px;
  text-align: center;
  font-size: 11px;
  line-height: 1.25;
  font-weight: 600;
  color: var(--app-text);
  background: color-mix(in srgb, var(--ui-mood-c) 34%, transparent);
  border: 1px solid color-mix(in srgb, var(--ui-mood-c) 52%, transparent);
}

/* The five fills are Pebble's, because they are the same ladder: the tile a
   person taps and the face that answers are one gesture, and two ladders that
   nearly matched would be worse than either. */
.ui-mood:nth-child(1) { --ui-mood-c: #b0a4d2; }
.ui-mood:nth-child(2) { --ui-mood-c: #a9bcda; }
.ui-mood:nth-child(3) { --ui-mood-c: #b4a6dc; }
.ui-mood:nth-child(4) { --ui-mood-c: #cfc186; }
.ui-mood:nth-child(5) { --ui-mood-c: #dcb894; }

/* THE SUGGESTION PAIR. One dark panel and one light, side by side and the
   same size — the design's note is that swapping the suggestion must read as
   normal rather than as a failure, which only holds if the thing beside it
   is its equal. */
.ui-heroes {
  display: flex;
  gap: var(--s-3);
  align-items: stretch;
}

.ui-hero {
  flex: 1 1 0;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-4);
  border-radius: 20px;
  padding: 16px;
  font-size: 14.5px;
  line-height: 1.3;
  font-weight: 600;
  background: var(--app-card);
  border: 1px solid var(--app-hairline);
}

.ui-hero--dark {
  background: var(--app-panel);
  color: var(--app-on-panel);
  border-color: transparent;
  flex: 1.35 1 0;
}

.ui-hero__go {
  align-self: flex-start;
  inline-size: 30px;
  block-size: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--app-accent);
  /* `calm.onAccent` — the ink the app puts ON its accent, which is the same
     value as `panelDark` and a different role, so it takes that token rather
     than a literal that happens to match today. */
  color: var(--app-panel);
}

.ui-hero--dark .ui-hero__go { background: var(--app-accent); }

/* The reroll. Deliberately quiet and deliberately not a button: on the screen
   it is a text control under the pair, and drawing it as a third button here
   would make the screen look like it offers three things. */
.ui-reroll {
  margin-top: var(--s-3);
  font-size: 13px;
  font-weight: 600;
  color: var(--app-accent-ink);
}

/* THE FAMILY ROWS. A tinted rule rather than a tinted fill: three filled
   rows under a check-in card is more colour than the screen carries, and the
   tint is there to say which family a row belongs to, not to decorate it. */
.ui-small {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  border-radius: 14px;
  padding: 12px 14px;
  background: var(--app-card);
  border: 1px solid var(--app-hairline);
  border-inline-start: 3px solid var(--ui-fam, var(--app-accent));
}

.ui-small__t {
  flex: 1;
  min-inline-size: 0;
  font-size: 14px;
  font-weight: 600;
}

.ui-small--sage  { --ui-fam: #aecb9e; }   /* AppV2Family.sage */
.ui-small--lilac { --ui-fam: #baacd6; }   /* AppV2Family.lilac */
.ui-small--steel { --ui-fam: #aac0da; }   /* AppV2Family.steel */

/* The tag. `quiet` is a tag rather than a measurement, which is exactly why
   it survived the cull that took the minute counts out. */
.ui-tag {
  flex: none;
  border-radius: 999px;
  padding: 3px 9px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--app-soft);
  background: color-mix(in srgb, var(--app-text) 7%, transparent);
}

/* LATELY, and never a chart on Home. The state in words, the retrospective
   count, and a chevron. */
.ui-lately {
  display: flex;
  align-items: center;
  gap: 12px;
  border-radius: 14px;
  padding: 13px 14px;
  background: var(--app-card);
  border: 1px solid var(--app-hairline);
}

.ui-lately__t { flex: 1; min-inline-size: 0; font-size: 13.5px; }
.ui-lately__n { flex: none; font-size: 12px; color: var(--app-soft); }


/* ==========================================================================
   51. WHAT d, g AND SMD MEAN
   ==========================================================================
   The approved design's `notation` block, and the one block of its evidence
   page this site never built. Its own comment above it reads: *"Nobody should
   need a statistics course to read this page."*

   It became necessary rather than nice once the chart shipped. The chart
   prints `d+ = 0.99`, `g = 0.83` and `SMD -0.32` down its right-hand side,
   and a reader who does not already know that g and d are the same scale, or
   that a minus sign on a sleep measure is the good direction, cannot tell
   whether the bars are comparable. So it sits directly above the chart.

   The ruler shares the chart's axis — see `--bars-max` in §47 — so 0.5 is at
   the same x in both. A ruler that taught one scale above a chart drawn on
   another would be worse than no ruler.

   The closing sentence is the design's, and it carries the page's whole
   argument: the conventions are conventions, and a small effect delivered
   cheaply to many people can matter more than a large one behind a waiting
   list. That is the case for the app existing, and it is why the ceiling at
   the top of the page is not an apology. */

.notation {
  margin: 0;
  padding: 28px;
  border-radius: var(--r-card);
  background: var(--glass-bg);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-lift);
}

.notation__terms {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  margin: 0;
}

.notation__term {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  flex-wrap: wrap;
}

.notation__tag {
  flex: none;
  min-inline-size: 52px;
  block-size: 32px;
  padding: 0 12px;
  border-radius: var(--r-pill);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-dark);
  color: var(--panel-on);
  font-family: var(--font-head);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
}

.notation__say { flex: 1 1 260px; min-inline-size: 0; }

.notation__say b {
  display: block;
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

.notation__say p {
  margin: var(--s-2) 0 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-soft);
}

.notation__rule {
  margin: var(--s-6) 0 var(--s-6);   /* snapped to the --s- scale */
  border: 0;
  border-block-start: 1px solid var(--hairline);
}

.notation__k {
  font-family: var(--font-head);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.notation__ruler {
  position: relative;
  block-size: 14px;
  margin-block-start: 16px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg,
    var(--soft-track), color-mix(in srgb, rgb(var(--rgb-sage)) 55%, transparent));
}

.notation__tick {
  position: absolute;
  inset-inline-start: calc(var(--t) / var(--bars-max) * 100%);
  inset-block-start: -5px;
  inline-size: 2px;
  block-size: 24px;
  background: var(--text-soft);
  opacity: 0.5;
}

.notation__scale {
  position: relative;
  block-size: 34px;
  margin-block-start: 8px;
}

.notation__at {
  position: absolute;
  inset-inline-start: calc(var(--t) / var(--bars-max) * 100%);
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
}

.notation__at b {
  display: block;
  font-family: var(--font-head);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  color: var(--text);
}

.notation__at span {
  display: block;
  margin-block-start: 4px;
  font-size: 11.5px;
  line-height: 1;
  color: var(--text-soft);
}

.notation__note {
  margin: var(--s-3) 0 0;
  max-width: 70ch;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-soft);
}

/* At the narrow end the three labels collide, so the ruler becomes a list.
   A scale nobody can read is worse than a sentence. */
@media (max-width: 560px) {
  .notation__ruler,
  .notation__scale { display: none; }

  .notation__bands {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2) var(--s-5);
    margin-block-start: 12px;
    font-size: 13px;
    color: var(--text-soft);
  }
}

@media (min-width: 561px) {
  .notation__bands { display: none; }
}

/* --------------------------------------------------------------------------
   45.6 The palette switcher fits the phone it is on
   --------------------------------------------------------------------------
   🔴 **Measured 2026-09-07: the switcher was cut off on every phone narrower
   than 390px, and under 320px it took the whole page sideways with it.**

   `.theme__set` is a flex row of three labelled pills that wants 292px, and
   `overflow: visible` (§45.5's block) let it push past its container instead
   of being contained by it. Measured in the browser, `documentElement`
   scrollWidth against the viewport:

       280px viewport -> 344px document   (page scrolls sideways)
       320px viewport -> 344px document   (page scrolls sideways)
       360px viewport -> fits, but the set has 266px of the 292 it needs,
                         so the third option is clipped
       390px and up   -> fits

   320px is the width this project's own tests treat as the narrowest phone it
   supports (`large_text_overflow_test` measures there), so the site was broken
   at its own floor. A page that scrolls sideways is the single most legible
   sign of an unfinished site, and it was on every page — the masthead is
   shared.

   **Wrapped rather than scrolled.** A horizontal scroller inside a pill needs
   an affordance or the third option is invisible with no hint that it exists;
   hiding the labels below a breakpoint would leave three unlabelled dots,
   which is worse for the same reason. Wrapping keeps every label readable and
   costs one extra row on small phones only. `align-self: flex-start` stops the
   wrapped block stretching the masthead row.

   The breakpoint is the measurement, not a round number: 390px is where the
   set stops being clipped. ----------------------------------------------- */

@media (max-width: 389px) {
  .theme__set {
    flex-wrap: wrap;
    justify-content: center;
    max-width: 100%;
    align-self: flex-start;
  }
}

/* --------------------------------------------------------------------------
   45.7 The features hero stops clipping its own heading
   --------------------------------------------------------------------------
   🔴 **Measured 2026-09-07 at 320px: 38px of *"Six screens, and this is the
   one it opens on."* was cut off and unreachable.**

   `.feat-hero` (§8166) is `display: grid` with no `grid-template-columns`, so
   it gets one implicit `auto` column — and a grid item's `min-width` defaults
   to `auto`, which means the column may not shrink below the widest item's
   max-content width. The `h1.display` at 40px measures 338px max-content, so
   the column resolved to **338.219px inside a 280px content box**, and
   `section.page-head` clips at `overflow-x: hidden` rather than scrolling. The
   text was not merely off-screen: it was unreachable.

   The page did not scroll sideways, so the document-level check that caught
   the masthead (§45.6) said nothing about this one — a clipped overflow is
   invisible to `scrollWidth`. It has to be measured per element against the
   viewport edge.

   The two-column rule at `min-width: 1000px` already reads
   `minmax(0, 52fr) minmax(0, 48fr)`, so the pattern was known and the
   single-column default simply missed it. This is that rule's first half,
   applied at every width below the breakpoint; the media query still wins
   above it. ------------------------------------------------------------- */

.feat-hero {
  grid-template-columns: minmax(0, 1fr);
}

/* --------------------------------------------------------------------------
   45.8 The prose page had no vertical rhythm, and no measure
   --------------------------------------------------------------------------
   🔴 **Measured 2026-09-13 on privacy.html — the only page in the site built
   as flat prose (`main.wrap.prose`, 168 direct children). Every other page is
   components, and a component carries its own spacing, so nothing about this
   was ever visible anywhere else.**

   Three faults, one cause. The base reset at §368-§397 zeroes `margin` on
   `h1-h4` and `p`, and `margin` *and* `padding` on `ul`/`ol`/`dl`/`dd`. That
   is right for a component page, where `.section` supplies `padding-block`
   and a grid supplies `gap`. On a page that is 168 bare elements in a row it
   means there is no spacing at all — the only breaks on the page were the
   ones `.prose .callout` (§3492) draws for itself, which is why the fault
   read as "some sections are fine".

   Measured at 1100px, before -> after:

   | | before | after |
   |---|---|---|
   | paragraph to paragraph | 7px, and that is leading, not margin | 20px |
   | paragraph to the h3 above it | **2px** | 32px |
   | h3 to the paragraph below it | 2px | 8px |
   | list item to list item | **1px** | 16px |
   | content column | 1012px | 772px, which is 62ch |
   | list markers | drawn outside the box | drawn |

   **The 2px is the finding.** A subheading sat closer to its neighbours than
   two ordinary paragraphs sat to each other, and by the *same* amount above
   and below, so it bound to neither — 11 of the page's 22 headings measured
   `above <= below`. A heading with equal air on both sides is a bold lead-in,
   and that is exactly how the page read. The rule the rhythm below encodes is
   that the space above a heading is 3-4x the space under it.

   The measure is a second, independent fault. §439 says
   `.prose { max-width: var(--measure) }`; §45.2, added by the redesign 12,000
   lines later, says `.wrap { max-width: 1240px }`. Both are one class of
   specificity, so **source order decided it and the later rule won** — the
   page had been running 1012px of content against its own stated 772px, 31%
   over measure, since the redesign landed. `.wrap.prose` is two classes, so
   the fix wins on specificity and does not depend on where it sits in the
   file. It adds the gutter back because `--measure` is a *content* width
   everywhere else it is used (§537, §544) and `.wrap` is `border-box`.

   The lists are the third: `padding: 0` against the default
   `list-style-position: outside` draws every marker outside the content box.
   The eight-item `ul` under *"The short version"* rendered as one wall of
   text with no bullets and no gaps.

   Scope: `.prose` is on privacy.html and on no other page, so none of this
   can reach the other seven. The heading-proximity probe that found it was
   run over all eight at 1100px and at 375px; the hits elsewhere are all
   eyebrow-then-heading pairs, which are bound deliberately and are correct.
   -------------------------------------------------------------------------- */

/* The measure, restored. Two classes, so §45.2 no longer outranks it. */
.wrap.prose {
  max-width: calc(var(--measure) + 2 * var(--wrap-gutter));
}

/* The default step between blocks. Everything else below is a departure from
   this one number, which is why it is set with the owl rather than per tag. */
.prose > * + * {
  margin-top: var(--s-5);
}

/* Above a heading. A heading's own `margin-bottom` cannot do this job: it is
   adjacent to the next block's `margin-top` with nothing between them, so the
   two collapse to the larger and the smaller one is simply discarded. The
   space *under* a heading is therefore set on the element that follows it. */
.prose > h2 {
  margin-top: var(--s-8);
}

.prose > h3 {
  margin-top: var(--s-7);
}

.prose > h4 {
  margin-top: var(--s-6);
}

/* Under a heading — set here, on the follower, for the collapsing reason
   above. `.prose .callout` and `.prose .quote` are two classes and keep their
   own 1.5rem, which is correct: a callout is not a paragraph. */
.prose > h2 + * {
  margin-top: var(--s-3);
}

.prose > h3 + *,
.prose > h4 + * {
  margin-top: var(--s-2);
}

/* Two of the page's headings are followed by a callout rather than by a
   paragraph. `.prose .callout` is two classes, so it outranked the follower
   rule above and kept its own 1.5rem — leaving those two headings with more
   air below than above, which is the exact defect this section exists to fix.
   Two classes and a type outrank two classes. */
.prose > h2 + .callout,
.prose > h2 + .quote,
.prose > h3 + .callout,
.prose > h3 + .quote,
.prose > h4 + .callout,
.prose > h4 + .quote {
  margin-top: var(--s-3);
}

/* The markers had nowhere to be drawn, and then nothing to draw. `padding: 0`
   at §397 put the marker box outside the content box, and `li { list-style:
   none }` at §401 removed the glyph outright. Both are right for the rest of
   the site — every other list on it is a nav, a step ladder or a card grid,
   none of which wants a disc — and both are wrong for a document. All 26 list
   items under `.prose` are in a bare `ul` or `ol` directly beneath it, so
   there is no component list here to disturb.

   `em` rather than a token, so the indent tracks the text it hangs off. */
.prose ul,
.prose ol {
  padding-left: 1.5em;
}

.prose ul > li {
  list-style-type: disc;
}

.prose ol > li {
  list-style-type: decimal;
}

/* The glyph is not the sentence. Full-strength ink on a disc pulls the eye to
   the marker instead of to the first word. */
.prose li::marker {
  color: var(--text-soft);
}

/* These items are paragraphs, not phrases — the eight under "The short
   version" run to four lines each — so they are spaced as paragraphs, one
   step below the paragraph step so the list still reads as one block. */
.prose li + li {
  margin-top: var(--s-4);
}
