/* =========================================================================
   PXP Peptides — site.css
   The page frame and every component in §4 of the build spec.

   Loads after brand.css and knows nothing about the shop. Order inside the
   file is frame → sections → components → the navy band's overrides →
   wider screens → print, and the bare rules are always the phone. Every
   `min-width` query is gathered under "Wider screens" near the foot rather
   than scattered, so that "what does this look like on a phone" is answered
   by reading straight down, once.
   ========================================================================= */

/* -------------------------------------------------------------------------
   The compliance strip — STYLED BUT NOT CURRENTLY USED

   This drew the navy bar above the header carrying "Research use only — not
   for human or veterinary consumption". It was removed from the markup of all
   nine pages on request; the rule is kept because restoring it is then one
   line of HTML rather than a rebuild, and because it is referenced by the
   print block at the foot of this file.

   To put it back, add this as the first element inside <body>, above
   <header class="site-head">, on every page:

     <div class="ruo-strip">Research use only — not for human or veterinary
     consumption</div>

   The compliance line itself has NOT left the site: it is in the footer of
   every page, in .ruo-panel at the foot of the shop and product pages, in the
   acknowledgement gate, and throughout legal.html.
   ------------------------------------------------------------------------- */

.ruo-strip {
  /* --band-ground, the same token as .section--navy and .ruo-panel, so the
     three dark bands on the site cannot drift apart. */
  background: var(--band-ground);
  color: var(--chrome-200);
  text-align: center;
  /* --t-small is 14.4px at the default root size, which is under the 15px
     floor the spec sets for anything a customer actually reads. `max()`
     keeps the token as the source of truth and enforces the floor. */
  font-size: max(15px, var(--t-small));
  line-height: 1.45;
  /* Inset-aware for the same reason .wrap is: viewport-fit=cover means the
     notch overlaps this strip in landscape, and this strip is the one line on
     the site that must never be half-covered. */
  padding-block: var(--sp-2);
  padding-inline:
    max(var(--sp-4), env(safe-area-inset-left))
    max(var(--sp-4), env(safe-area-inset-right));
}


/* ONE FLAT COLOUR for the whole document.

   The page was a wash: three radial gradients sized to the full document,
   which had taken four goes to get right and one more to make neutral. It is
   a flat light grey now, on request, and --page is its own token rather than
   --paper because the two have come apart — the page is grey and the things
   ON it are white. That difference is what gives a card an edge without a
   border doing it.

   WHAT THE WASH SOLVED, so the next person does not rediscover it the hard
   way. Three versions before it worked, each plausible:

   1. A fixed pseudo-element at z-index -1. It painted OVER the hero.
   2. `background-attachment: fixed` on body. Behind everything, but pinned to
      the viewport, so how much gradient a section showed depended on where
      the reader had scrolled to.
   3. Per-section gradients on .section--tint. Deterministic, but it made the
      page a stack of separately-shaded bands — the thing this design exists
      not to be.

   The answer was one gradient on body at `background-size: 100% 100%`, so the
   radials were fractions of the DOCUMENT and not of the window. To bring it
   back, that is the rule; --amb-1/2/3 are still declared in brand.css.

   The `html` colour was there to stop body's background propagating to the
   canvas — without it the browser hoists a body gradient to the root and
   sizes it against the viewport, quietly reinstating version 2. With no
   gradient left there is nothing to hoist, so the ground can simply live
   here. */
html { background-color: var(--page); }

/* THE HERO'S GROUND STOPS AT THE HERO. There is no join, on request, and the
   thing that used to be here is worth describing because it will look like an
   omission otherwise.

   The section after the hero used to carry a gradient that began as the
   band's exact colour and faded to nothing over 340px, so the page continued
   out of the hero and settled into the wash. It was on the SECTION rather
   than as a fourth layer on body, because a layer on body paints UNDER the
   three radials and the radials then darken it — measured, nine points darker
   than the band at half strength and sixteen at full. Composited above the
   wash, the top of the gradient was the band's colour by definition, and it
   started wherever the hero ended at every width.

   All true, and all in service of a soft join that is not wanted. The band
   ends where it ends. Two things make the hard edge work rather than read as
   a mistake:

     the band and the wash are the same neutral grey at two lightnesses, so
     the edge is a step in one dimension and not a change of material;

     --hero-scrim now equals --hero-ground, so the veil over the artwork does
     not lift the band's colour. The edge is exactly the band-to-wash step it
     appears to be, at every width, rather than being six points bigger on
     the left where the veil is heaviest.

   To bring the fade back, restore the rule above this comment. */

/* A reader who has asked for less transparency. There is no transparency left
   in the panels to turn off, but the ambient wash is still a soft field behind
   everything, so that goes. */
@media (prefers-reduced-transparency: reduce) {
  body { background-image: none; }
}

/* -------------------------------------------------------------------------
   Members-only chrome

   The Shop link, the basket and the footer's Shop column belong to a reader
   with an account. They ship carrying `hidden` and js/members.js takes the
   attribute off; that file explains why it is round that way, and why it
   stops at the chrome rather than gating the pages.
   ------------------------------------------------------------------------- */

/* `hidden` IS A UA RULE OF `display: none`, WHICH IS THE WEAKEST RULE THERE
   IS — so anything that sets its own display beats it, and the basket does:
   it is `display: grid`, and with the attribute on it and nothing else it
   stayed exactly where it was. This is the one `!important` on the site that
   earns its keep. The alternative is a `[hidden]` override written out again
   for every element the attribute lands on, and finding the next one that
   needs it by looking at the page. */
[data-members-only][hidden] { display: none !important; }

/* THE FOOTER NAMES FOUR TRACKS at 1200px and only three of the columns are
   left once Shop is away, so without this the remaining three sit in the
   first three tracks and stop short of the right edge.

   It reads before the rule it corrects, which sits with the rest of the
   footer near the foot of this file. That is safe on specificity rather than
   on order: `:has()` takes the weight of its argument, so this is a class
   plus a class plus an attribute against a bare class. */
@media (min-width: 1200px) {
  .site-foot__cols:has(> .site-foot__col[hidden]) {
    grid-template-columns: 1.4fr 1fr 1fr;
  }
}

/* -------------------------------------------------------------------------
   Header and navigation
   ------------------------------------------------------------------------- */

/* THE BAR IS WHITE GLASS WHEN IT IS FILLED IN.

   On every page but the home page that is all the time; on the home page it
   is transparent over the hero and comes up to this as you scroll — see
   --head-solid below.

   This declaration is the OPAQUE FALLBACK and is meant to be: a browser with
   no backdrop-filter gets a solid white bar, which is a bar. The glass is in
   the @supports block underneath, where the alpha and the blur arrive
   together — separate them and an unsupported browser gets a 75% white
   rectangle with the page legible through it.

   It was a neutral near-black for one revision. What is left of that is
   --bar-ink: the logo, the links and the two icon buttons all read it, so a
   fill that changes takes its contents with it and none of them can be left
   behind. With a white bar it resolves to the page's own ink and does
   nothing, which is the right kind of nothing — it costs a line and it is
   what makes the dark version a one-value change. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 50;
  --bar-ink: var(--head-ink);

  background: var(--head-fill);
  color: var(--bar-ink);
  border-bottom: 1px solid var(--line);
}

/* Translucent only where the browser can actually blur what is behind it;
   without the blur a 90% white bar over a product photograph is a smear.

   NO `saturate()`, and that is the fix for a blue bar rather than a taste
   call. The glass elsewhere on the site uses `saturate(180%)` to stop a blur
   going grey and lifeless, which is right over a photograph. Over the page
   wash it is not: the wash is a pale blue, the bar lets 10% of it through,
   and saturating that 10% by 1.8 before compositing pushes the red channel
   down far enough to read as a blue tint across the whole bar. Blur alone
   leaves it within a point of white. */
@supports (backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px)) {
  .site-head {
    /* --paper-rgb, not a literal white. Hard-coded here, the bar stayed
       white on a dark page while everything under it went dark, which was
       the most visible instance of the same mistake the glass recipe made.
       The token exists precisely so an alpha can be applied to the page's
       own ground colour in either theme. */
    background: rgba(var(--head-fill-rgb), var(--head-alpha));
    -webkit-backdrop-filter: blur(var(--head-blur)) saturate(160%);
            backdrop-filter: blur(var(--head-blur)) saturate(160%);
  }
}

/* -------------------------------------------------------------------------
   The transparent bar (index.html only)

   At rest the header has no ground of its own and sits directly on the hero
   photograph, with its contents reversed to suit. nav.js adds
   .site-head--scrolled the moment the page moves off the top, and everything
   here switches off again — so the solid bar is the default state and the
   transparent one is the exception, which is the right way round: a rule that
   fails leaves you with a readable header rather than an invisible one.

   The second logo is display:none by default, so the eleven pages without
   this modifier never show it and never download it twice.
   ------------------------------------------------------------------------- */

/* The hero runs UNDER the bar.

   A sticky header still takes its place in the flow, so making it transparent
   on its own just leaves a white strip above the photograph — which is what
   it did on the first attempt. The hero is pulled up by exactly the header's
   height and given that height back as padding, so nothing inside it moves.

   --head-h is measured and published by nav.js because the header's height
   comes from a clamp() and changes with the viewport. The fallback covers the
   moment before that runs, and the case where script never does: 64px is the
   header at its most common size, and being a few pixels out for one frame is
   a far better failure than a hero that has swallowed its own first line. */
.site-head--over-hero ~ main .hero {
  margin-top: calc(var(--head-h, 64px) * -1);
  padding-top: calc(var(--hero-pad) + var(--head-h, 64px));
}

/* The header's ground fades in with the scroll rather than switching on.

   --head-solid runs 0 to 1 across the first 96px of scroll and is written by
   nav.js on every frame it already had a handler for. Everything the bar
   gains as you leave the hero is a function of it: the white ground, the
   hairline, the blur and the shadow all come up together and in proportion,
   so there is no frame where the bar suddenly exists.

   It used to be a class toggled at 8px, which is the "just appears" this
   replaces. `site-head--scrolled` is still set, and still does its job on
   every other page; here it is overruled by the two-class selector below.

   The default of 0 matches what this did before with no script: transparent
   over the hero, forever. That is a deliberate carry-over rather than an
   oversight, and the bar is legible either way because --over-hero keeps the
   dark links and the dark logo.

   Note this is scroll-LINKED, not a transition. There is no duration to sit
   through and nothing to cancel if you scroll back up; the bar is simply a
   function of where the page is, so it tracks a finger exactly. That is also
   why it needs no prefers-reduced-motion case: nothing here moves on its
   own. */
.site-head.site-head--over-hero {
  --head-solid: 0;

  /* The ground comes up to --head-alpha, not to 1, so what the bar arrives at
     is the glass and not a white rectangle. Multiplying the two is the whole
     of it: at --head-solid 0 there is no bar, at 1 there is exactly the bar
     every other page has.

     Nothing here touches --bar-ink. The bar is white filled and the page is
     light, so the type is the same at both ends. If the fill ever goes dark
     again this is where the crossover goes, and a threshold will not do —
     at half fill it would put dark type on a half-dark bar. The shape it
     wants is:

       --bar-ink: color-mix(in srgb, var(--head-ink)
                            calc(var(--head-solid) * 100%), var(--ink)); */
  background-color: rgba(var(--head-fill-rgb),
                         calc(var(--head-alpha) * var(--head-solid)));
  border-bottom-color: rgba(0, 0, 0, calc(.13 * var(--head-solid)));
  /* The blur and the saturation arrive with the ground, so the glass builds
     rather than switching on. Scaled by the same variable — a fixed blur under
     a fading ground reads as a smeared window with nothing in it.

     --head-blur rather than a number, because a phone gets a smaller one. A
     backdrop-filter has to re-blur whatever is passing beneath it on every
     frame of a scroll, and beneath this one is a canvas being repainted. See
     the token. */
  -webkit-backdrop-filter:
    blur(calc(var(--head-blur) * var(--head-solid)))
    saturate(calc(100% + (60% * var(--head-solid))));
          backdrop-filter:
    blur(calc(var(--head-blur) * var(--head-solid)))
    saturate(calc(100% + (60% * var(--head-solid))));
  box-shadow:
    0 1px 2px rgba(7, 22, 52, calc(.06 * var(--head-solid))),
    0 8px 22px rgba(7, 22, 52, calc(.10 * var(--head-solid)));
}

/* The hero is a pale panel, so the bar over it keeps the ordinary logo and
   dark links — only the background goes away. The white link colour that
   used to be here went when the panel inverted, and the two-image logo swap
   went with the supplied artwork: there is one logo file now and it is dark
   on transparency.

   Worth stating plainly: the only thing `--over-hero` does at rest now is
   remove the bar's own ground so the photograph runs to the top of the
   window. Invert the hero back to a dark panel and this block has to come
   back with it — a white link colour AND a light version of the logo, which
   no longer exists and would have to be made. */
.site-head--over-hero:not(.site-head--scrolled) .site-nav__link {
  border-bottom-color: rgba(0, 0, 0, .10);
}

.site-head--over-hero:not(.site-head--scrolled) .site-head__whatsapp:hover,
.site-head--over-hero:not(.site-head--scrolled) .site-head__account:hover,
.site-head--over-hero:not(.site-head--scrolled) .site-head__basket:hover,
.site-head--over-hero:not(.site-head--scrolled) .site-head__toggle:hover {
  background: rgba(0, 0, 0, .06);
}

/* An open phone menu needs a ground, whatever the scroll position, or the
   rows sit on the photograph. THE RULE THAT USED TO BE HERE COULD NOT DO IT:
   js/nav.js writes --head-solid as an inline style on the header, and no
   selector outranks an inline style, so a stylesheet rule setting the same
   variable was dead from the moment that scroll handler was written. It read
   as working code for as long as nobody opened the menu at the top of the
   home page.

   headerState() in js/nav.js decides it now — an open panel is solid — which
   is also the only place the variable is written. Do not add a rule for it
   here; it will not take. */

/* Nothing to say here any more. Opening the panel sets --head-solid to 1,
   which fills the bar and takes --bar-ink to the filled value with it, so the
   links and the two buttons are already right. This rule used to force them
   back to --ink because the filled bar was white. */

/* nav.js sets this once the page has scrolled past the strip. The header
   needs an edge when content is running under it and looks better without
   one at rest. */
.site-head--scrolled { box-shadow: var(--shadow-1); }

.site-head__wrap {
  width: 100%;
  max-width: calc(var(--wrap) + var(--gutter) * 2);
  margin-inline: auto;

  /* Thinner on a phone: 4px a side rather than --sp-3. With the row gap below
     that takes the bar to 53px, from 81 originally.

     53 IS ESSENTIALLY THE FLOOR and it is worth knowing why, so the next
     person does not go hunting for another ten pixels. The logo link, the
     basket and the menu toggle are all held at 44px, which is the smallest a
     thumb should be asked to hit; 44 + 4 + 4 + the 1px rule is 53. Everything
     left in the bar's height is a touch target. The 960 query puts --sp-3
     back, where there is room for the bar to have some air in it. */
  padding-block: 4px;

  padding-inline:
    max(var(--gutter), env(safe-area-inset-left))
    max(var(--gutter), env(safe-area-inset-right));
  display: flex;
  flex-wrap: wrap;
  align-items: center;

  /* COLUMN gap only, and the row gap explicitly nothing. This was most of the
     bar's height and none of it was visible.

     .site-nav is a flex item on this wrapping row. Below 960 it is the mobile
     panel: closed, `max-height: 0`, zero tall — but `flex-basis: 100%` still
     WRAPS it onto a second line, and a wrapped line brings the row gap with
     it whether or not there is anything on it. The bar was 12px taller than
     its contents to hold a gap above an empty box.

     Written as two properties rather than `gap`, and the order matters: this
     used to be the shorthand, and a `row-gap: 0` added higher up the rule was
     silently reset by it. The shorthand sets both.

     The space between the logo, the basket and the toggle is unchanged. When
     the panel opens it now sits directly under the bar, which is where it
     belongs — it carries its own padding. */
  column-gap: var(--sp-3);
  row-gap: 0;
}

/* The 44px minimum goes on the anchor, not the image. The logo is the only
   "home" control in the header and it sits next to a toggle explicitly sized
   to 44px; at 320px the image's clamp below resolves to 34px, so the link was
   the one target on the bar under the floor. Putting the minimum here lets
   the mark stay visually small while the thing a thumb has to hit is not. */
.site-head__logo {
  display: flex;
  align-items: center;
  min-height: 44px;
  flex: 1 1 auto;
  min-width: 0;
  line-height: 0;
}

/* No mask any more, and no filter either. assets/logo.png IS navy.

   This used to be a runtime trick, and it was a good one for the file it had:
   the supplied artwork was full colour, the bar needed --navy-800, and no
   filter chain lands on a specific colour with any accuracy. So the anchor
   was painted navy and the logo supplied only its alpha channel, through a
   mask, with the <img> kept for its box and hidden. The footer did the same
   job differently, with `brightness(0) invert(1)`, which reaches white but
   only white.

   The new artwork made both unnecessary. It is a mark on transparency, so it
   was flattened to navy once, when it was built, rather than on every page
   load: `assets/logo.png` is now the silhouette in --navy-800 with its alpha
   intact. A plain <img> renders it, at any size, in both places.

   What that buys, beyond simplicity: the `@supports` guard is gone, and with
   it a fallback that would have been genuinely broken. The guard existed
   because an unmasked anchor is a navy rectangle. With the new artwork the
   unguarded fallback would have been worse still — the supplied file has
   WHITE letters, so on a light bar the letters would simply have been
   missing while the X showed. There is no fallback path now because there is
   nothing to fall back from. */

/* The mark carries width/height attributes so the page does not jump while
   it loads; height wins here and width follows it, which keeps the ratio. */
/* Shorter than the lockup this replaces, because it is a different shape.

   The old file was 176 x 106 and about a third of that height was the
   PEPTIDES subtitle and its rules, so at 44px tall the letters themselves
   came to roughly 30px. The supplied artwork is 480 x 166 and is ALL
   wordmark — at 44px the letters would be 44px, half again as big as they
   have ever been on this site, and next to a 15px nav that reads as shouting.
   These numbers put the letters back where the eye expects them.

   Height and `width: auto`, never the reverse: the mark is 2.89 wide for
   every 1 tall and the header's row height is what has to stay put. */
