/* Built by bin/build-assets.sh — edit src/css/, not this file. */

/* ---- tokens/palette.css ---- */
/* ============================================================
   PALETTE — dark

   Derived from the supplied fire-gradient background: the deep
   green grounds (#06120f → #0a261c) and the ember orange
   (#ff822a). This is the file the art direction lives in; no
   component references a colour literal, so this is the only
   place a re-skin touches.

   Contrast, measured against --colour-ground:
     text        17.0:1     text-2      11.1:1
     muted        7.2:1     accent       7.7:1
     border-strong 3.5:1    focus       11.0:1

   Note: white on the orange accent is only 2.48:1, so accent
   buttons use the dark ground as ink, not white.
   ============================================================ */

:root {
  /* Grounds. The page background itself is the gradient in
     components/atmosphere.css; this is the colour beneath it. */
  --colour-ground:        #06120f;
  --colour-hero-ground:   #000000;   /* the hero block behind the photograph */

  /* Surfaces are translucent so the gradient reads through them.
     A wall of opaque panels would hide the background entirely. */
  --colour-surface:       rgb(255 255 255 / 4%);
  --colour-surface-2:     rgb(255 255 255 / 7%);
  --colour-surface-3:     rgb(0 0 0 / 25%);

  --colour-border:        #24513f;   /* decorative only — 2.1:1 */
  --colour-border-strong: #3a7562;   /* anything that must be seen — 3.5:1 */

  --colour-text:          #eef3f0;
  --colour-text-2:        #b9c9c2;
  --colour-text-muted:    #8fa39b;
  --colour-text-invert:   #06120f;

  --colour-accent:        #ff822a;
  --colour-accent-hover:  #ffa055;
  --colour-accent-ink:    #06120f;   /* dark on orange — white fails */

  --colour-focus:         #6fd0ff;

  /* Opaque surfaces that sit OVER page content — the nav drawer and the
     quote bar — cannot be translucent or the text behind them shows
     through. Scrim is the semi-opaque sticky header. */
  /* ---- Header ----------------------------------------------------------
     Transparent bar: it sits directly on the page background, so its text
     takes the page's light colours.                                       */
  --colour-header-bg:     transparent;
  --colour-header-text:   #eef3f0;
  --colour-header-muted:  #8fa39b;
  --colour-header-border: rgb(255 255 255 / 18%);
  --colour-header-raised: #08201a;

  /* Reversed mark: the green letterforms become light so they read on the
     dark ground — 17:1, against 1.7:1 for the original green. The orange is
     untouched, already at 6.5:1 here. */
  --logo-ink-header:      #eef3f0;
  --logo-mark-header:     #fe680d;

  --colour-raised:        #08201a;
  --colour-scrim:         rgb(6 18 15 / 82%);
  --colour-recess:        rgb(0 0 0 / 30%);

  /* Shadows read weakly on a dark ground — depth comes mostly from the
     border. This is the one place a shadow still earns its place. */
  --shadow-raised:        0 -8px 28px rgb(0 0 0 / 45%);
  --shadow-float:         0 6px 24px rgb(0 0 0 / 38%);

  --colour-state-ok:      #6fcf9a;
  --colour-state-ok-bg:   rgb(111 207 154 / 12%);
  --colour-state-note:    #e3b45c;
  --colour-state-note-bg: rgb(227 180 92 / 12%);
  --colour-state-off:     #a0b0aa;
  --colour-state-off-bg:  rgb(160 176 170 / 12%);
}

/* ---- tokens/space.css ---- */
/* ============================================================
   SPACING — fixed steps, no in-between values.
   ============================================================ */

:root {
  --space-2xs: 0.25rem;   /*  4px */
  --space-xs:  0.5rem;    /*  8px */
  --space-sm:  0.75rem;   /* 12px */
  --space-md:  1rem;      /* 16px */
  --space-lg:  1.5rem;    /* 24px */
  --space-xl:  2rem;      /* 32px */
  --space-2xl: 3rem;      /* 48px */
  --space-3xl: 4rem;      /* 64px */
  --space-4xl: 6rem;      /* 96px */
  --space-5xl: 8rem;      /* 128px */

  /* Vertical rhythm between page sections, fluid so small screens
     do not inherit desktop's generosity. */
  --space-section: clamp(2.5rem, 7vw, 5rem);
}

/* ---- tokens/type.css ---- */
/* ============================================================
   TYPE

   System stack on purpose. Committing to a typeface would be
   committing to an art direction that is still open, and a
   greybox in someone else's typeface reads as a proposal.

   Phase 4 sets --font-display / --font-body / --font-mono and
   nothing else changes.
   ============================================================ */

:root {
  --font-display: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body:    system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;

  /* Fluid scale. Each step interpolates between a mobile and a
     desktop size, so no breakpoint has to restate type. */
  --text-xs:   0.75rem;
  --text-sm:   0.875rem;
  --text-base: 1rem;
  --text-md:   clamp(1.0625rem, 0.3vw + 1rem, 1.125rem);
  --text-lg:   clamp(1.1875rem, 0.6vw + 1rem, 1.375rem);
  --text-xl:   clamp(1.375rem, 1.1vw + 1rem, 1.75rem);
  --text-2xl:  clamp(1.625rem, 2vw + 1rem, 2.25rem);
  --text-3xl:  clamp(2rem, 3.4vw + 1rem, 3rem);

  --leading-tight: 1.12;
  --leading-snug:  1.3;
  --leading-body:  1.6;

  --tracking-tight: -0.02em;
  --tracking-wide:  0.14em;

  /* Running text stays near 65 characters regardless of container. */
  --measure: 66ch;
}

/* ---- tokens/layout.css ---- */
/* ============================================================
   LAYOUT + MEDIA RATIOS

   Image ratios are variables, not numbers baked into templates.
   The ratio decision is deliberately deferred until the layouts
   can be judged — changing it later means editing this file and
   regenerating thumbnails, not rebuilding components.
   ============================================================ */

:root {
  --container: 75rem;                        /* 1200px */
  --gutter:    clamp(1.25rem, 5vw, 7.5rem);  /* 20px → 120px */

  /* Square. The product photography is an isometric render on a flat orange
     field, and that combination is squarest in its bounding box and most
     forgiving of framing — a uniform background extends invisibly, so a shot
     that sits slightly off-centre costs nothing. Square also never crops
     badly across the card grid, the gallery, related products and the quote
     list, which a 4:3 would in at least one of them. */
  --ratio-product:  1 / 1;
  --ratio-wide:     16 / 9;
  --ratio-portrait: 3 / 4;
  --ratio-square:   1 / 1;

  --border-width: 1px;
  --radius: 0px;        /* square by default; a re-skin may soften it */
  --radius-pill: 999px; /* fully rounded — the floating header bar */

  /* Height of the credentials band. The hero subtracts it so the two make
     one screen between them, and the showcase uses it as its collapsed
     row — one number, two places that must agree. */
  --credentials-band: 15rem;

  /* Inset of the floating header from the viewport edge. */
  --header-inset: clamp(0.75rem, 2vw, 1.25rem);
}

/* ---- tokens/motion.css ---- */
/* ============================================================
   MOTION

   One global gate. Every transition and animation in the theme
   reads --motion-duration, so honouring a reduced-motion
   preference is a single override rather than a per-component
   media query that someone will forget.
   ============================================================ */

:root {
  --motion-fast:     120ms;
  --motion-duration: 220ms;
  --motion-ease:     cubic-bezier(0.2, 0, 0.2, 1);
  /* Decelerates hard at the end — for large layout moves that should land
     gently rather than stop. */
  --motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-fast:     0ms;
    --motion-duration: 0ms;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- base/reset.css ---- */
/* Minimal reset. Only what the theme actually relies on. */

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

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

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd, ul, ol {
  margin: 0;
}

ul[class], ol[class] { list-style: none; padding: 0; }

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

input, button, textarea, select { font: inherit; color: inherit; }

/* Wide content scrolls inside its own container; the page body never
   scrolls sideways. */
html, body { overflow-x: clip; }

/* A class that sets `display` overrides the user-agent [hidden] rule, so an
   element with both stays visible. This is the fix, applied once. */
[hidden] { display: none !important; }

/* ---- base/elements.css ---- */
/* Element defaults, driven entirely by tokens. */

body {
  /* The gradient itself is a fixed layer behind the page
     (components/atmosphere.css); this is the colour under it, and what
     shows if that layer ever fails to paint. */
  background: var(--colour-ground);
  color: var(--colour-text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); line-height: var(--leading-snug); }
h5, h6 { font-size: var(--text-md); line-height: var(--leading-snug); }

p { max-width: var(--measure); }

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover { text-decoration-thickness: 2px; }

strong, b { font-weight: 600; }

hr {
  border: 0;
  border-top: var(--border-width) solid var(--colour-border);
  margin: var(--space-xl) 0;
}

code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.9em; }

/* Digits that sit in columns should line up. */
.tabular { font-variant-numeric: tabular-nums; }

/* ---- base/a11y.css ---- */
/* ============================================================
   ACCESSIBILITY PRIMITIVES

   These are structural, not decorative. They are here from the
   first component rather than added in a later pass, because
   retrofitting them means rewriting markup.
   ============================================================ */

/* One focus treatment, applied everywhere. `outline: none` appears
   nowhere in this codebase — if it ever does, that is a bug. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--colour-focus);
  outline-offset: 2px;
  border-radius: 1px;
}

/* Visible only to assistive technology. */
.u-visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link — the first focusable thing on every page. */
.skip-link {
  position: absolute;
  top: 0;
  left: var(--space-md);
  z-index: 100;
  transform: translateY(-120%);
  background: var(--colour-accent);
  color: var(--colour-accent-ink);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  text-decoration: none;
  transition: transform var(--motion-fast) var(--motion-ease);
}

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

/* Every interactive control clears the 44×44 target guidance without
   each component having to remember. */
:where(a[class*="button"], button, [role="button"]) {
  min-height: 44px;
}

