/* ESCATOYS landing — "Equipment powered on".
 *
 * Layout is settled: left-aligned hero, model indices declared before the
 * headline, space instead of hairlines, card interiors set off from the
 * section around them.
 *
 * Design tokens (colour, fonts, structural variables) live in tokens.css.
 * This file is everything else: resets, components, animations, breakpoints.
 */

/* --- Base ------------------------------------------------------------------
 *
 * Smooth scrolling is a layout convenience, not an accessibility feature.
 * Under reduced motion the preference is respected — instant jumps only.
 */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
}

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

  *,
  *::before,
  *::after {
    animation-duration:    1ms !important;
    animation-delay:       0ms !important;
    transition-duration:   1ms !important;
  }
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* Theme transitions give a gentle cross-fade when the visitor toggles. The
     1ms override above collapses it under reduced motion. */
  transition: background 320ms var(--ease), color 320ms var(--ease);
}

h1, h2, h3 {
  margin: 0;
  font-weight: 400;
  text-wrap: balance;
}
p { margin: 0; }
a { color: inherit; }

/* Focus ring. The default accent clears 3:1 on --surface and --surface-alt,
   but on the graphite Safety plane and the footer --accent-bright is used
   because the light-mode accent (#AC5209) disappears against near-black. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.section--dark :focus-visible,
.site-footer :focus-visible {
  outline-color: var(--accent-bright);
}

/* --- Utilities ------------------------------------------------------------- */

.wrap {
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* --- Animations ------------------------------------------------------------
 *
 * rise is used for hero text elements: each child receives a different delay
 * via CSS rules in the reveal block below so they stagger without JS.
 *
 * reveal and rule-extend are driven by scroll-timeline — see the @supports
 * block further down. They are declared here so the @keyframes can be
 * referenced before the @supports block is parsed.
 */
@keyframes rise {
  from { opacity: 0; transform: translateY(1.5rem); }
  to   { opacity: 1; transform: none; }
}

@keyframes reveal {
  from { opacity: 0; transform: translateY(1.25rem); }
  to   { opacity: 1; transform: none; }
}

@keyframes rule-extend {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@keyframes header-settle {
  from { background: transparent; border-bottom-color: transparent; }
  to   { background: var(--header-bg); border-bottom-color: var(--rule); }
}

/* --- Site header ----------------------------------------------------------- */

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

/* The header fades its background in over the first 140px of scroll, so it
   starts transparent at the top of the page (where the hero background shows
   through) and becomes opaque once the hero is behind it. Browsers without
   scroll() support simply show the opaque background from the start. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .site-header {
      animation: header-settle linear both;
      animation-timeline: scroll(root);
      animation-range: 0 140px;
    }
  }
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 2rem;
  /* min-height, not height. If the tabs ever do need a second row the header
     grows to hold them; a fixed height would clip them against the rule. */
  min-height: 68px;
}

/* --- Brand wordmark -------------------------------------------------------- */

.brand {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.9375rem;
  letter-spacing: 0.16em;
  text-decoration: none;
  white-space: nowrap;
}

/* The mark sits beside the wordmark; it does not replace it. Only the header
   carries it — the footer brand is a text block introducing a paragraph, and a
   mark there would be decoration rather than identity. Scoped rather than put
   on .brand itself so the footer's <p class="brand"> keeps its block layout. */
.site-header .brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.brand__mark {
  flex: none;
  width: 1em;
  height: 1em;
  /* fill="currentColor" resolves against this, so the mark is the accent in
     whichever theme is showing — #FF7504 on dark, #AC5209 on light. A file
     referenced through <img> could not follow that. */
  color: var(--accent);
}
.brand sup {
  font-size: 0.55em;
  letter-spacing: 0;
  vertical-align: super;
}

/* --- Site nav -------------------------------------------------------------- */

/* wrap + nowrap, together. Without them the row could not break, so it shrank
   the tabs instead and broke the labels mid-phrase — "Radio-" over "controlled"
   — at every width between the breakpoint that hides the nav and about 1150px.
   A tab moving to a second row is a layout that got tight; a tab split down the
   middle is a layout that looks broken. */
.site-nav {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 1.75rem;
}
.site-nav a {
  position: relative;
  white-space: nowrap;
  font-size: 0.875rem;
  text-decoration: none;
  color: var(--ink-muted);
  padding-block: 0.5rem;
  transition: color var(--t-hover) var(--ease);
}
.site-nav a:hover { color: var(--ink); }

.site-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.25rem;
  height: 1px;
  width: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 260ms var(--ease);
}
.site-nav a:hover::after,
.site-nav a:focus-visible::after { transform: scaleX(1); }

/* Where you are. The current tab holds the same underline the others draw on
   hover — extended, thicker, and in the colour of the section you are standing
   in, which --key brings in from the data-category attribute. So the header has
   one underline mechanism rather than two, and this rule never learns which
   section is which colour.

   Two signals, not one: the ink goes to full contrast as well. Colour alone
   would be the only thing separating the current tab from the other five, and
   that is precisely what a visitor who cannot separate those two greys would
   not see.

   Note what this rule does NOT do: declare content, position or inset. That
   pseudo-element is already in use, and a second block re-declaring it was the
   bug this replaces — it set its own colour and height and won on both, then
   inherited transform: scaleX(0) from the hover rule below it and painted a
   bar of zero width. The box was in the layout, reported the right colour to
   getComputedStyle, and was invisible. */
.site-nav a[aria-current="page"] {
  color: var(--ink);
}
.site-nav a[aria-current="page"]::after {
  height: 2px;
  background: var(--key);
  transform: scaleX(1);
}

/* --- Narrow-screen menu -----------------------------------------------------
 *
 * A <details> disclosure, hidden above the breakpoint where the tab strip fits.
 * The panel is absolutely positioned against .site-header, which is the sticky
 * element — so it hangs below the header and over the page rather than pushing
 * the content down and moving what the visitor was reading.
 */
.site-menu { display: none; }