/* Taller than the 18-21px the old logo took, and the artwork is why.

   That was a wide wordmark, 2.89:1, so 21px of height bought 61px of width
   and the mark had presence in the bar. This one is 1.66:1, so the same 21px
   buys 35px and it reads as small. 28px brings the width back to 47px, and
   the bar does not grow: it is about 69px tall and was never being set by the
   logo.

   This is interim. The letters inside this mark occupy 60% of its height, so
   even at 28px they render about 17px, against roughly 21px for the old
   wordmark. The fix is a horizontal lockup from PXP, not more height here. */
/* No filter. The bar is white and assets/logo.png is a navy silhouette, which
   is the pair they were made for.

   It was knocked out to white for one revision, while the bar was dark, with
   `filter: brightness(calc(1 - var(--head-solid, 1))) invert(var(--head-solid,
   1))` — which crosses from the file untouched to `brightness(0) invert(1)`
   in step with the fill, passing through a grey on the way because invert(0.5)
   sends every colour to the middle. That is as close to a crossfade as one
   image gets, and it is what to paste back if the bar goes dark again. */
.site-head__logo img {
  height: clamp(24px, 4.5vw, 28px);
  width: auto;
}

/* The basket, always in the bar.

   `margin-left: auto` moved here from the toggle — this is now the first
   thing after the logo, so it is the one that pushes the group right.
   Position relative because the count sits on its corner. */
.site-head__basket {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: auto;
  border-radius: var(--r-sm);
  color: var(--bar-ink);
}

.site-head__basket:hover { background: var(--blue-100); }

/* WhatsApp, first in the right-hand cluster.

   The same 44px box as every other icon on the bar, so the cluster stays a
   row of equal targets and nothing has to be re-measured when one is added.
   It takes no `margin-left: auto`: the logo pushes this group right on its
   own, and a second auto margin in the row would fight it.

   Brand green, and the only icon on the bar that is not --bar-ink. The
   colour is --whatsapp and it is a token per theme, because the green that
   reads on the dark bar is unreadable on the white one — brand.css carries
   the two values and the measurements behind them. The hover stays the
   bar's own, so it still behaves like its neighbours even though it does
   not look like them. */
.site-head__whatsapp {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-sm);
  color: var(--whatsapp);
}

.site-head__whatsapp:hover { background: var(--blue-100); }

/* 22px, matching the account and basket glyphs rather than the theme
   toggle's 20px. This mark is a filled shape with no internal air, so at
   20px it reads heavier than the outlined icons it sits beside; at 22px the
   optical weight matches. */
.site-head__whatsapp svg { width: 22px; height: 22px; }

/* The theme toggle. Same box as the basket so the two sit as a pair. */
.site-head__theme {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--bar-ink);
  cursor: pointer;
}

.site-head__theme:hover { background: var(--blue-100); }

/* Parked in the phone menu by js/nav.js — the account link and the theme
   toggle — which adds two classes together: .site-nav__link for the row and
   this for the box. The link rule gives the 48px row, the hairline under it
   and the staggered arrival; what is left is the 44px icon box underneath,
   which has to come off or each is a pill floating in a list of full-width
   rows.

   `width: 100%` and not `auto`: a <button> shrink-to-fits even as a
   block-level flex container, so `auto` left a 144px row starting at the
   panel's left gutter — a hairline half the width of every other one, with
   its own right-aligned label stranded in the middle of the panel. The reset
   in brand.css makes every box border-box, so the padding is inside the 100%
   rather than added to it.

   Right-aligned like its neighbours, which the link rule already does — the
   label and the glyph are laid out by that, and the gap is all this adds. */
/* Scoped to the list, and that is load-bearing: `.site-nav__link--parked` is
   ONE class, so it ties with `.site-head__account` and `.site-head__theme`
   and loses to whichever of them the file happens to declare later. The
   account link came after and kept its 44px box; the toggle came before and
   did not. Two classes settles it for both, whatever moves in this file. */
.site-nav__list .site-nav__link--parked {
  width: 100%;
  height: auto;
  gap: var(--sp-2);
  border-radius: 0;

  /* A <button> does not inherit the page's font, and the rows beside it in
     the panel do. Without this the one row in the menu that is a button is
     the one row set in the browser's default UI face. */
  font: inherit;
}

/* The icon buttons light their whole 44px box on hover, which in a list of
   full-width rows would be a block of tint the width of the panel. The rows
   around them answer on the text instead, and so do these. */
.site-nav__list .site-nav__link--parked:hover {
  background: transparent;
  color: var(--blue-800);
}

/* NO GLYPHS IN THE MENU. In the bar an icon is the whole control and has to
   carry the meaning on its own; in the panel it sits beside a word that
   already does, and five words with two little pictures among them reads as
   two rows that did not get the memo. The rows are words here, like the
   links they are listed with.

   `display: none` and not `visibility: hidden`: the gap above is a flex gap
   and a hidden-but-present glyph would still be spaced for. */
.site-nav__list .site-nav__link--parked svg { display: none; }

/* EXCEPT THE THEME ROW, which is the two glyphs and nothing else — sun and
   moon side by side, where the other rows carry a word. In the bar this
   control shows one at a time, because there it is a single 44px target and
   the glyph has to BE the control; here both are visible and the pair reads
   as the switch it is.

   The element selector on the end is not decoration. `:root[data-theme=dark]
   .site-head__theme-sun` is (0,3,0) and so is a three-class selector, which
   would leave this decided by source order; adding `svg` makes it (0,3,1)
   and settles it wherever these rules end up in the file. */
.site-nav__list .site-nav__link--parked svg.site-head__theme-sun,
.site-nav__list .site-nav__link--parked svg.site-head__theme-moon {
  display: block;
}

/* Which of the two you are in. The lit glyph is the theme now in force, not
   the one the press would bring — with only one icon showing, "the moon
   invites you into the dark" was the only reading available; with both on
   screen a bright moon beside a dim sun says the lights are already off,
   which is the opposite. aria-pressed on the button carries the same fact
   for anyone not looking at it. */
.site-nav__list .site-nav__link--parked .site-head__theme-moon { opacity: .35; }
.site-nav__list .site-nav__link--parked .site-head__theme-sun { opacity: 1; }

:root[data-theme="dark"] .site-nav__list .site-nav__link--parked .site-head__theme-moon { opacity: 1; }
:root[data-theme="dark"] .site-nav__list .site-nav__link--parked .site-head__theme-sun { opacity: .35; }

/* The glyph keeps its own colour where it has one — WhatsApp is not parked,
   but the account icon and the moon both read --bar-ink, and the label
   beside them has to match rather than inherit a button's blue. */
.site-nav__parked-label {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}

/* The toggle, hidden while dark mode is off. js/theme.js puts this attribute
   on the root before first paint — see the note at ENABLED in that file — so
   there is no frame where the button is visible and no layout shift when it
   goes. Delete nothing to bring it back: set ENABLED to true. */
:root[data-theme-switch="off"] .site-head__theme { display: none; }

/* And the row it sits in inside the phone menu, which would otherwise be a
   hairline with nothing above it. :has() rather than a class on the <li>,
   so nav.js does not have to know this rule exists. */
:root[data-theme-switch="off"] .site-nav__list li:has(> .site-head__theme) {
  display: none;
}

/* No `display` here. `.site-head__theme svg` is a class plus an element
   (0,1,1) and the two icon classes below are a lone class (0,1,0), so a
   `display: block` in this rule wins and BOTH icons show at once. Sizing
   only; visibility is decided below. */
.site-head__theme svg { width: 20px; height: 20px; }

/* One icon at a time, chosen by THEME rather than by the button's pressed
   state, so the glyph is right on a system-dark page before any script has
   run. The button shows the moon in daylight — it is an invitation to go
   dark, not a report of where you are. */
.site-head__theme-moon { display: block; }
.site-head__theme-sun { display: none; }

:root[data-theme="dark"] .site-head__theme-moon { display: none; }
:root[data-theme="dark"] .site-head__theme-sun { display: block; }

.site-head__basket svg { width: 22px; height: 22px; }

/* The account icon, beside the basket.
   
   Out of <nav> for the same reason the basket is: below 960px the nav folds
   behind the hamburger, and "where are my invoices" is not a thing to make
   somebody open a menu for. It carries no badge — there is no number a
   signed-out visitor could be shown, and a signed-in one is told everything
   on the page itself.
   
   The `~` rule below is what lets this be added a page at a time. The basket
   used to start the right-hand cluster and carried `margin-left: auto` to
   push it there; now the account icon does, but ONLY on pages where the
   account icon actually precedes it. A page that hasn't gained the icon yet
   keeps its old layout exactly, with no second rule to remember. */
.site-head__account {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: auto;
  border-radius: var(--r-sm);
  color: var(--bar-ink);
}

.site-head__account:hover { background: var(--blue-100); }

.site-head__account svg { width: 22px; height: 22px; }

.site-head__account ~ .site-head__basket { margin-left: 0; }

/* The count rides the top-right corner rather than sitting beside a word,
   because there is no word any more. Overrides the inline-badge geometry in
   .site-nav__badge, which was written for "Basket 3". */
.site-head__basket .site-nav__badge {
  position: absolute;
  top: 1px;
  right: 0;
  margin: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  /* The accent, with near-black on it rather than white — white on this
     colour is 2.20:1 and the count is the one number in the header that has
     to be read at a glance. The 2px ring is still --paper: it cuts the dot
     out of the bar it is sitting on. */
  background: var(--accent);
  color: var(--on-accent);
  font-size: 11px;
  line-height: 18px;
  box-shadow: 0 0 0 2px var(--head-fill);
}

.site-head__toggle {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-sm);
  color: var(--bar-ink);
}

.site-head__toggle:hover { background: var(--paper-2); }

/* Three bars that fold into a cross. The middle bar is the element itself
   and the outer two are its pseudo-elements, so the whole control is one
   empty span in the markup. */
.site-head__bars,
.site-head__bars::before,
.site-head__bars::after {
  display: block;
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform .24s var(--ease), background-color .16s linear;
}

.site-head__bars { position: relative; }

.site-head__bars::before,
.site-head__bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.site-head__bars::before { top: -7px; }
.site-head__bars::after { top: 7px; }

.site-head--open .site-head__bars { background: transparent; }
.site-head--open .site-head__bars::before { transform: translateY(7px) rotate(45deg); }
.site-head--open .site-head__bars::after { transform: translateY(-7px) rotate(-45deg); }

/* The phone's navigation: a panel that grows out from under the header.
   `max-height` and not `display`, because a panel that appears has no way to
   animate and reads as a glitch — and `visibility` rather than opacity alone
   because a transparent link is still a tab stop, and tabbing into a menu
   nobody can see is the classic way to lose a keyboard user. */
.site-nav {
  flex-basis: 100%;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  transition: max-height .30s var(--ease), opacity .2s linear,
              visibility 0s linear .30s;
}

.site-head--open .site-nav {
  max-height: 80vh;
  overflow-y: auto;
  opacity: 1;
  visibility: visible;
  transition: max-height .34s var(--ease), opacity .22s linear, visibility 0s;
}

.site-nav__list {
  display: flex;
  flex-direction: column;
  padding-top: var(--sp-2);
}

.site-nav__link {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: var(--sp-2) var(--sp-1);
  border-bottom: 1px solid var(--line);
  color: var(--bar-ink);

  /* RIGHT, in the phone panel. The links sit under the menu button that
     opened them and above the basket beside it, so the right edge is where
     the reader's attention and thumb already are. The 960 query puts them
     back to the start, where they are a row and the logo anchors the left.

     Both properties: justify-content lays the flex line out, text-align
     handles a link that wraps to two. */
  justify-content: flex-end;
  text-align: right;

  /* AND THEY ARRIVE ONE AFTER THE OTHER. Hidden here and revealed by
     --open, which is the same shape as the panel's own max-height: nothing
     is hidden that is reachable without the class that shows it, so a
     reader whose script never runs cannot end up with an open panel full of
     invisible links — the panel does not open at all.

     .34s and 65ms a step. It was .16 and 30 — quick, as first asked, and
     then slower on second look. The five links now finish at 600ms against
     the panel's own .34s opening, so the panel arrives first and the links
     settle into it rather than racing it. That is the part that reads as
     deliberate; matching the panel exactly reads as one thing stuttering.

     The travel goes up with the duration. 10px over .16s and 14px over .34s
     are about the same apparent speed — a longer fade at the old distance
     looks like it has stalled. */
  opacity: 0;
  transform: translateX(14px);
  transition: opacity .34s var(--ease), transform .34s var(--ease);
  font-family: var(--font-display);
  /* 400, and it has come down twice: 600, then 500, now this. The links read
     as a set of labels rather than as small headings competing with the
     wordmark beside them. The current page is marked by the underline and by
     --current below, never by weight, so lightening the rest costs nothing.

     400 is as thin as this can go. The display face has no lighter weight
     loaded, and asking for one gets a synthesised thin from the browser,
     which is worse than the real 400 at this size. */
  font-weight: 400;

  /* Set in caps, and by text-transform rather than in the markup: the links
     stay sentence case in the HTML, so a screen reader announces "Purity and
     testing" instead of spelling it out, and the copy stays editable by
     somebody who is not thinking about the stylesheet.

     The tracking is not optional. Caps at label size close up and turn into a
     block; .08em is what keeps the letters apart. Set with the same tracking
     as .eyebrow so the two label treatments on the page match. */
  text-transform: uppercase;
  letter-spacing: .08em;
  text-decoration: none;
}

/* --blue-800: the aliases all resolve to the resting colour now. */
.site-head--open .site-nav__link {
  opacity: 1;
  transform: none;
}

/* nth-child on the <li>, not the link: the links are one per item, so the
   item is what has a position in the list. */
.site-head--open .site-nav__list li:nth-child(1) .site-nav__link { transition-delay: 65ms; }
.site-head--open .site-nav__list li:nth-child(2) .site-nav__link { transition-delay: 130ms; }
.site-head--open .site-nav__list li:nth-child(3) .site-nav__link { transition-delay: 195ms; }
.site-head--open .site-nav__list li:nth-child(4) .site-nav__link { transition-delay: 260ms; }
.site-head--open .site-nav__list li:nth-child(5) .site-nav__link { transition-delay: 325ms; }

/* A sixth link would arrive with the fifth rather than not at all. Add a
   line if the nav grows; do not build a loop for five items. */
.site-head--open .site-nav__list li:nth-child(n + 6) .site-nav__link { transition-delay: 325ms; }

.site-nav__link:hover { color: var(--blue-800); }

/* The current-page marker, on the RIGHT with the links.

   A negative inset shadow draws on the far edge instead of the near one, so
   the bar moves with the text rather than being left behind on the side the
   links used to start from. The padding moves with it: it is there so the
   label does not touch the bar, and it has to be on whichever side the bar
   is.

   The 960 rule turns this into an underline, because up there the links are a
   row and a vertical bar on one of them would read as a divider. */
.site-nav__link--current,
.site-nav__link[aria-current="page"] {
  color: var(--blue-700);
  box-shadow: inset -3px 0 0 var(--blue-600);
  padding-right: var(--sp-3);
}

.site-nav__cta {
  display: flex;
  width: 100%;
  margin: var(--sp-4) 0 var(--sp-3);
}

/* The basket count. `hidden` until there is something in it — the attribute
   is honoured by the reset, so nothing here has to think about it. */
.site-nav__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  margin-left: var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--paper);
  color: var(--blue-700);
  font-size: var(--t-small);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------------------------------
   The tinted ground

   Carries the banner's faint molecular lattice: the SVG tiles underneath, a
   near-white gradient sits over it to knock it back, and the text sits on
   top at full contrast. If assets/molecule.svg is ever missing the url()
   layer simply does not paint and what is left is a clean white-to-paper-2
   wash — which is the intended look with the volume turned down, not a
   broken page.

   The hero used to share this rule and now has a photograph instead; the
   same flat colour is kept underneath it as the fallback ground, so the two
   still belong to each other if the image never arrives.

   MINIMAL PASS: this used to stack three background layers — a blue radial
   bloom, a white-to-paper-2 wash, and molecule.svg tiling underneath. It was
   the single biggest source of visual noise on the site: every tinted band
   carried a faint hexagonal texture behind its text, which at small sizes
   reads as a slightly dirty screen rather than as a pattern. One flat colour
   now. The lattice asset is kept in assets/ because it is good and costs
   nothing to leave there, but nothing references it — restoring it is one
   background-image line here.
   ------------------------------------------------------------------------- */

/* No ground. The hero is not a band any more — the artwork sits on the page.

   This used to paint a flat #EDEDF0 so the `darken` blend below had
   something to composite against. That worked, but it made the top of the
   page a grey rectangle with a visible edge where it met the next section,
   which is the seam the continuous page wash exists to avoid. The token that
   held that colour is gone; brand.css says why.

   Transparent, plus the two stacking contexts removed from .hero below, lets
   the blend reach the page's own background instead: the artwork's white
   field loses to the page and the strands are printed straight onto it. The
   hero has no edges now because it has no surface.

   The cost is that a failed image leaves the page wash rather than a plain
   band, which is the better failure anyway. */
/* A flat grey-blue ground with a hard bottom edge.

   This was a gradient that faded to nothing, on the reasoning that a solid
   colour reinstates the horizontal seam the hero spent a while getting rid
   of. It is solid on request, and the seam is the point: the header now
   reads as a band that ends rather than as a tint that runs out, and the
   trust strip below it starts on clean page.

   So the edge lands exactly at the foot of .hero, which means `padding-block`
   there is now setting the height of a visible band and not just spacing.
   Change one and look at the other.

   background-color rather than a background-image, because there is no longer
   a gradient to describe. Neither creates a stacking context, so the blend
   below is untouched either way.

   The artwork's own mask still fades it out before this edge (see
   .hero__media img), which is deliberate and not a leftover: the band having
   a hard edge is a different question from the helix being guillotined by
   one. */
.hero { background-color: rgb(var(--hero-ground)); }

/* .section--tint no longer paints anything.

   It used to lay a #F5F5F7 band, and later a band with its own gradient, to
   separate one section from the next. With a single continuous page wash
   behind everything, a shaded band is exactly the seam the wash exists to
   avoid — the page reads as one surface or as a stack of trays, not both.

   The class stays in the markup on all 16 sections rather than being stripped
   out: it still marks "this section is a change of subject", which is worth
   keeping as a hook, and restoring a band is one background-color here.
   Separation is now carried by the glass cards' rims and by section spacing,
   which is what a minimal layout should be using anyway. */
.section--tint { background-color: transparent; }

