/*
 * ROOTS APAC / KOON APAC — DATUM, Version 2
 * Design tokens. Single source of truth for values.
 *
 * Governing rule: four brand colours exist and nothing else. The V1
 * monochrome-only rule is superseded by the client's updated branding
 * (01_Project_Inputs/04_Updated_Branding/). Greys are gone; where V1 used
 * an ink ramp, V2 uses alpha variants of the four brand colours. Any other
 * colour is a build failure — tools/qa.mjs enforces it.
 *
 * Photography remains the only source of any other colour, exactly as V1.
 *
 * Derived from the approved discovery questionnaire and the updated brand:
 *   palette: Forest Green, Sage Mist, Signal White, ROOTS Charcoal
 *   Q24 typeface: Roboto              Q27 iconography: none
 *   signature device: Infinity Ring   Q25 photography: case-study driven
 */

@font-face {
  font-family: "Roboto";
  src: url("/assets/fonts/Roboto-Variable.ttf") format("truetype");
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
}

:root {
  /* ---------------------------------------------------------------
   * 1. Palette — the only four colours that exist
   *
   *   --green     Forest Green      #395542   Pantone 7736 C
   *   --sage      Sage Mist         #bcc8b8   Pantone 5595 C
   *   --white     Signal White      #ffffff   —
   *   --charcoal  ROOTS Charcoal    #1c211d   Pantone Black 3 C
   *
   * Contrast matrix (WCAG 2.1 relative luminance, computed by the
   * architect — do not re-derive):
   *
   *   pair                 ratio    verdict
   *   charcoal on white    16.35:1  body text, light context
   *   charcoal on sage      9.41:1  body text on sage panels
   *   sage on charcoal      9.41:1  footer text
   *   white on green        8.15:1  body text on green sections
   *   white on charcoal    16.35:1  body text, dark context
   *   green on white        8.15:1  accent text, links
   *   sage on green         4.69:1  AA normal text — thin margin;
   *                                 prefer white on green at small sizes
   *   green on sage         4.69:1  AA — large text preferred
   *   sage on white         1.74:1  decorative only, never text
   *   green on charcoal     2.01:1  decorative only, never text
   *
   * House rule taken from that matrix: the 4.69:1 pair (green/sage in
   * either direction) carries NO text anywhere on this site. It is used
   * for hairlines, ring tracks and grounds only. Every text pair in use
   * is 4.8:1 or better.
   * ------------------------------------------------------------- */
  --green: #395542;
  --sage: #bcc8b8;
  --white: #ffffff;
  --charcoal: #1c211d;

  /* ---------------------------------------------------------------
   * 2. Alpha variants — what replaces the V1 ink ramp
   *
   * Every value below is one of the four brand colours at an alpha, so
   * the palette guard passes and the composite is always a blend of two
   * brand colours. Suffix = alpha percentage. Composited ratios are
   * given against the ground each variant is actually used on.
   *
   *   charcoal alphas — used over white and over sage
   *     72%  secondary text   6.46:1 on white   4.80:1 on sage
   *     65%  disabled / placeholder   5.11:1 on white
   *     28%  strong hairline on sage
   *     18%  strong hairline on white / hairline on sage
   *     12%  hairline on white
   *
   *   white alphas — used over green and over charcoal
   *     78%  secondary text   5.77:1 on green
   *     70%  disabled         5.01:1 on green
   *     24%  strong hairline on green and charcoal
   *     14%  hairline on green and charcoal
   *      8%  raised ground (mounts, cells) on green and charcoal
   *
   *   sage alphas — used over charcoal
   *     78%  secondary text   6.26:1 on charcoal
   *     68%  disabled         5.10:1 on charcoal
   *
   * Hairlines sit far below 3:1 by design. They are decorative dividers,
   * not UI components or meaningful graphics, so WCAG 1.4.11 does not
   * apply — no information is carried by a rule alone. Any rule that
   * later becomes the sole indicator of state must be re-checked.
   * ------------------------------------------------------------- */
  --charcoal-72: rgb(28 33 29 / 0.72);
  --charcoal-65: rgb(28 33 29 / 0.65);
  --charcoal-28: rgb(28 33 29 / 0.28);
  --charcoal-18: rgb(28 33 29 / 0.18);
  --charcoal-12: rgb(28 33 29 / 0.12);

  --white-78: rgb(255 255 255 / 0.78);
  --white-70: rgb(255 255 255 / 0.7);
  --white-24: rgb(255 255 255 / 0.24);
  --white-14: rgb(255 255 255 / 0.14);
  --white-08: rgb(255 255 255 / 0.08);

  --sage-78: rgb(188 200 184 / 0.78);
  --sage-68: rgb(188 200 184 / 0.68);

  /* ---------------------------------------------------------------
   * 3. Semantic roles
   * Set per ground, not globally. A page alternates grounds, so the
   * ground classes at the foot of this file carry the roles rather than
   * :root alone. :root is the paper default — white.
   *
   * Ground rhythm: white is the paper, sage is the panel, green is the
   * statement, charcoal is the floor.
   * ------------------------------------------------------------- */
  --bg: var(--white);
  --bg-raised: var(--white);
  --fg: var(--charcoal);
  --fg-muted: var(--charcoal-72);
  --fg-disabled: var(--charcoal-65);
  --accent: var(--green);
  --focus: var(--charcoal);
  --rule: var(--charcoal-12);
  --rule-strong: var(--charcoal-18);

  /* ---------------------------------------------------------------
   * 4. Type
   * Roboto only. Weights 400 / 500 / 700. Light is never used at
   * display size — it is the single clearest tell of the old direction.
   * Headings cap at 56px. Photography carries scale, not type.
   *
   * The updated wordmark artwork uses its own geometric face inside the
   * SVG paths. That is artwork, not a font choice; it is not chased here.
   * ------------------------------------------------------------- */
  --font: "Roboto", Arial, Helvetica, sans-serif;

  --type-title: clamp(2.125rem, 1.55rem + 2.4vw, 3.5rem); /* 34 → 56 */
  --type-section: clamp(1.75rem, 1.3rem + 1.9vw, 2.75rem); /* 28 → 44 */
  --type-sub: clamp(1.375rem, 1.15rem + 0.95vw, 1.75rem); /* 22 → 28 */
  --type-lead: clamp(1.125rem, 1.05rem + 0.3vw, 1.25rem); /* 18 → 20 */
  --type-body: 1.0625rem; /* 17 */
  --type-small: 0.9375rem; /* 15 */
  --type-label: 0.75rem; /* 12 */
  --type-meta: 0.6875rem; /* 11 */

  --lh-title: 1.05;
  --lh-section: 1.08;
  --lh-sub: 1.15;
  --lh-body: 1.6;
  --lh-label: 1.2;

  --tr-title: -0.02em;
  --tr-section: -0.015em;
  --tr-sub: -0.01em;
  --tr-body: 0;
  --tr-label: 0.14em;
  --tr-meta: 0.16em;

  --wt-regular: 400;
  --wt-medium: 500;
  --wt-bold: 700;

  /* ---------------------------------------------------------------
   * 5. Grid and baseline
   * 12 columns. Everything snaps to an 8px baseline. The grid is now
   * invisible: the client asked for the vertical column hairlines to go
   * (2026-08-19), so the 12 columns survive as the measuring system that
   * places every component while nothing draws them. Horizontal rules —
   * fact tables, footer, logo cells, section dividers — are unaffected.
   * Component spans are unchanged; with no rules to land on, the old
   * "spans must be multiples of 3" discipline no longer applies.
   * ------------------------------------------------------------- */
  --cols: 12;
  --gutter: 1.5rem; /* 24 */
  --margin: clamp(1.25rem, 5.5vw, 5rem); /* 20 → 80 */
  --container: 90rem; /* 1440 */
  --measure: 34rem; /* ~68 characters at 17px */

  --base: 0.5rem; /* 8 — the datum */
  --sp-1: 0.5rem;
  --sp-2: 1rem;
  --sp-3: 1.5rem;
  --sp-4: 2rem;
  --sp-6: 3rem;
  --sp-8: 4rem;
  --sp-12: 6rem;
  --sp-16: 8rem;
  --sp-20: 10rem;

  --band: clamp(4rem, 8vw, 8rem); /* vertical band padding */

  --hair: 1px;

  /* ---------------------------------------------------------------
   * 6. The Infinity Ring system  (V1 --oo-* renamed to --ring-*)
   *
   * Source artwork, measured directly:
   *   01_Project_Inputs/04_Updated_Branding/2. Infinity Ring/2. SVG/
   *   viewBox 0 0 1828.51 1080
   *     outer radius        540
   *     inner radius        412.1     (stroke 127.9)
   *     centres             (540, 540) and (1288.51, 540)
   *     centre separation   748.51
   *
   * Proportions against the V1 OO symbol:
   *     separation / outer   1.38613   (V1 1.38614)
   *     inner / outer        0.76315   (V1 0.76310)
   *     stroke / outer       0.23685   (V1 0.23689)
   *
   * Identical construction, rescaled. The VALUES below are therefore the
   * V1 numbers, unchanged: they express the same geometry at the scale
   * the ring is actually drawn at on screen. templates.mjs scales the
   * artwork by 31.73 / 127.9 = 0.2480844, which gives outer 133.97
   * against the 133.94 here — the same ring to four significant figures.
   *
   * The intersection of the two outer circles is a vesica. At artwork
   * scale it measures 331.49 x 778.545; rotated to landscape that is an
   * aspect of 2.34864, against the 2.3486 below. The house crop ratio
   * carries over unchanged.
   *
   * The mask-ellipse percentages that drew the vesica as a shape are
   * gone (2026-08-19): the client asked for the oval "lens" crop and the
   * cross-document ring wipe to be removed, and those were the only two
   * consumers. The measurements that survive are the ones the ratio and
   * the drawn header ring still need.
   * ------------------------------------------------------------- */
  --ring-r-outer: 133.94;
  --ring-r-inner: 102.21;
  --ring-separation: 185.66;
  --ring-stroke-ratio: 0.2369; /* (outer - inner) / outer */

  --ring-lens-w: 193.1;
  --ring-lens-h: 82.22;
  --ring-lens-aspect: 2.3486;

  /* ---------------------------------------------------------------
   * 7. Image ratios
   * Two ratios only. Every image is cropped to one of them and sits
   * inside a passe-partout with a hairline.
   *
   * The house ratio is the vesica's own 2.3486:1. This is the quiet
   * version of the idea: the Infinity Ring governs the crop of every
   * photograph on the site even though the lens shape itself is never
   * drawn any more.
   * ------------------------------------------------------------- */
  --ratio-house: var(--ring-lens-w) / var(--ring-lens-h); /* 2.3486 */
  --ratio-primary: 3 / 2;

  /* ---------------------------------------------------------------
   * 8. Motion
   *
   * Interface motion (hover, drawer, focus) uses --dur-fast / --dur /
   * --dur-slow. Scroll entry is its own, slower band: the client asked
   * for a noticeably more animated arrival (2026-08-19), so sections now
   * bring their children in on a stagger rather than fading as one
   * block.
   *
   *   --dur-reveal      620ms  a single decisive movement, not a drift
   *   --reveal-stagger   60ms  per child, capped in app.js at 8 steps so
   *                            a 24-cell logo wall finishes in ~1.1s
   *   --reveal-rise      20px  the distance travelled, transform only
   *   --reveal-scale     1.03  photographs settle to 1 from just over
   *   --reveal-floor     0.45  ENTRY OPACITY, NEVER 0
   *
   * The floor is the load-bearing number. Text starts at 45% opacity and
   * rises to 100%; it is legible the whole way, so a fast scroll to any
   * point in the document — or a jump to a #hash — can never show a
   * blank screen or an empty section waiting to be observed.
   *
   * --ease is the single easing curve for everything: a decisive
   * ease-out that lands without overshoot. Nothing bounces, nothing
   * spins (Q17, quietly confident).
   * ------------------------------------------------------------- */
  --dur-fast: 160ms;
  --dur: 240ms;
  --dur-slow: 420ms;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  --dur-reveal: 620ms;
  --reveal-stagger: 60ms;
  --reveal-rise: 20px;
  --reveal-scale: 1.03;
  --reveal-floor: 0.45;

  /* ---------------------------------------------------------------
   * 8b. The scroll-motion package (revision pass 7, 20 Aug 2026)
   *
   * Four systems, all governed by the same rule as the entry above:
   * transform and opacity only, no layout property is ever animated, the
   * 0.45 floor holds for every piece of text, and every one of them is
   * completely inert under prefers-reduced-motion. None of them scrubs,
   * smooths or otherwise touches native scrolling.
   *
   * PARALLAX — a full-bleed photograph drifts inside its clipped frame.
   *   --parallax-scale  1.08  overscan, so the drift has somewhere to go
   *   --parallax-shift    4%  travelled either side of centre, so the full
   *                           traversal is 8% of the image's own height
   * The frame never moves; only the image inside it does, and only on the
   * vertical axis. At the moment a reader is actually looking at a plate —
   * roughly centred in the viewport — the shift is ~0, and the visible crop
   * is the middle 92.6% of the file. The one-edge extreme is only reached
   * when the frame is leaving the viewport entirely.
   *
   * WORD RISE — a headline arrives one word at a time.
   *   --dur-word        480ms  per word
   *   --word-stagger     40ms  between words, capped in app.js
   *   --word-rise       0.6em  travelled, transform only, scales with type
   * The words carry BOTH legs of the entry (rise and fade) and the heading
   * itself is pinned, because opacity composites: a 0.45 word inside a 0.45
   * block would render at 0.20 and break the floor.
   *
   * RING DRAW-ON — the Infinity Ring draws itself into the green band.
   *   --dur-draw        900ms  per circle
   *   --draw-offset     180ms  the second circle follows the first
   *   --ring-draw-length       2π x 133.97, the outer circumference at the
   *                            scale the artwork is drawn at (see §6). The
   *                            same number is in styles.css; both must move
   *                            together.
   *
   * COUNT-UP — a small integer counts from zero.
   *   --dur-count       500ms  mirrored in app.js as COUNT_MS; both must
   *                            move together. Declared here so the value
   *                            lives with the rest of the motion system and
   *                            is auditable from the built stylesheet.
   * ------------------------------------------------------------- */
  --parallax-scale: 1.08;
  --parallax-shift: 4%;

  --dur-word: 480ms;
  --word-stagger: 40ms;
  --word-rise: 0.6em;

  --dur-draw: 900ms;
  --draw-offset: 180ms;
  --ring-draw-length: 841.76;

  --dur-count: 500ms;

  /* No radius, no shadow, no gradient. Declared so the absence is
   * explicit and reviewable rather than accidental. */
  --radius: 0;
  --shadow: none;

  color-scheme: light;
}

