/* ==========================================================================
   MagVanta design system
   --------------------------------------------------------------------------
   Structural language taken from the anime.js site and repository: a warm
   neutral canvas, monospace-led technical type, the layered grid texture, a
   per-section accent, and the circular instrument. The palette is INVERTED to
   light, because the brief asks for white space and sobriety for a procurement
   and formulation-chemist audience, and a near-black canvas serves a developer
   tool rather than a chemical supplier.

   The accent ramp is narrowed to petrol teal as primary and amber as the warm
   metallic accent, per the brief.

   Contrast notes are recorded inline against the canvas --bg-1 #faf9f7.
   Any change to a colour pair must be re-measured, not assumed.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'MagVanta Display';
  src: url('../fonts/montserrat-var-latin.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   2. Tokens
   --------------------------------------------------------------------------
   The palette is LIGHT. The structural language is taken from anime.js (warm
   neutral canvas, monospace-led type, the layered grid, a per-section accent,
   the instrument) but inverted: a near-black canvas suits a developer tool
   read at night, and works against a specialty-chemical brief that asks for
   white space, sobriety and a procurement audience reading in an office.

   Warmth is preserved deliberately. The canvas is #faf9f7, not #ffffff, which
   mirrors anime.js's warm near-black rather than switching to a clinical grey.

   Every ratio below is MEASURED, not estimated, and scripts/browser-verify.mjs
   re-measures the computed values on every run.
   -------------------------------------------------------------------------- */

:root {
  /* Surfaces. Straight from the brand colour system.
     "White space is part of the palette." */
  --bg-1: #fbf8f2; /* Porcelain, the primary light background */
  --bg-2: #ffffff; /* raised: cards, panels */
  --bg-3: #f6f1e8; /* Warm Ivory, quiet technical and editorial fields */
  --bg-4: #efe9dd; /* deeper ivory: table heads, wells */
  --bg-5: #e4ded2;

  /* Ink, built down from MagVanta Charcoal. fg-1 to fg-3 are body-text safe on
     every surface above. fg-4 is large text and icons only. fg-5 is hairlines. */
  --fg-1: #2e2a27; /* MagVanta Charcoal. 13.42:1 on Porcelain */
  --fg-2: #55504a; /*  7.53:1 on Porcelain */
  --fg-3: #6b655e; /*  5.43:1 on Porcelain. Muted text floor. */
  --fg-4: #7c7570; /*  Fume Gray. 4.27:1. Rules, details, large text only. */
  --fg-5: #e6e0d4; /*  Hairlines and dividers. Never text. */

  --border-control: #8b847d; /* 3.3:1 on white. Control boundaries, WCAG 1.4.11 */

  /* The material palette. These are the magnesium cue, and they are the ONLY
     colours the brand permits beyond the neutrals: no teal, no amber, no
     unrelated accent. Silver and mid are graphics tones and never carry text. */
  --mg-silver: #d8dddc; /* Magnesium Silver, primary refined-metal cue */
  --mg-mid: #aeb5b4; /* Magnesium Mid, facet depth and technical neutrals */
  --mg-ivory: #ded8cc; /* the warm facet tone taken from the emblem */
  --mg-pale: #f0eaea; /* the luminous facet tone taken from the emblem */

  /* Emphasis is carried by CHARCOAL and by weight, not by hue. The brand asks
     for restraint, so the primary action is a charcoal fill with white text. */
  --accent: #2e2a27;
  --accent-hover: #1d1a18;
  --accent-tint: #f6f1e8;

  /* Semantic, kept as close to neutral as legibility allows. */
  --red-1: #8c2c22; /* form errors only */
  --red-4: #e6cfc9;
  --red-6: #faf1ee;

  /* Type. */
  /* The brand specifies a geometric Avenir-style face for the wordmark and
     display, and Arial/Helvetica for body copy. The monospace-led treatment
     this site previously used read as a developer tool rather than a chemical
     supplier, which is why it is gone.

     Technical values keep their alignment through tabular figures rather than
     through a monospace face, so spec tables stay readable without the techy
     look. See .tabular below. */
  --font-display: 'MagVanta Display', 'Avenir Next', Avenir, 'Century Gothic',
    system-ui, sans-serif;
  --font-sans: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
    Arial, sans-serif;
  --font-mono: var(--font-sans);

  --step--2: 0.75rem;
  --step--1: 0.8125rem;
  --step-0: 1rem;
  --step-1: clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem);
  --step-2: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
  --step-3: clamp(1.5rem, 1.3rem + 1vw, 2rem);
  --step-4: clamp(1.875rem, 1.5rem + 1.9vw, 2.75rem);
  --step-5: clamp(2.25rem, 1.6rem + 3.2vw, 4rem);
  /* The top of the shared scale. Capped at 4.25rem: at 5rem a headline set in
     the hero column broke to six lines at 1440px, which read as cramped rather
     than bold. */
  --step-6: clamp(2.5rem, 1.6rem + 3.6vw, 4.25rem);

  /* The home hero headline has its own step, below the top of the shared
     scale, because it carries a far longer string than any other title on the
     site: 81 characters against the 30 to 55 a page title runs to. Measured on
     the shared step-6, that sentence set five lines at 1440px and nine at
     390px, and the hero unit finished 44px past the fold on desktop and 130px
     past it on a phone. Two fixed points, 28px at 390px and 56px at 1440px,
     put it on three lines in the desktop column and four on a phone. */
  --step-hero: clamp(1.75rem, 1.1rem + 2.67vw, 3.5rem);
  /* The credential band. It was --step--2, 12px, which read as incidental text
     rather than as a statement of standing. Clamped rather than fixed because
     the longest credential is 70 characters: a flat 17px would wrap it to
     three lines on a 390px screen and make the band taller than the value it
     adds. 14px on a phone, 17px from about 1200px up. */
  --step-cred: clamp(0.875rem, 0.78rem + 0.4vw, 1.0625rem);

  /* The strapline that closes the home hero headline. Off the shared scale for
     one reason: at --step-3 the line "Specified. Verified. Delivered." broke
     after "Verified." on a 390px phone and stranded "Delivered." on a line of
     its own, and a three-word strapline that wraps is not a strapline. Floored
     at 19px, which holds the phrase on one line inside a 342px column, and
     capped where --step-3 caps so the desktop end of the scale is unchanged. */
  --step-strap: clamp(1.1875rem, 0.89rem + 1.23vw, 2rem);

  /* Space, on a 4px base. */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;
  --sp-7: 3rem;
  --sp-8: 4rem;
  --sp-9: 6rem;
  --sp-10: 8rem;

  /* Shape. Radius halves when nested, as in the reference system. */
  --br: 1rem;
  --br-sm: 0.5rem;
  --br-xs: 0.25rem;
  --border-width: 1px;
  --hairline: var(--border-width) solid var(--fg-5);
  --hairline-strong: var(--border-width) solid #cfcac2;

  /* Layout. */
  --measure: 68ch;
  --container: 76rem;
  --container-narrow: 54rem;
  --header-h: 4rem;

  /* Motion. Every duration collapses to 0ms under reduced motion, below. */
  --dur-fast: 160ms;
  --dur-base: 320ms;
  --dur-slow: 640ms;
  --dur-slower: 900ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* The signature grid texture, now dark lines on a light canvas. */
  --cell: 100px;
  --grid-line-1: rgba(46, 42, 39, 0.05);
  --grid-line-2: rgba(46, 42, 39, 0.03);

  color-scheme: light;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
    --dur-slower: 0ms;
  }
}

/* h2 and .page-head__title both key off --step-4, and both measured with
   text-wrap: balance already active (confirmed via CSS.supports and computed
   style) yet still wrapped to 4 to 6 ragged lines with a single orphan word
   on a 390px phone: "The document package" split 230px/134px across two
   lines, which is the most even split available across only three words at
   that size, not a balancing failure. There is no wrap outcome that fixes an
   orphan when the container cannot fit the last word beside its neighbour.
   A smaller size buys enough characters per line that short headings settle
   on one line and long ones wrap into fewer, more even ones. */
@media (max-width: 25rem) {
  :root {
    --step-4: clamp(1.375rem, 1rem + 3vw, 1.875rem);
  }
}

/* --------------------------------------------------------------------------
   3. Reset and base
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--sp-5));
}

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

body {
  min-height: 100%;
  background-color: var(--bg-1);
  color: var(--fg-1);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: var(--accent);
  text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
  text-underline-offset: 0.2em;
  transition: color var(--dur-fast) var(--ease-out),
    text-decoration-color var(--dur-fast) var(--ease-out);
}

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

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

::selection {
  background: var(--mg-silver);
  color: var(--fg-1);
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  line-height: 1.12;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-5);
}
h2 {
  font-size: var(--step-4);
}
h3 {
  font-size: var(--step-2);
}
h4 {
  font-size: var(--step-1);
}

p {
  text-wrap: pretty;
  /* The measure floor for the whole site. Any paragraph that needs a
     different width already carries a class that sets its own max-width
     (.lede, .section__lede, .hero__lede, .page-head__lede, .prose, and so
     on), and a class selector always wins over this element selector
     regardless of source order. Without this, a bare <p> next to a
     flex-basis sibling (the pricing note, and the per-product note on every
     product page) stretched to whatever the container happened to be:
     measured at up to 143 characters per line before this rule existed. */
  max-width: var(--measure);
}

sub,
sup {
  font-size: 0.7em;
  line-height: 0;
  position: relative;
}
sub {
  bottom: -0.22em;
}
sup {
  top: -0.45em;
}

hr {
  border: 0;
  border-top: var(--hairline);
  margin: var(--sp-7) 0;
}

/* --------------------------------------------------------------------------
   4. Layout primitives
   -------------------------------------------------------------------------- */

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

@media (min-width: 60rem) {
  .container {
    padding-inline: var(--sp-6);
  }
}

.container--narrow {
  max-width: var(--container-narrow);
}

.section {
  position: relative;
  padding-block: var(--sp-8);
  border-top: var(--hairline);
}

@media (min-width: 60rem) {
  .section {
    padding-block: var(--sp-9);
  }
}

.section--tinted {
  background-color: var(--bg-2);
}

.section--flush {
  border-top: 0;
}

.section__head {
  max-width: var(--container-narrow);
  margin-bottom: var(--sp-7);
}

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

.section__lede {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  color: var(--fg-2);
  max-width: var(--measure);
}

.stack > * + * {
  margin-top: var(--sp-4);
}

.stack-lg > * + * {
  margin-top: var(--sp-6);
}

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

.prose > * + * {
  margin-top: var(--sp-4);
}

.prose h2 {
  font-size: var(--step-3);
  margin-top: var(--sp-7);
  color: var(--fg-1);
}

.prose h3 {
  font-size: var(--step-1);
  margin-top: var(--sp-6);
  color: var(--fg-1);
}

.prose ul,
.prose ol {
  padding-left: var(--sp-5);
}

.prose li + li {
  margin-top: var(--sp-2);
}

.prose strong {
  color: var(--fg-1);
  font-weight: 600;
}

/* The signature grid texture. Decorative only, never carries meaning. */
.grid-bg {
  position: relative;
  isolation: isolate;
}

.grid-bg::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: linear-gradient(-90deg, var(--grid-line-1) 1px, transparent 1px),
    linear-gradient(var(--grid-line-1) 1px, transparent 1px),
    linear-gradient(-90deg, var(--grid-line-2) 1px, transparent 1px),
    linear-gradient(var(--grid-line-2) 1px, transparent 1px);
  background-size: var(--cell) var(--cell), var(--cell) var(--cell),
    calc(var(--cell) / 10) calc(var(--cell) / 10),
    calc(var(--cell) / 10) calc(var(--cell) / 10);
  background-position: center top;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
  backface-visibility: hidden;
}

/* --------------------------------------------------------------------------
   5. Typographic utilities
   -------------------------------------------------------------------------- */

/* Technical values. Tabular figures keep columns aligned without a monospace
   face, which is what made the old design read as a developer tool. */
.mono,
.tabular,
.cell-value,
.datalist__val,
.beat__figure-value,
.dial__panel-value {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1, 'zero' 1;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-display);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.eyebrow::before {
  content: '';
  width: 1.5rem;
  height: 1px;
  background: currentColor;
  flex: none;
}

.eyebrow--amber {
  color: var(--fg-1);
}

.lede {
  font-size: var(--step-1);
  color: var(--fg-2);
  max-width: var(--measure);
}

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

.small {
  font-size: var(--step--1);
}

/* ---------------------------------------------------------------------------
   Reactivity strip.

   WEBMGVANTA 01 P1.2. The homepage argued that reactivity decides performance
   and then sent the reader to a product page for the seconds that prove it.
   This puts the two bands beside the argument. A definition list because that
   is what it is: a term and its value.

   Tokens only. Emphasis comes from scale and weight, not from a hue this
   palette does not have.
   --------------------------------------------------------------------------- */
.reactivity-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-6);
  margin: var(--sp-6) 0 0;
  padding-top: var(--sp-4);
  border-top: var(--hairline);
}