.hero {
  /* --hero-pad, declared on .hero and used TWICE: here, and in the
     --over-hero rule above, which has to add the header's height to the top.
     It was written out longhand in both places and the two drifted the moment
     one was changed, so the hero grew at the bottom and not at the top.

     Bigger than the rest of the site's rhythm on purpose. This is the one
     band that is a header rather than a section, and at the old
     clamp(44px, 6.5vw, 84px) it was the same height as a content section that
     happened to have a picture behind it. The floor matters as much as the
     ceiling: 44px on a phone is what made it read as a strip. */
  /* The floor is 34, down from 72, which is a phone-only change even though
     it is not in a media query: at 960 the 9vw term is 86 and takes over, so
     the clamp's floor only ever applies below about 380px wide. Above that
     nothing here has moved.

     On a phone the hero's top padding is this PLUS the header's height — see
     the --over-hero rule — so 72 was putting the eyebrow 153px down a 812px
     screen before a word had been read. */
  --hero-pad: clamp(34px, 9vw, 132px);
  padding-block: var(--hero-pad);
  position: relative;
  /* NO `isolation: isolate` and NO `z-index` here, and both absences are
     load-bearing.

     Either one creates a stacking context, and a stacking context is a wall
     that `mix-blend-mode` cannot see past: the artwork below would blend only
     against things inside .hero, which is how it ended up needing a flat
     ground to sit on. Without them the blend reaches the page's own
     background and the white field simply disappears into it.

     What this costs: the hero's three layers (media 0, veil 1, inner 2) are
     now in the page's stacking context rather than a private one. That is
     safe as things stand — the header is 50 and the research-use gate is 100,
     so nothing on the page competes in the 0-2 range — but it is no longer
     none of the rest of the page's business. Add anything with a z-index
     between 0 and 2 and check the hero. */
  overflow: hidden;
  /* Dark text from here down. The panel inverted — pale ground, dark
     wireframe — so its contents inherit ink and the veil below guarantees
     the ground stays light enough for them. */
  color: var(--ink);
}

/* -------------------------------------------------------------------------
   The hero photograph

   An <img> and not a background-image, because background-image has no
   srcset: the source file is 6720 wide and 17MB, and a background would post
   the lot to a handset to draw a band 400px tall. Here a phone takes 62KB.

   The veil is a white scrim, weighted to the left where the type sits and
   thinning to the right where the photograph's own subject is. It is doing
   contrast work, not mood: ink on the bare photograph runs into the strands
   wherever one crosses a letter, and through this it clears 12:1 on the left
   third where the headline actually sits.
   ------------------------------------------------------------------------- */

.hero__media,
.hero__veil {
  position: absolute;
  inset: 0;
  pointer-events: none;

  /* And fade the scrim out at the bottom too, for the same reason.

     The scrim is a pale blue and it stopped at the hero's boundary, so the
     hero read as a tinted band with a hard horizontal edge against the
     section below — exactly the band this design removed a revision ago,
     reintroduced by the fix for the missing gradient. Dissolving it into the
     page removes the edge and keeps the tint where the type needs it.

     The fade starts at 72%, below the lede and behind the buttons, which
     have their own fills and need nothing from the scrim. Contrast is
     measured on the text above that line, so this costs it nothing. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 72%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 72%, transparent 100%);
}

/* NO z-index on any of the three hero layers, and this is the subtle one.

   The order media → veil → inner is what it has always been, but it is now
   carried by DOM order alone. All three are positioned, and positioned
   elements with no z-index paint in the order they appear in the markup, so
   the result is identical — with one difference that is the entire point:

   `z-index` on a positioned element CREATES A STACKING CONTEXT. It does not
   have to be a big number, or a negative one; `z-index: 0` does it.

   That cost an hour. `isolation: isolate` and `z-index: 0` were removed from
   .hero so the artwork's `mix-blend-mode: darken` could reach the page's
   background — and it still could not, because .hero__media was
   `position: absolute; z-index: 0` and had quietly built a second wall one
   level down. The image was blending inside .hero__media, whose background
   is transparent, so it blended against nothing and painted its own white
   field as-is. It LOOKED correct, because the page behind it is white; the
   giveaway was that the page's gradient did not show through.

   If the hero ever needs an explicit layer order again, use `isolation` on
   .hero and negative-free z-indexes on the children — and accept that the
   blend will need a ground again. The two cannot both be had. */
.hero__media {
  /* Transparent, so the blend below composites against the PAGE rather than
     against this element. A background here is a ground, and a ground is the
     grey rectangle we just removed — it does not matter that .hero is
     transparent if its child is not. */
  background: transparent;
}

.hero__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;

  /* NO `mix-blend-mode`, and the reason is the artwork, not the technique.

     This used to be `mix-blend-mode: darken`, and it was the right call for
     the image it was written for: a black wireframe helix on a pure white
     field. `darken` keeps the per-channel minimum, so the white field lost
     to the page and vanished while the near-black strands stayed. A cut-out
     with none of the cost of one.

     The current artwork is a photographic render: chrome spheres on a pale
     grey field, with the spheres in the mid-tones rather than near black.
     `darken` against a ground of 238,239,241 keeps the ground almost
     everywhere the image is light, which flattens the render's own field
     into the band and leaves a ghost. Measured, it also changes nothing at
     all about legibility: the darkest pixel under the type is the image
     either way, so on and off give identical contrast. It was doing no work
     and costing the picture.

     If a wireframe-on-white image ever comes back, so should the blend, and
     with it the rule that .hero must create no stacking context (see there),
     because blend modes cannot see past one.
  */

  /* Fade the artwork out at the bottom rather than letting `overflow: hidden`
     guillotine it.

     The helix ran off the foot of the hero in a dead straight horizontal
     line, which is the one edge in the image that is obviously not part of
     the drawing. This dissolves the last third of it so the strands thin out
     into the page instead of being cut.

     The mask goes on the IMG and not on .hero__media, and that distinction
     is the same trap as the z-index one above: `mask-image` creates a
     stacking context, so masking the parent would wall the blend in again
     and the page's wash would stop showing through. On the blended element
     itself it is harmless — the element is already rendered as a group
     before it is composited with the backdrop. Verify with the same test:
     the strands must sit directly on the hero's ground with no pale
     rectangle of their own around them. (That test used to read "the page's
     gradient must show through". It does not any more — .hero now paints a
     ground over the top half, so the gradient is meant to be hidden there.
     Look for the rectangle's edge, or check the fade at the foot of the
     hero.) */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 48%, transparent 96%);
          mask-image: linear-gradient(180deg, #000 0%, #000 48%, transparent 96%);
}

/* The blend, for wireframe-on-white artwork only.

   A modifier rather than the base rule, and that is the whole point. The home
   page's render must NOT have it: `darken` against the hero ground keeps the
   ground everywhere the render is light and leaves a ghost of a picture.
   about.html's helix must: it is a black wireframe on a pure white field, and
   without the blend that field paints as an opaque white rectangle with a
   visible edge, which is exactly the failure the regression test describes.

   One image wants it and one does not, and the class is what says which. The
   rule that nothing in .hero may create a stacking context is load-bearing
   again because of this: blend modes cannot see past one. */
.hero__media--blend img { mix-blend-mode: darken; }

/* -------------------------------------------------------------------------
   The stage — the scroll-scrubbed clip

   A full-width band of footage running from the top of the page down past the
   trust strip and into the section after it, with everything scrolling over
   it. See js/hero-frames.js for why it is forty-eight stills and not a video.

   IT IS A SIBLING OF THE HERO, NOT ITS BACKGROUND, and that is the whole
   reason this block exists rather than a few rules on .hero__media. A hero's
   background stops where the hero stops; this has to keep going, and no
   amount of overflow on a section makes a section's background outlive it.

   THE STACKING, which is the only fiddly part. The stage is absolutely
   positioned at z-index 0 and everything after it in the markup is pushed to
   z-index 1, so the sections paint over it. `.hero-stage ~ *` rather than
   `main > *` on purpose: it is scoped to the one page that has a stage, so
   about.html's hero is left alone — giving that one a z-index would create a
   stacking context and wall in the mix-blend-mode its artwork depends on.

   Not z-index -1, which is the obvious first try and is wrong: a negative
   layer paints BEFORE the in-flow block backgrounds, so the page's own wash
   on <body> would paint on top of the footage and tint it.

   IT ENDS AT THE BOTTOM OF THE SECTION AFTER THE HERO — the hero plus the
   trust strip. It has been three lengths now: 175vh, then the hero alone,
   then this. The vertical room is not decoration: the clip is CONTAINED, so
   the box's height is what sets the picture's size, and every extra hundred
   pixels of stage is a wider picture and less padding around it. At the
   hero's height alone the frame drew at 20% of the page's width; over two
   sections it is about a third.

   The height is SET BY js/hero-frames.js, off real boxes, because a section's
   height is padding plus type and there is no CSS constant for it. A value
   here that was ten pixels out would show as a band edge ten pixels from the
   section boundary, which is worse than not trying. --stage-h below is the
   no-JS fallback and is deliberately approximate: without the script there is
   no clip either, only the poster, and a poster that ends near the right
   place is fine.

   PUSHED DOWN THE PAGE by --stage-top, roughly the header's height — and it
   is the FOOTAGE that is pushed down, not the stage. The stage starts at the
   top of the page and paints the clip's own field across its whole box; the
   picture inside it starts lower. So the strip the nav sits on is 225, the
   same value as the top of every frame, and there is no join to see.

   The first attempt moved the stage's own top edge down instead, which put
   the page's white above it and drew exactly the hard line this is meant to
   avoid. A box's background only exists inside the box.

   NO FADE AT THE FOOT. It had one while it ran into the middle of a section
   and had to dissolve rather than stop. Ending on the hero's own boundary is
   an edge that already exists, and it is hard on purpose — the same call as
   the hero band's.
   ------------------------------------------------------------------------- */
/* The stage is absolutely positioned against this, so it spans the content
   column's full width and starts at the top of the content rather than at the
   top of the document — which is the same thing here, since the header is
   fixed, but would not be if a banner were ever added above it. */
main { position: relative; }

.hero-stage {
  /* ONE SET OF VALUES, AT EVERY WIDTH, and it went through a phone-specific
     set and back on request.

     What that set did, so the option is on the record rather than lost: it
     pushed --stage-top past the hero card, shrank --art-h to a band, dropped
     the zoom to 1.2 and pushed the trust strip below the whole thing — so on
     a phone the clip was its own band between the copy and the bullets and
     nothing overlapped anything.

     What it costs to be back here is legibility on a phone, and it is
     measured rather than a worry. The clip is centred on the page and a
     phone has no right-hand side for it to sit in, so it lands under the
     words: the hero lede's darkest backdrop goes to near-black, which is
     about 1.6:1 against type that needs 4.5. The headline and the button are
     over it too. On a wide screen none of this happens, because the picture
     is off to the right of the copy and the lede is capped at 21ch to keep
     it there.

     If it should go back to a band, the four values are --stage-top
     clamp(380px, 54vh, 470px), --stage-h clamp(300px, 46vh, 420px),
     --art-h clamp(300px, 44vh, 400px) and --stage-zoom 1.2, with
     .hero-stage ~ #trust at clamp(300px, 44vh, 400px), all under a
     max-width query. */
  --stage-top: clamp(64px, 9vw, 132px);
  --stage-h: clamp(380px, 58vh, 660px);
  --stage-pad: clamp(8px, 1.2vw, 20px);

  /* The PICTURE's height, which is deliberately not the stage's.

     They used to be the same box, and that coupling was a trap: the clip is
     contained, so a taller box means a bigger picture, and the stage's height
     follows the trust strip. Pushing the strip down to get the icons off the
     orbs therefore made the orbs bigger and pushed them down too — measured,
     it would have taken 543px of padding to win a race against itself.

     Split, each does one job. This sets how big the picture is. The stage's
     own height, set in script, decides how far the grey goes. Move the strip
     now and only the grey follows it.

     753px is the box that draws the picture at 678 wide, which is the size
     that was settled on while the two were still coupled. The vh term does
     the work on a short window; the cap holds the size steady on everything
     else. Change the cap and the picture changes size — 678 is 0.5625 (the
     frame's aspect) times 1.6 (--stage-zoom) times this. */
  --art-h: min(92vh, 753px);

  /* How much bigger than `contain` the clip is drawn, and the ceiling on it
     is measured rather than guessed.

     Contain fits the frame whole, and for a 9:16 frame in a wide box that
     left the picture at under a third of the page's width. But the frame is
     mostly empty. Measured across ALL 48 frames, at a threshold that catches
     the faint edges, the orbs never reach within:

       top    22.9%   (frame 22)
       bottom 24.6%   (frame 44)
       left    2.8%   (frame 21)
       right   5.6%   (frame 30)

     Re-measured after the clip was recut from the 4K master at 1152x2048.
     Every figure moved by less than a tenth of a percent and the same four
     frames are still the tightest, which is the answer you want from a
     re-measurement: same clip, better sampled.

     That margin is flat 225 field — the same value as the stage behind it —
     so scaling past contain crops padding and nothing else.

     Scaling by k shows 1/k of whichever axis the box is bound by, cropping
     (1 - 1/k)/2 per side of it. So the ceiling is 1.85 when the crop falls
     top-and-bottom and 1.06 when it falls on the sides. This is 1.6, inside
     the vertical one; the phone value below is inside the horizontal one.

     BOTH NUMBERS WERE WRONG FOR A REVISION — 25.3/27.6 and 6.9, from five
     sampled frames at a loose threshold, reading as ceilings of 2.02 and
     1.15. Sample all of them or the number comes out flattering: 1.6 happened
     to be safe, 1.15 was over the real limit and was quietly clipping the
     leftmost orb on a phone.

     Re-measure if the clip is ever recut. The number that matters is the
     smallest empty margin across ALL frames, not the average — one frame with
     an orb near the edge sets the ceiling for the whole clip.

     1.05 HERE, 1.6 IN THE 960 QUERY, and the split is the same measurement
     read the other way round. The 1.85 ceiling is for cropping the frame's
     TOP AND BOTTOM, which is what a wide short box does. A phone's box is
     tall and narrow, so the picture goes width-bound and the zoom crops the
     SIDES instead, where the tightest margin is 2.7% — a ceiling of 1.06.

     So this is the largest the clip can be on a phone without the outermost
     orbs being cut off at the edge of the screen. It was 1.15 for a revision,
     which was over the limit on the bad measurement described above. The
     difference on screen is about 9% of the picture's width. */
  --stage-zoom: 1.05;

  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--stage-h);
  z-index: 0;
  overflow: hidden;
  pointer-events: none;

  /* The clip's own field, sampled off it — so the parts of this box the
     footage does not reach are the same colour as the parts it does, and so
     is the strip above it. */
  background-color: rgb(var(--hero-ground));
}

.hero-stage ~ * {
  position: relative;
  z-index: 1;
}

/* THE HERO GOES ONE ABOVE THE REST, and it is the phone's Sign in button
   that needs it. Below 960 `.hero--on-stage .hero__actions` is positioned
   under the orbs — past the hero's own bottom edge, inside #trust's top
   padding. The rule above put the hero and #trust at the same z-index, and
   two siblings at the same level paint in markup order, so the trust strip
   painted over the button and took every tap meant for it. It was a button
   you could see and not press, on every phone, for as long as the gate has
   been on. One step up and the hero — button included — sits over the strip;
   the strip's own content starts well below it, so nothing else changes.

   Scoped through .hero-stage like the rule it corrects: about.html's hero
   has no stage and must keep no z-index at all — see the note above. */
.hero-stage ~ .hero {
  z-index: 2;
}

/* CONTAIN, NOT COVER. The whole frame is on screen and what is left over is
   padding, which costs nothing because the stage's background is the clip's
   own field: the picture does not end anywhere you can see, it just stops
   having orbs in it.

   The one thing to know is what "all of it" means for a portrait frame in a
   wide box. Contain scales to whichever axis runs out first, and in a hero
   that is the height — so the clip lands as a tall column a few hundred
   pixels wide, centred, with the rest of the width as padding. That is the
   arithmetic of showing a 9:16 frame whole in a 21:9 space, not a bug. If it
   should fill the width instead, the frames have to go back to landscape.

   The box is sized here and NOT in the script. The script only sets the
   canvas's backing store from the box it is given; letting it set the box as
   well would mean the same numbers written in two places, and CSS is the one
   that knows about --stage-top and --stage-pad.

   WIDTH AND HEIGHT, not four insets, and this cost a revision. Both of these
   are REPLACED elements. An absolutely positioned replaced element with
   `width: auto` and both `left` and `right` set is over-constrained, and the
   spec says the intrinsic width wins and `right` is dropped — so the canvas
   laid itself out at its backing store's size, a couple of thousand pixels
   wide, and hung off the side of the page. Stating the size explicitly is the
   only way to make a replaced element fill an inset box.

   The height is --art-h and NOT the stage's, which is the whole point of
   that token: the picture stops where it stops, the grey carries on to
   wherever the trust strip has been pushed. --stage-top holds the top inset
   and is bigger than the side padding because it is clearing the header. */
.hero-stage > img,
.hero-stage > canvas {
  position: absolute;
  top: var(--stage-top);
  left: var(--stage-pad);
  display: block;
  width: calc(100% - (2 * var(--stage-pad)));
  height: var(--art-h);
  object-fit: contain;
  object-position: 50% 50%;
}

/* The zoom, on the POSTER ONLY. The canvas applies the same factor when it
   draws — it reads --stage-zoom off this element, so the two cannot drift —
   and a transform here as well would apply it twice. The stage's `overflow:
   hidden` is what turns the overflow into a crop. */
.hero-stage > img { transform: scale(var(--stage-zoom)); }

/* The canvas is added by script and starts invisible, so the poster is what is
   on screen until there is a frame to show — no blank rectangle waiting on two
   megabytes, and nothing at all to go wrong without JavaScript.

   NO CROSSFADE. The swap is instant, on request: the background should be
   there, not arrive. Only the words fade in, and a background fading up
   underneath them competes with the one thing that is meant to be moving.

   At the top of the page nothing is visible anyway — the canvas comes up on
   frame zero, which is the poster, so it replaces an identical picture. What
   this costs is the one case the fade was for: a reader who lands mid-page,
   or restores a scrolled tab, gets the canvas arriving on frame twenty as a
   hard cut from frame zero. That is a rarer moment than every first load, and
   first load is the one being designed here. */
.hero-stage > canvas { opacity: 0; }

.hero-stage--ready > canvas { opacity: 1; }
.hero-stage--ready > img { opacity: 0; }