/* -----------------------------------------------------------------
 * Ground contexts.
 *
 * A page alternates these; they re-point the semantic roles rather than
 * redefining the palette. Order matters: `.on-surface` is the generic
 * "not paper" marker and templates apply it alongside a ground modifier,
 * so `.band--sage` and `.band--green` are declared AFTER it and win at
 * equal specificity. Every one of them re-declares the full role set, so
 * no role can leak in from the ground it is layered on.
 * --------------------------------------------------------------- */

/* White — the paper. The default, restated so it can be nested inside
 * any darker ground and reset cleanly. */
.on-paper {
  --bg: var(--white);
  --bg-raised: var(--white);
  --fg: var(--charcoal);
  --fg-muted: var(--charcoal-72);
  --fg-disabled: var(--charcoal-65);
  --accent: var(--green);
  --focus: var(--charcoal);
  --rule: var(--charcoal-12);
  --rule-strong: var(--charcoal-18);
  background: var(--bg);
  color: var(--fg);
  color-scheme: light;
}

/* Charcoal — the floor. The footer, and any band that asks for the
 * darkest ground. Text is sage (9.41:1), per the brand's own footer
 * treatment; white is equally available at 16.35:1 where a component
 * needs maximum weight. */
.on-surface {
  --bg: var(--charcoal);
  --bg-raised: var(--white-08);
  --fg: var(--sage);
  --fg-muted: var(--sage-78);
  --fg-disabled: var(--sage-68);
  --accent: var(--sage);
  --focus: var(--sage);
  --rule: var(--white-14);
  --rule-strong: var(--white-24);
  background: var(--bg);
  color: var(--fg);
  color-scheme: dark;
}

