/* =============================================================================
   styles.css — Elegance Peptides
   Light-first. Apple's palette, metrics and rhythm: white page, #f5f5f7 bands,
   #1d1d1f ink, #6e6e73 secondary, #0066cc links, #0071e3 actions, hairlines at
   rgba(0,0,0,0.08), pill buttons at 980px radius, whitespace doing the
   separating. Dark mode follows prefers-color-scheme (#000 page, #1d1d1f cards).
   Honours prefers-reduced-motion, prefers-reduced-transparency, prefers-contrast.
   ========================================================================== */

/* ---------- 1. Tokens ---------------------------------------------------- */
:root {
  color-scheme: light dark;

  --page: #ffffff;
  --band: #f5f5f7;
  --band-2: #fafafa;
  --card: #ffffff;

  --ink: #1d1d1f;
  --ink-2: #6e6e73;
  --ink-3: #86868b;

  --link: #0066cc;
  --blue: #0071e3;
  /* Sampled from the product label. A restrained accent: hairline rules only,
     never body text, never a button. */
  --gold: #b08d4a;
  /* The label gold is 2.89:1 on white, which is under the 3:1 WCAG floor even
     for large text, so anything set in gold uses this slightly deeper tone
     (3.48:1). --gold stays as it is for rules and marks, which carry no
     contrast requirement. In dark mode the label gold already reads at 8.9:1
     on black and needs no adjustment. */
  --gold-ink: #a8843f;
  --blue-hover: #0077ed;
  --on-blue: #ffffff;

  --hairline: rgba(0, 0, 0, 0.08);
  --hairline-strong: rgba(0, 0, 0, 0.18);
  --field: #ffffff;
  --search-fill: #f5f5f7;
  --focus-ring: rgba(0, 113, 227, 0.35);

  --nav-bg: rgba(251, 251, 253, 0.8);
  --scrim: rgba(0, 0, 0, 0.48);

  /* The raised thumb on a segmented control. White above grey on light; on
     dark it has to be LIGHTER than its track, which --card is not. */
  --seg-thumb: #ffffff;
  --seg-thumb-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.04);

  --tile-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
  --tile-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.10);
  --sheet-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);

  --radius: 18px;
  --radius-sm: 12px;
  --pill: 980px;

  --gutter: 22px;
  --measure: 980px;
  --measure-wide: 1200px;
  --measure-read: 680px;

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui,
    "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #000000;
    --band: #161617;
    --band-2: #0b0b0c;
    --card: #1d1d1f;

    --ink: #f5f5f7;
    --ink-2: #a1a1a6;
    --ink-3: #86868b;

    --link: #2997ff;
    --blue: #0071e3;
    --gold: #c9a45f;
    --gold-ink: #c9a45f;      /* 8.96:1 on black; no adjustment needed */
    --blue-hover: #0077ed;

    --hairline: rgba(255, 255, 255, 0.12);
    --hairline-strong: rgba(255, 255, 255, 0.28);
    --field: #1d1d1f;
    --search-fill: rgba(255, 255, 255, 0.08);
    --focus-ring: rgba(41, 151, 255, 0.4);

    --nav-bg: rgba(22, 22, 23, 0.8);
    --scrim: rgba(0, 0, 0, 0.6);

    --seg-thumb: rgba(255, 255, 255, 0.24);
    --seg-thumb-shadow: none;

    --tile-shadow: none;
    --tile-shadow-hover: 0 8px 28px rgba(0, 0, 0, 0.55);
    --sheet-shadow: 0 12px 48px rgba(0, 0, 0, 0.7);
  }
}

@media (prefers-reduced-transparency: reduce) {
  :root { --nav-bg: var(--page); }
}
@media (prefers-contrast: more) {
  :root {
    --nav-bg: var(--page);
    --hairline: rgba(0, 0, 0, 0.45);
    --hairline-strong: rgba(0, 0, 0, 0.75);
    --ink-2: #4b4b50;
    --ink-3: #4b4b50;
  }
}
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
  :root {
    --hairline: rgba(255, 255, 255, 0.5);
    --hairline-strong: rgba(255, 255, 255, 0.85);
    --ink-2: #d8d8dd;
    --ink-3: #d8d8dd;
  }
}

/* ---------- 2. Base ------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anything the browser scrolls to by itself stops below the sticky nav
     instead of under it. app.js does its own arithmetic for the jumps it
     handles; this catches the ones it does not. */
  scroll-padding-top: 62px;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.47059;
  letter-spacing: -0.022em;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; }
[hidden] { display: none !important; }

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 3px;
  border-radius: 4px;
}
@media (prefers-contrast: more) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible { outline-width: 3px; }
}

.skip-link {
  position: absolute;
  left: 16px; top: -60px;
  z-index: 999;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--ink);
  font-size: 14px;
  box-shadow: var(--sheet-shadow);
  transition: top 140ms ease-out;
}
.skip-link:focus { top: 12px; }

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

/* ---------- 3. Type ------------------------------------------------------ */
.headline-1 {
  margin: 0;
  font-size: clamp(40px, 6.2vw, 72px);
  line-height: 1.05;
  letter-spacing: -0.015em;
  font-weight: 600;
  color: var(--ink);
}
.headline-2 {
  margin: 0;
  font-size: clamp(32px, 3.8vw, 48px);
  line-height: 1.08;
  letter-spacing: -0.003em;
  font-weight: 600;
  color: var(--ink);
}
.headline-3 {
  margin: 0;
  font-size: 24px;
  line-height: 1.16;
  letter-spacing: 0.009em;
  font-weight: 600;
  color: var(--ink);
}
.subhead {
  margin: 0;
  font-size: 21px;
  line-height: 1.381;
  letter-spacing: 0.011em;
  font-weight: 400;
  color: var(--ink-2);
}
.subhead-lg {
  margin: 0;
  font-size: clamp(21px, 2.4vw, 28px);
  line-height: 1.21;
  letter-spacing: 0.007em;
  font-weight: 400;
  color: var(--ink-2);
}
.body-2 { font-size: 17px; line-height: 1.47059; letter-spacing: -0.022em; color: var(--ink-2); margin: 0; }
/* One step up from body-2, for pages that are read rather than scanned. */
.body-1 {
  font-size: 17px;
  line-height: 1.47059;
  letter-spacing: -0.022em;
  color: var(--ink-2);
}
.fineprint {
  margin: 0;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink-2);
}
.mono { font-family: var(--font-mono); font-size: 0.94em; letter-spacing: 0; }
.centered { text-align: center; }
.flow-sm > * + * { margin-top: 6px; }
.flow-lg > * + * { margin-top: 28px; }

/* ---------- 4. Layout ---------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container-wide { max-width: var(--measure-wide); }
.container-read { max-width: var(--measure-read); }

.band { padding-block: clamp(72px, 9vw, 130px); background: var(--page); }
.band-soft { background: var(--band); }
.band-tight { padding-block: clamp(40px, 5vw, 64px); }
.band + .band-soft, .band-soft + .band { border: 0; }

/* ---------- 5. Advisory banner (required on every page) ------------------ */

/* Hold the chrome's space open from the very first paint.

   The advisory bar and the nav are built by app.js, so the document as parsed
   has nothing above <main>. app.js is a quarter of a megabyte arriving after a
   render-blocking stylesheet, and the browser will happily paint what it has
   while it waits -- a frame with <main> hard against the top of the window,
   then a second one with <main> 86px lower. That drop was the site's single
   largest layout shift: 0.067 of CLS at 1280, on every page, before anything
   on the page had been interacted with.

   These two numbers are the measured height of the chrome: 37px of advisory
   plus 49px of nav above 403px wide, and 49 + 49 below it, where the advisory
   line wraps in two. Both engines agree on the wrap to the pixel (the
   measurement is in scripts/chromereserve.js, which asserts it). mountChrome()
   sets data-chrome="mounted" in the same task as the insert, so no frame is
   ever composited between the reserve being released and the real bar filling
   it, and <main> sits at exactly the same offset on both sides of the swap.

   A browser with different metrics can still wrap the advisory somewhere else
   and be a line out. That costs 12px of shift instead of 86.

   Padding on the body, not a margin on <main>, and that is not a taste call.
   The margin version put <main> in exactly the right place -- the reserve
   harness measured it identical to the pixel on all 98 combinations -- and
   still logged the full 0.067, because with no border or padding between them
   a child's top margin collapses out through its parent: <body>'s own box
   moved down 86px and back, and the layout-shift API scores the body box,
   which is the whole page. Nothing visibly moved and the metric was unchanged.
   Padding does not collapse, so the body box stays where it is. */
html:not([data-chrome="mounted"]) body { padding-top: 86px; }
@media (max-width: 25.1875rem) {
  html:not([data-chrome="mounted"]) body { padding-top: 98px; }
}

.advisory {
  background: var(--band);
  border-bottom: 1px solid var(--hairline);
}
.advisory p {
  margin: 0;
  min-height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px var(--gutter);
  text-align: center;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* ---------- 6. Nav ------------------------------------------------------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--nav-bg);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid transparent;
  transition: border-color 220ms ease-out;
}
.nav[data-scrolled="true"] { border-bottom-color: var(--hairline); }
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .nav { -webkit-backdrop-filter: none; backdrop-filter: none; border-bottom-color: var(--hairline); }
}

.nav-inner {
  max-width: var(--measure-wide);
  margin-inline: auto;
  height: 48px;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  gap: 12px;
}
.brand {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.016em;
  color: var(--ink);
  white-space: nowrap;
}
.brand:hover { text-decoration: none; opacity: 0.8; }

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}
.nav-links a {
  display: block;
  padding: 6px 12px;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink);
  opacity: 0.88;
  white-space: nowrap;
  transition: opacity 140ms ease-out;
}
.nav-links a:hover { opacity: 1; text-decoration: underline; }
.nav-links a[aria-current="page"] { opacity: 1; font-weight: 600; }
@media (max-width: 62rem) { .nav-links li[data-hide="md"] { display: none; } }
@media (max-width: 48rem) { .nav-links li[data-hide="sm"] { display: none; } }
@media (max-width: 30rem) { .brand { font-size: 13px; } }

.nav-cart { margin-left: auto; }
.cart-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 4px 6px 8px;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  letter-spacing: -0.01em;
  cursor: pointer;
  opacity: 0.88;
  transition: opacity 140ms ease-out, transform 100ms ease-out;
}
.cart-button:hover { opacity: 1; }
.cart-button:active { transform: scale(0.96); }
.bag-wrap { display: flex; }
.bag { display: block; }
.cart-count {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--link);
  top: -0.6em;
}
.cart-count[data-empty="true"] { display: none; }

/* ---------- 7. Buttons and links ---------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 22px;
  border: 0;
  border-radius: var(--pill);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  line-height: 1.17648;
  letter-spacing: -0.022em;
  font-weight: 400;
  text-align: center;
  cursor: pointer;
  transition: background-color 160ms ease-out, transform 100ms ease-out, opacity 160ms ease-out;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: scale(0.97); }

.btn-blue { background: var(--blue); color: var(--on-blue); }
.btn-blue:hover { background: var(--blue-hover); }

.btn-outline { box-shadow: inset 0 0 0 1px var(--hairline-strong); }
.btn-outline:hover { background: rgba(0, 0, 0, 0.04); }

.btn-sm { padding: 8px 16px; font-size: 12px; line-height: 1.33337; letter-spacing: -0.01em; }
.btn-block { width: 100%; }

.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.36; cursor: not-allowed; }
.btn[disabled]:active { transform: none; }

.btn-link {
  border: 0;
  background: none;
  padding: 6px 0;
  font: inherit;
  font-size: 17px;
  letter-spacing: -0.022em;
  color: var(--link);
  cursor: pointer;
}
.btn-link:hover { text-decoration: underline; }

/* Chevron text link — Apple's standard "more" affordance */
.chev { display: inline-block; }
.chev::after { content: "\00a0\203A"; }
.chev-lg { font-size: 21px; line-height: 1.381; letter-spacing: 0.011em; }
.link-row { display: flex; flex-wrap: wrap; gap: 28px; margin: 0; }

/* ---------- 8. Hero: brand, with a rotating product spotlight ------------
   The whole composition has to land inside the first viewport at 375x667 as
   well as 1440x900, so the hero is a fixed BOX and everything in it is sized
   against that box rather than against the page width alone. Type sizes clamp
   on min(vw, vh): width alone gives a headline that fits a narrow phone
   beautifully and then pushes the vial off a short laptop screen.

   --hero-box is written by app.js as 100dvh minus the chrome actually above
   the hero, because the advisory bar wraps to two lines on a narrow phone and
   any hard-coded number would be wrong there. The value below is only the
   fallback for the moment before the script runs. */
.hero-spotlight {
  /* vh first, because every browser can resolve it. Safari only learned dvh in
     15.4, and a custom property holding an unresolvable unit does not fail at
     parse time -- it fails silently at every point of USE, taking the
     min-height of .hero-inner with it. The @supports block below upgrades this
     wherever dvh actually exists. */
  --hero-box: calc(100vh - 86px);
  --hero-pad: clamp(12px, 2.2vh, 32px);
  background: var(--page);
  padding-block: var(--hero-pad);
  overflow: hidden;              /* parallax must never widen the page */
}
/* The dynamic-viewport upgrade, applied only where it will actually resolve.
   dvh is the better value on a phone -- it tracks Safari's toolbar as it hides
   -- but it must never be the only value. */
@supports (height: 100dvh) {
  .hero-spotlight { --hero-box: calc(100dvh - 86px); }
}
.hero-inner {
  min-height: calc(var(--hero-box) - var(--hero-pad) * 2);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 1.8vh, 26px);
  text-align: center;
}
.hero-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(8px, 1.4vh, 18px);
  will-change: transform, opacity;
}

.display-xl {
  margin: 0;
  font-size: clamp(34px, min(8.6vw, 7.4vh), 96px);
  line-height: 1.04;
  letter-spacing: -0.018em;
  font-weight: 600;
  color: var(--ink);
}

/* Gold, used sparingly: a hairline under the brand and the compound's name. */
.hero-rule {
  display: block;
  width: clamp(40px, 5vw, 60px);
  height: 2px;
  border-radius: 2px;
  background: var(--gold);
}

.hero-tagline {
  margin: 0;
  font-size: clamp(15px, min(2.2vw, 2.1vh), 21px);
  line-height: 1.3;
  letter-spacing: 0.006em;
  color: var(--ink-2);
}

/* ---- the spotlight line: name, price, link ---- */
.hero-spot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 0.7vh, 8px);
  margin-top: clamp(2px, 1vh, 14px);
  will-change: transform, opacity;
}
.hero-spot-name {
  margin: 0;
  font-size: clamp(22px, min(4.4vw, 3.6vh), 44px);
  line-height: 1.1;
  letter-spacing: -0.014em;
  font-weight: 600;
  /* The one place gold carries a word. Large display type only: at body size
     it fails contrast and stops reading as a material and starts reading as a
     link that does not work. */
  color: var(--gold-ink);
}
.hero-spot-price {
  margin: 0;
  font-size: clamp(14px, min(2vw, 1.9vh), 19px);
  line-height: 1.35;
  letter-spacing: -0.012em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.hero-spot-link { margin: 0; }

/* ---- indicators and the pause control ---- */
.hero-controls {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: clamp(2px, 1vh, 12px);
}
.hero-dots { display: flex; align-items: center; gap: 8px; }
.hero-dot {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  width: 8px;
  height: 8px;
  border-radius: var(--pill);
  background: var(--hairline-strong);
  cursor: pointer;
  overflow: hidden;
  transition: width 320ms cubic-bezier(0.25, 0.1, 0.25, 1),
              background-color 320ms ease-out;
}
.hero-dot:hover { background: var(--ink-3); }
.hero-dot:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
.hero-dot[aria-current="true"] {
  width: 30px;
  background: var(--hairline-strong);
  cursor: default;
}
/* The fill IS the timer: the slide advances on this animation ending, so
   pausing the animation pauses the spotlight, with no second clock to drift. */
.hero-dot-fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--ink);
  transform-origin: left center;
  transform: scaleX(0);
}
.hero-dot[aria-current="true"] .hero-dot-fill {
  animation: hero-dot-fill var(--hero-interval, 6000ms) linear forwards;
}
.hero-spotlight[data-playing="false"] .hero-dot-fill { animation-play-state: paused; }
@keyframes hero-dot-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.hero-play {
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--pill);
  background: var(--search-fill);
  color: var(--ink-2);
  cursor: pointer;
  transition: background-color 200ms ease-out, color 200ms ease-out;
}
.hero-play:hover { color: var(--ink); }
.hero-play:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
.hero-play svg { width: 11px; height: 11px; fill: currentColor; }