.reactivity-strip > div {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.reactivity-strip dt {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.reactivity-strip dd {
  margin: 0;
  font-size: var(--step-2);
  font-weight: 600;
  line-height: 1.1;
  color: var(--fg-1);
  font-variant-numeric: tabular-nums;
}

.tag {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.04em;
  padding: 0.2em 0.6em;
  border: var(--border-width) solid var(--mg-mid);
  border-radius: var(--br-xs);
  background: var(--bg-3);
  color: var(--accent);
  white-space: nowrap;
}

.tag--amber {
  border-color: var(--mg-mid);
  background: var(--bg-3);
  color: var(--fg-1);
}

.tag--plain {
  border-color: var(--fg-5);
  background: var(--bg-3);
  color: var(--fg-2);
}

/* ---------------------------------------------------------------------------
   Per-product credential band.

   REPLACES .credlist, which is gone. That component rendered the credential
   set as a dot-separated inline run inside .page-head, between the tag row and
   the CTA row, where it read as a caption on the headline. The owner's buying
   sequence is product, then compliance and licence, then specification, so the
   set is now a section of its own, sitting between the identity block and the
   "Typical values" table. src/templates/credband.mjs carries the markup and
   the reasoning for one credential per card rather than one per slide.

   FIELD. Charcoal with Porcelain text, the same pairing as .btn--primary and
   the sitewide .credrail, so the two credential surfaces read as one system.
   --accent and --fg-1 are the same charcoal in this hueless palette, so this
   introduces no colour. It is the only full-bleed dark field on a long
   Porcelain page, and that is what gives it the weight of a stop.

   EVERY LIGHT TONE HERE IS PORCELAIN AT AN ALPHA. No new hex, no hand-mixed
   grey. Ratios computed against the charcoal field #2e2a27:

     rgb(251 248 242 / 1)     13.4:1   card text
     rgb(251 248 242 / 0.78)   8.8:1   lede
     rgb(251 248 242 / 0.72)   7.7:1   eyebrow
     rgb(251 248 242 / 0.6)    5.8:1   card index numeral
     rgb(251 248 242 / 0.38)   3.3:1   control boundary, meets WCAG 1.4.11
     rgb(251 248 242 / 0.26)   2.0:1   card hairline, decorative only

   Note for anyone re-running scripts/browser-verify.mjs: its contrast check
   reads the first three numbers out of a computed colour, so an alpha channel
   is invisible to it and every pair above is reported as the opaque 13.4:1.
   The ratios in that table were computed by hand and are the ones that matter.
   Alpha is used rather than element `opacity` deliberately: the same suite
   treats an opacity below 0.9 as content left faded by a broken animation.

   THREE STATES, and the base state is the one that ships in the markup.

   1. NO SCRIPT. `.js` is never set on <html>, so the track stays a wrapped
      flex list: every credential visible, one per row on a phone, two up from
      34rem, three up from 60rem. The controls carry `hidden` in the markup
      rather than being hidden here, because a control that cannot work must
      not be offered. Nothing in this file hides content that only script could
      bring back.
   2. SCRIPT. `.js` turns the same track into a horizontal scroller. The cards,
      their widths and their order are unchanged; only the wrapping is.
   3. REDUCED MOTION. Identical to state 2. Scrolling a container is not
      animation, so the controls keep working. site.js passes
      `behavior: 'auto'` instead of `'smooth'` so they jump rather than glide,
      and --dur-base is already 0ms under the preference, which stops the
      progress bar animating as well. There is no auto-advance in any state,
      so WCAG 2.2.2 has nothing to apply to.

   MOTION. Native scrolling, never replaced and never hijacked. The only
   animated property in the whole component is the progress bar's `transform`.
   --------------------------------------------------------------------------- */
.credband {
  background-color: var(--accent);
  color: var(--bg-1);
  padding-block: var(--sp-8);
}

@media (min-width: 60rem) {
  .credband {
    padding-block: var(--sp-9);
  }
}

/* The section that follows is .section, which opens with a hairline tuned for
   a light field. Against the bottom edge of a charcoal band that hairline is a
   seam, so the band closes the rule itself rather than the next section having
   to know it is there. */
.credband + .section {
  border-top-width: 0;
}

.credband__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-5);
  margin-bottom: var(--sp-6);
}

/* Only the text carries .reveal. The controls sit outside it, because a
   focusable element inside an entrance animation is a focus ring waiting to be
   clipped by a mid-transform ancestor. */
.credband__intro {
  max-width: 46ch;
}

/* .eyebrow is charcoal, which is this band's own field colour. */
.credband .eyebrow {
  color: rgb(251 248 242 / 0.72);
}

.credband__title {
  margin-top: var(--sp-3);
  /* One step down from the section default. The band has to read as a section,
     not out-shout the H1 four screens above it, and --step-4 set "What this
     grade carries" on three lines inside the intro column at 390px. */
  font-size: var(--step-3);
}

.credband__lede {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  color: rgb(251 248 242 / 0.78);
  max-width: 46ch;
}

/* --- controls ------------------------------------------------------------ */

/* Hidden until script proves there is something to scroll. The `hidden`
   attribute is set in the markup and removed by site.js; this rule is what
   makes `hidden` stick against the display value below. */
.credband__nav {
  display: none;
  flex: none;
  gap: var(--sp-3);
}

.js .credband__nav:not([hidden]) {
  display: flex;
}

.credband__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px, the tap-target floor, on an icon-only control. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: var(--border-width) solid rgb(251 248 242 / 0.38);
  border-radius: 999px;
  background-color: transparent;
  color: var(--bg-1);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.credband__btn:hover {
  background-color: rgb(251 248 242 / 0.12);
  border-color: rgb(251 248 242 / 0.6);
}

/* The site's focus ring is a 2px charcoal outline at a 3px offset. Charcoal is
   this band's field colour, so the ring would be invisible here. Same
   treatment, same weight, same offset, inverted to Porcelain. */
.credband__btn:focus-visible {
  outline: 2px solid var(--bg-1);
  outline-offset: 3px;
}

/* aria-disabled, never the `disabled` attribute: a button that stops being
   focusable underneath the visitor who has just pressed it throws focus to the
   top of the document. The button stays reachable and does nothing. */
.credband__btn[aria-disabled='true'] {
  border-color: rgb(251 248 242 / 0.18);
  color: rgb(251 248 242 / 0.35);
  cursor: default;
}

.credband__btn[aria-disabled='true']:hover {
  background-color: transparent;
  border-color: rgb(251 248 242 / 0.18);
}

/* --- track --------------------------------------------------------------- */

.credband__track {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.credband__item {
  /* A fixed width with no grow. Equal frames are what let a 5 character string
     and a 76 character one sit in the same row without one of them looking
     like a mistake, and they are what makes the scroll step predictable. */
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
  /* Outlined, not filled. A translucent fill on the card would also be the
     computed background behind the card text, and a hairline plus whitespace
     is the house treatment anyway. */
  border: var(--border-width) solid rgb(251 248 242 / 0.26);
  border-radius: var(--br-sm);
}

@media (min-width: 34rem) {
  .credband__item {
    width: calc((100% - var(--sp-4)) / 2);
  }
}

@media (min-width: 60rem) {
  .credband__item {
    width: calc((100% - 2 * var(--sp-4)) / 3);
  }
}

.credband__index {
  font-size: var(--step--2);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
  color: rgb(251 248 242 / 0.6);
}

.credband__text {
  font-size: var(--step-1);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0.005em;
  color: var(--bg-1);
  text-wrap: pretty;
}

/* --- scripted state: the same track, scrolling ---------------------------- */

.js .credband__track {
  flex-wrap: nowrap;
  overflow-x: auto;
  /* Paired with overflow-x so the row cannot also grow a vertical scrollbar.
     Safe to clip: no card contains a focusable element, so there is no focus
     ring in here for the clip to eat. */
  overflow-y: hidden;
  /* proximity, not mandatory. With mandatory snapping and start alignment the
     final scroll position can be un-snappable, which hides the tail of the
     last card. proximity tidies a swipe and never withholds content. */
  scroll-snap-type: x proximity;
  /* The bar would sit inside the charcoal field and change the band's height
     on Windows. Position is carried by the progress rule and by the buttons'
     disabled states instead. */
  scrollbar-width: none;
}

.js .credband__track::-webkit-scrollbar {
  display: none;
}

/* Chrome 124 and Firefox make a scrollable region a tab stop of its own, so a
   keyboard user can pan the track with the arrow keys as well as with the
   buttons. That is the right behaviour and it is left alone, but the ring it
   draws is the site's charcoal one, which is this band's field colour. Same
   inversion as the buttons. Nothing else here depends on it: the buttons reach
   every card on their own, which is the path a browser without that feature
   takes. */
.js .credband__track:focus-visible {
  outline: 2px solid var(--bg-1);
  outline-offset: 3px;
}

.js .credband__item {
  scroll-snap-align: start;
}

/* On a phone the card stops short of the full width so the next one shows at
   the edge. That peek is the affordance: it says the row continues before
   anyone has touched it. Above 34rem two and three up say the same thing by
   themselves. */
@media (max-width: 33.999rem) {
  .js .credband__item {
    width: 84%;
  }
}

/* --- position readout ---------------------------------------------------- */

.credband__progress {
  display: none;
  margin-top: var(--sp-5);
  height: 2px;
  background-color: rgb(251 248 242 / 0.16);
  border-radius: 999px;
  overflow: hidden;
}

.js .credband__progress:not([hidden]) {
  display: block;
}

/* Length and position both come from one transform, so nothing here touches
   width, left or margin. --credband-len is the visible fraction of the track
   and --credband-pos is how far into it the scroller has travelled; site.js
   sets both. The defaults draw a full bar, which is what a track with nothing
   to scroll would show if it were ever visible. */
.credband__progress-bar {
  display: block;
  width: 100%;
  height: 100%;
  background-color: rgb(251 248 242 / 0.7);
  transform-origin: left center;
  transform: translateX(calc(var(--credband-pos, 0) * 100%))
    scaleX(var(--credband-len, 1));
  transition: transform var(--dur-base) var(--ease-out);
}

/* ---------------------------------------------------------------------------
   Credential rail. The sitewide band between the header and <main>.

   The 2026-08-22 verdict on the previous band was that it did not read as
   designed, it read as a promotional bar added on top of a finished page. It
   was: a tinted full-bleed field, centre-aligned while every one of its
   neighbours is left-aligned to the container, wrapping so that the fifth item
   was stranded alone on a second line, and styled from inline attributes in
   the template rather than from this file.

   The fix is structural, not decorative. The rail now sits on the page canvas
   with no fill, holds the container grid, and divides into one cell per
   credential under its own hairline segment. Those segments read across the
   top of the rail as a measured scale and echo the .eyebrow rule that appears
   directly below them at the same left edge, which is what ties the rail to
   the page instead of floating above it.

   Emphasis is weight, rule, position and scale. No hue is introduced: the
   brand permits the neutrals and the two magnesium graphics tones and nothing
   else, so the only colours here are --fg-2 for text and --fg-5 for hairlines.

   Item widths come from flex-basis with flex-grow, never from a fixed track
   count, so the last row always fills the full width whether the page renders
   five credentials or the four that MgO 85 carries. That is what makes an
   orphaned single item on a short row impossible at every width.

   THIS BLOCK IS THE BASE STATE, and it is what renders with scripting off and
   what renders under prefers-reduced-motion: the full list, every credential
   visible, nothing moving. The single-line rotator that a scripted visitor
   sees is layered on top of it further down, under
   `@media (prefers-reduced-motion: no-preference)` and `.js`. Read both.
   --------------------------------------------------------------------------- */
.credrail {
  /* Charcoal field with light text, matching .btn--primary. --accent and
     --fg-1 are the same charcoal in this hueless palette, so this introduces
     no new colour. Porcelain on charcoal measures 13.4:1.

     The border-bottom is dropped: against a dark field it read as a seam. */
  background-color: var(--accent);
  color: var(--bg-1);
  /* Phone first. Measured at 390px, the rail was 214px tall, a quarter of an
     844px screen spent before the page had said anything, and the hero began
     below it. The compact variant below brings it to about 170px. */
  padding-block: var(--sp-3);
}

@media (min-width: 34rem) {
  .credrail {
    padding-block: var(--sp-4);
  }
}

@media (min-width: 60rem) {
  .credrail {
    padding-block: var(--sp-5);
  }
}

.credrail__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-3) var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.credrail__item {
  /* 100% basis: one credential per row at the narrowest widths, where the
     longest string needs the whole measure to stay on two lines. */
  flex: 1 1 100%;
  min-width: 0;
  font-size: var(--step-cred);
  line-height: 1.4;
  /* Medium, not regular. On a dark field a regular weight at this size looks
     thin and provisional; 500 gives the line the presence of a claim being
     made without pushing it toward a second headline. */
  font-weight: 500;
  letter-spacing: 0.01em;
  /* Porcelain on charcoal, 13.4:1. The previous --fg-2 was tuned for a light
     field and is unreadable on this one. */
  color: var(--bg-1);
  text-align: center;
  text-wrap: pretty;
}

/* The hairline segments start at 34rem, and only there.

   Below that the rail is a single stacked column, so every segment spans the
   full width and the set reads as five rules stacked one under another rather
   than as a scale across a row: the thing the rule was there to draw, the
   division of one row into measured cells, does not exist at that width. It
   was costing 5px of rule and padding per item and forcing a row gap wide
   enough to keep each rule attached to the item below it. Dropping it on
   phones takes about 40px out of the fold and loses no meaning, because one
   credential per line is already one credential per line.

   From 34rem the items sit two up, the segments divide a real row, and they
   come back.

   Two up at 40% rather than 50% so the basis plus the column gap can never
   round into a third track, and grow still equalises the pair. An odd item
   left on the final row grows to the full width and reads as a closing line
   rather than as a stranded fragment. */
@media (min-width: 34rem) {
  .credrail__list {
    /* The row gap has to stay clearly larger than the cell's own padding-top,
       or a hairline reads as belonging to the item above it rather than the
       one below. Two to three times is the range that holds. */
    gap: var(--sp-4) var(--sp-5);
  }

  .credrail__item {
    flex-basis: 40%;
    /* --hairline is tuned for a light field and disappears on charcoal. This
       is Porcelain at low alpha, so it is the same colour the text already
       uses rather than a new one. Only visible in the no-JS and
       reduced-motion states, where the five items stack. */
    border-top: 1px solid rgb(251 248 242 / 0.22);
    padding-top: var(--sp-2);
    line-height: 1.5;
  }
}

/* All credentials on one row. A zero basis with equal grow gives equal
   columns for any count, so the row is exactly filled at four items and at
   five, with no empty track and no ragged tail. */
@media (min-width: 64rem) {
  .credrail__item {
    flex-basis: 0;
  }
}

