/* Roizin Refinery — mail-in site.
   Colour, type and spacing come from brand.css. Nothing here retypes a token:
   a second copy of a hex value is how two surfaces silently drift apart. */

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

:root {
  /* Layout only. Every colour and face is inherited from brand.css. */
  --gut: clamp(20px, 5vw, 56px);
  --measure: 62ch;
  --wrap: 1180px;
  --rule: 1px solid var(--rz-aluminium-light);
  --ease: cubic-bezier(.16, 1, .3, 1);

  /* The house curve. The same cubic-bezier drives every entrance on the other
     Roizin-adjacent sites, so motion here reads as the same hand. Interactive
     state changes keep --ease; entrances use this. */
  --ease-house: cubic-bezier(0.22, 1, 0.36, 1);

  /* Two tints derived for surfaces that must sit *on* paper without becoming
     a second ground. Both are the token colours at low alpha, never new hues. */
  --tint-ink: rgba(26, 20, 19, .045);
  --tint-ox:  rgba(150, 25, 28, .07);
}

/* ---------- Web-only token overrides ----------

   These override --rz-size-* and --rz-aluminium, and add the three material
   colours, for this site and ONLY this site.

   They are deliberately NOT edited in css/brand.css, because that file is the
   shared token sheet: Marketing/design/brand/templates/invoice.html,
   letterhead.html and business-card.html import the same values. Those are
   print surfaces at a fixed physical size — an invoice is read at 300mm, a
   business card at 85mm, and neither wants a 19px body. Raising the token
   would silently resize every printed invoice and every card in the next
   order. A screen read at arm's length wants more; paper does not. So the
   scale forks here, downstream of the tokens, where only the web can see it.

   The same logic now applies to colour. --rz-aluminium at #8C8F92 measures
   2.94:1 against --rz-paper — well under the 4.5:1 AA floor for normal text —
   and it is what .benefit p, .flow p, .steps p, .deflist dd, .muted, .label,
   the footer links and .field__hint (css/quote.css, which loads this sheet
   too) all render with. Darkening the token in brand.css would darken every
   printed invoice and business card along with it, which is not the print
   surfaces' problem to inherit. #66696C keeps the aluminium hue and forks it
   here instead: 5.00:1 on bare paper, an 11% margin over the 4.5:1 floor, and
   still clearly lighter than --rz-ink (16.5:1) so the secondary/primary text
   hierarchy holds.

   There were nine --rz-web-<metal>-<stop> material colours here too, read by
   js/backdrop.js. The backdrop was removed 2026-08-31 and they went with it;
   nothing else on the site ever used them. If a surface needs gold, silver or
   platinum again, it forks here for the same reason the sizes do, and not in
   brand.css, which invoice.html, letterhead.html and business-card.html share.

   Keep this block the single place the web deviates. A one-off font-size or
   colour on a component is how the two scales drift back apart. */
:root {
  --rz-size-body: 19px;      /* was 16px */
  --rz-size-heading: 30px;   /* h3 — was 26px */
  --rz-size-subhead: 21px;   /* h4, .steps h3 — was 18px */
  --rz-size-label: 12px;     /* .label — was 11px */
  --rz-size-data: 17px;      /* was 15px */
  --rz-aluminium: #66696C;   /* was #8C8F92 — 2.94:1 on paper, now 5.00:1 */
}

html { -webkit-text-size-adjust: 100%; }

/* The masthead is sticky and 70px tall, so an in-page anchor lands its target
   underneath it. That is not cosmetic here: #site-footer is where the no-JS
   menu link goes, and a visitor with scripts off would arrive at a footer whose
   first line is behind the header. Set on the root so every fragment target on
   the site clears it, including .skip -> #main. */
html { scroll-padding-top: calc(70px + var(--rz-space-sm)); }

/* The paper ground lives on html, not body, and body is transparent below.
   That split was originally what let a z-index:-1 canvas sit between the two;
   the canvas is gone, but the arrangement is harmless and moving the ground
   back onto body would change what a full-height page paints when content is
   shorter than the viewport. Left as it is deliberately. */
html { background: var(--rz-paper); }

body {
  margin: 0;
  background: transparent;
  color: var(--rz-ink);
  font-family: var(--rz-font-text);
  font-size: var(--rz-size-body);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- Rhythm ---------- */

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gut); }
.wrap-narrow { max-width: 760px; }
section { padding-block: clamp(56px, 9vw, 112px); }
.rule-top { border-top: var(--rule); }

/* ---------- Type ---------- */

h1, h2, h3, h4 {
  font-family: var(--rz-font-display);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -.015em;
  margin: 0 0 var(--rz-space-md);
  text-wrap: balance;
}
h1 { font-size: clamp(46px, 32.10px + 3.564vw, 82px); letter-spacing: -.025em; }
h2 { font-size: clamp(32px, 25.05px + 1.782vw, 50px); }
h3 { font-size: var(--rz-size-heading); }
h4 { font-size: var(--rz-size-subhead); }

p { margin: 0 0 var(--rz-space-md); max-width: var(--measure); }
.lede { font-size: clamp(21px, 19.07px + 0.495vw, 26px); line-height: 1.55; color: var(--rz-ink); }
.muted { color: var(--rz-aluminium); }
.small { font-size: 17px; line-height: 1.55; }