/* ---- the product render ---- */
/* The shell is what the scroll parallax moves; the inner .hero-art is what the
   crossfade moves. Two elements, two transforms, so neither ever overwrites
   the other's matrix. */
.hero-art-shell {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: 100%;
  min-height: 0;
  will-change: transform;
}
.hero-art {
  display: flex;
  align-items: flex-end;
  justify-content: center;

  /* THIS ELEMENT OWNS THE ART'S SIZE. BOTH DIMENSIONS, EXPLICITLY.

     The hero vial rendered as a 0-pixel-wide column on a real iPhone: the
     debug panel showed #hero-art, .vial-tpl and the image all at 0 x 306 --
     full height, no width, image loaded and opaque. Every other render site on
     the site was fine.

     The reason is that the hero was the only WIDTH-DERIVED art box. Tiles,
     the product page, cart lines and the suggestion card all state a width
     (152px, 268px, 56px, 100%) and let height follow. The hero did the
     opposite: it took a height from the viewport and expected the width to
     come back up from .vial-tpl's aspect-ratio. That derivation cannot
     survive, because .vial-tpl carries `container-type: inline-size` -- which
     is inline-size CONTAINMENT, meaning its width must be computed WITHOUT
     looking at its contents. In a shrink-to-fit flex chain WebKit is then left
     with nothing to measure and the element contributes 0 to its ancestors'
     intrinsic width; Blink resolves the aspect-ratio width and passes it up.
     So every browser available here was fine and the shipping one was not.

     The fix is to stop deriving anything. One expression, --art-h, sets the
     height; the width is that same expression times the render's ratio. Both
     dimensions are definite before any child is consulted, which no engine can
     disagree about. --art-h keeps the vh/dvh pair for the reason every other
     viewport unit here does. */
  --art-ratio: 0.6666667;           /* 1024 / 1536 */
  --art-h: clamp(180px, 40vh, 460px);
  height: var(--art-h);
  width: calc(var(--art-h) * var(--art-ratio));
  flex: 0 0 auto;
  max-width: 100%;

  will-change: transform, opacity;
}
@supports (height: 100dvh) {
  .hero-art { --art-h: clamp(180px, 40dvh, 460px); }
}

/* Descendants fill the box and state no ratio of their own: exactly one
   element in the chain owns the ratio, and it is the one above with the
   definite height. */
.hero-art img, .hero-art svg,
.hero-art > .vial-tpl,
.hero-art > picture {
  width: 100%;
  height: 100%;
  max-width: none;
  aspect-ratio: auto;
  object-fit: contain;
}
.hero-art .vial-tpl .vial-img { width: 100%; height: 100%; }

/* NO `filter` ON THE HERO IMAGE. It used to carry
   `filter: drop-shadow(0 26px 30px ...)`, and that is a prime suspect for the
   art laying out but never painting on iOS: a drop-shadow forces the browser
   to rasterise the whole image into a filtered layer, this element already
   carries `will-change: transform, opacity` for the parallax and the
   crossfade, and at hero size on a 3x screen that layer is large. WebKit has a
   memory ceiling above which it gives up on such a layer silently -- no error,
   correct layout box, nothing drawn. The catalog tiles are a tenth the size
   and stay well under it, which matches the report exactly: tiles fine, hero
   blank.

   The grounding shadow is painted instead, as a soft ellipse behind the vial.
   It costs one gradient, composites on its own, and cannot take the product
   with it if it fails. Tiles keep their drop-shadow -- see section 8's
   .vial-img, where the size makes it safe. */
/* Explicitly none, not merely "not set here": the generic .vial-img rule in
   section 8 gives every vial on the site a drop-shadow, and that rule reaches
   this image too. Removing the hero's own filter declaration was not enough --
   the panel on a real device showed the shadow still applied. */
.hero-art .vial-img,
.hero-art picture { filter: none; }

.hero-art { position: relative; }
.hero-art::after {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  bottom: 0.5%;
  width: 66%;
  height: 5%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
              rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.17) 46%, rgba(0, 0, 0, 0) 78%);
  pointer-events: none;
}
@media (prefers-color-scheme: dark) {
  .hero-art::after {
    background: radial-gradient(closest-side,
                rgba(0, 0, 0, 0.78) 0%, rgba(0, 0, 0, 0.45) 46%, rgba(0, 0, 0, 0) 78%);
  }
}

/* ---- the affiliate programme's slide ------------------------------------
   The one slide in the rotation with no vial. It fills the same box, to the
   same height, and crossfades on the same springs -- only the contents differ.

   Typographic on purpose. An icon here would be the only icon on the page,
   and a vial would be a lie: nothing is being sold on this slide. What is
   left is the figure, the word, and two gold hairlines -- the same ornament
   as .hero-rule under the wordmark, which is the only accent this brand has.
   No gradient, no glyph, nothing that moves. */
.hero-art[data-kind="program"] {
  align-items: center;
}
/* The grounding shadow is for an object resting on a surface. There is no
   object, so there is no shadow. */
.hero-art[data-kind="program"]::after { display: none; }

.hero-program {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(12px, 2.4vh, 24px);
  width: 100%;
  height: 100%;
  min-width: 0;
  text-align: center;
}
.hero-program-rule {
  display: block;
  width: clamp(48px, 34%, 108px);
  height: 1px;
  background: var(--gold);
  opacity: 0.85;
}
.hero-program-figure {
  margin: 0;
  /* Sized against the art box's own height, the way the vial is, so the
     lockup keeps its proportion instead of drifting as the viewport changes.
     The vh pair is the house rule for any viewport unit here. */
  font-size: clamp(56px, 20vh, 132px);
  line-height: 0.92;
  letter-spacing: -0.03em;
  font-weight: 600;
  color: var(--gold-ink);
  font-variant-numeric: tabular-nums;
}
.hero-program-label {
  margin: 0;
  font-size: clamp(12px, min(1.5vw, 1.7vh), 15px);
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
@supports (height: 100dvh) {
  .hero-program-figure { font-size: clamp(56px, 20dvh, 132px); }
}

/* ---- wide: copy left, product right ----
   Side by side only when there is real width to split. Below this one
   column reads better and the vial keeps its height. */
@media (min-width: 62rem) {
  .hero-inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: clamp(32px, 5vw, 80px);
    text-align: left;
  }
  .hero-copy {
    align-items: flex-start;
    flex: 1 1 46%;
    max-width: 34rem;
  }
  .hero-art-shell { flex: 1 1 46%; align-items: center; }
  /* Set the ONE expression, not `height` directly -- the width is calculated
     from --art-h, so overriding height alone would leave the two dimensions
     describing different boxes. --hero-box always resolves (see the @supports
     pair above), but name a fallback anyway: an unresolvable custom property
     here would take the height and the width with it. */
  .hero-art { --art-h: min(calc(var(--hero-box, 100vh) - 64px), 620px); }
  .display-xl { font-size: clamp(40px, min(4.6vw, 8.4vh), 92px); }
}

/* Short and wide (a laptop in a small window): protect the vial's height by
   taking it out of the type first. */
@media (min-width: 62rem) and (max-height: 46rem) {
  .display-xl { font-size: clamp(34px, 6.4vh, 60px); }
  .hero-spot-name { font-size: clamp(20px, 4vh, 32px); }
}

/* Under reduced motion nothing advances on its own, so the dot fill is a
   position marker rather than a clock, and there is nothing to pause. */
@media (prefers-reduced-motion: reduce) {
  .hero-dot[aria-current="true"] .hero-dot-fill { animation: none; transform: scaleX(1); }
  .hero-dot { transition: none; }
}

/* Stat row — values and labels, separated by whitespace, not boxes */
/* A grid, not a wrapping flex row: flex-wrap gives 3+1 at around 800px, with
   the fourth stat orphaned on its own line right against the section boundary.
   Grid can only ever be 4-up or 2x2. */
.stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  justify-items: center;
  gap: clamp(36px, 4vw, 56px) clamp(20px, 3vw, 44px);
  margin: 0 auto;                 /* its own band now, not a hero appendix */
  padding: 0;
  max-width: 940px;
}
@media (max-width: 56.25rem) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 520px; }
}
.stat { text-align: center; max-width: 200px; }
.stat-value {
  margin: 0;
  font-size: clamp(28px, 3vw, 32px);
  line-height: 1.125;
  letter-spacing: 0.004em;
  font-weight: 600;
  color: var(--ink);
}
.stat-label {
  margin-top: 6px;
  font-size: 14px;
  line-height: 1.42859;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}

/* ---------- 8b. Delivery rail -------------------------------------------
   A horizontal card rail. The scroller is full-bleed so a card can sit half
   off the right edge -- that peek is what tells you the rail moves -- while
   the first card still lines up with the page gutter. */
.packaging-band {
  /* The page alternates page-white and band-grey down its length. This band
     sits between the stats strip and the catalog, both of which are grey, so
     it takes the white to keep the rhythm going. */
  background: var(--page);
  padding-block: clamp(56px, 8vw, 104px);
}
.rail-head { margin-bottom: clamp(24px, 3vw, 36px); }

/* The rail has two modes, and app.js picks between them by measuring.

   WHY THESE BOUNDARIES. A card was measured at a range of widths: at 240px its
   first paragraph runs to five lines and reads squeezed; at 260-300px it is
   four; at 320px and up, three. 280px is the comfort floor, and every boundary
   below is derived from it rather than picked to look tidy.

     >= 77rem (1232px)  four across. The container caps at 1200px, so each card
                        gets (1200 - 60) / 4 = 285px -- just over the floor.
     46rem to 77rem     two by two. At 736px that is 336px a card; at 1024px it
                        is 480px. Comfortable throughout, and a tablet in
                        portrait lands here rather than in a scroller.
     < 46rem (736px)    the rail. Two across would need 624px to stay above the
                        floor, so a phone gets the rail, which is the better
                        shape there anyway.

   Sliding a rail when everything already fits is friction with no payoff, so
   above 46rem there is no scroller, no snap, no drag and no dots at all. */
.rail-track {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* Full-bleed, with the lead-in matching the container gutter so card one
     aligns with the heading above it. */
  padding-inline: max(var(--gutter), calc((100vw - var(--measure-wide)) / 2));
  padding-block: 4px 8px;
  scroll-padding-left: max(var(--gutter), calc((100vw - var(--measure-wide)) / 2));
}

/* ---- grid mode ---- */
.rail-track[data-mode="grid"] {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Every card the same height, across rows as well as within one: four
     panels of different depths read as an accident rather than a set. */
  grid-auto-rows: 1fr;
  overflow: visible;
  scroll-snap-type: none;
  /* Back inside the container: full-bleed exists to let a card peek past the
     edge, and in grid mode nothing peeks. */
  padding-inline: max(var(--gutter), calc((100vw - var(--measure-wide)) / 2));
}
.rail-track[data-mode="grid"] .rail-card {
  width: auto;
  cursor: default;
  scroll-snap-align: none;
}
@media (min-width: 77rem) {
  .rail-track[data-mode="grid"] {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    /* A tighter gutter four across: it buys each card four more pixels, which
       is the difference between clearing the 280px floor and not at the
       narrow end of this range once a scrollbar takes its cut. */
    gap: 16px;
  }
}
.rail-track::-webkit-scrollbar { display: none; }
/* Smooths a scrollLeft assignment where the browser can manage it. app.js
   assigns the position rather than requesting an animation, so if this never
   runs the rail is still in the right place. */
.rail-track { scroll-behavior: smooth; }
.rail-track[data-dragging="true"] { cursor: grabbing; scroll-snap-type: none; }
.rail-track[data-dragging="true"] * { pointer-events: none; }

.rail-card {
  flex: 0 0 auto;
  width: min(360px, 78vw);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  padding: 32px 28px 34px;
  border-radius: var(--radius);
  background: var(--band);
  cursor: grab;
}
.rail-card:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

.rail-art {
  color: var(--ink-3);
  margin-bottom: 22px;
}
.rail-art svg { width: 96px; height: auto; display: block; }

.rail-title {
  margin: 0 0 14px;
  font-size: 21px;
  line-height: 1.19048;
  letter-spacing: 0.011em;
  font-weight: 600;
  color: var(--ink);
}
.rail-line {
  margin: 0 0 10px;
  font-size: 14px;
  line-height: 1.42859;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}
.rail-line:last-child { margin-bottom: 0; }

.rail-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: clamp(20px, 2.6vw, 30px);
}
.rail-dot {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  width: 8px;
  height: 8px;
  border-radius: var(--pill);
  background: var(--hairline-strong);
  cursor: pointer;
  transition: width 280ms cubic-bezier(0.25, 0.1, 0.25, 1), background-color 280ms ease-out;
}
.rail-dot:hover { background: var(--ink-3); }
.rail-dot:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
.rail-dot[aria-selected="true"] { width: 26px; background: var(--ink); cursor: default; }

@media (max-width: 40rem) {
  .rail-card { width: 82vw; padding: 26px 22px 28px; }
  .rail-art svg { width: 76px; }
  .rail-dot { min-height: 8px; }
}

/* Reduced motion: the rail is still a scroller, just without the smoothing
   and without the drag (which app.js does not attach at all in that mode). */
@media (prefers-reduced-motion: reduce) {
  .rail-track { scroll-behavior: auto; }
  .rail-dot { transition: none; }
}

/* ---------- 8c. "Frequently added together" ------------------------------
   Speaks the order-summary card's language: same radius, same shadow, same
   quiet. It is a suggestion, so it is styled as one -- no accent border, no
   badge, nothing that reads as a warning or a requirement. */
/* min-width: 0 is load-bearing, not tidiness. This is a grid item, and a grid
   item's automatic minimum size is its min-content size -- so without this,
   one over-eager descendant anywhere inside it (a long unbroken token, a
   table, anything set to nowrap) widens the column, then the layout, then the
   document, and the page scrolls sideways. Nothing on this site clips that
   away. The same reasoning applies to any other grid or flex item that holds
   arbitrary content. */
.cart-column { display: grid; gap: 16px; align-content: start; min-width: 0; }

.suggest-card { position: relative; }
.suggest-eyebrow {
  margin: 0 0 16px;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink-3);
}
.suggest-body { display: flex; gap: 18px; align-items: flex-start; }
.suggest-thumb { flex: 0 0 auto; width: 72px; display: block; }
.suggest-art { display: block; width: 100%; }
.suggest-art > svg, .suggest-art > img, .suggest-art > .vial-tpl {
  display: block;
  width: 100%;
}
.suggest-detail { flex: 1; min-width: 0; }
.suggest-name {
  font-size: 17px;
  line-height: 1.23536;
  letter-spacing: -0.022em;
  font-weight: 600;
  color: var(--ink);
}
.suggest-name:hover { color: var(--link); }
/* One line, and it is cut rather than wrapped. The card is a suggestion, not
   a product page: two or three lines of description here start competing with
   the item list above it, and at 320px the description ran to four.

   Clamped, NOT `white-space: nowrap`, and the difference broke the live site
   once already. `nowrap` makes this paragraph's MIN-CONTENT width the whole
   unwrapped sentence -- about 460px -- and `overflow: hidden` does not change
   that: it clips what is painted, not what the box asks for. `.suggest-detail`
   carries `min-width: 0`, so the flex item itself shrank happily; but the card
   sits in `.cart-column`, a grid item, and a grid item's automatic minimum
   size is its min-content size. The column refused to go below 525px inside a
   400px layout, and the whole cart page scrolled sideways with a dead band
   down the right at 440px. There is no blanket `overflow-x: hidden` on this
   site to hide that, by design.

   The clamp gets the same single line by letting the text wrap normally and
   then cutting it off after one, so min-content is the longest word. A
   browser that does not know the prefixed properties drops them and shows the
   description over two lines, which is merely less tidy. */