/* ---------------------------------------------------------------------------
   Credential rail, enhanced state: one credential at a time.

   The owner's verdict on the rail above was that it takes the eye before the
   headline does, and that it shows five things at once where one would be
   calmer. A five-cell rail with five hairline segments sitting directly on top
   of an h1 is, fairly, a second headline.

   So a scripted visitor gets one line that changes instead. Not a promo
   carousel: no arrows, no dot controls, no fill, no shadow, no separate panel.
   It is a status line under the masthead. Everything that made the rail loud
   is removed here rather than restyled: the per-cell hairlines go, the band
   drops to about a quarter of its height, and the text moves from --fg-2 down
   to --fg-3, the muted text floor, measured 5.43:1 on Porcelain and therefore
   still AA at this size. What is left cannot compete with a 56px charcoal
   headline at 13.42:1 sitting immediately beneath it.

   TWO GATES, BOTH IN CSS, NEITHER IN SCRIPT.

   `.js` is set on <html> by an inline script in <head>, before any markup is
   parsed. Without scripting the class never appears, this whole block never
   applies, and the base rail above renders untouched. That is the standing
   rule on this site: CSS as authored never hides anything that only script can
   bring back. The state that hides is the state where script is running.

   `prefers-reduced-motion: no-preference` is the second gate. Under reduced
   motion the visitor gets the same full static list as a no-JS visitor, with
   nothing moving and nothing to reveal. Putting that decision in the media
   query rather than in site.js means the correct layout is chosen during the
   first style resolution, so there is no frame in which the wrong one is
   painted, and no branch in script that could get it wrong.

   Because both gates resolve before first paint, the height difference between
   the two states costs nothing: the browser never lays out the tall version
   and then collapses it. Deliberately, no height is reserved for the state
   that is not being shown.

   Layout is a single-cell grid with every item stacked in it. That fixes the
   band height at the tallest credential, so the page below never moves as the
   items cycle. At 390px the longest string sets two lines and the shorter ones
   sit centred in that same box; anything that let the box resize per item
   would shift the whole hero every five seconds, which is a worse defect than
   a little unused space.

   ASSISTIVE TECHNOLOGY GETS ALL OF IT, ALWAYS.

   The items that are not showing are faded with `opacity`, never `display:
   none`, never `visibility: hidden`, never `aria-hidden`. They stay in the
   accessibility tree, so a screen reader reads the list once, in order, with
   its real item count, and moves on. There is no `aria-live` region: a polite
   region on a five-second cycle interrupts a reader every five seconds for as
   long as the page is open, which is hostile. The rotation is presentation and
   is not announced. The cost of that choice is that a magnifier user running a
   screen reader hears four credentials that are not on screen at that instant.
   That is the better half of the trade, because the alternative is either the
   interruptions or hiding four of the five credentials from AT entirely.

   `pointer-events: none` on the faded items keeps the invisible text from
   swallowing the pointer over the band.

   MOTION.

   Opacity and transform only. The two texts do not cross-dissolve through each
   other, which would be an unreadable smear of two different sentences in one
   box: the outgoing line fades out over --dur-base first, and the incoming one
   starts --dur-base later. About 960ms end to end against a five-second dwell,
   so the band changes twelve times a minute at most, far under the three-per-
   second flash threshold, and it is small, low-contrast text rather than a
   large high-contrast area.

   The 4px rise is what makes the change legible as a change rather than as a
   render glitch, since both lines are the same size, colour and position. It
   is small enough not to read as a slide. The outgoing item's transform is
   reset with a zero-duration transition delayed until after its fade, so it
   never appears to travel backwards.

   FAILURE MODE. site.js only moves the `is-current` class. If it never loads,
   `[data-live]` is never set, and `.credrail__item:first-child` below leaves
   the first credential on screen. A broken script costs the rotation, not the
   content.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .js .credrail {
    padding-block: var(--sp-2);
  }

  .js .credrail__list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    gap: 0;
  }

  .js .credrail__item {
    /* Every credential in the same cell. */
    grid-area: 1 / 1;
    border-top: 0;
    padding-top: 0;
    /* --fg-3 was a muted tone for a light field. On charcoal it is close to
       invisible, so the rotating item takes the same Porcelain as the static
       state. */
    color: var(--bg-1);
    opacity: 0;
    transform: translateY(4px);
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease-in-out),
      transform 0s linear var(--dur-base);
  }

  /* The resting default, and the fallback if site.js never runs. */
  .js .credrail__item:first-child {
    opacity: 1;
    transform: none;
  }

  /* Once script has taken over, the class is the only thing that decides. */
  .js .credrail__list[data-live] .credrail__item {
    opacity: 0;
    transform: translateY(4px);
  }

  .js .credrail__list[data-live] .credrail__item.is-current {
    opacity: 1;
    transform: none;
    transition: opacity var(--dur-slow) var(--ease-out) var(--dur-base),
      transform var(--dur-slow) var(--ease-out) var(--dur-base);
  }
}

@media (prefers-reduced-motion: no-preference) and (min-width: 34rem) {
  .js .credrail {
    padding-block: var(--sp-3);
  }
}

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  list-style: none;
  padding: 0;
}

.formula {
  font-family: var(--font-mono);
  white-space: nowrap;
}

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

/* --------------------------------------------------------------------------
   6. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 2.875rem;
  padding: 0.6rem 1.35rem;
  border: var(--border-width) solid transparent;
  border-radius: var(--br-sm);
  font-family: var(--font-display);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.03em;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn:active {
  transform: translateY(1px);
}

.btn--primary {
  background-color: var(--accent);
  color: #ffffff; /* 7.33:1 on teal-1 */
  border-color: var(--accent);
}

.btn--primary:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #ffffff;
}

.btn--ghost {
  background-color: transparent;
  color: var(--fg-1);
  border-color: var(--border-control);
}

.btn--ghost:hover {
  background-color: var(--bg-3);
  border-color: var(--fg-3);
  color: var(--fg-1);
}

.btn--amber {
  background-color: transparent;
  color: var(--fg-1);
  border-color: var(--fg-4);
}

.btn--amber:hover {
  background-color: var(--bg-3);
  border-color: var(--fg-1);
  color: var(--fg-1);
}

.btn--sm {
  /* 2.75rem (44px), not 2.25rem. The modifier is used only on three standalone
     CTAs (header "Request a Quote", and the /contact/ and /documents/ sidebar
     buttons), never inline or icon-only, so it needs the same tap-target
     minimum as the base button rather than a genuinely compact size. */
  min-height: 2.75rem;
  padding: 0.35rem 0.85rem;
  font-size: var(--step--2);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

/* The one place a button can push the document wider than the screen.

   At 360px the container leaves 312px between its gutters. The longest label
   on the site, "How verification and traceability work", set as a single
   nowrap line inside 1.35rem of side padding and a 1px border, measured 318px,
   and the home page reported a 6px horizontal overflow because of it. The
   label is the link's own words and is not the button's to shorten, so the
   button gives way: tighter side padding recovers 11px, which is enough on its
   own, and normal wrapping sits underneath it as the floor for any label
   longer still. The 44px tap target is untouched, since min-height holds and a
   wrapped label only makes the box taller. */
@media (max-width: 25rem) {
  .btn {
    padding-inline: 1rem;
    white-space: normal;
  }
}

.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-decoration: none;
}

.arrow-link::after {
  content: '\2192';
  transition: transform var(--dur-fast) var(--ease-out);
}

.arrow-link:hover::after {
  transform: translateX(0.25rem);
}

/* --------------------------------------------------------------------------
   7. Header and navigation
   -------------------------------------------------------------------------- */

.skip-link {
  position: absolute;
  top: 0;
  left: var(--sp-4);
  z-index: 100;
  transform: translateY(-150%);
  padding: var(--sp-3) var(--sp-4);
  background: var(--accent);
  color: #ffffff;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-decoration: none;
  border-radius: 0 0 var(--br-sm) var(--br-sm);
  transition: transform var(--dur-fast) var(--ease-out);
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: color-mix(in srgb, var(--bg-1) 85%, transparent);
  backdrop-filter: saturate(120%) blur(10px);
  border-bottom: var(--hairline);
}

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

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  /* 44px, not the mark's own 32px. Without this the clickable box was only as
     tall as the glyph, so the most repeated link on the site (header and
     footer, every page) fell 12px short of the WCAG tap-target minimum. */
  min-height: 2.75rem;
  text-decoration: none;
  color: var(--fg-1);
  flex: none;
}

.brand:hover {
  color: var(--fg-1);
}

.brand__mark {
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
}

.brand__word {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 500;
  letter-spacing: 0.005em;
}

/* "MagVanta remains joined as one name" per the brand guide, so the wordmark
   carries no colour split and no weight split. */
.brand__word b {
  font-weight: inherit;
  color: inherit;
}

.nav {
  display: none;
}

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  list-style: none;
}

.nav__link {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-2);
  text-decoration: none;
  padding-block: var(--sp-2);
  position: relative;
}

.nav__link:hover {
  color: var(--fg-1);
}

.nav__link[aria-current='page'] {
  color: var(--accent);
}

.nav__link[aria-current='page']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--accent);
}

.header__actions {
  display: none;
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 2.75rem;
  min-width: 2.75rem;
  padding: 0 var(--sp-3);
  background: transparent;
  border: var(--border-width) solid var(--border-control);
  border-radius: var(--br-sm);
  color: var(--fg-1);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

.nav-toggle__bars {
  display: grid;
  gap: 3px;
  width: 1rem;
}

.nav-toggle__bars span {
  display: block;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
}

.nav-toggle[aria-expanded='true'] .nav-toggle__bars span:nth-child(1) {
  transform: translateY(4.5px) rotate(45deg);
}

.nav-toggle[aria-expanded='true'] .nav-toggle__bars span:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded='true'] .nav-toggle__bars span:nth-child(3) {
  transform: translateY(-4.5px) rotate(-45deg);
}

/* Mobile drawer.
   The `js` class is set by a script in <head>, before any markup exists, so a
   visitor with JavaScript never paints the open drawer. An earlier version put
   that script after the drawer markup and relied on it running before first
   paint; frame capture on a throttled connection showed the fully open menu
   painted for 17 to 46 ms in 5 of 5 cold-cache trials, because the parser can
   yield between the markup and the script when the HTML arrives in separate
   network chunks. Paint-timing APIs cannot observe this, only frames can.

   Without JavaScript the class is never added, the drawer stays in the normal
   document flow, and the navigation remains fully usable. */
.mobile-nav {
  border-bottom: var(--hairline);
  background: var(--bg-2);
}

.js .mobile-nav {
  display: none;
}

.js .mobile-nav.is-open {
  display: block;
}

.mobile-nav[hidden] {
  display: none;
}

.mobile-nav__list {
  list-style: none;
  padding-block: var(--sp-3);
}

.mobile-nav__list a {
  display: block;
  padding: var(--sp-3) 0;
  font-family: var(--font-mono);
  font-size: var(--step-0);
  color: var(--fg-1);
  text-decoration: none;
  border-bottom: var(--hairline);
}

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

.mobile-nav__cta {
  padding-block: var(--sp-5);
  display: grid;
  gap: var(--sp-3);
}

@media (min-width: 64rem) {
  .nav {
    display: block;
  }
  .header__actions {
    display: flex;
    gap: var(--sp-3);
  }
  .nav-toggle {
    display: none;
  }
  .mobile-nav {
    display: none !important;
  }
}

/* --------------------------------------------------------------------------
   8. Hero
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  padding-top: var(--sp-8);
  padding-bottom: var(--sp-8);
  overflow: hidden;
}

@media (min-width: 60rem) {
  .hero {
    padding-top: var(--sp-9);
    padding-bottom: var(--sp-9);
  }
}

.hero__inner {
  display: grid;
  gap: var(--sp-7);
  align-items: center;
}

@media (min-width: 64rem) {
  .hero__inner {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
    gap: var(--sp-8);
  }
}

/* The story-led hero.
   The home page opens on the narrative, so the hero states who this is and what
   is on offer and then gets out of the way. It is single-column and short by
   design: the reader should reach the first beat of the story without
   scrolling past four sections of specification to find it. */
.hero--compact {
  /* 32px, not the 64px the base hero takes. On a 390px phone the header and
     the credential rail have already spent 230px before the hero starts, and
     a 64px lead-in was the difference between the primary button sitting above
     the fold and below it. */
  padding-top: var(--sp-6);
  padding-bottom: var(--sp-6);
}

@media (min-width: 60rem) {
  .hero--compact {
    padding-top: var(--sp-8);
    padding-bottom: var(--sp-6);
  }
}

@media (min-width: 64rem) {
  .hero__inner--stacked {
    grid-template-columns: minmax(0, 46rem);
  }
}

/* The handoff into the narrative. A real anchor, so it works with JavaScript
   off and is reachable from the keyboard. */
.hero__scroll {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  /* 32px, down from 48px. At 1440x900 the cue used to finish 6px past the
     fold, so the one element on the page whose whole job is to say there is
     more below was the element being cut off by the bottom of the screen. */
  margin-top: var(--sp-6);
  color: var(--fg-3);
  font-size: var(--step--1);
  letter-spacing: 0.02em;
  text-decoration: none;
}

.hero__scroll:hover {
  color: var(--fg-1);
}

/* A downward chevron drawn from a rotated square, so it costs no asset. */
.hero__scroll i {
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
}

.js .hero__scroll i {
  animation: scroll-nudge 2.4s var(--ease-out) infinite;
}

@keyframes scroll-nudge {
  0%, 68%, 100% { transform: rotate(45deg) translate(-1px, -1px); }
  80% { transform: rotate(45deg) translate(2px, 2px); }
}

@media (prefers-reduced-motion: reduce) {
  .js .hero__scroll i {
    animation: none;
  }
}

/* The headline runs on --step-hero, not the shared top of the scale. See the
   token for the measurements behind that. Leading is pulled in to 1.06 as
   well: at 56px the inherited 1.12 put 6.7px of extra air under every line,
   which on a three-line headline is a whole line of wasted height and reads as
   slack rather than generous. */
.hero__title {
  margin-top: var(--sp-4);
  font-size: var(--step-hero);
  line-height: 1.06;
  letter-spacing: -0.03em;
}

/* The strapline. It stays inside the h1, where the copy puts it, but it does
   not stay at headline size: at --step-hero it added two more full-width lines
   under an already long sentence and read as part of the headline rather than
   as a close to it. As a block one and a half steps down, in the accent, it
   reads as the strapline it is and costs one line instead of two. */