/* ---- base/reveal.css ---- */
/* ============================================================
   REVEALS

   Applied only once the script has marked the document, so a
   page without JavaScript is never left holding hidden content.
   ============================================================ */

.has-reveal .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 620ms var(--motion-ease-out) var(--reveal-delay, 0ms),
    transform 620ms var(--motion-ease-out) var(--reveal-delay, 0ms);
}

.has-reveal .reveal.is-revealed {
  opacity: 1;
  transform: none;
}

/* An element that is focused must be visible, whatever the observer thinks —
   otherwise tabbing ahead of the scroll lands on something invisible. */
.has-reveal .reveal:focus-within {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .has-reveal .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---- base/transitions.css ---- */
/* ============================================================
   PAGE TRANSITIONS

   Cross-document view transitions. Navigating between pages
   was a hard cut to white-then-content, which is what reads as
   jitter: the browser paints the new document before it has the
   stylesheet applied, so the eye catches a flash and a jump.

   This hands the browser the old and new pages and asks it to
   cross-fade between them, so a navigation is one continuous
   surface rather than two.

   Progressive by definition. A browser without support ignores
   the at-rule entirely and navigates exactly as it did before.
   ============================================================ */

@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: dax-page-out 180ms var(--motion-ease) both;
}

::view-transition-new(root) {
  animation: dax-page-in 320ms var(--motion-ease-out) both;
}

@keyframes dax-page-out {
  to { opacity: 0; }
}

@keyframes dax-page-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* The header and the top bar are on every page and in the same place. Giving
   them their own names holds them still while the body changes underneath,
   which is what stops a navigation looking like the whole site reloaded. */
.utility-bar { view-transition-name: dax-utility; }
.site-header { view-transition-name: dax-header; }

::view-transition-old(dax-header),
::view-transition-new(dax-header),
::view-transition-old(dax-utility),
::view-transition-new(dax-utility) {
  animation: none;
  mix-blend-mode: normal;
}

/* ============================================================
   FIRST PAINT

   The same arrival, for a page loaded directly rather than
   navigated to. Deliberately short: this runs before anyone has
   asked for anything, so it must never feel like waiting.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  #main > *:first-child {
    animation: dax-page-in 380ms var(--motion-ease-out) both;
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* ---- layout/container.css ---- */
/* Horizontal rhythm. One container, one gutter token. */

.container {
  width: 100%;
  max-width: calc(var(--container) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--narrow { max-width: calc(48rem + var(--gutter) * 2); }

/* Long-form reading. Sized to the prose measure rather than to the form
   width, so the heading and the text sit in one centred column instead of
   a narrow column pinned to the left of a wide empty one. Kept separate
   from --narrow because the form templates share that and need the room. */
.container--prose { max-width: calc(37rem + var(--gutter) * 2); }

/* Form and long-form pages keep the site's left rail rather than centring a
   narrow column. A page title should start in the same place on every page,
   and the measure is set on the content inside instead. */
.lead-column {
  max-width: 48rem;
}

/* ---- layout/sections.css ---- */
/* Vertical rhythm and alternating grounds. */

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

.section--surface { background: var(--colour-surface); }
.section--muted   { background: var(--colour-surface-3); }

/* Sections are translucent by default so the gradient behind the page stays
   visible. Opaque panels edge to edge would hide it entirely. */
.section--surface,
.section--muted {
  backdrop-filter: blur(2px);
}

.section__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-bottom: var(--space-xl);
}

.section__title { font-size: var(--text-2xl); }

/* A page title is display size wherever it appears. Seven templates put this
   class on their h1 — Brands, Search, the lead forms, solutions and the
   taxonomy archives — which sized a page title as a section heading and left
   the site with two different h1 sizes depending on which page you were on. */
h1.section__title { font-size: var(--text-3xl); }

.section__intro {
  color: var(--colour-text-2);
  font-size: var(--text-md);
}

/* An eyebrow is a label, not a heading — never marked up as one. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

/* ---- layout/grid.css ---- */
/* ============================================================
   GRIDS

   Every grid here is intrinsically responsive: it reflows on
   available width rather than on a breakpoint list. A component
   built this way already works at 320px, which is why mobile-first
   costs nothing to do now and a fortune to retrofit.
   ============================================================ */

.grid {
  display: grid;
  gap: var(--space-md);
}

/* min() guards the minimum against narrow viewports — without it the
   track floor overflows the screen on small phones. */
.grid--cards  { grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); }
.grid--tiles  { grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); }
.grid--halves { grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); }

/* Two-column editorial split: stacks until there is genuinely room. */
.split {
  display: grid;
  gap: var(--space-xl);
  align-items: start;
}

@media (min-width: 56rem) {
  .split { grid-template-columns: 1fr 1fr; gap: var(--space-2xl); }
  .split--media-first > :first-child { order: -1; }
}

/* Grid and flex children default to min-width:auto, which makes long
   words and wide tables blow out the layout. This is the fix, applied
   once rather than debugged repeatedly. */
.grid > *, .split > * { min-width: 0; }

.stack { display: flex; flex-direction: column; gap: var(--space-md); }
.stack--tight { gap: var(--space-sm); }
.stack--loose { gap: var(--space-lg); }

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  align-items: center;
}

/* ---- components/atmosphere.css ---- */
/* ============================================================
   ATMOSPHERE — fire gradient, ember drift, film grain

   Adapted from the supplied CSS in three ways, all forced by
   the fact that this sits under a whole scrolling site rather
   than one full-screen block:

   1. Fixed to the viewport, not a min-height:100vh wrapper.
      The glow stays anchored bottom-right as you scroll instead
      of drifting off the end of a 3,000px page.

   2. No `overflow: hidden` container. An overflow-hidden
      ancestor kills `position: sticky`, which the site header
      relies on.

   3. The grain is its own fixed layer rather than an ::after
      inside the background, because a z-index:-1 background
      cannot paint a child above the page content.
   ============================================================ */