/* Sage — the panel. Galleries and fact panels. Charcoal text at 9.41:1.
 * The accent stays charcoal here rather than green: green on sage is
 * 4.69:1, which passes AA but is the thinnest pair in the system, and
 * the components that would carry it are 12px uppercase labels. Green
 * appears on sage only as a hairline or a ring track, never as text. */
.band--sage {
  --bg: var(--sage);
  --bg-raised: var(--white);
  --fg: var(--charcoal);
  --fg-muted: var(--charcoal-72);
  --fg-disabled: var(--charcoal-72);
  --accent: var(--charcoal);
  --focus: var(--charcoal);
  --rule: var(--charcoal-18);
  --rule-strong: var(--charcoal-28);
  background: var(--bg);
  color: var(--fg);
  color-scheme: light;
}

/* Green — the statement. One band per page. White text at 8.15:1;
 * secondary at white 78% for 5.77:1. Sage is never used for text here
 * (4.69:1) — only for grounds and decoration. */
.band--green {
  --bg: var(--green);
  --bg-raised: var(--white-08);
  --fg: var(--white);
  --fg-muted: var(--white-78);
  --fg-disabled: var(--white-70);
  --accent: var(--white);
  --focus: var(--white);
  --rule: var(--white-14);
  --rule-strong: var(--white-24);
  background: var(--bg);
  color: var(--fg);
  color-scheme: dark;
}

/*
 * ROOTS APAC / KOON APAC — DATUM, Version 2
 * Site stylesheet.
 *
 * tokens.css is prepended to this file at build time, so every custom
 * property it declares is available here. Edit tokens for values;
 * edit this file for structure.
 *
 * Rule: four brand colours only — Forest Green, Sage Mist, Signal White,
 * ROOTS Charcoal, plus alpha variants of those four. Nothing here may
 * introduce a fifth colour; tools/qa.mjs fails the build if it does. All
 * other colour on the site comes from photography and from client logos,
 * which keep the colour they were supplied in.
 *
 * Colour is never referenced directly below. Components read the semantic
 * roles (--bg, --fg, --fg-muted, --accent, --focus, --rule, --rule-strong)
 * and the ground classes in tokens.css re-point those roles, so one
 * component is correct on white, sage, green and charcoal without a
 * per-ground override.
 */