.hero__title em {
  display: block;
  margin-top: var(--sp-5);
  font-size: var(--step-strap);
  font-style: normal;
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--accent);
}

/* One step down again, and muted, so the four elements above the buttons read
   as a descending sequence: eyebrow, headline, strapline, fact. */
.hero__lede {
  margin-top: var(--sp-5);
  font-size: var(--step-1);
  color: var(--fg-2);
  max-width: 44ch;
  line-height: 1.45;
}

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

.hero__trust {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  list-style: none;
}

.hero__trust li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-2);
}

.hero__trust svg {
  flex: none;
  color: var(--accent);
}

/* The hero diagram: a lattice of nodes that stand in for the supply path.
   Purely decorative, marked aria-hidden in the markup. */
.lattice {
  position: relative;
  aspect-ratio: 1 / 1;
  width: 100%;
  max-width: 30rem;
  margin-inline: auto;
  border: var(--hairline);
  border-radius: var(--br);
  background: linear-gradient(160deg, var(--bg-2), var(--bg-3) 100%);
  overflow: hidden;
}

.lattice svg {
  width: 100%;
  height: 100%;
}

.lattice__caption {
  position: absolute;
  left: var(--sp-4);
  bottom: var(--sp-4);
  right: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
}

/* --------------------------------------------------------------------------
   9. Cards and grids
   -------------------------------------------------------------------------- */

.card-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

/* auto-fit's 1fr stretches a track to fill the row when there are fewer
   items than columns. That is fine at four items or more (the document
   package grid), but the related-applications grid on a product page can
   hold as few as one: MagVanta USP MgO 99 lists a single related
   application, which stretched that lone card to the full 1152px container,
   ballooning its body copy to well over 100 characters per line. Capping the
   CARD, not the track, keeps the grid itself flexible while keeping any card
   that ends up alone in its row at a sane width. Scoped away from the --2
   and --3 modifiers, whose fixed or wider tracks already behave correctly at
   every item count they are actually used with. */
.card-grid:not(.card-grid--2):not(.card-grid--3) > .card {
  max-width: 24rem;
}

.card-grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
}

/* A fixed three-column track at desktop width. auto-fit resolves six cards to
   four plus two and five cards to four plus one, which reads as an accident.
   Three columns gives a deliberate 3x2 for the six-item grids and 3+2 for the
   five-item one. */
@media (min-width: 64rem) {
  .card-grid--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--sp-5);
  background: var(--bg-2);
  box-shadow: 0 1px 2px rgba(27, 26, 25, 0.04);
  border: var(--hairline);
  border-radius: var(--br);
  transition: border-color var(--dur-base) var(--ease-out),
    background-color var(--dur-base) var(--ease-out);
}

.section--tinted .card {
  background: var(--bg-2);
}

.card__title {
  font-size: var(--step-1);
  font-weight: 600;
}

.card__body {
  margin-top: var(--sp-3);
  color: var(--fg-3);
  font-size: var(--step--1);
  line-height: 1.65;
  /* 46ch, matching .beat__body's existing measure. .card sits in .card-grid,
     which is auto-fit with no upper bound on the track: a grid with only one
     or two items (the related-applications list on a product page) stretches
     its lone card to fill the row, and without this the body copy measured
     up to 143 characters per line. Capping the text, rather than the card,
     leaves the card free to fill the row while the copy stays readable. */
  max-width: 46ch;
}

.card__index {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  /* fg-3, not fg-4: this sits on bg-3 inside a tinted section, where fg-4
     measured 2.31:1, the worst pair on the site. */
  color: var(--fg-3);
  letter-spacing: 0.1em;
}

/* A card whose whole area is clickable via a stretched link. The link itself
   is a real anchor, so keyboard and screen-reader behaviour is unchanged. */
.card--link:hover {
  border-color: var(--mg-mid);
  background: var(--bg-3);
}

.card--link:focus-within {
  border-color: var(--accent-hover);
}

.card__link::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.card__link {
  text-decoration: none;
  color: inherit;
}

.card__link:hover {
  color: inherit;
}

.vp-card__icon {
  width: 2rem;
  height: 2rem;
  color: var(--accent);
  margin-bottom: var(--sp-4);
}

/* Product card */
.product-card {
  padding: var(--sp-5);
}

.product-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-bottom: var(--sp-4);
  border-bottom: var(--hairline);
}

.product-card__sku {
  font-family: var(--font-mono);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.product-card__formula {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-1);
  text-align: right;
  flex: none;
}

.product-card__grade {
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.product-card__summary {
  margin-top: var(--sp-3);
  color: var(--fg-2);
  font-size: var(--step--1);
  line-height: 1.65;
  /* Same measure cap as .card__body, and for the same reason. */
  max-width: 46ch;
  /* Three lines' worth, in em so it tracks the element's own font-size. The
     five product cards' summaries run two lines for three grades and three
     lines for two, so the tag row beneath sat at a different height from
     card to card in the same row. This reserves room for the longest one
     regardless of which grade is shorter. */
  min-height: 4.95em;
}

.product-card__foot {
  margin-top: auto;
  padding-top: var(--sp-5);
}

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

/* Numbered step list, used for the QC process and the supply chain. */
.steps {
  counter-reset: step;
  list-style: none;
  display: grid;
  gap: var(--sp-4);
}

@media (min-width: 48rem) {
  .steps--row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .steps--row {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.steps li {
  counter-increment: step;
  position: relative;
  padding: var(--sp-5);
  padding-top: var(--sp-6);
  background: var(--bg-2);
  box-shadow: 0 1px 2px rgba(27, 26, 25, 0.04);
  border: var(--hairline);
  border-radius: var(--br);
}

.section--tinted .steps li {
  background: var(--bg-2);
}

.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  top: var(--sp-4);
  left: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  color: var(--accent);
}

.steps h3 {
  font-size: var(--step-1);
}

.steps p {
  margin-top: var(--sp-2);
  color: var(--fg-3);
  font-size: var(--step--1);
  line-height: 1.65;
}

/* --------------------------------------------------------------------------
   10. Tables
   -------------------------------------------------------------------------- */

.table-wrap {
  border: var(--hairline);
  border-radius: var(--br);
  overflow-x: auto;
  /* background-color, not the background shorthand: the scroll-shadow
     gradients just below are layered on as background-image, and the
     contrast check walks up the ancestor chain looking at backgroundColor,
     so the solid colour needs to stay a real, separate background-color
     rather than folded into a shorthand a later gradient would erase. */
  background-color: var(--bg-2);
  /* A self-hiding cue that the table scrolls horizontally, built from a
     background pair rather than JavaScript: a solid "cover" the width of
     --bg-2 that travels WITH the scrolled content (background-attachment:
     local), and a soft charcoal "shadow" fixed to the box itself
     (background-attachment: scroll). While there is more content to the
     right, the cover sits clear of the box's right edge and the shadow
     shows; scrolled all the way, the cover reaches the edge and hides it.
     On a phone, the specification table's Method column was previously cut
     off with no indication it existed at all: scrollWidth 534px in a 340px
     box, and nothing on screen said so.

     The matching pair for the LEFT edge was tried and removed: the sticky
     parameter column below paints its own opaque background over exactly
     that region, so a left-edge shadow was dead CSS that could never
     actually show through it. The right edge has no sticky column sitting
     over it, so only the right cue is real. */
  background-image: linear-gradient(to left, var(--bg-2) 40%, rgba(255, 255, 255, 0)),
    radial-gradient(farthest-side at 100% 50%, rgba(46, 42, 39, 0.35), rgba(46, 42, 39, 0));
  background-repeat: no-repeat;
  background-size: 2rem 100%, 1.25rem 100%;
  background-position: right, right;
  background-attachment: local, scroll;
  -webkit-overflow-scrolling: touch;
}

.section--tinted .table-wrap {
  background-color: var(--bg-2);
}

/* The parameter name is the reader's anchor while scrolling right to reach
   the value and the method; pinning it keeps every row legible mid-scroll
   instead of turning into an unlabelled number. Inert, and harmless, at any
   width where the table already fits without scrolling. */
.table-wrap th[scope='row'] {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg-2);
}

.table-wrap tbody tr:hover th[scope='row'] {
  /* Matches .table-wrap tbody tr:hover below, so the pinned column tints
     with the rest of the row instead of standing out as unhighlighted. */
  background-color: var(--bg-3);
}

/* The top-left cell needs to stick in both directions at once: top during
   the page's own vertical scroll, matching every other header cell, and left
   during the table's horizontal scroll, matching the row labels beneath it. */
.table-wrap thead th:first-child {
  left: 0;
  z-index: 2;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
}

caption {
  text-align: left;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-3);
}

th,
td {
  padding: var(--sp-3) var(--sp-5);
  text-align: left;
  vertical-align: top;
  border-bottom: var(--border-width) solid var(--fg-5);
}

thead th {
  position: sticky;
  top: 0;
  background: var(--bg-3);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-3);
  white-space: nowrap;
}

.section--tinted thead th {
  background: var(--bg-4);
}

tbody tr:last-child th,
tbody tr:last-child td {
  border-bottom: 0;
}

tbody th {
  font-weight: 400;
  color: var(--fg-1);
}

.cell-value {
  font-family: var(--font-mono);
  color: var(--fg-1);
  white-space: nowrap;
}

.cell-method {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-3);
}

/* --------------------------------------------------------------------------
   11. Bands, notes, callouts
   -------------------------------------------------------------------------- */

.band {
  padding: var(--sp-6);
  border: var(--hairline);
  border-radius: var(--br);
  background: var(--bg-2);
}

@media (min-width: 48rem) {
  .band {
    padding: var(--sp-7);
  }
}

.band--teal {
  border-color: var(--mg-mid);
  background: linear-gradient(160deg, var(--bg-3), var(--bg-2) 78%);
}

.band--amber {
  border-color: var(--mg-mid);
  background: linear-gradient(160deg, var(--bg-3), var(--bg-2) 78%);
}

.note {
  display: flex;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border: var(--hairline);
  border-left: 3px solid var(--fg-4);
  border-radius: var(--br-sm);
  background: var(--bg-3);
  font-size: var(--step--1);
  color: var(--fg-2);
  line-height: 1.65;
}

.note__label {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-1);
  padding-top: 0.15em;
}

.split {
  display: grid;
  gap: var(--sp-6);
}

@media (min-width: 60rem) {
  .split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--sp-8);
    align-items: start;
  }
  .split--sticky > :first-child {
    position: sticky;
    top: calc(var(--header-h) + var(--sp-6));
  }
}

.datalist {
  display: grid;
  gap: 0;
  border-top: var(--hairline);
}

.datalist__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-bottom: var(--hairline);
}

.datalist__key {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.datalist__val {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-1);
  text-align: right;
}

/* --------------------------------------------------------------------------
   12. Breadcrumbs, page head
   -------------------------------------------------------------------------- */

.page-head {
  padding-block: var(--sp-7) var(--sp-7);
  border-bottom: var(--hairline);
}

@media (min-width: 60rem) {
  .page-head {
    padding-block: var(--sp-8);
  }
}

.page-head__title {
  margin-top: var(--sp-4);
  /* step-4, not step-5. These H1s carry the material name for search, so they
     run long. At step-5 the product and application headings wrapped to four
     and five lines. */
  font-size: var(--step-4);
  max-width: 22ch;
}

@media (min-width: 60rem) {
  .page-head__title {
    max-width: 28ch;
  }
}

.page-head__lede {
  margin-top: var(--sp-5);
  font-size: var(--step-2);
  color: var(--fg-2);
  max-width: 52ch;
  line-height: 1.45;
}

.breadcrumb {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-3);
}

.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  list-style: none;
}

.breadcrumb li + li::before {
  content: '/';
  margin-right: var(--sp-2);
  color: var(--fg-4);
}

.breadcrumb a {
  color: var(--fg-3);
  text-decoration: none;
}

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

.breadcrumb [aria-current='page'] {
  color: var(--fg-2);
}

/* --------------------------------------------------------------------------
   13. Forms
   -------------------------------------------------------------------------- */

.form {
  display: grid;
  gap: var(--sp-5);
}

.form__grid {
  display: grid;
  gap: var(--sp-5);
}

@media (min-width: 40rem) {
  .form__grid--2 {
    grid-template-columns: 1fr 1fr;
  }
}

.field {
  display: grid;
  gap: var(--sp-2);
  /* Pack the field's own rows at the start. Without this, a field sharing a
     grid row with a taller neighbour (one carrying a hint line) stretches, and
     its input grows to fill the extra height. */
  align-content: start;
}

.field__label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.field__req {
  color: var(--fg-1);
  margin-left: 0.35em;
}

.field__hint {
  font-size: var(--step--2);
  color: var(--fg-3);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 2.875rem;
  padding: 0.65rem 0.9rem;
  background: var(--bg-2);
  border: var(--border-width) solid var(--border-control);
  border-radius: var(--br-sm);
  color: var(--fg-1);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.5;
  transition: border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.field textarea {
  min-height: 8rem;
  resize: vertical;
}

.field select {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--fg-3) 50%),
    linear-gradient(135deg, var(--fg-3) 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 1.25rem, calc(100% - 0.8rem) 1.25rem;
  background-size: 0.35rem 0.35rem, 0.35rem 0.35rem;
  background-repeat: no-repeat;
  padding-right: 2.5rem;
}

.field input:hover,
.field select:hover,
.field textarea:hover {
  border-color: var(--fg-3);
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
  background: var(--bg-2);
  box-shadow: 0 0 0 3px var(--bg-3);
}

.field input::placeholder,
.field textarea::placeholder {
  /* fg-3, not fg-4. These placeholders carry worked examples, so they have to
     be readable. fg-4 measures 2.6:1 on the field background and fails AA. */
  color: var(--fg-3);
}

.field--error input,
.field--error select,
.field--error textarea {
  border-color: var(--red-1);
}

.field__error {
  font-size: var(--step--2);
  color: var(--red-1);
}

.field__error::before {
  content: '\26A0';
  margin-right: 0.4em;
}