.site-atmosphere {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  isolation: isolate;

  background:
    radial-gradient(ellipse at 90% 88%, rgb(255 130 42 / 8%) 0%, transparent 26%),
    radial-gradient(ellipse at 50% 115%, rgb(33 86 62 / 28%) 0%, rgb(9 43 34 / 12%) 32%, transparent 65%),
    linear-gradient(180deg, #06120f 0%, #071a15 42%, #09231b 72%, #0a261c 100%);
}

/* Offscreen fire glow. */
.site-atmosphere::before {
  content: "";
  position: absolute;
  z-index: -2;
  right: -22%;
  bottom: -42%;
  width: 88%;
  height: 76%;
  pointer-events: none;

  background:
    radial-gradient(ellipse at 82% 80%, rgb(255 125 34 / 52%) 0%, rgb(255 95 18 / 25%) 16%, rgb(255 95 18 / 8%) 34%, transparent 48%),
    radial-gradient(ellipse at 92% 95%, rgb(255 168 78 / 34%) 0%, rgb(235 98 23 / 16%) 26%, transparent 54%),
    radial-gradient(ellipse at 68% 100%, rgb(255 82 18 / 36%) 0%, rgb(186 53 12 / 12%) 36%, transparent 68%),
    radial-gradient(ellipse at 78% 88%, rgb(255 200 120 / 12%) 0%, transparent 22%);

  filter: blur(58px);
  transform-origin: bottom right;
  will-change: transform, opacity;

  animation:
    fireDrift 11.3s linear infinite,
    firePulse 4.7s ease-in-out infinite;
}

/* ---- Page backdrop ----------------------------------------------------
   A photograph as the ground for a whole page rather than a band at the top
   of it. It sits inside the atmosphere on purpose: above the gradient so it
   is visible, below the embers so they still drift across it.

   Fixed with the rest of this layer, so it holds still while the page
   scrolls over it instead of sliding away at the first screen.          */

.site-backdrop {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--backdrop);
  background-size: cover;
  background-position: right center;
  background-repeat: no-repeat;

  /* Anchored right and dissolved to nothing before it reaches the text.
     Masking rather than laying a scrim over it means the page's own
     gradient is what the photograph fades into, which is the only way the
     join is invisible. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 34%, rgb(0 0 0 / 18%) 48%, rgb(0 0 0 / 68%) 62%, #000 76%);
  mask-image: linear-gradient(90deg, transparent 0%, transparent 34%, rgb(0 0 0 / 18%) 48%, rgb(0 0 0 / 68%) 62%, #000 76%);
}

/* Narrow screens have no room for a text column beside a photograph, so it
   becomes a wash behind the whole page instead of a right-hand panel. */
@media (max-width: 61.99rem) {
  .site-backdrop {
    background-position: 72% center;
    /* Held well back. There is no dark column to put the text on at this
       width, so the whole page is read over the photograph and legibility
       has to come from the image being faint rather than from where it is. */
    -webkit-mask-image: linear-gradient(180deg, rgb(0 0 0 / 24%) 0%, rgb(0 0 0 / 10%) 46%, transparent 78%);
    mask-image: linear-gradient(180deg, rgb(0 0 0 / 24%) 0%, rgb(0 0 0 / 10%) 46%, transparent 78%);
  }
}

/* Film grain, above the content. */
.site-grain {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: soft-light;
  background-repeat: repeat;
  background-size: 180px 180px;
  background-image:
    url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.9'/%3E%3C/svg%3E");
}

/* Embers. */
.embers {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
}

.embers span {
  position: absolute;
  bottom: -30px;
  display: block;
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: rgb(255 170 70 / 95%);
  box-shadow: 0 0 6px rgb(255 170 70 / 90%), 0 0 14px rgb(255 110 30 / 55%);
  opacity: 0;
  will-change: transform, opacity;
  animation-name: emberRise;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.embers span:nth-child(1)  { right: 7%;  width: 3px; height: 3px; animation-duration: 4.8s; animation-delay: -1.1s; }
.embers span:nth-child(2)  { right: 10%; width: 5px; height: 5px; animation-duration: 5.9s; animation-delay: -2.7s; }
.embers span:nth-child(3)  { right: 13%; width: 2px; height: 2px; animation-duration: 3.7s; animation-delay: -0.9s; }
.embers span:nth-child(4)  { right: 16%; width: 4px; height: 4px; animation-duration: 6.4s; animation-delay: -3.4s; }
.embers span:nth-child(5)  { right: 18%; width: 3px; height: 3px; animation-duration: 4.1s; animation-delay: -2.2s; }
.embers span:nth-child(6)  { right: 21%; width: 6px; height: 6px; animation-duration: 6.8s; animation-delay: -4s;   }
.embers span:nth-child(7)  { right: 24%; width: 2px; height: 2px; animation-duration: 3.9s; animation-delay: -1.6s; }
.embers span:nth-child(8)  { right: 26%; width: 4px; height: 4px; animation-duration: 5.2s; animation-delay: -0.3s; }
.embers span:nth-child(9)  { right: 29%; width: 3px; height: 3px; animation-duration: 4.6s; animation-delay: -2.9s; }
.embers span:nth-child(10) { right: 31%; width: 5px; height: 5px; animation-duration: 6.1s; animation-delay: -4.8s; }
.embers span:nth-child(11) { right: 34%; width: 2px; height: 2px; animation-duration: 4.4s; animation-delay: -1.3s; }
.embers span:nth-child(12) { right: 37%; width: 3px; height: 3px; animation-duration: 5.5s; animation-delay: -3.6s; }
.embers span:nth-child(13) { right: 40%; width: 2px; height: 2px; animation-duration: 4.9s; animation-delay: -2s;   }
.embers span:nth-child(14) { right: 43%; width: 4px; height: 4px; animation-duration: 6.7s; animation-delay: -5.1s; }

@keyframes fireDrift {
  0%   { transform: translate3d(1%, 3%, 0)   scaleX(0.94) scaleY(0.96) rotate(-1.5deg); }
  11%  { transform: translate3d(-2%, 1%, 0)  scaleX(1.02) scaleY(1.01) rotate(0.6deg); }
  23%  { transform: translate3d(2%, -3%, 0)  scaleX(1.05) scaleY(1.08) rotate(1.4deg); }
  37%  { transform: translate3d(-3%, 0%, 0)  scaleX(0.98) scaleY(1)    rotate(-0.9deg); }
  49%  { transform: translate3d(1%, -2%, 0)  scaleX(1.08) scaleY(0.97) rotate(1.9deg); }
  61%  { transform: translate3d(-1%, 2%, 0)  scaleX(0.96) scaleY(1.05) rotate(-1.2deg); }
  74%  { transform: translate3d(3%, -1%, 0)  scaleX(1.03) scaleY(1.10) rotate(0.8deg); }
  88%  { transform: translate3d(-2%, -4%, 0) scaleX(1)    scaleY(0.95) rotate(-1.8deg); }
  100% { transform: translate3d(1%, 3%, 0)   scaleX(0.94) scaleY(0.96) rotate(-1.5deg); }
}

@keyframes firePulse {
  0%   { opacity: 0.74; filter: blur(61px); }
  17%  { opacity: 0.92; filter: blur(47px); }
  31%  { opacity: 0.80; filter: blur(58px); }
  46%  { opacity: 0.98; filter: blur(43px); }
  63%  { opacity: 0.76; filter: blur(56px); }
  79%  { opacity: 0.90; filter: blur(49px); }
  100% { opacity: 0.82; filter: blur(55px); }
}

@keyframes emberRise {
  0%   { transform: translate3d(0, 0, 0) scale(0.7);          opacity: 0; }
  10%  {                                                       opacity: 0.95; }
  35%  { transform: translate3d(-8px, -90px, 0) scale(1);      opacity: 0.9; }
  65%  { transform: translate3d(12px, -190px, 0) scale(0.95);  opacity: 0.7; }
  85%  { transform: translate3d(-10px, -270px, 0) scale(0.8);  opacity: 0.22; }
  100% { transform: translate3d(8px, -320px, 0) scale(0.65);   opacity: 0; }
}

/* Animating `filter` repaints the whole blurred layer every frame, which is
   the most expensive thing on the page. On small screens — where the glow is
   largely offscreen anyway and the battery cost lands hardest — keep the blur
   static and let opacity alone carry the flicker. */
@media (max-width: 48rem) {
  .site-atmosphere::before {
    animation: firePulseCheap 4.7s ease-in-out infinite;
  }

  @keyframes firePulseCheap {
    0%, 100% { opacity: 0.78; }
    46%      { opacity: 0.96; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .site-atmosphere::before { animation: none; }
  .embers span { animation: none; opacity: 0.35; }
}

/* ---- components/button.css ---- */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-lg);
  border: var(--border-width) solid var(--colour-accent);
  border-radius: var(--radius);
  background: var(--colour-accent);
  color: var(--colour-accent-ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  text-align: center;
  transition: background var(--motion-fast) var(--motion-ease),
              border-color var(--motion-fast) var(--motion-ease);
}

.button:hover { background: var(--colour-accent-hover); border-color: var(--colour-accent-hover); }

.button--secondary {
  background: transparent;
  color: var(--colour-accent);
}
.button--secondary:hover { background: var(--colour-surface-2); }

.button--tertiary {
  background: transparent;
  color: var(--colour-text-2);
  border-color: var(--colour-border-strong);
}
.button--tertiary:hover { background: var(--colour-surface-2); color: var(--colour-text); }

.button[aria-disabled="true"],
.button:disabled { opacity: 0.5; cursor: not-allowed; }

/* On narrow screens a pair of CTAs stacks and fills, rather than
   shrinking into two unreadable half-width buttons. */
.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

@media (max-width: 26rem) {
  .button-row { flex-direction: column; align-items: stretch; }
}

/* ---- components/chip.css ---- */
/* A standards chip is a label, not a control.
   It is a <span>, and it never looks clickable unless it is a link. */

.chip {
  display: inline-block;
  padding: 0.15rem var(--space-xs);
  border: var(--border-width) solid var(--colour-border-strong);
  background: var(--colour-surface-2);
  color: var(--colour-text-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.5;
  white-space: nowrap;
}

.chip--certified {
  border-color: var(--colour-state-ok);
  background: var(--colour-state-ok-bg);
  color: var(--colour-state-ok);
}

.chip--reference {
  border-color: var(--colour-state-note);
  background: var(--colour-state-note-bg);
  color: var(--colour-state-note);
}

.chip--off {
  border-color: var(--colour-state-off);
  background: var(--colour-state-off-bg);
  color: var(--colour-state-off);
}

a.chip { text-decoration: none; }
a.chip:hover { border-color: var(--colour-text); color: var(--colour-text); }

/* ---- components/image-slot.css ---- */
/* ============================================================
   IMAGE SLOT

   Ratio-agnostic by design. The aspect ratio is a token, so the
   deferred ratio decision is a variable change plus regenerated
   thumbnails — not a rebuild.

   The empty state is a designed state, not a missing one: pages
   are complete and reviewable before any photograph exists.
   ============================================================ */

.image-slot {
  position: relative;
  aspect-ratio: var(--slot-ratio, var(--ratio-product));
  background: var(--colour-recess);
  overflow: hidden;
}

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

.image-slot--wide     { --slot-ratio: var(--ratio-wide); }
.image-slot--portrait { --slot-ratio: var(--ratio-portrait); }
.image-slot--square   { --slot-ratio: var(--ratio-square); }

.image-slot--empty {
  display: grid;
  place-items: center;
  border: 1px dashed var(--colour-border-strong);
  background: var(--colour-surface);
}

.image-slot__note {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
  text-align: center;
  padding: var(--space-md);
}

/* ---- components/card-product.css ---- */
.card-product {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--colour-surface);
  border: var(--border-width) solid var(--colour-border-strong);
  height: 100%;
}

.card-product__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

.card-product__title {
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
}

/* The whole card is clickable, but only the title is in the tab order —
   so keyboard users get one stop per card, not three. */
.card-product__title a { text-decoration: none; }
.card-product__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.card-product { position: relative; }

.card-product__summary {
  color: var(--colour-text-2);
  font-size: var(--text-sm);
}

/* Controls must sit above the card-wide click target. */
.card-product__actions {
  position: relative;
  z-index: 1;
  margin-top: auto;
  padding-top: var(--space-xs);
}

/* ---- components/prose.css ---- */
/* ============================================================
   PROSE

   Long-form editor content: the About page, and anything else
   written in the block editor.

   The reset zeroes every margin, which is right for components
   and wrong for prose. Without this, paragraphs sat flush
   against each other and headings sat flush against the
   paragraph below them, so the whole page read as one
   undifferentiated block with no way in.

   Spacing here is asymmetric on purpose: more room above a
   heading than below it. A heading belongs to the text that
   follows it, and equal spacing on both sides makes it float
   between two sections instead of introducing one.
   ============================================================ */

.prose {
  /* The ch unit is the width of a zero, which is wider than average
     lowercase, so 60ch renders around 75 actual characters. This lands
     lands near 68, inside the range where a line is long enough not to feel
     clipped and short enough that the eye still finds the next one. Light
     type on a dark ground blooms slightly at the edges, so it wants a
     shorter line than the same text would in print, but 62 was too far. */
  --measure: 55ch;

  max-width: var(--measure);
  line-height: 1.65;

  /* Distributes the rag rather than filling every line to the same width,
     and stops a paragraph ending on a single stranded word. This is the
     thing that makes line lengths vary as the eye moves down. */
  text-wrap: pretty;
}

.prose > * + * {
  margin-block-start: var(--space-md);
}

.prose > :first-child {
  margin-block-start: 0;
}

.prose h2,
.prose h3,
.prose h4 {
  text-wrap: balance;
}

.prose > h2 {
  margin-block-start: var(--space-3xl);
  margin-block-end: var(--space-md);
}

.prose > h3 {
  margin-block-start: var(--space-2xl);
  margin-block-end: var(--space-sm);
}

/* The paragraph directly under a heading keeps the heading's own bottom
   margin rather than adding its own on top of it. */
.prose > h2 + *,
.prose > h3 + *,
.prose > h4 + * {
  margin-block-start: 0;
}

/* A heading immediately after another heading is a subhead, not a new
   section, so it does not get the full gap. */
.prose > h2 + h3 {
  margin-block-start: var(--space-lg);
}

.prose ul,
.prose ol {
  padding-inline-start: var(--space-lg);
}

.prose li + li {
  margin-block-start: var(--space-xs);
}

.prose a {
  text-underline-offset: 0.2em;
}

.prose strong {
  color: var(--colour-text);
}

/* ---- components/brand-tile.css ---- */
/* ============================================================
   BRAND TILE

   The whole tile is the link. There is no heading: the mark is
   the name, and repeating it in type underneath said the same
   thing twice.

   The interaction is one idea — the chevron is genuinely behind
   the tile's right edge, clipped by it, and slides out. It is
   not a fade. Occlusion is what makes something read as having
   been there all along rather than having appeared, and it is
   the whole difference between this and a chevron that blinks
   into existence.
   ============================================================ */

.brand-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
  padding-right: calc(var(--space-lg) + 2.75rem);  /* room the chevron enters */
  background: var(--colour-surface);
  border: var(--border-width) solid var(--colour-border-strong);
  height: 100%;

  /* The clip that puts the chevron behind the edge. */
  overflow: hidden;

  color: inherit;
  text-decoration: none;

  transition:
    background-color var(--motion-duration) var(--motion-ease),
    border-color var(--motion-duration) var(--motion-ease),
    transform var(--motion-duration) var(--motion-ease-out),
    box-shadow var(--motion-duration) var(--motion-ease-out);
}

/* Our own line reads differently from the brands we distribute. */
.brand-tile--own {
  border-color: var(--colour-accent);
  border-width: 2px;
}

/* ---- The light the chevron comes out of -------------------------------
   A warm wash anchored off the right edge, scaled from nothing. The
   chevron arrives lit from the same side it emerges from, so it reads as
   coming out of somewhere rather than being pasted on top.            */
.brand-tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
    120% 140% at 108% 50%,
    rgb(255 130 42 / 22%) 0%,
    rgb(255 130 42 / 7%) 34%,
    transparent 62%
  );
  opacity: 0;
  transition: opacity var(--motion-duration) var(--motion-ease);
}