/* -----------------------------------------------------------------
 * 1. Base
 * --------------------------------------------------------------- */

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

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

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--type-body);
  font-weight: var(--wt-regular);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  background: var(--bg);
  color: var(--fg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body.menu-open {
  overflow: hidden;
}

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

figure,
figcaption,
p,
h1,
h2,
h3,
address,
blockquote {
  margin: 0;
}

address {
  font-style: normal;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Selection inverts whatever ground it lands on. Custom properties are
 * inherited by the originating element, so one rule covers white, sage,
 * green and charcoal — the V1 `.on-surface ::selection` override is no
 * longer needed. Every resulting pair is 8.15:1 or better. */
::selection {
  background: var(--fg);
  color: var(--bg);
}

/* Focus is never currentColor: muted and disabled text would draw a
 * translucent ring. --focus is a solid brand colour chosen per ground —
 * charcoal on white and sage, white on green, sage on charcoal — so the
 * ring is at least 8.15:1 against every ground on the site. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}

/* -----------------------------------------------------------------
 * 2. Utilities
 * --------------------------------------------------------------- */

.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;
}

.skip-link {
  position: fixed;
  z-index: 100;
  top: 0.5rem;
  left: 0.5rem;
  padding: 0.75rem 1rem;
  background: var(--charcoal);
  color: var(--white);
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  transform: translateY(-200%);
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus {
  transform: translateY(0);
}

/* The skip link carries its own charcoal ground while the page around it
 * is white, so it needs its own focus colour or the ring disappears. */
.skip-link:focus-visible {
  outline-color: var(--white);
}

/* -----------------------------------------------------------------
 * 3. Shell, grid, bands
 * --------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--margin);
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: var(--gutter);
}

/* The visible column grid — a `.rules` overlay drawing hairlines at
 * columns 1, 4, 7 and 10 — was removed at the client's request on
 * 2026-08-19, markup and styles together. The 12-column grid above is
 * still what places every component; nothing draws it any more.
 * Horizontal hairlines are a separate device and all stay: fact tables,
 * the footer rules, logo-cell borders, `.rule` and `.rule--tick`. */

.band {
  position: relative;
  padding-block: var(--band);
}

.band > .shell {
  position: relative;
  z-index: 1;
}

.band--tight {
  padding-block: var(--sp-8);
}

/* The ground modifiers --band--sage and --band--green live in tokens.css
 * with the other ground contexts, so all four grounds are declared in one
 * place and their cascade order is fixed. They set only colour roles; the
 * structural band rules above are shared. */

/* -----------------------------------------------------------------
 * 4. Type roles
 * --------------------------------------------------------------- */

.title {
  font-size: var(--type-title);
  font-weight: var(--wt-medium);
  line-height: var(--lh-title);
  letter-spacing: var(--tr-title);
  text-wrap: balance;
}

.section-title {
  font-size: var(--type-section);
  font-weight: var(--wt-medium);
  line-height: var(--lh-section);
  letter-spacing: var(--tr-section);
  text-wrap: balance;
}

.sub {
  font-size: var(--type-sub);
  font-weight: var(--wt-medium);
  line-height: var(--lh-sub);
  letter-spacing: var(--tr-sub);
}

.lead {
  font-size: var(--type-lead);
  line-height: 1.5;
  max-width: var(--measure);
}

.body {
  font-size: var(--type-body);
  line-height: var(--lh-body);
  max-width: var(--measure);
  color: var(--fg-muted);
}

.label {
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  line-height: var(--lh-label);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

.meta {
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  line-height: var(--lh-label);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

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

/* -----------------------------------------------------------------
 * 5. The wordmark and the Infinity Ring device
 * Ring geometry is the symbol's own construction. See tokens.css §6.
 * --------------------------------------------------------------- */

/* The header brand link. Charcoal on white; the mark takes its fill from
 * currentColor so the same paths serve the footer on charcoal. */
.brand,
.brand-link {
  display: flex;
  align-items: center;
  color: var(--fg);
}

/* Inline wordmark SVG, viewBox 0 0 956.88 380.36 (2.516:1). Sized by
 * height so the aspect is carried by the artwork: 26px on a phone rising
 * to 33px on a desktop, which keeps the mark's cap height in step with
 * the 4rem header without ever crowding it. */
.brand-mark,
.brand-logo {
  display: block;
  height: clamp(1.625rem, 1.2rem + 1.5vw, 2.0625rem); /* 26 → 33 */
  width: auto;
  fill: currentColor;
}

.oo {
  display: block;
  height: 1.375rem;
  width: auto;
}

.oo circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 31.73;
}

.oo .oo-track {
  opacity: 0.28;
}

/* Rotation is carried by the SVG transform attribute alone. A CSS
 * transform-origin here re-anchors the same property and throws the
 * arc outside the viewBox.
 *
 * Circumference = 2π x 118.10 = 742.06, the mid-radius of the Infinity
 * Ring scaled into the header. The same number is in app.js and in
 * templates.mjs; all three must move together. */
.oo .oo-progress {
  stroke: var(--accent);
  opacity: 1;
  stroke-dasharray: 742.06;
  stroke-dashoffset: 742.06;
  transition: stroke-dashoffset 120ms linear;
}

.marker {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.marker .oo {
  height: 0.75rem;
  flex: none;
}

/* The drawn lens — a vesica-shaped mask cut from the intersection of the
 * two outer rings — was removed at the client's request on 2026-08-19.
 * Every photograph it held is now a plain rectangular plate on one of
 * the two house ratios, and the --ring-mask-* tokens that shaped it are
 * gone from tokens.css with it.
 *
 * `.media-slot` is what replaces `.lens-slot`: the same five-column
 * position beside a text column, holding an ordinary mounted image. */
.media-slot {
  position: relative;
  z-index: 1;
  grid-column: 1 / -1;
  margin-top: var(--sp-6);
}

@media (min-width: 64rem) {
  .media-slot {
    grid-column: 8 / span 5;
    margin-top: 0;
    align-self: end;
  }
}

/* -----------------------------------------------------------------
 * 6. Plates — the passe-partout. Images are mounted, never bled raw.
 * --------------------------------------------------------------- */

.plate {
  border: var(--hair) solid var(--rule-strong);
  padding: var(--sp-1);
  background: var(--bg-raised);
  /* Clips the 3% overscan of the scroll-entry settle (§14). At rest the
   * image is exactly its own box, so nothing is cropped by this. */
  overflow: hidden;
}

.plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.plate--house img {
  aspect-ratio: var(--ratio-house);
}

.plate--primary img {
  aspect-ratio: var(--ratio-primary);
}

.caption,
.media-caption {
  margin-top: var(--sp-1);
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.media-caption {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

/* -----------------------------------------------------------------
 * 6b. Hero media — the brand motion loop
 *
 * Full bleed inside its column: no passe-partout, because the loop is
 * the brand device rather than a photograph. Cropped to the house ratio
 * so the 16:9 render sits in the same rhythm as every plate on the page.
 * The sage ground is what shows before the video decodes, and all that
 * shows if the motion files have not landed yet — a finished, quiet
 * state rather than a white hole. The <video> element's own poster
 * attribute covers the reduced-motion and JavaScript-off cases.
 * --------------------------------------------------------------- */

.hero-media {
  position: relative;
  aspect-ratio: var(--ratio-house);
  width: 100%;
  overflow: hidden;
  background: var(--sage);
}

.hero-media__video,
.hero-media video,
.hero-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* -----------------------------------------------------------------
 * 7. Rules and ticks — the datum lines everything measures from
 * --------------------------------------------------------------- */

.rule {
  border: 0;
  border-top: var(--hair) solid var(--rule-strong);
  margin: 0;
}

.rule--tick {
  position: relative;
}

/* The tick is the one place the accent appears as pure geometry: green
 * on white, white on green, sage on charcoal. */
.rule--tick::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 0;
  width: var(--hair);
  height: 7px;
  background: var(--accent);
}

/* -----------------------------------------------------------------
 * 8. Header
 * --------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg);
  border-bottom: var(--hair) solid var(--rule);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 4rem;
}

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

.nav {
  display: none;
  gap: var(--sp-3);
}

@media (min-width: 56rem) {
  .nav {
    display: flex;
  }
}

/* Idle nav is charcoal at 72% (6.46:1); hover and the current page take
 * the accent. The current page also keeps its underline, so state is
 * never carried by colour alone. */
.nav a {
  display: block;
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  padding-block: 0.875rem;
  color: var(--fg-muted);
  border-bottom: var(--hair) solid transparent;
  transition:
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.nav a:hover {
  color: var(--accent);
}

.nav a[aria-current="page"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Menu toggle — two hairlines, no icon set. Q27 answered "none". */
.menu-toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 2.75rem;
  height: 2.75rem;
  margin-right: -0.75rem;
}

@media (min-width: 56rem) {
  .menu-toggle {
    display: none;
  }
}

/* Bars are targeted by class, never by `span` or child position — the
 * button's accessible name is an aria-label precisely so no text node
 * can be mistaken for a bar. */
.menu-toggle__bar {
  display: block;
  width: 1.25rem;
  height: var(--hair);
  margin-inline: auto;
  background: var(--fg);
  transition: transform var(--dur) var(--ease);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:first-child {
  transform: translateY(3px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:last-child {
  transform: translateY(-3px) rotate(-45deg);
}

/* Mobile drawer. 44px minimum targets. */
.mobile-menu {
  position: fixed;
  inset: 4rem 0 0;
  z-index: 39;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border-top: var(--hair) solid var(--rule);
  transform: translateY(-100%);
  visibility: hidden;
  transition:
    transform var(--dur-slow) var(--ease),
    visibility var(--dur-slow);
}

.mobile-menu[data-open="true"] {
  transform: translateY(0);
  visibility: visible;
}

@media (min-width: 56rem) {
  .mobile-menu {
    display: none;
  }
}

.mobile-menu__inner {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--sp-4) var(--margin) var(--sp-6);
}

.mobile-menu a {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-height: 2.75rem;
  padding-block: 0.75rem;
  border-bottom: var(--hair) solid var(--rule);
  font-size: var(--type-sub);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-sub);
}

.mobile-menu a[aria-current="page"] {
  color: var(--accent);
}

.mobile-menu a span {
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  color: var(--fg-muted);
}

.mobile-menu__email {
  margin-top: var(--sp-4);
  font-size: var(--type-body) !important;
  letter-spacing: 0 !important;
  border-bottom: 0 !important;
}

/* -----------------------------------------------------------------
 * 9. Page hero
 * --------------------------------------------------------------- */

.hero__type {
  position: relative;
  z-index: 1;
  grid-column: 1 / -1;
}

@media (min-width: 64rem) {
  .hero__type {
    grid-column: 1 / span 7;
  }
}

.hero__title {
  margin-top: var(--sp-3);
}

.hero__rule {
  margin-block: var(--sp-4);
  max-width: 22rem;
}

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

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

/* Registration index — gives the hero's right columns verifiable fact
 * rather than decorative emptiness. */
.hero__index {
  position: relative;
  z-index: 1;
  grid-column: 1 / -1;
  margin-top: var(--sp-6);
  align-self: end;
}

@media (min-width: 64rem) {
  .hero__index {
    grid-column: 10 / span 3;
    margin-top: 0;
  }
}

.hero__media {
  position: relative;
  z-index: 1;
  margin-top: var(--sp-8);
}

/* Inner pages get a compact hero: no index block, less air. */
.hero--compact {
  padding-bottom: var(--sp-8);
}

/* -----------------------------------------------------------------
 * 10. Market rail — the groove
 * Stays on the paper ground: it sits directly above the first sage
 * panel, and giving it a ground of its own would merge the two.
 * --------------------------------------------------------------- */

.rail {
  border-top: var(--hair) solid var(--rule);
  border-bottom: var(--hair) solid var(--rule);
  background: var(--bg);
}

.rail__inner {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 48rem) {
  .rail__inner {
    grid-template-columns: repeat(6, 1fr);
  }
}

.rail__item {
  padding: var(--sp-3) var(--sp-1) var(--sp-3) var(--sp-2);
  border-left: var(--hair) solid var(--rule);
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 11. Fact tables, stats, galleries
 * All laid on the 12-column grid so separators fall on structural
 * lines rather than arbitrary fractions.
 * --------------------------------------------------------------- */

.facts {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.facts th,
.facts td {
  padding: 0.75rem 0;
  border-top: var(--hair) solid var(--rule);
  vertical-align: baseline;
}

.facts tr:last-child th,
.facts tr:last-child td {
  border-bottom: var(--hair) solid var(--rule);
}

.facts th {
  width: 38%;
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.facts td {
  font-size: var(--type-small);
  font-weight: var(--wt-regular);
}

/* Fact tables carry the enquiry address, including inside the green
 * statement band. The underline means the link is not signalled by
 * colour alone on any ground. */
.facts a {
  color: var(--accent);
  border-bottom: var(--hair) solid currentColor;
}

.stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gutter);
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
}

.stats__item {
  padding: var(--sp-3) 0;
  border-bottom: var(--hair) solid var(--rule);
}

@media (min-width: 64rem) {
  .stats {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .stats__item {
    grid-column: span 3;
    border-bottom: 0;
    border-left: var(--hair) solid var(--rule);
    padding-left: var(--sp-2);
  }

  .stats__item:first-child {
    border-left: 0;
    padding-left: 0;
  }
}

/* Tabular figures are what make the count-up (§29) free of any movement: a
 * value that counts 0 → 5 rewrites this element about thirty times, and with
 * proportional digits every rewrite would be a different width. Tabular gives
 * every digit the same advance, and the element is a block box in a fixed grid
 * cell, so neither the number nor its label shifts while it counts. */
.stats__value {
  font-size: var(--type-sub);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-sub);
  font-variant-numeric: tabular-nums;
}

.stats__label {
  margin-top: 0.25rem;
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.gallery {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gutter);
  margin-top: var(--sp-8);
}

@media (min-width: 64rem) {
  .gallery {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .gallery > * {
    grid-column: span 6;
  }
}

/* -----------------------------------------------------------------
 * 12. Links
 * Green on white (8.15:1), white on green, charcoal on sage, sage on
 * charcoal. The underline is always present, so the link is legible as
 * a link without relying on the colour shift.
 * --------------------------------------------------------------- */

.link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding-block: 0.5rem;
  color: var(--accent);
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  border-bottom: var(--hair) solid currentColor;
  transition: gap var(--dur-fast) var(--ease);
}

.link:hover {
  gap: 0.875rem;
}

.link__arrow {
  font-size: 1em;
  line-height: 1;
}

/* -----------------------------------------------------------------
 * 13. Footer — the charcoal floor, sage text (9.41:1)
 * --------------------------------------------------------------- */

.site-footer {
  padding-block: var(--band) var(--sp-6);
}

.footer-lead {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-6);
  border-bottom: var(--hair) solid var(--rule);
}

.footer-tagline {
  font-size: var(--type-sub);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-sub);
}

.footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6) var(--gutter);
  margin-top: var(--sp-6);
}

@media (min-width: 48rem) {
  .footer-grid {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .footer-grid > * {
    grid-column: span 4;
  }
}

.footer-entity {
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
}

.footer-grid address {
  margin-top: var(--sp-2);
  font-size: var(--type-small);
  color: var(--fg-muted);
}

/* The enquiry address steps up from sage-78 to solid sage and takes an
 * underline, so it reads as the one actionable line in the block. */
.footer-grid address a {
  color: var(--accent);
  border-bottom: var(--hair) solid currentColor;
}

.footer-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-top: var(--sp-8);
  padding-top: var(--sp-3);
  border-top: var(--hair) solid var(--rule);
}

/* The footer wordmark is a <use> reference to the header's paths and
 * inherits the footer's sage currentColor. Held smaller than the header
 * mark so the floor stays quiet — and smaller again since 2026-08-19,
 * because the `.footer-entity` line below it now carries the name in
 * readable text. The mark is a brand device here, not the label. */
.footer-logo {
  height: 1.125rem;
  width: auto;
  margin-bottom: var(--sp-2);
}

/* -----------------------------------------------------------------
 * 14. Scroll transitions — the staggered section entry
 *
 * Replaces the V1/V2 single-block fade. As each section reaches the
 * viewport its children arrive one after another on a short rise, and
 * photographs settle the last 3% of a scale. The client asked for
 * noticeably more animation (2026-08-19); the discipline that keeps it
 * quietly confident rather than busy is in the numbers, all of which
 * live in tokens.css §8:
 *
 *   duration  620ms   one decisive movement per element
 *   stagger    60ms   per child, capped at 8 steps in app.js
 *   distance   20px   upward, transform only
 *   easing     cubic-bezier(0.22, 1, 0.36, 1) — ease-out, no overshoot,
 *                     nothing bounces and nothing spins
 *
 * Four rules this system may never break:
 *
 * 1. TRANSFORM AND OPACITY ONLY. Both are composited off the main
 *    thread, so a stagger across 24 logo cells costs no layout and no
 *    paint. Nothing here animates height, margin, top or width, so
 *    scroll position can never shift under the reader's finger — every
 *    element occupies its final box from first paint.
 * 2. NO BLANK SCREENS. Entry opacity is --reveal-floor (0.45), never 0.
 *    Text is legible from the first frame at any scroll speed; a reader
 *    who lands mid-document, jumps to a #hash, or scrolls faster than
 *    the observer fires still sees content, never an empty band.
 * 3. NOTHING DEPENDS ON JAVASCRIPT. Every rule below is scoped to `.js`,
 *    which app.js sets on the documentElement before first paint. With
 *    JavaScript off nothing is transformed or faded at all.
 * 4. REDUCED MOTION REMOVES IT ENTIRELY — see the block at the end of
 *    this section. Not a shorter animation: no animation, content shown
 *    in its final state.
 *
 * `--reveal-delay` is the per-element stagger offset. app.js sets it
 * inline the moment an element enters; it defaults to 0ms so any element
 * the observer never reaches still transitions correctly.
 * --------------------------------------------------------------- */

.js .reveal {
  opacity: var(--reveal-floor);
  transform: translateY(var(--reveal-rise));
  transition:
    opacity var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms),
    transform var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms);
}

.js .reveal.is-in {
  opacity: 1;
  transform: none;
}

/* Collections stagger their own children instead of arriving as one
 * block: the logo wall, the stat row, the capability index, the values
 * list, the capability galleries. The container itself is pinned static
 * so a child is never moved twice — 20px inside a parent that is also
 * travelling 20px reads as a lurch. */
.js .reveal[data-reveal-stagger] {
  opacity: 1;
  transform: none;
  transition: none;
}

.js [data-reveal-stagger] > * {
  opacity: var(--reveal-floor);
  transform: translateY(var(--reveal-rise));
  transition:
    opacity var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms),
    transform var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms);
}