/* The honeypot. Hidden from sight and from assistive technology, but still a
   real field a naive bot will fill in. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}

.checkbox-row input[type='checkbox'] {
  width: 1.15rem;
  height: 1.15rem;
  min-height: 0;
  margin-top: 0.25rem;
  flex: none;
  accent-color: var(--accent);
}

.checkbox-row label {
  font-size: var(--step--1);
  color: var(--fg-2);
  line-height: 1.55;
}

.form__alert {
  padding: var(--sp-4) var(--sp-5);
  border: var(--border-width) solid var(--red-4);
  border-radius: var(--br-sm);
  background: var(--red-6);
  color: var(--fg-1);
  font-size: var(--step--1);
}

.form__alert--ok {
  border-color: var(--mg-mid);
  background: var(--bg-3);
}

.form__alert ul {
  margin-top: var(--sp-2);
  padding-left: var(--sp-5);
}

fieldset {
  border: var(--hairline);
  border-radius: var(--br);
  padding: var(--sp-5);
}

legend {
  padding-inline: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.checkbox-grid {
  display: grid;
  gap: var(--sp-3);
}

@media (min-width: 40rem) {
  .checkbox-grid {
    grid-template-columns: 1fr 1fr;
  }
}

/* --------------------------------------------------------------------------
   14. Call to action band and footer
   -------------------------------------------------------------------------- */

.cta-band {
  border-top: var(--hairline);
  padding-block: var(--sp-8);
  background: var(--bg-3);
}

.cta-band__inner {
  display: grid;
  gap: var(--sp-6);
  align-items: center;
}

@media (min-width: 60rem) {
  .cta-band__inner {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--sp-8);
  }
}

.cta-band h2 {
  font-size: var(--step-3);
}

.cta-band p {
  margin-top: var(--sp-3);
  color: var(--fg-3);
  max-width: 52ch;
}

.site-footer {
  border-top: var(--hairline);
  padding-block: var(--sp-8) var(--sp-6);
  font-size: var(--step--1);
  color: var(--fg-3);
}

.footer__grid {
  display: grid;
  gap: var(--sp-7);
}

@media (min-width: 48rem) {
  .footer__grid {
    grid-template-columns: 1.4fr repeat(3, 1fr);
    gap: var(--sp-6);
  }
}

.footer__title {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-2);
  margin-bottom: var(--sp-4);
}

.footer__list {
  list-style: none;
  display: grid;
  gap: var(--sp-3);
}

.footer__list a {
  color: var(--fg-3);
  text-decoration: none;
}

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

.footer__blurb {
  margin-top: var(--sp-4);
  max-width: 34ch;
  line-height: 1.65;
}

.footer__bottom {
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: var(--hairline);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
  justify-content: space-between;
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-3); /* was fg-4, 2.64:1, on every page */
}

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  list-style: none;
}

.footer__legal a {
  color: var(--fg-3); /* was fg-4, 2.64:1, on every page */
  text-decoration: none;
}

.footer__legal a:hover {
  color: var(--fg-2);
}

/* --------------------------------------------------------------------------
   15. Motion hooks
   --------------------------------------------------------------------------
   These classes are ATTACHMENT POINTS ONLY. They set no hidden state in CSS.
   The hidden start state is applied by JavaScript at runtime, so if the script
   never loads, nothing was ever hidden. See src/static/assets/js/motion.src.js.
   -------------------------------------------------------------------------- */

.reveal,
.reveal-child {
  /* intentionally empty */
}

/* A single decorative sheen used on the hero rule. Guarded below. */
@keyframes hairline-sweep {
  from {
    transform: scaleX(0);
    transform-origin: left;
  }
  to {
    transform: scaleX(1);
    transform-origin: left;
  }
}

.rule-sweep {
  animation: hairline-sweep var(--dur-slower) var(--ease-out) both;
}

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

/* --------------------------------------------------------------------------
   16. Print
   -------------------------------------------------------------------------- */

@media print {
  .site-header,
  .site-footer,
  .cta-band,
  .nav-toggle,
  .mobile-nav,
  .skip-link {
    display: none !important;
  }
  body {
    background: #fff;
    color: #000;
  }
  .card,
  .band,
  .table-wrap {
    border: 1px solid #999;
    background: #fff;
  }
  a::after {
    content: ' (' attr(href) ')';
    font-size: 0.85em;
  }
}

/* --------------------------------------------------------------------------
   17. The process narrative
   --------------------------------------------------------------------------
   A scroll-driven sequence following magnesium from the deposit to a finished
   product, modelled on the persistent circular instrument that anime.js uses on
   its own home page: a fixed dial on one side whose contents change as the
   reader moves through the beats on the other.

   Structure, and why it is built this way:
   - The stage is `position: sticky` inside a tall track. No JavaScript measures
     or injects anything, so there is no scroll-linked layout work, and the
     sequence cannot break at a viewport size nobody tested. This is the same
     approach anime.js's own onscroll-sticky example takes.
   - Every beat is in the document and readable at all times. JavaScript only
     sets `data-stage` on the dial. With no JavaScript the reader gets the whole
     story as a plain sequence of sections and loses nothing but the dial.
   - The accent hue turns over at the beat where responsibility passes from the
     producer to MagVanta. The design carries the compliance boundary rather
     than a disclaimer having to.
   -------------------------------------------------------------------------- */

.process {
  position: relative;
  border-top: var(--hairline);
  background: var(--bg-3);
  overflow: clip;
}

.process__intro {
  padding-block: var(--sp-8) var(--sp-6);
}

.process__track {
  display: grid;
  /* minmax(0, 1fr), not the bare implicit column the base rule shipped with.
     Below 64rem this is a single-column track, but a bare column has no
     constraint on its own max-content width, so a nowrap CTA button with a
     long label ("Request a quote for USP MgO 99 VHR") pushed the track wider
     than its container. The section clips overflow rather than scrolling it,
     so the product photograph's right edge was silently cut on three product
     pages at 390px. minmax(0, 1fr) caps the track at the container's width,
     exactly as the 64rem two-column rule below already does per column. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-6);
}

@media (min-width: 64rem) {
  .process__track {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--sp-8);
    align-items: start;
  }
}

/* The sticky instrument.
   It used to be hidden below 1024px, which meant the entire visual story was
   absent on a phone: the reader got eight paragraphs and nothing to look at.
   It is now shown at every width, as a shorter sticky panel on small screens
   and the full-height column on large ones. */
.process__stage {
  display: block;
  position: sticky;
  top: calc(var(--header-h) - 1px);
  z-index: 2;
  height: min(46vh, 22rem);
  /* Fully opaque and matching the section, because the beats scroll underneath
     it on small screens. A mask-image fade was tried here first and was wrong:
     it made the lower part of the panel translucent, so beat headings showed
     THROUGH the dial rings. The fade is now a separate strip below the panel
     instead, leaving the panel itself completely opaque. */
  background: var(--bg-3);
}

/* The fade strip, below the panel rather than part of it. */
.process__stage::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 100%;
  height: var(--sp-6);
  background: linear-gradient(to bottom, var(--bg-3), rgb(246 241 232 / 0));
  pointer-events: none;
}

@media (min-width: 64rem) {
  .process__stage {
    top: calc(var(--header-h) + var(--sp-6));
    height: min(78vh, 40rem);
    background: none;
  }

  .process__stage::after {
    display: none;
  }
}

.dial {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}

.dial svg {
  width: 100%;
  height: 100%;
  max-height: 100%;
  overflow: visible;
}

/* The hue the whole instrument reads in, swapped per beat. Producer stages run
   amber, because the defining variable upstream is heat. MagVanta's stages run
   teal, the brand primary. */
.dial {
  /* Producer stages read in the material tones. MagVanta's stage reads in
     charcoal, which is the brand's authority tone. Value, not hue: the brand
     forbids unrelated accent colours. */
  --dial-accent: #8b847d;
  --dial-accent-dim: var(--mg-mid);
  --dial-tint: var(--bg-3);
  transition: none;
}

.dial[data-owner='magvanta'] {
  --dial-accent: var(--fg-1);
  --dial-accent-dim: var(--fg-3);
  --dial-tint: var(--mg-silver);
}

.dial__ring {
  fill: none;
  stroke: #d9d4cc;
  stroke-width: 1;
}

/* The outer ring, one segment per beat. Segments already travelled read as
   accent, the rest stay muted. This is the progress scale. */
.dial__seg {
  fill: none;
  stroke: #ddd8d0;
  stroke-width: 4;
  stroke-linecap: butt;
  transition: stroke var(--dur-base) var(--ease-out),
    stroke-width var(--dur-base) var(--ease-out);
}

.dial__seg.is-past {
  stroke: var(--dial-accent);
}

.dial__seg.is-current {
  stroke: var(--dial-accent);
  stroke-width: 7;
}

/* Concentric guide arcs. Structural depth, as on the reference instrument.
   The origin MUST be the dial centre: an SVG element rotates about the user
   space origin by default, which sent these swinging outside the instrument. */
.dial__guide {
  fill: none;
  stroke: #ddd7cc;
  stroke-width: 1.5;
  transform-origin: 240px 240px;
}

/* Same for the ticks and segments, which are also rotated. */
.dial__tick,
.dial__seg {
  transform-origin: 240px 240px;
}

/* The faint dot matrix in the centre field. */
.dial__matrix {
  fill: #ded9d1;
}

/* Retained for the reactivity scene, which draws a curve with this class. */
.dial__arc {
  fill: none;
  stroke: var(--dial-accent);
  stroke-width: 3;
  stroke-linecap: butt;
}

.dial__tick {
  stroke: #cbc6be;
  stroke-width: 1;
  transition: stroke var(--dur-base) var(--ease-out);
}

.dial__tick--hot {
  stroke: var(--dial-accent);
}

/* The readout, beneath the instrument. It is HTML rather than SVG text so it
   uses the real type scale and cannot collide with the scene geometry. */
.dial {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--sp-4);
  place-items: center;
}

/* The ring keeps its aspect ratio, so it must be allowed to shrink to the row
   rather than forcing the grid taller than the sticky panel. It shares row 1
   with the WebGL canvas, which is what keeps the two concentric. */
.dial > svg {
  grid-area: 1 / 1;
  place-self: center;
  max-height: 100%;
  max-width: 100%;
}


.dial__panel {
  display: grid;
  justify-items: center;
  gap: var(--sp-2);
  text-align: center;
  /* Roomier than it was. The readout is the one place a number is quoted at
     display size, and it was sitting tight against its own border. */
  padding: var(--sp-5) var(--sp-6);
  border: var(--border-width) solid var(--fg-5);
  border-radius: var(--br-sm);
  background: var(--bg-2);
  min-width: 12rem;
}

/* On small screens the readout panel is dropped entirely. It reports the same
   figure the beat card directly beneath it already states at full size, and
   keeping it overflowed the sticky panel so it sat on top of the beat title.
   This must come AFTER .dial__panel's own display rule: a media query adds no
   specificity, so an earlier override would simply lose to it. */
@media (max-width: 63.999rem) {
  .dial__panel {
    display: none;
  }
}

.dial__scrub {
  display: flex;
  gap: 3px;
  margin-bottom: var(--sp-2);
}

.dial__scrub-cell {
  width: 14px;
  height: 4px;
  border-radius: 1px;
  background: #ddd8d0;
  transition: background-color var(--dur-base) var(--ease-out);
}

.dial__scrub-cell.is-past {
  background: var(--dial-accent);
}

.dial__scrub-cell.is-current {
  background: var(--dial-accent);
  box-shadow: 0 0 0 2px var(--dial-tint);
}

.dial__panel-label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.16em;
  /* NO text-transform, for the same reason the formula labels further down
     refuse it. This slot now carries product names, and "MgVanta" has load
     bearing internal casing: the company is MagVanta and a product is
     MgVanta, a rule scripts/verify.mjs fails the build over. Uppercasing it
     to MGVANTA collapses the one letter that carries the distinction. The
     other values that land here, "Stage 01", "Start", "The fork", read
     correctly in sentence case with the mono face and the tracking. */
  color: var(--fg-3);
}

.dial__panel-value {
  font-family: var(--font-mono);
  font-size: var(--step-3);
  font-weight: 600;
  line-height: 1.1;
  color: var(--dial-accent);
  transition: color var(--dur-base) var(--ease-out);
}

.dial__panel-unit {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-3);
}

/* Each beat owns a group inside the dial. Only the active one is shown. */
.dial__scene {
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
}

.dial__scene.is-active {
  opacity: 1;
}

/* Under reduced motion the dial still switches scenes, it just does not tween:
   the durations above collapse to 0ms via the token block. Nothing is lost. */

.dial__particle {
  fill: var(--dial-accent);
}

.dial__particle--open {
  fill: none;
  stroke: var(--dial-accent);
  stroke-width: 1.6;
}

.dial__gas {
  fill: none;
  stroke: var(--fg-4);
  stroke-width: 1.2;
  stroke-dasharray: 3 4;
}

/* The beats column. */
.process__beats {
  display: grid;
  gap: var(--sp-6);
}

@media (min-width: 64rem) {
  .process__beats {
    gap: 0;
  }
}

.beat {
  position: relative;
  padding: var(--sp-6);
  border: var(--hairline);
  border-radius: var(--br);
  background: var(--bg-2);
}

@media (min-width: 64rem) {
  .beat {
    border: 0;
    border-left: 2px solid var(--fg-5);
    border-radius: 0;
    background: transparent;
    padding: var(--sp-8) 0 var(--sp-8) var(--sp-6);
    min-height: 60vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    transition: border-color var(--dur-base) var(--ease-out);
  }

  .beat.is-active {
    border-left-color: var(--beat-accent, var(--fg-1));
  }
}

.beat[data-owner='magvanta'] {
  --beat-accent: var(--fg-1);
  --beat-text: var(--fg-1);
}

.beat[data-owner='producer'] {
  /* Graphics tone for the rule, text tone for the tag. #8b847d measures
     3.28:1 on the ivory field, which is legal for a border and fails for
     small text. */
  --beat-accent: #8b847d;
  --beat-text: var(--fg-3); /* 5.12:1 on ivory */
}