/* The hero has no ground of its own on this page: the stage is the ground,
   and a background-color here would sit on top of it.

   `overflow: visible` because below 960 the call to action is lifted out from
   under the lede and positioned under the PICTURE, which ends well past the
   hero's own bottom edge. The hero clips on every other page because it has
   artwork in it to clip; this one's artwork is in the stage. */
.hero--on-stage {
  background-color: transparent;
  overflow: visible;
}

/* The hero arrives in order: eyebrow, headline, lede, button — and slowly.

   nav.js opts these four into the reveal (see REVEAL_ALSO there); this puts
   them in a sequence and gives them their own pace.

   1.8s, against the .38s every other reveal on the site uses. It has been
   .38, then 1.1, and is now nearly five times the site's own value — which is
   a deliberate split and was not the first instinct, because a hero that
   arrives at a different pace from every other section risks looking like a
   different site.

   It earns the exception because it is a different EVENT. A section fading up
   as it is scrolled to should be quick: the reader is already moving and is
   waiting for it to finish. The hero is the first thing on the page, with
   nothing to wait for and nobody scrolling yet, and it is the only moment on
   the site where a slow arrival is the point rather than a delay.

   180ms a step. The four overlap heavily — the last one starts at 540ms and
   the first is still going — which is what makes it read as one thing
   arriving in sequence rather than four things queueing. The whole entrance
   finishes at about 2.34s.

   The background does NOT fade with them; see .hero-stage > canvas. The words
   are the only thing moving, which is what makes a slow fade read as
   deliberate rather than as a page loading badly.

   Section reveals elsewhere are untouched at .38s. If the two should match,
   the value to change is in brand.css, not here.

   Inside the same `no-preference` query the reveal itself lives in, so a
   reader who has asked for less motion gets nothing to delay or slow down. */
@media (prefers-reduced-motion: no-preference) {
  .hero__eyebrow[data-shown="true"],
  .hero__title[data-shown="true"],
  .hero__lede[data-shown="true"],
  .hero__actions[data-shown="true"] {
    transition-duration: 1.8s;
  }

  .hero__eyebrow[data-shown] { transition-delay: 0ms; }
  .hero__title[data-shown]   { transition-delay: 180ms; }
  .hero__lede[data-shown]    { transition-delay: 360ms; }
  .hero__actions[data-shown] { transition-delay: 540ms; }
}

/* Except on a phone, where the button is not part of that sequence at all —
   it is below the fold under the picture, and arrives when it is scrolled to.
   A 270ms delay on something the reader has just scrolled into view is a
   button that hesitates. */
@media (max-width: 959px) {
  .hero__actions[data-shown] { transition-delay: 0ms; }
}

/* -------------------------------------------------------------------------
   The call to action, under the picture (below 960)

   On a wide screen the button is the last line of the hero card, under the
   lede, with the picture off to the right. A phone has no right-hand side, so
   the button and the picture were sharing the same space and the button was
   sitting on the orbs. It moves to the one place on a phone that is clearly
   below the picture and clearly still the hero's: centred, under it, above
   the ticker.

   --orbs-bottom is published by js/hero-frames.js — it is where the ORBS
   end, not where the picture's box does. The two are a couple of hundred
   pixels apart because the bottom quarter of every frame is empty field, and
   anchoring to the box left the button adrift in that emptiness with nothing
   above it. It depends on the zoom, the box, the crop and a measured property
   of the clip, none of which the stylesheet can work out. The fallback of 0px
   is not a guess at the right place; it is the value that leaves the button
   at the top of the hero if the script never runs, which is where it was.

   .hero__inner goes static so the containing block is .hero itself, which is
   the box --pic-bottom is measured against. It is relative on other pages so
   it paints above the media layers underneath it; there are none here, they
   are all in the stage. */
.hero--on-stage .hero__inner { position: static; }

.hero--on-stage .hero__actions {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--orbs-bottom, 0px) + 24px);
  justify-content: center;
  margin: 0;

  /* The gutter has to come back by hand. This is positioned against .hero,
     not against the .wrap it used to sit in, so it inherits none of the
     page's side spacing and would otherwise run edge to edge. */
  padding-inline: var(--gutter);
}

/* Sized to its words, not to the column. .hero__actions .btn is
   `flex: 1 1 240px` on a phone, which is right for a call to action that is
   the full width of a card and wrong for one that is a centred pill under a
   picture — stretched, it spans the whole screen and stops reading as a
   button at all. */
.hero--on-stage .hero__actions .btn { flex: 0 0 auto; }

/* The trust strip drops below the clip, so the icons are clear of the orbs
   rather than sitting among them. Top padding only — the bottom stays where
   .section--tight put it, so the strip moves rather than growing at both ends.

   THE STAGE FOLLOWS IT AT NO COST. Its height is measured off this section's
   bottom in js/hero-frames.js, so the grey goes on behind the icons for
   exactly as far as they have moved. That is the whole reason the stage is
   sized from the page rather than given a number — and, since --art-h split
   the picture off from the stage, moving the strip no longer moves the orbs
   with it.

   Scoped through .hero-stage so it is the one page with a clip that gets it.
   about.html's hero is followed by its own #trust and should not move. */
/* Top padding only, and a thin bottom one, because the ticker is meant to
   run along the very foot of the grey. Any extra room the band needs in
   order to centre the clip goes on TOP of this section — see fit() in
   js/hero-frames.js — so growing the band pushes the ticker down to the
   bottom edge rather than leaving it stranded in the middle. */
.hero-stage ~ #trust {
  padding-block-start: clamp(120px, 16vw, 220px);
  padding-block-end: var(--sp-4);
}

/* -------------------------------------------------------------------------
   The two things dark mode cannot do with a token

   Everything else on this site changes theme by resolving a variable
   differently. These two are images, and an image has no variable to
   resolve — so they are the only theme-aware SELECTORS in the stylesheet,
   and both are here rather than scattered.

   1. THE ARTWORK IS INVERTED. Both hero images are drawn dark-on-light. On a
      dark page the render would be a bright rectangle, and the helix would
      vanish outright: `darken` keeps the per-channel minimum, and against a
      dark ground the ground wins everywhere, so the strands disappear rather
      than the white field.

      Inverting fixes both at once. The render becomes light spheres on dark.
      The helix becomes white strands on black, and its blend flips to
      `lighten` — the mirror of the same trick, keeping the maximum, so the
      black field loses to the ground and the strands stay.

      Both files are effectively greyscale, which is what makes a plain
      `invert(1)` safe. A coloured photograph would come back with its hues
      rotated and would need a different approach — and one has arrived. The
      orbs on the home page are a colour render with real cyan in the glass,
      so a plain invert turns them orange. `invert(1) hue-rotate(180deg)` is
      the standard pairing for that: the invert flips lightness AND hue, the
      rotate puts the hue back, and what is left is the lightness flip on its
      own. It applies to the canvas and to the poster under it, so the two
      cannot disagree during the crossfade.

   2. THE LOGO IS KNOCKED OUT WHITE. assets/logo.png is a navy silhouette
      with its alpha intact, so `brightness(0) invert(1)` takes every opaque
      pixel to black and then to white without touching the alpha. Exactly
      the technique the footer used to use in reverse, before the footer went
      light. No second asset.

   Both blocks are duplicated for the media query, for the reason given at the
   palette in brand.css: one rule carrying both selectors would stop the
   toggle from beating the system preference.
   ------------------------------------------------------------------------- */
:root[data-theme="dark"] .hero__media img { filter: invert(1); }
:root[data-theme="dark"] .hero__media--blend img { mix-blend-mode: lighten; }
/* THE CLIP IS NOT INVERTED HERE. js/hero-frames.js does it with ctx.filter, on
   the canvas, because a CSS filter and a background-color do not land on the
   same colour even when the arithmetic says they must.

   The clip's field is 226. Inverted it should be 29, and --hero-ground in this
   palette is 29 so the band around the picture matches the picture. Through
   ctx.filter it is 29 exactly — measured, drawImage then getImageData. Through
   a CSS filter it composites visibly darker than a plain rgb(29,29,29) beside
   it: reproduced on a bare div, a bare canvas, with and without the hue-rotate.
   CSS filters are not applied in the same space the background-color is.

   That was invisible while the frames covered their whole box. The crop made it
   a hard edge straight across the hero, because the removed bands show
   --hero-ground and the kept band shows the filtered picture.

   The POSTER is hidden in dark instead of filtered, for the same reason — it
   would flash that same mismatched band for as long as it is up. The cost is
   that a dark-mode reader with no JavaScript gets the flat band and no still,
   which is a fair trade for the seam being gone for everybody else. */
/* THE HUE-ROTATE, AND ONLY THE HUE-ROTATE. js/hero-frames.js inverts the clip
   itself, with a difference composite, because CSS invert() loses 22 of the
   256 levels and this picture lives entirely in the dark end where that shows.
   The hue is a different matter: it is a plain matrix, it is lossless, and as
   an ELEMENT filter it works everywhere — which the 2D context's ctx.filter
   did not, silently doing nothing on iOS and leaving the orbs copper rather
   than cyan. Applied here it lands on whatever the canvas has already drawn,
   so the order takes care of itself.

   The canvas is opaque and its ground is neutral, and hue-rotate leaves a
   neutral alone, so the band around the picture is unaffected and still
   matches --hero-ground beside it. */
:root[data-theme="dark"] .hero-stage > canvas { filter: hue-rotate(180deg); }

:root[data-theme="dark"] .hero-stage > img { opacity: 0; }
:root[data-theme="dark"] .site-head__logo img,
:root[data-theme="dark"] img.site-foot__logo { filter: brightness(0) invert(1); }

.hero__veil {
  /* No z-index — it sits above the media because it comes after it in the
     markup, and giving it one would build the same stacking-context wall
     described at .hero__media. */
  /* Much heavier than the dark version was, and heaviest of all on a phone.
     Two things changed when the panel inverted.

     One: the strands are now the darkest thing in the band and the type is
     dark too. Under the old dark scrim both were white, so a strand crossing
     a letter cost almost nothing; here one strand through the lede took it
     to 4.01:1 at 1280, under the 4.5 floor.

     Two, and this is the one that decides these numbers: `object-fit: cover`
     into a 375-wide band crops a 2560-wide render to its middle 375, which
     is the densest part of the helix, at roughly 2× zoom, directly behind
     the text. Measured at 375 with the desktop alphas the lede came out at
     2.06:1 and the eyebrow at 2.72:1. Not a near miss.

     So on a phone the photograph is a watermark and the type is legible;
     above 960px, where the crop is wide enough that the helix sits to the
     right of the text, the query below lets far more of it through. That
     trade is the right way round — nobody chooses the picture over the
     sentence. */
  background:
    linear-gradient(100deg,
      rgba(var(--hero-scrim), .99) 0%,
      rgba(var(--hero-scrim), .96) 36%,
      rgba(var(--hero-scrim), .86) 66%,
      rgba(var(--hero-scrim), .66) 100%),
    linear-gradient(180deg, rgba(var(--hero-scrim), .30) 0%, rgba(var(--hero-scrim), 0) 40%);
}

.hero__inner {
  /* `position: relative` and no z-index: it is last in the markup, so it
     paints above both layers behind it without claiming a level. Same reason
     as the veil — see .hero__media. */
  position: relative;
  display: grid;
  justify-items: start;
  text-align: left;
  gap: var(--sp-5);
  /* No max-width here. It used to cap this block at 46rem, and because .wrap
     also sets `margin-inline: auto` that centred the whole thing — the panel
     sat 320px in from the left edge on a 1280 screen rather than at the
     gutter. The limit belongs on .hero__card below, which is a child and so
     hugs the left of this column rather than being centred in it. */
}

/* THE CARD IS OFF. Trying the words on the band itself.

   What is left of this rule is the layout — the stack, the gap and the width
   limit, which the words need wherever they sit. The surface is commented out
   directly below rather than deleted, so this is one uncomment to put back.

   The one thing to check before keeping it this way is contrast, because the
   card is what has been carrying it: the type used to be read against the
   artwork through the veil, and every measurement of this hero was about how
   much of the picture could pass behind a letter. That question is back.
   Two things say it should hold, and both want measuring rather than
   assuming — see the note below the rule.

   `max-width` in ch rather than px so it tracks the type: the title caps at
   18ch and the lede at 44ch, and a column a little wider than the longer of
   them is the column those two want. Left at the gutter because it is a
   child of .hero__inner rather than another .wrap, so it never picks up the
   centring margin. */
.hero__card {
  display: grid;
  justify-items: start;
  gap: var(--sp-5);
  max-width: 52ch;

  /* The surface. Uncomment all four to bring the card back; the padding
     scales because at 24px a 56px headline looks posted into the corner and
     at 44px on a phone there is no room left for the words.

  padding: clamp(24px, 3.4vw, 44px);
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--surface);
  */
}

/* The size, weight and tracking that used to live here are the base heading
   rules now — every title on the site was matched to this one. What is left
   is what is genuinely particular to the hero: a measure, and a colour for
   type sitting directly on a photograph. */
/* The hero title sets its own size and does not take --t-hero.

   Every h1 and h2 on the site shares one size, which was asked for and is
   right for the body of the page: it makes the outline read as one level
   rather than a competition. A hero is not in the body of the page. It is the
   first thing on the site and it has a photograph behind it, so at 28px it
   was a section heading that happened to be at the top.

   This is a separate ramp rather than a bigger --t-hero, because moving that
   token would take every h2 on twelve pages with it. */
/* The one word the headline turns on, drawn hollow and in the accent.

   --accent-ink AND NOT --accent, which is the thing to get right here. The
   token is already theme-aware — #01698E in daylight, #02BBFD at night — and
   it is the readable half of the pair: measured on the hero's own ground it is
   4.75:1 light and 7.65:1 dark, against the 3:1 that large text needs. The
   bright brand blue on the light ground is 1.70:1, which is under the floor
   for solid type and hopeless for a hollow outline, where there is a fraction
   as much ink to carry the contrast.

   THE STROKE IS IN em, not pixels. --t-hero runs from 33.6px on a phone to
   56px on a desktop; a fixed width would be heavy at the small end and spindly
   at the large one, and .03em holds the same weight relative to the letter at
   both — 1.7px on a desktop.

   WITH A FLOOR, though, because proportional alone breaks down at the bottom:
   .03em of the phone's 33.6px is barely a pixel, and a sub-pixel stroke gets
   antialiased into a grey suggestion of a letter rather than a blue one. The
   1.25px floor is where it still resolves as a line.

   paint-order puts the stroke under the fill rather than over it, so it grows
   outward and the counters inside the P and the R stay open — which matters
   more the thinner this gets, since a stroke painted inward would start
   closing them. */
.hero__word {
  color: transparent;
  -webkit-text-stroke: max(1.25px, .03em) var(--accent-ink);
  paint-order: stroke fill;
}

/* No hollow type where the outline cannot be trusted to render — Windows high
   contrast replaces colours wholesale and would leave the word invisible. */
@media (forced-colors: active) {
  .hero__word {
    color: HighlightText;
    -webkit-text-stroke: 0;
  }
}

.hero__title {
  max-width: 18ch;
  color: var(--ink);
  /* --t-hero, which IS this clamp — the token was moved to match this rule
     rather than the other way round, so the section headings could be the
     same size. Written out here for a revision and then shared; two copies of
     a clamp is two chances to change one of them. */
  font-size: var(--t-hero);
  line-height: 1.05;
  /* -.03em, tightened from -.02 on request, and it is not the same tracking
     the other headings get: h1-h6 in brand.css sit at -.01em. Display type
     needs to close up as it grows — the gaps between letters scale with the
     point size but the eye's tolerance for them does not — and at 56px in a
     600 weight this is a line of words rather than a row of letters. It is
     the largest type on the site, so it is also the safest place to do it.
     Do not push the same value down into the body ranges. */
  letter-spacing: -.03em;

  /* 500, and the only element on the site above 400 apart from <strong>.

     The rest of the type came down to 400 deliberately and stays there. This
     is the exception because it is the exception typographically: at 56px a
     display line carries a heavier weight without reading as bold, where the
     same step at body size would.

     600, and the face is loaded for it. The pages request
     `wght@400;500;600`, which they did not when this was 500 — at that point
     600 would have been a weight the browser faked by smearing the 500, and a
     synthesised weight at 56px looks like a rendering fault rather than a
     heavier headline. The rule stands for whoever comes next: to go heavier
     again, load the face; do not just type a bigger number. */
  font-weight: 600;
}

.hero__lede {
  /* 21ch, down from 44, so the sentence stops before the clip does.

     THE NUMBER IS MEASURED, not chosen, and the method is worth keeping
     because the answer was not the obvious one. The clip is a centred column
     and the lede runs from the gutter into it; compositing all 48 frames and
     taking, for every column of pixels, the darkest value that appears there
     in ANY frame gives the point past which no text is safe. At 1280 that is
     305px from the gutter — and it is the same 305px whatever colour the type
     is, because at that column an orb's refraction edge goes to near-black
     and no ink beats it. Darkening the lede was the cheaper-looking fix and
     it buys nothing.

     21ch is 288px, which leaves about 17px of margin for the type rendering
     a shade wider than it measured.

     Re-measure if --stage-zoom, the stage's height or the clip changes: all
     three move the picture's left edge. Widen the lede and the last words of
     the sentence go onto the orbs, which is where this started. */
  max-width: 21ch;
  color: var(--ink-soft);
  /* Back up, and the reason it came down is gone. It was 16px because at a
     28px headline an 18px lede was nearly the same weight as the thing above
     it. The headline is now up to 56px, so there is room for a lede that
     reads as a sentence rather than as a caption. */
  font-size: clamp(1.05rem, .98rem + .45vw, 1.35rem);
  line-height: 1.45;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: var(--sp-3);
}

/* On a 320px screen two buttons side by side are two buttons nobody can
   read. They take a row each until there is room for both. */
.hero__actions .btn { flex: 1 1 240px; }

/* -------------------------------------------------------------------------
   Hover motion

   One rule for the whole site rather than a transform bolted onto each
   component, because "how much does a thing move when you point at it" is one
   decision and should have one answer.

   Two behaviours:

     icons  lift 2px and grow 3%
     cards  gain a 1px blue rim

   THE TRANSITION GOES ON THE RESTING STATE, not inside :hover. On :hover only,
   the movement animates in and then snaps back the instant the pointer
   leaves, which reads as a glitch rather than as a return.

   `@media (hover: hover)` so none of it applies on a touch screen, where
   :hover latches after a tap and leaves a card looking selected until you
   press something else.

   Reduced motion is already handled globally in brand.css, which cuts every
   transition on the site to 0.01ms. The rim still appears there — it is a
   colour, not a movement, and someone who has asked for less motion has not
   asked for less feedback.
   ------------------------------------------------------------------------- */