.js [data-reveal-stagger].is-in > * {
  opacity: 1;
  transform: none;
}

/* The photographic settle. A mounted image starts 3% over and eases back
 * into its mount; the mount, its hairline and the layout never move.
 * `.plate` clips only so the overscan cannot spill across the
 * passe-partout mid-transition — at rest the image is exactly its own
 * box, so the static rendering is unchanged. The opacity leg keeps the
 * work-row hover fade alive, which a bare transform list would drop. */
.js .reveal .plate img {
  transform: scale(var(--reveal-scale));
  transition:
    transform var(--dur-reveal) var(--ease) var(--reveal-delay, 0ms),
    opacity var(--dur) var(--ease);
}

.js .reveal.is-in .plate img,
.js [data-reveal-stagger].is-in .plate img {
  transform: none;
}

/* Set only for the length of the transition, cleared in app.js on
 * transitionend: a permanent will-change would hold a compositor layer
 * for every plate on the page. */
.js .is-transitioning,
.js [data-reveal-stagger].is-transitioning > * {
  will-change: opacity, transform;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0s !important;
  }

  /* Everything the scroll system touches is returned to its finished
   * state — visible, unmoved, unscaled — whether or not the observer
   * ever ran. Nothing is left hidden waiting for a class. */
  .js .reveal,
  .js [data-reveal-stagger] > *,
  .js .reveal .plate img {
    opacity: 1;
    transform: none;
  }

  .js .is-transitioning,
  .js [data-reveal-stagger].is-transitioning > * {
    will-change: auto;
  }
}

/* -----------------------------------------------------------------
 * 15. Section index
 * Sticky sub-navigation on Capabilities and About. Sits directly under
 * the header, so its offset must track the header's 4rem height.
 * --------------------------------------------------------------- */

.section-index {
  position: sticky;
  top: 4rem;
  z-index: 30;
  background: var(--bg);
  border-top: var(--hair) solid var(--rule);
  border-bottom: var(--hair) solid var(--rule);
}

.section-index__inner {
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
}

.section-index__inner::-webkit-scrollbar {
  display: none;
}

.section-index a {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex: none;
  min-height: 2.75rem;
  padding: 0.875rem var(--sp-2) 0.875rem 0;
  margin-right: var(--sp-3);
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--fg-muted);
  border-bottom: 2px solid transparent;
  transition:
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.section-index a:hover {
  color: var(--accent);
}

.section-index a[aria-current="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* V1 set this number in --ink-500, which was 4.13:1 on paper and failed
 * AA for normal text. It now shares --fg-muted at 6.46:1. */
.section-index a span {
  font-size: var(--type-meta);
  letter-spacing: var(--tr-meta);
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 16. Work index
 * A list, not cards. Repetition is the point: every row is identical
 * so the work, not the layout, supplies the variation.
 * --------------------------------------------------------------- */

.work-list {
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
}

.work-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gutter);
  padding-block: var(--sp-6);
  border-bottom: var(--hair) solid var(--rule);
}

@media (min-width: 64rem) {
  .work-row {
    grid-template-columns: repeat(var(--cols), 1fr);
    align-items: start;
  }

  .work-row__text {
    grid-column: 1 / span 5;
  }

  .work-row__media {
    grid-column: 7 / span 6;
  }
}

.work-row__title {
  margin-top: var(--sp-2);
}

.work-row__cta {
  margin-top: var(--sp-3);
}

.work-row__media img {
  transition: opacity var(--dur) var(--ease);
}

.work-row a:hover .work-row__media img,
.work-row__media a:hover img {
  opacity: 0.88;
}

/* -----------------------------------------------------------------
 * 17. Capability index and sections
 * --------------------------------------------------------------- */

.capability-index {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gutter);
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
}

.capability-index__item {
  padding: var(--sp-3) 0;
  border-bottom: var(--hair) solid var(--rule);
}