.suggest-desc {
  margin: 4px 0 0;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  line-clamp: 1;
}
.suggest-variants { justify-content: flex-start; margin-top: 12px; }
/* Price at the left edge of the content column, Add at its right edge --
   the same rhythm a catalog tile uses. These used to sit side by side at the
   left with a gap between them, which left both of them anchored to nothing
   and reading as though they had landed at random. */
.suggest-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 16px;
}
.suggest-actions .btn { flex: 0 0 auto; }

/* Very narrow: the row would leave the price and the button squeezed against
   each other, so they go one above the other and Add takes the width. */
@media (max-width: 24rem) {
  .suggest-actions { flex-direction: column; align-items: stretch; gap: 10px; }
  .suggest-actions .suggest-price { text-align: left; }
  .suggest-actions .btn { width: 100%; }
}
.suggest-price {
  font-size: 17px;
  letter-spacing: -0.022em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Dismiss: present, findable, and not competing with the Add button. */
.suggest-dismiss {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-3);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 180ms ease-out, color 180ms ease-out;
}
.suggest-dismiss:hover { background: var(--band); color: var(--ink); }
.suggest-dismiss:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

@media (max-width: 40rem) {
  .suggest-body { gap: 14px; }
  .suggest-thumb { width: 60px; }
  .suggest-dismiss { width: 44px; height: 44px; top: 4px; right: 4px; }
}

/* ---------- 8d. Product page: lot documentation note --------------------
   Replaces the lot/document table when the site is set to provide paperwork
   with the order. Centred and quiet: it is a statement of fact, not a feature,
   and it should not compete with the specifications table above it. */
.doc-note {
  max-width: var(--measure-read);
  margin: 0 auto;
  text-align: center;
}
.doc-note-title {
  margin: 0;
  font-size: 21px;
  line-height: 1.19048;
  letter-spacing: 0.011em;
  font-weight: 600;
  color: var(--ink);
}
.doc-note-body {
  margin: 12px 0 0;
  font-size: 15px;
  line-height: 1.46667;
  letter-spacing: -0.012em;
  color: var(--ink-2);
}

/* ---------- 8e. Quality page ---------------------------------------------
   Alternating full-width bands, art and copy swapping sides down the page.
   Generous type: this is a page people read rather than scan, and the measure
   is kept short enough to read comfortably. */
.quality-hero {
  text-align: center;
  padding-block: clamp(56px, 9vw, 120px) clamp(48px, 7vw, 96px);
}
.quality-hero-art {
  display: flex;
  justify-content: center;
  color: var(--ink-3);
  margin-bottom: clamp(24px, 3vw, 40px);
}
.quality-hero-art svg { width: clamp(88px, 12vw, 128px); height: auto; }
.quality-hero .subhead-lg { max-width: 46ch; margin: 20px auto 0; }

.display-lg {
  margin: 0;
  font-size: clamp(38px, 6.4vw, 72px);
  line-height: 1.06;
  letter-spacing: -0.015em;
  font-weight: 600;
  color: var(--ink);
}

.quality-band { padding-block: clamp(56px, 8vw, 110px); }
.quality-inner {
  display: grid;
  grid-template-columns: minmax(0, 0.7fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(32px, 6vw, 88px);
}
/* Odd sections put the art on the right, so the eye zig-zags down the page
   instead of running along one rail. */
.quality-band[data-flip="true"] .quality-inner { grid-template-columns: minmax(0, 1fr) minmax(0, 0.7fr); }
.quality-band[data-flip="true"] .quality-art { order: 2; }

.quality-art {
  display: flex;
  justify-content: center;
  color: var(--ink-3);
}
.quality-art svg { width: clamp(120px, 18vw, 200px); height: auto; }

.quality-copy { max-width: 44ch; }
.quality-copy .headline-2 { margin: 0 0 18px; }
.quality-copy .body-1 { margin: 0 0 14px; }
.quality-copy .body-1:last-of-type { margin-bottom: 0; }

@media (max-width: 56.25rem) {
  .quality-inner,
  .quality-band[data-flip="true"] .quality-inner { grid-template-columns: 1fr; text-align: center; }
  .quality-band[data-flip="true"] .quality-art { order: -1; }
  .quality-art { order: -1; }
  .quality-copy { max-width: none; margin: 0 auto; }
  .quality-copy .link-row { justify-content: center; }
  .quality-art svg { width: 116px; }
}

/* ---------- 8f. Discount and referral codes ------------------------------
   The code control lives INSIDE the order summary card, because a code is an
   adjustment to a total and belongs with the total. It used to be two cards
   of its own -- one holding a single small link, a second for the applied
   code -- which is two cards more than the idea is worth, and it broke the
   rhythm of the column either side of it.

   Four states, and not one of them is a card:
     resting   a slim row under a hairline at the foot of the summary
     open      that row becomes an inline field and Apply
     applied   a line in the totals like any other, with a small remove control
     held      one line of fineprint, for a code the cart has not yet earned */
.checkout-summary > .code-slot {
  margin-top: 14px;
  padding-top: 2px;
  border-top: 1px solid var(--hairline);
}
.code-slot > * + * { margin-top: 0; }

.code-toggle {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  background: none;
  border: 0;
  margin: 0;
  padding: 12px 0 4px;
  font: inherit;
  font-size: 15px;
  letter-spacing: -0.016em;
  text-align: left;
  color: var(--link);
  cursor: pointer;
}
.code-toggle:hover { text-decoration: underline; }
.code-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 6px; }

/* The applied state. Deliberately the same box as .code-toggle -- same
   padding, same font, same single line -- because the whole point of it is
   that swapping one for the other changes the card's height by nothing, so
   the payment form below does not move when the server comes back with a
   re-priced order. scripts/ asserts the two heights match. */
.code-applied {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  margin: 0;
  padding: 12px 0 4px;
  font-size: 15px;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}
.code-applied-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Opening it replaces the row rather than pushing another one under it. */
.code-slot[data-open="true"] .code-toggle,
.code-slot[data-open="true"] .code-applied { display: none; }
.code-slot .code-entry { display: none; overflow: hidden; }
.code-slot[data-open="true"] .code-entry { display: block; padding: 10px 0 2px; }

.code-row { display: flex; gap: 8px; align-items: center; }
.code-row input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 16px;                 /* 16px or iOS zooms on focus */
  text-transform: uppercase;
  height: 38px;
  padding: 0 12px;
  border-radius: 9px;
  border: 1px solid var(--hairline);
  background: var(--field);
  color: var(--ink);
}
.code-row input:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; border-color: transparent; }
.code-apply {
  flex: 0 0 auto;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  height: 38px;
  padding: 0 16px;
  border-radius: var(--pill);
  background: var(--ink);
  color: var(--page);
  font: inherit;
  font-size: 14px;
  letter-spacing: -0.012em;
  cursor: pointer;
}
.code-apply:hover { opacity: 0.86; }
.code-apply:disabled { opacity: 0.4; cursor: default; }
.code-apply:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

.code-msg { margin: 8px 0 0; }
.code-msg[data-bad="true"] { color: var(--red, #b3261e); }

/* The held state. A code that is real and waiting for a bigger cart is worth
   one quiet line, and nothing more than that. */
.code-held { margin: 12px 0 0; color: var(--ink-2); }

/* The discount line in the summary. Gold, the same accent the chip beside the
   total uses, so the saving is legible without shouting. */
.totals-discount span:last-child { color: var(--gold-ink); }
.totals-code { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }

/* Small on purpose: it sits in a row of figures and must not compete with
   them. The hit area is grown underneath it instead -- see section 26. */
.code-remove {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 2px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-3);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.code-remove:hover { background: var(--band); color: var(--ink); }
.code-remove:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* The code, said again beside the total, so the collapsed card still explains
   why the number is what it is. */
.summary-code-chip {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 8px;
  border-radius: var(--pill);
  color: var(--gold-ink);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Wide enough for any code anyone will actually print on a card. A code may
     be 32 characters long, so the ellipsis stays as a backstop -- but it should
     be a backstop, not the normal case: a truncated identifier reads as a bug. */
  max-width: 18ch;
}

/* Re-pricing is a short round trip to the server. Dim the figures that are
   about to change; never blank the card, and never touch the card form below
   it -- the mounted Stripe fields must not so much as flicker. */
.checkout-summary[data-busy="true"] .totals,
.checkout-summary[data-busy="true"] .summary-total {
  opacity: 0.45;
  transition: opacity 140ms linear;
}
.checkout-summary[data-busy="true"] .code-remove,
.checkout-summary[data-busy="true"] .code-apply { pointer-events: none; }

/* ---- the banner a referral link raises ----
   Bottom centre, brief, dismissible, and never in the way of anything. */
.ref-banner {
  position: fixed;
  left: 50%;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: min(520px, calc(100vw - 32px));
  padding: 12px 10px 12px 18px;
  border-radius: var(--pill);
  background: var(--ink);
  color: var(--page);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.26);
  font-size: 14px;
  line-height: 1.35;
  letter-spacing: -0.012em;
  transform: translate(-50%, 16px);
  opacity: 0;
  transition: opacity 280ms ease-out, transform 280ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
.ref-banner[data-in="true"] { opacity: 1; transform: translate(-50%, 0); }
.ref-banner-text { flex: 1; }
.ref-banner strong { font-weight: 600; }
.ref-banner-close {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  opacity: 0.7;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.ref-banner-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.14); }
.ref-banner-close:focus-visible { outline: 2px solid var(--page); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ref-banner { transition: opacity 120ms linear; transform: translate(-50%, 0); }
}

@media (max-width: 40rem) {
  .ref-banner { left: 16px; right: 16px; transform: translate(0, 16px); max-width: none; }
  .ref-banner[data-in="true"] { transform: translate(0, 0); }
}

/* ---------- 9. Section headers ------------------------------------------ */
.section-head { text-align: center; margin-bottom: clamp(36px, 4vw, 56px); }
.section-head .subhead { max-width: 52ch; margin: 14px auto 0; }

/* ---------- 10. Catalog tiles ------------------------------------------- */
.catalog-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px 20px;
  margin-bottom: clamp(28px, 3.5vw, 44px);
}
/* The search field is defined at the very end of this file, after every
   generic form rule, so nothing can reintroduce a second box. */
.catalog-count { color: var(--ink-2); margin: 0; }

.grid-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 20px;
}
@media (max-width: 62rem) { .grid-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 40rem) { .grid-tiles { grid-template-columns: 1fr; } }

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 40px 24px 32px;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--tile-shadow);
  transform: translate3d(0, calc(var(--lift, 0) * -1px), 0);
  transition: box-shadow 260ms ease-out;
  will-change: transform;
}
.tile[data-hover="true"] { box-shadow: var(--tile-shadow-hover); }
.tile-link { position: absolute; inset: 0; border-radius: inherit; z-index: 1; }
.tile-link:focus-visible { outline-offset: -4px; }
.tile-art { display: block; width: 152px; margin-bottom: 22px; }
.tile-name {
  margin: 0;
  font-size: 21px;
  line-height: 1.19;
  letter-spacing: 0.011em;
  font-weight: 600;
  color: var(--ink);
}
.tile-meta { margin: 6px 0 0; font-size: 12px; line-height: 1.33337; letter-spacing: -0.01em; color: var(--ink-2); }
.tile-price { margin: 16px 0 0; font-size: 17px; letter-spacing: -0.022em; color: var(--ink); }
.tile-add { position: relative; z-index: 2; margin-top: 18px; }

/* ---------- 10a. Category filter pills -----------------------------------
   App Store filter chips: a quiet fill at rest, and the selected one inverts
   to the ink colour rather than turning blue. Blue is what you press to do
   something; this only changes what you are looking at, so it stays neutral. */
.catalog-filters { margin-bottom: clamp(24px, 3vw, 36px); }

.filter-pills {
  display: flex;
  gap: 8px;
  justify-content: center;
  flex-wrap: wrap;
}
.filter-pill {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  margin: 0;
  padding: 8px 16px;
  border-radius: var(--pill);
  background: var(--search-fill);
  color: var(--ink-2);
  font: inherit;
  font-size: 14px;
  line-height: 1.28577;
  letter-spacing: -0.016em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 200ms ease-out, color 200ms ease-out;
}
.filter-pill:hover { color: var(--ink); }
.filter-pill[aria-checked="true"] {
  background: var(--ink);
  color: var(--page);
  cursor: default;
}
.filter-pill:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* A coarse pointer gets a full target; a mouse does not need one. */
@media (pointer: coarse) {
  .filter-pill { min-height: 44px; padding-inline: 18px; }
}

/* ---------- 10b. Strength picker ----------------------------------------
   One card serves every strength of a compound, so the numbers sit on a
   shared track with the unit stated once at the end: "5 10 15 mg". Above
   the tile link in z-order, or the card's own anchor would swallow the tap. */
.variant-picker {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 14px;
}
.variant-option {
  appearance: none;
  -webkit-appearance: none;
  min-width: 38px;
  padding: 5px 10px;
  border: 0;
  border-radius: var(--pill);
  background: var(--search-fill);
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--ink-2);
  font: inherit;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: color 180ms ease-out, background-color 180ms ease-out,
              box-shadow 180ms ease-out;
}
.variant-option:hover { color: var(--ink); }
.variant-option[aria-pressed="true"] {
  background: var(--ink);
  color: var(--page);
  box-shadow: none;
  cursor: default;
}
.variant-option:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.variant-option:active { transform: scale(0.96); }
.variant-unit {
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink-3);
}

/* The product page carries the same control at reading size. */
.variant-picker-lg { justify-content: flex-start; gap: 8px; margin-top: 18px; }
.variant-picker-lg .variant-option { min-width: 52px; padding: 9px 16px; font-size: 15px; }
.variant-picker-lg .variant-unit { font-size: 15px; }

.search-notice {
  max-width: var(--measure-read);
  margin: 0 auto clamp(28px, 3vw, 40px);
  padding: 20px 24px;
  border-radius: var(--radius-sm);
  background: var(--band);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.42859;
  letter-spacing: -0.016em;
  text-align: center;
}
.band-soft .search-notice { background: var(--card); }
.search-notice a { text-decoration: underline; }

/* Product art shares ONE aspect box so a tile keeps the same geometry whichever
   source it uses — otherwise the name and price sit a few pixels lower on one
   tile than on its neighbours in the row. The renders are 1024 x 1536. */
.tile-art, .line-item-art { aspect-ratio: 1024 / 1536; }
.tile-art > svg, .tile-art > img, .tile-art > .vial-tpl,
.line-item-art > svg, .line-item-art > img, .line-item-art > .vial-tpl,
.product-art > svg, .product-art > img, .product-art > .vial-tpl {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
}

/* ---- Template art: one render, per-product text laid over its blank label --
   The component is its own container, so `cqw` inside it means "percent of the
   art's width" — which is what makes the overlay track the image at every size
   without a single resize listener. */
.vial-tpl {
  position: relative;
  display: block;
  aspect-ratio: 1024 / 1536;
  container-type: inline-size;
}
/* <picture> wraps the img so the browser can choose WebP over PNG. It is a
   plain block filling the template, so the image below it lays out exactly as
   it did when it was a direct child -- no reliance on `display: contents`,
   which would leave the image unsized anywhere it is not supported. */