.site-menu > summary {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  color: var(--ink-muted);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  cursor: pointer;
  list-style: none;
  transition: color var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
/* Both are needed: ::-webkit-details-marker for Safari, ::marker for the rest.
   Without them the browser draws its own triangle beside the icon. */
.site-menu > summary::-webkit-details-marker { display: none; }
.site-menu > summary::marker { content: ""; }
.site-menu > summary:hover { color: var(--ink); border-color: var(--accent); }
.site-menu[open] > summary { color: var(--ink); border-color: var(--accent); }

/* The icon states the destination, like the theme toggle: bars while closed
   ("open this"), a cross while open ("close this"). */
.site-menu .icon-close { display: none; }
.site-menu[open] .icon-open  { display: none; }
.site-menu[open] .icon-close { display: inline; }

.site-menu__panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  padding: 0.5rem var(--gutter) 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}
.site-menu__links { display: grid; }
.site-menu__links a {
  padding: 0.875rem 0;
  font-size: 1.0625rem;
  color: var(--ink-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
}
.site-menu__links a:last-child { border-bottom: 0; }
/* Where you are, marked the same way as in the tab strip: full-contrast ink and
   a mark in the section's own colour. A bar to the left rather than a rule
   underneath, because these are stacked rows and an underline would read as a
   divider like every other row's. */
.site-menu__links a[aria-current="page"] {
  padding-left: 0.875rem;
  color: var(--ink);
  box-shadow: inset 3px 0 0 var(--key);
}

/* The theme control, as a row rather than the boxed icon it is in the header:
   inside the panel it is one item in a list, and a bordered square in a stack of
   text rows reads as something that escaped from somewhere else. */
/* Selected through the panel, not on its own class. .theme-toggle is defined
   further down this file and carries the same specificity, so a bare
   .site-menu__toggle loses on source order and the button stays a 36px square
   with the label spilling out underneath it. */
.site-menu__panel .theme-toggle {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  height: auto;
  padding: 0.875rem 0;
  margin-top: 0.5rem;
  border: 0;
  border-top: 1px solid var(--rule);
  border-radius: 0;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  text-align: left;
}

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

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

.theme-toggle {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  color: var(--ink-muted);
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  cursor: pointer;
  transition: color var(--t-hover) var(--ease),
              border-color var(--t-hover) var(--ease);
}
.theme-toggle:hover {
  color: var(--ink);
  border-color: var(--ink-muted);
}

/* The icon shows the destination, not the current state: a sun on the dark
   theme means "go light". The three-state mirror from tokens.css applies:
   dark default → show sun; OS light (no override) → show moon;
   explicit light → show moon; explicit dark → show sun. */
.icon-moon { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-mode="dark"]) .icon-sun  { display: none; }
  :root:not([data-mode="dark"]) .icon-moon { display: inline; }
}
:root[data-mode="light"] .icon-sun  { display: none; }
:root[data-mode="light"] .icon-moon { display: inline; }

/* --- CTA button ------------------------------------------------------------ */

.cta {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--cta-fg);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.625rem 1.25rem;
  text-decoration: none;
  transition: background var(--t-hover) var(--ease),
              transform var(--t-hover) var(--ease);
}
.cta:hover  { background: var(--accent-deep); transform: translateY(-1px); }
.cta:active { transform: translateY(0); }

/* --- Eyebrow --------------------------------------------------------------- */

.eyebrow {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 2.5rem;
}
.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}
.eyebrow--dark { color: var(--ink-inverse-muted); }
.eyebrow--dark::after { background: var(--rule-inverse); }


/* --- Sections -------------------------------------------------------------- */

.section { padding-block: var(--section-y); }
.section--alt {
  background: var(--surface-alt);
  transition: background 320ms var(--ease);
}
.section--dark {
  background: var(--surface-dark);
  color: var(--ink-inverse);
}

.section__head { max-width: 46rem; }
.section__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.875rem, 3.6vw, 2.75rem);
  line-height: 1.12;
  letter-spacing: -0.02em;
}
.section__lede {
  margin-top: 1.25rem;
  max-width: 34rem;
  font-size: 1.0625rem;
  color: var(--ink-muted);
}
.section--dark .section__lede { color: var(--ink-inverse-muted); }

/* --- Hero (Direction A initial styles) -------------------------------------
 *
 * The Direction A centered layout is defined first; Direction B overrides it
 * below. This is the structure the mockup uses — both sets are preserved so
 * the intent of the overrides is clear in the source.
 */
.hero {
  padding-block: clamp(5rem, 12vw, 9.5rem) clamp(4rem, 8vw, 7rem);
  text-align: center;
}
.hero .eyebrow {
  justify-content: center;
  margin-bottom: 3rem;
}
.hero .eyebrow::after  { display: none; }
.hero .eyebrow::before {
  content: "";
  width: 3rem;
  height: 1px;
  background: var(--rule-strong);
}
.hero__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(2.375rem, 6vw, 4.25rem);
  line-height: 1.06;
  letter-spacing: -0.03em;
  max-width: 18ch;
  margin-inline: auto;
}
.hero__lede {
  margin: 1.75rem auto 0;
  max-width: 42ch;
  font-size: clamp(1.0625rem, 1.6vw, 1.1875rem);
  color: var(--ink-muted);
}

/* Shared frame for every photograph on the page: same inset ground, same
   hairline, same corner. A picture is an object placed in the layout, not a
   shape cut out of it. */
.hero__figure { margin: 0; display: grid; gap: 0.875rem; }

.hero__media,
.principle__media {
  margin: 0;
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--surface-inset);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.hero__media img,
.principle__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The platforms are stacked in the hero frame and cross-faded. The frame never
   resizes, so selecting one cannot move the page — and the key bar above it
   takes that platform's colour, the same colour its card carries further down.

   Which image is visible is decided by an attribute on the image itself, not by
   a rule per product key. CSS cannot compare one element's attribute with
   another's, so the previous version enumerated the keys — and silently stopped
   working the moment the catalogue was renamed, leaving the hero frame empty on
   every load while everything else still behaved. The server marks the first
   image, main.js moves the mark; neither this file nor anything else needs
   touching when a product is added, renamed or removed. */
.hero__media img {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 420ms var(--ease);
}
.hero__media img[data-active="true"] {
  opacity: 1;
}

/* The key bar doubles as the rotation indicator: ::before is the track, the
   span on top is the fill. Paused or with reduced motion the fill simply sits
   at full width, so the bar looks exactly like the solid key bar it was. */
.hero__media::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: color-mix(in srgb, var(--key) 26%, transparent);
  z-index: 3;
}
.hero__progress {
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--key);
  transform-origin: left;
  transform: scaleX(1);
  z-index: 4;
}