@media (min-width: 64rem) {
  .capability-index {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .capability-index__item {
    grid-column: span 3;
    border-bottom: 0;
    border-left: var(--hair) solid var(--rule);
    padding-left: var(--sp-2);
  }

  .capability-index__item:first-child {
    border-left: 0;
    padding-left: 0;
  }
}

.capability-index__number {
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  color: var(--fg-muted);
}

.capability-index__title {
  margin-top: 0.25rem;
  font-size: var(--type-body);
  font-weight: var(--wt-medium);
}

.capability-index__summary {
  margin-top: var(--sp-1);
  font-size: var(--type-small);
  color: var(--fg-muted);
}

.capability {
  border-top: var(--hair) solid var(--rule);
  padding-block: var(--band);
}

.capability:first-of-type {
  border-top: 0;
}

.capability__examples {
  grid-column: 1 / -1;
  margin-top: var(--sp-4);
}

@media (min-width: 64rem) {
  .capability__examples {
    grid-column: 8 / span 5;
    margin-top: 0;
  }
}

.capability__examples li {
  padding-block: 0.625rem;
  border-top: var(--hair) solid var(--rule);
  font-size: var(--type-small);
  color: var(--fg-muted);
}

.capability__examples li:last-child {
  border-bottom: var(--hair) solid var(--rule);
}

/* Selected-programme disclosure.
 *
 * Everything below applies only after app.js has upgraded a programme into
 * a button — `.is-interactive`, `.programmes__toggle`, `.programmes__panel`
 * and `.programmes__sign` do not exist in the served HTML. With no script
 * the list keeps exactly the rules above it and reads as it always did.
 *
 * The row's padding MOVES from the <li> onto the button so the whole row is
 * the target rather than a text-sized strip inside it: 0.625rem either side
 * of a 15px line is a ~42px target, comfortably over the WCAG 2.5.8 minimum
 * of 24px. The <li> keeps its hairline and its colour, which the button
 * inherits, so an upgraded row is pixel-identical to a plain one at rest. */
.capability__examples li.is-interactive {
  padding-block: 0;
}

.programmes__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  width: 100%;
  padding-block: 0.625rem;
  text-align: left;
  transition: color var(--dur) var(--ease);
}

/* --fg on --bg is 16.35:1 on paper; the resting --fg-muted is 5.74:1. The
 * hover state darkens rather than tints, so no fifth colour appears. */
.programmes__toggle:hover {
  color: var(--fg);
}

/* A plus that becomes a minus, drawn from two hairlines so it matches the
 * rules around it exactly. Only the vertical stroke moves, and it moves by
 * transform — no layout, nothing for the reduced-motion block to undo
 * beyond killing the duration it already kills globally. */
.programmes__sign {
  position: relative;
  flex: none;
  width: 0.6875rem;
  height: 0.6875rem;
}

.programmes__sign::before,
.programmes__sign::after {
  content: "";
  position: absolute;
  background: currentColor;
}

.programmes__sign::before {
  top: 50%;
  right: 0;
  left: 0;
  height: var(--hair);
  margin-top: calc(var(--hair) / -2);
}

.programmes__sign::after {
  top: 0;
  bottom: 0;
  left: 50%;
  width: var(--hair);
  margin-left: calc(var(--hair) / -2);
  transition: transform var(--dur) var(--ease);
}

.programmes__toggle[aria-expanded="true"] .programmes__sign::after {
  transform: scaleY(0);
}

/* The panel is shown and hidden with the `hidden` attribute — no height
 * animation, so opening one can never shift the page under a reader. The
 * padding is on the panel rather than the row above it, so a closed
 * programme adds nothing at all to the list's resting height. */
.programmes__panel {
  padding-bottom: var(--sp-2);
}

/* -----------------------------------------------------------------
 * 18. Charter and values
 * --------------------------------------------------------------- */

/* Purpose and methodology are the same statement type, so they share this
 * class: label, statement, supporting line — twice. The replacement
 * questionnaire states both ("Cultivating Success" / "Empowering Potential")
 * and treats the second as how the first is delivered, which is why they sit
 * together rather than in separate sections. */
.charter__purpose {
  margin-top: var(--sp-2);
  font-size: var(--type-section);
  font-weight: var(--wt-medium);
  line-height: var(--lh-section);
  letter-spacing: var(--tr-section);
}

.charter__note {
  margin-top: var(--sp-3);
  max-width: 46ch;
  font-size: var(--type-small);
  color: var(--fg-muted);
}

.charter__label {
  margin-top: var(--sp-6);
}

/* Three values on a 12-column grid. They span 3, not 4: at span 4 the
 * dividers land on columns 5 and 9, which are not rule positions, so
 * they read as unrelated lines sitting between the structure. At span 3
 * they fall exactly on the column 4 and 7 rules. Columns 10–12 stay
 * empty, matching the hero's quiet right-hand side. */
.values {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gutter);
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
}

.values > * {
  padding-block: var(--sp-3);
  border-bottom: var(--hair) solid var(--rule);
}

@media (min-width: 48rem) {
  .values {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .values > * {
    grid-column: span 3;
    border-bottom: 0;
    border-left: var(--hair) solid var(--rule);
    padding-left: var(--sp-2);
  }

  .values > *:first-child {
    border-left: 0;
    padding-left: 0;
  }
}

.values dt {
  font-size: var(--type-body);
  font-weight: var(--wt-medium);
}

.values dd {
  margin: var(--sp-1) 0 0;
  font-size: var(--type-small);
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 19. Client logo field
 * Logos keep the colour they were supplied in. Recolouring a partner's
 * mark is a brand-compliance decision that is not ours to make, and the
 * updated branding changes nothing about that.
 *
 * Each cell is explicitly Signal White rather than --bg-raised, so a
 * supplied light-ground mark still reads if the wall is ever placed on
 * a sage, green or charcoal band. The logo wall is the one component
 * that does not follow its ground.
 * --------------------------------------------------------------- */

/* Two columns across the whole wall, four from the tablet breakpoint up
 * (client request, 2026-08-19). Twenty-four marks divide exactly both
 * ways — 4 x 6 on desktop, 2 x 12 on a phone — so the wall is always a
 * complete rectangle with no orphan cell on the last row. Four cells to
 * a 1440px container gives each mark a 320px cell — the cell the
 * per-brand optical sizes below are calibrated against. */
.logo-field {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
  border-left: var(--hair) solid var(--rule);
}

@media (min-width: 48rem) {
  .logo-field {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* Cell proportion follows the column count. At two columns a cell is
 * narrow, so 3:2 keeps it from becoming a letterbox; at four columns a
 * cell is ~320px wide and 3:2 would make a 1280px-tall wall, so the cell
 * takes the house ratio instead and the whole wall lands near 800px.
 * The mark inside is centred either way, and sized per-brand by the rule
 * below rather than by the cell. */
.logo-field__cell {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 3 / 2;
  padding: var(--sp-2);
  border-right: var(--hair) solid var(--rule);
  border-bottom: var(--hair) solid var(--rule);
  background: var(--white);
}

@media (min-width: 48rem) {
  .logo-field__cell {
    aspect-ratio: var(--ratio-house);
  }
}

/* Optical sizing, not uniform sizing.
 *
 * A single capped height across marks running from 1.07:1 to 8.42:1 makes
 * rendered area track aspect ratio, so Shell and AWS read tiny while Thales
 * and Gigamon read enormous. Each mark now carries its own `--logo-size` —
 * a rendered width derived from equal area, computed in site-data.mjs and
 * printed on the element as a static value. Nothing is measured in the
 * browser.
 *
 * The width is a percentage of the cell's content box, so the whole wall
 * scales together and the equal-area relationship survives every breakpoint;
 * `height: auto` keeps each mark on its natural ratio, so no client's
 * artwork is ever distorted. The 55%-of-cell height ceiling is enforced when
 * the numbers are generated rather than by a `max-height` here, which with a
 * set width could only be honoured by squashing the mark.
 *
 * `--logo-cap` is the retina ceiling, present only on marks whose supplied
 * file is too small to reach the target at 2x — Montran alone today. */
.logo-field__cell img {
  width: var(--logo-size, 100%);
  max-width: var(--logo-cap, 100%);
  height: auto;
  object-fit: contain;
}

/* -----------------------------------------------------------------
 * 20. Reserved / pending content modules
 * Used where the client has not yet supplied approved content. Visible
 * and honest rather than filled with invented material.
 * --------------------------------------------------------------- */

.reserved {
  margin-top: var(--sp-6);
  border-top: var(--hair) solid var(--rule);
}

/* `.reserved__row` / `.reserved__slot` are the three-across variant. The
 * About team module stopped using them on 2026-08-19 — the client will not
 * supply individual portraits — but the names are part of the markup
 * contract and the pattern still fits any future multi-slot reservation,
 * so the rules stay. */
.reserved__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gutter);
}

@media (min-width: 48rem) {
  .reserved__row {
    grid-template-columns: repeat(3, 1fr);
  }
}

.reserved__slot {
  padding: var(--sp-3) 0;
  border-bottom: var(--hair) solid var(--rule);
}

.reserved__slot span {
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  color: var(--fg-muted);
}

.reserved__slot p {
  margin-top: 0.25rem;
  font-size: var(--type-body);
  font-weight: var(--wt-medium);
}

/* The single wide reservation: one team group photograph, held at the house
 * ratio so the slot is already the shape the supplied file will be cut to.
 * Sage rather than an empty outline — the module has to read as deliberately
 * reserved, not as an image that failed to load. The label sits bottom-left,
 * out of the way of the frame's centre, so the proportion is what registers
 * first. */
.reserved__frame {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 0.25rem;
  aspect-ratio: 3 / 2;
  margin-top: var(--sp-4);
  padding: var(--sp-3);
  border: var(--hair) solid var(--rule);
  background: var(--sage);
}

/* At two columns' worth of width the 3:2 above keeps the slot from becoming
 * an unreadable strip on a phone; from the tablet breakpoint up it takes the
 * house ratio, the same proportion every other wide plate on the site uses. */
@media (min-width: 48rem) {
  .reserved__frame {
    aspect-ratio: var(--ratio-house);
    padding: var(--sp-4);
  }
}

/* When the frame holds an image (currently the AI-generated placeholder,
 * 20 Aug 2026), the photograph fills the slot edge to edge and the
 * empty-reservation padding is dropped. Where :has() is unsupported the
 * image simply sits inset by the padding — still legible, never broken. */
.reserved__frame:has(img) {
  padding: 0;
  overflow: hidden;
}

.reserved__frame img {
  width: 100%;
  height: 100%;
  align-self: stretch;
  object-fit: cover;
}

/* Both lines take full-strength charcoal — 9.41:1 on sage. The muted
 * charcoal-72 used elsewhere lands at 4.80:1 over sage, which clears AA but
 * not by enough to spend on an 11px uppercase label. Hierarchy comes from
 * size and tracking instead of from colour. */
.reserved__frame-label {
  font-size: var(--type-body);
  font-weight: var(--wt-medium);
  color: var(--charcoal);
}

.reserved__frame-ratio {
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--charcoal);
}

.content-status {
  margin-top: var(--sp-3);
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 21. Contact
 * --------------------------------------------------------------- */

.office-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6) var(--gutter);
  margin-top: var(--sp-8);
  border-top: var(--hair) solid var(--rule);
  padding-top: var(--sp-6);
}