.vial-tpl > picture {
  display: block;
  width: 100%;
  height: 100%;
}
.vial-tpl .vial-img {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
}
.vial-tpl-name,
.vial-tpl-chip {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: none;
  /* The label is artwork, not page text: it must not pick up page theming,
     and it must never be synthesised into a fake bold. */
  font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
  font-weight: 600;
  font-synthesis: none;
  line-height: 1;
  white-space: nowrap;
}
/* The inner span carries the size and the cap-height correction, so the shift
   never moves the box it is centred in. */
.vial-tpl-name > span,
.vial-tpl-chip > span { display: block; line-height: 1; }

/* Below about 96px of art the name is under 4px tall and the strength under
   2px: not small type, an illegible smear. At that size the vial is a
   thumbnail standing for the product, and the clean template reads better than
   a blur, so the overlay steps aside — our painted chip included, since an
   empty gold pill would be worse than the baked one. */
@container (max-width: 96px) {
  .vial-tpl-name, .vial-tpl-chip { display: none; }
}

/* letter-spacing also adds a gap AFTER the final glyph, which pulls the
   centred string very slightly left. The negative margin cancels exactly that
   trailing gap, so the ink is centred rather than the ink-plus-gap. */
.vial-tpl-name { color: #ffffff; }
.vial-tpl-name > span { letter-spacing: 0.13em; margin-right: -0.13em; }

/* Painted over the template's own chip — see TEMPLATE_ART in app.js. */
.vial-tpl-chip { color: #000000; }
.vial-tpl-chip > span { letter-spacing: 0.05em; margin-right: -0.05em; }

/* Container queries are iOS 16+. Without them every `cqw` in the overlay is
   unresolvable, so the compound name and the strength chip would be laid over
   the label at whatever size the font happened to inherit -- text of the wrong
   size sitting in the wrong place on the artwork, which reads as a defect
   rather than as a missing enhancement. The vial itself does not depend on any
   of this, so the honest degradation is to show the render and drop the
   overlay: a blank label is a blank label, and every product page states the
   compound and strength in real text beside it anyway. */
@supports not (container-type: inline-size) {
  .vial-tpl-name,
  .vial-tpl-chip { display: none; }
}

/* Product photographs (data.js `image`). Cut-outs on transparency, so they
   need no blend mode or backing panel: the drop shadow follows the silhouette
   and the art sits correctly on white, on #f5f5f7 and on black alike. */
.vial-img {
  display: block;
  width: 100%;
  aspect-ratio: 1024 / 1536;
  object-fit: contain;
  object-position: center bottom;
  filter: drop-shadow(0 10px 12px rgba(0, 0, 0, 0.14));
}
@media (prefers-color-scheme: dark) {
  .vial-img { filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.55)); }
}

/* Entrance state — scripted only, so content never depends on JS */
html.js .enter:not([data-entered]) { opacity: 0; }

/* The hero art is the one .enter element that is above the fold on every
   screen, and the one that must never be invisible. Everything else here
   reveals on scroll and can wait for the script; this cannot.

   Scoped deliberately to the hero shell rather than to .enter at large -- a
   blanket timer would pop the whole page in after three seconds and defeat the
   scroll reveal. If the entrance has not run by then, the script has thrown or
   its clock has stalled, and a visible vial beats a correct animation.
   Animations outrank inline styles in the cascade, so this wins over a spring
   that froze mid-fade. */
html.js #hero-art-shell.enter:not([data-entered]) {
  animation: hero-art-failsafe 1ms linear 3s forwards;
}
@keyframes hero-art-failsafe { to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* No entrance to wait for; do not wait three seconds to say so. */
  html.js #hero-art-shell.enter:not([data-entered]) { animation-delay: 0s; }
}

/* ---------- 11. Cards, notices, fine print ------------------------------ */
.card {
  padding: clamp(24px, 3vw, 36px);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--tile-shadow);
}
.band-soft .card { box-shadow: var(--tile-shadow); }

.notice {
  padding: 18px 20px;
  border-radius: var(--radius-sm);
  background: var(--band);
  font-size: 14px;
  line-height: 1.42859;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}
.card .notice { background: var(--band); }
.notice strong { color: var(--ink); }
@media (prefers-color-scheme: dark) {
  .card .notice { background: rgba(255, 255, 255, 0.05); }
}

.legal-note { max-width: var(--measure-read); margin-inline: auto; text-align: center; }

/* ---------- 12. Tech specs table ---------------------------------------- */
.specs-wrap { max-width: var(--measure-read); margin: clamp(32px, 4vw, 48px) auto 0; }
.specs { margin: 0; }
.specs-row {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: 8px 32px;
  padding: 18px 0;
  border-bottom: 1px solid var(--hairline);
}
.specs-row:first-child { border-top: 1px solid var(--hairline); }
.specs dt { color: var(--ink-2); }
.specs dd { margin: 0; color: var(--ink); }
@media (max-width: 34rem) {
  .specs-row { grid-template-columns: 1fr; gap: 2px; }
}

/* ---------- 13. Product page -------------------------------------------- */
.product-hero { text-align: center; padding-top: clamp(28px, 4vw, 48px); }
.product-art { width: 100%; max-width: 268px; margin: clamp(8px, 2vw, 24px) auto clamp(24px, 3.5vw, 40px); }
.product-hero .subhead { max-width: 46ch; margin: 16px auto 0; }
.product-price { margin: 22px 0 0; font-size: 24px; letter-spacing: 0.009em; font-weight: 600; color: var(--ink); }
.product-hero .fineprint { max-width: 52ch; margin: 20px auto 0; }

.buy-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 22px;
}
.added-flag { font-size: 14px; letter-spacing: -0.016em; color: var(--ink-2); opacity: 0; }

.qty {
  display: inline-flex;
  align-items: center;
  border-radius: var(--pill);
  box-shadow: inset 0 0 0 1px var(--hairline-strong);
  overflow: hidden;
}
.qty button {
  width: 42px; height: 42px;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 140ms ease-out, transform 100ms ease-out;
}
.qty button:hover { background: rgba(0, 0, 0, 0.04); }
.qty button:active { transform: scale(0.9); }
@media (prefers-color-scheme: dark) { .qty button:hover { background: rgba(255, 255, 255, 0.07); } }
.qty output { min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; }

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin: 0 0 clamp(20px, 3vw, 32px);
  padding: 0;
  list-style: none;
  font-size: 12px;
  letter-spacing: -0.01em;
  color: var(--ink-2);
}
.breadcrumb li + li::before { content: "\203A"; margin-right: 6px; }

/* ---------- 14. Cart ----------------------------------------------------- */
.cart-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}
@media (max-width: 56rem) { .cart-layout { grid-template-columns: 1fr; } }

.line-item {
  display: grid;
  grid-template-columns: 56px 1fr auto;
  gap: 18px;
  align-items: center;
  padding: 22px 0;
  border-bottom: 1px solid var(--hairline);
}
.line-item:last-of-type { border-bottom: 0; padding-bottom: 0; }
.card > .headline-3 + .line-item { border-top: 1px solid var(--hairline); margin-top: 20px; }
.line-item-art { width: 56px; }
.line-item-name { font-size: 17px; font-weight: 600; letter-spacing: -0.022em; color: var(--ink); }
.line-item-name:hover { color: var(--link); text-decoration: none; }
.line-item-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.line-item-actions .qty button { width: 34px; height: 34px; font-size: 15px; }
.line-item-actions .price { font-size: 17px; letter-spacing: -0.022em; }
@media (max-width: 34rem) {
  .line-item { grid-template-columns: 48px 1fr; }
  .line-item-art { width: 48px; }
  .line-item-actions { grid-column: 1 / -1; align-items: flex-start; }
}
.btn-text {
  border: 0; background: none; padding: 0;
  font: inherit; font-size: 12px; letter-spacing: -0.01em;
  color: var(--link); cursor: pointer;
}
.btn-text:hover { text-decoration: underline; }

.totals { margin: 20px 0 0; }
.totals-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 0;
  font-size: 14px;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}
/* The money column never wraps: "$155.00" broken across two lines reads as a
   different number. The item name is what gives, and it is allowed to. */
.totals-row span:last-child { color: var(--ink); text-align: right; white-space: nowrap; }
.totals-row span:first-child { min-width: 0; }
.totals-row.total {
  margin-top: 6px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.022em;
  color: var(--ink);
}
.card > * + * { margin-top: 18px; }
.card > .line-item + .line-item { margin-top: 0; }
.order-state { max-width: var(--measure-read); margin-inline: auto; }

/* Inline checkout error: quiet fine print, tinted just enough to register. */
.checkout-error {
  margin-top: 12px;
  color: #b3261e;
  text-align: center;
}
@media (prefers-color-scheme: dark) {
  .checkout-error { color: #ff6961; }
}

/* ---------- 15. Forms ---------------------------------------------------- */
.field { display: grid; gap: 8px; margin-top: 22px; }
.field label { font-size: 14px; letter-spacing: -0.016em; color: var(--ink-2); }
.field-hint { font-size: 12px; letter-spacing: -0.01em; color: var(--ink-3); margin: 0; }

input[type="text"], input[type="search"], select, textarea {
  width: 100%;
  padding: 12px 14px;
  border: 0;
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px var(--hairline-strong);
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  letter-spacing: -0.022em;
  transition: box-shadow 160ms ease-out;
}
select {
  appearance: none;
  padding-right: 40px;
  background-image: linear-gradient(45deg, transparent 49%, currentColor 50%),
    linear-gradient(-45deg, transparent 49%, currentColor 50%);
  background-size: 7px 7px, 7px 7px;
  background-position: right 20px center, right 15px center;
  background-repeat: no-repeat;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--blue);
}

.check {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 14px;
  align-items: start;
  padding: 16px 0;
  border-top: 1px solid var(--hairline);
  font-size: 14px;
  line-height: 1.42859;
  letter-spacing: -0.016em;
  color: var(--ink-2);
  cursor: pointer;
}
.check:first-of-type { border-top: 0; }
.check strong { color: var(--ink); font-weight: 600; }
.check a { text-decoration: underline; }
.check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 22px; height: 22px;
  margin: 0;
  border-radius: 6px;
  background-color: var(--field);
  box-shadow: inset 0 0 0 1px var(--hairline-strong);
  cursor: pointer;
  transition: background-color 140ms ease-out, box-shadow 140ms ease-out;
}
.check input[type="checkbox"]:checked {
  background-color: var(--blue);
  box-shadow: inset 0 0 0 1px var(--blue);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.4l3 3 6-6.4'/%3E%3C/svg%3E");
  background-size: 15px 15px;
  background-position: center;
  background-repeat: no-repeat;
}
.check input[type="checkbox"]:disabled { opacity: 0.45; cursor: default; }

/* ---------- 16. COA table ------------------------------------------------ */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; font-size: 14px; letter-spacing: -0.016em; }
caption {
  text-align: left;
  padding-bottom: 16px;
  font-size: 12px;
  letter-spacing: -0.01em;
  color: var(--ink-2);
}
th, td {
  text-align: left;
  padding: 14px 20px 14px 0;
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
  white-space: nowrap;
}
thead th {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-2);
  border-bottom: 1px solid var(--hairline-strong);
}
tbody th { font-weight: 400; }
tbody tr:last-child td, tbody tr:last-child th { border-bottom: 0; }
.lot-row-archived th, .lot-row-archived td { color: var(--ink-2); }

.pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--pill);
  background: var(--band);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-2);
}
.card .pill { background: var(--band); }
.pill-current { background: rgba(0, 113, 227, 0.12); color: var(--link); }

#assay-list { margin: 20px 0 0; padding-left: 20px; color: var(--ink-2); font-size: 17px; }
#assay-list li { margin-bottom: 8px; }

/* ---------- 17. Legal pages --------------------------------------------- */
.legal-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: clamp(32px, 5vw, 64px);
  align-items: start;
}
/* minmax(0,1fr), not 1fr: the policy index is a nowrap flex row on a phone,
   and a plain 1fr track refuses to shrink below its min-content width, which
   is every pill laid end to end. That widened the column and clipped the
   document text off the right of the screen. */
@media (max-width: 56rem) {
  .legal-layout { grid-template-columns: minmax(0, 1fr); }
  .legal-layout > * { min-width: 0; }
}
.legal-nav { position: sticky; top: 72px; list-style: none; margin: 0; padding: 0; }
/* On a phone a twelve-item vertical policy index pushes the actual document
   most of a screen down the page. It becomes a horizontal scroller instead:
   one row, the current page marked, edge-faded to show it moves. */
@media (max-width: 56rem) {
  .legal-nav {
    position: static;
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    padding-bottom: 4px;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
  }
  .legal-nav::-webkit-scrollbar { display: none; }
  .legal-nav li { flex: 0 0 auto; scroll-snap-align: center; }
  .legal-nav a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0 14px;
    border-radius: var(--pill);
    background: var(--search-fill);
    white-space: nowrap;
    font-size: 13px;
  }
  /* Specificity raised deliberately: the base rule below sets colour on the
     same selector later in the file, which painted ink text on an ink pill. */
  .legal-nav li a[aria-current="page"] { background: var(--ink); color: var(--page); }
}
.legal-nav a {
  display: block;
  padding: 8px 0;
  font-size: 14px;
  letter-spacing: -0.016em;
  color: var(--ink-2);
}
.legal-nav a:hover { color: var(--ink); text-decoration: none; }
.legal-nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }

.legal-body { max-width: var(--measure-read); }
.legal-body h2 {
  margin: 40px 0 10px;
  font-size: 21px;
  line-height: 1.19;
  letter-spacing: 0.011em;
  font-weight: 600;
  color: var(--ink);
}
.legal-body h2:first-child { margin-top: 0; }
.legal-body p { margin: 0 0 16px; color: var(--ink-2); }
.legal-body ul { margin: 0 0 16px; padding-left: 20px; color: var(--ink-2); }
.legal-body li { margin-bottom: 8px; }

/* The superseded-version banner. Deliberately loud: the one thing that must
   not happen on an archived policy page is a reader taking it for the current
   agreement. */
.archive-flag {
  border-left: 3px solid var(--gold);
}
.archive-block {
  margin-top: 40px;
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
}
.archive-block h2 { margin-top: 0; font-size: 17px; }
.archive-block p { font-size: 13px; color: var(--ink-3); margin: 0; }
.archive-list { margin: 10px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.archive-list li { font-size: 13px; }
/* The all-sales-final disclosure. Quiet, but it must not read as fine print
   the eye can skip: it carries its own rule and a little more contrast than
   the surrounding small text. */
.final-sale-notice {
  margin: 16px 0 0;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--band);
  border-left: 3px solid var(--gold);
  font-size: 13px;
  line-height: 1.38;
  letter-spacing: -0.008em;
  color: var(--ink-2);
}
.final-sale-notice strong { color: var(--ink); }
.final-sale-notice a { color: var(--link); white-space: nowrap; }

.version-line {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  margin-top: 20px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--hairline);
  font-size: 12px;
  letter-spacing: -0.01em;
  color: var(--ink-3);
}