.hero__pause {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin-left: 0.25rem;
  padding: 0;
  color: var(--ink-muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: color var(--t-hover) var(--ease);
}
.hero__pause:hover { color: var(--ink); }
.hero__pause .icon-play { display: none; }
.hero__pause[aria-pressed="true"] .icon-pause { display: none; }
.hero__pause[aria-pressed="true"] .icon-play  { display: inline; }

.hero__caption {
  display: flex;
  align-items: baseline;
  gap: 0.875rem;
  font-size: 0.875rem;
  color: var(--ink-muted);
}
.hero__caption-index {
  font-family: var(--font-data);
  color: var(--accent);
}
.hero__caption-name  { color: var(--ink); }
.hero__caption-note {
  margin-left: auto;
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The chip strip runs the full width of the hero, under both columns, and is a
   grid rather than a wrapping flex row.

   It sat in the text column while it held four chips. It holds fifteen now —
   the whole photographed range instead of the radio-controlled line — and in a
   half-width column that is a wall of chips twice the height of the photograph
   beside it. Across both columns the same fifteen come to three tidy rows.

   Nothing here is a fixed width or a fixed count. The count follows whichever
   products have been photographed, and 13rem is chosen so the columns divide
   the full measure exactly five ways; below that the grid drops to four, three
   and two on its own. */
.chips {
  grid-column: 1 / -1;
  /* No margin of its own: it is a grid row now, so the hero's own row gap
     already separates it. The 3.5rem it used to carry was for flow layout
     inside the text column, and left on top of the gap it doubled up. */
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem;
}
.chip {
  display: grid;
  gap: 0.125rem;
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  text-decoration: none;
  text-align: left;
  transition: border-color 220ms var(--ease),
              background   220ms var(--ease),
              transform    220ms var(--ease);
}
.chip:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  transform: translateY(-3px);
}
.chip__index {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  color: var(--accent);
}
/* The 7px square carries the platform key colour — same mark the card slot
   uses, scaled down. Colour-blind visitors get the letter index beside it. */
.chip__index::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 1px;
  background: var(--key);
}
.chip[aria-current="true"] {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
/* The product name, not its kind. Kind distinguished a buggy from a plane from
   a boat while the strip was the radio-controlled four; across the whole range
   it is four chips reading "Quad" and two reading "Chair", separated only by an
   index. */
.chip__name { font-size: 0.8125rem; color: var(--ink-muted); }
.hero__cta { margin-top: 3rem; }

/* --- Hero (Direction B override) -------------------------------------------
 *
 * Direction B is approved. The hero becomes a specification header: left-
 * aligned, model indices declared before the headline, the rule running the
 * full measure. The grid makes the photograph an equal column, not a background
 * treatment — it earns the space it takes.
 */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  /* Top-aligned, not centred. The two columns are different heights — a five
     line headline against a 3/2 frame — and centring split the difference into
     an empty band above the photograph, right where the eye lands first. Held
     to the top, the frame sits beside the headline it belongs to and what is
     left over falls beside the chips, which are small enough not to mind. */
  align-items: start;
  text-align: left;
  padding-block: clamp(4.5rem, 10vw, 8rem) clamp(4rem, 8vw, 7rem);
}
.hero .eyebrow {
  justify-content: flex-start;
  font-size: 0.875rem;
  letter-spacing: 0.18em;
  color: var(--ink);
}
.hero .eyebrow::before { display: none; }
.hero .eyebrow::after  { display: block; }
.hero__title  { margin-inline: 0; max-width: 16ch; }
.hero__lede   { margin-inline: 0; }

/* --- Models ---------------------------------------------------------------- */

.models {
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
.card { display: flex; flex-direction: column; }

.slot {
  position: relative;
  aspect-ratio: 3 / 2;
  background: var(--surface-inset);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  container-type: inline-size;
  transition: border-color 320ms var(--ease), background 320ms var(--ease);
}

/* Platform key bar across the top of the slot — same treatment as the hero
   progress bar, anchored to the model's hue so the mark survives when a real
   photograph replaces the ghost index. */
.slot::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--key);
  z-index: 2;
}

/* The photograph covers the ghost index rather than replacing it: the slot
   keeps working the moment a src is empty or fails, and nothing in the layout
   moves either way. */
.slot img {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.slot::after {
  content: attr(data-index);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-data);
  font-size: clamp(3.5rem, 18cqw, 7rem);
  color: var(--slot-ghost);
  letter-spacing: 0.05em;
  pointer-events: none;
  z-index: 0;
  transition: color 420ms var(--ease), letter-spacing 520ms var(--ease);
}
.card:hover .slot { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.card:hover .slot::after {
  color: color-mix(in srgb, var(--accent) 22%, transparent);
  letter-spacing: 0.12em;
}

.card__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.25rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--rule);
}
.card__index {
  font-family: var(--font-data);
  font-size: 0.8125rem;
  color: var(--accent);
}
.card__age {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.card__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.375rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
  margin-top: 0.875rem;
}
.card__summary {
  margin-top: 0.5rem;
  font-size: 0.9375rem;
  color: var(--ink-muted);
  max-width: 32ch;
}
.card__link {
  margin-top: 1.25rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  align-self: flex-start;
  transition: border-color var(--t-hover) var(--ease);
}
.card__link:hover { border-bottom-color: var(--accent); }
.card__link .arrow {
  display: inline-block;
  transition: transform 240ms var(--ease);
}
.card__link:hover .arrow,
.card:hover .card__link .arrow { transform: translateX(4px); }

/* --- Craft ----------------------------------------------------------------- */

.principles {
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 4vw, 3.5rem);
}
.principle__media { margin-bottom: 1.375rem; }
.principle__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
  /* Direction B drops the rule: space alone separates the columns. */
  border-top-color: transparent;
  padding-top: 0;
}
.principle__body {
  margin-top: 1rem;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

/* --- Safety ---------------------------------------------------------------- */

.safety {
  margin-top: 3.5rem;
  display: grid;
  /* The table column leads now. It carries four regulations rather than four
     ages, and the prose beside it is held to 40ch anyway, so the space is worth
     more on the left: at the old 0.85fr the longest regime ran to three lines. */
  grid-template-columns: 1fr 0.9fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

/* Column headings inside the safety grid use the data font at caption scale —
   not the section__title treatment, because they label table columns, not
   sections. */
.safety h3 {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-inverse-muted);
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--rule-inverse);
}