@media (hover: hover) {
  /* The icon discs: the trust strip, the card icons, the small key icons in
     a spec list, and the numbered steps. */
  .trust__icon,
  .card__icon,
  .spec__icon,
  .step__n {
    transition: transform .22s var(--ease);
  }

  .trust:hover .trust__icon,
  .card:hover .card__icon,
  .spec:hover .spec__icon,
  .step:hover .step__n {
    transform: translateY(-2px) scale(1.03);
  }

  /* The glyph inside a button that is entirely an icon, add-to-basket being
     the one that matters. The button already has its own hover; this is the
     glyph moving within it. */
  .btn--icon svg {
    transition: transform .22s var(--ease);
  }

  .btn--icon:hover svg { transform: translateY(-1px) scale(1.06); }
}

/* The rim, on both kinds of card.

   `box-shadow` and not `border` or `outline`. A border would change the
   element's box and shift its contents by a pixel on hover; an outline sits
   outside the radius on some engines and squares off the corner. An inset
   shadow at 0 blur is a 1px line that follows the radius exactly and costs no
   layout.

   Written into the existing shadow stack rather than replacing it, so the
   card keeps its lift and its sheen and gains an edge.

   Stated as raw channels rather than as var(--accent), and the reason has
   changed. It used to be that no token held a colour that stayed a colour in
   both themes — --blue-600 was navy in daylight and near-white in dark, so
   an accent rim had to be written out by hand. --accent is that token now.
   What is written out here is the ALPHA: .85 rather than solid, because a
   1px line at full strength reads as a hard edge and this is meant to be a
   glow. Change the hue here and change --accent with it. */
.card,
.p-card {
  --rim: transparent;
  transition: box-shadow .22s var(--ease), transform .22s var(--ease);
}

.card:hover,
.p-card:hover,
.card:focus-within,
.p-card:focus-within {
  --rim: rgba(2, 187, 253, .85);
}

@media (hover: none) {
  /* A tap latches :hover on a touch screen, so the rim would stay on the last
     card touched. Focus still shows it, which is the case that matters there. */
  .card:hover,
  .p-card:hover { --rim: transparent; }
}

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

.section {
  padding-block: var(--section-y);
  scroll-margin-top: 88px;
}

.section--tight { padding-block: clamp(36px, 5vw, 64px); }

/* The banner's dark band. It sets its own colour as well as its own ground —
   a section that changes the background and leaves the text to inherit is
   how you end up with navy on navy the first time somebody drops a plain
   paragraph into it.

   The ramp is --grad-band, a token, and it did not used to be: the three
   stops were written here as --navy-900, --navy-800 and --navy-700. That
   broke in dark mode, where --navy-800 is near-white because it is also the
   primary button's fill. The band was painting black to white with pale grey
   text across it. A ground and a fill cannot share a token; see brand.css. */
.section--navy {
  background-color: var(--band-ground);
  background-image: var(--grad-band);
  color: var(--chrome-200);
}

/* Nothing to set. `h2` is already --t-hero at weight 500 in brand.css, and
   .section__title exists now only as the hook the build spec names and the
   markup uses. Left in place rather than stripped from 40-odd elements. */
.section__title { }

.section__lede {
  max-width: 62ch;
  margin-top: var(--sp-3);
  color: var(--ink-soft);
  font-size: clamp(1.02rem, 1rem + .25vw, 1.15rem);
}

/* The section head sets the rhythm: eyebrow, title, lede, then air before
   whatever the section is actually made of. */
.section__lede + *,
.section__title + *:not(.section__lede) { margin-top: var(--sp-6); }

/* The tracked properties are repeated here rather than relied on from
   `.tracked`. The markup contract pairs the two classes, but an eyebrow that
   loses its tracking because somebody wrote one class instead of two is a
   silent, site-wide regression nobody would spot in review. */
.eyebrow {
  display: block;
  margin-bottom: var(--sp-2);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .18em;
  /* Matching .hero .eyebrow below, which is the only eyebrow on the site
     today. Left as --blue-600 this would hand the next eyebrow added anywhere
     the accent that this one was just asked to drop. .coa__issuer shares the
     recipe above but is a different component and keeps its blue. */
  color: var(--ink-soft);
}

/* The eyebrow on the hero band.

   `.hero .eyebrow` (two classes) rather than `.hero__eyebrow` (one). The flat
   version tied with `.eyebrow` on specificity and lost on source order —
   .eyebrow is defined below it — so the eyebrow came out navy on navy.
   Qualifying it means the rule wins wherever either is later moved to.

   GREY, ON REQUEST, AND IT HAS BEEN BOTH. This was --ink-soft, went to the
   accent on request, and has come back on request. Recorded so a later reading
   does not take the grey for an oversight and "fix" it.

   THE CONSTRAINT THAT SURVIVES EITHER CHOICE: the eyebrow must not be the
   headline's colour. An eyebrow the same colour as the line under it has
   stopped being an eyebrow. --ink is 12.1:1 on this band and --ink-soft is
   5.6:1, so the separation is by value rather than by hue — which was the
   accent's argument for the job, and is what is given up by dropping it.

   NOT --chrome-500, which is the obvious grey and measures 2.5:1 on this
   ground: under the floor for a line of text at label size, which is the last
   thing on the page that can carry a marginal colour. --ink-soft clears it at
   5.6:1 light and 7.0:1 dark — better, as it happens, than the 4.7:1 the
   accent was managing. */
.hero .eyebrow {
  color: var(--ink-soft);
  margin-bottom: calc(var(--sp-2) * -1);
}

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

   An `<a>` and a `<button>` are indistinguishable here on purpose: the same
   control does the same job whether it navigates or acts, and a reader
   should never have to work out which one they are looking at.
   ------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  /* 44px is the floor for a finger, and it is the floor everywhere, not just
     on the small screens — a tablet is a touch device at 1024px too. */
  min-height: 44px;
  padding: 11px 22px;
  /* No border: the rim below is the edge, and a border on top of it reads as
     two edges. `position: relative` so the rim has something to sit in. */
  position: relative;
  /* A border again. The note above said the rim was the edge and a border on
     top of it read as two edges; the rim has gone with the glass, so this is
     the only edge now. */
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body);
  /* 500 and 15px, not 700 at 16px with letterspacing. A bold, tracked,
     full-size label makes every button shout; RGM's read as quiet because
     the type in them is nearly normal weight. The pill shape and the fill
     do the work of saying "this is pressable". */
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  /* No shadow. It was a lift plus an inset line reading as the pane's
     thickness catching the light, and neither has anything to sit on now. */
  transition: background-color .18s var(--ease), color .18s var(--ease),
              box-shadow .18s var(--ease), transform .18s var(--ease);
}


.btn:hover {
  background: var(--paper-2);
  color: var(--ink);
  border-color: var(--chrome-300);
}

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

/* Tinted glass, not paint.

   `.btn` above already carries the whole system — the backdrop blur, the
   masked rim, the sheen along the top. This used to drop an OPAQUE navy on
   top of it, which left the rim and the blur technically applied and
   completely invisible: nothing can show through a solid fill. The colour is
   the same navy at 72%, so the blur behind it finally does something and the
   button picks up whatever it is sitting on — the helix in the hero, the
   page wash everywhere else.

   LIGHT glass with navy text, not tinted glass with white text. It went
   through a navy-at-72% stage on the way here; that read as a dark button
   with the background faintly visible through it, rather than as glass.
   Clear glass over a pale page is the thing itself.

   The tint is gone, so contrast is no longer the binding constraint —
   --navy-800 on this is 13:1 at worst. What matters instead is that it
   still reads as the PRIMARY action next to .btn--ghost, and that is now
   carried by two things only: a fill where the ghost has none, and a real
   drop shadow where the ghost has none. Take either away and the pair
   becomes two outlines. */
/* NO FILL AT REST. It carried --surface, which is white, and on the hero's
   225 band that read as a white pill stuck on grey rather than as an outline
   drawn on it. `transparent` rather than a colour, so it matches whatever it
   is standing on — the band here, the paper on every other page, and anything
   either of those becomes later — with no second value to keep in step.

   What marks it out is its edge: on a page with no shadows and no fills,
   weight of edge is what says which control is the one to press.

   THIS IS WHERE THE ACCENT GOES, and it is the only button that gets it. An
   accent that lands on every control is a second body colour; an accent that
   lands on one control per view is a signpost. Everything else on the page —
   ghost buttons, filters, the header icons — stays --ink.

   Two forms of the accent, for the reason set out in brand.css. At rest the
   edge and the label are --accent-ink, the readable form, because a #02BBFD
   hairline on white is 2.2:1 and would read as no border at all.

   HOVER FILLS AND THE LABEL GOES WHITE, on request, and the fill is the
   reason the request needed a decision rather than a value. It used to fill
   with the accent as supplied and flip the label to near-black. White on the
   supplied colour is 2.2:1 — the same number that keeps it off every other
   surface on this site — so a white label and a #02BBFD fill cannot both be
   had. --accent-ink is 5.6:1 under white, which is the same deep blue the
   button already wears on its edge, so what the reader sees is the outline
   filling itself in.

   --btn-fill and --btn-fill-ink rather than the two colours directly,
   because dark mode wants the opposite arrangement and not the same one in
   different values: a deep blue button on a near-black page is barely a
   button, so there the fill is the accent at full strength and the label
   goes near-black. See brand.css. Nothing here needs to know that.

   It was a 28% white pane over the hero, chosen so the artwork behind it
   stayed the thing you noticed. There is nothing behind it now. */
.btn--primary {
  background: transparent;
  color: var(--accent-ink);
  border-color: var(--accent-ink);
}

.btn--primary:hover {
  background: var(--btn-fill);
  color: var(--btn-fill-ink);
  border-color: var(--btn-fill);
}

.btn--ghost {
  background: transparent;
  border-color: var(--chrome-300);
  color: var(--ink);
  box-shadow: none;
}

.btn--ghost:hover {
  background: var(--blue-100);
  border-color: var(--blue-400);
  color: var(--ink);
}

/* The one destructive control on this site: "Close my account", on the
   account page, and nothing else. It is a ghost button wearing --bad rather
   than a filled red block, and that is the whole design: a solid red button
   is the loudest thing on any page it appears on, and loudness is not the
   same as care. The outline says which button this is; the sentence above
   it, the disclosure it sits behind and the password it asks for are what
   actually slow the press down.

   It fills on hover exactly as .btn--primary does — same gesture, different
   colour — so the confirmation is that the outline you were reading has
   become the button you are pressing. White on --bad is 6.1:1 in daylight;
   the dark palette's --bad is a light rose, so there the label goes to the
   page's own ground instead, which is the same swap .btn--onnavy makes. */
.btn--danger {
  background: transparent;
  color: var(--bad);
  border-color: var(--bad);
}

.btn--danger:hover {
  background: var(--bad);
  border-color: var(--bad);
  color: var(--paper);
}

/* For the dark band, where a light-on-dark button has nothing to sit against.
   THE BAND'S OWN TWO COLOURS, SWAPPED: its ink for the ground, its ground for
   the label. That holds in both palettes because both of those tokens are
   fixed — --on-band is white either way and --band-ground is dark either way —
   so the button is white with dark type on navy in daylight and white with
   near-black type at night.

   IT USED TO BE --paper AND --ink, both of which flip. On a dark page that
   made a #121212 button on an #080808 band: 1.1:1, which is a button you
   cannot see the edge of, under the 3:1 that WCAG 1.4.11 asks for a control's
   boundary. The label stayed readable at 15:1, so nothing looked broken — it
   just stopped looking like a button. The comment here used to describe that
   flip as the point of the thing. It was not; it was the bug. */
.btn--onnavy {
  background: var(--on-band);
  border-color: var(--on-band);
  color: var(--band-ground);
}

.btn--onnavy:hover {
  background: var(--chrome-200);
  border-color: var(--chrome-200);
  color: var(--band-ground);
}

/* The outline button on a dark ground. .btn--ghost is a chrome outline with
   navy text and disappears entirely on the hero; this is the same shape in
   reverse. Used beside .btn--onnavy so the pair reads as primary + secondary
   rather than as two solid blocks. */
.btn--ghost-light {
  /* Glass on a dark ground is a lightening, not a white tint — a 58% white
     pane over a navy photograph is just a pale blob. */
  background: var(--glass-bg-dark);
  color: var(--on-band);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28);
}

.btn--ghost-light::before {
  background: linear-gradient(135deg,
    var(--glass-rim-dark) 0%,
    rgba(255, 255, 255, .14) 46%,
    rgba(255, 255, 255, .10) 62%,
    var(--glass-rim-dark) 100%);
}

.btn--ghost-light:hover {
  background: rgba(255, 255, 255, .12);
  border-color: var(--on-band);
  color: var(--on-band);
}

/* Smaller in the ways that do not cost anything — tighter sides, a smaller
   face — and exactly as tall as every other button. An earlier version let
   this one drop to 38px wherever the browser reported a fine pointer, which
   is a reasonable-sounding rule and the wrong one: the 44px floor in the
   spec is unqualified, a laptop with a touchscreen reports a fine pointer,
   and this is the button that adds things to the basket. */
.btn--sm {
  padding: 9px 16px;
  font-size: .95rem;
}

/* A button whose whole label is a picture.

   Square, and 44px even when it follows .btn--sm — the small modifier trims
   the padding a text button can spare, but a finger needs the same target
   whether the button says a word or shows a glyph. Equal padding on all four
   sides is what makes it a circle rather than a squashed pill.

   The button must carry an aria-label; there is no text in here to read.
   js/product-card.js names the product in it rather than saying "Add to
   basket" twelve times down a shop page. */
.btn--icon {
  padding: 0;
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  flex: 0 0 auto;
}

/* 24 in a 44 button, not the 22 this started at. The client's e-commerce
   pack is a fine-line style on a 24 viewBox — more detail than a 2px-stroke
   glyph and therefore more to lose. At 22px the plus in its circle was
   closing up on a navy fill.

   `.btn.btn--icon svg`, not `.btn--icon svg`. There is a `.btn svg { width:
   20px }` further down this file for the glyph that sits BESIDE a label, and
   one class plus one element ties with one class plus one element — so the
   later rule was quietly winning and this was rendering at 20. Two classes
   settles it wherever either rule is moved to. */
.btn.btn--icon svg {
  width: 24px;
  height: 24px;
  display: block;
}

.btn--block {
  display: flex;
  width: 100%;
}

/* Disabled, and it takes its colour back off the accent as well as dropping
   to half opacity. A greyed-out control that is still wearing the accent is
   a control that still looks pressable; the .5 opacity alone was not saying
   it, and .btn--primary is the variant that actually gets disabled here (out
   of stock). The hover rule below repeats these values rather than picking
   up the variant's own, so nothing moves under the cursor. */
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: .5;
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-soft);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}

.btn[disabled]:hover,
.btn[aria-disabled="true"]:hover {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-soft);
}

.btn--ghost[disabled]:hover,
.btn--ghost[aria-disabled="true"]:hover {
  background: transparent;
  border-color: var(--chrome-300);
}

.btn svg { width: 20px; height: 20px; flex: none; }

/* -------------------------------------------------------------------------
   Pills and the chevron badge
   ------------------------------------------------------------------------- */

/* `justify-self` matters more than it looks: a pill dropped straight into a
   grid — which is what `.pdp__body` and `.p-card__body` are — gets blockified
   and stretches the full width of the column, and a tag as wide as the
   paragraph under it stops reading as a tag. Ignored outside grid, so it is
   safe everywhere else. */
.pill {
  display: inline-flex;
  align-items: center;
  justify-self: start;
  gap: var(--sp-1);
  position: relative;
  padding: 4px 12px;
  border: none;
  border-radius: var(--r-pill);
  background: var(--glass-bg);
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  box-shadow:
    0 1px 3px rgba(7, 22, 52, .06),
    inset 0 1px 0 rgba(255, 255, 255, .8);
  color: var(--ink-soft);
  font-size: var(--t-small);
  font-weight: 400;
  line-height: 1.5;
  white-space: nowrap;
}

.pill--blue {
  background: var(--blue-100);
  border-color: color-mix(in srgb, var(--blue-400) 26%, var(--blue-100));
  color: var(--ink);
}

.pill--chrome {
  background: var(--chrome-200);
  border-color: var(--chrome-300);
  color: var(--on-chrome);
}

/* The banner's flag. On a phone it is a plain pill: a 60px pennant with two
   words stacked inside it is a shrunken version of a thing that only works
   big, and a pill at least reads. The flag proper arrives at 560px. */
.badge-chevron {
  display: inline-flex;
  align-items: baseline;
  gap: .4em;
  padding: 9px 18px;
  border-radius: var(--r-pill);
  background: var(--navy-800);
  color: var(--on-band);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-small);
  text-transform: uppercase;
  letter-spacing: .16em;
  text-align: center;
}

/* -------------------------------------------------------------------------
   The trust row — PRECISION / PURITY / UK BASED

   auto-fit rather than a fixed three, because the compliance band at the
   foot of the home page is the same component with four items in it.
   ------------------------------------------------------------------------- */

.trust-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}

.trust {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--sp-4);
  text-align: left;
}

.trust__icon {
  position: relative;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--blue-700);
}


.trust__icon svg { position: relative; z-index: 2; }

.trust__icon svg { width: 24px; height: 24px; }

.trust__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--ink);
}