.beat[data-owner='customer'] {
  --beat-accent: var(--fg-3);
  --beat-text: var(--fg-3);
}

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

.beat__index {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.14em;
  color: var(--fg-3);
}

/* Who performs this stage. This is not decoration: it is the compliance
   boundary, stated on every single beat so it cannot be missed. */
.beat__owner {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.2em 0.6em;
  border-radius: var(--br-xs);
  border: var(--border-width) solid var(--beat-accent, var(--fg-4));
  color: var(--beat-text, var(--fg-3));
}

.beat__title {
  margin-top: var(--sp-4);
  font-size: var(--step-3);
}

.beat__body {
  margin-top: var(--sp-4);
  color: var(--fg-2);
  max-width: 46ch;
  line-height: 1.65;
}

.beat__figure {
  margin-top: var(--sp-5);
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.beat__figure-value {
  font-family: var(--font-mono);
  font-size: var(--step-3);
  font-weight: 600;
  color: var(--beat-accent, var(--fg-1));
}

.beat__figure-label {
  font-size: var(--step--1);
  color: var(--fg-3);
  max-width: 34ch;
}

.beat__equation {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: var(--hairline);
  border-radius: var(--br-sm);
  background: var(--bg-3);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-1);
  overflow-x: auto;
}

/* The handover marker, between the last producer beat and the first MagVanta
   beat. */
.process__handover {
  margin-block: var(--sp-6);
  padding: var(--sp-5) var(--sp-6);
  border: var(--border-width) solid var(--mg-mid);
  border-radius: var(--br);
  background: linear-gradient(120deg, var(--bg-3), var(--bg-2) 78%);
}

.process__handover p {
  color: var(--fg-2);
  max-width: 52ch;
  margin-top: var(--sp-2);
}

/* On mobile the dial is dropped entirely rather than shrunk to illegibility.
   The beats carry the whole story on their own. */
@media (max-width: 63.999rem) {
  .beat {
    min-height: 0;
  }
}

/* --------------------------------------------------------------------------
   18. THE CELL: the 3D object that carries the process narrative
   --------------------------------------------------------------------------
   CSS 3D transforms, no WebGL. See src/templates/cell.mjs for the reasoning.

   Hard rules for anything added here:
   - transform and opacity only, plus the registered custom properties below.
     Never top, left, width, height, filter or box-shadow.
   - Nothing inside is content. The whole figure is aria-hidden and it depicts
     only what the beats already say in words.
   -------------------------------------------------------------------------- */

/* Registered so the shear interpolates on the compositor rather than being set
   per frame on the main thread. Driving all three to zero turns the
   rhombohedron into a cube, which IS the carbonate to rock-salt transition. */
@property --lat { syntax: '<number>'; initial-value: 0.34; inherits: true; }
@property --sh-a { syntax: '<number>'; initial-value: 0.26; inherits: false; }
@property --sh-b { syntax: '<number>'; initial-value: 0.12; inherits: false; }
@property --sh-c { syntax: '<number>'; initial-value: 0.20; inherits: false; }
@property --co3-lift { syntax: '<length>'; initial-value: 0px; inherits: true; }
@property --co3-op { syntax: '<number>'; initial-value: 1; inherits: true; }

.cell-stage {
  --L: clamp(104px, 13vw, 158px);
  --rx: -22deg;
  --ry: 34deg;
  --rz: 0deg;
  --s: 1;
  --sh-a: 0.26;
  --sh-b: 0.12;
  --sh-c: 0.20;
  --cell-line: rgb(124 117 112 / 0.42); /* Fume Gray */
  --cell-fill: rgb(216 221 220 / 0.10); /* Magnesium Silver */
  --lat: 0.34;
  --sh-a: 0.26;
  --cell-node: #9aa3a2;

  position: relative;
  display: grid;
  place-items: center;
  min-height: min(58vh, 26rem);
  perspective: 1400px;
  perspective-origin: 50% 44%;
}

/* MagVanta's own stage. Value, not hue: the brand forbids accent colours, so
   the handover reads as the material stepping from silver to charcoal. */
.cell-stage[data-owner='magvanta'] {
  --cell-line: rgb(46 42 39 / 0.75);
  --cell-node: #2e2a27;
}

.cell-3d {
  position: relative;
  width: var(--L);
  height: var(--L);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx)) rotateY(var(--ry)) rotateZ(var(--rz)) scale(var(--s));
}

.cell__lattice,
.cell__twin {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* The cell outline carries the shear. Interaxial angles near 74 and 106 degrees
   read as a calcite-type cleavage rhomb; driving the shear to zero is the
   carbonate to rock-salt transition. */
.cell__cage {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: matrix3d(1, 0, 0, 0, var(--sh-a), 1, 0, 0, var(--sh-b), var(--sh-c), 1, 0, 0, 0, 0, 1);
}

.cell__twin {
  opacity: 0;
  transform: translateX(calc(var(--L) * 0.95)) scale(0.62);
}

/* The cell outline: twelve edges, no shear. Magnesium oxide crystallises in the
   cubic rock-salt system, so a cube is the correct cell. The earlier sheared
   box read as a construction error rather than as a rhombohedral precursor, and
   the carbonate stage now makes its point through the carbonate groups and the
   lattice spacing instead, which is legible where a shear was not.

   --edge is tied to the lattice parameter, so the cell breathes with the
   structure rather than staying a fixed container. */
.cell__cage {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  --edge: calc(var(--L) * var(--lat) * 2.6);
}

.cell__edge {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--edge);
  height: 1px;
  margin-left: calc(var(--edge) / -2);
  margin-top: -0.5px;
  background: var(--cell-line);
  transition: background-color var(--dur-base) var(--ease-out);
}

.cell__edge--x {
  transform: translate3d(
    0,
    calc(var(--y) * var(--edge) / 2),
    calc(var(--z) * var(--edge) / 2)
  );
}

.cell__edge--y {
  transform: translate3d(
      calc(var(--x) * var(--edge) / 2),
      0,
      calc(var(--z) * var(--edge) / 2)
    )
    rotateZ(90deg);
}

.cell__edge--z {
  transform: translate3d(
      calc(var(--x) * var(--edge) / 2),
      calc(var(--y) * var(--edge) / 2),
      0
    )
    rotateY(90deg);
}

/* The ion sites. Spacing is driven by --lat so a stage can compress or expand
   the lattice parameter, which is what actually changes between these phases. */
.cell__ion {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 19px;
  height: 19px;
  margin: -9.5px 0 0 -9.5px;
  border-radius: 50%;
  background: var(--cell-node);
  box-shadow: inset -2px -2px 4px rgb(46 42 39 / 0.28),
    inset 2px 2px 4px rgb(255 255 255 / 0.55);
  /* The shear is applied to the POSITION, not to the element, so an ion stays
     a sphere instead of being squashed into a sliver by the parent matrix. */
  /* Translate into the rotated lattice space, then counter-rotate so the disc
     always faces the camera. Without this the ions are flat quads being
     foreshortened by the parent rotation, and they render as slivers rather
     than as atoms. The inverse is applied in reverse order to the parent's
     rotateX then rotateY. */
  transform: translate3d(
      calc(var(--x) * var(--L) * var(--lat)),
      calc(var(--y) * var(--L) * var(--lat)),
      calc(var(--z) * var(--L) * var(--lat))
    )
    rotateY(calc(-1 * var(--ry, 0deg)))
    rotateX(calc(-1 * var(--rx, 0deg)));
  transition: background-color var(--dur-base) var(--ease-out);
}

/* The oxygen sites: smaller and open, the conventional way the structure is
   drawn so the alternation is legible. */
.cell__ion--o {
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  background: transparent;
  border: 1.5px solid var(--cell-line);
  box-shadow: none;
}

/* Carbonate groups. Drawn open, because this is the part that leaves. */
.cell__co3 {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  margin: -7px 0 0 -8px;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 13px solid rgb(124 117 112 / 0.8);
  opacity: var(--co3-op, 1);
  transform: translate3d(
      calc(var(--x) * var(--L) * var(--lat) * 1.05),
      var(--co3-lift, 0px),
      calc(var(--z) * var(--L) * var(--lat) * 1.05)
    )
    rotateY(calc(-1 * var(--ry, 0deg)))
    rotateX(calc(-1 * var(--rx, 0deg)));
}

/* Crystallites, present from the sinter onward. */
.cell__grain {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--L) * var(--gs));
  height: calc(var(--L) * var(--gs));
  margin: calc(var(--L) * var(--gs) / -2) 0 0 calc(var(--L) * var(--gs) / -2);
  background: rgb(174 181 180 / 0.42);
  border: 1px solid rgb(124 117 112 / 0.7);
  opacity: 0;
  transform: translate3d(
      calc(var(--x) * var(--L) * 0.72),
      calc(var(--y) * var(--L) * 0.72),
      calc(var(--z) * var(--L) * 0.72)
    )
    rotateY(var(--ga, 0deg)) rotateX(var(--gb, 0deg));
}

/* Silica inclusions, ejected by the sorter. */
.cell__incl {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  background: transparent;
  border: 1.5px solid #8b847d;
  opacity: 0;
  transform: translate3d(
    calc(var(--x) * var(--L)),
    calc(var(--y) * var(--L)),
    0
  );
}

/* The sensor plane that sweeps through on Z at stage 3. */
.cell__sensor {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--L) * 1.9);
  height: calc(var(--L) * 1.9);
  margin: calc(var(--L) * -0.95) 0 0 calc(var(--L) * -0.95);
  background: linear-gradient(
    180deg,
    transparent,
    rgb(46 42 39 / 0.07) 45%,
    rgb(46 42 39 / 0.07) 55%,
    transparent
  );
  border-top: 1px solid rgb(46 42 39 / 0.35);
  border-bottom: 1px solid rgb(46 42 39 / 0.35);
  opacity: 0;
  transform: translateZ(calc(var(--L) * -1.4));
}

/* The certificate at stage 7, folded from the cell. */
.cell__doc {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--L) * 0.86);
  height: calc(var(--L) * 1.16);
  margin: calc(var(--L) * -0.58) 0 0 calc(var(--L) * -0.43);
  background: #ffffff;
  border: 1px solid rgb(46 42 39 / 0.5);
  opacity: 0;
  transform: translateZ(1px);
  display: grid;
  align-content: center;
  gap: 9%;
  padding: 14%;
}

.cell__bar {
  display: block;
  height: 4px;
  width: 0%;
  background: #2e2a27;
  opacity: 0.85;
}

/* Mobile drops the object entirely rather than shrinking it to illegibility.
   The beats carry the story on their own, exactly as with the dial. */
@media (max-width: 63.999rem) {
  .cell-stage {
    display: none;
  }
}

/* Reduced motion keeps the object, still, in its first legible state. Nothing
   is lost: it is decoration and it is aria-hidden. */
@media (prefers-reduced-motion: reduce) {
  .cell-3d {
    transition: none !important;
  }
}

/* The instrument: the SVG dial provides the frame, the 3D cell sits inside it. */
.process__instrument {
  position: relative;
  display: grid;
  place-items: center;
  height: 100%;
}

.process__instrument .dial {
  position: absolute;
  inset: 0;
}

.process__instrument .cell-stage {
  position: relative;
  z-index: 1;
  min-height: 0;
  /* Sit in the dial's centre field, above the readout panel. */
  margin-bottom: 22%;
}

/* Product photography. Contained deliberately: the still lifes are shot on the
   Porcelain canvas colour, so they sit flush on the page rather than as a bare
   rectangle floating on it. */
.product-shot {
  margin: 0;
  max-width: 44rem;
}

.product-shot img {
  width: 100%;
  height: auto;
  border: var(--hairline);
  border-radius: var(--br);
  background: var(--bg-2);
}

.product-shot figcaption {
  margin-top: var(--sp-3);
  font-size: var(--step--1);
  color: var(--fg-3);
}

/* The product photograph on a card. A buyer choosing between eight grades is
   choosing partly on appearance: coarse granule against fine powder is a real
   distinction and the photograph makes it in a glance. */
.product-card__shot {
  /* The base reset caps images at 100% of the container, which defeats the
     full-bleed calc below. */
  max-width: none;
  width: calc(100% + var(--sp-5) * 2);
  margin: calc(var(--sp-5) * -1) calc(var(--sp-5) * -1) var(--sp-4);
  height: 9.5rem;
  object-fit: cover;
  border-radius: var(--br) var(--br) 0 0;
  background: var(--bg-3);
  transition: transform var(--dur-base) var(--ease-out);
}

.card--link:hover .product-card__shot {
  transform: scale(1.03);
}