.ages {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr auto;
  /* Stretch, not baseline. Every cell draws its own bottom border, and baseline
     alignment leaves each one sitting at its own text's height — invisible while
     every row was one line, and three misaligned rules per row the moment the
     middle column started wrapping. Stretched cells share the row height, so the
     three borders meet. */
  align-items: stretch;
}
.ages dt,
.ages dd {
  margin: 0;
  padding-block: 0.875rem;
  border-bottom: 1px solid var(--rule-inverse);
}
.ages dt {
  font-family: var(--font-data);
  font-size: 0.875rem;
  color: var(--ink-inverse-muted);
  padding-right: 1.25rem;
}
.ages__kind  { font-size: 0.9375rem; }
.ages__value {
  font-family: var(--font-data);
  font-size: 0.875rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.safety__body {
  margin-top: 1.5rem;
  font-size: 1rem;
  color: var(--ink-inverse-muted);
  max-width: 40ch;
}
.safety__body + .safety__body { margin-top: 1rem; }
.safety__body strong {
  font-weight: 400;
  color: var(--ink-inverse);
}

/* --- FAQ ------------------------------------------------------------------- */

.faq {
  margin-top: 3rem;
  max-width: 46rem;
}
.faq details { border-bottom: 1px solid var(--rule); }
.faq summary {
  list-style: none;
  cursor: pointer;
  padding-block: 1.125rem;
  padding-right: 2rem;
  position: relative;
  font-size: 1.0625rem;
  transition: color var(--t-hover) var(--ease);
}
.faq summary:hover { color: var(--accent); }
/* Remove the native browser marker. Vendor-prefixed because ::details-marker
   (the standard way) is not yet universal. */
.faq summary::-webkit-details-marker { display: none; }
/* The chevron is a CSS ::after pseudo-element to keep the question text clean
   for screen readers. A DOM span inside <summary> would be read aloud; a
   pseudo-element with content:"" is not. */
.faq summary::after {
  content: "";
  position: absolute;
  right: 0.375rem;
  top: 50%;
  width: 9px;
  height: 9px;
  border-right: 1.5px solid var(--ink-muted);
  border-bottom: 1.5px solid var(--ink-muted);
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--t-hover) var(--ease);
}
.faq details[open] summary::after { transform: translateY(-30%) rotate(225deg); }
.faq p {
  padding-bottom: 1.5rem;
  max-width: 60ch;
  color: var(--ink-muted);
  font-size: 0.9375rem;
}

/* Animated open/close via the ::details-content pseudo-element. This is a
   progressive enhancement — browsers without interpolate-size support open
   instantly, which is still correct behaviour. */
@supports (interpolate-size: allow-keywords) {
  @media (prefers-reduced-motion: no-preference) {
    :root { interpolate-size: allow-keywords; }
    .faq details::details-content {
      block-size: 0;
      overflow: hidden;
      opacity: 0;
      transition: block-size 380ms var(--ease),
                  opacity    300ms var(--ease),
                  content-visibility 380ms allow-discrete;
    }
    .faq details[open]::details-content { block-size: auto; opacity: 1; }
  }
}

/* --- Request form ---------------------------------------------------------- */

.request {
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: 0.8fr 1fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
}

.promises {
  margin: 2rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.875rem;
}
.promises li {
  font-size: 0.9375rem;
  color: var(--ink-muted);
  padding-left: 1.25rem;
  position: relative;
}
/* A short rule instead of a bullet: the style is consistent with the data-
   forward aesthetic, and it renders clearly at any resolution. */
.promises li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 8px;
  height: 1px;
  background: var(--accent);
}

.form {
  display: grid;
  gap: 1.25rem;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--surface-alt);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  transition: background 320ms var(--ease);
}
.form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
.field { display: grid; gap: 0.4375rem; }
.field label {
  font-family: var(--font-data);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.field abbr { text-decoration: none; color: var(--accent); }
.field input,
.field select,
.field textarea {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: 0.625rem 0.75rem;
  width: 100%;
  transition: border-color var(--t-hover) var(--ease);
}
.field textarea { resize: vertical; min-height: 6.5rem; }
.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--ink-muted); }
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible { border-color: var(--accent); }

.consent {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.75rem;
  align-items: start;
  font-size: 0.875rem;
  color: var(--ink-muted);
}
.consent input {
  margin-top: 0.25rem;
  accent-color: var(--accent);
}
.consent a { color: var(--accent); }

.form__hint { font-size: 0.8125rem; color: var(--ink-muted); }

.form button {
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--cta-fg);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.8125rem 1.5rem;
  cursor: pointer;
  justify-self: start;
  transition: background var(--t-hover) var(--ease),
              transform  var(--t-hover) var(--ease);
}
.form button:hover  { background: var(--accent-deep); transform: translateY(-1px); }
.form button:active { transform: translateY(0); }

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

.site-footer {
  background: var(--footer-bg);
  color: var(--ink-inverse);
  padding-block: clamp(3rem, 6vw, 4.5rem);
  transition: background 320ms var(--ease);
}
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
  justify-content: space-between;
  align-items: flex-start;
}
.site-footer .brand { color: var(--ink-inverse); }
.footer__note {
  margin-top: 1rem;
  font-size: 0.8125rem;
  color: var(--ink-inverse-muted);
  max-width: 34ch;
}
/* The sections column. Sits between the brand block and the legal row, and the
   footer's flex-wrap puts it on its own line when there is no room. */
.footer__nav {
  display: grid;
  gap: 0.625rem;
  align-content: start;
}
.footer__nav a {
  font-size: 0.9375rem;
  color: var(--ink-inverse-muted);
  text-decoration: none;
  transition: color var(--t-hover) var(--ease);
}
.footer__nav a:hover { color: var(--ink-inverse); }
.footer__nav a[aria-current="page"] { color: var(--ink-inverse); }

.footer__legal { display: flex; flex-wrap: wrap; gap: 1.25rem; }
.footer__legal a {
  font-size: 0.8125rem;
  color: var(--ink-inverse-muted);
  text-decoration: none;
  transition: color var(--t-hover) var(--ease);
}
.footer__legal a:hover { color: var(--ink-inverse); }