.trust__text {
  margin-top: var(--sp-2);
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

/* -------------------------------------------------------------------------
   .trust-row--split — the strip under the hero

   The same three items as the banner, and styled from the banner rather than
   from the glass system: a drawn navy ring instead of a glass disc, tighter
   tracking on the label, and hairline rules between the three once there is
   room for a row. Scoped to the modifier because the plain .trust-row is
   also the compliance band at the foot of the page, where the glass disc and
   the wide tracking are right.
   ------------------------------------------------------------------------- */

/* A ring, not a disc. The glass treatment needs something behind it to
   refract; up here the ground is the page's own pale wash and a glass disc
   reads as a grey circle. The banner draws these as outlines and that is
   also the honest thing to do on a flat ground. */
/* Filled discs, and the glyph knocked out of them.

   One flat --navy-800, which is THE blue. It went through a navy-to-royal
   gradient and then a flat royal blue on the way here, and both were wrong
   for the same reason in the end: three discs across a row have to be three
   identical circles, and any second colour in them — whether within one disc
   or against the palette — makes the row read as an assortment.

   This also puts the strip back under the one-blue rule. The logo is the
   only artwork on the site carrying a brighter blue, and it can, because it
   is a supplied file rather than a surface this stylesheet paints.

   `color` is what does the glyph: every icon in this project is drawn with
   `stroke="currentColor"`, so setting the colour on the disc knocks the
   strokes white without touching a single path. White on --navy-800 is 16:1,
   which is as much headroom as this site has anywhere. */
/* Icon above the words, not beside them.

   Beside them, the label and its sentence had to share a narrow column and
   both wrapped — "PRECISION." on one line and "Engineered for results." on
   the next, in a strip that has the full page width available to it. Stacking
   gives each line the whole column and the wrap goes away.

   Centred, which is Craig's call and overrides the note that used to sit
   here arguing for left. The argument was that every other block on the page
   starts at the gutter — still true, and this strip is now the one exception.
   It gets away with it because it is a row of three equal things rather than
   a block of prose: the eye reads it as a set, not as a paragraph that has
   wandered off the margin. */
/* A TICKER BELOW 960, and a centred three-across strip above it.

   The three items run as a marquee along the foot of the hero's grey band.
   They have been three things on a phone now — the strip stacked, then a
   bullet list, now this — and the reason the stack went is worth keeping:
   600px of chrome for nine words, with three discs that read as a second set
   of orbs next to the clip behind them. A ticker is nine words tall.

   HOW THE LOOP IS SEAMLESS. nav.js duplicates the three items, so the track
   holds two identical halves; translating it by exactly -50% lands on the
   copy in the same place the original started, and the jump back to 0 is
   invisible. Change the number of items and nothing here needs touching. The
   copies are aria-hidden — see nav.js.

   THE WRAP IS THE WINDOW. A marquee needs something to be clipped by, and
   the section's own .wrap is already there and already the full width of the
   content column; below 960 it gives up its gutter and its max-width so the
   strip runs edge to edge, and takes `overflow: hidden` so the track can be
   wider than the screen without the page scrolling sideways.

   26s for about 1000px of track is roughly 40px a second: fast enough to
   read as moving, slow enough to read.

   No `data-reveal` on this list. The reveal animates a transform and so does
   the ticker; one of them has to lose and it should not be this one. */
.hero-stage ~ #trust > .wrap {
  max-width: none;
  padding-inline: 0;
  overflow: hidden;
}

.trust-row--split {
  display: flex;
  width: max-content;
  gap: 0;
  animation: pxp-ticker 26s linear infinite;
}

.trust-row--split > .trust {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding-inline: var(--sp-5);
  white-space: nowrap;
}

.trust-row--split .trust__icon {
  width: 26px;
  height: 26px;
}

.trust-row--split .trust__icon svg { width: 14px; height: 14px; }

.trust-row--split .trust__body { display: contents; }

@keyframes pxp-ticker {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Still duplicated, still readable, just not moving. */
@media (prefers-reduced-motion: reduce) {
  .trust-row--split { animation: none; }
}

/* Glass discs, to the same recipe as the cards.

   These went navy-filled → flat navy → glass. The recipe's two lit EDGES are
   not used here and cannot be: `::before` is a 1px line across the top of the
   element's box, which on a circle clipped to `border-radius: 50%` is a point.
   A disc needs a rim that runs all the way round, which is what the masked
   gradient below does — the same technique the cards used before the recipe
   replaced it.

   What does carry over is the part that matters: a thin translucent pane,
   9px of blur, and the inner glow. The glow is scaled to the disc — 8px/4px
   rather than the card's 12px/6px, because at 60px across the card's's value would
   flood the whole circle and leave no glass to see through.

   The glyph goes back to navy. It was white for a navy fill; on a pale pane
   white would vanish. */
.trust-row--split .trust__icon {
  /* No size here any more. It was 72px, and that is now set in the 960 query
     with the rest of the strip — below that width this IS a bullet, which is
     what the old comment here said it must not be, and the answer turned out
     to depend on how much room there is rather than on principle. */

  /* NO FILL. It was --surface, which is white, and these three sit on the
     hero band's 225 grey — so they read as white coins dropped on it rather
     than as rings drawn on it. `transparent` rather than a matching value,
     for the same reason as .btn--primary: it takes whatever it is standing
     on and there is no second number to keep in step.

     The hairline stays. Without a fill it is the only thing that makes this
     a disc rather than a floating glyph, and --line is black at 13%, which
     reads on the band without being a drawn circle.

     Only this modifier, which is only on index.html. The unsplit .trust__icon
     on about.html and quality.html keeps its fill — those grounds are
     --paper, so a --surface disc is invisible there anyway and changing it
     would be churn for no visible difference. */
  background: transparent;
  border: 1px solid var(--line);
  box-shadow: none;
  color: var(--blue-700);
}

/* The masked glass rim, off. `content: none` rather than `display: none` —
   the element never generates, so there is nothing for the mask-composite to
   cost on a page that has plenty of it already. */



/* .18em is the label tracking used on eyebrows and on the compliance band,
   where the text is a lone word doing a signpost's job. Here it sits above a
   sentence and reads as part of it, so it closes up to something nearer the
   banner's own setting. */
.trust-row--split .trust__title {
  font-size: max(15px, var(--t-small));
  letter-spacing: .04em;
}

/* -------------------------------------------------------------------------
   Cards
   ------------------------------------------------------------------------- */

.cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}

/* A pane, like the buttons and the icon discs. No border — the rim below is
   the edge, and a border drawn on top of a rim reads as two edges. */
/* The glass card, to Craig's recipe.

   Replaces the masked-rim version this used before. The differences are
   deliberate and worth naming, because they are what makes it read as glass
   rather than as a tinted panel:

     - 9px of blur, not 22. Heavy blur turns the backdrop into a flat colour;
       at 9 you can still tell WHAT is behind the pane, which is the whole
       effect.
     - An inner glow, `inset 0 0 12px 6px`, that the old rim had no equivalent
       for. This is the light caught in the thickness of the pane and it is
       the single most convincing part of the recipe.
     - A real 1px border plus two edge highlights, instead of one masked
       gradient rim around the whole perimeter. The top edge is lit and fades
       at both ends; the left edge is lit at the top and falls away. Light
       from the top-left, consistently.

   `overflow: hidden` is required — the ::after runs the full height of the
   card and would otherwise square off the rounded corners. */
/* Flat, with a hairline. This was glass: a translucent fill, a 9px
   backdrop-filter, a lit top edge, a shaded bottom edge and a 12px inner
   bloom, plus two pseudo-elements drawing lit edges. All of it has gone.

   `isolation: isolate` stays. It is not left over from the glass — it keeps
   the card its own stacking context so nothing inside can paint over a
   neighbour, and the hover rim below relies on the box-shadow order being
   predictable. */
.card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--surface);
  box-shadow:
    /* The hover rim, declared in the resting state so it transitions and
       transparent until --rim is set. See the hover block below. */
    inset 0 0 0 1px var(--rim, transparent);
}



.card > * { position: relative; z-index: 2; }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .card { background: var(--paper); box-shadow: var(--shadow-1); }
}

@media (prefers-reduced-transparency: reduce) {
  .card {
    background: var(--paper);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* The icon above a card's title.

   A disc like .trust__icon, at the smaller end — a card already has a heading
   doing the identifying, so the icon is a marker rather than the subject.
   currentColor is what lets the same file be navy here and white on the navy
   band, which is the whole reason the pack's hardcoded #000 had to go. */
.card__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-bottom: var(--sp-3);
  border-radius: var(--r-pill);
  background: var(--blue-100);
  color: var(--blue-700);
}

.card__icon svg { width: 22px; height: 22px; }

/* On the dark band the disc inverts rather than the icon disappearing. */
.section--navy .card__icon {
  background: rgba(255, 255, 255, .12);
  color: var(--on-band);
}

/* Two lines' worth of space whether the title uses them or not.

   "Identity by mass spectrometry" wraps and "Purity by HPLC" does not, so
   without this the body copy in a row of three cards starts at three
   different heights and the row reads as ragged even though the cards are
   the same height. 2.5em is exactly two lines at the 1.25 line-height these
   inherit — measured, not guessed, and it has to move with that number.

   A third line still pushes, which is the right failure: a title that long
   is a copy problem and should look like one. */
.card__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-h3);
  line-height: 1.25;
  min-height: 2.5em;
  color: var(--ink);
}

.card__text {
  margin-top: var(--sp-3);
  color: var(--ink-soft);
}

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

/* -------------------------------------------------------------------------
   Specification lists — the chemical and supply facts

   A key above its value on a phone, side by side once there is room. The
   value is told it may break anywhere: a peptide sequence is one word 40
   characters long and it is the single most likely thing on this site to
   push a 320px page sideways.
   ------------------------------------------------------------------------- */

.spec-list {
  border-top: 1px solid var(--line);
}

.spec {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-1);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--line);
}

.spec__key {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--ink-soft);
}

/* An icon beside a specification label.

   Inside the <dt> rather than in a third grid column, because a <dl> gives
   you exactly two children per row and inventing a wrapper to get a third
   would change the markup's meaning to gain nothing. The key becomes a flex
   row and the icon rides along with it.

   The disc is smaller than .card__icon's 40px: a spec row is a line of text,
   not a panel, and a 40px disc beside an 11px tracked label makes the label
   look like a caption on the icon rather than the other way round. */
.spec__key {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.spec__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: var(--r-pill);
  background: var(--blue-100);
  color: var(--blue-700);
}

.spec__icon svg { width: 17px; height: 17px; }

/* On the dark band the disc inverts, exactly as .card__icon does. */
.section--navy .spec__icon {
  background: rgba(255, 255, 255, .12);
  color: var(--on-band);
}

.spec__val {
  color: var(--ink);
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

/* Two columns where there is room: a short label reads better beside its
   explanation than stacked above it, and stacked is what .spec does by
   default because on a product page it sits in a narrow side column. */
@media (min-width: 640px) {
  .spec {
    grid-template-columns: 13rem 1fr;
    gap: var(--sp-5);
    align-items: baseline;
  }
}

/* dl/dt/dd carry browser margins the reset does not clear. */
.spec-list dd { margin: 0; }

/* -------------------------------------------------------------------------
   "Where the 99% comes from" — three plain points beside a photograph

   The points were three `.card`s with an icon each. It is a short list of
   facts, and three bordered panes made it the fourth grid of cards on a page
   that had three already. A <dl> says what it is: a term and its explanation,
   three times, separated by hairlines.

   A full-bleed band with the photograph across the top and numbered points
   beneath was built and rejected — see git if it is ever wanted back. The
   photograph sits in the column beside the copy instead.
   ------------------------------------------------------------------------- */

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

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

.purity__media {
  margin: 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0, 0, 0, .1);
  /* A hairline over the photograph's own edge — the shot runs to a pale
     ceiling at the top and would otherwise dissolve into the page rather
     than ending. */
  outline: 1px solid var(--line);
  outline-offset: -1px;
}

.purity__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Right of centre. The analysers are in the right two thirds and the left
     edge is a foreground blur; a centred crop in a tall column keeps the
     blur and loses the equipment. */
  object-position: 62% 50%;
}

.points {
  margin-top: var(--sp-6);
  border-top: 1px solid var(--line);
}

.point {
  border-bottom: 1px solid var(--line);
}

/* The row: tick, heading, then the chevron pushed to the far edge by the
   auto margin on ::after. The heading is a bare text node between the two,
   which is all a flex row needs — it becomes an anonymous flex item and
   sizes to its content, so nothing has to be wrapped in a span to make the
   spacing work. */
.point__q {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 56px;
  padding-block: var(--sp-4);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 400;
  /* Its own ramp, not --t-h3, and the reason is the column rather than
     taste. The FAQ's headings run the full width of the page; these run
     half of it, and lose a further 55px to the tick and the chevron. At
     --t-h3 the longest of the three — "Identity by mass spectrometry" —
     broke over two lines on a 375 phone, with most of the first line
     empty. This tops out just under --t-h3 and bottoms out at 16px, which
     keeps all three on one line at 375. Lengthen a heading and check it
     there before assuming it still fits. */
  font-size: clamp(1rem, .78rem + .58vw, 1.32rem);
  color: var(--ink);
}

/* Both engines, and both are needed: `list-style: none` above kills the
   marker in Firefox and Chrome, this one in older WebKit. */
.point__q::-webkit-details-marker { display: none; }

.point__q:hover { color: var(--blue-700); }

/* The checkbox. A ticked one — it is not an input and there is nothing to
   toggle; it reads as "this is already the case", which is the whole claim
   the section is making. aria-hidden in the markup for the same reason:
   read aloud it would announce a control that does not exist.

   Same glass as .card, with two changes, both forced by the size.

   The inner glow is scaled down: the card's `inset 0 0 12px 6px` on a 22px
   square fills the whole box with white and swallows the tick.

   And the rim is navy at low alpha, not the white the rest of the glass
   uses. A white rim works on a card because a card is large and sits over
   the wash; a 22px white outline on a near-white page is invisible, and
   what is left reads as a loose tick floating beside the heading. The ring
   is what makes it a mark rather than a stray glyph — take it away and the
   whole thing stops reading.

   A circle, not the rounded square it started as. That does cost a little:
   a 22px circle only has about 15px of usable width across the middle, so
   the tick inside has less room than the same box would give it, and it is
   the reason the glyph is 12px here rather than the 13 it was. */
.point__tick {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  /* White disc, accent tick, on request — the reverse of what this was for
     about ten minutes, which was a solid accent disc with the tick knocked
     out of it in near-black.

     The one thing to know about it: the tick is the supplied #02BBFD at full
     strength and that is 2.2:1 against a white disc. It is not readable as a
     shape the way a dark tick would be. It does not need to be — the tick
     repeats what the heading beside it already says, so nothing depends on
     resolving it — and the reason it still reads at all is chroma rather
     than lightness. An electric sky blue on white is unmistakably a colour
     even when it is barely a contrast. Anything that has to be READ takes
     --accent-ink instead; this is a decoration and takes the real colour.

     --line for the disc's edge, the site's general hairline: black-on-light
     and white-on-dark, so it flips with the theme. It was navy at .20, which
     on a dark page is a dark line on a dark ground.

     Dark mode needs no handling. --surface goes to #121A2B and the same blue
     on it is 7.9:1, so the tick that is faint in daylight is the brightest
     mark in the section at night. */
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--accent);
}

.point__tick svg {
  display: block;
  width: 12px;
  height: 12px;
}

/* The chevron, drawn the same way as the FAQ's: two borders on a square
   turned 45 degrees, so there is no icon file and nothing to load. The auto
   margin is what sends it to the right-hand edge. */
.point__q::after {
  content: "";
  flex: none;
  margin-inline-start: auto;
  margin-inline-end: 5px;
  width: 9px;
  height: 9px;
  border-right: 2px solid var(--blue-600);
  border-bottom: 2px solid var(--blue-600);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .24s var(--ease);
}

.point[open] > .point__q::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

/* Indented to sit under the heading rather than under the tick, so the
   panel reads as belonging to the words above it and the ticks stay in one
   unbroken column down the left. The figure is the tick plus the row gap. */
.point__a {
  padding-inline-start: calc(22px + var(--sp-3));
  padding-bottom: var(--sp-5);
  color: var(--ink-soft);
  max-width: 68ch;
}

.point__a > * + * { margin-top: var(--sp-3); }

/* -------------------------------------------------------------------------
   "What is in the box" — the list beside the photograph

   Mobile-first, so this is the stacked order and it is also the DOM order:
   heading, photograph, list. The picture sits between the sentence that sets
   it up and the list that details it, which is where it belongs on a phone;
   at 960 the same three elements become two columns without the DOM moving.
   ------------------------------------------------------------------------- */

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

.arrives__media {
  margin: 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-1);
  /* A hairline over the photograph's own edge. The image runs to a pale
     laboratory blur at the top right and without this it dissolves into the
     page rather than ending. */
  outline: 1px solid var(--line);
  outline-offset: -1px;
}

.arrives__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Hard to the left, and the vertical half of this is deliberately inert.

     The source is landscape (1280×853) and the desktop column is portrait,
     so `cover` scales to the column's height and crops horizontally ONLY —
     the Y value cannot do anything, and setting it to taste is how you spend
     an afternoon wondering why nothing moves. The vials sit in the left
     third of the frame and the rest is bokeh, so the crop starts near the
     left edge; centred, it cut the first vial in half. */
  object-position: 10% 50%;
}

/* -------------------------------------------------------------------------
   Steps — how ordering works
   ------------------------------------------------------------------------- */

.steps {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  counter-reset: none;
}

.step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  /* And the ROWS start too, which is a different instruction and the one
     that was missing. The cards are stretched to a common height by .steps,
     and a grid with auto rows spends leftover height ON THOSE ROWS by
     default — so the card with the shortest paragraph put 80px between its
     number and its heading, and the three headings started at 77px, 104px
     and 157px down their own cards. align-content collects the slack at the
     bottom of the card instead, where it is just the card being taller than
     its text. */
  align-content: start;
  gap: var(--sp-4);
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  box-shadow: var(--shadow-1);
}

/* A real element rather than a counter, because the number is content: it
   is what somebody says out loud when they ring up about step two. */
.step__n {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--grad-brand);
  color: var(--on-band);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.05rem;
  line-height: 1;
}

.step h3,
.step .card__title { font-size: var(--t-h3); }

.step p { margin-top: var(--sp-2); color: var(--ink-soft); }

/* -------------------------------------------------------------------------
   FAQ — a <details> accordion

   The native disclosure marker is removed on both engines and replaced with
   a chevron that turns, because the default triangle sits outside the
   padding and cannot be aligned with anything.
   ------------------------------------------------------------------------- */

.faq {
  border-top: 1px solid var(--line);
}

.faq__item {
  border-bottom: 1px solid var(--line);
}

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: 56px;
  padding: var(--sp-4) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-h3);
  color: var(--ink);
}

.faq__q::-webkit-details-marker { display: none; }

.faq__q:hover { color: var(--blue-700); }

/* The chevron: two borders on a rotated square, so there is no icon file and
   nothing to load. */
.faq__q::after {
  content: "";
  flex: none;
  width: 11px;
  height: 11px;
  margin-right: 6px;
  border-right: 2px solid var(--blue-600);
  border-bottom: 2px solid var(--blue-600);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .24s var(--ease);
}

.faq__item[open] > .faq__q::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.faq__a {
  padding-bottom: var(--sp-5);
  color: var(--ink-soft);
  max-width: 68ch;
}

.faq__a > * + * { margin-top: var(--sp-3); }