/* ---------- 18. Footer --------------------------------------------------- */
.site-footer {
  background: var(--band);
  border-top: 1px solid var(--hairline);
  padding-block: 40px 24px;
  font-size: 12px;
  line-height: 1.33337;
  letter-spacing: -0.01em;
  color: var(--ink-2);
}
.footer-inner { max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter); }
.footer-legal { margin: 0 0 28px; padding-bottom: 24px; border-bottom: 1px solid var(--hairline); color: var(--ink-2); }
.footer-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px 24px;
  padding-bottom: 28px;
  border-bottom: 1px solid var(--hairline);
}
@media (max-width: 56rem) { .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 34rem) { .footer-grid { grid-template-columns: 1fr; } }
.footer-col h2 { margin: 0 0 10px; font-size: 12px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
.footer-list { list-style: none; margin: 0; padding: 0; }
.footer-list li { margin-bottom: 8px; }
.footer-list a, .footer-col address a { color: var(--ink-2); }
.footer-list a:hover, .footer-col address a:hover { color: var(--ink); }
.footer-col address { font-style: normal; line-height: 1.6; color: var(--ink-2); }

.footer-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 24px;
  padding-top: 18px;
}
.footer-base p { margin: 0; }
.footer-base-list { display: flex; flex-wrap: wrap; gap: 8px 20px; list-style: none; margin: 0; padding: 0; align-items: center; }
.footer-base button {
  border: 0; background: none; padding: 0;
  font: inherit; font-size: 12px; letter-spacing: -0.01em;
  color: var(--ink-2); cursor: pointer; text-decoration: underline;
}
.footer-base button:hover { color: var(--ink); }

/* ---------- 19. Entry gate ---------------------------------------------- */
body.gate-open { overflow: hidden; }
body.gate-open .blurable {
  filter: blur(16px);
  pointer-events: none;
  user-select: none;
}
@media (prefers-reduced-motion: reduce) { body.gate-open .blurable { filter: blur(8px); } }

.gate-root {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 20px;
  overflow-y: auto;
}
.gate-scrim { position: fixed; inset: 0; background: var(--scrim); opacity: 0; }
.gate-dialog {
  position: relative;
  width: 100%;
  max-width: 540px;
  margin: auto;
  padding: clamp(28px, 4vw, 44px);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--sheet-shadow);
  text-align: center;
  opacity: 0;
  transform: scale(0.96);
  will-change: transform, opacity, filter;
}
.sheet-title {
  margin: 0;
  font-size: 28px;
  line-height: 1.14;
  letter-spacing: 0.007em;
  font-weight: 600;
  color: var(--ink);
}
.sheet-title-sm { font-size: 21px; line-height: 1.19; letter-spacing: 0.011em; }

/* The intro is legal context, not the headline act. It reads at a secondary
   weight and on a tighter measure so the eye reaches the controls -- which are
   the thing the visitor is actually here to operate -- without wading. The
   words are untouched; only their treatment changes. */
.gate-body {
  margin: 14px auto 0;
  max-width: 46ch;
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: -0.012em;
  color: var(--ink-2);
}
#gate-form { text-align: left; }
.gate-scroll { text-align: center; }
.gate-scroll > .field { text-align: left; }

/* Real gaps between groups: the select, the confirmations and the fine print
   are three separate asks and should not read as one wall. */
.gate-scroll > .field { margin-top: 24px; }
.gate-checks { margin-top: 24px; border-top: 1px solid var(--hairline); text-align: left; }
.gate-checks .check:first-of-type { border-top: 0; }
.gate-actions { display: grid; justify-items: center; gap: 4px; margin-top: 26px; }
.gate-actions .btn-blue { width: 100%; }
.gate-version { margin-top: 26px; padding-top: 20px; border-top: 1px solid var(--hairline); text-align: center; }
/* Kept on the sheet -- transparency matters -- but quiet, and free to sit
   below the fold now that the body scrolls. */
.gate-version .fineprint { font-size: 12px; line-height: 1.42; color: var(--ink-2); }
.gate-legal-note { margin-top: 10px; color: var(--ink-3); }
.gate-grabber { display: none; }

/* ---------- 20. Cart sheet ---------------------------------------------- */
.sheet-root { position: fixed; inset: 0; z-index: 150; display: none; }
.sheet-root[data-open="true"] { display: block; }
.sheet-scrim { position: absolute; inset: 0; background: var(--scrim); opacity: 0; }
.sheet {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(400px, 100%);
  display: flex;
  flex-direction: column;
  padding: 20px 24px 24px;
  background: var(--card);
  box-shadow: var(--sheet-shadow);
  transform: translate3d(100%, 0, 0);
  will-change: transform;
  overscroll-behavior: contain;
}
.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--hairline);
}
.sheet-grab {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 18px;
  border: 0; padding: 0;
  background: none;
  cursor: grab;
  touch-action: none;
}
.sheet-grab::after {
  content: "";
  position: absolute;
  left: 7px; top: 50%;
  width: 4px; height: 44px;
  margin-top: -22px;
  border-radius: var(--pill);
  background: var(--hairline-strong);
}
/* ---- touch-action discipline ----
   Each surface declares what the browser may claim, so the script only has to
   arbitrate the one genuinely ambiguous case (a downward pan that starts at
   the top of the content).

   The scrim takes nothing: a drag there is a dismiss gesture, not a pan of
   whatever is behind it. The chrome takes nothing: it is always a sheet drag.
   The content may pan vertically, and `overscroll-behavior: contain` is what
   stops a flick that reaches the end of the list from chaining into the page
   behind -- the second half of the "bleeds through" fix, the first being the
   body scroll lock. */
.sheet-scrim { touch-action: none; }
.sheet-grab, .sheet-head { touch-action: none; }
.sheet-items {
  flex: 1;
  overflow-y: auto;
  margin: 0; padding: 0;
  list-style: none;
  touch-action: pan-y;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* While the sheet itself is moving, the list underneath must hold still --
   two surfaces sliding under one finger reads as a broken control. */
.sheet[data-dragging="true"] .sheet-items { overflow-y: hidden; touch-action: none; }
.sheet-item {
  display: grid;
  grid-template-columns: 40px 1fr auto;
  gap: 14px;
  align-items: center;
  padding: 16px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 14px;
  letter-spacing: -0.016em;
}
/* The close control the design does not show and accessibility still needs.
   Clipped to nothing until it is focused, then it takes its full size and
   appears -- so a keyboard or switch user tabs to a real, visible Close, while
   everyone else sees only the grabber and dismisses by dragging. Never
   display:none, which would take it out of the tab order entirely. */
.sr-only-focusable:not(:focus):not(:focus-visible) {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* :focus as well as :focus-visible. With only the latter, a mouse press would
   drop focus-visible and collapse the button back to 1px between mousedown and
   mouseup -- the click would land on nothing. */
.sheet-close:focus,
.sheet-close:focus-visible {
  position: static;
  width: auto; height: auto;
  margin: 0; overflow: visible;
  clip: auto; clip-path: none;
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* The line an add landed on, when the sheet was already open: the flight has
   nowhere to go (the bag is behind the sheet), so the confirmation happens
   here, where the buyer is looking. */
.sheet-item[data-flash="true"] {
  animation: sheet-item-flash 700ms ease-out;
}
@keyframes sheet-item-flash {
  0%   { background: color-mix(in srgb, var(--blue) 16%, transparent); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .sheet-item[data-flash="true"] { animation: none; background: color-mix(in srgb, var(--blue) 10%, transparent); }
}

.sheet-foot { padding-top: 16px; border-top: 1px solid var(--hairline); display: grid; gap: 12px; }
.icon-btn {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border: 0;
  border-radius: var(--pill);
  background: var(--band);
  color: var(--ink);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: transform 100ms ease-out, opacity 140ms ease-out;
}
.icon-btn:hover { opacity: 0.75; }
.icon-btn:active { transform: scale(0.9); }

/* ---------- 21. Cookie panel -------------------------------------------- */
.cookie-panel {
  position: fixed;
  left: 20px; bottom: 20px;
  z-index: 120;
  width: min(400px, calc(100% - 40px));
  padding: 26px;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--sheet-shadow);
  opacity: 0;
}
.cookie-panel .body-2 { margin-top: 10px; font-size: 14px; line-height: 1.42859; letter-spacing: -0.016em; }
.cookie-options { margin-top: 18px; border-top: 1px solid var(--hairline); }
.cookie-options .check:first-of-type { border-top: 0; }
.cookie-options .fineprint { display: block; margin-top: 2px; }
.cookie-actions { display: grid; justify-items: center; gap: 4px; margin-top: 20px; }
.cookie-actions .btn-blue { width: 100%; }

/* ---------- 22. Reduced motion ------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 180ms !important;
  }
  .btn:active, .qty button:active, .icon-btn:active, .cart-button:active { transform: none; }
  .tile { transform: none !important; }
  .gate-dialog { transform: none !important; }
  .sheet { transition: opacity 200ms ease; }
}


/* =============================================================================
   23. Search field — LAST WORD IN THE CASCADE

   This section is deliberately the final thing in the file. Section 15 defines
   shared form controls (padding, 12px radius, an inset hairline and a solid
   --field fill), and in dark mode that hairline is rgba(255,255,255,0.28) —
   loud enough to read as a second box drawn inside the pill. The container IS
   the field here; the input is only a transparent text layer inside it.

   The five properties that can draw a box on the inner input are forced, so no
   later rule, theme block or media query in any future edit can put the second
   outline back. Everything else stays in the normal cascade.
   ========================================================================== */
.search {
  display: flex;
  align-items: center;
  gap: 8px;
  width: min(100%, 340px);
  height: 40px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--pill);
  background: var(--search-fill);
  box-shadow: none;
  transition: box-shadow 160ms ease-out, background-color 160ms ease-out;
}
/* The wrapper's one and only ring. */
.search:focus-within { box-shadow: 0 0 0 4px var(--focus-ring); }
.search-icon { flex: none; width: 15px; height: 15px; color: var(--ink-2); }

/* The fill flips with the surface underneath, so the field is always visible
   and never the same colour as its ground — but it is still one surface. */
.band-soft .search { background: var(--card); }
.card .search { background: var(--search-fill); }
@media (prefers-color-scheme: dark) {
  /* One translucent fill reads correctly on page, band and card alike. */
  .band-soft .search,
  .card .search { background: var(--search-fill); }
}
@media (prefers-contrast: more) {
  /* Still one box: the wrapper trades its fill for a defined edge. */
  .search { box-shadow: inset 0 0 0 1px var(--hairline-strong); }
  .search:focus-within { box-shadow: inset 0 0 0 2px var(--blue), 0 0 0 4px var(--focus-ring); }
}

/* The inner input paints nothing, in every theme and every state. */
.search input[type="search"],
.search input[type="search"]:hover,
.search input[type="search"]:focus,
.search input[type="search"]:focus-visible,
.search input[type="search"]:active {
  box-shadow: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  background: transparent !important;
  padding: 0 !important;
  outline: none;
  margin: 0;
  flex: 1;
  min-width: 0;
  width: auto;
  height: 100%;
  -webkit-appearance: none;
  appearance: none;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  letter-spacing: -0.016em;
}
.search input[type="search"]::placeholder { color: var(--ink-2); opacity: 1; }
.search input[type="search"]::-webkit-search-cancel-button,
.search input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

/* =============================================================================
   24. CHECKOUT (checkout.html)
   ========================================================================== */
.band-checkout { padding-block: clamp(32px, 5vw, 64px); }

/* ONE centred column, at every width.

   A side-by-side checkout was tried and rejected: it splits attention between
   two columns exactly when a buyer wants a single obvious path. This is that
   path, top to bottom -- what it costs, how you want to pay, where it goes,
   the card, the disclosure, the button -- at a measure that stays comfortable
   on a wide screen instead of stretching to fill it.

   Everything that is not on that path sits below the button. */
.checkout-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--checkout-gap);
  max-width: 640px;
  margin-inline: auto;
}
.checkout-research {
  max-width: 640px;
  margin-inline: auto;
}

/* ---- the rhythm of the checkout column ----
   One gap down the whole column. The code control used to be a card sitting in
   the middle of this, which left a space that belonged to nothing once you
   noticed it; it is part of the summary card now and the rhythm closes up.
   Everything below uses --checkout-gap so the three places that need to agree
   cannot drift apart again. It sits on the root so the reference block below
   the layout inherits it too. */
#checkout-root { --checkout-gap: 16px; }
.checkout-pane {
  display: flex;
  flex-direction: column;
  gap: var(--checkout-gap);
}
.method-switch-wrap { margin-bottom: 0; }
.checkout-research { margin-top: var(--checkout-gap); }

/* Headings inside the card form: one size, one margin, and a clear break
   before the second of them rather than an inline style on the element. */
.checkout-form-card .headline-3 { font-size: 20px; margin: 0; }
.checkout-form-card #address-element + .headline-3 { margin-top: 30px; }
/* The checkout page is nearly empty for its first few hundred milliseconds --
   a "Preparing checkout…" card and nothing else -- so the footer sat in the
   middle of the screen and then slid away as the real content arrived. That is
   the largest layout shift the page records, and it is not Stripe's doing.

   Reserving a page-height's worth of room means the footer starts below the
   fold, where it belongs, and never travels through the viewport. */
#checkout-root { min-height: 78vh; }

/* Space reserved for the Stripe iframes before they exist.

   These are the numbers the mounted elements actually settle at, measured at
   each breakpoint rather than guessed: the address element is 151px at every
   width, the payment element is 379px with room for a tab row and 654px when
   the tabs wrap on a phone. Reserving them is what stops the mount from
   pushing the Pay button and the footer down the page twice while it builds.

   A skeleton fills the space until Stripe reports the element ready, so the
   reservation reads as "loading" rather than as a hole in the form. */