/* --- Scroll-driven reveal --------------------------------------------------
 *
 * Two separate animation-timeline mechanisms — do not conflate them.
 * (1) .site-header uses scroll(root) — the header settles on scroll position.
 * (2) Everything below uses view() — elements animate as they enter the
 *     viewport. The stagger is achieved by varying animation-range-end on
 *     nth-child selectors, not by JS delays.
 *
 * Both are inside @supports guards and both are suppressed by reduced motion.
 * Content is visible with or without this block; it is cosmetic only.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal,
    .models .card,
    .principles .principle,
    .faq details {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 20%;
    }

    /* Card stagger: later cards have a later range-end, so each takes a little
       longer to fully appear — a natural reading order effect. */
    .models .card:nth-child(2) { animation-range: entry 4% cover 26%; }
    .models .card:nth-child(3) { animation-range: entry 4% cover 24%; }
    .models .card:nth-child(4) { animation-range: entry 4% cover 30%; }

    .principles .principle:nth-child(2) { animation-range: entry 4% cover 26%; }
    .principles .principle:nth-child(3) { animation-range: entry 4% cover 32%; }

    .faq details:nth-child(2) { animation-range: entry 4% cover 23%; }
    .faq details:nth-child(3) { animation-range: entry 4% cover 26%; }
    .faq details:nth-child(4) { animation-range: entry 4% cover 29%; }
    .faq details:nth-child(5) { animation-range: entry 4% cover 32%; }

    /* The eyebrow rule draws from left to right as the section enters. */
    .eyebrow::after {
      animation: rule-extend linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 24%;
      transform-origin: left;
    }

    /* Hero children stagger via delay, not view(), because the hero is always
       in the viewport on load — view() would never trigger for it. */
    /* .hero .chips is named separately because the strip is no longer a child
       of .hero__text — it spans both columns. */
    .hero__text > *,
    .hero .chips,
    .hero__media { animation: rise var(--t-reveal) var(--ease) both; }
    .hero .eyebrow { animation-delay: 80ms; }
    .hero__title   { animation-delay: 200ms; }
    .hero__lede    { animation-delay: 360ms; }
    .hero .chips   { animation-delay: 500ms; }
    .hero__cta     { animation-delay: 640ms; }
    .hero__media   { animation-delay: 260ms; }
  }
}

/* --- Accessibility utilities -----------------------------------------------
 *
 * .skip-link: off-screen at rest, slides in on keyboard focus so keyboard
 *   users can bypass the repeated header navigation on every page.
 * .to-top: fixed corner button. It stays out of the way over the first screen,
 *   where the top is right there anyway, and fades in once the page has been
 *   scrolled past it. That is done on the scroll position itself rather than
 *   with a script, so it costs nothing and cannot jitter. Where scroll-driven
 *   animation is unsupported the button is simply always visible — the
 *   degraded state has to be the usable one, never the hidden one.
 * .field--honeypot: moves the spam trap input completely off-screen so neither
 *   sighted visitors nor screen readers encounter it.
 */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 1rem;
  z-index: 200;
  padding: 0.5rem 1rem;
  background: var(--accent);
  color: var(--cta-fg);
  border-radius: var(--radius);
  text-decoration: none;
  font-family: var(--font-body);
  font-size: 0.9375rem;
}

.skip-link:focus-visible {
  left: 1rem;
  outline: 2px solid var(--accent-bright);
  outline-offset: 2px;
}

.to-top {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  background: var(--surface-alt);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  color: var(--ink-muted);
  text-decoration: none;
  transition: color var(--t-hover) var(--ease),
              border-color var(--t-hover) var(--ease),
              background var(--t-hover) var(--ease);
  z-index: 90;
}

.to-top:hover {
  color: var(--ink);
  border-color: var(--accent);
  background: var(--surface);
}

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

@supports (animation-timeline: scroll()) {
  .to-top {
    animation: to-top-in linear both;
    animation-timeline: scroll(root);
    /* Appears across the second screen: gone while the hero is in view,
       settled by the time the model grid is. */
    animation-range: 60vh 110vh;
  }

  @keyframes to-top-in {
    from {
      opacity: 0;
      transform: translateY(0.75rem);
      visibility: hidden;
    }
    1% {
      visibility: visible;
    }
    to {
      opacity: 1;
      transform: none;
      visibility: visible;
    }
  }

  /* Keyboard users must be able to reach it wherever they are in the page, so
     focus overrides the scroll-driven state outright. */
  .to-top:focus-visible {
    animation: none;
    opacity: 1;
    transform: none;
    visibility: visible;
  }
}

@media (prefers-reduced-motion: reduce) {
  .to-top { animation: none; opacity: 1; transform: none; visibility: visible; }
}

.field--honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- Form feedback ---------------------------------------------------------
 *
 * .form__status: the full-width banner rendered above the form when the page
 *   returns with ?sent=ok|invalid|throttled.
 * .field__error: per-field error paragraph when Django form validation fails.
 */

.form__status {
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  font-size: 0.9375rem;
  border: 1px solid;
  margin-bottom: 1.5rem;
}

.form__status--ok {
  border-color: #2a7d4f;
  color: #1d5c39;
  background: #d4edda;
}

.form__status--invalid,
.form__status--throttled {
  border-color: #c0392b;
  color: #922b21;
  background: #fdecea;
}

.field__error {
  font-size: 0.8125rem;
  color: #c0392b;
  margin-top: 0.25rem;
}

/* --- Responsive ------------------------------------------------------------
 *
 * Two breakpoints:
 * 900px — hero becomes single column; safety, request, principles stack.
 * 720px — nav hides; models stack; chips go 2×2; form rows single-column.
 */
/* The tabs and the header's own gaps tighten before the tabs run out of room.
   Below this the header is carrying six links, a wordmark and two controls in
   less than a thousand pixels, and 28px between tabs is generosity it cannot
   afford. */
@media (max-width: 1100px) {
  .site-header__inner { gap: 1.25rem; }
  .site-nav           { gap: 1.125rem; }
}

@media (max-width: 900px) {
  .hero            { grid-template-columns: 1fr; }
  .safety          { grid-template-columns: 1fr; }
  .request         { grid-template-columns: 1fr; }
  .principles      { grid-template-columns: 1fr; gap: 2.25rem; }
}