@media (min-width: 48rem) {
  .office-grid {
    grid-template-columns: repeat(var(--cols), 1fr);
  }

  .office-grid > * {
    grid-column: span 4;
  }
}

.office__logo {
  height: 1.5rem;
  width: auto;
  margin-block: var(--sp-2);
}

.office address,
.office p {
  font-size: var(--type-small);
  color: var(--fg-muted);
}

/* An office card's entity name is a heading, not body copy. Without this the
 * rule above — one specificity point heavier than `.footer-entity` — would
 * mute "ROOTS ASIA PACIFIC" and "KOON APAC" to secondary text, which is the
 * legibility problem this block was changed to fix. Full-strength charcoal,
 * label size, both cards identical. */
.office .footer-entity {
  font-size: var(--type-label);
  color: var(--fg);
}

.office a {
  color: var(--accent);
  border-bottom: var(--hair) solid currentColor;
}

/* Form fields are hairline-underlined, never boxed. */
.enquiry-form {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4) var(--gutter);
  margin-top: var(--sp-6);
}

@media (min-width: 48rem) {
  .enquiry-form {
    grid-template-columns: repeat(2, 1fr);
  }

  .field--full {
    grid-column: 1 / -1;
  }

  .enquiry-form__actions {
    grid-column: 1 / -1;
  }
}