/* The card clips the photograph's corners. */
.product-card {
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   20. Pinned reveal
   --------------------------------------------------------------------------
   The pattern the GTA VI site uses: media held pinned while content travels
   over it, and each block revealed by a wipe rather than a fade. Studied on the
   live site, which runs 12 pinned elements and 6 clip-path reveals.

   Applied here to the beat column: the instrument is already pinned, so each
   beat wipes up over it as it enters. The reveal is driven by a class, and the
   resting state is fully visible, so a beat is readable whether or not the
   script ever runs.
   -------------------------------------------------------------------------- */

@media (min-width: 64rem) {
  .beat__title,
  .beat__body,
  .beat__figure {
    /* The wipe. clip-path is compositor-friendly and, unlike opacity, leaves
       the text selectable and findable the whole time. */
    clip-path: inset(0 0 0 0);
  }

  .js .beat.is-entering .beat__title,
  .js .beat.is-entering .beat__body,
  .js .beat.is-entering .beat__figure {
    animation: beat-wipe 720ms var(--ease-out) both;
  }

  .js .beat.is-entering .beat__body {
    animation-delay: 90ms;
  }

  .js .beat.is-entering .beat__figure {
    animation-delay: 170ms;
  }
}

@keyframes beat-wipe {
  from {
    clip-path: inset(0 0 100% 0);
    transform: translateY(10px);
  }
  to {
    clip-path: inset(0 0 0 0);
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .js .beat.is-entering .beat__title,
  .js .beat.is-entering .beat__body,
  .js .beat.is-entering .beat__figure {
    animation: none;
  }
}

/* The WebGL crystal. Sits in the centre of the instrument, above the dial
   furniture and below the readout. Hidden until the renderer reports success,
   so a machine without WebGL simply keeps the CSS fallback. */
/* The canvas shares the dial's first grid row with the ring SVG, so the two are
   concentric by construction. Squaring it and matching the row height means it
   tracks the ring at any size. The earlier version was inset against the whole
   instrument, which pushed its centre below the ring's centre. */
.crystal {
  grid-area: 1 / 1;
  place-self: center;
  aspect-ratio: 1;
  /* Slightly inside the ring, so the figure is framed by it rather than
     touching it. The renderer derives its camera distance from this box, so
     this single number controls how large the body reads. */
  height: 88%;
  max-width: 100%;
  z-index: 1;
  /* Pointer events are ENABLED: the figure can be dragged. */
  cursor: grab;
  touch-action: pan-y;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}

.crystal.is-dragging {
  cursor: grabbing;
}

/* Without a pointer there is nothing to grab, so do not advertise it. */
@media (hover: none) {
  .crystal {
    cursor: default;
  }
}

.crystal.is-live {
  opacity: 1;
}

.crystal canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The CSS cell is the fallback and steps aside once WebGL is running. */
.cell-stage.is-fallback-hidden {
  opacity: 0;
  visibility: hidden;
}

/* --------------------------------------------------------------------------
   20. Interaction states

   The site already had hover states, but every one of them changed only
   colour, so nothing on the page ever moved under the cursor and the whole
   thing read as inert. These add motion.

   Rules this block obeys:
     - Only `transform`, `opacity` and `box-shadow` are animated. Those are the
       compositor-friendly properties; animating `top` or `height` here would
       cost a layout on every frame.
     - Every effect is reversible and is disabled wholesale under
       prefers-reduced-motion at the end of this block.
     - No accent hues, per the brand. Emphasis is by movement, depth and value.
   -------------------------------------------------------------------------- */

/* Buttons lift toward the cursor and press in when clicked, so a click feels
   like it landed rather than merely recolouring. */
.btn {
  transition:
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(27, 26, 25, 0.13);
}

.btn:active {
  transform: translateY(0);
  box-shadow: 0 1px 3px rgba(27, 26, 25, 0.14);
}

/* A trailing arrow that only exists on hover, so the primary action reads as
   forward motion without carrying permanent decoration. */
.btn--primary::after {
  content: '\2192';
  display: inline-block;
  max-width: 0;
  margin-left: 0;
  opacity: 0;
  overflow: hidden;
  transform: translateX(-0.25rem);
  transition:
    max-width var(--dur-fast) var(--ease-out),
    margin-left var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn--primary:hover::after,
.btn--primary:focus-visible::after {
  max-width: 1.5rem;
  margin-left: var(--sp-2);
  opacity: 1;
  transform: translateX(0);
}

/* Cards rise off the page. The lift is what makes a grid of specifications
   feel like a set of things you can pick up rather than a table. */
.card {
  transition:
    transform var(--dur-mid) var(--ease-out),
    box-shadow var(--dur-mid) var(--ease-out),
    border-color var(--dur-mid) var(--ease-out),
    background-color var(--dur-mid) var(--ease-out);
}

.card--link:hover,
.card--link:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 14px 30px rgba(27, 26, 25, 0.1);
}

/* The index number on a card fades up as the card is approached. */
.card__index {
  transition:
    color var(--dur-mid) var(--ease-out),
    transform var(--dur-mid) var(--ease-out);
}

.card--link:hover .card__index {
  color: var(--fg-2);
  transform: translateX(2px);
}

/* Product cards gain an explicit affordance on hover. The SKU alone did not
   tell a first-time reader the card was clickable. */
/* NO position here. It previously made this heading the containing block for
   .card__link::after, which is the invisible layer that makes the WHOLE card
   clickable, so the click target collapsed to just the heading. The underline
   is drawn with a background gradient instead, which needs no positioning. */
.product-card__sku .card__link {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--dur-mid) var(--ease-out);
}

.card--link:hover .product-card__sku .card__link,
.card--link:focus-within .product-card__sku .card__link {
  background-size: 100% 1px;
}

/* Specification rows highlight under the cursor, so a long table can be read
   across without losing the line. This is the single most useful interaction
   on a product page and it costs nothing. */
.table-wrap tbody tr {
  transition: background-color var(--dur-fast) var(--ease-out);
}

.table-wrap tbody tr:hover {
  background-color: var(--bg-3);
}

/* Steps and beats acknowledge the cursor without shouting. */
.steps li {
  transition:
    transform var(--dur-mid) var(--ease-out),
    border-color var(--dur-mid) var(--ease-out);
}

.steps li:hover {
  transform: translateX(3px);
  border-color: var(--mg-mid);
}

/* Navigation: an underline that grows from the left rather than appearing. */
.nav__link {
  position: relative;
}

.nav__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.35rem;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-mid) var(--ease-out);
}

.nav__link:hover::after,
.nav__link[aria-current='page']::after {
  transform: scaleX(1);
}

/* Reduced motion: keep every state change, drop every movement. A reader who
   has asked for less motion still needs to know what is interactive, so the
   colour and shadow cues stay and only the transforms go. */
@media (prefers-reduced-motion: reduce) {
  .btn,
  .card,
  .card__index,
  .steps li {
    transition-property: background-color, border-color, color, box-shadow;
  }

  .btn:hover,
  .btn:active,
  .card--link:hover,
  .card--link:focus-within,
  .card--link:hover .card__index,
  .steps li:hover {
    transform: none;
  }

  .btn--primary::after,
  .btn--primary:hover::after,
  .btn--primary:focus-visible::after,
  .product-card__sku .card__link::before,
  .nav__link::after {
    transition: none;
  }

  .nav__link:hover::after,
  .nav__link[aria-current='page']::after {
    transform: scaleX(1);
  }
}

/* --------------------------------------------------------------------------
   21. The grade story: a lens, not a dial

   Product pages deliberately do NOT reuse the home page's circular instrument.
   The subject here is the real photograph of the powder, magnified beat by
   beat, with a graticule that appears when a beat quotes a measured value.

   Everything is driven by two custom properties set by the motion layer,
   --lens-z and --lens-focus, so the animation is one compositor-friendly
   transform and no layout is recalculated while scrolling.
   -------------------------------------------------------------------------- */

.grade-story {
  position: relative;
  border-top: var(--hairline);
  background: var(--bg-3);
  overflow: clip;
}

.lens {
  --lens-z: 1;
  --lens-focus: 50% 50%;
  position: relative;
  margin: 0;
  display: grid;
  gap: var(--sp-4);
}

/* The window. Fixed aspect, so magnifying never changes the page's layout. */
.lens__frame {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--br-sm);
  border: var(--border-width) solid var(--fg-5);
  background: var(--bg-2);
}

.lens__shot {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(var(--lens-z));
  transform-origin: var(--lens-focus);
  /* Slower than the 900ms it used to run, and on a symmetric curve rather than
     an out-curve. The old timing snapped most of the way in the first third,
     which reads as a jump; with the larger zoom range now in storyline.mjs
     that was the difference between a camera move and a lurch. Both properties
     share one duration so the push and the reframe stay locked together. */
  transition:
    transform 1500ms var(--ease-in-out),
    transform-origin 1500ms var(--ease-in-out);
  will-change: transform;
}

/* The reactivity comparison. Only shown on the beat that argues about speed. */
.lens__meter {
  position: absolute;
  /* TOP, not bottom. The powder settles into a heap in the lower third of the
     frame, and a panel docked to the bottom sat right on top of it. */
  left: var(--sp-4);
  right: var(--sp-4);
  top: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--br-sm);
  border: var(--border-width) solid var(--fg-5);
  background: rgb(251 248 242 / 0.94);
  opacity: 0;
  transform: translateY(-8px);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.lens.is-meter .lens__meter {
  opacity: 1;
  transform: translateY(0);
}

.lens__meter-title {
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
  margin: 0 0 var(--sp-3);
}

.lens__meter-row {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
  font-size: var(--step--1);
}

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

.lens__meter-track {
  height: 6px;
  border-radius: 3px;
  background: #e6e0d8;
  overflow: hidden;
}

/* The bar grows from the fast edge of the range to the slow edge, so the
   width itself shows the tolerance rather than hiding it behind one number. */
.lens__meter-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--fg-2);
  width: var(--from);
  transform-origin: left center;
  transition: width 700ms var(--ease-out);
}

.lens.is-meter .lens__meter-fill {
  width: var(--to);
}

.lens__meter-row--muted .lens__meter-fill {
  background: var(--mg-mid);
}

.lens__meter-val {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.lens__meter-note {
  margin: var(--sp-2) 0 0;
  font-size: var(--step--2);
  color: var(--fg-3);
}

.lens__caption {
  font-size: var(--step--1);
  color: var(--fg-3);
}

/* With JavaScript running, exactly one card is shown at a time. Without it,
   the rule below never applies and every card stays visible, so a reader with
   no JavaScript sees all the figures rather than none. */
.js .lens__card {
  display: none;
}

.js .lens__card.is-current {
  display: block;
  animation: lens-card-in var(--dur-slow) var(--ease-out) both;
}

:where(html:not(.js)) .lens__card {
  display: block;
  margin-top: var(--sp-3);
}

@keyframes lens-card-in {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lens__shot,
  .lens__grid,
  .lens__meter,
  .lens__meter-fill {
    transition: none;
  }

  .js .lens__card.is-current {
    animation: none;
  }

  /* Magnification is the whole point, so it is kept, but it snaps rather than
     animating. A reader who asked for less motion still gets the information. */
}

/* Only once the canvas is actually running does the photograph step back. */
.lens.has-kiln .lens__shot {
  opacity: 0;
  transition:
    opacity 700ms var(--ease-out),
    transform 900ms var(--ease-out),
    transform-origin 900ms var(--ease-out);
}

.lens.has-kiln.is-photo .lens__shot {
  opacity: 1;
}

/* The canvas fades out as the photograph arrives, so the settled powder hands
   over to the real material rather than sitting behind it. */
.lens.has-kiln .kiln__canvas {
  opacity: 1;
  transition: opacity 700ms var(--ease-out);
}

.lens.has-kiln.is-photo .kiln__canvas {
  opacity: 0;
}

/* The frame is dark-free and warm while the kiln runs, so the fire reads. */
.lens.has-kiln .lens__frame {
  background: var(--bg-3);
}

@media (prefers-reduced-motion: reduce) {
  .lens.has-kiln .lens__shot,
  .lens.has-kiln .kiln__canvas {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   23. Specification callouts

   A technical drawing laid over the product photograph. An anchor ring sits on
   a point in the powder, a hairline leader runs to the frame edge, and a label
   states one real figure.

   This replaced a particle simulation that rebuilt the photo pixel by pixel.
   Mid transition that read as a low-resolution image loading, which looks like
   a fault rather than like craft. Annotating the photograph is more polished
   and, unlike particles, every element carries information.

   Only opacity, transform and clip are animated.
   -------------------------------------------------------------------------- */

.callout {
  position: absolute;
  top: var(--cy);
  display: flex;
  align-items: center;
  gap: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-mid) var(--ease-out);
}

/* Anchored left of centre: the leader runs left to the frame edge. */
.callout--left {
  left: var(--sp-3);
  right: calc(100% - var(--cx));
  flex-direction: row-reverse;
}

/* Anchored right of centre: the leader runs right. */
.callout--right {
  left: var(--cx);
  right: var(--sp-3);
}

.js .callout.is-current {
  opacity: 1;
}

/* Without JavaScript every callout is shown, so no figure is ever lost. */
:where(html:not(.js)) .callout {
  opacity: 1;
}

/* The anchor: a small ring on the material, not a filled dot, so it reads as a
   measurement point rather than a marker pin. */
.callout__dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--fg-1);
  background: rgb(251 248 242 / 0.55);
  transform: scale(0.4);
  transition: transform var(--dur-mid) var(--ease-out);
}

.js .callout.is-current .callout__dot {
  transform: scale(1);
}

/* The leader. Grows from the anchor toward the label. */
.callout__line {
  /* Fixed length. Previously this flexed to fill the gap to the frame edge,
     which is what pushed the label into being squeezed. */
  flex: none;
  width: 2rem;
  height: 1px;
  background: var(--fg-1);
  opacity: 0.55;
  transform: scaleX(0);
  transition: transform var(--dur-slow) var(--ease-out);
}

.callout--right .callout__line {
  transform-origin: left center;
}

.callout--left .callout__line {
  transform-origin: right center;
}

.js .callout.is-current .callout__line {
  transform: scaleX(1);
}

.callout__label {
  /* Its natural width where there is room, and never wider than the space
     between its leader and the frame edge. Values wrap at spaces if squeezed;
     the anchor clamp guarantees room for the longest unbreakable token. */
  flex: 0 1 auto;
  width: max-content;
  max-width: 100%;
  min-width: 0;
  display: grid;
  gap: 1px;
  padding: var(--sp-2) var(--sp-3);
  border: var(--border-width) solid var(--fg-5);
  border-radius: var(--br-sm);
  background: rgb(251 248 242 / 0.95);
  text-align: right;
  transform: translateY(4px);
  transition: transform var(--dur-slow) var(--ease-out);
}

.callout--left .callout__label {
  text-align: left;
}

.js .callout.is-current .callout__label {
  transform: none;
}

.callout__label b {
  font-size: var(--step-0);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--fg-1);
  /* MUST wrap. Held on one line inside a label capped at 38% width, a value
     like "0.40 to 0.55 g/cm3" overflowed its own box by 84px and was then
     clipped by the frame, so the figure was unreadable. A capped box and
     nowrap content cannot both hold. */
  white-space: normal;
  /* break-word, not anywhere: "anywhere" split "2.80%" across two lines. This
     only breaks a token that genuinely cannot fit, so values wrap at spaces. */
  overflow-wrap: break-word;
}