@media (max-width: 720px) {
  /* The tab strip goes and the disclosure arrives. Both exist in the document at
     every width; display:none takes the hidden one out of the accessibility tree
     as well, so a screen reader is never offered the same six links twice. */
  .site-nav             { display: none; }
  .site-menu            { display: block; }
  /* The direct child only — the panel's own toggle is nested deeper inside this
     same container, and a descendant selector would hide both. */
  .site-header__actions > .theme-toggle { display: none; }
  .site-header__inner   { gap: 0.875rem; }
  .site-header__actions { gap: 0.75rem; }
  .site-header__inner   { justify-content: space-between; }
  .models               { grid-template-columns: 1fr; }
  /* Two columns at narrow widths, and every chip stays on the page: a phone
     fits two readable chips across and no more. A horizontal scroller would
     keep the hero shorter, but the strip is what the auto-rotation moves
     through, so it would have to scroll itself every few seconds — under the
     visitor's thumb. Height is the cheaper cost. */
  .chips                { grid-template-columns: 1fr 1fr; }
  .chip                 { padding: 0.625rem 0.875rem; }
  .form__row            { grid-template-columns: 1fr; }

  /* The rules table stops being a table. Three columns inside 350px squeezed the
     regulation into a four-word-tall ribbon and pushed the age against the edge,
     so below this it stacks: category, then the rule that governs it, then the
     age, with the row's hairline under the last of the three. Flow layout rather
     than reassigned grid cells, so it does not care how many categories there
     are. */
  .ages                 { display: block; }
  .ages dt              { border-bottom: 0; padding-bottom: 0.25rem; }
  .ages__kind           { border-bottom: 0; padding-block: 0 0.25rem; }
  .ages__value          { padding-top: 0; text-align: left; }
  /* Remnant from Direction A where the toggle had an inline text label.
     Direction B does not render it, but the rule is kept so the breakpoint
     does not break if a label is added back. */
  .switch__label        { display: none; }
}

/* --- Page head -------------------------------------------------------------
 *
 * The opening block on every page that is not the home page. It is deliberately
 * NOT a second hero: no photograph, no rotation, no call to action competing
 * with the one in the header. A rule in the category's key colour is the only
 * decoration, so the page announces which of the three legal worlds it belongs
 * to before a single sentence is read.
 */
.page-head {
  /* padding-block, not the padding shorthand: this element also carries .wrap,
     whose inline padding is what aligns every section to the same left edge.
     The shorthand sets the inline sides to zero and quietly breaks that
     alignment on exactly the pages that have no other content to compare to. */
  padding-block: clamp(3.5rem, 9vw, 6.5rem) clamp(2rem, 5vw, 3.5rem);
}

/* Measure is held on the children, never on .page-head itself. That element
   also carries .wrap, and .wrap centres what it constrains — so a max-width
   set here does not shorten the lines, it indents the entire block away from
   the left edge every other section starts at. */
.page-head__title {
  max-width: 18ch;
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 6vw, 4rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  margin: 0.75rem 0 0;
}

.page-head__lede {
  margin-top: 1.25rem;
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  color: var(--ink-muted);
  max-width: 54ch;
}

/* The category's own colour, carried on a rule rather than on text: at this
   size a coloured heading would fail contrast on one ground or the other,
   and a 3px mark only has to meet the 3:1 graphic threshold. */
.page-head__meta {
  margin-top: 1.75rem;
  padding-top: 1rem;
  border-top: 3px solid var(--key);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  max-width: 46ch;
}

/* --- Prose -----------------------------------------------------------------
 *
 * The legal pages. The class was in the markup from the start and was never
 * defined, which is part of why those pages read as unfinished: browser
 * defaults for h2, p and ul against this typography look like a document
 * nobody laid out.
 *
 * Measure is held on the children, never on .prose itself — that element also
 * carries .wrap, and .wrap centres what it constrains, so a max-width here
 * would indent the whole page away from the left edge every other section
 * starts at. Same trap the page header already fell into once.
 */
/* No padding of its own: the wrapper is already a .section, which carries the
   site's vertical rhythm. Setting it here too stacked the two and opened a
   quarter-screen of nothing above the title. */
.prose > * {
  max-width: 64ch;
}
.prose__title {
  max-width: 20ch;
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 5vw, 3.25rem);
  line-height: 1.08;
  letter-spacing: -0.02em;
  margin: 0.75rem 0 0;
}
.prose__lede {
  margin-top: 1.5rem;
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  line-height: 1.5;
  color: var(--ink);
  max-width: 52ch;
}

/* h2 carries the same hairline the sections use, so a long document scans as a
   stack of parts rather than a wall. */
.prose h2 {
  margin: 3.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.prose h2 + p,
.prose h2 + ul {
  margin-top: 1rem;
}
.prose p {
  margin: 1rem 0 0;
  line-height: 1.65;
  color: var(--ink-muted);
}
.prose ul {
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}
.prose li {
  position: relative;
  margin-top: 0.625rem;
  padding-left: 1.375rem;
  line-height: 1.6;
  color: var(--ink-muted);
}
/* The same en-dash marker the promises list beside the form uses, rather than a
   bullet — this site has no round dots anywhere else. */
.prose li::before {
  content: "–";
  position: absolute;
  left: 0;
  color: var(--accent);
}
/* Links inside the prose need to be visible as links — these documents point at
   each other and at the category pages, and an underline is the only signal that
   survives a visitor who cannot separate the accent from the body colour. */
.prose p a,
.prose li a {
  color: var(--accent);
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}
.prose p a:hover,
.prose li a:hover {
  color: var(--ink);
}
/* The call to action is a button, not prose, and .prose p a above would paint
   its label var(--accent) — which is also the button's own background. It
   shipped that way: a solid orange rectangle with an invisible word in it, on
   every one of the five documents. Two class selectors beat one class and one
   element, so this block wins; it has to restate the colour rather than rely on
   .cta, which loses on specificity from inside .prose.

   Same shape as the current-tab underline recorded in CLAUDE.md — a rule that
   computes exactly as written and paints nothing you can read, because the
   property that mattered was set somewhere else. */
.prose .cta {
  margin-top: 2.5rem;
  color: var(--cta-fg);
}
.prose .cta:hover {
  color: var(--cta-fg);
}
/* The closing date is an aside about the document, not part of it. */
.prose__updated {
  margin-top: 3.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}

/* --- Hub entries -----------------------------------------------------------
 *
 * The five ways into the site. A whole card is one link, so the target is the
 * card and not a four-word phrase inside it — the difference matters most on a
 * phone, where the phrase is the hardest thing on the page to hit.
 */
.entries {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(2rem, 5vw, 3rem);
}

.entry {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.5rem 1.5rem 1.25rem;
  background: var(--surface-inset);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--key);
  border-radius: var(--radius);
  text-decoration: none;
  transition: border-color 320ms var(--ease), background 320ms var(--ease),
              transform 320ms var(--ease);
}