.faq__a ul,
.faq__a ol { padding-left: 1.25em; list-style: disc; }
.faq__a ol { list-style: decimal; }

/* -------------------------------------------------------------------------
   Forms
   ------------------------------------------------------------------------- */

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

/* -------------------------------------------------------------------------
   The home page enquiry — copy beside the form

   Stacked on a phone, two columns at 960. The form gets the wider half: it
   is the thing being used, and a text field narrower than the sentence
   asking for it looks like an afterthought.
   ------------------------------------------------------------------------- */

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

/* The intro caps its own measure rather than the wrap doing it. At the full
   page width a lede runs to about 110 characters, which is well past the
   point where the eye loses the start of the next line. */
.enquire__intro { max-width: 46ch; }

/* The WhatsApp route under the lede. A block of its own, which buys the space
   above it and nothing else: .enquire__intro is a plain block, so the
   inline-flex button already sizes to its own label here — the wrapper the
   form's submit needs is only needed because .form is a grid. */
.enquire__whatsapp { margin-top: var(--sp-4); }

/* Brand green on the glyph, and the label stays --ink.

   The same argument as the bar's icon, and the reason --whatsapp is a token
   per theme rather than one hex: the green that reads on the dark bar is
   unreadable on the white one, and brand.css carries both values with the
   contrast measurements behind them. `color` and not `fill`, because the mark
   is drawn `fill="currentColor"` — the same file the header uses, unchanged.

   The glyph only. A button filled in green would out-shout "Send enquiry"
   beside it, and that is the action this section exists for. */
.btn--whatsapp svg { color: var(--whatsapp); }

.field {
  display: grid;
  gap: var(--sp-2);
  min-width: 0;
}

.field__label {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: max(15px, var(--t-small));
  color: var(--ink);
}

.field__input {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid var(--chrome-300);
  border-radius: var(--r-sm);
  background: var(--paper);
  color: var(--ink);
  /* Never below 16px. Mobile Safari zooms the whole page in on a field
     smaller than that, and the reader is then left on a zoomed page with a
     horizontal scrollbar they did not ask for. --t-body is 1rem at its
     floor, so this is safe by construction. */
  font-size: var(--t-body);
  font-family: inherit;
  box-shadow: none;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}

.field__input:hover { border-color: var(--chrome-500); }

.field__input:focus {
  outline: none;
  border-color: var(--blue-400);
  box-shadow: var(--ring);
}

/* The outline still has to be there for the keyboard, on top of the ring. */
.field__input:focus-visible {
  outline: 2px solid var(--blue-400);
  outline-offset: 1px;
}

/* Lighter than a typed value, but still a legible 5:1. --chrome-500 is the
   obvious choice and the wrong one — it lands at 3.2:1, and a placeholder is
   text a person is expected to read, not a hairline. */
.field__input::placeholder {
  color: color-mix(in srgb, var(--ink-soft) 85%, var(--paper));
  opacity: 1;
}

textarea.field__input {
  min-height: 150px;
  resize: vertical;
  line-height: 1.55;
}

/* The native select keeps its own arrow. Replacing it means an appearance
   reset plus an image, and the platform's control is better understood on a
   phone than anything drawn here would be. */
select.field__input {
  padding-right: var(--sp-6);
  cursor: pointer;
}

.field__input[aria-invalid="true"] {
  border-color: var(--bad);
  background: color-mix(in srgb, var(--bad) 4%, var(--paper));
}

.field__input[disabled] {
  background: var(--paper-2);
  color: var(--ink-soft);
  cursor: not-allowed;
}

.field__hint {
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

.field__error {
  color: var(--bad);
  font-size: max(15px, var(--t-small));
  font-weight: 400;
}

/* The error slot lives in the markup from the start so that a live region
   has something to announce into — but an empty one must not open a gap. */
.field__error:empty { display: none; }

/* The acknowledgement tick. The whole label is the target, and the row is
   48px tall so the target is a target rather than a 16px box. */
.field--check {
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--sp-3);
  min-height: 48px;
  padding: var(--sp-3) 0;
}

.field--check input[type="checkbox"] {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  flex: none;
  accent-color: var(--blue-600);
  cursor: pointer;
}

.field--check .field__label {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--t-body);
  color: var(--ink);
  cursor: pointer;
  line-height: 1.5;
}

.form__note {
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

.form__note a { font-weight: 400; }

/* -------------------------------------------------------------------------
   Notes and the compliance panel
   ------------------------------------------------------------------------- */

.note {
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--line);
  border-left: 4px solid var(--chrome-300);
  border-radius: var(--r-sm);
  background: var(--paper-2);
  color: var(--ink-soft);
}

.note > * + * { margin-top: var(--sp-3); }

.note strong { color: var(--ink); }

.note--info {
  background: var(--blue-100);
  border-color: color-mix(in srgb, var(--blue-400) 30%, var(--blue-100));
  border-left-color: var(--blue-600);
  color: var(--ink);
}

.note--warn {
  background: color-mix(in srgb, var(--warn) 12%, var(--paper));
  border-color: color-mix(in srgb, var(--warn) 30%, var(--paper));
  border-left-color: var(--warn);
  /* Body text stays --ink. --warn itself is 3.9:1 on white, which is a fine
     colour for the rule down the side and a failing one for the sentence. */
  color: var(--ink);
}

/* The caption under the specimen certificate.

   It used to be a .note--warn, which is the beige alarm panel the shop and
   the checkout use when something has actually gone wrong. Saying "this
   drawing is a drawing" is not an alarm, and spending the alarm colour on it
   makes the real ones count for less. A rule, an icon and quiet text say the
   same thing without shouting. */
.coa__note {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: var(--t-small);
}

/* Optically on the first line rather than mathematically at its top: the
   glyph's own weight sits above its box's centre. */
.coa__note-icon {
  width: 20px;
  height: 20px;
  margin-top: 1px;
  flex: none;
  color: var(--ink-soft);
}

/* The fuller compliance block, used at the foot of the shop pages. Dark on
   purpose: it is the same statement as the thin strip at the top of the
   page, and the reader has scrolled a long way since then. */
.ruo-panel {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background-color: var(--band-ground);
  background-image: var(--grad-band);
  color: var(--chrome-200);
}

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

.ruo-panel h2,
.ruo-panel h3 { color: var(--on-band); }

.ruo-panel strong { color: var(--on-band); }

.ruo-panel ul {
  padding-left: 1.15em;
  list-style: disc;
}

.ruo-panel li + li { margin-top: var(--sp-2); }
.ruo-panel li::marker { color: var(--chrome-300); }

/* Buttons excluded, for the same reason they are on the navy band. */
.ruo-panel a:not(.btn) {
  color: var(--on-band);
  text-decoration: underline;
  /* Was --blue-400. On a navy ground the single dark blue is invisible, so
     the underline goes to chrome — still quieter than the white label it
     sits under, which is the job it was doing. */
  text-decoration-color: var(--chrome-300);
  text-decoration-thickness: 2px;
}

/* -------------------------------------------------------------------------
   Breadcrumbs and the in-page contents
   ------------------------------------------------------------------------- */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

/* Padded out to the 44px floor and pulled back by the same amount, so the
   target grows without the breadcrumb row growing with it — the same trick
   `.p-card__name a` uses. At 15px on a 1.6 line-height the bare anchor box is
   24px, which is a small target sitting directly above the page heading. */
.crumbs a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: 10px;
  margin-block: -10px;
  text-decoration: none;
}
.crumbs a:hover { text-decoration: underline; }

.crumbs li + li::before {
  content: "/";
  margin-right: var(--sp-2);
  color: var(--ink-soft);
}

.crumbs li { display: inline-flex; align-items: center; }

/* Two columns where there is room, one where there is not. Used by the legal
   page to park its contents list beside the text it points into. */
.cols {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
  align-items: start;
}

/* A `1fr` track has `min-width: auto`, which means it will not shrink below
   its content's min-content width — so a child with anything wide and
   unbreakable in it pushes the track past its own container and the whole
   page scrolls sideways. `min-width: 0` lets the track do what `1fr` implies.

   Found by re-measuring after the type changed to Be Vietnam Pro: it is a
   little wider than Inter, which pushed legal.html's contents list five
   pixels over at 320px. The bug was always there; the old face was just
   narrow enough to hide it. */
.cols > * { min-width: 0; }

.toc {
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper-2);
}

.toc h2 {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--ink-soft);
  margin-bottom: var(--sp-3);
}

.toc a {
  display: flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
  font-weight: 400;
}

.toc a:hover { text-decoration: underline; }

/* -------------------------------------------------------------------------
   The example certificate — quality.html

   The page draws a certificate rather than describing one, because "what a
   certificate of analysis contains" stays an abstraction until you can see
   the shape of the document. It is a specimen and it has to keep saying so:
   the panel is labelled Example, its batch number is not a batch number, and
   every value in it is a square-bracketed placeholder. A convincing one on a
   marketing page is a forgery somebody will eventually screenshot.

   There is no `min-width` query anywhere in this block, and that is the
   design rather than an omission. The head wraps, the metadata falls back on
   `.spec-list`, and the results table keeps its own width inside a scroll
   box — so one set of rules describes the phone and the desktop and there is
   no second number to keep in step with the first.
   ------------------------------------------------------------------------- */

.coa {
  padding: var(--sp-5);
  border: 1px solid var(--chrome-300);
  border-radius: var(--r-md);
  background: var(--paper);
  /* The heavier of the two shadows, so a white document reads as a sheet of
     paper lying on the page rather than as another white box on white. */
  box-shadow: var(--shadow-2);
  /* Every figure on a certificate is read down a column and compared with
     the one above it, which only works if the digits line up. */
  font-variant-numeric: tabular-nums;
}

.coa > * + * { margin-top: var(--sp-5); }

.coa__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-4);
  border-bottom: 2px solid var(--ink);
}

/* The issuing company's name on the specimen certificate.

   This was `.eyebrow tracked` and it was the only "eyebrow" on the site that
   was not one: an eyebrow is a kicker introducing a section heading, and this
   is the company name printed at the top of a document the page is depicting.
   When the eyebrows came off the site it kept the look and lost the class,
   because a facsimile certificate with no issuer on it is a worse facsimile.

   Same declarations the old `.eyebrow tracked` pair produced, spelled out —
   there is one of these and it should not depend on a utility that no longer
   appears anywhere near it. */
.coa__issuer {
  display: block;
  margin-bottom: var(--sp-2);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--blue-600);
}

/* The scroll box, and the one thing this component must never do is widen
   the page. Four columns of specification text cannot fit across 320px and
   no amount of wrapping makes them fit — a cell squeezed to one character
   per line is not a readable table, it is a stack of letters. So the table
   keeps a usable width and the reader pushes it sideways inside its own box.

   `overflow-x` belongs on this wrapper and not on the table: a table that is
   its own scroll container loses its collapsed borders in WebKit.

   The markup puts `tabindex="0"` and a labelled `role="region"` on this
   element, and both are load-bearing. A scroll container that cannot take
   focus cannot be scrolled from the keyboard at all, and the focus ring in
   brand.css is what tells the reader where they have landed. */
.coa__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.coa__table {
  /* Not the reset's `width: 100%`. At 320px that instruction is obeyed to
     the letter and produces four unreadable slivers instead of a table that
     scrolls. `auto` plus a 100% floor means: be as wide as you need to be,
     and fill the box once the box is wide enough. */
  width: auto;
  min-width: 100%;
  font-size: max(15px, var(--t-small));
}

.coa__table caption {
  padding-bottom: var(--sp-3);
  text-align: left;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--ink-soft);
}

.coa__table th,
.coa__table td {
  padding: var(--sp-3) var(--sp-4) var(--sp-3) 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  /* The floor that guarantees the scroll. Four columns at eleven characters
     is wider than any phone, so the box always has something to scroll and
     no column can be crushed to a single word per line. */
  min-width: 11ch;
}

.coa__table thead th {
  border-bottom: 1px solid var(--chrome-300);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .14em;
  white-space: nowrap;
}

.coa__table tbody th {
  color: var(--ink);
  font-weight: 400;
}

/* The band that separates the rows on every certificate from the ones a
   laboratory reports where it determined them. It is a real th with
   scope="colgroup", not a styled td, so the grouping is announced rather
   than only drawn — a screen reader reaching "Single impurity" is told what
   the group it sits in is called. min-width is cleared because the 11ch
   floor that guarantees the sideways scroll is a rule for data columns, and
   this cell spans all four of them. */
.coa__table .coa__group th {
  padding-top: var(--sp-5);
  border-bottom: 1px solid var(--chrome-300);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .14em;
  min-width: 0;
}

.coa__sign {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

/* The line somebody would sign on: a border with air above it, rather than a
   row of underscores. A screen reader reads underscores out, one at a time,
   and what it is reading out is a decoration. */
.coa__rule {
  margin-top: var(--sp-7);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--ink);
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

/* -------------------------------------------------------------------------
   The research-use acknowledgement

   Injected by gate.js on the shop pages only, and overlaid on a page that is
   already rendered — with JavaScript off there is no gate and the catalogue
   is still there, which is the point: this is an acknowledgement, not a lock,
   and hiding the products behind a script would hide them from search
   engines too.

   There is no separate "open" class, and that is not an oversight: gate.js
   builds the element when it opens and removes it from the document when it
   closes, so `.gate` being in the DOM *is* the open state. An earlier draft
   here hid `.gate` by default and waited for a modifier that gate.js never
   sets, which produced a gate that was fully present, sealed the page behind
   `inert`, and could not be seen. The entrance is an animation rather than a
   transition for the same reason — there is no second state to transition to.
   ------------------------------------------------------------------------- */

.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  align-items: end;
  justify-items: stretch;
  padding: 0;
  background: rgba(7, 22, 52, .62);
}

@supports (backdrop-filter: blur(2px)) {
  .gate { backdrop-filter: blur(2px); }
}

/* A sheet up from the bottom of the phone, a dialog in the middle of a
   desktop. Both are the same panel with different edges. */
.gate__panel {
  width: 100%;
  max-height: 92vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-6) var(--sp-5) max(var(--sp-6), env(safe-area-inset-bottom));
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: var(--paper);
  box-shadow: var(--shadow-2);
}

/* The panel slides; the overlay does not fade. That asymmetry is on purpose.

   An animation only advances while the page is actually rendering, and an
   entrance that starts at `opacity: 0` is one stalled frame away from an
   invisible dialog that has already sealed the rest of the page behind
   `inert` — a page that cannot be read and cannot be dismissed. A stalled
   transform is 16 pixels of wrongness nobody notices. So nothing here ever
   animates the gate's visibility: worst case it appears without ceremony,
   which is the correct failure.

   Reduced motion is handled in brand.css, which cuts the duration to
   nothing; `both` then holds the finished position rather than letting the
   panel snap back. */
@media (prefers-reduced-motion: no-preference) {
  .gate__panel { animation: gate-panel-in .28s var(--ease) both; }
}

@keyframes gate-panel-in {
  from { transform: translateY(16px); }
  to { transform: none; }
}

.gate__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-h2);
  color: var(--ink);
}

.gate__text {
  margin-top: var(--sp-4);
  color: var(--ink-soft);
}

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

.gate__text strong { color: var(--ink); }

.gate__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

.gate__actions .btn { flex: 1 1 180px; }

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

   Element selectors as well as classes, deliberately. The four columns are
   described in the spec but their inner class names are not, so a column
   built out of a heading and a plain list has to come out right whether or
   not anybody remembered `.site-foot__title`.
   ------------------------------------------------------------------------- */

/* The same ground as the header band, and for the same reason: the page now
   opens and closes on one colour instead of running from a grey band into a
   navy one. It gets the same hard edge against the wash that the header has,
   top and bottom, which is what makes them read as a pair.

   This was navy with white type. Everything below it inverted with it: the
   headings, the body copy, the links, the hairline above the bottom row, and
   the logo, which was a white knockout of a dark file and is now simply the
   dark file. Change the ground back and all six move together. */
.site-foot {
  background: rgb(var(--hero-ground));
  color: var(--ink-soft);
  padding-block: var(--sp-8) var(--sp-6);
}

.site-foot__cols {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
}

.site-foot__col { min-width: 0; }

.site-foot img { height: 40px; width: auto; }

/* The header's logo file, as it is.

   There is no knockout here any more. It used to be
   `filter: brightness(0) invert(1)`, which took every opaque pixel to black
   and then flipped it to white for the navy ground, giving the mark's exact
   silhouette rather than a washed-out navy PNG. On a light ground the file is
   already the right colour and the filter would erase it.

   36px rather than the 40 its siblings get. This is the full wordmark and the
   old footer image was the X on its own, so at the same height it would read
   noticeably heavier than everything around it.

   `img.site-foot__logo`, not `.site-foot__logo`. The rule above it is
   `.site-foot img` — one class plus one element, which outranks one class on
   its own, so the bare version set a height that never applied. */
img.site-foot__logo {
  height: 36px;
}

/* `.site-foot .site-foot__title`, not the bare class, and it matters for
   exactly one element: the first column's title is a <p>, so `.site-foot p`
   below it is a class plus an element (0,1,1) and beat a lone class (0,1,0).
   That title has been taking body colour since the footer was navy, where it
   was --chrome-200 instead of --paper and nobody noticed because both are
   pale on a dark ground. On a light ground the same bug is a heading that
   does not look like one. Fourth time this pattern has bitten in this file;
   see also img.site-foot__logo directly above. */
.site-foot h2,
.site-foot h3,
.site-foot .site-foot__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--ink);
  margin-bottom: var(--sp-3);
}

.site-foot p {
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

.site-foot ul { display: grid; }

.site-foot li a,
.site-foot__col a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink-soft);
  text-decoration: none;
}

.site-foot a:hover {
  color: var(--blue-800);
  text-decoration: underline;
}

.site-foot__bottom {
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
  display: grid;
  gap: var(--sp-3);
  color: var(--ink-soft);
  font-size: max(15px, var(--t-small));
}

/* -------------------------------------------------------------------------
   On the navy band

   Everything inside `.section--navy` is white or --chrome-200. --chrome-500
   is 2.4:1 here and 3.2:1 on white; it draws hairlines and icon strokes and
   it never carries a sentence, on either ground.
   ------------------------------------------------------------------------- */

.section--navy h1,
.section--navy h2,
.section--navy h3,
.section--navy h4,
.section--navy .section__title,
.section--navy .card__title,
.section--navy .trust__title,
.section--navy strong { color: var(--on-band); }

.section--navy p,
.section--navy li,
.section--navy .section__lede,
.section--navy .card__text,
.section--navy .trust__text,
.section--navy .spec__val,
.section--navy .form__note { color: var(--chrome-200); }