.brand-tile > * {
  position: relative;
  z-index: 1;
}

/* ---- The chevron ------------------------------------------------------ */
.brand-tile__go {
  position: absolute;
  z-index: 1;
  top: 50%;
  right: 0;
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  color: var(--colour-accent);

  /* Parked fully outside the padding box, so the tile's own overflow clip
     hides it. Translating rather than hiding is what allows it to travel
     back out from under the edge. */
  transform: translate(100%, -50%);
  opacity: 0;

  transition:
    transform var(--motion-duration) var(--motion-ease-out),
    opacity var(--motion-fast) linear;
}

.brand-tile__go svg {
  width: 1.85rem;
  height: 1.85rem;
}

/* ---- Hover and keyboard, treated identically -------------------------- */
.brand-tile:hover,
.brand-tile:focus-visible {
  background-color: var(--colour-surface-2);
  border-color: var(--colour-accent);
  transform: translateY(-2px);
  /* Offset and blur, so it is a shadow rather than a halo. */
  box-shadow: 0 6px 18px rgb(0 0 0 / 38%);
}

.brand-tile:hover::before,
.brand-tile:focus-visible::before {
  opacity: 1;
}

.brand-tile:hover .brand-tile__go,
.brand-tile:focus-visible .brand-tile__go {
  transform: translate(-0.55rem, -50%);
  opacity: 1;
}

/* The blurb warms with the rest rather than staying muted while everything
   around it moves. */
.brand-tile__blurb {
  color: var(--colour-text-2);
  font-size: var(--text-sm);
  transition: color var(--motion-duration) var(--motion-ease);
}

.brand-tile:hover .brand-tile__blurb,
.brand-tile:focus-visible .brand-tile__blurb {
  color: var(--colour-text);
}

/* ---- The mark --------------------------------------------------------- */

.brand-tile__logo {
  display: grid;
  place-items: center;
  min-height: 4rem;
  border: 1px dashed var(--colour-border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

/* The dashed frame is the EMPTY state — it says "a logo belongs here". Once
   one is uploaded the frame has nothing left to say, and a real mark inside a
   dashed box looks like an unfinished layout. */
.brand-tile__logo--filled {
  border: 0;
  place-items: center start;
  min-height: 3.4rem;
}

/* The marks arrive at wildly different proportions — a wordmark eight times
   wider than it is tall next to a near-square badge. Constraining height and
   letting width follow is what makes a row of them look like a set. */
.brand-tile__mark {
  display: block;
  max-height: 2.6rem;
  width: auto;
  max-width: min(100%, 13rem);
  object-fit: contain;
  object-position: left center;
}

.brand-tile--own .brand-tile__mark {
  max-height: 3.1rem;
}

/* ---- Reduced motion ---------------------------------------------------
   The chevron still arrives, because it is the thing that says the tile is
   a link — but it arrives in place, with no travel and no lift.        */
@media (prefers-reduced-motion: reduce) {
  .brand-tile__go {
    transform: translate(-0.55rem, -50%);
  }

  .brand-tile:hover,
  .brand-tile:focus-visible {
    transform: none;
  }
}

/* ---- components/header.css ---- */
/* ============================================================
   HEADER

   Transparent bar sitting directly on the page background.
   The logo keeps its own brand inks and is separated from the
   ground by an outline rather than by a panel behind it.
   ============================================================ */

.utility-bar {
  background: transparent;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--colour-header-muted);
}

.utility-bar a { color: var(--colour-header-text); }

.utility-bar__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
  align-items: center;
  justify-content: space-between;
  padding-block: var(--space-xs);
}

.utility-bar__facts { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-lg); }

@media (max-width: 63.99rem) {
  .utility-bar { display: none; }
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--colour-header-bg);
  color: var(--colour-header-text);
  transition: background var(--motion-duration) var(--motion-ease);
}

/* Transparent over the hero; grounded the moment anything scrolls beneath
   it. Without this the light nav sits on whatever passes under — the pale
   manufacture block leaves it unreadable. Opaque rather than a wash,
   because what passes underneath ranges from near-black to near-white. */
.site-header.is-grounded {
  background: var(--colour-scrim);
  backdrop-filter: blur(14px);
  border-bottom: var(--border-width) solid var(--colour-header-border);
}


.site-header__inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-lg);
  padding-block: var(--space-md);
}

@media (max-width: 63.99rem) {
  .site-header__inner {
    /* Nav and the desktop search are hidden here, so two columns — a 1fr
       middle column would stretch the toggle across the bar. */
    grid-template-columns: auto auto;
    justify-content: space-between;
  }
}

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

.site-brand {
  display: flex;
  align-items: center;
  text-decoration: none;
  flex: 0 0 auto;
}

.site-brand__name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-md);
  letter-spacing: var(--tracking-tight);
  color: var(--colour-header-text);
}

.site-brand__logo {
  height: var(--logo-height, 2rem);
  width: auto;
  max-width: 100%;
}

/* Inlined SVG so the two brand inks are CSS properties: the green reverses
   for the dark ground while the orange stays as it is. */
.site-brand__logo--inline {
  --logo-ink: var(--logo-ink-header);
  --logo-mark: var(--logo-mark-header);

  display: block;
  height: var(--logo-height, 2rem);
}

.site-brand__logo--inline svg {
  display: block;
  height: 100%;
  width: auto;
  overflow: visible;
}

@media (min-width: 64rem) {
  .site-brand__logo,
  .site-brand__logo--inline { --logo-height: 2.5rem; }
}

/* ---- Navigation ---- */

.primary-nav__list {
  display: flex;
  gap: var(--space-2xl);
  align-items: center;
}

.primary-nav a {
  color: var(--colour-header-text);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
}

.primary-nav a:hover { color: var(--colour-accent); }
.primary-nav a[aria-current="page"] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }

.nav-toggle {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-md);
  background: transparent;
  color: var(--colour-header-text);
  border: var(--border-width) solid var(--colour-header-border);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

/* ---- Search ---- */

.header-search { display: flex; align-items: center; }

.header-search__field {
  width: clamp(10rem, 18vw, 15rem);
  padding: var(--space-xs) var(--space-sm);
  min-height: 44px;
  background: rgb(255 255 255 / 6%);
  border: var(--border-width) solid var(--colour-header-border);
  border-radius: var(--radius);
  color: var(--colour-header-text);
  font-size: var(--text-sm);
}

.header-search__field::placeholder { color: var(--colour-header-muted); }

.header-search__field:hover { border-color: var(--colour-text-muted); }

/* ---- Drawer ---- */

@media (max-width: 63.99rem) {
  .primary-nav,
  .header-actions__desktop { display: none; }

  .primary-nav[data-open="true"] {
    display: block;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    /* Opaque: it sits over page content. */
    background: var(--colour-header-raised);
    border-block: var(--border-width) solid var(--colour-header-border);
    padding: var(--space-md) var(--gutter) var(--space-lg);
  }

  .primary-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .primary-nav__list a { display: block; padding-block: var(--space-md); border-bottom: var(--border-width) solid var(--colour-header-border); }
  .primary-nav__list li:last-child a { border-bottom: 0; }

  .drawer-extras { margin-top: var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); }
  .drawer-extras .header-search__field { width: 100%; }
}

@media (min-width: 64rem) {
  .nav-toggle, .drawer-extras { display: none; }
  .primary-nav { display: block; justify-self: center; }
}

/* ---- components/footer.css ---- */
.site-footer {
  background: var(--colour-recess);
  border-top: 2px solid var(--colour-border-strong);
  padding-block: var(--space-2xl) var(--space-lg);
  margin-top: var(--space-section);
}

.site-footer__grid {
  display: grid;
  gap: var(--space-xl);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}

.site-footer__heading {
  font-size: var(--text-md);
  margin-bottom: var(--space-sm);
}

.site-footer a { color: var(--colour-text-2); font-size: var(--text-sm); }