#address-element, #payment-element {
  margin-top: 16px;
  border-radius: 12px;
}
#address-element { min-height: 152px; }
#payment-element { min-height: 380px; }
@media (max-width: 30rem) {
  #payment-element { min-height: 656px; }
}
#address-element:not([data-ready]),
#payment-element:not([data-ready]) {
  background:
    linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--ink) 4%, transparent) 50%, transparent 100%),
    var(--field);
  background-size: 200% 100%, auto;
  animation: checkout-skeleton 1.4s ease-in-out infinite;
}
@keyframes checkout-skeleton {
  0%   { background-position: -100% 0, 0 0; }
  100% { background-position:  100% 0, 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  #address-element:not([data-ready]),
  #payment-element:not([data-ready]) { animation: none; }
}
.checkout-summary { padding: 16px 22px; }
/* The toggle row and the first figure had three separate top margins stacked
   under them -- the card's own child spacing, the body's padding and the
   totals block's margin -- which came to about 54px of nothing. One gap now,
   and the first row's own padding is what makes it. */
.checkout-summary .summary-body { display: none; margin-top: 0; padding-top: 4px; }
.checkout-summary[data-open="true"] .summary-body { display: block; }
.checkout-summary .totals { margin-top: 0; }
.checkout-summary .summary-note { margin-top: 14px; }

/* The amount is repeated where the buyer is actually paying, so it is on
   screen at the moment of the decision as well as at the top of the page. */
.pay-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--hairline);
}
.pay-total-label { font-size: 15px; color: var(--ink-2); letter-spacing: -0.016em; }
.pay-total-value {
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -0.022em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.pay-button { margin-top: 26px; }
.checkout-error { margin-top: 14px; }

/* ---- order summary disclosure ----
   The toggle only exists on a phone; a desktop has room for both columns, so
   the summary is simply open and the button is not rendered to the user. */
.summary-toggle {
  /* Shown at every width: the total is the first thing on the page, and
     opening it is how you see the detail. */
  display: flex;
  width: 100%;
  align-items: center;
  gap: 12px;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  min-height: 44px;
}
/* Never wrapped: with a code chip beside it, "Order summary" was breaking
   across two lines on a phone. The chip is what gives way instead. */
.summary-label { flex: 1 1 auto; white-space: nowrap; font-size: 17px; font-weight: 600; letter-spacing: -0.022em; }
.summary-total { font-size: 17px; letter-spacing: -0.022em; font-variant-numeric: tabular-nums; }
.summary-chev-wrap { display: flex; color: var(--ink-3); }
.summary-chev {
  width: 12px;
  height: 12px;
  transition: transform 260ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
.checkout-summary[data-open="true"] .summary-chev { transform: rotate(180deg); }
.summary-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 6px; }

/* ---- how would you like to pay ----
   These class names used to be .segmented / .segmented-option, shared with the
   catalog layout toggle. That toggle was replaced by the filter pills and its
   CSS went with it, which left this control rendering as two default browser
   buttons. It owns its styling now, so nothing else can take it away. */
.method-switch-wrap { margin-bottom: 20px; }
.method-switch-wrap .headline-3 { margin: 0 0 14px; }
.method-switch {
  display: flex;
  padding: 3px;
  border-radius: var(--pill);
  background: var(--search-fill);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.method-option {
  flex: 1;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  margin: 0;
  padding: 9px 14px;
  min-height: 40px;
  border-radius: var(--pill);
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 14px;
  line-height: 1.28577;
  letter-spacing: -0.016em;
  cursor: pointer;
  white-space: nowrap;
  transition: color 180ms ease-out, background-color 180ms ease-out, box-shadow 180ms ease-out;
}
.method-option:hover { color: var(--ink); }
.method-option[aria-checked="true"] {
  background: var(--seg-thumb);
  color: var(--ink);
  box-shadow: var(--seg-thumb-shadow);
  cursor: default;
}
.method-option:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.method-option:active { transform: scale(0.98); }

/* ---- our own address form (the mail pane) ---- */
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.field-grid .field { display: grid; gap: 6px; grid-column: 1 / -1; }
.field-grid .field-half { grid-column: auto; }
.field-grid label {
  font-size: 13px;
  line-height: 1.33337;
  letter-spacing: -0.008em;
  color: var(--ink-2);
}
.field-grid input {
  width: 100%;
  /* 16px minimum: anything smaller and iOS zooms the page on focus. */
  font: inherit;
  font-size: 16px;
  padding: 12px 14px;
  min-height: 46px;
  border-radius: 10px;
  border: 1px solid var(--hairline);
  background: var(--field);
  color: var(--ink);
}
.field-grid input:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; border-color: transparent; }

/* =============================================================================
   24b. AFFILIATE PROGRAM (affiliates.html)

   A marketing page, built from the same parts as the rest of the site: the
   band rhythm, the card, the pill, the hairline. Nothing here invents a new
   look; it arranges the existing one to sell something.

   Every grid and flex item that holds text carries `min-width: 0`. That is
   not decoration -- a grid item's automatic minimum size is its min-content
   size, and one long unbroken handle or URL in the roster is all it takes to
   push the whole page sideways. There is no overflow-x to hide it.
   ========================================================================== */

.eyebrow {
  margin: 0 0 18px;
  font-size: 15px;
  line-height: 1.33337;
  letter-spacing: -0.012em;
  font-weight: 600;
  color: var(--blue);
}
.headline-4 {
  margin: 0;
  font-size: 19px;
  line-height: 1.21053;
  letter-spacing: 0.012em;
  font-weight: 600;
  color: var(--ink);
}
.btn-lg { padding: 15px 30px; font-size: 19px; }

.affiliate-hero { padding-block: clamp(72px, 10vw, 148px); text-align: center; }
.affiliate-hero .display-lg { max-width: 16ch; margin-inline: auto; }
.affiliate-hero .subhead-lg { max-width: 46ch; margin: 22px auto 0; }
.hero-cta { margin: 34px 0 0; }

/* ---- how it works ---- */
.affiliate-steps { padding-block: clamp(56px, 8vw, 104px); }
.affiliate-steps .headline-2 { text-align: center; }
.step-grid {
  list-style: none;
  margin: clamp(34px, 5vw, 56px) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2.4vw, 26px);
  counter-reset: step;
}
.step-card {
  position: relative;
  min-width: 0;
  padding: clamp(24px, 3vw, 34px);
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--tile-shadow);
}
.step-art { display: block; width: 44px; height: 44px; color: var(--blue); }
.step-art > svg { display: block; width: 100%; height: 100%; }
.step-n {
  position: absolute;
  top: clamp(24px, 3vw, 34px);
  right: clamp(24px, 3vw, 34px);
  font-size: 13px;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}
.step-card .headline-4 { margin-top: 22px; }
.step-card .body-2 { margin-top: 8px; }

/* ---- the three facts ---- */
.fact-row {
  margin: clamp(30px, 4vw, 48px) 0 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2.4vw, 30px);
  padding-top: clamp(26px, 3vw, 38px);
  border-top: 1px solid var(--hairline);
}
.fact { min-width: 0; }
.fact dt {
  font-size: clamp(30px, 3.4vw, 40px);
  line-height: 1.08;
  letter-spacing: -0.004em;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.fact dd {
  margin: 10px 0 0;
  font-size: 15px;
  line-height: 1.4;
  letter-spacing: -0.012em;
  color: var(--ink-2);
}

/* ---- the rules ---- */
.affiliate-rules { padding-block: clamp(56px, 8vw, 104px); }
.rules-head { max-width: var(--measure-read); }
.rules-head .body-1 { margin-top: 18px; }
.rule-list {
  list-style: none;
  margin: clamp(32px, 4.4vw, 52px) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 2.6vw, 32px);
}
.rule {
  min-width: 0;
  padding-top: 20px;
  border-top: 1px solid var(--hairline);
}
.rule .body-2 { margin-top: 8px; }

/* ---- the form ---- */
.affiliate-apply { padding-block: clamp(56px, 8vw, 104px); scroll-margin-top: 90px; }
.affiliate-card { margin-top: clamp(26px, 3vw, 38px); }
.affiliate-card .field-grid { margin-top: 4px; }
/* select and textarea were never styled: only `input` was, because the mail
   pane is all inputs. Without these two the form is half Apple and half 1998.

   Two things here are load-bearing, and both were got wrong the first time.

   `background-color`, not the `background` shorthand: the shorthand resets
   background-image, and the chevron below is a background image.

   And NO `#affiliate-form select` in this list. It was here, and an id
   outranks a class no matter what order the rules are in -- so this block's
   reset beat the chevron rule below it and the dropdowns shipped with no
   chevron and no room for one. Both selects live in .field-grid, so the class
   selector already covers them; the id only ever added a way to lose. */
.field-grid select,
#affiliate-form textarea {
  width: 100%;
  font: inherit;
  font-size: 16px;                 /* 16px or iOS zooms the page on focus */
  padding: 12px 14px;
  min-height: 46px;
  border-radius: 10px;
  border: 1px solid var(--hairline);
  background-color: var(--field);
  color: var(--ink);
}
/* A focused field must not end up under the iOS keyboard. The browser scrolls
   focus into view; this tells it to stop short of the sticky nav and leave
   the label visible with it. */
#affiliate-form input,
#affiliate-form select,
#affiliate-form textarea { scroll-margin-block: 120px 160px; }
#affiliate-form textarea { min-height: 112px; resize: vertical; line-height: 1.45; }
.field-grid select { appearance: none; -webkit-appearance: none; padding-right: 38px;
  /* The chevron is a background image so the control keeps the site's shape
     rather than the platform's. currentColor cannot be used in a data URI, so
     the two themes get one each below. */
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 11px 7px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 7'%3E%3Cpath d='M1 1l4.5 4.5L10 1' fill='none' stroke='%236e6e73' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.field-grid select:focus-visible,
#affiliate-form textarea:focus-visible,
#affiliate-form select:focus-visible {
  outline: 2px solid var(--blue); outline-offset: 1px; border-color: transparent;
}
/* The generic .field carries `margin-top: 22px` for forms that are a plain
   column of fields. Inside a grid that margin is added to the grid's own gap,
   which is where the form's 34px of accidental air came from. The gap is the
   only thing that should be setting this spacing. */
.field-grid .field { margin-top: 0; }
.consent-boxes { margin-top: 26px; }
#affiliate-form > .field { margin-top: 22px; }
#affiliate-form .btn-block { margin-top: 26px; }
/* Errors render under the button and are reserved nothing, because nothing
   above them moves -- but they must be legible, not a grey whisper. */
#affiliate-form .checkout-error { font-size: 14px; line-height: 1.43; }

/* The honeypot. Off-screen rather than display:none -- some bots skip what is
   not rendered, and all of them fill in what they can find. Nothing here is
   reachable by keyboard, pointer or screen reader. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.affiliate-done { text-align: center; padding: clamp(18px, 3vw, 34px) 0; }
.done-check {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 22px;
  border-radius: 50%;
  background: var(--blue);
  color: var(--on-blue);
}
.done-check > svg { width: 28px; height: 28px; }
.affiliate-done .body-1 { max-width: 34ch; margin-inline: auto; }

@media (prefers-color-scheme: dark) {
  .field-grid select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 7'%3E%3Cpath d='M1 1l4.5 4.5L10 1' fill='none' stroke='%2386868b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

@media (max-width: 56rem) {
  .step-grid { grid-template-columns: 1fr; }
  .fact-row { grid-template-columns: 1fr; gap: 22px; }
  .rule-list { grid-template-columns: 1fr; gap: 18px; }
}

/* ---- the phone pass -----------------------------------------------------
   Everything below is this page at 375 and 440, looked at section by section.
   Nothing here is a new idea; it is the same page with the air taken out of
   the places that had too much and put into the places that had none. */
@media (max-width: 40rem) {
  /* HERO. "Earn 10% of every sale." broke as "Earn 10% of every / sale." --
     a one-word last line, which is the worst break available in a four-word
     headline. `balance` evens the lines out instead. A browser that does not
     know it drops this one declaration and wraps as before, which is exactly
     what it did yesterday. */
  .affiliate-hero .display-lg,
  .affiliate-hero .subhead-lg { text-wrap: balance; }
  .affiliate-hero { padding-block: clamp(52px, 13vw, 84px); }
  .affiliate-hero .display-lg { max-width: 14ch; }
  .affiliate-hero .subhead-lg { max-width: 30ch; margin-top: 18px; font-size: 19px; }
  .hero-cta { margin-top: 30px; }

  /* STEPS. The art sat alone on a line with 22px under it and the numeral
     floated in the far corner, so each card read as three unrelated things.
     One row: the mark and its number on the left, the words on the right. */
  .step-grid { gap: 12px; }
  .step-card {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr);
    column-gap: 16px;
    row-gap: 0;
    align-items: start;
    padding: 20px;
  }
  /* The mark sits on the title's line, not above the whole block: a 34px icon
     with three lines of text beside it looks adrift if it is aligned to the
     top of the text rather than to the thing it labels. */
  .step-art { grid-column: 1; grid-row: 1; width: 34px; height: 34px; align-self: center; }
  /* The numeral stays where it is at every other width -- top right, quiet.
     It was tried in the left column under the mark, and the body text is
     taller than the mark, so it came to rest at the BOTTOM of the card with
     nothing near it. An ordinal belongs beside the thing it numbers. */
  .step-n { top: 20px; right: 20px; }
  .step-card .headline-4 {
    grid-column: 2; grid-row: 1;
    margin-top: 0; font-size: 18px;
    align-self: center;
    padding-right: 20px;          /* clears the numeral */
  }
  .step-card .body-2 { grid-column: 2; grid-row: 2; margin-top: 8px; font-size: 15px; }

  /* FACTS. Three display figures stacked at desktop size took most of a
     screen and floated with nothing tying them together. Hairlines make them
     a list; the smaller figure keeps all three on one screen. */
  .fact-row { gap: 0; padding-top: 22px; }
  .fact { padding: 16px 0; border-top: 1px solid var(--hairline); }
  .fact:first-child { border-top: 0; padding-top: 0; }
  .fact dt { font-size: 28px; }
  .fact dd { margin-top: 6px; font-size: 14px; }

  /* RULES. This is the secondary section and has to read like one. The bodies
     were at 17px, the same size as the page's primary copy, which made the
     compliance block the longest thing on the phone. */
  .affiliate-rules { padding-block: clamp(44px, 11vw, 76px); }
  .rules-head .body-1 { margin-top: 14px; font-size: 15px; }
  .rule-list { gap: 0; margin-top: 26px; }
  .rule { padding: 16px 0; }
  .rule .headline-4 { font-size: 17px; }
  .rule .body-2 { margin-top: 5px; font-size: 15px; }

  /* FORM. One column, label above, and the gap is the only thing setting the
     rhythm between fields. */
  .affiliate-apply { padding-block: clamp(44px, 11vw, 76px); }
  .affiliate-card .field-grid { gap: 18px; }
  .affiliate-card { padding: 22px 20px; }
  #affiliate-form .check { padding: 14px 0; }
}

/* The page's last band sits above the footer; on a phone in landscape, or any
   device with a home indicator, its bottom padding has to clear the inset. */
.affiliate-apply { padding-bottom: calc(clamp(44px, 11vw, 76px) + env(safe-area-inset-bottom, 0px)); }

/* =============================================================================
   25. MOBILE

   Everything below is phone and tablet work. The rules are grouped by what
   they fix rather than by component, because most of them exist to satisfy one
   of four goals: nothing overflows 375px, every target is at least 44px, iOS
   never zooms a focused field, and sheets respect the home indicator.
   ========================================================================== */

/* ---- 25.1 Never scroll sideways ----------------------------------------
   Deliberately NO `overflow-x: hidden` on html/body. Setting it on the root
   makes the body the scroll container, which silently breaks window scroll
   position (so scroll-linked motion stops working), interferes with
   position: sticky, and — worst of all — hides real overflow instead of
   fixing it. The rules below remove the causes; the audit then has to pass
   honestly. */
html, body { max-width: 100%; }
/* Long unbroken strings (formulae, lot ids, emails) are the usual culprit. */
.specs dd, .legal-body p, .legal-body li, .tile-meta, .footer-col address,
.line-item-name, .totals-row span:first-child, .mono { overflow-wrap: anywhere; }
/* Tables stay scrollable inside their own region, never pushing the page. */
.table-scroll { max-width: 100%; }

/* ---- 25.2 Touch targets: 44px minimum ---------------------------------- */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  .btn-sm { min-height: 44px; padding-block: 12px; }
  .nav-links a { min-height: 44px; display: flex; align-items: center; }
  .cart-button { min-height: 44px; padding-inline: 10px; }
  .qty button { width: 44px; height: 44px; }
  .icon-btn { width: 44px; height: 44px; }
  .btn-text, .btn-link { min-height: 44px; display: inline-flex; align-items: center; }
  .legal-nav a { min-height: 44px; display: inline-flex; align-items: center; }
  /* The 44px floor in the coarse-pointer block above is the rule now. */
  .footer-list a { display: inline-flex; align-items: center; }
  .chev { min-height: 44px; display: inline-flex; align-items: center; }
  /* The tick box itself stays 22px, but its row is a comfortable target. */
  .check { padding-block: 14px; min-height: 44px; }
}

/* Remove the grey flash on tap; we draw our own press states. */
a, button, input, select, textarea, label, .tile, summary {
  -webkit-tap-highlight-color: transparent;
}

/* ---- 25.3 iOS must not zoom when a field is focused --------------------
   Safari zooms any input whose computed font-size is under 16px. Every field
   is therefore >= 16px on touch devices, including the search field, which is
   14px on the desktop layout. */
@media (pointer: coarse) {
  input[type="text"], input[type="search"], select, textarea,
  .search input[type="search"] { font-size: 16px; }
}

/* ---- 25.4 Nav: wordmark left, bag right, menu button ------------------- */
.nav-burger {
  display: none;
  width: 44px; height: 44px;
  margin-right: -10px;
  align-items: center;
  justify-content: center;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
}
.burger-bars { position: relative; display: block; width: 18px; height: 12px; }
.burger-bars i {
  position: absolute;
  left: 0;
  width: 18px; height: 1.5px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 260ms cubic-bezier(0.4, 0, 0.2, 1), opacity 160ms linear;
}
.burger-bars i:first-child { top: 2px; }
.burger-bars i:last-child { bottom: 2px; }
.nav-burger.is-open .burger-bars i:first-child { transform: translateY(4.25px) rotate(45deg); }
.nav-burger.is-open .burger-bars i:last-child { transform: translateY(-4.25px) rotate(-45deg); }