/* The one small-caps label style. Mono, tracked, aluminium — it marks a section
   without spending the accent colour on it. */
.label {
  font-family: var(--rz-font-data);
  font-size: var(--rz-size-label);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--rz-aluminium);
  margin: 0 0 var(--rz-space-sm);
  display: block;
}

/* Figures must line up in a column or they get misread. brand.css defines
   .rz-data / .rz-money / .rz-weight; this is the display-size variant. */
.figure {
  font-family: var(--rz-font-data);
  font-variant-numeric: tabular-nums;
  font-size: clamp(34px, 27.82px + 1.584vw, 50px);
  letter-spacing: -.02em;
  line-height: 1;
}

a { color: inherit; }
a:not([class]) {
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
  text-decoration-color: var(--rz-aluminium-light);
  transition: text-decoration-color .25s var(--ease);
}
a:not([class]):hover { text-decoration-color: var(--rz-oxblood); }

/* Focus is never removed, only restyled. Keyboard is the whole funnel's
   fallback and it has to stay visible on every ground. */
:focus-visible {
  outline: 2px solid var(--rz-oxblood);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------- Skip link ---------- */

.skip {
  position: absolute; left: -9999px;
  background: var(--rz-oxblood); color: #fff;
  padding: 10px 18px; border-radius: var(--rz-radius-pill);
  z-index: 200; font-size: 16px;
}
.skip:focus { left: var(--gut); top: 12px; }

/* ---------- Header ---------- */

.masthead {
  position: sticky; top: 0; z-index: 60;
  background: color-mix(in srgb, var(--rz-paper) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: var(--rule);
}
.masthead .wrap { display: flex; align-items: center; justify-content: space-between; gap: var(--rz-space-md); min-height: 70px; }
.masthead__logo { display: flex; align-items: center; min-height: 44px; }

/* A link that IS its paragraph is a standalone destination, not a word inside a
   sentence, so the WCAG 2.5.8 prose exemption does not cover it and 23px is not
   enough to hit. There is one on the site today ("Read the full program
   rules"); the selector is here so the next one is born the right size. */
p > a:only-child { display: inline-flex; align-items: center; min-height: 44px; }
/* Brand minimum is 140px for the lockup — below that use the symbol. */
.masthead__logo img { height: 30px; width: auto; display: block; }

.nav { display: flex; align-items: center; gap: clamp(14px, 2.2vw, 30px); }
.nav a {
  font-size: 16px; text-decoration: none; color: var(--rz-ink);
  position: relative; padding-block: 4px;
}
.nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: var(--rz-oxblood); transition: width .32s var(--ease);
}
.nav a:hover::after, .nav a[aria-current="page"]::after { width: 100%; }
.nav a[aria-current="page"] { color: var(--rz-oxblood); }

/* ---------- The mobile menu ----------

   What was here: `@media (max-width: 780px) { .nav .nav__hide { display:none } }`
   — five navigation links deleted, nothing put in their place, on every phone
   AND on iPad portrait, which is 768. The site below that width had no
   navigation at all except the footer.

   Two controls ship in the markup and exactly one is ever shown. `.nav__nojs`
   is a plain anchor to the footer, which already lists every page; it is the
   default, so a visitor with no JavaScript gets something that works. js/nav.js
   adds `html.rz-nav` and the button takes over. Both are 44px in the same slot,
   so the swap costs no layout shift when a deferred script lands. */

.nav__toggle { display: none; }
.nav__nojs {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 16px; text-decoration: none; color: var(--rz-ink);
}
html.rz-nav .nav__nojs { display: none; }
html.rz-nav .nav__toggle {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 44px; padding: 0 4px;
  background: none; border: 0; cursor: pointer;
  font-family: var(--rz-font-text); font-size: 16px; color: var(--rz-ink);
}

/* Three lines, drawn rather than shipped as an icon file. */
.nav__burger, .nav__burger::before, .nav__burger::after {
  display: block; width: 20px; height: 2px; background: var(--rz-ink);
  border-radius: 2px; content: "";
}
.nav__burger { position: relative; }
.nav__burger::before { position: absolute; top: -6px; }
.nav__burger::after  { position: absolute; top: 6px; }

.masthead .wrap { position: relative; }   /* the panel anchors to this */

@media (max-width: 719.98px) {
  /* Not `html.rz-nav .nav__menu` — the row has to be down below the tier
     whether or not the script ran. Scoped to rz-nav it stayed in normal flow
     with JS off, which gave a 177px three-row header AND a redundant "Menu"
     link beside the links it was offering to go and find. Only the drawer,
     which needs JS by definition, ever opens it. */
  .nav__menu { display: none; }
  html.rz-nav .nav__menu.is-open {
    display: flex; flex-direction: column; align-items: stretch; gap: 0;
    position: absolute; left: 0; right: 0; top: 100%;
    background: var(--rz-paper); border-bottom: var(--rule);
    padding: var(--rz-space-sm) var(--gut) var(--rz-space-md);
    /* vh first, dvh second — Safari < 15.4 ignores the dvh line and keeps the
       vh one. Do not let a formatter sort these. */
    max-height: calc(100vh - 70px);
    max-height: calc(100dvh - 70px);
    overflow-y: auto;
  }
  html.rz-nav .nav__menu.is-open a {
    display: flex; align-items: center; min-height: 52px;
    border-bottom: var(--rule); font-size: 18px;
  }
  html.rz-nav .nav__menu.is-open a.btn {
    margin-top: var(--rz-space-md); border-bottom: 0; justify-content: center;
  }
  /* The sliding underline is for a horizontal row and reads as a glitch on a
     stacked list. */
  html.rz-nav .nav__menu.is-open a::after { display: none; }
}

/* Above the tier the row is the nav and neither control is shown.

   Every selector here is qualified with .nav on purpose. The touch-target floor
   above sets `.nav a { display: inline-flex }`, which is (0,1,1) — a bare
   `.nav__nojs { display: none }` is (0,1,0) and LOSES to it however late it
   comes in the file. With JS on nothing showed, because `html.rz-nav .nav__nojs`
   is (0,2,0) and wins; with JS off the no-JS link sat next to the full row it
   was offering to go and find. */
@media (min-width: 720px) {
  .nav .nav__menu { display: flex; align-items: center; gap: clamp(14px, 2.2vw, 30px); }
  .nav .nav__toggle, .nav .nav__nojs,
  html.rz-nav .nav .nav__toggle, html.rz-nav .nav .nav__nojs { display: none; }
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  font-family: var(--rz-font-text); font-size: 17px; font-weight: 500;
  padding: 13px 26px; border-radius: var(--rz-radius-pill);
  border: 1px solid transparent; cursor: pointer; text-decoration: none;
  transition: background-color .25s var(--ease), color .25s var(--ease),
              border-color .25s var(--ease), transform .25s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn--primary { background: var(--rz-oxblood); color: #fff; }
.btn--primary:hover { background: color-mix(in srgb, var(--rz-oxblood) 86%, #000); }
.btn--ghost { border-color: var(--rz-aluminium-light); color: var(--rz-ink); background: transparent; }
.btn--ghost:hover { border-color: var(--rz-ink); }
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--rz-aluminium-light); color: color-mix(in srgb, var(--rz-ink) 45%, transparent);
  border-color: transparent; cursor: not-allowed; pointer-events: none;
}
.btn--lg { padding: 17px 34px; font-size: 18px; }
/* The hero pair used to be two buttons in one <p> separated by an inline
   margin-left, and this rule trimmed --lg below 460px to keep them on one line.
   Both halves of that were wrong: the pair still wrapped on a narrow phone, and
   when it did the second button carried the inline margin down as a stray 10px
   indent. They are a real .linkrow now (see below) and there are no inline
   margins left in any of the nine places. The trim stays because a 34px
   horizontal pad on a 320px screen is still too wide, not because it is holding
   a layout together. */
@media (max-width: 460px) { .btn--lg { padding: 15px 24px; font-size: 17px; } }

/* A row of secondary links that has to wrap. The two hero buttons above sit in
   one <p> separated by a hardcoded margin-left, which is fine for two and falls
   apart at four — the comment above records that even two --lg buttons stopped
   fitting a 390px line. So this is a real flex row with a gap instead of an
   inline margin, and the buttons in it are deliberately NOT --lg: they are
   places to go and verify us, not the thing the page is asking you to do. */
.linkrow {
  display: flex; flex-wrap: wrap; gap: var(--rz-space-sm);
  margin-top: var(--rz-space-md);
}
.linkrow .btn { font-size: 16px; padding: 10px 20px; }

/* The hero pair is a .linkrow too, but it is the thing the page is asking you
   to do, so it keeps the --lg scale. Without this the rule above would shrink
   seven hero buttons to the size of a "verify us" link — build-listings.mjs
   emits a plain .linkrow on index.html and the two would collide. */
.linkrow--lead .btn { font-size: 18px; padding: 17px 34px; }
@media (max-width: 460px) { .linkrow--lead .btn { padding: 15px 24px; font-size: 17px; } }

/* ---------- Touch targets ----------

   A 44px floor on anything you can tap, and it is UNCONDITIONAL — no
   @media (pointer: coarse). Three reasons, in order of weight:

     1. Puppeteer does not report `pointer: coarse` unless the page is launched
        with hasTouch, so a pointer-gated rule is a rule tools/test-mobile.mjs
        silently skips. A floor the harness cannot see is not a floor.
     2. WCAG 2.5.8 (24x24, AA) and 2.5.5 (44x44, AAA) are not pointer-
        conditional, and every control below was under 2.5.8 on a DESKTOP too.
     3. One code path to read.

   The one place a pointer query IS right is js/scrub.js deciding whether a
   machine should download 2MB of frames. That is a capability question, which
   is what the query is for. This is a human-finger question, and every human
   has the same finger. */

.btn { min-height: 44px; }

/* No padding-inline here on purpose: the underline at .nav a::after is
   left: 0 and animates width 0 -> 100%, so horizontal padding would make it
   overshoot the word on both sides. Height is what was missing (~34px), and
   height is all this adds. */
.nav a { min-height: 44px; display: inline-flex; align-items: center; }

/* 26px tall on a 9px pitch. The gap goes to 0 and the rows grow instead, so
   the link boxes touch without overlapping — pitch 35px -> 44px. Sliding a
   negative-inset pseudo-element under each link would have kept the airy look,
   but at a 9px gap the hit areas would overlap, and a tap landing on the link
   above the one you aimed at is a worse bug than the one being fixed.

   This visibly lengthens the footer on all twelve pages. That is the cost and
   it is intended. */
.footer ul { gap: 0; }
.footer a { display: flex; align-items: center; min-height: 44px; }
/* .footer__base is phone, email and the legal line — links inline in running
   prose, exempt under WCAG 2.5.8, and boxing them to 44px would look broken. */
.footer__base a { display: inline; min-height: 0; }

/* A tap leaves :hover stuck on a touch device, so the underline and the colour
   shift both hang around after the finger has gone. Gate them on a pointer
   that can actually hover. */
@media (hover: none) {
  .nav a:hover::after { width: 0; }
  .nav a[aria-current="page"]::after { width: 100%; }
}

/* ---------- The bar scrub ---------- */

/* Frames carry alpha (tools/key-frames.mjs), so the bar composites straight
   onto the paper ground. It has to be alpha rather than a matching stage
   colour: the generated backgrounds differ per metal and platinum's drifts
   218–229 across the rotation, which would show as a pulsing rectangle. */
.scrub {
  position: relative;
  /* Opaque, and still load-bearing after the backdrop was removed: the frames
     carry alpha, so the bar composites onto whatever is behind it and this is
     what that is. It has to be the paper token exactly — see the note above
     about platinum's generated background drifting 218–229 across the
     rotation. */
  background: var(--rz-paper);
}
.scrub__sticky {
  position: sticky; top: 0;
  /* vh first, dvh second: Safari < 15.4 ignores the dvh line and keeps the vh
     one. Order is load-bearing — do not let a formatter sort these. iOS counts
     the collapsing toolbar in vh, so 100vh overshoots the glass by ~60px. */
  height: 100vh;
  height: 100dvh;
  display: grid; place-items: center; overflow: hidden;
}
.scrub__canvas { width: min(52vw, 460px); height: min(52vw, 460px); display: block; }
/* An author `display` on the canvas/img outranks the UA's [hidden] rule, so the
   static fallback rendered *underneath* the live canvas — two bars, stacked.
   Author-level and !important, or `hidden` silently does nothing. */
[hidden] { display: none !important; }
.scrub__glow {
  position: absolute; inset: 0; margin: auto;
  width: 46%; height: 46%; border-radius: 50%;
  background: radial-gradient(closest-side, var(--tint-ox), transparent 70%);
  filter: blur(26px); z-index: 0; pointer-events: none;
}
.scrub__panel {
  position: absolute; top: 50%; transform: translateY(-50%) translateX(-22px);
  width: min(34vw, 380px); opacity: 0; z-index: 2;
  transition: opacity .55s var(--ease), transform .6s var(--ease);
}
.scrub__panel.is-on { opacity: 1; transform: translateY(-50%) translateX(0); }
.scrub__panel--left  { left: max(20px, 4vw); }
.scrub__panel--right { right: max(20px, 4vw); transform: translateY(-50%) translateX(22px); }
.scrub__panel--right.is-on { transform: translateY(-50%) translateX(0); }

.scrub__ticks { position: absolute; left: 50%; bottom: max(22px, 4vh); transform: translateX(-50%); z-index: 3; display: flex; gap: 7px; }
.scrub__ticks span { width: 26px; height: 3px; border-radius: 2px; background: var(--rz-aluminium-light); overflow: hidden; }
.scrub__ticks span::after { content: ""; display: block; height: 100%; width: 0; background: var(--rz-oxblood); transition: width .4s ease; }
.scrub__ticks span.is-done::after { width: 100%; }
.scrub__ticks span.is-active::after { width: 50%; }

/* Stacked fallback: reduced motion, no canvas, low-power, or narrow. The layout
   has to be complete without a single frame having loaded. */
.scrub.is-static .scrub__sticky { position: relative; height: auto; padding-block: var(--rz-space-xl); }
.scrub.is-static .scrub__panel {
  position: relative; top: auto; left: auto; right: auto;
  transform: none; opacity: 1; width: 100%; max-width: 560px;
  margin: var(--rz-space-lg) auto 0;
}
.scrub.is-static .scrub__ticks { display: none; }

@media (max-width: 900px) {
  .scrub__panel { position: relative; top: auto; left: auto; right: auto; transform: none; width: min(92vw, 460px); margin-inline: auto; }
  .scrub__panel.is-on { transform: none; }
}

/* ---------- Cards, rails, tables ---------- */

.card {
  background: #fff; border: var(--rule); border-radius: var(--rz-radius-md);
  padding: clamp(20px, 3vw, 30px);
}
.card--flush { background: transparent; border: 0; padding: 0; }

.grid { display: grid; gap: var(--rz-space-lg); }
@media (min-width: 720px) { .grid--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .grid--3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 900px) { .grid--4 { grid-template-columns: repeat(4, 1fr); } }
.grid--split { gap: clamp(30px, 5vw, 72px); }
@media (min-width: 900px) { .grid--split { grid-template-columns: 1fr 1fr; align-items: center; } }

/* Numbered steps. The numeral is mono and oxblood — the accent for this
   surface, spent once. */
.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.steps > li { counter-increment: step; padding: var(--rz-space-md) 0; border-top: var(--rule); display: grid; grid-template-columns: 2.8rem 1fr; gap: var(--rz-space-md); }
.steps > li::before {
  content: counter(step, upper-roman);
  font-family: var(--rz-font-data); font-size: 14px; letter-spacing: .08em;
  color: var(--rz-oxblood); padding-top: .35em;
}
.steps h3 { margin-bottom: 6px; font-size: var(--rz-size-subhead); font-family: var(--rz-font-text); font-weight: 600; letter-spacing: 0; }
.steps p { margin: 0; font-size: 17px; color: var(--rz-aluminium); }

/* Grade chips — what we accept. Not prices. */
.grades { display: flex; flex-wrap: wrap; gap: 8px; padding: 0; margin: 0; list-style: none; }
.grades li {
  font-family: var(--rz-font-data); font-size: 15px; letter-spacing: .04em;
  border: var(--rule); border-radius: var(--rz-radius-sm);
  /* 11px, not 8: not interactive, but it sits in a row beside things that are,
     and a 38px chip next to a 44px chip reads as a rendering fault. */
  padding: 11px 14px; background: #fff;
}

.deflist { margin: 0; }
.deflist > div { display: grid; gap: 4px; padding: var(--rz-space-md) 0; border-top: var(--rule); }
@media (min-width: 700px) { .deflist > div { grid-template-columns: 17rem 1fr; gap: var(--rz-space-lg); } }
.deflist dt { font-weight: 600; font-size: 17px; }
.deflist dd { margin: 0; color: var(--rz-aluminium); font-size: 17px; }

/* ---------- Notices ---------- */

.notice {
  border-left: 2px solid var(--rz-oxblood);
  background: var(--tint-ox);
  padding: var(--rz-space-md) var(--rz-space-lg);
  border-radius: 0 var(--rz-radius-sm) var(--rz-radius-sm) 0;
  font-size: 17px;
}
.notice p:last-child { margin-bottom: 0; }
.notice--quiet { border-left-color: var(--rz-aluminium); background: var(--tint-ink); }

/* ---------- Footer ---------- */

.footer { border-top: var(--rule); padding-block: var(--rz-space-xl) var(--rz-space-lg); font-size: 16px; }
.footer__cols { display: grid; gap: var(--rz-space-lg); }
@media (min-width: 760px) { .footer__cols { grid-template-columns: 1.6fr 1fr 1fr; } }
.footer a { text-decoration: none; color: var(--rz-aluminium); }
.footer a:hover { color: var(--rz-ink); }
.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.footer__base { margin-top: var(--rz-space-xl); padding-top: var(--rz-space-md); border-top: var(--rule); color: var(--rz-aluminium); display: flex; flex-wrap: wrap; gap: var(--rz-space-md); justify-content: space-between; }
.footer__logo img { height: 26px; width: auto; margin-bottom: var(--rz-space-sm); }

/* ---------- Reveal ----------

   Entrances are opt-IN, not opt-out. Nothing below applies until two things
   are true at once:

     1. `html.rz-ready` is present — js/reveal.js adds it once document.fonts
        has settled (or after a 1200ms timeout, whichever comes first). Before
        that class lands every element is at its normal, visible, final state.
        No script, no class, no rule, and the page is simply complete. A
        webfont swapping in mid-transition is what makes an entrance read as a
        flash of the wrong typeface, so the gate is on fonts, not on load.

     2. The user has not asked for less motion. The whole system is nested in
        `prefers-reduced-motion: no-preference`, so under `reduce` these rules
        do not exist at all — there is no `animation: none` override to lose a
        specificity fight with, because there is nothing to override.

   Only three properties transition — opacity, transform, clip-path — all of
   them compositor-friendly. No layout property is ever animated. */

@media (prefers-reduced-motion: no-preference) {
  html.rz-ready [data-reveal] > *,
  html.rz-ready .reveal {
    opacity: 0;
    transform: translateY(22px);
    transition: opacity .62s var(--ease-house),
                transform .62s var(--ease-house),
                clip-path .62s var(--ease-house);
  }

  /* Five variants. A long page that repeats one effect reads as a single
     tic; giving each kind of block its own direction keeps the scroll alive
     without any of them being loud. */
  html.rz-ready [data-reveal="up"]    > * { transform: translateY(22px); }
  html.rz-ready [data-reveal="left"]  > * { transform: translateX(-26px); }
  html.rz-ready [data-reveal="right"] > * { transform: translateX(26px); }

  /* The horizontal pair has to clip its own overflow, and this is not
     cosmetic. A full-width block offset 26px to the right widens the document
     by 26px, and a phone at 320px gets a horizontal scrollbar for as long as
     the block has not yet been revealed. `clip`, not `hidden`: hidden would
     make these scroll containers and break `position: sticky` on the masthead
     from anywhere inside them. `clip` on one axis leaves the other visible,
     which `hidden` does not. The clip margin is there so a focus ring on a
     control at the container's edge is not shaved off. */
  html.rz-ready [data-reveal="left"],
  html.rz-ready [data-reveal="right"] {
    overflow-x: clip;
    overflow-clip-margin: 8px;
  }
  html.rz-ready [data-reveal="scale"] > * { transform: scale(.975); transform-origin: left center; }
  html.rz-ready [data-reveal="clip"]  > * { transform: none; clip-path: inset(0 100% 0 0); }

  html.rz-ready [data-reveal].in > *,
  html.rz-ready .reveal.in,
  html.rz-ready .reveal.is-in {
    opacity: 1; transform: none; clip-path: inset(0 0 0 0);
  }

  /* 90ms apart, capped at six. Past the sixth child the eye has stopped
     counting and a linear ramp only delays the tail — everything from the
     seventh on lands together at half a second. */
  html.rz-ready [data-reveal].in > *:nth-child(1) { transition-delay: 0s; }
  html.rz-ready [data-reveal].in > *:nth-child(2) { transition-delay: .09s; }
  html.rz-ready [data-reveal].in > *:nth-child(3) { transition-delay: .18s; }
  html.rz-ready [data-reveal].in > *:nth-child(4) { transition-delay: .27s; }
  html.rz-ready [data-reveal].in > *:nth-child(5) { transition-delay: .36s; }
  html.rz-ready [data-reveal].in > *:nth-child(6) { transition-delay: .45s; }
  html.rz-ready [data-reveal].in > *:nth-child(n+7) { transition-delay: .5s; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
}

/* ---------- The paper ground on text blocks ----------

   These blocks take var(--rz-paper) explicitly rather than inheriting it.

   The reason was the falling-grain backdrop: small --rz-aluminium secondary
   text clears 4.5:1 on bare paper with little to spare, and could not be
   relied on to clear it over a grain that happened to land behind a word, so
   every block carrying such text was given an opaque ground and the field was
   seen around them. The backdrop was removed 2026-08-31 and that job is gone.

   The declarations stay, because one of them is quietly doing a second job:
   .card sets `background: #fff` up in "Cards, rails, tables", and this later
   rule of equal specificity overrides it to paper. Every card on the site is
   paper-coloured because of this line, not white. Deleting the rule as dead
   backdrop scaffolding would repaint them, which is a design change and not a
   cleanup. If cards should be white, change them there and drop this rule in
   the same commit, deliberately.

   The list is the nine it always was. .label, .muted, figcaption and .diagram
   were added while the backdrop existed and are removed with it. */
.card,
.benefit,
.flow figure,
.steps,
.deflist,
.notice,
.buy figcaption,
.bench figcaption,
.footer {
  background-color: var(--rz-paper);
}

/* The blocks above are inline-sized to their text. Give them a little room so
   the paper reads as a deliberate surface rather than a tight cut-out. */
.benefit, .steps, .deflist { padding-inline: var(--rz-space-md); }
.benefit { padding-block: var(--rz-space-md) var(--rz-space-lg); }
.buy figcaption, .bench figcaption { padding: 4px 8px; border-radius: var(--rz-radius-sm); }

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

/* ---------- Benefit grid ---------- */

.benefit { padding-top: var(--rz-space-md); border-top: 2px solid var(--rz-oxblood); }
.benefit h3 {
  font-family: var(--rz-font-text); font-weight: 600; font-size: 19px;
  letter-spacing: 0; margin: 0 0 8px;
}
.benefit p { font-size: 17px; color: var(--rz-aluminium); margin: 0; text-wrap: pretty; }

/* ---------- What we buy ---------- */

/* minmax(0,1fr), not 1fr: a bare 1fr track floors at the content's min-content
   width, and these images are intrinsically 1100px wide. */
.buy { display: grid; gap: var(--rz-space-md); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 640px) { .buy { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 980px) { .buy { grid-template-columns: repeat(5, minmax(0, 1fr)); } }

/* The five-across grid is for the homepage "what we buy" row. The per-page
   rows carry one or two images, and dropping those into a five-column track
   left them stranded at 198px in the first two columns with 60% of the row
   empty. These size to their content instead. */
.buy--few { grid-template-columns: repeat(auto-fit, minmax(220px, 320px)); justify-content: start; gap: var(--rz-space-lg); }
@media (min-width: 640px) { .buy--few { grid-template-columns: repeat(auto-fit, minmax(260px, 340px)); } }

.buy figure { margin: 0; text-align: center; }
/* Photos are alpha-keyed (tools/prep-photos.mjs) so they sit straight on the
   paper ground. A white card behind them would reintroduce the rectangle the
   keying exists to remove. */
.buy img {
  /* height:auto is load-bearing. The width/height attributes are on the tag to
     reserve layout space, and a specified height beats aspect-ratio — without
     this every image rendered at its intrinsic 1100px instead of filling its
     ~220px cell. */
  width: 100%; height: auto; aspect-ratio: 1; object-fit: contain; display: block;
  mix-blend-mode: multiply;
}
.buy figcaption {
  font-family: var(--rz-font-data); font-size: 13px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--rz-aluminium); margin-top: 7px;
}

/* ---------- Process strip with imagery ---------- */

.flow { display: grid; gap: var(--rz-space-lg); }
@media (min-width: 760px) { .flow { grid-template-columns: repeat(3, 1fr); } }
.flow figure { margin: 0; }
.flow img { width: 100%; height: auto; max-width: 220px; aspect-ratio: 1; object-fit: contain; display: block; margin-inline: auto; mix-blend-mode: multiply; }
.flow h3 { font-family: var(--rz-font-text); font-weight: 600; font-size: 19px; letter-spacing: 0; margin: var(--rz-space-md) 0 7px; }
.flow p { font-size: 17px; color: var(--rz-aluminium); margin: 0; text-wrap: pretty; }

/* ---------- The bench (real photograph) ---------- */

.bench img {
  width: 100%; height: auto; border-radius: var(--rz-radius-md); display: block;
  border: var(--rule);
}
.bench figcaption { font-size: 15px; color: var(--rz-aluminium); margin-top: 11px; text-wrap: pretty; }

/* ---------- Stat row ---------- */

.stats { display: grid; gap: var(--rz-space-lg); grid-template-columns: repeat(2, 1fr); }
@media (min-width: 760px) { .stats { grid-template-columns: repeat(4, 1fr); } }
.stats dt {
  font-family: var(--rz-font-data); font-variant-numeric: tabular-nums;
  font-size: clamp(27px, 23.53px + 0.891vw, 36px); letter-spacing: -.02em; color: var(--rz-ink);
}
.stats dd {
  margin: 7px 0 0; font-size: 14px; color: var(--rz-aluminium);
  letter-spacing: .04em; text-transform: uppercase; font-family: var(--rz-font-data);
}

/* ---------- Diagrams ----------
   Hand-authored inline SVG. There was no SVG styling and no <svg> anywhere on
   this site before, so this is the whole component.

   NEVER nest a .diagram inside .buy or .flow: both set
   `mix-blend-mode: multiply` on their images, which is correct for the
   alpha-keyed photographs sitting on the paper ground and would eat an inline
   SVG's whites. This container deliberately sets none. */
.diagram { margin: 0; }
.diagram svg {
  display: block;
  width: 100%;
  max-width: 760px;
  height: auto;                 /* with viewBox, this is what keeps it responsive */
  margin-inline: auto;
  overflow: visible;
}
.diagram figcaption {
  font-family: var(--rz-font-data);
  font-size: 12px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--rz-aluminium);
  text-align: center;
  margin-top: var(--rz-space-sm);
}

/* Stroke and fill conventions, so twelve diagrams drawn separately read as one
   set. Bind to tokens — never a literal hex inside an SVG. */
.diagram .d-line   { stroke: var(--rz-aluminium-light); stroke-width: 1.5; fill: none; }
.diagram .d-rule   { stroke: var(--rz-aluminium); stroke-width: 1; fill: none; }
.diagram .d-node   { stroke: var(--rz-ink); stroke-width: 1.5; fill: var(--rz-paper); }
.diagram .d-accent { stroke: var(--rz-oxblood); stroke-width: 2; fill: none; }
.diagram .d-fill   { fill: var(--rz-oxblood); }
.diagram .d-dim    { fill: var(--rz-aluminium-light); stroke: none; }
.diagram .d-label  { font-family: var(--rz-font-text); font-size: 13px; fill: var(--rz-ink); }
.diagram .d-sub    { font-family: var(--rz-font-text); font-size: 12px; fill: var(--rz-aluminium); }
.diagram .d-num    { font-family: var(--rz-font-data); font-size: 12px; fill: var(--rz-oxblood); letter-spacing: .06em; }
.diagram .d-strike { stroke: var(--rz-aluminium); stroke-width: 1.5; }

/* Below the compact tier the drawing steps aside and its sentence takes over.
   The old rule here dropped .d-label from 13px to 12px, which at a 0.46x scale
   is a change of about one real pixel — it looked like a mobile concession and
   did nothing. tools/build-diagrams.mjs writes the paragraph; the <title> it
   comes from is the same sentence, so exactly ONE of the two is in the
   accessibility tree at any width: display:none removes an element from it, so
   above the tier the SVG title carries the meaning and below it the paragraph
   does. No aria-hidden, and nothing is announced twice.

   720px, not 560: at 768 an iPad renders these at 0.91x and 11.8px labels,
   which is legible, so iPad portrait sits just above the line and keeps the
   drawings. */
.diagram__say { display: none; }
@media (max-width: 719.98px) {
  .diagram svg { display: none; }
  .diagram__say {
    display: block; margin: 0;
    font-size: 17px; line-height: 1.6; color: var(--rz-ink);
    text-wrap: pretty;
  }
  .diagram {
    border-left: 2px solid var(--rz-aluminium-light);
    padding-left: var(--rz-space-md);
  }
  .diagram figcaption { text-align: left; margin-top: var(--rz-space-xs); }
}

/* ---------- Four-across process strip ----------
   .flow is three columns; the homepage strip gains a fourth node so the
   graphic stops contradicting its own heading, which says "Four steps". */
@media (min-width: 760px) {
  .flow--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1040px) {
  .flow--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.flow--4 img { max-width: 170px; }

/* ---------- Graphics pass (2026-10-07) ----------
   Layout for placed photographs and the quote funnel's media column. Everything
   here sits on the existing .buy figure (alpha-keyed WebP on paper, multiply
   blend). NEVER put a .diagram inside .buy; a diagram goes beside it in .split.

   Placement vocabulary:
     .buy.buy--few    row of 1-3 images (above)
     .buy.buy--one    one image, max 300px
     .buy.buy--pair   exactly two images, side by side at every width
     .buy.buy--thumb  one small image (<=120px) inside a list item, benefit,
                      Q&A row or scrub panel; no figcaption
     .split           text/diagram and media in two columns from 760px, stacked
                      below it (media after the text). .split--flip puts the
                      media first on wide screens only.
     .funnel--media   quote.html two-column funnel (see below)            */

.buy--one  { grid-template-columns: minmax(0, 300px); justify-content: start; }
.buy--pair { grid-template-columns: repeat(2, minmax(0, 240px)); justify-content: start; gap: var(--rz-space-md); }
@media (max-width: 479.98px) { .buy--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--rz-space-sm); } }
.buy--thumb { display: block; width: min(100%, 120px); }
.buy--thumb figcaption { display: none; }
.scrub__panel .buy--thumb { width: 140px; margin-top: var(--rz-space-md); }
.scrub.is-static .scrub__panel .buy--thumb { margin-inline: 0; }

.split { display: grid; gap: var(--rz-space-lg); align-items: center; margin-top: var(--rz-space-lg); }
.split > * { min-width: 0; }
.split > .buy, .split > .diagram { margin-top: 0; }
@media (min-width: 760px) {
  .split { grid-template-columns: minmax(0, 1fr) minmax(0, 300px); gap: var(--rz-space-xl); }
  .split--flip > :first-child { order: 2; }
  .split--wide { grid-template-columns: minmax(0, 300px) minmax(0, 1fr); }
}
/* A deflist has no room beside media (its 17rem term column crushed the text to ~60px): full width, media below. */
.split:has(> .deflist) { grid-template-columns: minmax(0, 1fr); align-items: start; }

/* Image beside a list row: a steps item, a benefit, a Q&A. */
.with-thumb { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: var(--rz-space-md); align-items: start; }
.with-thumb > .buy--thumb { width: 88px; }
@media (min-width: 760px) {
  .with-thumb { grid-template-columns: 120px minmax(0, 1fr); }
  .with-thumb > .buy--thumb { width: 120px; }
}
.benefit .buy--thumb { margin-bottom: var(--rz-space-sm); }

/* The quote funnel gains a media column. Nothing goes inside a fieldset, a
   legend, label.opt or .quote-card: the aside is a SIBLING of the fieldset
   (or of the section's own content).

   >= 901px: .funnel--media is 640px of form + 260px of media. Each .funnel__row
   (a fieldset and its aside) is a two-column grid; the aside sits in column 2
   at the top of the fieldset it belongs to. Sections #result, #timeSection and
   #confirm do the same with their own children.
   <  901px: the aside is a two-up strip (~120px each) BEFORE the fieldset.
   Specificity is deliberate: css/quote.css loads after this file and sets
   .funnel { max-width: 640px }. */
.funnel__media {
  display: grid; grid-template-columns: repeat(2, 120px); gap: var(--rz-space-sm);
  justify-content: start; margin: 0 0 var(--rz-space-md);
}
.funnel__media .buy { display: block; }
.funnel__media .buy figcaption { display: none; }
.funnel__media:has(> :only-child) { grid-template-columns: 120px; }
@media (min-width: 901px) {
  main .funnel.funnel--media { max-width: 960px; }
  .funnel--media > .lede, .funnel--media > h1, .funnel--media > .form-error { max-width: 640px; }
  .funnel--media .funnel__row,
  .funnel--media > .block[id] {
    display: grid; grid-template-columns: minmax(0, 640px) minmax(0, 260px);
    column-gap: var(--rz-space-xl); align-items: start;
  }
  .funnel--media .funnel__row > fieldset { grid-column: 1; grid-row: 1; }
  .funnel--media .funnel__row > .funnel__media,
  .funnel--media > .block[id] > .funnel__media {
    grid-column: 2; grid-row: 1; margin: 0; position: sticky; top: 96px;
    grid-template-columns: repeat(2, minmax(0, 1fr)); align-self: start;
  }
  .funnel--media .funnel__media:has(> :only-child) { grid-template-columns: minmax(0, 200px); }
  .funnel--media > .block[id] > :not(.funnel__media):not(.diagram) { grid-column: 1; }
  .funnel--media > .block[id] > .diagram { grid-column: 1 / -1; }
  .funnel--media .funnel__media .buy { width: 100%; }
}
.funnel--media .diagram { margin-top: var(--rz-space-lg); }
@media (max-width: 479.98px) { .card > .buy--thumb { width: 84px; } }

/* .steps > li is already a two-track grid (counter, text); a thumbnail adds a third. */
.steps > li.with-thumb { grid-template-columns: 2.8rem 72px minmax(0, 1fr); }
.steps > li.with-thumb > .buy--thumb { width: 72px; }
@media (min-width: 760px) {
  .steps > li.with-thumb { grid-template-columns: 2.8rem 120px minmax(0, 1fr); }
  .steps > li.with-thumb > .buy--thumb { width: 120px; }
}
@media (max-width: 479.98px) { .buy--one { grid-template-columns: minmax(0, 240px); } }