.site-footer__legal {
  margin-top: var(--space-2xl);
  padding-top: var(--space-md);
  border-top: var(--border-width) solid var(--colour-border);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* ---- components/table-standards.css ---- */
/* ============================================================
   STANDARDS TABLE

   Real tabular data, so a real <table> with a caption and scoped
   headers — a screen reader should be told this is a table.

   On narrow screens it becomes stacked rows rather than a
   sideways-scrolling table, because horizontally scrolling
   compliance information is how people miss compliance
   information.
   ============================================================ */

.standards {
  width: 100%;
  border-collapse: collapse;
  border: var(--border-width) solid var(--colour-border-strong);
  background: var(--colour-surface);
}

.standards caption {
  caption-side: top;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
  padding-bottom: var(--space-xs);
}

.standards th, .standards td {
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  border-bottom: var(--border-width) solid var(--colour-border);
  vertical-align: top;
  font-size: var(--text-sm);
}

.standards thead th {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
  background: var(--colour-surface-2);
}

.standards tbody tr:last-child td { border-bottom: 0; }

.standards__scope { color: var(--colour-text-muted); }

@media (max-width: 40rem) {
  .standards thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .standards, .standards tbody, .standards tr, .standards td { display: block; width: 100%; }
  .standards tr { border-bottom: var(--border-width) solid var(--colour-border); padding: var(--space-sm) 0; }
  .standards tr:last-child { border-bottom: 0; }
  .standards td { border: 0; padding-block: var(--space-2xs); }
  .standards td::before {
    content: attr(data-label) ": ";
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--colour-text-muted);
  }
}

/* ---- components/page-hero.css ---- */
/* ============================================================
   PAGE HERO

   Built like the homepage hero: photograph behind, copy on its
   dark side, and a scrim that guarantees the contrast rather
   than trusting the picture to stay dark where the text sits.
   ============================================================ */

.page-hero {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-height: clamp(22rem, 52vh, 34rem);
  padding-block: var(--space-3xl);
  /* No ground of its own. Painting one meant the photograph could only ever
     fade to a flat colour, and the page behind is a gradient with embers
     moving in it — so the "blend" was a flat rectangle sitting on a
     gradient, with a visible seam where it ended. */
}

.page-hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;

  /* Feathered by masking the image away, not by laying a colour over it.
     Fading to transparent lets the page's own gradient show through, which
     is the only way this can actually blend; fading to a colour can only
     ever approximate the one pixel row it was matched against. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgb(0 0 0 / 55%) 80%, rgb(0 0 0 / 12%) 93%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgb(0 0 0 / 55%) 80%, rgb(0 0 0 / 12%) 93%, transparent 100%);
}

.page-hero__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 72% center;
}

/* Horizontal on wide screens so the copy column stays dark while the
   photograph opens to the right, with a short fade at the bottom so the
   block joins the page rather than ending on a line. */
.page-hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Contrast only. The fade is the mask's job now. */
  background:
    linear-gradient(180deg, rgb(0 0 0 / 92%) 0%, rgb(0 0 0 / 0%) 18%, transparent 100%),
    linear-gradient(90deg, rgb(6 18 15 / 94%) 0%, rgb(6 18 15 / 88%) 32%, rgb(6 18 15 / 52%) 58%, rgb(6 18 15 / 14%) 80%, transparent 100%);
}

.page-hero__inner { width: 100%; }
.page-hero--media .page-hero__inner > * { max-width: 38rem; }

.page-hero__title { font-size: var(--text-3xl); }

.page-hero__intro {
  font-size: var(--text-md);
  color: var(--colour-text-2);
}

.page-hero__intro p { max-width: 34rem; }

/* ---- Overflow variant ----

   The photograph is sized past the block and the block does not clip it, so
   it breaks the grid and bleeds into the space below instead of stopping on
   an edge. The section that follows is pushed clear of it. */

.page-hero--overflow {
  --hero-bleed: 15rem;
  overflow: visible;
  min-height: clamp(26rem, 64vh, 42rem);
  /* No bottom margin: the next section is pulled up into the bleed instead,
     so the photograph is the ground under both the hero copy and the
     heading that follows it. */
  margin-bottom: 0;
}

/* The section after the hero rises into the overflow. The hero's media sits
   at a negative z-index inside the hero, and this section carries no ground
   of its own, so the photograph reads straight through it.

   The pull is its own value, not the bleed: the bleed governs how far the
   photograph extends, the pull governs how far the copy moves. Tying them
   together drove the heading straight into the hero's own paragraph. */
.page-hero--overflow + .section {
  margin-top: calc(var(--hero-pull, 8rem) * -1);
  padding-top: var(--space-lg);
  position: relative;
}

.page-hero--overflow .page-hero__media {
  /* Taller than its block, hanging past the bottom. */
  inset: 0 0 calc(var(--hero-bleed, 15rem) * -1) 0;
  overflow: visible;
}

.page-hero--overflow .page-hero__image {
  /* Larger than the frame as well, so it reads as overflowing rather than
     merely tall. */
  width: 108%;
  max-width: none;
  margin-left: -4%;
  object-position: 64% center;
}

/* The bleed has to end somewhere: it fades out rather than being cut, which
   would just move the hard edge lower down. */
/* A longer mask than the standard hero: the copy that follows sits inside
   this fade, so the photograph has to be well out of the way by the time it
   arrives. */
.page-hero--overflow .page-hero__media {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 46%, rgb(0 0 0 / 62%) 68%, rgb(0 0 0 / 22%) 84%, transparent 96%);
  mask-image: linear-gradient(180deg, #000 0%, #000 46%, rgb(0 0 0 / 62%) 68%, rgb(0 0 0 / 22%) 84%, transparent 96%);
}

@media (max-width: 52rem) {
  .page-hero__image { object-position: 66% center; }

  .page-hero__media::after,
  .page-hero--overflow .page-hero__media::after {
    background: linear-gradient(
      180deg,
      rgb(0 0 0 / 100%) 0%,
      rgb(0 0 0 / 52%) 18%,
      rgb(6 18 15 / 78%) 60%,
      rgb(6 18 15 / 96%) 100%
    );
  }

  .page-hero--overflow { --hero-bleed: 4rem; }

  /* Set on the section itself, not the hero: a custom property inherits
     down the tree, and this element is the hero's SIBLING — declaring it on
     .page-hero--overflow never reached here, which is why the desktop value
     was only ever the fallback. */
  .page-hero--overflow + .section { margin-top: -2rem; }
  .page-hero--overflow .page-hero__image { width: 100%; margin-left: 0; }
}

/* A tinted panel immediately below a hero undoes the feathering: the
   photograph dissolves into the page, and then the page steps a shade
   lighter on a hard horizontal line exactly where it finished. The section
   keeps its spacing and loses only the tint. */
.page-hero + .section--surface {
  background: transparent;
}

/* ---- components/showcase.css ---- */
/* ============================================================
   SHOWCASE

   Three credibility panels that open into the manufacture /
   curate stage as you scroll into them.

   One grid, four cells, fixed areas. Panel 3 spans both rows in
   the right column from the outset, so only the row heights
   animate — swapping grid-areas would jump, and a track list
   mixing fr with a length would not interpolate at all.

   Base state (no JS, reduced motion, or under 62rem) is a band
   of panels with the stage below. Everything keyed to
   .is-enhanced is enhancement, never a requirement for reading.
   ============================================================ */

.showcase {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto;
  /* Transparent, and unbounded: no rules top or bottom either, or the band
     reads as a block sitting on the page rather than part of it. The page's
     gradient and embers carry straight through; hovering is the only thing
     that reveals three regions. */
}

/* ---- The pin ----

   The block is held against the nav while it opens. Expanding in place did
   not work: by the time a delayed trigger fired, the block's top had already
   travelled above the viewport, so the expansion grew downward from a top
   edge nobody could see and the manufacture heading opened off-screen.

   Pinned, the top edge is exactly at the nav when it opens, and the hold
   itself is what gives the credentials their moment. */
.showcase-pin.is-pinning {
  /* Runway for the expanded block plus time to read it. No hold: the
     credentials have already had the hero's screen to themselves. */
  min-height: calc(100svh + 55svh);
}

.showcase-pin.is-pinning > .showcase {
  position: sticky;
  top: var(--nav-height, 4.75rem);
}

.panel--1 { grid-area: 1 / 1 / 2 / 2; }
.panel--2 { grid-area: 1 / 2 / 2 / 3; }
.panel--3 { grid-area: 1 / 3 / 3 / 4; }
.showcase__stage { grid-area: 2 / 1 / 3 / 3; }

/* Enhanced, the row height governs — the band min-height would otherwise
   fight it. */
.showcase.is-enhanced .panel { min-height: 0; }

/* ---- Panel shell ---- */

.panel {
  position: relative;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--space-sm);
  padding: var(--space-2xl) var(--space-lg);
  min-height: clamp(13rem, 26vh, 17rem);
  text-align: center;
  text-decoration: none;
  color: var(--colour-text);
  /* No divider. The band reads as one continuous block; hovering is what
     reveals that it is three regions. */
  overflow: hidden;
  transition:
    background var(--motion-duration) var(--motion-ease),
    color var(--motion-duration) var(--motion-ease),
    min-height 1200ms var(--motion-ease-out),
    padding 1200ms var(--motion-ease-out);
}

/* The wash rises from the floor of the panel rather than flooding it, so the
   swap reads as a deliberate move and not a state toggle. */
.panel::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--colour-accent);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 420ms var(--motion-ease);
}

a.panel:hover::before,
a.panel:focus-visible::before { transform: scaleY(1); }

/* Focus gets everything hover gets — a hover-only reveal is invisible to
   anyone using a keyboard, and these panels are this block's only links. */
a.panel:hover,
a.panel:focus-visible { color: var(--colour-accent-ink); }

/* ---- Faces ---- */

.panel__faces {
  position: relative;
  display: grid;
  grid-template-areas: "face";
  align-items: center;
  justify-items: center;
  width: 100%;
}