@media (max-width: 47.99rem) {
  .nav-links { display: none; }
  .nav-cart { margin-left: auto; }
  .nav-burger { display: flex; }
  .brand span { display: inline; }
}

/* Full-screen slide-down menu */
.mobile-menu-root {
  position: fixed;
  inset: 0;
  z-index: 55;           /* under the nav, so the close button stays reachable */
  display: none;
}
.mobile-menu-root[data-open="true"] { display: block; }
.mobile-menu {
  position: absolute;
  inset: 0;
  /* padding-top is set at open time from the nav's measured bottom (app.js);
     this is the fallback before that runs. */
  padding: calc(48px + env(safe-area-inset-top, 0px) + 24px) var(--gutter)
           calc(24px + env(safe-area-inset-bottom, 0px));
  background: var(--page);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.mobile-menu-list { list-style: none; margin: 0; padding: 0; }
.mobile-menu-list li { border-bottom: 1px solid var(--hairline); }
.mobile-menu-list a {
  display: flex;
  align-items: center;
  min-height: 64px;
  font-size: 28px;
  line-height: 1.14;
  letter-spacing: 0.007em;
  font-weight: 600;
  color: var(--ink);
}
.mobile-menu-list a:hover { text-decoration: none; opacity: 0.7; }
body.menu-open { overflow: hidden; }
@media (min-width: 48rem) {
  .mobile-menu-root { display: none !important; }
}

/* ---- 25.5 Phone layout ------------------------------------------------- */
@media (max-width: 30rem) {
  :root { --gutter: 20px; }

  /* Display type: tighter clamp so the headline never leaves an orphan. */
  .headline-1 { font-size: clamp(34px, 9.2vw, 44px); line-height: 1.07; }
  .headline-2 { font-size: clamp(28px, 7.6vw, 34px); }
  .subhead-lg { font-size: 19px; line-height: 1.26; }
  .subhead { font-size: 17px; line-height: 1.4; }
  /* The spotlight hero sets its own padding from the viewport it has to fit
     inside; a fixed 36/56 here would push the vial off a small phone. Other
     heroes (the inner pages) still want the generous block. */
  .hero:not(.hero-spotlight) { padding-block: 36px 56px; }
  .hero .headline-1 { max-width: 14ch; }

  /* 2 x 2 comes from the base grid; just tighten it for the phone. */
  .stats { gap: 30px 18px; }
  .stat { max-width: none; }
  .stat-value { font-size: 26px; }
  .stat-label { font-size: 13px; }

  .band { padding-block: 56px; }
  .band-tight { padding-block: 36px; }
  .section-head { margin-bottom: 28px; }

  /* Catalog goes single column; the tile art shrinks with it. */
  .grid-tiles { grid-template-columns: 1fr; gap: 16px; }
  .tile { padding: 32px 20px 26px; }
  .tile-art { width: 132px; }

  /* Search bar and count go one above the other rather than squeeze. */
  .catalog-bar { flex-direction: column; align-items: stretch; gap: 10px; }
  /* Taps, not clicks: the strength numbers get a full 44px target. */
  .variant-option { min-height: 34px; padding: 7px 12px; font-size: 13px; }
  .variant-picker-lg .variant-option { min-height: 44px; }
  .search { width: 100%; }
  .catalog-count { text-align: center; }

  /* The pill row scrolls sideways instead of wrapping. Eight categories plus
     All would otherwise wrap four rows deep on a phone and push the grid off
     the screen -- which is the thing this row exists to avoid. The fade masks
     are the only hint that there is more to the right, so they matter. */
  .catalog-filters {
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
  }
  .catalog-filters::-webkit-scrollbar { display: none; }
  .filter-pills {
    flex-wrap: nowrap;
    justify-content: flex-start;
    width: max-content;
    min-width: 100%;
  }
  .filter-pill { scroll-snap-align: center; }

  /* Cards, summaries and tables become single column. */
  .card { padding: 22px 18px; border-radius: 14px; }
  .cart-layout, .checkout-layout { grid-template-columns: 1fr; }

  /* ---- checkout on a phone ----
     Collapsed summary, then how to pay, then the form, then Pay, then the
     reference text. The summary used to sit open above everything and put the
     Pay button roughly four screens down. */
  .checkout-summary { padding: 14px 18px; }

  /* The page title on a checkout should not cost a screen and a half. */
  .band-checkout { padding-block: 20px 32px; }
  .band-checkout .section-head { margin-bottom: 20px; }
  .band-checkout .breadcrumb { margin-bottom: 10px; font-size: 12px; }
  .band-checkout .headline-2 { font-size: 26px; }

  .method-switch-wrap .headline-3 { font-size: 17px; margin-bottom: 12px; }
  .method-option { padding: 11px 10px; min-height: 44px; font-size: 13px; white-space: normal; }

  /* One column of fields; the half-width pairs are cramped at this size. */
  .field-grid { grid-template-columns: 1fr; gap: 12px; }
  .field-grid .field-half { grid-column: 1 / -1; }
  .totals-row { font-size: 15px; gap: 12px; }
  .totals-row span:last-child { white-space: nowrap; }
  .specs-row { grid-template-columns: 1fr; gap: 2px; padding: 16px 0; }
  .specs-wrap { margin-top: 28px; }

  /* Product page */
  .product-art { max-width: 208px; }
  .product-hero { padding-top: 20px; }
  .buy-row { gap: 12px; }
  .buy-row .btn { flex: 1 1 100%; }

  /* Legal pages: full width minus the gutter, body type stays 17px. */
  .legal-layout { gap: 28px; }
  .legal-body { max-width: none; }
  .legal-body p, .legal-body li { font-size: 17px; }
  /* The horizontal policy scroller is defined in section 15 and owns this
     now; the old wrapping row it replaced used to live here. */

  /* Footer columns single-file */
  .footer-base { flex-direction: column; align-items: flex-start; gap: 10px; }
}

/* Tablet: two-up catalog. */
@media (min-width: 30.01rem) and (max-width: 62rem) {
  .grid-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- 25.6 Entry gate becomes a full-screen sheet on phones -------------
   It used to centre everything in one non-scrolling column, which meant the
   whole gate had to fit one screen: six lines of legal intro at full body
   size, checkbox rows pressed against their hairlines, the fine print stacked
   straight under the buttons, and nothing to separate one group from the next.

   It is a proper sheet now. The body scrolls, so nothing has to cram; the
   Continue button lives in a translucent footer pinned to the bottom, so the
   thing the visitor came to press is always in reach however long the legal
   text runs. */
@media (max-width: 30rem) {
  .gate-root { padding: 0; place-items: stretch; overflow: hidden; }
  .gate-dialog {
    max-width: none;
    width: 100%;
    /* A hair of scrim above it, so the rounded corners actually read as a
       sheet edge instead of being clipped flat by the screen. */
    height: calc(100% - 10px);
    margin: 10px 0 0;
    padding: 0;
    /* Rounded top corners and a grabber: the shape says "sheet". It is not
       swipe-dismissible and must not be -- this gate is required. */
    border-radius: 18px 18px 0 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .gate-grabber {
    display: block;
    flex: 0 0 auto;
    width: 36px;
    height: 5px;
    margin: 8px auto 0;
    border-radius: 999px;
    background: var(--hairline);
  }

  #gate-form {
    display: flex;
    flex-direction: column;
    min-height: 0;          /* or the scroll area cannot shrink */
    flex: 1 1 auto;
  }
  .gate-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: calc(18px + env(safe-area-inset-top, 0px)) 20px 24px;
  }

  /* Headroom for the title, then a clear step down to the intro. */
  .sheet-title { font-size: 24px; margin-top: 6px; }
  .gate-body { font-size: 15px; max-width: 38ch; }

  /* The controls are the focus: bigger gaps between groups, and rows tall
     enough that the hairlines are separators rather than a cage. */
  .gate-scroll > .field { margin-top: 26px; }
  .gate-checks { margin-top: 26px; }
  .gate-checks .check { padding-block: 16px; }
  .gate-version { margin-top: 28px; padding-top: 18px; }

  /* Sticky footer. Translucent over the scrolling content, with a hairline to
     separate it and the home-indicator inset respected. */
  .gate-actions {
    flex: 0 0 auto;
    margin-top: 0;
    padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--hairline);
    background: color-mix(in srgb, var(--card) 88%, transparent);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    gap: 2px;
  }
  /* Where the blur cannot be done, the bar stays opaque rather than letting
     text scroll under it illegibly. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .gate-actions { background: var(--card); }
  }
  .gate-actions .btn-link { min-height: 44px; }
}

/* ---- 25.7 Cookie panel and cart sheet become bottom sheets ------------- */
@media (max-width: 30rem) {
  .cookie-panel {
    left: 0; right: 0; bottom: 0;
    width: 100%;
    border-radius: 18px 18px 0 0;
    padding: 24px 20px calc(24px + env(safe-area-inset-bottom, 0px));
    /* Same treatment as the gate, lighter touch: the explanation steps back to
       secondary and the option rows get room to breathe. Nothing here scrolls
       or sticks -- the panel is short enough that it does not need to. */
    max-height: 86dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .cookie-panel .body-2 { font-size: 14px; line-height: 1.5; color: var(--ink-2); }
  .cookie-options { margin-top: 22px; }
  .cookie-options .check { padding-block: 16px; }
  .cookie-actions { gap: 8px; margin-top: 24px; }

  .sheet {
    top: auto;
    left: 0; right: 0; bottom: 0;
    width: 100%;
    max-height: 88vh;
    max-height: 88dvh;
    border-radius: 18px 18px 0 0;
    padding: 18px 20px calc(20px + env(safe-area-inset-bottom, 0px));
    /* Enters from the bottom, so it must leave the same way. The drag handle
       is repositioned to match (see below); app.js drives the axis. */
  }
  .sheet-grab {
    left: 0; right: 0; top: 0; bottom: auto;
    width: auto; height: 26px;
    cursor: grab;
  }
  .sheet-grab::after {
    left: 50%; top: 9px;
    width: 40px; height: 4px;
    margin: 0 0 0 -20px;
  }
  .sheet-head { padding-top: 10px; }
}

/* ---- 25.8 Checkout on a phone ----------------------------------------- */
@media (max-width: 30rem) {
  .band-checkout { padding-block: 24px 32px; }
  /* Three things across one narrow row: tighten the gaps and the chip so the
     heading and the total both keep their own line. */
  .summary-toggle { gap: 8px; }
  .summary-code-chip { font-size: 10px; padding: 2px 6px; max-width: 14ch; }
  /* Summary first: people want to see what they are paying before paying. */
  .checkout-summary { order: -1; }
  .pay-button { margin-top: 22px; }
}

/* ---- 25.9 Respect the accessibility settings in all the new UI --------- */
@media (prefers-reduced-motion: reduce) {
  .burger-bars i { transition-duration: 0.01ms !important; }
  .mobile-menu { transition: opacity 180ms ease; }
  .mobile-menu, .mobile-menu-list li { transform: none !important; }
}
@media (prefers-contrast: more) {
  .mobile-menu { background: var(--page); }
  .mobile-menu-list li { border-bottom-color: var(--hairline-strong); }
}


/* ---------- 25b. Add-to-cart confirmation --------------------------------
   Three layers: the button morphs to a check (always), the bag badge pulses
   (always), and a dot flies between them (when motion is welcome and the
   target is actually visible). Transform and opacity only. */
/* One state, every surface: a centred check inside the button's own pinned
   box. `overflow: hidden` is the backstop -- whatever the content, nothing can
   spill past the pill's rounded edge and make the button look like it grew,
   which is exactly how the previous "✓ Added" version failed on a narrow
   tile. Padding is dropped because the box is already the right size. */
.btn.is-added {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  overflow: hidden;
}
.add-check { width: 17px; height: 17px; flex: 0 0 auto; }

/* Refused: the add could not happen, so there is no check and no claim that
   anything was added. */
.btn.is-refused { background: var(--red, #b3261e); }

/* The dot. Fixed so it is positioned against the viewport the buttons and the
   bag are measured in, and out of flow so it can never shift the layout. */
.fly-dot {
  position: fixed;
  left: 0; top: 0;
  z-index: 190;                 /* under the sheet (200), over the page */
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--blue);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.22);
  pointer-events: none;
  will-change: transform, opacity;
  contain: strict;              /* it cannot affect anything else's layout */
}

@media (prefers-reduced-motion: reduce) {
  /* No flight and no pulse scale; the check still appears, instantly. */
  .fly-dot { display: none; }
  .add-check path { stroke-dashoffset: 0 !important; }
}

/* =============================================================================
   26. TOUCH TARGETS — LAST WORD IN THE CASCADE

   This block is last on purpose. Several older rules in section 25 set their
   own smaller floors (40px here, 34px there) from before there was a single
   standard, and wherever they came after these they quietly won. Rather than
   hunt each one down and risk the next edit reintroducing it, the floor lives
   at the end of the file where nothing can undercut it.
   ========================================================================= */

/* ---- Touch targets -------------------------------------------------
   A finger needs about 44px; several controls here are deliberately small
   because they should look small -- page dots are 8px on purpose. The answer
   is to grow the HIT AREA without growing the thing you see, which is what the
   ::after overlays below do. Everything else simply gets a floor.

   Scoped to coarse pointers so a mouse still gets the compact design. */
@media (pointer: coarse) {
  /* Dots: 8px of ink, 44px of target, centred on the ink. */
  .hero-dot, .rail-dot { position: relative; }
  .hero-dot::after, .rail-dot::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
  /* The dot rows keep their visual spacing; only the targets overlap, and a
     tap between two dots resolves to whichever centre is nearer. */
  .hero-dots, .rail-dots { gap: 14px; }

  .hero-play { width: 44px; height: 44px; }

  /* Strength pills on tiles and the product page. */
  .variant-option { min-height: 44px; }
  .variant-picker { gap: 8px; }

  /* The search field and any other bare input. */
  .search input, input[type="search"], input[type="text"], input[type="email"],
  input[type="tel"], input[type="number"], input[type="password"], select {
    min-height: 44px;
  }

  /* Footer and nav links: the text stays where it is, the box grows around it. */
  .footer-list a, .footer-base-list a, .footer-base-list button, .legal-nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .footer-list li, .footer-base-list li { display: flex; }
  .brand { display: inline-flex; align-items: center; min-height: 44px; }

  /* Quantity steppers, remove buttons, dismiss buttons. */
  .qty button, .btn-text, .suggest-dismiss, .filter-pill, .method-option { min-height: 44px; }

  /* Text-styled buttons — "Leave this site" on the gate, and its kin. These
     were 37px on a touch tablet, which is under the floor everywhere else on
     the site. */
  .btn-link { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; }

  /* The code control. The field and Apply take a real height; the remove ×
     stays 20px of ink in a row of figures and grows its target underneath,
     the same trick the page dots use. */
  /* .code-applied must carry the same floor as .code-toggle or the swap
     between them changes the card's height, which is the one thing it exists
     not to do. */
  .code-toggle, .code-applied, .code-apply, .code-row input { min-height: 44px; }
  .code-apply { height: 44px; }
  .code-row input { height: 44px; }
  .code-remove::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }

  /* Anything using the chevron link style. */
  .chev { display: inline-flex; align-items: center; min-height: 44px; }
  .link-row { gap: 4px 28px; }

  /* The mailto: and tel: links in the footer address each sit on their own
     line, so they can take a real target without disturbing the block. */
  .footer-col address a { display: inline-flex; align-items: center; min-height: 44px; }

  /* Only visible when focused, but that is exactly when it is being used. */
  .skip-link { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ==========================================================================
   24. Research library
   --------------------------------------------------------------------------
   Long-form reading, in the same design system as everything else. The only
   new idea here is the measure: body copy is set to ~68 characters at 17px
   with 1.6 line-height, which is a reading column rather than the 980px
   content column the rest of the site uses.

   Mobile-first. The contents list is a disclosure on a phone and a sticky rail
   from 64rem up, which is the width at which a rail stops stealing from the
   measure. Every interactive target clears 44px, as everywhere else.

   Nothing in here is commerce: there is no price, no button that adds to a
   cart, and no product image. That separation is enforced in the build, not
   by remembering it here.
   ====================================================================== */

.rl { display: block; }

/* ---------- head ----------
   .headline-1 is the PRODUCT hero size: one or two words, centred, meant to
   dominate a first screen. An article title is a sentence, and at that size it
   wrapped to five lines and pushed the standfirst and the first section below
   the fold. Reset it here to an article scale — still the largest thing on the
   page, but a heading rather than a billboard. */
.rl-head .breadcrumb { justify-content: flex-start; }
.rl-title {
  text-align: left;
  font-size: clamp(30px, 5.2vw, 46px);
  line-height: 1.08;
  letter-spacing: -0.025em;
  max-width: 20ch;
}
.rl-standfirst {
  max-width: 68ch;
  margin-inline: 0;
  margin-top: 14px;
  text-align: left;
  font-size: clamp(16px, 2vw, 19px);
  line-height: 1.5;
}
/* The head band is a title and a byline, not a hero: it does not need the
   standard band's 72-130px of vertical padding. */
.rl-head { padding-block: clamp(28px, 4vw, 52px); }

.rl-byline {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 18px 0 0;
  font-size: 13px;
  color: var(--ink-2);
}
.rl-byline-dates { display: flex; flex-wrap: wrap; gap: 4px 18px; }
.rl-byline a { color: var(--link); }
.rl-hubs { margin: 10px 0 0; font-size: 13px; color: var(--ink-2); }
.rl-credentials { margin: 8px 0 0; font-size: 14px; color: var(--ink-2); }

/* The draft marker. Quiet, but impossible to mistake for body copy. */
.rl-ribbon {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 20px;
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  background: rgba(176, 141, 74, 0.12);
  border: 1px solid var(--gold);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.rl-ribbon-note { font-weight: 400; color: var(--ink-2); }

/* ---------- layout ---------- */
.rl-body-band { padding-block: clamp(36px, 5vw, 64px); }

.rl-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 28px;
  align-items: start;
}

.rl-body {
  /* min-width:0 because this is a grid item holding arbitrary content,
     including a 39-character monospace sequence. Without it the grid refuses
     to go below that string's min-content and the page scrolls sideways at
     320px — the exact failure scripts/overflow.js exists to catch. */
  min-width: 0;
  max-width: 68ch;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink);
}

.rl-body p { margin: 0 0 1.05em; }
.rl-body p:last-child { margin-bottom: 0; }

.rl-section { margin: 0 0 clamp(36px, 5vw, 56px); scroll-margin-top: 110px; }
.rl-section:last-child { margin-bottom: 0; }

.rl-h2 {
  margin: 0 0 14px;
  font-size: clamp(24px, 3.4vw, 30px);
  line-height: 1.16;
  letter-spacing: -0.02em;
  font-weight: 600;
}
.rl-h3 {
  margin: 30px 0 10px;
  font-size: clamp(18px, 2.2vw, 20px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  font-weight: 600;
  scroll-margin-top: 110px;
}
.rl-h4 { margin: 22px 0 6px; font-size: 16px; font-weight: 600; }

.rl-list { margin: 0 0 1.05em; padding-left: 1.3em; }
.rl-list li { margin: 0 0 0.45em; }

.rl-note { margin: 14px 0 0; font-size: 15px; color: var(--ink-2); }
.rl-fineprint { margin: 14px 0 0; }
.rl-unknown { color: var(--ink-3); font-style: italic; }
.rl-placeholder {
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(176, 141, 74, 0.16);
  color: var(--gold-ink);
  font-weight: 600;
}
.rl-stamp { margin: 14px 0 0; font-size: 13px; color: var(--ink-2); }

.rl-specs { margin-top: 4px; }
/* The sequence is one 39-character word. Let it wrap anywhere rather than
   drag the column past the viewport. */
.rl-specs .seq { word-break: break-all; }

/* ---------- evidence tier chips ---------- */
.rl-evidence { margin: 0 0 16px; }
.rl-evidence-chip {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 3px 11px;
  border-radius: var(--pill);
  border: 1px solid var(--hairline-strong);
  background: var(--band);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--ink-2);
}
.band .rl-evidence-chip { background: var(--band); }
[data-evidence="human-clinical"] .rl-evidence-chip { border-color: var(--gold); color: var(--gold-ink); }

/* ---------- citations ---------- */
.rl-cites { font-size: 0.72em; line-height: 0; white-space: nowrap; }
.rl-cite {
  color: var(--link);
  text-decoration: none;
  padding: 0 1px;
}
.rl-cite:hover { text-decoration: underline; }

/* ---------- references ---------- */
.rl-refs {
  margin: 0;
  padding-left: 1.5em;
  font-size: 15px;
  line-height: 1.55;
}
.rl-ref { margin: 0 0 0.85em; }
.rl-ref-authors { color: var(--ink-2); }
.rl-ref-title { }
.rl-ref-journal { font-style: italic; color: var(--ink-2); }
.rl-ref-year { color: var(--ink-2); }
/* min-width:0 because this is a flex container holding a DOI, and a flex item
   defaults to min-width:auto — i.e. min-content. A DOI is one unbreakable
   token, so without this the reference list refuses to go below the width of
   its longest identifier and the whole page scrolls sideways. That is exactly
   the failure scripts/overflow.js exists to catch, and it caught this one at
   320px the moment Wave 2 added entries with longer DOIs. */
.rl-ref-links { display: inline-flex; flex-wrap: wrap; gap: 4px 12px; min-width: 0; }

.rl-ext {
  display: inline;
  color: var(--link);
  /* NOT nowrap. A DOI has no spaces, so nowrap made its min-content the whole
     identifier; `anywhere` lets it break mid-token at narrow widths. The icon
     may then wrap to its own line on a 320px screen, which is a cosmetic cost
     worth paying to keep the page from scrolling sideways. */
  overflow-wrap: anywhere;
  word-break: break-word;
  min-width: 0;
}
.rl-ext-icon {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  display: inline-block;
  vertical-align: -1px;
  margin-left: 3px;
}

/* ---------- FAQ ---------- */
.rl-faq { border-top: 1px solid var(--hairline); }
.rl-faq-item { border-bottom: 1px solid var(--hairline); }
.rl-faq-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 44px;
  padding: 12px 0;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.rl-faq-item > summary::-webkit-details-marker { display: none; }
.rl-faq-item > summary::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-right: 1.6px solid var(--ink-3);
  border-bottom: 1.6px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 180ms ease;
}
.rl-faq-item[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.rl-faq-a { padding: 0 0 16px; font-size: 16px; line-height: 1.6; color: var(--ink-2); }

/* ---------- contents list ---------- */
.rl-toc { min-width: 0; }
.rl-toc-details {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--card);
}
.rl-toc-details > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.rl-toc-details > summary::-webkit-details-marker { display: none; }
.rl-toc-details > summary::after {
  content: "";
  width: 8px; height: 8px;
  border-right: 1.6px solid var(--ink-3);
  border-bottom: 1.6px solid var(--ink-3);
  transform: rotate(45deg) translateY(-2px);
  transition: transform 180ms ease;
}
.rl-toc-details[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }

.rl-toc-list {
  margin: 0;
  padding: 0 16px 14px 32px;
  font-size: 14px;
  line-height: 1.45;
}
.rl-toc-list li { margin: 0 0 2px; }
.rl-toc-list a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  color: var(--ink-2);
}
.rl-toc-list a:hover { color: var(--ink); }
.rl-toc-list a[aria-current="true"] { color: var(--ink); font-weight: 600; }