.entry:hover {
  background: var(--surface);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  border-top-color: var(--key);
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .entry:hover { transform: none; }
}

.entry__eyebrow {
  font-family: var(--font-data);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
}

.entry__name {
  font-family: var(--font-display);
  font-size: 1.375rem;
  line-height: 1.15;
  color: var(--ink);
}

.entry__summary {
  font-size: 0.9375rem;
  color: var(--ink-muted);
  flex: 1;
}

/* The category's contents, between the summary and the meta line. Set in the
   data font at caption scale so it reads as a list of parts rather than as more
   prose — the summary above it is the prose. */
.entry__contents {
  display: grid;
  gap: 0.375rem;
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}
.entry__item {
  display: flex;
  gap: 0.625rem;
  align-items: baseline;
  font-size: 0.875rem;
  color: var(--ink-muted);
}
/* Products carry an index; subjects and categories do not, and are not given a
   fake one. The column reserves its width either way so the names still line up
   down the card. */
.entry__index {
  flex: none;
  min-width: 2.5rem;
  font-family: var(--font-data);
  font-size: 0.75rem;
  color: var(--accent);
}

.entry__meta {
  margin-top: 0.5rem;
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}

.entry__go {
  margin-top: 0.75rem;
  font-size: 0.875rem;
  color: var(--accent);
}
.entry:hover .entry__go .arrow { transform: translateX(3px); }

/* --- Rules -----------------------------------------------------------------
 *
 * The which-rules-apply block on category pages and the status block on the
 * training page. Measure is held near 62 characters because these paragraphs
 * are the densest prose on the site and the only ones a visitor may genuinely
 * need to read twice.
 */
.rules {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  max-width: 62ch;
  margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

.rules__body {
  color: var(--ink-inverse-muted);
  font-size: 1rem;
  line-height: 1.7;
}

/* The closing paragraph states what we have NOT got. It is set brighter than
   the rules above it on purpose: it is the sentence a visitor is most likely
   to be looking for, and burying it would be the dishonest way to lay this out. */
.rules__body--stand {
  color: var(--ink-inverse);
  padding-top: 1.25rem;
  border-top: 1px solid color-mix(in srgb, var(--ink-inverse) 18%, transparent);
}

/* --- Light lines -----------------------------------------------------------
 *
 * The one place a joke is allowed to sit. Muted and smaller than the summary
 * above it, so it reads as an aside rather than as product information — and
 * kept structurally separate from the age and the rules, which are never
 * written this way.
 */
.card__aside,
.section__lede--aside {
  color: var(--ink-muted);
  font-style: italic;
}

.card__aside {
  margin-top: 0.5rem;
  font-size: 0.875rem;
}

/* Secondary call to action: same size and shape as .cta so the two align, but
   without the filled ground, so the primary action still wins the eye. */
/* A beat, not a chapter: two thirds of the usual vertical rhythm, because what
   is inside is a heading, a paragraph and a link rather than a grid of cards. */
.section--band {
  padding-block: clamp(3rem, 6vw, 5rem);
}
.band-note {
  margin-top: 2rem;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
/* The aside sits against the section's own key colour, on a rule rather than in
   coloured text: at this size a coloured paragraph would fail contrast on one
   ground or the other, and a 3px mark only has to clear the 3:1 graphic bar. */
.band-note__aside {
  padding-top: 1.25rem;
  border-top: 3px solid var(--key);
  font-size: clamp(1rem, 1.4vw, 1.125rem);
  line-height: 1.55;
  color: var(--ink-muted);
  max-width: 34ch;
}
@media (max-width: 900px) {
  .band-note { grid-template-columns: 1fr; }
}

/* The "read the whole thing" line that closes a teaser section. Its own class
   rather than a bare margin on .cta, because the two teasers on the hub have to
   sit at the same distance from what they summarise. */
.section__more {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
}

.cta--quiet {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--rule);
}
.cta--quiet:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: transparent;
}

/* --- Training page ---------------------------------------------------------
 *
 * The page has no photographs and never will have many: what it sells is a
 * skill, and a stock photo of a soldering iron would say less than the
 * sentence next to it. So the structure carries the page, and these three
 * components exist to keep five text sections from reading as one long wall.
 */

/* The line that says what a person can actually do afterwards. Set apart from
   the body copy because it is the only unhedged promise on the page — every
   other claim here is qualified by the fact that nothing has been scheduled. */
/* Grid items already stretch to equal height, but the content inside them does
   not, so the outcome rules landed at three different heights across the row.
   The card component solved this the same way. */
.principle { display: flex; flex-direction: column; }
.principle__body { flex: 1; }

.principle__outcome {
  margin-top: 1rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--ink);
}

/* Starting points. A two-column definition list rather than a third card grid:
   the page already has cards above and below, and a dl is what this content
   actually is. */
.levels {
  margin: clamp(2rem, 5vw, 3rem) 0 0;
  display: grid;
  /* The description column gets an explicit measure rather than 1fr: at the
     full page width 1fr produced a 90-character line, and constraining the
     whole list instead squeezed the descriptions into a gutter. */
  grid-template-columns: minmax(9rem, 15rem) minmax(0, 58ch);
  gap: 0 clamp(1.5rem, 4vw, 3.5rem);
}

.levels dt,
.levels dd {
  padding: 1.25rem 0;
  border-top: 1px solid var(--rule);
}

.levels dt {
  font-family: var(--font-data);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}

.levels dd {
  margin: 0;
  color: var(--ink-muted);
}

/* Below the fold of a phone the two columns would leave the term on its own
   line with a rule under it, which reads as a heading with nothing beneath. */
@media (max-width: 40rem) {
  .levels { grid-template-columns: 1fr; }
  .levels dt { padding-bottom: 0.375rem; }
  .levels dd { padding-top: 0; border-top: 0; }
}