.panel__face {
  grid-area: face;
  display: grid;
  justify-items: center;
  gap: var(--space-2xs);
  transition:
    opacity 320ms var(--motion-ease),
    transform 420ms var(--motion-ease);
}

.panel__value {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
  transition: font-size 1200ms var(--motion-ease-out);
}

.panel__label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.66;
}

.panel__face--cta {
  opacity: 0;
  transform: translateY(10px);
  gap: var(--space-md);
}

.panel__cta {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: var(--leading-snug);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  max-width: 15ch;
}

.panel__arrow {
  width: 3rem;
  height: auto;
  /* Draws itself in behind the label, a beat after it. */
  transform: translateX(-0.75rem);
  opacity: 0;
  transition:
    transform 460ms var(--motion-ease) 90ms,
    opacity 300ms var(--motion-ease) 90ms;
}

a.panel:hover .panel__face--stat,
a.panel:focus-visible .panel__face--stat {
  opacity: 0;
  transform: translateY(-10px);
}

a.panel:hover .panel__face--cta,
a.panel:focus-visible .panel__face--cta {
  opacity: 1;
  transform: translateY(0);
}

a.panel:hover .panel__arrow,
a.panel:focus-visible .panel__arrow {
  opacity: 1;
  transform: translateX(0);
}

/* ---- Stage ----

   The drawing behind this block is near-white, so the ink inverts with it.
   Measured on the paper: text 15.0:1, muted 6.5:1 — but the orange accent
   only 2.1:1, which is why the button here is dark rather than accented. */

.showcase__stage { overflow: hidden; }

.showcase__stage--image {
  --colour-text: #121a17;
  --colour-text-2: #26302c;
  --colour-text-muted: #4a5651;

  position: relative;
  background-color: #ececec;
  background-image: var(--stage-image);
  background-size: cover;
  /* Shifted right: a lower position value slides the image's left edge into
     view, moving the artwork across the block. */
  background-position: 28% center;
  color: var(--colour-text);
}

/* The copy sits top-right and bottom-left, so the ground has to follow that
   diagonal rather than fade one way across the block. Two soft pools under
   the text, and a light overall wash so the paper never disappears —
   leaving the drawing to read through the middle. */
.showcase__stage--image::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 62% at 76% 20%, rgb(236 236 236 / 96%) 0%, rgb(236 236 236 / 82%) 42%, rgb(236 236 236 / 0%) 74%),
    radial-gradient(ellipse 58% 58% at 22% 82%, rgb(236 236 236 / 96%) 0%, rgb(236 236 236 / 82%) 42%, rgb(236 236 236 / 0%) 74%),
    linear-gradient(rgb(236 236 236 / 28%), rgb(236 236 236 / 28%));
}

.showcase__stage--image .showcase__stage-inner { position: relative; }

/* Accent on paper is 2.1:1. The button borrows the ink instead. */
.showcase__stage--image .button--secondary {
  color: #121a17;
  border-color: #121a17;
}

.showcase__stage--image .button--secondary:hover {
  background: #121a17;
  color: #ececec;
}

.showcase__stage-inner {
  height: 100%;
  display: grid;
  align-content: center;
  gap: var(--space-2xl);
  padding: var(--space-3xl) var(--gutter);
}

/* Copy arrives after the layout has mostly settled, so the block reads as
   opening and then filling rather than everything at once. */
.showcase.is-enhanced .showcase__stage-inner {
  opacity: 0;
  transform: translateY(12px);
  transition:
    opacity 520ms var(--motion-ease) 0ms,
    transform 520ms var(--motion-ease) 0ms;
}

.showcase.is-expanded .showcase__stage-inner {
  opacity: 1;
  transform: none;
  transition-delay: 520ms;
}

/* A diagonal: manufacture high on the right, curate low on the left, with
   the drawing running between them. */
.showcase__stage--image .showcase__stage-inner {
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: 1fr 1fr;
  align-content: stretch;
  gap: var(--space-lg);
}

.stage-block { max-width: 30rem; }

.showcase__stage--image .stage-block--manufacture {
  grid-column: 6 / 13;
  grid-row: 1;
  align-self: start;
  justify-self: end;
}

.showcase__stage--image .stage-block--curate {
  grid-column: 1 / 7;
  grid-row: 2;
  align-self: end;
  justify-self: start;
}

.showcase__stage h2 { font-size: var(--text-2xl); }

/* ============================================================
   ENHANCED

   The rows are animated as typed custom properties rather than by
   transitioning grid-template-rows directly. A track list does not
   interpolate between a clamp() and a calc() — measured: it holds at
   the start value and never moves. Registering the two heights as
   <length> gives the browser something it can genuinely interpolate,
   and the grid simply reads them.
   ============================================================ */

@property --row-band {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@property --row-stage {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.showcase.is-enhanced {
  /* A band again, sharing the hero's screen rather than taking one. The
     void that used to open beneath it is avoided by expanding while the
     block is still descending — see the trigger — so it grows to fill the
     screen before the gap can appear. */
  --row-band: var(--credentials-band, 15rem);
  --row-stage: 0px;

  grid-template-rows: var(--row-band) var(--row-stage);
}

/* The transition is withheld until a frame after enhancement. Declaring it on
   .is-enhanced meant the very act of enhancing animated the rows up from the
   @property initial value of 0 — the band slid open on every page load. */
.showcase.is-animatable {
  transition:
    --row-band 1200ms var(--motion-ease-out),
    --row-stage 1200ms var(--motion-ease-out);
}

  /* Set above with the fade-out. Height stops at the nav, so the block sits
     exactly between the bar and the bottom of the viewport. */
.showcase.is-expanded {
  --row-stage: calc(100svh - var(--nav-height, 4.75rem));
}

/* ---- The other two leave ----
   Expanded, the block is the stage and the tall column; the first two
   statistics fade out and their row closes to nothing. visibility, not just
   opacity, so they leave the tab order with the layout. */

.showcase.is-expanded {
  --row-band: 0px;
}

.showcase.is-expanded .panel--1,
.showcase.is-expanded .panel--2 {
  min-height: 0;
  padding: 0;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 480ms var(--motion-ease),
    visibility 0s linear 480ms,
    padding 1200ms var(--motion-ease-out);
}

/* ---- The tall column ----
   Given room, it stops being a statistic in a box and becomes the panel the
   eye lands on: the figure at display size, the label under it, weight
   carried low. */

/* Given the full column it takes a ground of its own — the drawing beside it
   is near-white, and a transparent column against that reads as a gap rather
   than a panel. */
.showcase.is-expanded .panel--3 {
  align-content: center;
  justify-items: center;
  text-align: center;
  padding: var(--space-3xl) var(--space-2xl);
  background: var(--colour-recess);
}

.showcase.is-expanded .panel--3 .panel__faces { justify-items: center; }

.showcase.is-expanded .panel--3 .panel__face {
  justify-items: center;
  gap: var(--space-sm);
}

.showcase.is-expanded .panel--3 .panel__value {
  font-size: var(--text-3xl);
  white-space: normal;
}

.showcase.is-expanded .panel--3 .panel__cta {
  font-size: var(--text-xl);
  max-width: 12ch;
}

.showcase.is-expanded .panel--3 .panel__arrow { width: 4rem; }

/* ---- Narrow ----
   The three-into-one choreography needs width it does not have here. Panels
   stack, the stage follows, nothing animates. */

@media (max-width: 61.99rem) {
  .showcase-pin.is-pinning { min-height: 0; }
  .showcase-pin.is-pinning > .showcase { position: static; }

  .showcase,
  .showcase.is-enhanced,
  .showcase.is-expanded {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    transition: none;
  }

  .panel--1, .panel--2, .panel--3, .showcase__stage { grid-area: auto; }

  .panel {
    min-height: 0;
    padding: var(--space-2xl) var(--gutter);
    border-right: 0;
    border-bottom: var(--border-width) solid var(--colour-border-strong);
  }

  .panel--3 { border-bottom: 0; }

  .showcase__stage-inner { padding-block: var(--space-2xl); }
}

/* ---- components/solution-picker.css ---- */
/* ============================================================
   SOLUTION PICKER

   Two interactions per card, kept as two sibling elements: a
   label bound to the checkbox, and a link. Nesting the link
   inside the label would make "view more" toggle the box on the
   way through, and nesting interactive elements is invalid
   besides.
   ============================================================ */

.solution-picker {
  display: grid;
  gap: var(--space-xl);
}

/* The intro section and the picker are separate sections, so their padding
   stacked into a gap far larger than either intended. */
.section:has(> .solution-picker) { padding-top: 0; }

/* Except when it is the section pulled up into an overflowing hero. There
   the zero closes the gap the pull already narrowed, and the hero's own
   paragraph ends up sitting directly on this heading. */
.page-hero--overflow + .section:has(> .solution-picker) {
  padding-top: var(--space-2xl);
}

.solution-picker__head { display: grid; gap: var(--space-sm); }

.solution-picker__lede {
  color: var(--colour-text-2);
  font-size: var(--text-md);
  max-width: 58ch;
}

.solution-grid {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}

.solution-card {
  position: relative;
  display: flex;
  border: var(--border-width) solid var(--colour-border-strong);
  background: var(--colour-recess);
  transition:
    border-color var(--motion-duration) var(--motion-ease),
    background var(--motion-duration) var(--motion-ease);
}

.solution-card__label {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "tick  body"
    "tick  state";
  gap: var(--space-xs) var(--space-md);
  width: 100%;
  padding: var(--space-lg);
  padding-bottom: var(--space-3xl);
  cursor: pointer;
}

.solution-card__tick {
  grid-area: tick;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border: var(--border-width) solid var(--colour-border-strong);
  color: var(--colour-accent-ink);
  transition:
    background var(--motion-fast) var(--motion-ease),
    border-color var(--motion-fast) var(--motion-ease);
}

.solution-card__tick svg {
  width: 0.85rem;
  height: 0.85rem;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity var(--motion-fast) var(--motion-ease), transform var(--motion-fast) var(--motion-ease);
}

.solution-card__body { grid-area: body; display: grid; gap: var(--space-2xs); }

.solution-card__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
}

.solution-card__summary { color: var(--colour-text-2); font-size: var(--text-sm); }

.solution-card__state {
  grid-area: state;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

.solution-card__state-on { display: none; }

/* ---- Selected ---- */

.solution-card__input:checked ~ .solution-card__label .solution-card__tick {
  background: var(--colour-accent);
  border-color: var(--colour-accent);
}

.solution-card__input:checked ~ .solution-card__label .solution-card__tick svg {
  opacity: 1;
  transform: scale(1);
}

.solution-card__input:checked ~ .solution-card__label .solution-card__state-off { display: none; }
.solution-card__input:checked ~ .solution-card__label .solution-card__state-on {
  display: inline;
  color: var(--colour-accent);
}

.solution-card:has(.solution-card__input:checked) {
  border-color: var(--colour-accent);
  background: var(--colour-accent-soft);
}

/* The checkbox is visually hidden, so its focus ring has to be drawn by
   the card — otherwise keyboard users get no indication at all. */
.solution-card__input:focus-visible ~ .solution-card__label {
  outline: 3px solid var(--colour-focus);
  outline-offset: -3px;
}

.solution-card:hover { border-color: var(--colour-text-muted); }

/* ---- View more ---- */

.solution-card__more {
  position: absolute;
  right: var(--space-lg);
  bottom: var(--space-lg);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 0;
  padding: var(--space-2xs) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-accent);
  text-decoration: none;
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--motion-duration) var(--motion-ease), transform var(--motion-duration) var(--motion-ease);
}