/* Chrome, not blue. The palette used to keep an electric --blue-400 partly
   because it was the one blue that cleared AA on this ground at 4.8:1; with a
   single blue there is no such blue left — the accent IS --navy-800 now, so
   on this ground it is literally 1:1. --chrome-200 is 12.6:1 here and is what the rest of the
   band's secondary text already uses. */
.section--navy .eyebrow { color: var(--chrome-200); }

.section--navy .spec-list,
.section--navy .spec { border-color: rgba(226, 233, 242, .20); }

.section--navy .spec__key { color: var(--chrome-200); }

/* `:not(.btn)` is load-bearing. Without it this rule outranks `.btn--onnavy`
   — one class plus a type beats one class — and paints the white button's
   label white on white. Excluding buttons from the link treatment fixes
   every variant at once instead of racing each of them on specificity. */
.section--navy a:not(.btn) {
  color: var(--on-band);
  text-decoration: underline;
  /* Was --blue-400. On a navy ground the single dark blue is invisible, so
     the underline goes to chrome — still quieter than the white label it
     sits under, which is the job it was doing. */
  text-decoration-color: var(--chrome-300);
  text-decoration-thickness: 2px;
}

/* The one place the global focus ring cannot work. brand.css paints it in
   --blue-700, which is right everywhere on white and 1.5:1 here — a keyboard
   user would cross the navy band and the compliance panel with no visible
   focus at all. White is 12.6:1 on this ground. */
.section--navy :focus-visible,
.ruo-panel :focus-visible {
  outline-color: var(--on-band);
}

/* On navy the pane is a lightening rather than a white tint, and its rim is
   white at low alpha — --glass-rim-hi at 95% would draw a bright wire around
   every card on the dark band. */
.section--navy .card {
  background: var(--glass-bg-dark);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16);
}

.section--navy .card::before {
  background: linear-gradient(135deg,
    var(--glass-rim-dark) 0%,
    rgba(255, 255, 255, .12) 44%,
    rgba(255, 255, 255, .08) 62%,
    var(--glass-rim-dark) 100%);
}

.section--navy .trust__icon {
  background: rgba(255, 255, 255, .10);
  color: var(--on-band);
}

.section--navy .pill {
  background: rgba(255, 255, 255, .12);
  border-color: rgba(226, 233, 242, .24);
  color: var(--on-band);
}

/* -------------------------------------------------------------------------
   Wider screens

   Mobile-first: everything above is the phone. These add up from 560px.
   ------------------------------------------------------------------------- */

/* ---- 560px: large phone ---- */
@media (min-width: 560px) {
  /* The flag proper. The point is cut with clip-path and the bottom padding
     leaves room for it, so no word ever lands inside the taper. Wrapped in
     @supports so that a browser without clip-path keeps a clean navy plaque
     with a rounded top rather than a rectangle with a wasted inch under the
     words — a deliberate shape either way. */
  .badge-chevron {
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 112px;
    padding: var(--sp-4) var(--sp-5) var(--sp-4);
    border-radius: var(--r-sm) var(--r-sm) 0 0;
    font-size: .82rem;
    line-height: 1.25;
  }

  @supports (clip-path: polygon(0 0, 100% 0, 50% 100%)) {
    .badge-chevron {
      padding-bottom: calc(var(--sp-4) + 20px);
      clip-path: polygon(0 0, 100% 0, 100% calc(100% - 20px), 50% 100%,
                         0 calc(100% - 20px));
    }
  }

  .spec {
    grid-template-columns: minmax(140px, 34%) minmax(0, 1fr);
    gap: var(--sp-4);
    align-items: baseline;
  }

  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .site-foot__bottom {
    grid-template-columns: 1fr;
    justify-items: start;
  }
}

/* ---- 760px: tablet ---- */
@media (min-width: 760px) {
  .trust-row { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

  /* --split opts out of the auto-fit above and stays stacked to 960.

     Measured at 760, three-across: a third of the column is 258px, the ring
     takes 60 and the gap 16, and "UK BASED." breaks over two lines with
     "Proudly based in the UK." over three. Left to auto-fit it is worse —
     two items on one row and the third alone underneath. A strip whose items
     are taller than they are wide has stopped being a strip, so it waits. */
  .trust-row--split { grid-template-columns: 1fr; }

  .cards { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

  .gate {
    align-items: center;
    justify-items: center;
    padding: var(--gutter);
  }

  .gate__panel {
    max-width: 560px;
    max-height: min(80vh, 640px);
    padding: var(--sp-7);
    border-radius: var(--r-lg);
  }

  .site-foot__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Two even columns, not `1fr auto`.

     `auto` sizes to max-content, and the second cell is a 140-character
     research-use sentence — so it took a single 585px line and left the
     company details 150px to wrap over five. Both halves are long; neither
     gets to be the one that fits on one line. */
  .site-foot__bottom {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: var(--sp-5);
  }
}

/* ---- 960px: small desktop. The nav uncollapses here. ---- */
@media (min-width: 960px) {
  /* Three across, and not before 960. At 760 this produced three 230px
     columns, which is narrower than the paragraphs in them want to be —
     "specification" alone is most of a line, and the text came out as three
     ribbons. Below this the step stays one full-width row with its number
     beside the text, which is the same information and reads. */
  .steps { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  .step {
    grid-template-columns: 1fr;
    gap: var(--sp-3);
  }

  .site-head__toggle { display: none; }

  /* Two columns, and every child's row named explicitly.

     Auto-placement only walks forward, so with the media second in the DOM
     and the list third, leaving the rows to the browser puts the list on row
     2 of column 2 — underneath the photograph — rather than back in column
     1. The same trap as the header nav. Naming all three takes the cursor
     out of it. */
  .arrives {
    grid-template-columns: minmax(0, 1fr) minmax(0, 38%);
    column-gap: var(--sp-7);
    row-gap: var(--sp-5);
    align-items: start;
  }

  .enquire {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    column-gap: var(--sp-8);
    align-items: start;
  }

  /* Copy left, photograph right — the same split as .arrives. Two children
     only, so auto-placement cannot get it wrong; name the rows explicitly if
     a third is ever added. */
  .purity {
    grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
    column-gap: var(--sp-8);
    align-items: start;
  }

  /* The photograph takes its height from the copy column, and since the
     three points became an accordion that column is no longer fixed: at
     1280 it is 393px closed, 472-500 with any one panel open, and 658 with
     all three. The photo tracks all of it, which is fine at this range.

     It was not always. An earlier, much longer draft of the panel copy took
     the all-open state to 988px and the photo more than doubled in height
     while someone read, so a `max-height` was added to stop it. The copy is
     a third of that length now and the cap only ever clipped 18px, so it
     has gone. If the panels are ever lengthened again, measure this before
     assuming it still holds. */
  .purity__media { align-self: stretch; }

  .arrives__head { grid-column: 1; grid-row: 1; }
  .arrives__list { grid-column: 1; grid-row: 2; }

  /* Spanning both rows and stretched, so the photograph is exactly as tall
     as the heading and the list together — which is what makes the two
     columns read as one block rather than as a picture parked beside some
     text. */
  .arrives__media {
    grid-column: 2;
    grid-row: 1 / 3;
    align-self: stretch;
  }

  /* An explicit three, not the auto-fit the plain .trust-row uses. auto-fit
     is right for the compliance band, which is the same component with a
     different number of items in it; here the count is fixed at three and
     the dividers have to land on the column edges, which they only do if the
     columns are the ones I said. */
  .site-head__wrap { padding-block: var(--sp-3); }

  /* Full size, where the box is wide and short and the crop falls on the
     frame's empty top and bottom rather than on its sides. See --stage-zoom. */
  .hero-stage { --stage-zoom: 1.6; }

  /* And a deeper blur on the bar, where there is a GPU to spend it on. */
  .site-head { --head-blur: 18px; }

  /* The button goes back into the hero card, under the lede, where there is
     a right-hand side for the picture to live on. Static positioning also
     stops fit() reserving room for it under the picture — it tests
     `position === "absolute"`. */
  .hero--on-stage .hero__inner { position: relative; }

  .hero--on-stage .hero__actions {
    position: static;
    justify-content: flex-start;

    /* The gutter goes back off. It is there on a phone because the button is
       positioned against .hero and inherits no side spacing; back in the card
       it is inside a .wrap that already has one, and leaving it on set the
       button 30px in from the words above it. */
    padding-inline: 0;
  }

  /* The ticker is a phone treatment. Up here the wrap is a wrap again, the
     list is a grid again, and the duplicates nav.js added are hidden — so
     what is left is the same three-across strip as before any of this. */
  .hero-stage ~ #trust > .wrap {
    max-width: calc(var(--wrap) + var(--gutter) * 2);
    padding-inline: var(--gutter);
    overflow: visible;
  }

  .trust-row--split {
    display: grid;
    width: auto;
    animation: none;
  }

  .trust-row--split > .trust[aria-hidden="true"] { display: none; }

  .trust-row--split > .trust {
    display: grid;
    white-space: normal;
  }

  .trust-row--split .trust__body { display: block; }

  .trust-row--split {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0;
  }

  /* The strip proper, reinstated over the bullet list the base sets: the
     label goes under the disc rather than beside it, and the disc goes back
     to the size that carries an item on its own. Below this width it is a
     list; see the note at .trust-row--split .trust. */
  .trust-row--split .trust {
    grid-template-columns: 1fr;
    justify-items: center;
    align-items: start;
    text-align: center;
    gap: var(--sp-4);
  }

  .trust-row--split .trust__icon { width: 72px; height: 72px; }
  .trust-row--split .trust__icon svg { width: 32px; height: 32px; }

  /* The rule belongs to the item on its right, so the strip never opens or
     closes with one. Padding on both sides of the join keeps the hairline
     off the text. */
  /* Symmetric padding on ALL three, not just a trailing gutter on each.

     This was `padding-inline-end` on every item and `padding-inline-start`
     only on the ones with a divider — fine while the contents were
     left-aligned, because the text started at the same place either way.
     Centred, it put the first item's content box 32px narrower on one side
     than the other, so its icon sat visibly left of the two beside it. Equal
     padding makes all three columns identical and the centring true. */
  .trust-row--split > .trust { padding-inline: var(--sp-6); }

  .trust-row--split > .trust + .trust {
    border-inline-start: 1px solid var(--line);
  }

  /* Logo left, links and basket right.

     `1fr auto auto`: the logo's column takes every spare pixel, so the nav
     and the basket are pushed to the far end and sit together.

     It was `1fr auto 1fr`, which put the links in the TRUE centre of the bar
     rather than merely between the logo and the basket. That distinction is
     why this is a grid and not a flex row: `justify-content: center` on flex
     centres the nav in the space left over, which is only the middle of the
     bar when the logo and the basket happen to be the same width. The
     distinction stops mattering the moment the nav is not centred, but the
     grid stays — it is also what puts the three items in visual order
     without touching the DOM.

     Note `justify-self` cannot do this on its own. Column 2 was sized `auto`,
     which is exactly the nav's width, so there was no room inside the track
     to move the nav within. The template had to change.

     The DOM order is logo, basket, toggle, nav — chosen so the basket is
     reachable early by keyboard and so it survives the nav collapsing on a
     phone. grid-column puts them back in visual order here without touching
     the markup, and the toggle is display:none at this width so it claims no
     track. */
  .site-head__wrap {
    display: grid;
    grid-template-columns: 1fr auto auto auto auto auto;
    align-items: center;
  }

  /* grid-row: 1 on all three, not just grid-column.

     Without it the nav lands on a second row and the header doubles in
     height. Auto-placement walks forward only: the logo takes column 1 and
     the basket column 3, so by the time the cursor reaches the nav — which is
     last in the DOM — column 2 of row 1 is behind it, and a sparse grid will
     not reverse to fill a hole. Naming the row for each item removes the
     cursor from the question entirely. */
  .site-head__logo { grid-column: 1; grid-row: 1; justify-self: start; }
  /* The theme toggle needs a named track like everything else in this grid.
     Added without one it auto-placed past the three columns that existed and
     landed on row two, doubling the header's height — which is the exact
     failure the note above describes. Four items, four columns. */
  /* Six items, six columns, and the numbers below shifted by one when
     WhatsApp was added. The note above about auto-placement is not a
     historical remark: leave this one unnamed and it lands on row two and
     doubles the header's height, exactly as the theme toggle once did. */
  .site-head__whatsapp { grid-column: 3; grid-row: 1; justify-self: end; }
  .site-head__theme { grid-column: 4; grid-row: 1; justify-self: end; }
  /* Account before basket, which is the order every shop puts them in. */
  .site-head__account { grid-column: 5; grid-row: 1; justify-self: end; margin-left: 0; }
  .site-head__basket { grid-column: 6; grid-row: 1; justify-self: end; margin-left: 0; }

  /* `end`, not `center`. With the template above the nav's track is its own
     width, so this is belt and braces rather than the thing doing the work,
     but it states the intent and it is what would take effect if the track
     ever grew. The gap between the nav and the basket is the wrap's own. */
  .site-nav,
  .site-head--open .site-nav { grid-column: 2; grid-row: 1; justify-self: end; }

  /* Both selectors, because `.site-head--open .site-nav` outranks a bare
     `.site-nav` and the panel state would otherwise survive into the desktop
     layout as an 80vh scrolling box. nav.js also clears the class on resize,
     but the CSS must be right on its own — a page loaded wide has never had
     a resize event. */
  .site-nav,
  .site-head--open .site-nav {
    flex: 0 0 auto;
    flex-basis: auto;
    max-height: none;
    overflow: visible;
    opacity: 1;
    visibility: visible;
    transition: none;
    display: flex;
    align-items: center;
    gap: var(--sp-5);
    margin-left: 0;
  }

  .site-nav__list {
    flex-direction: row;
    align-items: center;
    gap: var(--sp-5);
    padding-top: 0;
  }

  .site-nav__link {
    min-height: 44px;
    padding: var(--sp-2) 0;
    border-bottom: 0;

    /* The panel's treatment comes off. Up here the nav is a row on the bar,
       always visible and never opened, so there is nothing to stagger and
       nothing to reveal — and a link left at opacity 0 would simply be gone,
       because .site-head--open never appears at this width. */
    justify-content: flex-start;
    text-align: left;
    opacity: 1;
    transform: none;
    transition: none;
    /* Smaller than the 15px it was, because caps read larger than sentence
       case at the same size: the cap height IS the type size, where lowercase
       spends most of its height below it. 13px in caps takes about the same
       room on the bar as 15px did in sentence case, and 13 is the floor for
       a tracked label of this weight. */
    font-size: 13px;
    /* Not the .01em this used to carry. The base rule's .08em is the whole
       reason the caps stay legible, and a value here would quietly cancel
       it. */
  }

  /* The stagger's delays, off. `transition: none` above already stops
     anything running, so this changes nothing today — but the delays are set
     by a three-class selector and would come straight back the moment someone
     gave these links a transition again. Same specificity, later in the file,
     which is what wins the tie. */
  .site-head--open .site-nav__list li .site-nav__link { transition-delay: 0s; }

  .site-nav__link--current,
  .site-nav__link[aria-current="page"] {
    padding-right: 0;
    box-shadow: inset 0 -2px 0 var(--blue-600);
  }

  .site-nav__cta {
    display: inline-flex;
    width: auto;
    margin: 0;
  }

  .cols { grid-template-columns: minmax(0, 1fr) 260px; }

  /* Contents on the left, text on the right, without reordering the source:
     the list is second in the markup so that a phone gets the words first. */
  .cols > .toc { grid-column: 2; grid-row: 1; }

  .toc {
    position: sticky;
    top: 96px;
    max-height: calc(100vh - 128px);
    overflow-y: auto;
  }

  .hero__actions .btn { flex: 0 0 auto; }

  /* The veil lightens a little where there is room, so more of the
     photograph comes through beside the type rather than behind it. Still a
     scrim — this hero is a dark panel at every width. */
  .hero__veil {
    background:
      linear-gradient(100deg,
        rgba(var(--hero-scrim), .96) 0%,
        rgba(var(--hero-scrim), .90) 40%,
        rgba(var(--hero-scrim), .56) 68%,
        rgba(var(--hero-scrim), .26) 100%),
      linear-gradient(180deg, rgba(var(--hero-scrim), .26) 0%, rgba(var(--hero-scrim), 0) 42%);
  }
}

/* ---- 1200px: wide ---- */
@media (min-width: 1200px) {
  .site-foot__cols { grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--sp-7); }

  .trust-row { gap: var(--sp-6); }
}

/* -------------------------------------------------------------------------
   Print
   ------------------------------------------------------------------------- */

@media print {
  .site-head,
  .site-nav,
  .site-head__toggle,
  /* A full-bleed photograph is half a cartridge of ink and tells a reader
     holding the page nothing they cannot see on screen. */
  .hero__media,
  .hero__veil,
  .gate { display: none !important; }

  .ruo-strip {
    background: #fff !important;
    color: #000 !important;
    border-bottom: 1pt solid #000;
    padding: 0 0 6pt;
    text-align: left;
    font-weight: 400;
  }

  .section,
  .section--tint,
  .section--navy,
  .hero,
  .ruo-panel,
  .site-foot {
    background: #fff !important;
    background-image: none !important;
    color: #000 !important;
    padding-block: 12pt;
  }

  .section--navy *,
  .ruo-panel *,
  .site-foot * { color: #000 !important; }

  .ruo-panel {
    border: 1pt solid #000;
    border-radius: 0;
  }

  .card,
  .step,
  .note,
  .toc {
    border: 1pt solid #999;
    box-shadow: none !important;
    background: #fff !important;
  }

  .coa {
    border: 1pt solid #000;
    border-radius: 0;
    box-shadow: none !important;
    background: #fff !important;
  }

  /* On paper there is no sideways. The table takes the page width and its
     cells wrap, which is a worse read than the scrolling version on screen
     and a far better one than a column silently cut off at the margin. */
  .coa__scroll { overflow: visible !important; }

  .coa__table {
    width: 100% !important;
    min-width: 0 !important;
  }

  .coa__table th,
  .coa__table td { min-width: 0 !important; }

  .btn { display: none !important; }

  .toc { position: static !important; }

  /* A closed accordion prints as a list of questions with no answers, which
     is the one thing the FAQ page must not do on paper. */
  .faq__item > .faq__a {
    display: block !important;
    content-visibility: visible !important;
  }

  .faq__q::after { display: none; }

  .card, .step, .faq__item, .spec, .trust { break-inside: avoid; }
}