.callout__label em {
  font-style: normal;
  font-size: var(--step--2);
  letter-spacing: 0.04em;
  /* NO text-transform. These labels are chemical formulae and uppercasing them
     destroys the meaning: CaO became "CAO" and MgO became "MGO", neither of
     which is a real species. Casing is data here, not styling. */
  color: var(--fg-3);
  /* Allowed to wrap. Held on one line it overflowed the label box and was
     then clipped by the frame, so the parameter name was unreadable. */
  white-space: normal;
}

@media (max-width: 47.99rem) {
  /* On a phone the leaders would cross the whole frame, so the label docks to
     the edge and the line is dropped. The figure is what matters. */
  .callout__line {
    display: none;
  }

  .callout__label b {
    font-size: var(--step--1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .callout,
  .callout__dot,
  .callout__line,
  .callout__label {
    transition: none;
  }

  .js .callout.is-current .callout__line {
    transform: scaleX(1);
  }
}

/* The flow canvas. Sits over the photograph and replaces it once the shader is
   live, so a machine without WebGL simply keeps the still image. */
/* Stacking inside the lens frame. The shader canvas is created by JavaScript
   and appended last, so without this it painted over the callouts and the
   meter and hid every figure on the image. */
.lens__shot {
  position: relative;
  z-index: 0;
}

.callout,
.lens__meter {
  z-index: 3;
}

/* --------------------------------------------------------------------------
   24. Process scenes

   Animated technical line art. Stroke weights live here, not inline, so the
   whole set is tuned from one place.

   Everything animated is transform or opacity. The scene is aria-hidden and
   the beat beside it states the same thing in words, so under reduced motion
   the drawing simply holds still and loses nothing.
   -------------------------------------------------------------------------- */

.scene {
  display: block;
  width: 100%;
  height: 100%;
}

.sc__ln {
  fill: none;
  stroke: var(--fg-1);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.sc__ln--thin {
  stroke-width: 1;
  opacity: 0.55;
}

/* Ribs travelling along the shell: this is what rotation looks like in section. */
.sc__rib {
  fill: none;
  stroke: var(--fg-1);
  stroke-width: 1;
  opacity: 0.3;
  animation: kiln-rotate 3.2s linear infinite;
  animation-delay: calc(var(--i) * -0.355s);
}

@keyframes kiln-rotate {
  from { transform: translateX(0); }
  to   { transform: translateX(28px); }
}

/* The charge tumbling low in the tube. Each lump lags the one before it. */
.sc__lump {
  fill: var(--fg-1);
  opacity: 0.16;
  animation: kiln-tumble 2.4s ease-in-out infinite;
  animation-delay: calc(var(--i) * -0.28s);
}

@keyframes kiln-tumble {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-3px) rotate(6deg); }
}

/* Feed entering, and product leaving. */
.sc__chip,
.sc__grain {
  fill: var(--fg-1);
  opacity: 0.5;
  animation: sc-drop 1.8s linear infinite;
  animation-delay: calc(var(--i) * -0.36s);
}

.sc__grain {
  opacity: 0.35;
  animation-duration: 1.4s;
}

@keyframes sc-drop {
  0%   { transform: translateY(-10px); opacity: 0; }
  15%  { opacity: 0.55; }
  85%  { opacity: 0.55; }
  100% { transform: translateY(26px); opacity: 0; }
}

/* Discharge heat: concentric arcs breathing outward. Value, never hue. */
.sc__arc {
  fill: none;
  stroke: var(--fg-1);
  stroke-width: 1.2;
  stroke-linecap: round;
  opacity: 0;
  animation: sc-heat 2.6s ease-out infinite;
  animation-delay: calc(var(--i) * 0.55s);
}

@keyframes sc-heat {
  0%   { opacity: 0; transform: translateX(-4px) scaleX(0.9); }
  30%  { opacity: 0.42; }
  100% { opacity: 0; transform: translateX(10px) scaleX(1.05); }
}

.sc__tag {
  fill: var(--fg-3);
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-anchor: middle;
}

/* Every animated element has its transform-box set to its own bounding box,
   or an SVG child rotates about the viewBox origin and flies out of frame. */
.sc__rib,
.sc__lump,
.sc__chip,
.sc__grain,
.sc__arc {
  transform-box: fill-box;
  transform-origin: center;
}

@media (prefers-reduced-motion: reduce) {
  .sc__rib,
  .sc__lump,
  .sc__chip,
  .sc__grain,
  .sc__arc {
    animation: none;
  }

  /* Held still, the heat arcs would be invisible at opacity 0. */
  .sc__arc {
    opacity: 0.3;
  }
}

/* The settled heap in the receiver: a filled form, so the drawing has one
   solid mass and does not read as entirely hollow outline. */
.sc__heap {
  fill: var(--fg-1);
  opacity: 0.12;
  stroke: none;
}

/* Scene switching. Only the current beat's drawing is displayed, so all six
   ship as one inline SVG with no extra request. */
.sc {
  display: none;
}

.scene .sc.is-current,
:where(html:not(.js)) .scene .sc:first-of-type {
  display: block;
}

/* The magnesite seam. Pale, because that is what this mineral is: a light
   carbonate, not a dark ore. */
.sc__seam {
  fill: none;
  stroke: var(--fg-1);
  stroke-width: 9;
  stroke-linecap: round;
  opacity: 0.1;
}

.sc__seam--soft {
  stroke-width: 4;
  opacity: 0.07;
}

/* Dust drifting off a working face. */
.sc__dust {
  fill: var(--fg-1);
  opacity: 0;
  animation: sc-dust 5.2s ease-out infinite;
  animation-delay: calc(var(--i) * -0.68s);
}

@keyframes sc-dust {
  0%   { opacity: 0; transform: translate(0, 0); }
  20%  { opacity: 0.3; }
  100% { opacity: 0; transform: translate(22px, -30px); }
}

/* Material travelling along a belt. The belt itself never moves; the load on
   it does, which is what a conveyor actually looks like. */
.sc__travel {
  fill: var(--fg-1);
  opacity: 0.2;
  animation: sc-travel 3.6s linear infinite;
  animation-delay: calc(var(--i) * -0.4s);
}

@keyframes sc-travel {
  from { transform: translateX(0); }
  to   { transform: translateX(32px); }
}

/* Rejected fragments, thrown clear of the stream and dropping to the bin. */
.sc__reject {
  fill: var(--fg-1);
  opacity: 0;
  animation: sc-reject 2.8s ease-in infinite;
  animation-delay: calc(var(--i) * -0.52s);
}

@keyframes sc-reject {
  0%   { opacity: 0; transform: translate(0, 0); }
  12%  { opacity: 0.34; }
  100% { opacity: 0; transform: translate(34px, 74px); }
}

/* A drop leaving the burette. */
.sc__drop {
  fill: var(--fg-1);
  opacity: 0;
  animation: sc-titrate 2.2s ease-in infinite;
  animation-delay: calc(var(--i) * -0.73s);
}

@keyframes sc-titrate {
  0%   { opacity: 0; transform: translateY(-6px); }
  10%  { opacity: 0.5; }
  70%  { opacity: 0.5; transform: translateY(34px); }
  80%  { opacity: 0; transform: translateY(38px); }
  100% { opacity: 0; transform: translateY(38px); }
}

/* Liquid in a burette, and the swirl in the flask below it. */
.sc__fill {
  fill: var(--fg-1);
  opacity: 0.12;
  stroke: none;
}

.sc__swirl {
  fill: var(--fg-1);
  opacity: 0.14;
  stroke: none;
  transform-box: fill-box;
  transform-origin: center;
  animation: sc-swirl 4.4s ease-in-out infinite;
}

@keyframes sc-swirl {
  0%, 100% { transform: rotate(-4deg) translateX(-2px); }
  50%      { transform: rotate(4deg) translateX(2px); }
}

.sc__dust,
.sc__travel,
.sc__reject,
.sc__drop {
  transform-box: fill-box;
  transform-origin: center;
}

@media (prefers-reduced-motion: reduce) {
  .sc__dust,
  .sc__travel,
  .sc__reject,
  .sc__drop,
  .sc__swirl {
    animation: none;
  }

  /* Held still these would sit at opacity 0 and vanish. */
  .sc__dust { opacity: 0.28; }
  .sc__reject { opacity: 0.3; }
  .sc__drop { opacity: 0.45; }
}

/* The rock mass behind a benched face. */
.sc__rock {
  fill: var(--fg-1);
  opacity: 0.06;
  stroke: none;
}

/* The drawing sits in the same frame as the photograph and cross-fades with it,
   so a process beat and a product beat are the same object changing state
   rather than two separate panels. */
.scene {
  position: absolute;
  inset: 0;
  padding: var(--sp-4);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  pointer-events: none;
  z-index: 2;
}

.lens.is-scene .scene {
  opacity: 1;
}

/* While a drawing is showing, the photograph and its annotations step back. */
.lens.is-scene .lens__shot,
.lens.is-scene .callout {
  opacity: 0 !important;
}

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

/* --------------------------------------------------------------------------
   25. Photography as structure

   Several pages carried 500 to 1000 words with no image at all. These give the
   real product photography room to do the work that prose was doing badly.
   -------------------------------------------------------------------------- */

.figure-wide {
  margin: 0 0 var(--sp-8);
  border-radius: var(--br-sm);
  overflow: hidden;
  background: var(--bg-2);
}

.figure-wide img {
  display: block;
  width: 100%;
  /* A wide crop. The source is 4:3, so this takes a band through the middle
     rather than letterboxing it. */
  aspect-ratio: 21 / 8;
  object-fit: cover;
  object-position: center 58%;
  max-width: none;
}

@media (max-width: 47.99rem) {
  .figure-wide img {
    aspect-ratio: 16 / 9;
  }
}

/* The process photographs are natively 16:9, unlike the 4:3 product shots the
   rule above was written for. Cropping them to 21:8 would cut a third of the
   height off a composed frame and decapitate the bulk bag, so they keep their
   own ratio and a centred origin instead of the 58% bias that suits a dish of
   powder sitting low in a 4:3 frame. */
.figure-wide--photo img {
  aspect-ratio: 16 / 9;
  object-position: center;
}

/* A small square of the actual grade beside its name. */
.grade-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 7;
  object-fit: cover;
  object-position: center 60%;
  border-radius: calc(var(--br-sm) - 2px);
  margin-bottom: var(--sp-3);
  max-width: none;
}

/* A scene rather than a product. The 60% vertical bias above is tuned for the
   grade photographs, where the material sits low in the frame in a dish. An
   industry photograph is composed normally, so biasing it downward crops the
   top off the subject. Same box, same aspect, centred crop. */
.grade-thumb--scene {
  object-position: center;
}

/* --------------------------------------------------------------------------
   26. The reel

   A full-bleed video band. Decorative and aria-hidden: it states nothing the
   copy does not. It never autoplays under reduced motion, and it never loads
   its 2.5 MB until it is close to the viewport.
   -------------------------------------------------------------------------- */

.reel {
  position: relative;
  overflow: hidden;
  background: var(--fg-1);
  /* A band, not a full screen. The material is the subject, not the scenery. */
  height: clamp(15rem, 42vh, 26rem);
}

.reel__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  max-width: none;
}

/* A soft vignette so the caption always has something to sit on, whatever
   frame the video happens to be showing. */
.reel__scrim {
  position: absolute;
  inset: 0;
  /* The bottom stop was tuned for a dark mountain. The footage is actually a
     pale, warm quarry wall under flat overcast light, so cream text on 0.62
     over light stone measured around 2.8:1. Only the anchor stop the caption
     rests on is raised; the midpoint and top are left alone so the frame does
     not darken. */
  background: linear-gradient(
    to top,
    rgb(27 26 25 / 0.72) 0%,
    rgb(27 26 25 / 0.18) 42%,
    rgb(27 26 25 / 0) 72%
  );
  pointer-events: none;
}

.reel__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--sp-5);
  margin: 0 auto;
  padding: 0 var(--container-pad);
  /* Was the full container width, which made sense for a full sentence and
     leaves a four-word caption stranded across a 76rem band. Narrow box,
     centred, so the phrase reads as a deliberate mark rather than a strip. */
  max-width: 34ch;
  text-align: center;
  color: #f6f1e8;
  /* Half the words can carry twice the size. --step-1 is the tier the site
     already uses for supporting copy, so this stops the caption reading as a
     disclaimer without promoting it to a heading. */
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: 0.02em;
  /* Body leading of 1.6 sits loose when a short caption wraps on a phone. */
  line-height: 1.3;
  text-shadow: 0 1px 12px rgb(27 26 25 / 0.6);
}

@media (prefers-reduced-motion: reduce) {
  /* The poster frame is a real frame of the real footage, so nothing is lost. */
  .reel__video {
    display: block;
  }
}

/* -------------------------------------------------------------------------
   Process footage.

   One clip per grade, emitted only when the file exists (see
   src/templates/clip.mjs). The video is decorative and aria-hidden: it shows
   the process stage the grade is defined by, and every fact it depicts is
   already stated in the copy beside it.
   ------------------------------------------------------------------------- */

.clip {
  margin: 0;
  border-radius: var(--br-sm);
  overflow: hidden;
  background: var(--bg-2);
}

.clip__video {
  display: block;
  width: 100%;
  height: auto;
  /* The clips are encoded 1280x720. Stating the ratio here means the space is
     reserved before the poster loads, so a clip arriving late cannot push the
     copy beneath it down the page. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  max-width: none;
}

.clip__cap {
  margin: var(--sp-3) 0 0;
  color: var(--fg-3);
  font-size: var(--step--1);
}

/* The controlled comparison. Two clips, one variable, so they must be the same
   size and sit on the same baseline: any difference in scale reads as a
   difference in the material rather than in the process. */
.clip-pair {
  margin: 0;
}

.clip-pair__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
  align-items: start;
}

.clip-pair__label {
  margin: var(--sp-3) 0 0;
  color: var(--fg-2);
  font-size: var(--step--1);
  font-weight: 500;
}

@media (max-width: 47.99rem) {
  .clip-pair__grid {
    grid-template-columns: 1fr;
    gap: var(--sp-5);
  }
}