.solution-card__more svg { width: 1.5rem; height: auto; }

/* Revealed on hover, and on focus — a hover-only control is unreachable
   from the keyboard, and this one is the only route to the detail page. */
.solution-card:hover .solution-card__more,
.solution-card:focus-within .solution-card__more,
.solution-card__more:focus-visible {
  opacity: 1;
  transform: none;
}

.solution-card__more:hover { text-decoration: underline; text-underline-offset: 3px; }

/* Touch has no hover, so it is simply always visible there. */
@media (hover: none) {
  .solution-card__more { opacity: 1; transform: none; }
}

/* ---- Submit bar ---- */

.solution-picker__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-top: var(--space-lg);
  border-top: var(--border-width) solid var(--colour-border-strong);
}

.solution-picker__count {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.solution-picker__count[data-has-selection="true"] { color: var(--colour-accent); }

/* ============================================================
   THE TRACK

   Four steps on a rule rather than four boxes: numbered nodes
   sitting on a line that runs the width of the section.
   ============================================================ */

.track {
  display: grid;
  gap: var(--space-xl);
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: track;
}

.track__step {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-md);
  align-items: start;
}

.track__marker {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  border: var(--border-width) solid var(--colour-border-strong);
  border-radius: 999px;
  background: var(--colour-ground);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--colour-accent);
}

.track__body { display: grid; gap: var(--space-2xs); }

.track__label {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  letter-spacing: var(--tracking-tight);
}

.track__detail { color: var(--colour-text-2); font-size: var(--text-sm); max-width: 34ch; }

/* Vertical rule joining the nodes on narrow screens. */
@media (max-width: 61.99rem) {
  .track__step:not(:last-child)::before {
    content: "";
    position: absolute;
    left: 1.5rem;
    top: 3rem;
    bottom: calc(var(--space-xl) * -1);
    width: var(--border-width);
    background: var(--colour-border-strong);
  }
}

@media (min-width: 62rem) {
  .track {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-lg);
  }

  .track__step {
    grid-template-columns: 1fr;
    gap: var(--space-md);
    padding-top: var(--space-md);
  }

  /* One rule through all four nodes, drawn per step so it stops at the last. */
  .track__step:not(:last-child)::before {
    content: "";
    position: absolute;
    left: 3rem;
    right: calc(var(--space-lg) * -1);
    top: calc(var(--space-md) + 1.5rem);
    height: var(--border-width);
    background: var(--colour-border-strong);
  }

  .track__detail { max-width: none; }
}

/* The selection carried through to the consultation page. */
.picked-summary {
  display: grid;
  gap: var(--space-xs);
  padding: var(--space-md) var(--space-lg);
  border-left: 3px solid var(--colour-accent);
  background: var(--colour-recess);
}

.picked-summary__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--font-display);
  font-weight: 600;
}

/* ---- components/quote-bar.css ---- */
/* ============================================================
   QUOTE BAR

   A cart-shaped mechanism with no prices. Hidden until the list
   has something in it, and its count is announced politely so a
   screen-reader user knows an item was added without the focus
   being stolen.
   ============================================================ */

.quote-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 50;
  background: var(--colour-raised);
  border-top: 2px solid var(--colour-accent);
  box-shadow: var(--shadow-raised);
  transform: translateY(101%);
  transition: transform var(--motion-duration) var(--motion-ease);
}

.quote-bar[data-visible="true"] { transform: translateY(0); }

.quote-bar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
}

.quote-bar__count { font-size: var(--text-sm); font-weight: 600; }
.quote-bar__note  { font-size: var(--text-sm); color: var(--colour-text-muted); }

@media (max-width: 34rem) {
  .quote-bar__note { display: none; }
  .quote-bar__inner { flex-wrap: nowrap; }
  .quote-bar .button { flex: 1 1 auto; }
}

/* Keep the fixed bar from covering the end of the page. */
body.has-quote-items { padding-bottom: 5rem; }

.quote-toggle[data-in-list="true"] { background: var(--colour-state-ok); border-color: var(--colour-state-ok); color: var(--colour-accent-ink); }

/* Two ways to use the same list. Side by side on a wide bar, stacked when
   there is no room, and never wider than the bar itself. */
.quote-bar__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: center;
}

@media (max-width: 40rem) {
  .quote-bar__actions {
    width: 100%;
  }

  .quote-bar__actions .button {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* ---- components/form.css ---- */
.field { display: flex; flex-direction: column; gap: var(--space-2xs); }

.field__label { font-size: var(--text-sm); font-weight: 600; }

.field__hint { font-size: var(--text-sm); color: var(--colour-text-muted); }

.field input[type="text"],
.field input[type="email"],
.field input[type="tel"],
.field input[type="number"],
.field select,
.field textarea,
.dax-form input[type="text"],
.dax-form input[type="email"],
.dax-form input[type="tel"],
.dax-form select,
.dax-form textarea {
  width: 100%;
  padding: var(--space-sm);
  min-height: 44px;
  background: var(--colour-recess);
  border: var(--border-width) solid var(--colour-border-strong);
  border-radius: var(--radius);
  color: var(--colour-text);
}

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

/* Errors are never signalled by colour alone. */
.field--error input,
.field--error select,
.field--error textarea { border-color: var(--colour-state-off); border-width: 2px; }

.field__error {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--colour-state-off);
}
.field__error::before { content: "\2717\00a0"; }

.form-errors {
  border: 2px solid var(--colour-state-off);
  background: var(--colour-state-off-bg);
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
}

.form-grid {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}

/* ---- pages/home.css ---- */
/* ============================================================
   HERO

   The photograph is the background of the block, not a slot
   beside it. Its dark left side is where the copy sits, and a
   scrim guarantees that rather than relying on it.
   ============================================================ */

.hero {
  position: relative;
  isolation: isolate;
  padding-block: var(--space-2xl) var(--space-xl);
}

.hero--media {
  display: grid;
  align-items: center;
  /* Sized so the hero and the credentials band together make one screen.
     The band sits below the image rather than getting a screen of its own. */
  min-height: max(
    26rem,
    calc(100svh - var(--nav-height, 4.75rem) - var(--utility-height, 2.2rem) - var(--credentials-band, 15rem))
  );
  padding-block: var(--space-2xl);
  background: var(--colour-hero-ground);
  overflow: hidden;
}

/* Below the pinning breakpoint the credentials stack into a column far
   taller than a band, so there is no pair of blocks to fit into one screen
   and nothing to subtract. */
@media (max-width: 61.99rem) {
  .hero--media {
    min-height: clamp(26rem, 68vh, 40rem);
  }
}

.hero__media {
  position: absolute;
  /* Anchored to the bottom, not stretched to fill: the block is taller than
     the photograph and the extra height above it is simply black. */
  inset: auto 0 0;
  z-index: -1;
  overflow: hidden;
  line-height: 0;
}

.hero__image {
  width: 100%;
  /* Natural aspect ratio — the image is never scaled to the block. */
  height: auto;
}

/* Scrim. Horizontal by default so the copy column stays dark while the
   photograph opens up to the right, with a short fade at the bottom so the
   block joins the page instead of ending on a hard edge. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    /* Feathers the image's top edge into the black above it, so the join
       is a transition rather than a line, and fades the bottom into the
       page below. */
    linear-gradient(
      180deg,
      rgb(0 0 0 / 100%) 0%,
      rgb(0 0 0 / 0%) 16%,
      transparent 60%,
      rgb(6 18 15 / 55%) 88%,
      rgb(6 18 15 / 95%) 100%
    ),
    linear-gradient(
      90deg,
      rgb(6 18 15 / 94%) 0%,
      rgb(6 18 15 / 89%) 34%,
      rgb(6 18 15 / 58%) 60%,
      rgb(6 18 15 / 15%) 82%,
      transparent 100%
    );
}

/* Narrow screens: the copy spans the full width, so the scrim has to as
   well — a horizontal fade would leave text over the bright figure. */
/* Narrow screens: at its natural ratio the photograph would be a thin strip
   under a tall black field, so it gets a floor and crops instead. Cropping
   changes what you see; it still never distorts the image. */
@media (max-width: 52rem) {
  .hero__media { height: 48%; }

  .hero__image {
    height: 100%;
    object-fit: cover;
    object-position: 66% center;
  }

  .hero__media::after {
    background: linear-gradient(
      180deg,
      rgb(0 0 0 / 100%) 0%,
      rgb(0 0 0 / 52%) 20%,
      rgb(6 18 15 / 78%) 62%,
      rgb(6 18 15 / 95%) 100%
    );
  }
}