/* ---------- index and hubs ---------- */
.rl-index-list, .rl-hub-grid, .rl-hub-entries, .rl-related-list {
  margin: 0; padding: 0; list-style: none;
}
.rl-index-item { padding: 18px 0; border-bottom: 1px solid var(--hairline); }
.rl-index-item h3 { margin: 0 0 4px; font-size: 19px; font-weight: 600; }
.rl-index-item p { margin: 0; font-size: 15px; color: var(--ink-2); }
.rl-index-draft { margin-top: 6px !important; color: var(--gold-ink) !important; font-weight: 600; }

.rl-hub-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin-top: 8px; }
.rl-hub-card {
  min-width: 0;
  padding: 20px;
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: var(--tile-shadow);
}
.rl-hub-card h3 { margin: 0 0 6px; font-size: 17px; font-weight: 600; }
.rl-hub-blurb { margin: 0 0 8px; font-size: 14px; line-height: 1.5; color: var(--ink-2); }
.rl-hub-count { margin: 0; font-size: 12px; color: var(--ink-3); }

.rl-hub-entries { margin-top: 18px; }
.rl-hub-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  min-width: 0;
  padding: 11px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 16px;
}
.rl-hub-entry > a { min-height: 34px; display: inline-flex; align-items: center; }
.rl-hub-def { font-size: 14px; color: var(--ink-2); min-width: 0; }

/* A compound with no page yet: text, never a link. */
.rl-future { color: var(--ink-2); }
.rl-hub-declined .rl-soon { color: var(--gold-ink); }
.rl-soon {
  font-size: 11px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.rl-related { margin-top: clamp(36px, 5vw, 56px); }
.rl-related-list { margin-top: 12px; }
.rl-related-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  padding: 9px 0; border-bottom: 1px solid var(--hairline); font-size: 16px;
}
.rl-related-list a { min-height: 34px; display: inline-flex; align-items: center; }

.rl-reagent { margin: 24px 0 0; }

/* ---------- comparison pages ---------- */
/* The table is the one element allowed to be wider than the reading measure,
   so it gets its own scroll container. Everything else on the page must stay
   inside the column — see scripts/overflow.js, which is why this is a wrapper
   rather than an overflow rule on the table itself. */
.rl-compare-wrap {
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 0 6px;
}
.rl-compare {
  width: 100%;
  min-width: 460px;
  border-collapse: collapse;
  font-size: 15px;
  line-height: 1.45;
}
.rl-compare th, .rl-compare td {
  padding: 10px 12px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--hairline);
}
.rl-compare thead th {
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.01em;
  color: var(--ink);
  border-bottom: 1px solid var(--hairline-strong);
}
.rl-compare tbody th {
  font-weight: 400;
  color: var(--ink-2);
  width: 30%;
}
.rl-compare tbody tr:last-child th,
.rl-compare tbody tr:last-child td { border-bottom: 0; }

.rl-compare-links { margin-top: clamp(32px, 4vw, 48px); }
.rl-compare-links .rl-h3 { margin-top: 0; }

/* A hub's optional hand-written introduction. */
.rl-hub-intro { max-width: 68ch; }
.rl-hub-intro .rl-section { margin-bottom: clamp(24px, 3vw, 36px); }
.rl-preamble { margin: 0 0 28px; }

/* The "Scientific information" link on a product page, and the component
   links a blend carries instead. A blend can name several compounds, so the
   row wraps and each link keeps its own 44px target. */
.product-sci-link {
  margin: 18px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 2px 18px;
}
.product-sci-lead { color: var(--ink-2); font-size: 14px; }
/* Strength split on a blend: "GHK-Cu 50 mg · BPC-157 10 mg". */
.product-sci-strength { color: var(--ink-2); font-size: 13px; margin-left: 2px; }

/* An identifier the catalog does not carry, shown from a public register via
   the compound's library entry. The dagger is what the footnote refers to, so
   it is generated here rather than typed into the value — a marker that can
   drift from its own footnote is worse than no marker. */
[data-source="library"]::after {
  content: "\2020";
  margin-left: 2px;
  color: var(--gold-ink);
  font-weight: 600;
}
.spec-source-note { margin-top: 14px; }

/* ---------- from 64rem: the contents list becomes a sticky rail ---------- */
@media (min-width: 64rem) {
  .rl-layout {
    grid-template-columns: minmax(0, 68ch) minmax(0, 260px);
    gap: 56px;
    justify-content: center;
  }
  .rl-toc {
    order: 2;
    position: sticky;
    /* Clears the chrome reserve; see chromereserve.js. */
    top: 104px;
    max-height: calc(100vh - 130px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .rl-body { order: 1; }
  .rl-toc-details { border: 0; background: transparent; }
  .rl-toc-details > summary {
    padding: 0 0 8px;
    font-size: 12px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--ink-3);
    cursor: default;
  }
  /* On the rail it is always open, so the chevron is noise. */
  .rl-toc-details > summary::after { display: none; }
  .rl-toc-list { padding: 0 0 0 14px; border-left: 1px solid var(--hairline); }
  .rl-hub-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
}

@media (min-width: 48rem) {
  .rl-hub-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
}

@media (prefers-color-scheme: dark) {
  .rl-ribbon { background: rgba(201, 164, 95, 0.14); }
  .rl-placeholder { background: rgba(201, 164, 95, 0.2); }
  .rl-hub-card { box-shadow: none; border: 1px solid var(--hairline); }
}

@media (prefers-reduced-motion: reduce) {
  .rl-faq-item > summary::after,
  .rl-toc-details > summary::after { transition: none; }
}


/* ---------- Research library band (home page) --------------------------- */
/* Quiet by design: a name and a category, nothing else. The cards carry no
   description because the home page is scanned in full against
   PROHIBITED_TERMS -- see buildResearchBand() in app.js. */
.rbx-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}
.rbx-item { min-width: 0; }
.rbx-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  min-width: 0;
  min-height: 72px;
  padding: 16px 18px;
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: var(--tile-shadow);
  color: var(--ink);
  transition: box-shadow 220ms ease, transform 220ms ease;
}
.rbx-card:hover { box-shadow: var(--tile-shadow-hover); }
.rbx-name {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  /* A compound name can be long and unbreakable ("N-Acetyl-Semax"); let it
     wrap rather than widen the grid track past the viewport. */
  overflow-wrap: anywhere;
}
.rbx-cat {
  font-size: 12px;
  letter-spacing: 0.01em;
  color: var(--ink-3);
}

@media (min-width: 34rem) {
  .rbx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 60rem) {
  .rbx-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (prefers-color-scheme: dark) {
  .rbx-card { box-shadow: none; border: 1px solid var(--hairline); }
  .rbx-card:hover { border-color: var(--hairline-strong); }
}
@media (prefers-reduced-motion: reduce) {
  .rbx-card { transition: none; }
}


/* ---------- link modules ------------------------------------------------ */
/* Further reading, Related research, the hub's sibling row, and an article's
   compound strip. Quiet: these are navigation, not content, and must not
   compete with the prose above them. */
.rl-further, .rl-related, .rl-hub-siblings { margin-top: clamp(36px, 5vw, 56px); }
.rl-further .rl-h2, .rl-related .rl-h2 { margin-bottom: 8px; }

.rl-related-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
  min-width: 0;
  padding: 11px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 16px;
}
.rl-related-list li:last-child { border-bottom: 0; }
.rl-fr-kind {
  flex: 0 0 auto;
  font-size: 11px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.rl-compounds-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 18px;
  margin-top: 16px;
}
.rl-fr-lead { font-size: 14px; color: var(--ink-2); }

.rl-hub-sibling-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.rl-hub-sibling-row li { min-width: 0; }
.rl-hub-sibling-row a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px 14px;
  border-radius: var(--pill);
  border: 1px solid var(--hairline);
  background: var(--card);
  font-size: 13px;
  color: var(--ink-2);
}
.rl-hub-sibling-row a:hover { color: var(--ink); border-color: var(--hairline-strong); }

/* An auto-inserted contextual link reads exactly like any other link. It is
   marked in the DOM for the link-graph audit, not for the eye. */
a[data-autolink] { }