/* The intended shape: two prose columns that collapse to one. */
.session {
  margin-top: clamp(2rem, 5vw, 3rem);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: clamp(2rem, 4vw, 3.5rem);
}

/* .rules was written for the dark plane and takes its colours from the
   inverse tokens. On a light section those are invisible, so the modifier
   swaps them rather than duplicating the whole block. */
.rules--light .rules__body { color: var(--ink-muted); }
.rules--light .rules__body--stand {
  color: var(--ink);
  border-top-color: var(--rule);
}

/* --- Category band ---------------------------------------------------------
 *
 * A wide image between the page header and the range. Optional and atmospheric:
 * it carries no information, so it is skippable content rather than a slot that
 * must be filled, and a category without one loses nothing.
 */
.band {
  margin: 0 0 clamp(1rem, 3vw, 2rem);
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--rule);
  aspect-ratio: 7 / 3;
}

.band img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --- Reel -------------------------------------------------------------------
 *
 * The video at the top of a category page. Same frame as every other picture on
 * the site — inset ground, hairline, one corner radius — but 16/9 rather than
 * the band's 7/3, because cropping a video to a letterbox throws away the part
 * of the frame the camera was pointed at.
 */
.reel {
  position: relative;
  margin: 0 0 clamp(1rem, 3vw, 2rem);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-inset);
}
.reel__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The controls sit on the video, so they cannot borrow the page's ink colour —
   they need a ground of their own to stay legible over whatever frame is
   showing. Fixed dark plate and light glyph in both themes, for the same reason
   the safety plane is fixed: the video does not change with the theme. */
.reel__controls {
  position: absolute;
  right: 0.75rem;
  bottom: 0.75rem;
  display: flex;
  gap: 0.375rem;
}
.reel__button {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  color: #EDEEEC;
  background: rgba(11, 13, 14, 0.72);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(237, 238, 236, 0.22);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--t-hover) var(--ease), border-color var(--t-hover) var(--ease);
}
.reel__button:hover {
  background: rgba(11, 13, 14, 0.88);
  border-color: rgba(237, 238, 236, 0.45);
}
.reel__play .icon-play { display: none; }
.reel__play[aria-pressed="true"] .icon-pause { display: none; }
.reel__play[aria-pressed="true"] .icon-play  { display: inline; }
/* aria-pressed="true" means the sound is on, so the crossed-out speaker is the
   resting state and the waves are the pressed one. */
.reel__sound .icon-loud { display: none; }
.reel__sound[aria-pressed="true"] .icon-muted { display: none; }
.reel__sound[aria-pressed="true"] .icon-loud  { display: inline; }

/* --- Acknowledgement bar --------------------------------------------------
 *
 * Rendered by the server only while the `notice` cookie is unset, so it never
 * flashes in after paint for somebody who dismissed it a page ago.
 *
 * A bar, not a modal. No overlay, no focus trap, no blur behind it: the page is
 * fully readable and fully operable while it is on screen, because there is
 * nothing here to decide — it names the terms that already apply.
 *
 * z-index sits above .to-top (90) and below the disclosure menu (200), which is
 * attached to the header and must stay on top of everything.
 */
.notice {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 100;
  background: var(--surface-alt);
  border-top: 1px solid var(--rule-strong);
  /* The safe-area inset keeps the bar clear of the home indicator on a phone
     held in portrait; it resolves to 0 everywhere else. */
  padding-block: 0.75rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
}

.notice__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
}

.notice__text {
  margin: 0;
  max-width: 60ch;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--ink-muted);
}

.notice__text a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

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

.notice__ok {
  flex: none;
  /* Pushed to the end rather than left where flex-wrap drops it. Below about
     560px the sentence takes the whole line and the button lands on a line of
     its own, where space-between has nothing to space it against. */
  margin-inline-start: auto;
  min-width: 5.5rem;
  padding: 0.55rem 1.25rem;
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* --surface, not --ink-inverse. The two are the same near-white in the dark
     theme, because --ink-inverse is not "the opposite of ink" — it is the ink
     used on --surface-dark, a plane that stays near-black in both themes, and
     so it is light in both. Painted on an --ink fill it gave a white pill with
     white lettering: the button was there, at full size and full opacity, and
     invisible. --surface is the page ground, so this pair inverts properly in
     both themes — dark lettering on a light pill and the reverse.

     A filled button rather than the .cta orange: the header already carries the
     one accent-filled control on the page, and two of them argue about which is
     the real call to action. */
  color: var(--surface);
  background: var(--ink);
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  cursor: pointer;
  transition: color var(--t-hover) var(--ease),
              background var(--t-hover) var(--ease),
              border-color var(--t-hover) var(--ease);
}

.notice__ok:hover {
  color: var(--cta-fg);
  background: var(--accent);
  border-color: var(--accent);
}

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

/* The bar and the back-to-top control both live in the bottom-right corner, so
 * one has to move. A fixed offset was tried and is wrong: the bar is 61px on a
 * desktop and 115px at 390px, where the sentence takes two lines and the button
 * drops below it — and a translated string moves it again. Any number written
 * here is a guess that is correct at exactly one width.
 *
 * main.js measures the bar and publishes --notice-h, re-measuring on resize.
 * The 7rem default is what applies before that runs and with scripting off,
 * where the bar cannot be dismissed at all: generous enough to clear the tallest
 * form of the bar, which is the direction to be wrong in.
 */
:root {
  --notice-h: 7rem;
}

.has-notice .to-top {
  bottom: calc(1.5rem + var(--notice-h));
}

/* --- Published contact address --------------------------------------------
 *
 * Three places print company.contacts.email: the footer, the privacy notice
 * and the block beside the request form. The prose links inside .prose are
 * already styled; these two are not, so they are given the same underlined
 * treatment rather than left as bare browser-blue.
 */
.request__mail {
  margin: 1.25rem 0 0;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

.request__mail a,
.footer__mail {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
  transition: color var(--t-hover) var(--ease);
}

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

/* The footer sits on the permanently dark plane, so it brightens to
   --ink-inverse on hover rather than going to --accent: that token follows the
   visitor's theme, and its light-theme value is a dark orange that all but
   disappears against the footer's near-black. */
.footer__mail:hover {
  color: var(--ink-inverse);
}