.hero__inner { width: 100%; }

.hero--media .hero__inner > * { max-width: 38rem; }

.hero__title { font-size: var(--text-3xl); }
.hero__subtitle { font-size: var(--text-md); color: var(--colour-text-2); }
.hero__actions { margin-top: var(--space-xs); }
.hero__note { font-size: var(--text-sm); color: var(--colour-text-muted); }

.stats {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  text-align: center;
}
.stat__value { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 600; }
.stat__label { font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--colour-text-muted); }

.steps { display: grid; gap: var(--space-md); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); counter-reset: step; }
.step { padding: var(--space-md); background: var(--colour-surface); border: var(--border-width) solid var(--colour-border); }
.step__number { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--colour-text-muted); letter-spacing: 0.1em; }

/* ---- pages/products.css ---- */
/* ============================================================
   PROTECTION CATEGORIES

   A bento rather than a queue: the spans repeat on a six-item
   cycle, so the rhythm holds whatever number of categories the
   client ends up with — no positions tied to specific terms.

   No grid-auto-flow: dense. It would fill the trailing gap, but
   at the cost of visual order drifting from DOM order, which
   leaves keyboard focus jumping around the grid.
   ============================================================ */

.category-grid {
  display: grid;
  gap: var(--space-sm);
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: minmax(9rem, auto);
}

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

@media (min-width: 64rem) {
  .category-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-md);
  }

  /* Scoped to the grid. Unscoped, these spans leaked onto every other
     .category-card on the site — the homepage programme list among them,
     which turned into a bento nobody asked for. */
  .category-grid > .category-card:nth-child(6n + 1) {
    grid-column: span 2;
    grid-row: span 2;
  }

  .category-grid > .category-card:nth-child(6n + 4) { grid-column: span 2; }
}

/* ---- Card ----

   Frosted glass, built from the same materials as the nav bar: the same
   scrim tint over a backdrop blur, so the page gradient reads through it
   rather than being blocked. Over that, an etch — fine diagonal hatching
   and a grain — which is what stops a translucent panel looking like a
   flat tint.

   Accent is the ACTIVE state, not the resting one: the wash rises from the
   floor of the card, the same move used on the credibility panels, and the
   ink inverts to dark. Both states measure well clear of 4.5:1. */
.category-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-lg);

  background: var(--colour-scrim);
  backdrop-filter: blur(14px) saturate(115%);
  border: var(--border-width) solid var(--colour-border-strong);

  color: var(--colour-text);
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  transition:
    color var(--motion-duration) var(--motion-ease),
    border-color var(--motion-duration) var(--motion-ease);
}

/* The accent, rising. */
.category-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--colour-accent);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 420ms var(--motion-ease);
}

/* The etch. Hatching plus a fine grain, sitting over whichever ground is
   showing, so the panel reads as one surface being lit rather than two
   different materials swapping. */
.category-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.55;
  transition: opacity var(--motion-duration) var(--motion-ease);
  background-image:
    repeating-linear-gradient(
      135deg,
      rgb(255 255 255 / 5%) 0 1px,
      transparent 1px 8px
    ),
    repeating-linear-gradient(
      45deg,
      rgb(255 255 255 / 2.5%) 0 1px,
      transparent 1px 14px
    ),
    url("data:image/svg+xml,%3Csvg viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='e'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23e)' opacity='.5'/%3E%3C/svg%3E");
  background-size: auto, auto, 140px 140px;
  mix-blend-mode: soft-light;
}

/* Content clears both layers. */
.category-card > * { position: relative; z-index: 2; }

.category-card:hover,
.category-card:focus-visible {
  color: var(--colour-accent-ink);
  border-color: var(--colour-accent);
}

.category-card:hover::before,
.category-card:focus-visible::before { transform: scaleY(1); }

/* The etch stays, but quieter — the surface is lit, not replaced. */
.category-card:hover::after,
.category-card:focus-visible::after { opacity: 0.3; }

/* An arrow that is only implied until the card is engaged. */
.category-card__go {
  position: absolute;
  right: var(--space-lg);
  bottom: var(--space-lg);
  z-index: 2;
  width: 2.5rem;
  opacity: 0;
  transform: translateX(-0.5rem);
  transition:
    opacity var(--motion-duration) var(--motion-ease) 80ms,
    transform 420ms var(--motion-ease) 80ms;
}

.category-card:hover .category-card__go,
.category-card:focus-visible .category-card__go {
  opacity: 1;
  transform: none;
}

.category-card__icon {
  width: clamp(3.5rem, 22%, 5.5rem);
  height: auto;
  aspect-ratio: 1;
  opacity: 0.9;
  transition: transform 420ms var(--motion-ease);
}

.category-card:hover .category-card__icon { transform: translateY(-3px); }

/* The feature cell has far more room, so its mark scales with the cell
   rather than sitting in it at the same size as the small ones. */
.category-grid > .category-card:nth-child(6n + 1) .category-card__icon {
  width: clamp(6rem, 34%, 11rem);
}

.category-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.category-card__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
}

.category-grid > .category-card:nth-child(6n + 1) .category-card__name { font-size: var(--text-2xl); }

.category-card__count {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* ---- Homepage programme list ----
   Its own component. It used to borrow .category-card, which is how the
   bento spans — and now the accent fill — reached a section that wanted
   neither. */
.programme-card {
  display: block;
  padding: var(--space-lg);
  background: var(--colour-recess);
  border: var(--border-width) solid var(--colour-border-strong);
  text-decoration: none;
  transition: border-color var(--motion-duration) var(--motion-ease), background var(--motion-duration) var(--motion-ease);
}

.programme-card:hover,
.programme-card:focus-visible {
  border-color: var(--colour-accent);
  background: var(--colour-accent-soft);
}

.programme-card__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-md);
  letter-spacing: var(--tracking-tight);
}

/* Filter rail. Server-rendered links first; JavaScript only upgrades it. */
.filters {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding: var(--space-md);
  background: var(--colour-surface);
  border: var(--border-width) solid var(--colour-border);
}

/* A rail label, not a heading in the visual hierarchy. Given the same small
   mono treatment the site's other labels use, so a 14px h2 reads as a label
   rather than as a heading that shrank. */
.filters__group-title {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-muted);
  margin-bottom: var(--space-xs);
}
.filters__list { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }

.filters__link {
  display: inline-block;
  padding: 0.2rem var(--space-xs);
  border: var(--border-width) solid var(--colour-border-strong);
  font-size: var(--text-sm);
  text-decoration: none;
}
.filters__link[aria-current="true"] { background: var(--colour-accent); color: var(--colour-accent-ink); border-color: var(--colour-accent); }

.catalogue { display: grid; gap: var(--space-xl); }

@media (min-width: 60rem) {
  .catalogue { grid-template-columns: 15rem minmax(0, 1fr); gap: var(--space-2xl); }
  .catalogue__rail { position: sticky; top: 6rem; }
}

.result-count { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--colour-text-muted); }

/* Search: the empty state does the work of a dead end. */
.search-empty { max-width: 60ch; }

.search-empty__lede {
  font-size: var(--text-md);
  color: var(--colour-text-2);
}

/* ---- pages/about.css ---- */
/* ============================================================
   ABOUT

   The photograph is the page's ground, set behind everything in
   the atmosphere layer. Everything here is about keeping the
   text on the dark half of it and legible over the rest.
   ============================================================ */

.about-lead {
  padding-block-start: var(--space-3xl);
  padding-block-end: var(--space-2xl);
}

.about-lead__statement {
  font-size: var(--text-3xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
  max-width: 21ch;
  margin: 0;
}

.about-lead__sub {
  margin-block-start: var(--space-lg);
  max-width: 42ch;
  color: var(--colour-text-2);
  font-size: var(--text-md);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

/*
 * The text stays on the left half so the photograph is not read through.
 * Narrower than the prose measure used elsewhere for the same reason: the
 * right of the screen belongs to the image.
 */
@media (min-width: 62rem) {
  .has-backdrop .prose {
    --measure: 46ch;
  }
}

/* ---- pages/product.css ---- */
/* Product detail. Media above data on narrow screens, side by side
   when there is room. No scroll motion anywhere on this page — it is
   the conversion page. */

.pdp { display: grid; gap: var(--space-xl); align-items: start; }

@media (min-width: 60rem) {
  .pdp { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-2xl); }
}

.pdp__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

.pdp__title { font-size: var(--text-3xl); }

.pdp__summary { font-size: var(--text-md); color: var(--colour-text-2); }

.pdp__gallery { display: grid; gap: var(--space-sm); }
.pdp__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr)); gap: var(--space-xs); }

.downloads { display: grid; gap: var(--space-xs); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }

.download {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border: var(--border-width) solid var(--colour-border-strong);
  background: var(--colour-surface);
  text-decoration: none;
  font-size: var(--text-sm);
}
.download:hover { background: var(--colour-surface-2); }
.download__meta { color: var(--colour-text-muted); font-family: var(--font-mono); font-size: var(--text-xs); }

.specs { width: 100%; border-collapse: collapse; }
.specs th, .specs td { text-align: left; padding: var(--space-xs) 0; border-bottom: var(--border-width) solid var(--colour-border); font-size: var(--text-sm); vertical-align: top; }
.specs th { width: 40%; font-weight: 600; color: var(--colour-text-2); }

.breadcrumb {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--colour-text-muted);
  padding-block: var(--space-sm);
}
.breadcrumb ol { display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; padding: 0; margin: 0; }
.breadcrumb li:not(:last-child)::after { content: " / "; color: var(--colour-border-strong); }
.breadcrumb a { text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }

/* On phones the two decisions stay reachable without scrolling back up. */
@media (max-width: 59.99rem) {
  .pdp__actions {
    position: sticky;
    bottom: 0;
    background: var(--colour-surface);
    border-top: var(--border-width) solid var(--colour-border);
    padding-block: var(--space-sm);
    z-index: 20;
  }
}