.field label {
  display: block;
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.field input,
.field textarea {
  display: block;
  width: 100%;
  margin-top: 0.375rem;
  padding: 0.5rem 0;
  font-family: inherit;
  font-size: var(--type-body);
  color: var(--fg);
  background: transparent;
  border: 0;
  border-bottom: var(--hair) solid var(--rule-strong);
  border-radius: 0;
  transition: border-color var(--dur-fast) var(--ease);
}

.field textarea {
  resize: vertical;
}

/* Placeholders are the one "disabled" role in the system: charcoal at
 * 65%, 5.11:1 on white — clearly subordinate to entered text at 16.35:1
 * but still legible, where V1's --ink-600 sat at 4.64:1 doing both jobs. */
.field input::placeholder,
.field textarea::placeholder {
  color: var(--fg-disabled);
  opacity: 1;
}

.field input:focus,
.field textarea:focus {
  border-bottom-color: var(--accent);
}

.button-line {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding-block: 0.5rem;
  color: var(--accent);
  font-size: var(--type-label);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  border-bottom: var(--hair) solid currentColor;
  transition: gap var(--dur-fast) var(--ease);
}

.button-line:hover {
  gap: 0.875rem;
}

/* Held while an endpoint submission is in flight, so the form cannot be
 * posted twice. --fg-disabled is 5.11:1 on paper, so the button stays
 * legible while it is unavailable. */
.button-line[disabled] {
  color: var(--fg-disabled);
  cursor: default;
}

.button-line[disabled]:hover {
  gap: 0.5rem;
}

/* The honeypot. Removed from the visual and the accessibility tree both
 * — off-screen, aria-hidden and out of the tab order — so only a bot
 * filling every field it finds will put anything in it. app.js aborts
 * the submission silently when it is non-empty. It keeps a real <label>
 * so the form still passes the QA gate's labelling check. */
.field--trap {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.form-status {
  margin-top: var(--sp-2);
  font-size: var(--type-small);
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 22. Case study
 * --------------------------------------------------------------- */

.case__media {
  position: relative;
  z-index: 1;
  margin-top: var(--sp-8);
}

.next-project {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gutter);
  margin-top: var(--sp-8);
  padding-top: var(--sp-6);
  border-top: var(--hair) solid var(--rule);
}

@media (min-width: 64rem) {
  .next-project {
    grid-template-columns: repeat(var(--cols), 1fr);
    align-items: start;
  }

  .next-project__text {
    grid-column: 1 / span 5;
  }

  .next-project__media {
    grid-column: 7 / span 6;
  }
}

/* -----------------------------------------------------------------
 * 23. Contact band — the repeated closing action
 * This is the one green statement band per page. Its colour roles come
 * from `.band--green` in tokens.css; only layout is set here.
 * --------------------------------------------------------------- */

.contact-band__statement {
  margin-top: var(--sp-3);
}

.contact-band__meta {
  grid-column: 1 / -1;
  margin-top: var(--sp-6);
}

@media (min-width: 64rem) {
  .contact-band__meta {
    grid-column: 9 / span 4;
    margin-top: 0;
  }
}

/* -----------------------------------------------------------------
 * 24. Page transition — removed
 *
 * The cross-document Infinity Ring wipe (@view-transition plus the
 * ::view-transition-* rules and the oo-wipe-in / oo-wipe-out keyframes)
 * was removed at the client's request on 2026-08-19. Navigation is now
 * an ordinary instant page load in every browser, which is also what
 * every non-Chromium browser was already doing. Nothing replaces it: the
 * animated arrival now happens inside the page, in §14.
 * --------------------------------------------------------------- */

/* -----------------------------------------------------------------
 * 25. Section placeholder
 * Any remaining Phase 2 stub. qa.mjs counts these so none can ship
 * unnoticed.
 * --------------------------------------------------------------- */

.pending {
  margin-top: var(--sp-6);
  padding: var(--sp-4) 0;
  border-top: var(--hair) solid var(--rule);
  border-bottom: var(--hair) solid var(--rule);
  font-size: var(--type-meta);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-meta);
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* -----------------------------------------------------------------
 * 26. Scroll parallax — the drift inside a plate
 *
 * A full-bleed photograph is overscanned by --parallax-scale and drifts
 * --parallax-shift either side of centre as its frame crosses the viewport.
 * The mount, its hairline and the page never move: the only thing that
 * changes is which 92.6% of the file is visible through a frame that is
 * already `overflow: hidden` (§6).
 *
 * Two mechanisms, never both. Where the browser has scroll-driven
 * animations the timeline IS the scroll position — no listener, no script,
 * no main-thread work at all, and `view()` gives exactly the mapping wanted
 * (0% as the frame's top edge reaches the viewport bottom, 100% as its
 * bottom edge leaves the top). Where it does not, app.js writes
 * --parallax-y from one passive rAF-throttled scroll listener, and it
 * feature-detects the same condition so the two can never both be live.
 *
 * `translate` and `scale` are used rather than a `transform` string
 * deliberately: `.js .reveal .plate img` (§14) already owns `transform` for
 * the entry settle, and the individual properties compose with it instead
 * of fighting it — the settle's 1.03 multiplies the 1.08 overscan for the
 * length of the entry and then releases, exactly as it does on any other
 * plate. Nothing rotates, nothing moves horizontally.
 *
 * Which plates drift is a content decision made in templates.mjs: the case
 * hero and the home feature slot, both house-ratio and both full width.
 * A photograph whose composition lives in the outer 4% opts out there
 * (the Dulux stage reveal does) rather than being cropped by motion.
 * --------------------------------------------------------------- */

.js .plate--drift img {
  scale: var(--parallax-scale);
  translate: 0 var(--parallax-y, 0%);
}

@supports (animation-timeline: view()) {
  .js .plate--drift img {
    /* The custom property is left at its default here: with a timeline
     * driving the animation, app.js never runs its fallback and never sets
     * one. Duration is `auto` — the timeline supplies the progress. */
    animation: media-drift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}

@keyframes media-drift {
  from {
    translate: 0 calc(var(--parallax-shift) * -1);
  }

  to {
    translate: 0 var(--parallax-shift);
  }
}

/* -----------------------------------------------------------------
 * 27. Headline word rise
 *
 * app.js splits an `h1.title` or an `h2.section-title` into one span per
 * word and the words arrive in reading order, each 40ms behind the one
 * before it. The split is layout-neutral by construction — inline-block
 * words with the original spaces left as text nodes between them, so lines
 * break exactly where they broke before — and app.js verifies that by
 * comparing offsetHeight before and after, reverting the whole thing if the
 * two ever disagree. Under reduced motion no heading is split at all.
 *
 * THE HEADING IS PINNED, ITS WORDS ARE NOT. This is the floor rule doing
 * real work. Opacity composites down a subtree: a word entering at 0.45
 * inside a block entering at 0.45 renders at 0.20, which is the blank-screen
 * failure the floor exists to prevent. So a heading that carries a word rise
 * takes no fade and no movement of its own, and its parent is promoted to a
 * `data-reveal-stagger` group by app.js so the marker, rule, lead and CTA
 * around it still arrive on the house stagger. Every element in the chain
 * above a word is at opacity 1; the word's own 0.45 is therefore the real
 * on-screen value.
 * --------------------------------------------------------------- */

.js [data-reveal-stagger] > [data-word-rise],
.js .reveal[data-word-rise] {
  opacity: 1;
  transform: none;
  transition: none;
}

/* --reveal-delay is set on the heading by app.js as an ordinary stagger
 * child and inherits down to the words, so a headline low in a block still
 * waits its turn before its first word moves. --word-delay is the per-word
 * offset, written once at split time. */
.js .word {
  display: inline-block;
  opacity: var(--reveal-floor);
  translate: 0 var(--word-rise);
  transition:
    opacity var(--dur-word) var(--ease)
      calc(var(--reveal-delay, 0ms) + var(--word-delay, 0ms)),
    translate var(--dur-word) var(--ease)
      calc(var(--reveal-delay, 0ms) + var(--word-delay, 0ms));
}

.js .is-in [data-word-rise] .word,
.js [data-word-rise].is-in .word {
  opacity: 1;
  translate: none;
}

/* -----------------------------------------------------------------
 * 28. The Infinity Ring draw-on — quiet background art in the green band
 *
 * One ring per page, in the one green statement band per page. It is the
 * symbol's own construction at its outer radius: two circles, r 133.97,
 * centres (133.97, 133.97) and (319.65, 133.97), which is the artwork's
 * r 540 / separation 748.51 scaled by 31.73 / 127.9 exactly as the header
 * indicator is (tokens.css §6). The viewBox is inset by 2 units so a
 * hairline sitting on the outermost circle is not clipped by its own box.
 *
 * On entry the circles draw from the twelve o'clock position — the left one
 * forward, the right one backward, so they grow towards each other and meet
 * at the interlock, 180ms apart. stroke-dashoffset is the one property here
 * that is not a compositor property; it repaints two hairline circles in a
 * decorative SVG and cannot cause layout, which is the same trade the header
 * scroll indicator has always made.
 *
 * It is art, not information: aria-hidden, pointer-events none, sage at 20%
 * over green, absolutely positioned behind `.band > .shell` and clipped by
 * the band. Sage on green is 4.69:1 and carries no text anywhere on this
 * site — the house rule in tokens.css §1 permits it for exactly this, a ring
 * track. Nothing about it can move the page: it is out of flow from the
 * first frame.
 * --------------------------------------------------------------- */

.band--green {
  overflow: hidden;
}

.band-ring {
  position: absolute;
  z-index: 0;
  top: 50%;
  right: 0;
  width: clamp(18rem, 34vw, 30rem);
  translate: 22% -50%;
  opacity: 0.2;
  pointer-events: none;
}

.band-ring svg {
  display: block;
  width: 100%;
  height: auto;
}

/* No vector-effect: with non-scaling-stroke the dash pattern would be
 * resolved in screen space too, and --ring-draw-length is in user units.
 * 1.6 user units renders between 1.0px and 1.7px across the width clamp
 * above, which is the hairline range the rest of the site draws in. */
.band-ring circle {
  fill: none;
  stroke: var(--sage);
  stroke-width: 1.6;
}

.js .band-ring circle {
  stroke-dasharray: var(--ring-draw-length);
  stroke-dashoffset: var(--ring-draw-length);
  transition: stroke-dashoffset var(--dur-draw) var(--ease);
}

/* The second ring starts from the far side of the dash pattern, so it draws
 * anticlockwise into the same meeting point. */
.js .band-ring circle:nth-of-type(2) {
  stroke-dashoffset: calc(var(--ring-draw-length) * -1);
  transition-delay: var(--draw-offset);
}

.js .band-ring.is-drawn circle {
  stroke-dashoffset: 0;
}

/* -----------------------------------------------------------------
 * 29. Stat count-up
 *
 * Entirely in app.js — a small integer counts from zero to its rendered
 * value over --dur-count on the house easing curve. There is no CSS state
 * to declare: the served HTML already contains the final number, so with no
 * script, with no IntersectionObserver, or under reduced motion, the value
 * that ships is the value that shows. The only stylesheet obligation is the
 * one already met on `.stats__value` in §11 — tabular figures, so counting
 * cannot change the element's width.
 * --------------------------------------------------------------- */

/* -----------------------------------------------------------------
 * 30. Reduced motion — the scroll-motion package, removed
 *
 * Not shortened: removed. Each system is returned to the state it would
 * have had if it had never run, and in every case that state is the
 * finished one — image at its natural crop, headline fully visible and
 * unmoved, ring fully drawn, numbers at their final values (which app.js
 * guarantees by not touching them at all). §14's block does the same for
 * the section entry; this one is scoped to what pass 7 added.
 * --------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* Parallax: no timeline, no overscan, no drift. The photograph is exactly
   * its frame, which is a tighter composition than the animated one ever
   * shows. */
  .js .plate--drift img {
    animation: none;
    scale: none;
    translate: none;
  }

  /* Word rise: app.js does not split a heading under reduced motion, so
   * these rules are the belt for a setting switched on mid-visit, when the
   * spans already exist. */
  .js .word {
    opacity: 1;
    translate: none;
    transition: none;
  }

  /* Ring: drawn in full from the first frame. */
  .js .band-ring circle,
  .js .band-ring circle:nth-of-type(2) {
    stroke-dashoffset: 0;
    transition: none;
  }
}
