/* ==========================================================================
   Pryme Research — components

   The rule this file obeys: a compound's colour gets a large area only at
   --peptide-wash strength. Full pastel is reserved for marks small enough to
   read as punctuation — a dot, a chip, a rule, a hover fill. Photography does
   the heavy colour work.
   ========================================================================== */

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

.btn {
  --btn-bg: var(--ink);
  --btn-fg: #ffffff;
  --btn-edge: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: 3.125rem;
  padding: 0 var(--space-lg);
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid var(--btn-edge);
  border-radius: var(--radius-control);
  font-family: var(--font-text);
  font-size: var(--size-sm);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.btn:hover {
  --btn-bg: #000;
}

.btn[disabled],
.btn[aria-disabled='true'] {
  cursor: not-allowed;
  opacity: 0.4;
}

/* Sits on the wash, so its label is the deep variant. Hover flips to the deep
   fill — the one moment a compound colour goes solid. */
.btn--compound {
  --btn-bg: var(--peptide-wash);
  --btn-fg: var(--peptide-deep);
  --btn-edge: var(--peptide);
  font-weight: 600;
}

.btn--compound:hover {
  --btn-bg: var(--peptide-deep);
  --btn-fg: #ffffff;
  --btn-edge: var(--peptide-deep);
}

.btn--quiet {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  --btn-edge: var(--rule-strong);
}

.btn--quiet:hover {
  --btn-bg: var(--paper-sunk);
  --btn-edge: var(--ink);
}

.btn--full {
  width: 100%;
}

.link-action {
  display: inline-block;
  color: var(--ink);
  font-size: var(--size-sm);
  font-weight: 500;
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--rule-strong);
  transition: border-color var(--dur-fast) var(--ease-out);
}

.link-action:hover {
  border-color: var(--ink);
}

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

/* Shopify wraps every section in its own div, and that wrapper is exactly as
   tall as the header. A sticky element can only travel inside its containing
   block, so sticking the inner <header> gave it zero travel and it scrolled
   away immediately. The wrapper carries the stickiness instead — the section
   schema's "class" puts .site-header on it. */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
}

/* --- Header ---------------------------------------------------------------
   Flush at rest, floating once scrolled.

   At the top of the page the nav is a plain full-width bar with a hairline
   under it. Once the page scrolls past it, the bar lifts into a contained
   rounded surface with a blur and a shadow.

   The vertical padding is CONSTANT across both states, and the hairline lives
   on .header rather than on the bar. That matters: .site-header is sticky and
   therefore still occupies its space in flow, so changing its height on scroll
   would shove every section below it down by that amount at the exact moment
   the state flips. Only paint changes here — never box height. */

.header {
  /* The bar's own inner padding is taken OUT of the container gutter, so the
     logo lands on exactly the same vertical line as the hero headline and every
     section heading below. Without this the bar's edge aligned but its contents
     sat one padding-width further in, which is the part you actually read. */
  --bar-pad: var(--space-lg);
  /* Distance from the window edge to the wordmark and to the last control.
     Both ends move together by design — the wordmark and the icon cluster are
     the two things the bar holds against its edges, and insetting one without
     the other reads as a bar that has slipped sideways. */
  --header-inset: 5.5rem;

  /* No base rule: it drew a line across the top of the hero that nothing below
     it lined up with. On scroll the header takes a translucent ground and a
     soft shadow instead, which separates it from what passes underneath
     without putting an edge on the page. */
  /* The bar's own min-height carries the height now. Padding here was adding
     16px on top of it, which is why the bar measured 112px when the controls
     inside it are 50px. */
  position: relative;
  padding-block: var(--space-3xs);
  transition:
    background-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

/* The bar's own inner padding is taken out of this, so the two always add up
   to --header-inset and the wordmark lands on one predictable line whatever
   --bar-pad is set to. Wider than the page gutter: at exactly the gutter the
   wordmark sat hard against the glass. */
.header > .container {
  padding-inline: max(var(--space-2xs), calc(var(--header-inset) - var(--bar-pad)));
}

.header__bar {
  /* Grid, not flex with space-between: the logo and the action cluster are
     different widths, so space-between leaves the nav off-centre. Equal 1fr
     side columns put it on the true centre line regardless. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  /* The distance from the window edge to the wordmark is fixed by
     --header-inset, because it has to land on the same vertical line as the
     hero headline and every section heading below. So the breathing room the
     bar needed had to come from between its three groups rather than from
     their outer edges, which would have moved that line. */
  gap: var(--space-xl);
  min-height: var(--header-height);
  /* Symmetric on purpose. Asymmetric padding shifts the content box's centre by
     half the difference, which knocked the centred nav 10px off the true middle
     even though the side columns were equal 1fr. The icon buttons' own 3.125rem
     boxes supply the optical inset on the right. */
  padding-inline: var(--bar-pad);
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  transition:
    background-color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    border-radius var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

/* Scrolled: the bar stays a bar.

   It used to round into a floating pill. The background now goes on .header
   rather than on .header__bar, and that is not a detail — the bar lives inside
   a max-width container, so a square background painted on it would stop at
   the measure and leave two strips of bare page at the edges on any wide
   screen. Rounded, that was invisible; square, it would be obvious.

   Separation is a soft shadow rather than a rule, since the header carries no
   line at rest either. */
.site-header[data-stuck='true'] .header {
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: saturate(1.5) blur(18px);
  box-shadow: 0 4px 16px -14px rgb(19 19 21 / 14%);
}

.site-header[data-stuck='true'] .header__bar {
  background: transparent;
  border-color: transparent;
  border-radius: 0;
  box-shadow: none;
}


.header__logo {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: var(--ink);
}

.header__logo img {
  height: auto;
}

.header__nav {
  grid-column: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3xs);
}

.header__link {
  font-size: var(--size-sm);
  text-decoration: none;
  color: var(--ink-soft);
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-control);
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.header__link:hover,
.header__link[aria-current='page'] {
  color: var(--ink);
  background: var(--paper-sunk);
}

/* A badge on one nav link. Set as an inline-flex row on the link so the word
   and the badge share one baseline box and the link's padding still describes
   its whole hit area. */
.header__link--badged {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

/* Small caps rather than a coloured pill: a pill here would be the loudest
   thing in a header whose entire job is to be quiet, and the cart count is
   already the one place in this bar allowed to shout. The accent tint keys it
   to the palette the rest of the site runs on. */
.header__badge {
  flex: none;
  padding: 0.1em 0.45em 0.15em;
  border-radius: 999px;
  background: color-mix(in srgb, #B7D9FC 62%, var(--paper));
  color: var(--ink);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  line-height: 1.4;
}

/* The controls were 50px boxes holding a 22px glyph with an 8px gap between
   them, so the space between the two marks measured 36px and they read as two
   unrelated objects rather than as one cluster. 44px boxes took that to 26px,
   which was tight enough that the two marks started to read as one shape;
   8px puts the gap between the glyphs at 30px, which is a cluster with room to
   breathe. The boxes still clear the 44px hit target. */
.header__actions {
  grid-column: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* The boxes are 3.125rem around a 20px glyph, so most of what separates the
     marks is their own padding; the gap is the part that is actually set. Kept
     small — this is a cluster of three related controls, and past a certain
     distance they stop reading as one group. */
  gap: var(--space-xs);
}

.header__actions .icon-btn {
  width: 2.75rem;
  height: 2.75rem;
}

/* Icon-only control. The hit area stays 44px even though the glyph is 20px. */
.icon-btn {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 3.125rem;
  height: 3.125rem;
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  text-decoration: none;
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.icon-btn:hover {
  color: var(--ink);
  background: var(--paper-sunk);
}

.icon-btn .icon {
  display: block;
}

.cart-toggle__count {
  position: absolute;
  top: 0.42rem;
  right: 0.3rem;
  display: inline-grid;
  place-items: center;
  min-width: 1.05rem;
  height: 1.05rem;
  padding-inline: 0.25rem;
  background: var(--ink);
  color: #fff;
  border: 2px solid var(--paper);
  border-radius: var(--radius-control);
  font-family: var(--font-data);
  font-size: 0.6rem;
  line-height: 1;
}

.header__menu-btn {
  display: none;
}

@media (max-width: 62rem) {
  .header {
    /* The desktop bar holds its contents 4.5rem off the window edge so the
       wordmark lands on the hero's alignment line. On a phone there is no such
       line to hit and no room to spend: 72px of inset a side plus a 56px gap
       left 42px for a 160px wordmark, and `img { max-width: 100% }` duly
       shrank it to that. The bar sits on the page gutter here instead. */
    --header-inset: var(--gutter);
    --bar-pad: var(--space-2xs);
  }

  .header__bar {
    grid-template-columns: auto 1fr;
    /* The desktop bar's gap is breathing room between three groups. Here there
       are two, and that gap is the difference between a legible wordmark and a
       squashed one. */
    gap: var(--space-sm);
  }

  /* Capped rather than left to the merchant's desktop width: 160px of wordmark
     beside three 44px controls does not fit a 390px screen with anything to
     spare. */
  .header__logo img {
    width: 7.5rem;
  }

  /* The menu, as a panel under the bar.

     It used to be `display: none` and nothing else, while the script toggled
     the nav's hidden ATTRIBUTE — which a display rule in a media query beats
     every time. So the button flipped aria-expanded, the nav stayed at
     display:none, and the entire navigation was unreachable below 62rem. The
     open state is an attribute on the header now, so the thing the script
     changes and the thing the stylesheet reads are the same thing. */
  .header__nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 1;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-inline: var(--gutter);
    padding: var(--space-2xs);
    background: var(--paper-raised);
    border: 1px solid var(--rule);
    border-radius: var(--radius-surface);
    box-shadow: var(--lift-drawer);
  }

  .site-header[data-nav-open='true'] .header__nav {
    display: flex;
    animation: nav-in var(--dur-base) var(--ease-out);
  }

  /* Full-width rows with a real touch target, rather than the desktop bar's
     tightly padded inline links. */
  .header__nav .header__link {
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-small);
    font-size: var(--size-base);
    color: var(--ink);
  }

  .header__actions {
    grid-column: 2;
    /* No gap between the boxes here. The 8px that separates them on the
       desktop bar is breathing room in a row that has the width to spare; on a
       phone it pushes three controls across a third of the screen. Zero brings
       the marks from 30px apart to 22px, which is the floor worth having —
       the boxes stay 44px so the tap targets do not shrink with the gap. */
    gap: 0;
  }

  .header__menu-btn {
    display: inline-grid;
  }
}

@keyframes nav-in {
  from {
    opacity: 0;
    translate: 0 -6px;
  }
}

/* The floating offer tab steps aside for the footer.

   The footer carries the same signup, so the tab is redundant the moment the
   footer is on screen — and on a phone it was worse than redundant: it sat on
   top of the footer's base row, covering the privacy link. */
.offer-tab--away {
  opacity: 0;
  translate: 0 12px;
  pointer-events: none;
}

/* --- Catalogue cards ------------------------------------------------------
   A gradient panel in the compound's own colour with a darker floor band, and
   the vial oversized and anchored to the bottom so the frame crops it. The
   renders are cropped to their alpha bounds, which is what lets the vial be
   positioned precisely instead of floating in dead space. */

.index-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
  gap: var(--space-md);
}

/* Phones: two across rather than one. At one per row each card is most of a
   screen tall and eight compounds is a long scroll; two keeps the catalogue
   browsable, with the card's type and padding stepped down to fit. */
@media (max-width: 34.99rem) {
  .index-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
  }

  .index-grid .card__body {
    padding: var(--space-sm);
  }

  .index-grid .card__name {
    font-size: var(--size-sm);
  }

  .index-grid .card__sub {
    font-size: var(--size-xs);
  }

  .index-grid .card__flag,
  .index-grid .card__dose {
    padding: 0.2rem 0.45rem;
    font-size: 0.625rem;
  }
}

.card {
  --card-color: var(--peptide);
  --card-deep: var(--peptide-deep);

  display: flex;
  flex-direction: column;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-surface);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition:
    box-shadow var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out);
}

/* The card itself does not move. It used to lift 4px on hover, which took the
   chips, the compound name and the price up with it — eight cards in a grid
   each sliding their whole contents around is a lot of movement for a pointer
   passing over one of them, and the chips are pinned to the frame's corners
   precisely so they can be relied on to stay there.

   The motion is now confined to the photograph and the ground behind it, and
   holding the chips and the body still is what makes it legible: the vial
   rises off a surface that pushes gently toward the viewer, and the fixed
   elements are the reference that lets you see it happen. */
.card:hover {
  border-color: transparent;
  box-shadow: var(--lift-card);
}

/* The horizon at 72% is what makes the vial read as standing on something. */
.card__media {
  position: relative;
  /* Taller than square so the vial can be large without the frame clipping it
     through the middle of its label. No overflow here — the chip straddles the
     bottom edge and must not be clipped by it. */
  aspect-ratio: 4 / 5;
}

.card__frame {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: var(--radius-surface) var(--radius-surface) 0 0;
}

/* The ground, on its own layer rather than on the frame, so it can move
   without taking the frame's other children — the two chips — with it.

   It scales from its bottom edge, which is the one piece of geometry that must
   not move: the gradient's floor band is what the vial stands on, and growing
   from the base keeps that contact point fixed while the horizon eases up
   about 3px. Scale rather than a lift for the same reason — translating this
   layer up would open a gap along the bottom of the frame. */
.card__frame::before {
  content: '';
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--card-color) 14%, #fff) 0%,
    color-mix(in srgb, var(--card-color) 30%, #fff) 72%,
    color-mix(in srgb, var(--card-color) 52%, #fff) 72%,
    color-mix(in srgb, var(--card-color) 38%, #fff) 100%
  );
  transition: transform var(--dur-slow) var(--ease-out);
}

.card:hover .card__frame::before {
  transform: scale(1.03);
}

.card__vial {
  position: absolute;
  left: 50%;
  bottom: -5%;
  height: 96%;
  width: auto;
  transform: translateX(-50%) rotate(-3deg);
  transform-origin: bottom center;
  filter: drop-shadow(0 18px 22px rgb(19 19 21 / 14%));
  transition: transform var(--dur-slow) var(--ease-out);
}

.card:hover .card__vial {
  transform: translateX(-50%) rotate(-3deg) translateY(-3%) scale(1.03);
}

/* A product with no photograph yet gets a deliberate empty frame. */
.card__media:not(:has(.card__vial))::after {
  content: '';
  position: absolute;
  inset: 22% 34%;
  border: 1px dashed color-mix(in srgb, var(--card-deep) 40%, transparent);
  border-radius: var(--radius-small);
}

.card__flag,
.card__dose {
  position: absolute;
  /* space-xs, not 2xs. The frame's top corners are rounded at
     --radius-surface, so at 8px each chip's own corner sat right against the
     curve. 12px clears it without the chips reading as floating in the middle
     of the frame. The value is shared by both chips so they cannot drift out
     of line with each other. */
  --chip-inset: var(--space-xs);

  top: var(--chip-inset);
  z-index: 2;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-control);
  font-size: var(--size-xs);
  line-height: 1;
}

.card__flag {
  right: var(--chip-inset);
  /* The compound's own deep colour, not the stop signal. A crimson chip was
     the single most saturated thing on a page built entirely from pastels, and
     eight of them in a row read as a clearance rail rather than as this
     catalogue's pricing. In the compound colour each flag instead reinforces
     the per-compound identity the rest of the card already carries.

     White on every one of the thirteen deeps measures 5.04:1 or better, so the
     figure stays AA whichever compound it lands on. */
  background: var(--card-deep, var(--signal-stop));
  color: #fff;
  font-weight: 600;
}

.card__dose {
  left: var(--chip-inset);
  background: color-mix(in srgb, #fff 82%, transparent);
  backdrop-filter: blur(6px);
  color: var(--card-deep);
}

.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding: var(--space-md);
  /* Grow to fill the card so the price row can be pinned to the bottom. */
  flex: 1;
}

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

.card__sub {
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.card__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
  /* Pinned to the bottom, so a two-line product name does not push the price
     out of line with its neighbours. */
  margin-top: auto;
  padding-top: var(--space-sm);
}

.card__price {
  font-family: var(--font-data);
  font-size: var(--size-sm);
  font-variant-numeric: tabular-nums;
}

/* Signed out: where the price would be (inc/compliance.php). */
.card__signin {
  font-family: var(--font-body, inherit);
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.card__price del {
  color: var(--ink-faint);
  margin-right: 0.4rem;
}

.stock {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: var(--space-2xs);
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.stock--out {
  /* No marker. "Out of stock" is unambiguous on its own, and this is the only
     state that renders at all. */
  color: var(--ink-soft);
}

/* --- The rack (optional homepage section) ---------------------------------
   Kept as an editor option. Spaced cards, not a fused row of colour blocks. */

.rack {
  padding-block: clamp(var(--space-xl), 7vw, var(--space-3xl)) 0;
}

.rack__head {
  max-width: 32ch;
  margin-bottom: clamp(var(--space-lg), 5vw, var(--space-2xl));
}

.rack__lede {
  margin-top: var(--space-md);
  max-width: 46ch;
}

.rack__shelf {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(13rem, 1fr);
  gap: var(--space-sm);
  padding-inline: var(--gutter);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

.vial {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-surface);
  background: var(--vial-wash, var(--peptide-wash));
  overflow: hidden;
  text-decoration: none;
  color: var(--ink);
  scroll-snap-align: start;
}

.vial__media {
  position: absolute;
  inset: 0;
}

.vial__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-md) var(--space-md) 22%;
  transition: transform var(--dur-slow) var(--ease-out);
}

.vial:hover .vial__media img,
.vial:focus-visible .vial__media img {
  transform: scale(1.04);
}

.vial__plate {
  position: absolute;
  inset: auto var(--space-2xs) var(--space-2xs);
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding: var(--space-sm) var(--space-md);
  background: color-mix(in srgb, #fff 88%, transparent);
  backdrop-filter: blur(10px);
  border-radius: var(--radius-small);
}

.vial__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-base);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
}

.vial__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  color: var(--ink-soft);
}

/* --- Assurance band -------------------------------------------------------
   Four identical cards spanning the content width, one compound colour each.

   An earlier version bled the colour to the viewport edge and gave the outer
   panels --edge padding so their text aligned with the page. The four then had
   wildly different internal padding — 266px on the first, 36px on the second —
   and stopped reading as a set. Identical cards win: same padding, same radius,
   same rhythm, and the row lines up with every other section. */

.assurance-band {
  padding-block: clamp(var(--space-lg), 3vw, var(--space-xl));
}

.assurance {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-2xs);
}

.assurance__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: clamp(var(--space-md), 2vw, var(--space-lg));
  border-radius: var(--radius-surface);
  /* Wash top-left to a touch deeper bottom-right, so four flat tints read as
     four lit surfaces rather than four swatches. */
  background: linear-gradient(
    150deg,
    color-mix(in srgb, var(--acc) 16%, #fff) 0%,
    color-mix(in srgb, var(--acc) 36%, #fff) 100%
  );
}

.assurance__icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: var(--space-2xs);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, #fff 74%, transparent);
  color: var(--acc-deep);
}

.assurance__value {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(var(--size-lg), 2.2vw, var(--size-xl));
  line-height: 1.02;
  letter-spacing: var(--tracking-tight);
  color: var(--acc-deep);
}

.assurance__label {
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink);
  /* Room for two lines at every width, so no card ends up a line taller than
     its neighbours. */
  max-width: 26ch;
}

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

/* --- Hero -----------------------------------------------------------------
   Layout lives in .split (pryme-base.css). Only hero-specific spacing here. */

/* The hero aligns to the WIDE measure, not the page measure, so its headline
   starts on the same vertical line as the nav bar directly above it. Aligned to
   the page measure it sat 82px further in than the chrome, which read as the
   copy being held back from the edge. Sections further down still align to the
   page measure — the hero is the only thing that shares an edge with the
   header. */
@media (min-width: 56rem) {
  .hero .split__copy {
    padding-inline: var(--edge-wide) clamp(var(--space-lg), 3vw, var(--space-2xl));
  }

  /* Larger than the shared .h1 so the headline actually fills the column it has
     been given. At the shared size it wrapped at 631px inside a 953px column,
     which read as timid next to a full-bleed panel. */
  .hero .h1 {
    font-size: clamp(var(--size-2xl), 3.9vw, 4.25rem);
  }
}

/* Any residual offset between this and the nav bar's edge is the 100vw /
   scrollbar difference: --edge is computed from 100vw, which includes the
   scrollbar, while a container resolves against the content width. Not worth
   chasing — there is no CSS length for the content width, and the alternative
   is subtracting a hard-coded scrollbar guess that breaks on overlay
   scrollbars. */

.hero__copy > * + * {
  margin-top: var(--space-md);
}

.hero__lede {
  max-width: 40ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-top: var(--space-lg);
}

.hero__footnote {
  margin-top: var(--space-md);
  max-width: 38ch;
}

/* --- The Pryme Standard ---------------------------------------------------
   Was a numbered ledger of five long paragraphs, 1340px tall and all text.
   Now a scannable grid: icon, title, one line. Each card takes a different
   compound accent so five similar rows are distinguishable at a glance
   without numbering — decorative, which is why each also has its own icon. */

/* .section-head now supplies the row, the hairline and the bottom margin. The
   46ch cap and the margin that used to live here would fight it: the cap
   squeezed both halves back into one narrow column, which is the layout this
   replaced. */
.standard__head {
  margin-bottom: 0;
}

.standard__head .standard__lede {
  max-width: 44ch;
}

.standard__eyebrow {
  color: var(--ink-faint);
  letter-spacing: var(--tracking-wide);
  margin-bottom: var(--space-sm);
}

/* No top margin: as a flex child of .section-head it is bottom-aligned beside
   the heading, and a top margin only pushes it below that baseline. */
.standard__lede {
  margin-top: 0;
}

/* --- Featured compound -----------------------------------------------------
   The dark band, kept for the spine it gives a long white page, but selling
   rather than explaining. The page had nowhere to buy a single compound
   without leaving it.

   Copy left, product right, with the compound's own colour behind the glass —
   which on a dark ground is the only way a transparent render reads at all. */
.spotlight__inner {
  display: grid;
  gap: clamp(var(--space-xl), 6vw, var(--space-3xl));
  align-items: center;
}

@media (min-width: 56rem) {
  .spotlight__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
  }
}

.spotlight__heading {
  max-width: 11em;
}

.spotlight__lede {
  margin-top: var(--space-md);
  max-width: 44ch;
}

.spotlight__points {
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-2xs);
}

.spotlight__points li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  font-size: var(--size-sm);
  color: var(--ink-ground-soft);
}

.spotlight__tick {
  flex: none;
  display: grid;
  place-items: center;
  margin-top: 0.12em;
  color: var(--spot-color);
  /* The disc is filled with the compound's pastel, so the tick inside it has to
     knock out in the ground's colour rather than in white. */
  --check-ink: var(--ink-ground);
}

/* --- Buying ---------------------------------------------------------------
   Set off from the copy by a rule, because this is the part of the section
   that does something rather than says something. */
.spotlight__buy {
  margin-top: clamp(var(--space-lg), 4vw, var(--space-xl));
  padding-top: var(--space-md);
  border-top: 1px solid var(--ink-ground-rule);
}

.spotlight__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: 0;
}

.spotlight__price del {
  font-size: var(--size-base);
  color: var(--ink-ground-soft);
}

.spotlight__now {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-xl);
  letter-spacing: var(--tracking-tight);
  color: #fff;
}

.spotlight__variant {
  font-size: var(--size-xs);
  color: var(--ink-ground-soft);
}

.spotlight__saving {
  margin: var(--space-3xs) 0 0;
  font-size: var(--size-xs);
  color: var(--spot-color);
}

.spotlight__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-top: var(--space-md);
}

/* The primary action in the compound's own colour, with the band's ground as
   its text — the one saturated thing in the section, which is what a buy
   button on a dark field should be. */
.btn--spot {
  --btn-bg: var(--spot-color);
  --btn-fg: var(--ink-ground);
  --btn-edge: var(--spot-color);
}

.btn--spot:hover:not([disabled]) {
  --btn-bg: #fff;
  --btn-edge: #fff;
}

.spotlight__error {
  margin: var(--space-2xs) 0 0;
  color: var(--signal-stop);
}

.spotlight__error:empty {
  display: none;
}

/* --- The vial --------------------------------------------------------------
   A transparent render on a near-black ground disappears without something
   behind it. The glow is the compound's pastel at low opacity, heavily
   blurred — it reads as the product being lit rather than as a shape. */
.spotlight__figure {
  position: relative;
  display: grid;
  place-items: center;
  padding-block: var(--space-md);
}

.spotlight__glow {
  position: absolute;
  width: min(26rem, 92%);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--spot-color) 40%, transparent),
    transparent 72%
  );
  filter: blur(18px);
}

.spotlight__link {
  position: relative;
  display: block;
  line-height: 0;
}

.spotlight__vial {
  height: clamp(20rem, 34vw, 28rem);
  width: auto;
  max-width: none;
  filter: drop-shadow(0 30px 40px rgb(0 0 0 / 45%));
  transition: transform var(--dur-slow) var(--ease-out);
}

.spotlight__link:hover .spotlight__vial {
  transform: translateY(-6px);
}

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

/* --- Certificate -----------------------------------------------------------
   Left-aligned and contained: the claim on the left, the document on the
   right. Every other section down this page hangs off the left margin, and the
   centred diagram this replaced read as a different site dropped into the
   middle of them. Contained rather than full-bleed because the hero and the
   blend are both full-bleed split panels already. */
.coa {
  /* Warmer than the page's own paper and ruled in a warm grey, because printed
     stock is not the same white as a screen and matching them exactly is part
     of what made the sheet read as a UI panel. */
  --coa-paper: #fdfdfa;
  --coa-hairline: #e2e0d9;
}

.coa__inner {
  display: grid;
  gap: clamp(var(--space-xl), 6vw, var(--space-3xl));
  align-items: center;
}

@media (min-width: 56rem) {
  .coa__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  }
}

.coa__heading {
  max-width: 12em;
}

.coa__lede {
  margin-top: var(--space-md);
  max-width: 46ch;
}

/* The steps that produce the document beside them. A hairline per step rather
   than a boxed card each: the section already has one strong object in it, and
   four boxes next to a sheet of paper would be competing with it. */
.coa__steps {
  margin: clamp(var(--space-lg), 3vw, var(--space-xl)) 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}

.coa__step {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  border-bottom: 1px solid var(--rule);
}

/* Optically aligned to the title's cap height rather than its line box. */
.coa__step-icon {
  flex: none;
  display: grid;
  place-items: center;
  margin-top: 0.1em;
  color: var(--coa-deep, var(--ink-soft));
}

.coa__step-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.coa__step-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-base);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.coa__step-body {
  max-width: 52ch;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.coa__cta {
  margin-top: clamp(var(--space-lg), 3vw, var(--space-xl));
}

/* --- The sheet -------------------------------------------------------------
   A document, not a card, and the distance between the two is entirely
   furniture. Square corners, because paper has no border radius and a 24px one
   was the single loudest tell. Letter proportions rather than the almost-square
   box it was. Dense rows, because forms are dense. Ruled blanks you could write
   on instead of grey pills. A plot with an axis. A foot carrying an
   authorisation line, boilerplate and a page number, because every real report
   has them and their absence is what made it read as a panel with a
   certificate written on it.

   The shadow is tight and offset rather than a wide even glow: paper lying on
   a surface is lit from somewhere. */
.coa__figure {
  position: relative;
  display: grid;
  place-items: center;
  padding-block: var(--space-lg);
}

.coa__stack {
  position: relative;
  width: min(24rem, 100%);
}

.coa__sheet {
  position: relative;
  aspect-ratio: 1 / 1.294;
  padding: 1.35rem 1.5rem 1.15rem;
  background: var(--coa-paper);
  border: 1px solid var(--rule-strong);
  /* No radius. This is the one that mattered. */
  border-radius: 0;
  box-shadow:
    1px 2px 0 rgb(19 19 21 / 3%),
    10px 16px 28px -14px rgb(19 19 21 / 26%);
  rotate: -1.4deg;
  font-size: 0.62rem;
  line-height: 1.5;
  color: var(--ink);
}

/* A second sheet behind. One perfectly placed rectangle reads as a graphic;
   two read as paperwork.

   Tinted in the compound's own pastel rather than left white — it is the only
   place the section's colour appears at any size, and a coloured under-copy is
   a real thing a form has. Mixed into the paper tone rather than used neat, so
   it still reads as stock and not as a colour swatch behind a document. The
   colour follows whichever vial is chosen, so this is apricot on GHK-Cu and
   would be sage on Mots-C. */
.coa__sheet--behind {
  position: absolute;
  inset: 0;
  rotate: 2.2deg;
  translate: 2% -1.5%;
  background: color-mix(in srgb, var(--coa-color, #F8D7A7) 55%, var(--coa-paper));
  border-color: color-mix(in srgb, var(--coa-color, #F8D7A7) 70%, var(--ink-faint));
  box-shadow: 6px 10px 20px -12px rgb(19 19 21 / 20%);
}

.coa__masthead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-bottom: 0.6rem;
  /* Two rules, close together, the way a masthead is ruled off. */
  border-bottom: 2px solid var(--ink);
  margin-bottom: 0.9rem;
}

.coa__mark {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  color: var(--ink);
}

.coa__doctype {
  margin: 0.1rem 0 0;
  font-size: 0.6rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}

.coa__meta {
  margin: 0;
  display: grid;
  gap: 0.2rem;
  min-width: 7.5rem;
}

.coa__meta > div {
  display: grid;
  grid-template-columns: 3.6rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0.3rem;
}

.coa__meta dt {
  font-size: 0.55rem;
  color: var(--ink-faint);
}

.coa__meta dd {
  margin: 0;
}

/* Group captions, set like the headings on a lab form. */
.coa__caption {
  margin: 0 0 0.25rem;
  font-family: var(--font-data);
  font-size: 0.53rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--coa-deep, var(--ink-soft));
}

.coa__rows {
  margin: 0 0 0.9rem;
}

.coa__row {
  display: grid;
  grid-template-columns: 5.4rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0.5rem;
  padding-block: 0.26rem;
  border-bottom: 1px solid var(--coa-hairline);
}

.coa__row dt {
  color: var(--ink-soft);
}

.coa__row dd {
  margin: 0;
}

.coa__value {
  font-family: var(--font-data);
  font-size: 0.58rem;
  color: var(--ink);
}

/* A line to write on, which is what a blank field on a form is. The grey pill
   this replaces looked like a loading skeleton. */
.coa__rule {
  display: block;
  height: 0.85em;
  border-bottom: 1px solid var(--ink-faint);
}

/* The trace, boxed and given the axis a plot actually has. */
.coa__plot {
  padding: 0.4rem 0.3rem 0.2rem;
  border: 1px solid var(--coa-hairline);
  margin-bottom: 0.8rem;
}

.coa__plot svg {
  display: block;
  width: 100%;
  height: 2.9rem;
}

.coa__axis,
.coa__tick {
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 0.8;
}

.coa__signal {
  fill: none;
  stroke: var(--coa-deep, var(--ink));
  stroke-width: 1.1;
}

.coa__foot {
  position: absolute;
  left: 1.5rem;
  right: 1.5rem;
  bottom: 1.15rem;
}

.coa__sign {
  width: 9rem;
  margin-bottom: 0.85rem;
}

.coa__sign .coa__rule {
  height: 1.5em;
}

.coa__sign-label {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.55rem;
  color: var(--ink-faint);
}

.coa__boiler {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin: 0;
  padding-top: 0.45rem;
  border-top: 1px solid var(--coa-hairline);
  font-size: 0.53rem;
  color: var(--ink-faint);
}

.coa__page {
  flex: none;
  font-family: var(--font-data);
}

/* Standing in front of the sheet's lower corner: the product and its paperwork
   in one frame, which is the entire argument of the section. */
.coa__vial {
  position: absolute;
  /* Clear of the page number in the sheet's bottom corner. At 4% it cut
     "Page 1 of 1" mid-word, which reads as a rendering fault rather than as one
     object standing in front of another. */
  right: 0;
  bottom: 0;
  height: 56%;
  width: auto;
  max-width: none;
  filter: drop-shadow(-10px 18px 24px rgb(19 19 21 / 22%));
}

@media (max-width: 55.999rem) {
  .coa__sheet {
    rotate: -1deg;
  }

  .coa__vial {
    height: 44%;
    right: 0;
  }
}

.coa__note {
  margin-top: clamp(var(--space-lg), 4vw, var(--space-xl));
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
  max-width: 68ch;
}

/* --- Specification ledger --------------------------------------------------
   Two columns: the term, then what it commits to. Hairline rows, no boxes, no
   icons — the section is a document now, and the whole point of the device is
   that it looks like one. It is also the only layout on the page that is not a
   grid of cards, which is why it exists in this form: as cards it was the
   fourth section-head-above-a-grid and read as a repeat of the assurance strip.

   The term column is fixed rather than fluid so every specification starts on
   the same vertical line all the way down; a fluid column would ragged them
   against whichever term happened to be longest. */
.ledger {
  margin: 0;
  /* No top rule of its own: the section head's bottom border is directly above
     it and serves as the ledger's header rule. Both drawn, they sat 45px apart
     and the section opened with a pair of parallel lines. The head's margin is
     zeroed alongside this so the first row starts against that rule, which is
     how a table's first row meets its header. */
  border-top: 0;
}

.ledger__row {
  display: grid;
  gap: var(--space-2xs) var(--space-lg);
  padding-block: var(--space-md);
  border-bottom: 1px solid var(--rule);
  transition: background-color var(--dur-fast) var(--ease-out);
}

/* Three columns, not two.

   As term-and-sentence, 651px of every 1388px row was empty — 47% of the
   table, seven rows deep — and a row whose content stops halfway reads as an
   unfinished line rather than as a record. Splitting each claim into the
   specification and how it is established gives the row something true to say
   all the way across, which is also how an actual certificate is laid out: the
   field, the value, the method.

   The term column is fixed so every specification starts on the same vertical
   line down the whole ledger; the other two are proportional so the detail
   takes the slack at any width. */
@media (min-width: 46rem) {
  .ledger__row {
    grid-template-columns: 11rem minmax(0, 0.72fr) minmax(0, 1fr);
    align-items: baseline;
  }

}

/* A row lights up under the cursor. Seven hairline rows are easy to lose your
   place in, and the table is long enough to be scanned rather than read. */
.ledger__row:hover {
  background: color-mix(in srgb, var(--acc-deep) 6%, transparent);
}

/* Each term in a different compound's deep tone. This is where the palette
   lives in this section, rather than in a tint behind the whole band. */
.ledger__key {
  margin: 0;
  font-size: var(--size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--acc-deep, var(--ink-soft));
}

/* Below the breakpoint the value is a normal block again — display: contents
   only applies where the three-column grid does — so the two parts stack. */
.ledger__value {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin: 0;
}

/* The scannable answer. Display type because this is the column the eye lands
   on when someone is looking for one figure rather than reading the table. */
.ledger__spec {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

/* How it is established. Quieter than the specification, because it qualifies
   the answer rather than being it. */
.ledger__detail {
  max-width: 46ch;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* display: contents dissolves the <dd> so its two children become grid items
   of the row itself, landing in columns two and three. It has to be declared
   AFTER the stacked base rule above — as part of the earlier media query it
   was overridden by `display: flex` further down the file, and the row kept
   three tracks while putting both parts in the second one. */
@media (min-width: 46rem) {
  .ledger__value {
    display: contents;
  }
}

/* The table's closing row: note left, the way out right, both hanging off the
   ledger's last rule. */
.ledger__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.ledger__note {
  margin: 0;
  max-width: 52ch;
}

.ledger__cta {
  flex: none;
}

/* --- Feature compound ------------------------------------------------------
   Layout is .split; this is the panel gradient and the overhanging vial.
   Nothing here clips: the overhang is the point. */

.feature__panel {
  background: linear-gradient(
    145deg,
    color-mix(in srgb, var(--feature-color) 46%, #fff) 0%,
    color-mix(in srgb, var(--feature-color) 22%, #fff) 55%,
    color-mix(in srgb, var(--feature-color) 38%, #fff) 100%
  );
  /* Overrides .split__panel — the vial has to cross the join. */
  overflow: visible;
}

/* --- Panel motif -----------------------------------------------------------
   The packaging's pattern, sitting behind the product.

   EXACTLY THE PANEL BOX — inset:0, no oversize, no translate. The previous
   version was 146% of the panel and pushed 200px past the document edge,
   which gave the whole page a horizontal scrollbar; nothing here can overflow
   now because nothing here is bigger than its container. The cropping that
   makes the pattern read as continuing ground is done inside the SVG instead,
   by `preserveAspectRatio="slice"` on a viewBox whose clusters already run off
   its edges.

   The variants differ as drawings, so no rotation or mirroring is applied.
   Turning one copy three times still reads as one copy turned. */
.feature__motif {
  position: absolute;
  inset: 0;
  color: var(--feature-deep);
  opacity: 0.32;
  pointer-events: none;
}

.feature__motif svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* --- Cast shadow -----------------------------------------------------------
   An ellipse on the surface beneath the vial, with a gap between the two: a
   shadow touching an object reads as contact, one clearly separated from it
   reads as height. The attached drop-shadow on the vial is small for the same
   reason — the mass of a hovering object's shadow belongs on the ground, and a
   big soft one welded to the glass is what makes it look like a sticker.

   Drawn as a radial gradient rather than a blurred solid: identical result, but
   a gradient is one texture the compositor can scale while filter: blur()
   repaints every frame of the float. */
.feature__cast {
  position: absolute;
  /* Measured, not guessed. The vial's real base — its lowest opaque row, mapped
     through the 7deg rotation — sits at 46.5% across and 90.9% down the panel,
     and is 12.2% of the panel wide. A first pass at 50% / 13% put the ellipse
     ABOVE the base and over the glass, which reads as a smudge on the vial
     rather than as a shadow under it.

     The tilt mirrors between the two feature sections, so the base does too.
     --cast-x is the flip, and for a long time this comment promised one that
     was never written: both sections took 46.5% while only one of them leans
     +7deg, so on the mirrored section the ellipse sat 58px to the left of the
     glass standing on it. Measured, the two bases land at 45.4% and 54.6%. */
  --cast-x: 46.5%;

  left: var(--cast-x);
  bottom: 2.9%;
  /* About 1.6x the base's own width, because the light in this scene is broad
     and soft — a shadow the exact width of the object implies a point source. */
  width: 20%;
  height: 4.4%;
  translate: -50% 0;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgb(19 19 21 / 34%) 0%,
    rgb(19 19 21 / 22%) 46%,
    rgb(19 19 21 / 0%) 100%
  );
  pointer-events: none;
  animation: feature-cast 9s var(--ease-in-out) infinite;
}

/* In phase with the vial's own float — same duration, so the shadow shrinks
   and fades as the vial rises. A static shadow under a moving object reads as
   a printed mark. */
@keyframes feature-cast {
  50% {
    transform: scale(0.9);
    opacity: 0.62;
  }
}

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

.feature__vial-link {
  position: absolute;
  inset: 0;
  display: block;
}

/* Centred in its panel. It previously hung off the left edge to cross the
   split's join, which put most of the product off the panel and, at the
   homepage's proportions, partly off screen. Centred and sized to fit, the
   product is the subject rather than a texture bleeding out of frame. */
.feature__vial {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 84%;
  width: auto;
  max-width: none;
  /* Centring lives on `translate` and `rotate`, not on `transform`, so the
     float below owns `transform` outright and cannot knock the vial off
     centre. Same split the hero cluster uses. */
  translate: -50% -50%;
  /* Tilt follows the layout's hand rather than the product, so a mirrored pair
     of feature sections gets a mirrored pair of vials without anyone
     remembering to set it. .split--reverse is the panel-left variant; the
     panel-right one below takes the opposite lean. */
  --tilt: 7deg;
  --sway: -1deg;

  rotate: var(--tilt);
  filter: drop-shadow(0 12px 16px rgb(19 19 21 / 14%));
  animation: feature-float 9s var(--ease-in-out) infinite;
}

/* A loop, not a hover. On hover the vial only moved for someone who happened to
   put a cursor on it, and never on touch — which is most visitors. A slow
   continuous drift means the product is alive whether or not it is touched, and
   the amplitude stays small enough to be noticed second rather than first. */
/* Panel on the right: everything mirrors, the lean and its drift included. */
.feature:not(.split--reverse) .feature__vial {
  --tilt: -7deg;
  --sway: 1deg;
}

/* ...and so does the ground the vial stands on. 53.5% is 46.5% mirrored about
   the panel's centre line, which is exactly what flipping the lean does to the
   base. */
.feature:not(.split--reverse) .feature__cast {
  --cast-x: 53.5%;
}

/* The sway is read from the element, so it flips with the tilt. Held at a fixed
   sign it ran further from upright at mid-cycle on one of the two sections
   instead of easing back toward it — the same numbers reading as the opposite
   motion once the vial was mirrored. */
@keyframes feature-float {
  50% {
    transform: translateY(-1.6%) rotate(var(--sway));
  }
}

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

@media (max-width: 55.999rem) {
  /* Stacked: the vial sits inside its band rather than crossing anything. */
  .feature__panel {
    overflow: hidden;
    min-height: 26rem;
  }

  .feature__vial {
    height: 78%;
  }
}

/* Rotated sticker on the panel's outer corner. */
.feature__badge {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 4.25rem;
  height: 4.25rem;
  border-radius: var(--radius-control);
  background: var(--feature-deep);
  color: #fff;
  font-family: var(--font-data);
  font-size: var(--size-xs);
  transform: rotate(8deg);
}

.feature__eyebrow {
  color: var(--feature-deep);
  letter-spacing: var(--tracking-wide);
  margin-bottom: var(--space-sm);
}

.feature__list {
  list-style: none;
  margin: var(--space-lg) 0 0;
  padding: 0;
  display: grid;
  /* Wider than 2xs. These are three separate claims, and at 8px they read as a
     single wrapped paragraph rather than as a list. */
  gap: var(--space-xs);
}

.feature__list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* Optically aligned to the text's cap height rather than its line box: the
   disc is round, so centring it on the line leaves it sitting visibly low
   against the first letter. */
.feature__check {
  flex: none;
  display: grid;
  place-items: center;
  margin-top: 0.12em;
  color: var(--feature-deep);
}

/* A section that owns a compound tints the emphasis to match it. */
.feature {
  --em-color: var(--feature-color);
}

.feature .btn--compound {
  --btn-bg: var(--feature-color);
  --btn-fg: var(--feature-deep);
  --btn-edge: transparent;
}

.feature .btn--compound:hover {
  --btn-bg: var(--feature-deep);
  --btn-fg: #fff;
}

/* --- Product page ---------------------------------------------------------- */

.product {
  display: grid;
  gap: clamp(var(--space-lg), 5vw, var(--space-2xl));
  grid-template-columns: 1fr;
  padding-block: var(--space-lg) var(--space-2xl);
}

@media (min-width: 60rem) {
  .product {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    /* Sticky needs this. Grid items stretch to the row's height by default, so
       a sticky child is already as tall as the thing it is meant to stick
       inside and never moves. */
    align-items: start;
  }

  /* The gallery holds still and the copy scrolls past it.

     It used to be the other way round, which could not work: the info column
     carries the price, the buy controls, the specification and the whole
     description, so it is always the taller of the two and is the column you
     are actually scrolling through. Sticking it meant sticking the thing that
     has to move. The gallery is the short column and the one worth keeping in
     view while you read the spec, so it is the one that stays. */
  /* Never taller than the space it is pinned in, or the bottom of a tall
     gallery would sit off-screen with no way to scroll it into view. The cap
     is on the width: the stage is square, so a narrower gallery is a shorter
     one, and the thumbnails follow it down. (Capping the stage's height
     instead left its grid row sized to the uncapped square, and on a short
     window the image ran over the thumbnails.) */
  .product__gallery {
    position: sticky;
    top: calc(var(--header-height) + var(--space-lg));
    width: min(100%, calc(100dvh - var(--header-height) - var(--space-3xl)));
  }
}

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

.product__stage {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-panel);
  overflow: hidden;
  background: var(--peptide-wash);
}

.product__stage img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: clamp(var(--space-lg), 5vw, var(--space-2xl));
}

.product__thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));
  gap: var(--space-2xs);
}

.product__thumb {
  aspect-ratio: 1 / 1;
  background: var(--peptide-wash);
  border: 1px solid transparent;
  border-radius: var(--radius-small);
  overflow: hidden;
  padding: 0;
}

.product__thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-2xs);
}

.product__thumb[aria-current='true'] {
  border-color: var(--peptide-deep);
}

/* Photographs fill the frame; only the cut-out silo is contained on the
   compound colour with room around it (and its shadow, see Silo shadow).
   Set per image by data-fit — see pryme_image_fit(). */
.product__stage[data-fit='cover'] img,
.product__thumb[data-fit='cover'] img {
  object-fit: cover;
  padding: 0;
  filter: none;
}

.product__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(var(--size-xl), 4vw, var(--size-3xl));
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}

.product__subtitle {
  margin-top: var(--space-2xs);
  color: var(--ink-soft);
}

/* The quantity ("10 / 20 MG") wraps as a unit, never across its slash. */
.product__subtitle .data {
  white-space: nowrap;
}

.product__price {
  font-family: var(--font-data);
  font-size: var(--size-md);
  font-variant-numeric: tabular-nums;
}

.product__price del {
  color: var(--ink-faint);
  margin-right: var(--space-2xs);
}

/* --- Variant picker -------------------------------------------------------- */

.variants {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.variant {
  position: relative;
}

.variant input {
  position: absolute;
  opacity: 0;
  inset: 0;
  cursor: pointer;
}

.variant span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.875rem;
  padding-inline: var(--space-md);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  font-size: var(--size-sm);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.variant input:checked + span {
  border-color: var(--peptide-deep);
  background: var(--peptide-wash);
  font-weight: 500;
}

.variant input:focus-visible + span {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.variant input:disabled + span {
  color: var(--ink-faint);
  text-decoration: line-through;
  border-style: dashed;
}

/* --- Specification --------------------------------------------------------- */

/* Hairline rows on the page's own ground, not a filled panel — the same call
   the certificate's step list makes, and for the same reason: a box around a
   list of facts is furniture the facts did not ask for.

   It also fixes what the panel could not survive. With one or two values
   populated, a full-width box with a 60% value column left most of its area
   empty and the row read as a mistake. Two columns from 64rem halve the width
   a short value has to carry, and the hairlines mean a sparse table still
   looks ruled rather than abandoned. */
.spec {
  display: grid;
  gap: 0 var(--space-xl);
  border-top: 1px solid var(--rule);
}

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

/* The label column is one fixed width, wide enough for the longest label
   ("Reconstitute in", "Molecular mass"), and labels never wrap. It was a
   fraction of each row, so the longest label broke onto two lines while
   every other row sat on one, and the values did not line up. Only a long
   value wraps now. */
.spec__row {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: var(--space-sm);
  padding-block: var(--space-xs);
  border-bottom: 1px solid var(--rule);
}

.spec__key {
  font-size: var(--size-sm);
  color: var(--ink-soft);
  white-space: nowrap;
}

.spec__value {
  font-family: var(--font-data);
  font-size: var(--size-xs);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* The research-use line on a product page. Not .compliance, which is shared
   with the cart, the box builder and the contact form's status messages, where
   a filled panel is right.

   Here it was a full-width recessed box holding two lines of type, which is the
   treatment a promotion gets — and the footer already settled that dressing a
   legal statement as a promotion is the wrong signal in both directions. A rule
   and the line itself says the same thing and reclaims the block of white the
   box was carrying. */
.product__compliance {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.product__compliance strong {
  color: var(--ink);
  font-weight: 600;
}

.product__compliance p {
  margin: 0;
}

.product__compliance p + p {
  margin-top: var(--space-2xs, 0.25rem);
}

/* --- Attach ----------------------------------------------------------------
   The one cross-sell on the page, and it earns its place by being a
   requirement rather than a suggestion: a lyophilised peptide cannot be used
   without something to reconstitute it in. It is a bordered row rather than a
   hairline because, unlike the specification above it, it IS a separate object
   with its own price and its own button. */
.attach {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  padding: var(--space-2xs) var(--space-sm) var(--space-2xs) var(--space-2xs);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-small);
  transition: border-color var(--dur-fast) var(--ease-out);
}

.attach:hover {
  border-color: var(--ink-faint);
}

/* The thumbnail sits on the attached product's own pastel, the way a catalogue
   card does, so it reads as a product and not as an icon. */
.attach__thumb {
  flex: none;
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--radius-small);
  background: color-mix(in srgb, var(--attach-color, var(--peptide)) 26%, #fff);
  overflow: hidden;
}

.attach__thumb img {
  width: 78%;
  height: 78%;
  object-fit: contain;
}

.attach__text {
  flex: 1;
  min-width: 0;
}

.attach__title {
  margin: 0;
  font-size: var(--size-sm);
  font-weight: 600;
}

.attach__note {
  margin: 0;
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.attach__price {
  flex: none;
  font-family: var(--font-data);
  font-size: var(--size-xs);
  font-variant-numeric: tabular-nums;
}

.attach__add {
  flex: none;
  min-height: 2.25rem;
  padding-inline: var(--space-sm);
  font-size: var(--size-xs);
}

/* Stacked: name and price on the first line, the button on its own beneath.

   A modifier rather than only a media query, because the cart drawer is a
   28rem column sitting inside a viewport that is usually much wider — a
   viewport query never fires for it, and the row would stay cramped on the one
   screen where it matters most. */
.attach--stack {
  flex-wrap: wrap;
}

.attach--stack .attach__text {
  flex: 1 1 55%;
}

.attach--stack .attach__add {
  flex: 1 1 100%;
}

@media (max-width: 30rem) {
  .attach {
    flex-wrap: wrap;
  }

  .attach__text {
    flex: 1 1 60%;
  }

  .attach__add {
    flex: 1 1 100%;
  }
}

/* --- Page not found ---------------------------------------------------------
   A small hero rather than a form. The split is the theme's own layout, so the
   page a visitor reaches by accident looks like the shop rather than like an
   error console. */

.lost {
  align-items: stretch;
}

.lost__heading {
  max-width: 14ch;
  text-wrap: balance;
}

.lost__body {
  margin: var(--space-md) 0 0;
  max-width: 46ch;
}

.lost__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.lost__index {
  margin-top: var(--space-2xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

.lost__index-label {
  margin: 0 0 var(--space-xs);
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

/* Wraps rather than scrolls: on a catalogue this size the whole line fits, and
   the point of the block is that nothing is hidden behind an interaction. */
.lost__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The compound's own colour, at wash strength — this is a large area, and the
   discipline in pryme-tokens.css is that large areas take the wash. */
.lost__panel {
  display: grid;
  place-items: center;
  padding: var(--space-xl);
  background: linear-gradient(
    145deg,
    color-mix(in srgb, var(--lost-color, var(--peptide)) 40%, #fff) 0%,
    color-mix(in srgb, var(--lost-color, var(--peptide)) 18%, #fff) 55%,
    color-mix(in srgb, var(--lost-color, var(--peptide)) 32%, #fff) 100%
  );
}

.lost__vial {
  display: block;
  width: min(15rem, 62%);
  rotate: -5deg;
  transition: transform var(--dur-slow) var(--ease-out);
}

.lost__vial img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 20px 26px rgb(19 19 21 / 18%));
}

.lost__vial:hover {
  transform: translateY(-6px);
}

@media (max-width: 55.999rem) {
  .lost__panel {
    min-height: 18rem;
    order: -1;
  }

  .lost__vial {
    width: min(11rem, 46%);
  }
}

/* --- Search bar -------------------------------------------------------------
   A field and its button on one line. The whole of this was inline styles on
   the section, and it carried the theme's only horizontal-overflow bug: the
   input was `flex: 1` with no `min-width: 0`, and a flex item will not shrink
   below its intrinsic minimum without it. A search input's intrinsic minimum
   is the better part of 200px, so under about 380px the field refused to give
   way, the button pushed past the edge, and the whole page scrolled sideways.

   The basis and the wrap are the belt to that fix's braces: if the submit
   label is ever translated into something long, the button drops to its own
   line instead of squeezing the field to nothing. */
.search-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.search-form input {
  flex: 1 1 8rem;
  min-width: 0;
  min-height: 3rem;
  padding-inline: var(--space-sm);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: var(--size-sm);
}

.search-form input::placeholder {
  color: var(--ink-faint);
}

.search-form input:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.search-form .btn {
  flex: none;
}

/* --- Compliance note ------------------------------------------------------- */

.compliance {
  padding: var(--space-md);
  background: var(--paper-sunk);
  border-radius: var(--radius-surface);
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.compliance strong {
  color: var(--ink);
  font-weight: 600;
}

/* --- Modal frame (the account modal, snippets/auth-gate.php) ---------------- */

.gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: color-mix(in srgb, var(--paper) 78%, transparent);
  backdrop-filter: blur(20px) saturate(1.2);
}

.gate[hidden] {
  display: none;
}

.gate__panel {
  width: min(33rem, 100%);
  /* Capped to the viewport and scrolled internally, the same way the offer
     panel is.

     Without this the gate was impassable on a small phone. Two attestations,
     a paragraph of fineprint and an exit link add up to more than 700px, the
     panel had no height limit and no overflow, and .gate locks body scroll
     while it is up — so below about 400px wide the submit button sat under
     the fold with no way to reach it and the store could not be entered at
     all. Capping at the container's own height rather than making .gate
     scroll keeps the panel centred and avoids the centred-overflow trap,
     where an over-tall grid item has its top cut off out of reach. */
  max-height: calc(100dvh - var(--gutter) * 2);
  overflow-y: auto;
  padding: clamp(var(--space-lg), 5vw, var(--space-xl));
  background: var(--paper-raised);
  border-radius: var(--radius-panel);
  box-shadow: var(--lift-drawer);
  text-align: center;
  animation: gate-in var(--dur-slow) var(--ease-out) both;
}

@keyframes gate-in {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.985);
  }
}

.gate__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-xl);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

.gate__body {
  margin-top: var(--space-xs);
  color: var(--ink-soft);
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
}

.gate__checks {
  display: grid;
  gap: var(--space-2xs);
  margin-block: var(--space-lg);
  text-align: left;
}

.check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--rule);
  border-radius: var(--radius-small);
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.check:has(input:checked) {
  border-color: var(--ink);
  background: var(--paper-sunk);
}

.check input {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.2rem;
  accent-color: var(--ink);
}

.gate__fineprint {
  margin-top: var(--space-md);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

.gate__exit {
  margin-top: var(--space-sm);
  font-size: var(--size-xs);
  color: var(--ink-faint);
}

/* --- Partner: the terms ----------------------------------------------------
   Set as the head of a document rather than as a hero. Three hairlines carry
   the whole structure — under the masthead, above the term cells, above the
   foot — and there is no panel, tile or card anywhere in it. That is
   deliberate: this page is about to make claims involving money, and on a
   catalogue whose entire argument is documentary (a certificate per lot, a
   specification per compound) the terms should look like a record and not like
   an advertisement.

   The cells put the label above the figure, which is how a specification table
   reads and the reverse of the stat-tile convention every referral page uses.
   The rules between them are drawn with borders on the cells rather than on
   the container, so the grid can go from four columns to two without a rule
   ending in mid-air. */

.pact__body {
  position: relative;
  display: grid;
  gap: var(--space-xl);
  /* No bottom padding: the row has to end exactly on the terms rule, or the
     padding swallows the vial's negative margin and it hangs politely above a
     line it was supposed to cross. The copy carries that space itself. */
  padding-block: clamp(var(--space-xl), 5vw, var(--space-3xl)) 0;
}

/* 46rem, not the 56rem the terms switch at. Between the two the copy is wide
   enough that the buttons sit on one row, which leaves a band about 3rem tall
   at the bottom of the copy — and a vial group bottom-aligned into a 3rem band
   has nowhere to go but up into the last line of the lede. Side by side there
   is no overlap to get wrong at all, so the columns arrive early and the
   stacked rules below only ever run at widths where the buttons stack too. */
@media (min-width: 46rem) {
  .pact__body {
    /* The second track carries nothing — the vials are positioned over it.
       It is here to keep the copy off them, which a max-width on the text
       could not do at every width. */
    grid-template-columns: minmax(0, 1fr) clamp(12rem, 30%, 26rem);
    align-items: end;
  }

  .pact__copy {
    padding-bottom: clamp(var(--space-xl), 5vw, var(--space-3xl));
  }
}

.pact__heading {
  margin: 0;
  max-width: 16ch;
  text-wrap: balance;
}

.pact__lede {
  margin: var(--space-md) 0 0;
  max-width: 46ch;
}

.pact__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

/* --- The vials -------------------------------------------------------------
   A group falling away into the page rather than one bottle standing in a
   space: the front compound sharp, two more behind it smaller, softer and
   further out of focus. The terms apply to the whole catalogue and a line of
   vials receding says "and the rest of them" without another paragraph.

   Everything here is sized from the group's HEIGHT, not its width, and that is
   the load-bearing decision. The render is about twice as tall as it is wide,
   so sizing by width sets the height at 2:1 — and the moment the group is
   taller than the copy beside it, it is the group and not the text that sets
   the height of the row, the negative margin below gets absorbed into the row
   instead of pulling the front vial down, and the rule it was supposed to
   break goes unbroken. Fixing the height instead makes that impossible.

   The front vial breaks the terms rule over the empty tail of the last cell,
   where the label and the figure have both already ended, so it crosses a line
   and never a word. */

.pact__mark {
  position: relative;
  justify-self: end;
  align-self: end;
  width: 100%;
  height: clamp(7rem, 20vw, 17rem);
  z-index: 0;
}

/* Lifted out of the grid once there is room for it. In flow the group had to
   stay shorter than the copy beside it — a taller item sets the row height,
   swallows the negative margin below and leaves the terms rule unbroken — so
   it was pinned to a narrow column and three vials ended up stacked on one
   spot in the bottom corner with the whole right half of the hero empty
   around them. Positioned, it owes the row nothing: the column still reserves
   the width so no text can run under it, and the group gets a box wide and
   tall enough for the vials to actually recede across. */
@media (min-width: 46rem) {
  .pact__mark {
    position: absolute;
    right: 0;
    bottom: calc(var(--space-lg) * -1);
    width: clamp(12rem, 30%, 26rem);
    height: clamp(11rem, 25vw, 21rem);
  }
}

.pact__vial {
  position: absolute;
  bottom: 0;
  display: block;
}

.pact__vial img {
  display: block;
  width: auto;
  height: 100%;
  max-width: none;
}

/* In focus, on the ground, and the only one carrying a shadow — the other two
   are far enough back that a cast under them would read as a second light
   source rather than as distance. */
.pact__vial--1 {
  right: 3%;
  height: 100%;
  rotate: 6deg;
  filter: drop-shadow(0 18px 22px rgb(19 19 21 / 16%));
}

/* Each one further left, smaller, higher and softer than the one in front of
   it: four cues pulling the same way, which is what reads as distance. One or
   two of them alone reads as three bottles of different sizes.

   The tilts alternate, and they are set against what is on screen rather than
   against zero — the renders are not shot square-on, so a small negative angle
   on the middle vial still left it leaning the same way as the front one. */
.pact__vial--2 {
  right: 36%;
  bottom: 9%;
  height: 74%;
  rotate: -17deg;
  filter: blur(2px);
  opacity: 0.7;
}

.pact__vial--3 {
  right: 66%;
  bottom: 20%;
  height: 54%;
  rotate: 14deg;
  filter: blur(4px);
  opacity: 0.42;
}

/* Stacked, the group shares the copy's cell instead of taking a band of its
   own below it. Given its own row it added about 350px of scrolling to say
   nothing the words had not, and pushed the terms — the reason anyone opened
   this page — most of a screen further down. The bottom right of the copy is
   empty at every phone width, because the buttons are left-aligned and
   narrower than half the column, so the group stands beside them and costs no
   height at all. */
@media (max-width: 45.999rem) {
  .pact__copy,
  .pact__mark {
    grid-area: 1 / 1;
  }

  .pact__copy {
    position: relative;
    z-index: 1;
  }

  .pact__mark {
    align-self: end;
    /* Narrow, because the two at the back are hidden here and the wash is sized
       from this box: left at group width it spread well clear of the one vial
       still showing and read as a green smudge beside it rather than as light
       behind it. */
    width: min(34%, 7.5rem);
    pointer-events: none;
  }

  /* The two at the back come off on a phone. At this size they are a smudge
     behind the front vial rather than a shelf receding, and they are the first
     thing that would collide with the last line of the lede. */
  .pact__vial--2,
  .pact__vial--3 {
    display: none;
  }
}

/* --- The slip --------------------------------------------------------------
   A worked month, at the size of a receipt, lying over the vials.

   The figures were tried once as a full panel beside the copy and that version
   is what turned the page into an earnings pitch: the largest thing on screen
   was a number nobody is promised. Small and off to one side, it is available
   to anyone who wants it and ignorable by anyone who does not — the right
   weight for a figure this conditional.

   It floats only where there is room to float. Below 64rem the copy column is
   narrow enough that a floating slip would land on the lede, so it drops into
   the flow under the copy instead of being positioned over a guess. */

.pact__slip {
  width: min(100%, 22rem);
  /* In flow its bottom edge would land exactly on the terms rule — the body
     carries no bottom padding, by design, so the vials can cross that line. */
  margin: 0 0 var(--space-xl);
  padding: var(--space-md) var(--space-lg);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-surface);
  box-shadow: var(--lift-drawer);
}

@media (min-width: 46rem) {
  /* The vials are positioned, so without this the slip auto-places into the
     column reserved for them and lands under the front vial. */
  .pact__slip {
    grid-column: 1;
  }
}

@media (min-width: 64rem) {
  .pact__slip {
    position: absolute;
    z-index: 2;
    right: clamp(9rem, 16%, 13rem);
    bottom: 46%;
    width: clamp(16.5rem, 25%, 20.5rem);
    margin: 0;
  }
}

.pact__slip-caption {
  font-family: var(--font-data);
  font-size: 0.5625rem;
  line-height: var(--leading-normal);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pact__slip-total {
  margin: var(--space-3xs) 0 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--size-xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.pact__slip-rows {
  margin: var(--space-sm) 0 0;
  /* The rule sits above the breakdown, so the total reads as the headline and
     the rows as what it is made of — the reverse of a ledger, which totals
     downward. Nobody reaching for this wants to do the addition first. */
  padding-top: var(--space-2xs);
  border-top: 1px solid var(--rule);
}

.pact__slip-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: 0.15rem;
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.pact__slip-row dt {
  min-width: 0;
}

.pact__slip-row dd {
  flex: none;
  margin: 0;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.pact__slip-note {
  margin: var(--space-2xs) 0 0;
  font-size: 0.625rem;
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

.pact__terms {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin: 0;
  border-top: 1px solid var(--rule);
}

.pact__term {
  padding: var(--space-md) var(--space-lg);
  border-inline-start: 1px solid var(--rule);
}

.pact__term:nth-child(2n + 1) {
  padding-inline-start: 0;
  border-inline-start: 0;
}

.pact__term:nth-child(n + 3) {
  border-top: 1px solid var(--rule);
}

@media (min-width: 56rem) {
  .pact__terms {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .pact__term:nth-child(n + 3) {
    border-top: 0;
  }

  /* Restated before the 4n rule below, because at four columns the third cell
     is no longer the start of a row and needs its rule back. */
  .pact__term:nth-child(2n + 1) {
    padding-inline-start: var(--space-lg);
    border-inline-start: 1px solid var(--rule);
  }

  .pact__term:nth-child(4n + 1) {
    padding-inline-start: 0;
    border-inline-start: 0;
  }
}

.pact__term-label {
  font-family: var(--font-data);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pact__term-value {
  margin: var(--space-3xs) 0 0;
  font-family: var(--font-display);
  font-weight: 600;
  /* "Attribution window" is the widest label and "30 days" the widest figure;
     at two columns on a small phone the cell is about 10rem and the figure has
     to come down to stay inside its rule. */
  font-size: clamp(var(--size-md), 3.4vw, var(--size-lg));
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.pact__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-lg);
  margin: 0;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

.pact__foot-note {
  max-width: 62ch;
}

.pact__foot-ref {
  flex: none;
  font-size: 0.625rem;
  letter-spacing: 0.14em;
}


/* --- Referral: the closing ask ---------------------------------------------
   A band with two vials standing against it and breaking its edges. The band
   clips nothing — the overhang is the whole device, the same one the feature
   sections use — so the grid below is three columns with the copy in the
   middle and the vials given their own tracks either side. Absolute
   positioning would have been fewer lines and would have let the vials sit on
   top of the copy the moment the text ran long.

   The vials break the band vertically and barely at all horizontally. Hanging
   one off the left or right by any real distance is a page that scrolls
   sideways on somebody's laptop; the vertical break costs nothing because the
   sections above and below are already spaced apart. */

.refer-cta {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 2.4fr) minmax(0, 0.8fr);
  align-items: center;
  /* Published, because the vials' overhang is measured from it: a grid area
     starts inside the padding, so anything less than this pulls a vial toward
     the band's edge without ever reaching it. */
  --band-pad: clamp(var(--space-xl), 6vw, var(--space-3xl));

  gap: var(--space-md);
  padding: var(--band-pad) var(--space-lg);
  border-radius: var(--radius-panel);
  /* Two colours, not four, and both of them the label colours of the vials
     standing at either end: warm where the left one stands, violet where the
     right one does, meeting in the middle. The previous version ran the whole
     line's spectrum across the band — apricot, mint, sky, lilac — which was
     pretty and said nothing, and the two colours it passed through were not
     the two compounds in the picture.

     Held to a tint, because the band is large and the colour discipline in
     pryme-tokens.css is that big areas take the wash rather than the colour.
     The soft radial on top lifts the centre so the copy is never reading off
     the deepest part of either end. */
  background:
    radial-gradient(
      68% 82% at 50% 50%,
      color-mix(in srgb, var(--paper) 72%, transparent),
      transparent 76%
    ),
    linear-gradient(
      104deg,
      color-mix(in srgb, var(--band-a, #f8d7a7) 40%, var(--paper)) 0%,
      color-mix(in srgb, var(--band-a, #f8d7a7) 20%, var(--paper)) 28%,
      color-mix(in srgb, var(--band-b, #c6b6c4) 20%, var(--paper)) 72%,
      color-mix(in srgb, var(--band-b, #c6b6c4) 42%, var(--paper)) 100%
    );
}

.refer-cta__copy {
  grid-column: 2;
  text-align: center;
}

.refer-cta__heading {
  margin: 0;
  text-wrap: balance;
}

.refer-cta__body {
  margin: var(--space-sm) auto 0;
  max-width: 46ch;
  font-size: var(--size-base);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.refer-cta__action {
  margin: var(--space-lg) 0 0;
}

/* The terms restated, in the data face so they read as figures rather than as
   a second sentence. */
.refer-cta__points {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2xs) var(--space-lg);
  margin: var(--space-lg) 0 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-data);
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.refer-cta__points li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
}

/* Sized, not stretched. Left to fill its track a vial came out 567px tall and
   became the thing setting the band's height — the copy has to do that, or the
   band grows with whatever render happens to be picked. A fixed width keeps
   the pair subordinate to the words they stand beside. */
.refer-cta__vial {
  position: relative;
  display: block;
  width: min(9.5rem, 100%);
  justify-self: center;
}

.refer-cta__vial img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 18px 22px rgb(19 19 21 / 16%));
}

/* Each breaks a different edge — the left vial rises past the top, the right
   one drops past the bottom — so the pair reads as two objects standing at the
   band rather than as a symmetrical ornament stuck on either end.

   They lean toward each other, not apart: the left one tilts right and the
   right one tilts left, which turns the two of them into a pair addressing the
   copy between them. Leaning outward, they pointed away from the only thing on
   the band anyone is meant to read.

   The overhang clears the band's own padding and then goes 3rem further, so
   the break is exactly 3rem at every width. A flat negative margin could not
   do it: the padding is fluid, and at 1440 it is 86px, which swallowed a
   3.5rem pull entirely and left both vials sitting inside the band. */
.refer-cta__vial--a {
  grid-column: 1;
  align-self: start;
  margin-top: calc((var(--band-pad) + 3rem) * -1);
  rotate: 9deg;
}

.refer-cta__vial--b {
  grid-column: 3;
  align-self: end;
  margin-bottom: calc((var(--band-pad) + 3rem) * -1);
  rotate: -8deg;
}

@media (max-width: 46rem) {
  .refer-cta {
    /* One column, and the vials flank the copy as a pair beneath it rather
       than squeezing it to nothing between them. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 var(--space-md);
    padding-inline: var(--space-md);
  }

  /* Rows pinned, not left to auto-placement. The copy spans the full row, so
     the first vial could not share its line and was pushed into a row of its
     own ABOVE the words — the pair split either side of the heading instead of
     standing together under it. */
  .refer-cta__copy {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  .refer-cta__vial--a,
  .refer-cta__vial--b {
    width: min(7rem, 100%);
    align-self: end;
    margin-top: var(--space-lg);
    margin-bottom: calc((var(--band-pad) + 1.5rem) * -1);
  }

  .refer-cta__vial--a {
    grid-column: 1;
    grid-row: 2;
    justify-self: end;
  }

  .refer-cta__vial--b {
    grid-column: 2;
    grid-row: 2;
    justify-self: start;
  }
}

/* --- The offer -------------------------------------------------------------
   The line introduces itself, and then asks. The photograph across the head of
   the panel is the same bench shot the home page opens with — five vials, five
   compounds, the whole range in one frame — so the first thing the visitor
   meets is what is actually being sold, not a percentage.

   That is also why this panel carries no pastel of its own. The design
   direction is explicit that photography is where full colour lives and that
   flat fields of pastel are what made an earlier pass read as a swatch dump;
   with a photograph this size in the panel, anything else tinted would be
   competing with it. The card is white, the type is ink, and the colour is in
   the shot. */

.offer-section {
  /* The line's eight colours. The photograph shows five of them; the tab has
     no room for a photograph, so it carries the same range as a strip. */
  --run:
    #f8d7a7 0%, #ede8bb 14%, #cee8d7 28%, #b7c6b8 42%, #c0d8dc 56%,
    #b7d9fc 70%, #bab5f3 82%, #c6b6c4 91%, #ecb9b9 100%;
}

/* --- The tab ---------------------------------------------------------------
   The launcher, and the one piece of this that is a control rather than a
   surface. It used to be a little card with the line's colours barred across
   its head — a miniature of a panel that no longer exists, since the modal
   became a photograph. Left alone it was a leftover: a 190px slab of mono
   capitals, quiet to the point of looking like a cookie notice.

   So it is what the tokens say a control is: a pill, filled in ink, exactly
   the treatment .btn gives the primary action everywhere else. On a page this
   pale it needs no colour of its own to be found, and being the only dark mark
   on the screen is what makes it read as the thing to press. */

.offer-tab {
  position: fixed;
  /* Closer to the corner than the page gutter, which is 56px on a desktop and
     left the tab floating in the middle of the margin. */
  right: clamp(var(--space-sm), 2vw, var(--space-md));
  bottom: clamp(var(--space-sm), 2vw, var(--space-md));
  z-index: var(--z-offer);
  display: inline-flex;
  align-items: baseline;
  gap: 0.3em;
  padding: 0.7rem 1.15rem;
  border: 0;
  border-radius: var(--radius-control);
  background: var(--ink);
  color: #fff;
  font-family: var(--font-text);
  font-size: var(--size-sm);
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--lift-popover);
  /* Arrives once, on the panel's own rise. No fill-mode: once it has run the
     tab is governed by the cascade again, which is what lets it fade out under
     the open panel and at the footer. */
  animation: offer-tab-in var(--dur-slow) var(--ease-out);
  transition:
    opacity var(--dur-base) var(--ease-out),
    translate var(--dur-base) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.offer-tab[hidden] {
  display: none;
}

/* Phones: a smaller tab, so it covers less of what scrolls beneath it
   (buttons and toggles sat under the full-size one at 390px). */
@media (max-width: 40rem) {
  .offer-tab {
    padding: 0.45rem 0.75rem;
    font-size: var(--size-xs);
  }

  .offer-tab .offer-tab__figure {
    font-size: var(--size-sm);
  }
}

@keyframes offer-tab-in {
  from {
    opacity: 0;
    translate: 0 10px;
  }
}

/* The same lift .btn takes: to black, not to a tint. */
.offer-tab:hover {
  background: #000;
  box-shadow: var(--lift-drawer);
}

.offer-tab:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* The tab is what opened the panel; leaving it sitting under the scrim is the
   control still asking for something it already got. */
.offer-section:has(.offer[data-open='true']) .offer-tab {
  opacity: 0;
  pointer-events: none;
}

.offer-tab__label {
  display: inline;
}

.offer-tab__label[hidden] {
  display: none;
}

/* The figure carries the display face, as it does in the announcement bar and
   on the panel; the word beside it stays in the text face so the two read as
   one phrase rather than as a badge with a caption. */
.offer-tab__figure {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
}

/* --- The panel -------------------------------------------------------------
   Deliberately a different kind of object from the researcher gate, the
   theme's other overlay. The gate is a stop sign: centred type, full-bleed
   blur, no way past it, and centred type is reserved for that one place. This
   is an offer, so it aligns left like every other surface on the site, takes
   the cart drawer's plain scrim rather than the gate's blur, and can be left
   from three directions: the close control, the scrim, Escape. */

.offer {
  position: fixed;
  inset: 0;
  z-index: var(--z-offer);
  display: grid;
  place-items: center;
  padding: var(--gutter);
  pointer-events: none;
}

.offer[hidden] {
  display: none;
}

/* Not the gate's backdrop-filter. A blurred-out page says the site is
   unavailable until you answer, which an offer has no right to say. */
.offer__scrim {
  position: absolute;
  inset: 0;
  background: rgb(19 19 21 / 20%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.offer__panel {
  --offer-pad: clamp(var(--space-md), 4vw, var(--space-lg));

  position: relative;
  display: flex;
  flex-direction: column;
  width: min(30rem, 100%);
  max-height: calc(100dvh - var(--gutter) * 2);
  overflow: hidden;
  background: var(--paper-raised);
  border-radius: var(--radius-panel);
  box-shadow: var(--lift-drawer);
  opacity: 0;
  translate: 0 12px;
  transition:
    opacity var(--dur-base) var(--ease-out),
    translate var(--dur-base) var(--ease-out);
}

.offer[data-open='true'] {
  pointer-events: auto;
}

.offer[data-open='true'] .offer__scrim {
  opacity: 1;
}

.offer[data-open='true'] .offer__panel {
  opacity: 1;
  translate: none;
}

/* --- The shot -------------------------------------------------------------
   16:8.5 rather than a square or a 16:9. The bench shot is a wide, shallow
   arrangement — five vials in a row — and a taller crop either loses the
   outer two or fills the frame with ceiling. */
.offer__shot {
  position: relative;
  flex: none;
  aspect-ratio: 16 / 8.5;
  overflow: hidden;
  /* Holds the frame's shape while a lazy image is still arriving, so opening
     the panel never reflows the copy underneath. */
  background: var(--paper-sunk);
}

.offer__shot img {
  width: 100%;
  height: 100%;
  /* 62%, not centre: it puts the bench line low in the frame and the caps of
     the vials just under the top edge, which is the composition the shot was
     taken for. Centred, the crop ran through the labels. */
  object-fit: cover;
  object-position: 50% 62%;
}

.offer__close {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-control);
  /* Over a photograph, so it carries its own ground rather than trusting
     whatever pixel it lands on. */
  background: color-mix(in srgb, #fff 82%, transparent);
  backdrop-filter: blur(6px);
  color: var(--ink);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}

.offer__close:hover {
  background: #fff;
}

.offer__copy {
  flex: 1;
  min-height: 0;
  /* Only the copy scrolls, so the close control stays pinned over the shot on
     a short viewport instead of scrolling out of reach. */
  overflow-y: auto;
  padding: var(--offer-pad);
}

/* Figure and words on one baseline, which is how the announcement bar sets
   this same offer. */
.offer__claim {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.4em;
  margin: 0;
}

.offer__figure {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(var(--size-xl), 8vw, var(--size-2xl));
  line-height: 1;
  letter-spacing: var(--tracking-tight);
}

.offer__heading {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-md);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
}

.offer__lede {
  margin: var(--space-2xs) 0 0;
  max-width: 42ch;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

/* The footer's signup shell, which is the theme's control for exactly this.
   The offer's own version of it had the submit taking half the shell's width,
   which made the field look like the afterthought and the button like the
   subject. The label carries the fix — the body copy above has already said
   what the code is, so the control only has to name the action — and the field
   is given a floor it cannot be squeezed below. */
.offer__form {
  margin-top: var(--space-md);
}

.offer .signup-field input {
  min-width: 7rem;
}

.offer__error {
  margin: var(--space-2xs) 0 0;
  font-size: var(--size-xs);
  color: var(--signal-stop);
}

.offer__terms {
  margin: var(--space-2xs) 0 0;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

/* The code, in the pill printed on every vial label. A pill belongs here in a
   way it did not on a paper document: this panel is an interface surface, and
   the tokens are explicit that controls and labels in this theme are rounded.
   Sized up from the announcement bar's, because in the bar the code is one
   item passing by and here it is the thing you came for. */
.offer__code {
  --code-size: var(--size-md);

  margin-top: var(--space-sm);
}

.offer__action {
  margin: var(--space-md) 0 0;
}

/* Both states are in the DOM and one is shown. display:none rather than an
   opacity trick, so the hidden state's controls are out of the tab order and
   out of the focus trap's reach — trapFocus filters on offsetParent. */
.offer__state {
  display: none;
}

.offer[data-state='ask'] .offer__state--ask,
.offer[data-state='given'] .offer__state--given {
  display: block;
}

/* Above a rule and last in both states. Not a footnote to the offer — the
   condition every sale on this site is made under. */
.offer__note {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

/* --- Cart drawer ----------------------------------------------------------- */

.drawer {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  pointer-events: none;
}

.drawer[hidden] {
  display: none;
}

.drawer__scrim {
  position: absolute;
  inset: 0;
  background: rgb(19 19 21 / 20%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.drawer__panel {
  position: absolute;
  inset-block: 0;
  right: 0;
  width: min(28rem, 100%);
  display: flex;
  flex-direction: column;
  background: var(--paper-raised);
  box-shadow: var(--lift-drawer);
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
}

@media (min-width: 30rem) {
  .drawer__panel {
    border-radius: var(--radius-panel) 0 0 var(--radius-panel);
  }
}

.drawer[data-open='true'] {
  pointer-events: auto;
}

.drawer[data-open='true'] .drawer__scrim {
  opacity: 1;
}

.drawer[data-open='true'] .drawer__panel {
  transform: translateX(0);
}

.drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--rule);
}

.drawer__body {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md) var(--space-lg);
}

.drawer__foot {
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: var(--space-sm);
}

/* The research-use line under the checkout button: a reminder of something the
   footer and the product page both state at readable size, so here it can sit
   down as genuine fine print. 10px, and set tight — 11px still read as a line
   of copy, 9px was further than wanted.

   The colour is the part worth explaining. --ink-faint, which this used before,
   measures 2.6:1 on paper — well under AA, and this is the one sentence in the
   drawer that is a condition of sale rather than decoration. --ink-soft clears
   it easily but reads heavier than fine print should. So: mixed between them.

   The ratio is 70/30 rather than something rounder because --ink-soft is not
   the value written in pryme-tokens.css: css-variables.liquid republishes it
   from a merchant setting, so the live colour is #5A6274, a blue-cast grey
   rather than the neutral one. Mixed 60/40 against that, this measured 4.21:1
   and quietly missed AA. 70/30 clears it. If that setting is ever moved much
   lighter, re-check this line — it is the only text on the site set small
   enough for the margin to be thin. */
.drawer__fineprint {
  margin: 0;
  font-size: 0.625rem;
  line-height: 1.45;
  letter-spacing: 0.005em;
  color: color-mix(in srgb, var(--ink-soft) 70%, var(--ink-faint));
}

.line-item {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  border-bottom: 1px solid var(--rule);
}

.line-item__media {
  aspect-ratio: 1 / 1;
  background: var(--line-color, var(--peptide-wash));
  border-radius: var(--radius-small);
  overflow: hidden;
}

.line-item__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-3xs);
}

.qty {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  overflow: hidden;
}

.qty button {
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  color: var(--ink-soft);
}

.qty button:hover {
  background: var(--paper-sunk);
  color: var(--ink);
}

.qty input {
  width: 2.25rem;
  height: 2rem;
  border: 0;
  text-align: center;
  font-family: var(--font-data);
  font-size: var(--size-xs);
  background: transparent;
}

/* --- Testing story --------------------------------------------------------- */

.testing {
  display: grid;
  gap: clamp(var(--space-lg), 5vw, var(--space-2xl));
}

@media (min-width: 56rem) {
  .testing {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    align-items: start;
  }
}

/* Copy only: one column at reading width, not a half-empty pair. */
@media (min-width: 56rem) {
  .testing--solo {
    grid-template-columns: minmax(0, 44rem);
  }
}

.testing__figure {
  margin: 0;
}

.testing__figure img {
  width: 100%;
  border-radius: var(--radius-surface);
  background: var(--paper-raised);
}

/* --- Protocol -------------------------------------------------------------- */

.protocol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-sm);
}

.protocol__step {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: var(--space-md);
  align-items: start;
  padding: var(--space-lg);
  background: var(--paper-sunk);
  border-radius: var(--radius-surface);
}

/* Across, for sequences short enough to take in at once. The number moves on
   top of its step rather than beside it, because four narrow columns cannot
   spare 2.5rem of their width to a digit. Two columns before one: four steps
   in a single file is the stacked layout with extra height. */
@media (min-width: 46rem) {
  .protocol--row {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }

  .protocol--row .protocol__step {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
  }
}

@media (min-width: 30rem) and (max-width: 45.999rem) {
  .protocol--row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .protocol--row .protocol__step {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
  }
}

.protocol__num {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  background: #fff;
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  font-size: var(--size-xs);
}

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

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

.faq__item {
  background: var(--paper-sunk);
  border-radius: var(--radius-surface);
  padding-inline: var(--space-lg);
}

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-md);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-base);
  letter-spacing: var(--tracking-tight);
}

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

.faq__q:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.faq__mark {
  position: relative;
  flex: none;
  width: 0.85rem;
  height: 0.85rem;
}

.faq__mark::before,
.faq__mark::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1.5px;
  border-radius: 2px;
  background: var(--ink);
}

.faq__mark::after {
  transform: rotate(90deg);
  transition: transform var(--dur-base) var(--ease-out);
}

/* The panel is what gets animated: it wraps the answer so its height is the
   answer's height and nothing else. The bottom padding lives here rather than
   on the item, so it animates in with the content instead of appearing the
   instant the element opens. */
.faq__panel {
  padding-bottom: var(--space-md);
}

.faq__a {
  /* Kept off the disclosure mark's column, so a long answer does not run to a
     different right edge than the question above it. */
  padding-right: var(--space-xl);
}

.faq__item[open] .faq__mark::after {
  transform: rotate(0deg);
}

/* While the collapse plays, the element is still [open] — the script holds the
   attribute back until the animation finishes — so the mark has to be told to
   go back to a plus now rather than snapping at the end. */
.faq__item[data-closing] .faq__mark::after {
  transform: rotate(90deg);
}

/* A little lift on the open item, so an answer reads as the thing in front
   rather than as the row that happens to be taller. */
.faq__item {
  transition: background-color var(--dur-base) var(--ease-out);
}

.faq__item[open]:not([data-closing]) {
  background: var(--paper-raised);
  box-shadow: var(--lift-card);
}

.faq__a {
  padding-bottom: var(--space-md);
  max-width: 62ch;
}

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

.contact-form {
  display: grid;
  gap: var(--space-md);
  max-width: 34rem;
}

.field {
  display: grid;
  gap: var(--space-3xs);
}

.field label {
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.field input,
.field textarea,
.field select {
  width: 100%;
  min-height: 3.125rem;
  padding: var(--space-2xs) var(--space-md);
  background: var(--paper-sunk);
  border: 1px solid transparent;
  border-radius: var(--radius-small);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.field textarea {
  resize: vertical;
  line-height: var(--leading-normal);
  padding-block: var(--space-xs);
}

.field input:hover,
.field textarea:hover {
  border-color: var(--rule-strong);
}

.field input:focus-visible,
.field textarea:focus-visible {
  background: #fff;
  border-color: var(--peptide-deep);
}

/* --- Footer ----------------------------------------------------------------
   Columns, then the research-use statement, then the base line. The compound
   index is one of the columns rather than a full-width band of its own — as a
   band it gave a list of eight links the weight of a section. */
.footer {
  /* Inset from the page edges rather than full-bleed. A rounded block that
     runs edge to edge only shows its curve at the top two corners, so the
     rounding reads as a cropping accident; held off the edges on all four
     sides it reads as a panel the page is sitting on, which is the idea the
     rounding was there to carry in the first place.

     The side inset matches the page gutter, so the footer's own left edge
     lines up with nothing in particular but its CONTENT still lines up with
     every other section's content — the inner container adds the gutter
     again, which is why the columns sit further in than the panel edge. */
  margin-top: var(--space-2xl);
  margin-inline: var(--gutter);
  margin-bottom: var(--gutter);
  padding-block: var(--space-2xl) var(--space-lg);
  border-radius: var(--radius-panel);

  /* The announcement bar's spectrum, reused so the page opens and closes on
     the same band of colour. It is laid over the sunk paper rather than used
     alone, and held far fainter than the bar's 0.34: that strip is 40px of
     tint behind one line of type, while this is a 500px panel carrying four
     columns of small text, and colour that reads as a tint at 40px reads as a
     background illustration at 500. Same gradient, same stops, same order —
     a tenth of the strength.

     Painted with a layered background rather than a ::before so nothing new
     enters the stacking context; the footer's own children keep their order. */
  background-image: linear-gradient(
    to right,
    #f8d7a7 0%,
    #ede8bb 14%,
    #cee8d7 28%,
    #b7c6b8 42%,
    #c0d8dc 56%,
    #b7d9fc 70%,
    #bab5f3 82%,
    #c6b6c4 91%,
    #ecb9b9 100%
  );
  background-color: var(--paper-sunk);
  background-blend-mode: multiply;
}

/* The gradient at full strength would be a rainbow behind body copy. Fading it
   into the paper keeps it as a wash. */
.footer {
  background-image:
    linear-gradient(
      to right,
      rgba(248, 215, 167, 0.30) 0%,
      rgba(237, 232, 187, 0.30) 14%,
      rgba(206, 232, 215, 0.30) 28%,
      rgba(183, 198, 184, 0.30) 42%,
      rgba(192, 216, 220, 0.30) 56%,
      rgba(183, 217, 252, 0.30) 70%,
      rgba(186, 181, 243, 0.30) 82%,
      rgba(198, 182, 196, 0.30) 91%,
      rgba(236, 185, 185, 0.30) 100%
    );
  background-blend-mode: normal;
}

.footer__top {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--space-xl) var(--space-lg);
  padding-bottom: var(--space-xl);
  border-bottom: 1px solid var(--rule-strong);
}

/* Tablet: the signup takes its own full-width row. Squeezed into a third
   auto-fit column it left the email field about 5rem wide ("you@lat"). */
@media (min-width: 36rem) and (max-width: 61.99rem) {
  .footer__signup {
    grid-column: 1 / -1;
  }
}

/* Tracks matched to the number of blocks, so a client running fewer does not
   get an empty column and a stretch of dead ground on the right. The brand and
   the signup are the two that need room; the link columns do not. */
@media (min-width: 62rem) {
  .footer__top {
    grid-template-columns: 1.5fr 1fr 1.6fr;
  }

  .footer__top:has(> :nth-child(4)) {
    grid-template-columns: 1.5fr 1fr 1fr 1.6fr;
  }

  .footer__top:has(> :nth-child(5)) {
    grid-template-columns: 1.5fr 1fr 1fr 1fr 1.6fr;
  }
}

.footer__logo {
  display: inline-block;
  line-height: 0;
}

.footer__logo img {
  width: min(11.9rem, 60%);
  height: auto;
  /* The wordmark asset carries transparent margin baked into it: measured on
     pryme-wordmark.png, the ink starts 87px into a 1254px file, or 6.94% of
     the width. At the footer's render size that is about 13px of nothing, so
     the logo's box lined up with the tagline beneath it while the letters
     visibly did not.

     Pulled back by exactly that fraction. As a translate rather than a
     negative margin because the percentage then resolves against the image's
     own width, which is fluid here, instead of against the column's. The
     header is left alone: its inset was tuned with this margin in place, and
     trimming the asset would move the wordmark off the hero's alignment. */
  translate: -6.94% 0;
}

.footer__tagline {
  margin: var(--space-md) 0 0;
  max-width: 28ch;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* --- Social ----------------------------------------------------------------
   Outlined rather than filled, so five platform marks sit as one row of this
   interface instead of five houses' worth of brand colour. */
.social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: var(--space-md) 0 0;
  padding: 0;
  list-style: none;
}

.social__link {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.social__link:hover {
  color: var(--ink);
  border-color: var(--ink);
}

.social__link:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.footer__heading {
  margin: 0 0 var(--space-xs);
  font-size: var(--size-sm);
  font-weight: 600;
}

.footer__list {
  display: grid;
  gap: var(--space-3xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer__list a {
  font-size: var(--size-sm);
  color: var(--ink-soft);
  text-decoration: none;
}

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

/* The compound index uses the shared marker system, so whatever is chosen for
   a compound's name here is the same mark used beside one in a review. The
   thumbnail is the marker set on it: in a column this short the image cost is
   worth it, because it is the only mark that shows what actually arrives. */
.footer__list--compounds {
  gap: var(--space-3xs);
}

.footer__list--compounds .cmk {
  color: var(--ink-soft);
}

.footer__list--compounds .cmk:hover {
  color: var(--ink);
}

/* --- Signup ---------------------------------------------------------------
   The offer leads, at a size that reads as one. Nobody joins a list to receive
   notifications. */
.footer__offer {
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-2xs);
}

.footer__signup-body {
  margin: 0 0 var(--space-sm);
  max-width: 34ch;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* One shell around the field and its button, so the signup reads as a single
   control rather than as two stacked ones.

   Named for the job rather than for the footer, because the offer modal asks
   for the same address in the same shell. Two copies of this under two names
   would drift the moment one of them was adjusted. */
.signup-field {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 0.3rem 0.3rem 0.3rem var(--space-sm);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: var(--paper);
  transition: border-color var(--dur-fast) var(--ease-out);
}

.signup-field:focus-within {
  border-color: var(--ink);
}

.signup-field input {
  flex: 1;
  min-width: 0;
  min-height: 2.25rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--size-sm);
  color: var(--ink);
}

/* The shell already shows focus, and two rings at once looks like a fault. */
.signup-field input:focus {
  outline: none;
}

.signup-field input::placeholder {
  color: var(--ink-faint);
}

.signup-field button {
  flex: none;
  min-height: 2.25rem;
  padding-inline: var(--space-sm);
  border: 0;
  border-radius: var(--radius-control);
  background: var(--ink);
  color: #fff;
  font: inherit;
  font-size: var(--size-sm);
  font-weight: 600;
  cursor: pointer;
}

.footer__terms {
  margin: var(--space-2xs) 0 0;
  font-size: var(--size-xs);
  color: var(--ink-faint);
}

.footer__ok {
  margin: var(--space-2xs) 0 0;
  font-size: var(--size-sm);
  color: var(--signal-ok);
}

/* --- The statement ---------------------------------------------------------
   Plain text, full width, above the base line. It was in a bordered panel with
   a heavy rule down one side — the treatment a promotion gets, and dressing a
   legal statement as a promotion is the wrong signal in both directions. */
/* The full research-use statement. Set a step below body size for the same
   reason the reviews disclaimer is: it is the terms the whole catalogue is
   sold under, not something to be read before anything else on the page. One
   step to --size-xs and no further, keeping --ink-soft, because a legal
   statement nobody can read is worth nothing. */
.footer__legal {
  margin: var(--space-xl) 0 0;
  max-width: 92ch;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.footer__base {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  color: var(--ink-faint);
}

.footer__copyright {
  margin: 0;
}

.footer__policies {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer__policies a {
  color: var(--ink-faint);
  text-decoration: none;
}

.footer__policies a:hover {
  color: var(--ink);
}

/* --- Footer, centred on a phone ---------------------------------------------
   Once the columns stack, every block is the full width of the screen and a
   left rule down one edge is doing nothing but pointing at the gutter. Centred,
   the stack reads as one column rather than four left-aligned blocks that
   happen to sit under each other.

   Held to 48rem rather than the 62rem where the columns actually stack: between
   those two the lines are long enough that centring them hurts, and the layout
   is a narrow desktop rather than a phone. */
@media (max-width: 48rem) {
  .footer__top,
  .footer__legal,
  .footer__base {
    text-align: center;
  }

  /* The wordmark is pulled left by its own transparent margin so its letters
     line up with the text beneath it. Centred there is no rule to line up
     with, and the asset's margins balance each other, so the correction comes
     back off.

     margin-inline is what actually centres it. The reset in pryme-base.css
     makes every image display:block, and a block box ignores the text-align
     this footer sets — so the link stretched the full 310px column while the
     186px wordmark sat against its left edge, centred by every measure except
     the one you can see. */
  .footer__logo img {
    translate: none;
    margin-inline: auto;
  }

  .social,
  .footer__base,
  .footer__list .cmk {
    justify-content: center;
  }

  .footer__tagline,
  .footer__signup-body,
  .footer__legal {
    margin-inline: auto;
  }

  /* Typing into a centred field is the one place this reads as a mistake
     rather than a choice. */
  .signup-field {
    text-align: left;
  }
}

/* --- Marquee ---------------------------------------------------------------
   A breakout band between two sections. Full-bleed on purpose: the page runs
   contained, split, contained, split, and this is the one thing on it that
   touches both edges without being a panel.

   The track holds the item list TWICE and travels exactly -50% of its own
   width, so the second copy lands where the first began. Any other distance
   leaves a visible jump at the loop point, and a percentage of the TRACK is
   the only measure that stays correct when the client edits the claims. */
.marquee {
  overflow: hidden;
  padding-block: var(--space-md);
  /* Nothing inside is a link, so nothing here should swallow a drag or a
     text selection the visitor meant for the sections either side. */
  user-select: none;
}

.marquee--ink {
  background: var(--ink-ground);
  color: #fff;
}

.marquee--paper {
  background: var(--paper);
  color: var(--ink);
}

.marquee--sunk {
  background: var(--paper-sunk);
  color: var(--ink);
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-run var(--marquee-speed, 45s) linear infinite;
}

.marquee__run {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-lg), 5vw, var(--space-2xl));
  margin: 0;
  padding-left: clamp(var(--space-lg), 5vw, var(--space-2xl));
  list-style: none;
}

.marquee__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  white-space: nowrap;
}

/* A square, cycling through the compound palette — the separator is the same
   colour system as the rest of the page, and it is not a dot. */
.marquee__mark {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  background: var(--mark);
}

.marquee__text {
  font-size: var(--size-xs);
  letter-spacing: 0.1em;
}

@keyframes marquee-run {
  to {
    transform: translateX(-50%);
  }
}

/* Motion here is decoration, so it goes entirely rather than being slowed: the
   claims still read, they simply sit still. */
@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none;
  }
}

/* --- Announcement bar -----------------------------------------------------
   The ground is a spectrum through all eight compound colours — a chromatogram,
   which is the assay that actually validates every lot — so the colour system
   introduces itself before any product does. The code sits in a rounded outline
   pill, the same treatment printed on every vial label. */

.announce {
  position: relative;
  isolation: isolate;
  border-bottom: 1px solid var(--rule);
  background: var(--paper-raised);
}

.announce__spectrum {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to right,
    #f8d7a7 0%,
    #ede8bb 14%,
    #cee8d7 28%,
    #b7c6b8 42%,
    #c0d8dc 56%,
    #b7d9fc 70%,
    #bab5f3 82%,
    #c6b6c4 91%,
    #ecb9b9 100%
  );
  /* Held right down: this is a tint under type, not a rainbow. */
  opacity: 0.34;
}

.announce__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
}

.announce__messages {
  position: relative;
  display: grid;
  /* Centred in the cell, not stretched to it.

     All three messages share one grid cell so the rotator can cross-fade
     without reflowing. Grid stretches items by default, so the cell takes the
     height of the LONGEST message and the other two were pinned to the top of
     it — on a phone, where the certificate line wraps to two, the short
     messages sat in a 53px box with 25px of dead air under them. */
  align-items: center;
}

/* Stacked in one cell so rotating between them costs no layout shift. */
.announce__message {
  grid-area: 1 / 1;
  display: flex;
  /* Baseline, not centre. The figure is larger than the text beside it and
     carries line-height 1, so its box is tight around the glyphs while the
     text's box has leading — centring the two BOXES therefore drops the big
     number below the small text's baseline, which is what made the 15% look
     like it had slipped. Baseline alignment is what a figure set into running
     text wants in the first place. */
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  font-size: var(--size-sm);
  color: var(--ink);
  text-align: center;
  /* The incoming message waits one duration before it starts. Both messages
     were crossfading at once, so mid-transition each sat near 50% and the two
     were legible on top of each other — which reads as a rendering fault, not
     as a rotator. The delay is on the visible state and zeroed on the hidden
     one below, so the outgoing message leaves immediately and the next arrives
     into an empty bar. */
  transition: opacity var(--dur-base) var(--ease-out) var(--dur-base);
}

.announce__message[aria-hidden='true'] {
  opacity: 0;
  pointer-events: none;
  transition-delay: 0s;
}

.announce__figure {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--size-md);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
}

/* The vial label's own pill: hairline outline, fully rounded, quiet. Named for
   the treatment rather than for the bar, since it is the packaging's own mark
   for a code and any surface that quotes one should be able to reach for it.
   The offer slip deliberately does not: a pill sitting on printed stock is the
   one element that would still look like a web page, so the slip writes its
   code on a ruled blank instead. */
.code-pill {
  /* In em, so the pill keeps its proportions if the size is ever changed. */
  padding: 0.33em 1em;
  border: 1px solid var(--ink);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-data);
  font-size: var(--code-size, 0.68rem);
  letter-spacing: 0.06em;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.code-pill:hover {
  background: var(--ink);
  color: var(--paper);
}

.code-pill[data-copied='true'] {
  background: var(--signal-ok);
  border-color: var(--signal-ok);
  color: #fff;
}

.announce__link {
  font-size: var(--size-sm);
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--ink);
  padding-bottom: 1px;
}

@media (max-width: 40rem) {
  .announce__figure {
    font-size: var(--size-base);
  }

  .announce__message {
    font-size: var(--size-xs);
  }
}

/* 100vw includes the scrollbar, so the bleed would otherwise add a hair of
   horizontal page scroll. Clip rather than hidden: clip does not create a
   scroll container, so sticky descendants keep working. */
.carousel-section {
  overflow-x: clip;
}

/* --- Catalogue page --------------------------------------------------------
   Header, facet bar, grid. The header gets a real measure and its own eyebrow
   rather than being a bare h1 on a white field, which is what a listing page
   built from the default template looks like. */
.plp__head {
  margin-bottom: clamp(var(--space-lg), 4vw, var(--space-xl));
}

.plp__title {
  max-width: 14em;
}

.plp__intro {
  margin-top: var(--space-md);
  max-width: 56ch;
}

.plp__empty {
  padding-block: var(--space-2xl);
  text-align: center;
  color: var(--ink-soft);
}

.plp__pages {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--space-2xl);
}

.plp__note {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

/* --- Category pills -------------------------------------------------------
   One row, no legend, no counts. All leads and is the reset. Toggles rather
   than links, so aria-pressed rather than aria-current, and the on state is
   FILLED — on a page whose entire content changes with the selection, the
   current state has to be unmistakable at a glance.

   The row scrolls rather than wraps: a wrapped second row of two pills reads
   as an accident, and there are few enough categories that a swipe reaches all
   of them. */
.pills {
  display: flex;
  gap: var(--space-2xs);
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-3xs);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.pills::-webkit-scrollbar {
  display: none;
}

.pill {
  flex: none;
  padding: 0.55rem 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--size-sm);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.pill:hover {
  border-color: var(--ink);
}

.pill:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.pill[aria-pressed='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

/* --- Catalogue controls row -----------------------------------------------
   Search on the left, sort on the right. Search leads because someone
   shopping compounds usually arrives knowing the name they want. */
.plp__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.search-field {
  position: relative;
  flex: 1 1 20rem;
  max-width: 28rem;
}

.search-field__icon {
  position: absolute;
  top: 50%;
  left: var(--space-sm);
  translate: 0 -50%;
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  pointer-events: none;
}

.search-field__input {
  width: 100%;
  min-height: 2.75rem;
  /* Left padding clears the icon; the icon's box is 17px plus its inset. */
  padding: 0.5rem var(--space-sm) 0.5rem 2.6rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: var(--size-sm);
}

.search-field__input::placeholder {
  color: var(--ink-faint);
}

.search-field__input:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-color: var(--ink);
}

/* Chrome's own clear affordance is a grey cross that ignores the theme. Hidden
   in favour of Escape and the All pill, both of which are documented in the
   script. */
.search-field__input::-webkit-search-cancel-button {
  appearance: none;
}

/* --- Select ---------------------------------------------------------------
   Two states of one component. Before the script runs, a native <select> with
   appearance stripped and our own mark — which is a real, working control, so
   nothing is broken without JavaScript. After, .select--custom hides that
   select and shows a button and a listbox instead.

   The option list is the reason the second state exists at all: a native
   select's list is drawn by the operating system, so its radius, font and
   colours cannot be reached from CSS. Stripping the appearance styles the
   closed control and nothing else. */
.select {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.select select,
.select__button {
  min-height: 2.75rem;
  /* Right padding = the mark's inset plus its width, doubled, so the longest
     option label can never run under the chevron. */
  padding: 0.5rem 2.75rem 0.5rem var(--space-sm);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: var(--size-sm);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}

.select select {
  appearance: none;
}

.select select:hover,
.select__button:hover {
  border-color: var(--ink);
}

.select select:focus-visible,
.select__button:focus-visible,
.select__list:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-color: var(--ink);
}

.select__mark {
  position: absolute;
  top: 50%;
  right: var(--space-sm);
  translate: 0 -50%;
  display: grid;
  place-items: center;
  color: var(--ink-soft);
  pointer-events: none;
  /* The chevron icon points right; a closed select's mark points down, and an
     open one points up. Rotation only — the icon is never swapped, so the two
     states animate into each other. */
  rotate: 90deg;
  transition: rotate var(--dur-base) var(--ease-out);
}

.select.is-open .select__mark {
  rotate: -90deg;
}

/* --- Select: enhanced ------------------------------------------------------
   The native select is kept in the DOM because it is still what submits. It is
   hidden with the clip technique rather than display:none: a display:none
   control is excluded from form submission in some browsers, and the whole
   point of leaving it there is that it submits. */
.select--custom select {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  pointer-events: none;
}

.select__button {
  display: inline-flex;
  align-items: center;
  width: 100%;
}

.select__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.select__list {
  position: absolute;
  z-index: 20;
  top: calc(100% + var(--space-3xs));
  right: 0;
  min-width: 100%;
  /* Tall enough that a nine-option sort list does not scroll at all, capped
     against the viewport so it still fits on a short screen. The scrollbar
     below is for lists this cannot hold, not for this one. */
  max-height: min(24rem, 60vh);
  margin: 0;
  /* space-2xs, not 3xs. The scrollbar lives inside this padding box, so at 4px
     the track sat almost against the border and crowded the corner radius. */
  padding: var(--space-2xs);
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-surface);
  box-shadow: 0 18px 40px rgb(19 19 21 / 14%);

  /* Firefox. `thin` plus a themed thumb, rather than the platform's default
     grey slab. */
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}

/* WebKit. The thumb is inset from its own track by a transparent border and
   clipped to the padding box, which is the only way to put space around a
   scrollbar thumb — width alone just makes a narrower bar hard against the
   edge. */
.select__list::-webkit-scrollbar {
  width: 13px;
}

.select__list::-webkit-scrollbar-track {
  background: transparent;
}

.select__list::-webkit-scrollbar-thumb {
  background-color: var(--rule-strong);
  background-clip: padding-box;
  border: 4px solid transparent;
  border-radius: var(--radius-control);
}

.select__list::-webkit-scrollbar-thumb:hover {
  background-color: var(--ink-faint);
}

.select__option {
  /* Extra right padding so a long label keeps clear of the thumb when the list
     does scroll, and the rows still look centred when it does not. */
  padding: 0.55rem var(--space-md) 0.55rem var(--space-sm);
  border-radius: var(--radius-control);
  font-size: var(--size-sm);
  white-space: nowrap;
  cursor: pointer;
}

/* Hover and keyboard activation share one appearance, because they mean the
   same thing — this is the option that Enter would take. */
.select__option:hover,
.select__option.is-active {
  background: var(--paper-sunk);
}

/* The chosen option, which is not the same as the active one: it stays marked
   while the arrow keys move through the list. */
.select__option[aria-selected='true'] {
  font-weight: 600;
}

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

.plp__count {
  margin: 0 0 var(--space-md);
}

@media (max-width: 46rem) {
  .search-field {
    flex-basis: 100%;
    max-width: none;
  }

  .plp__sort {
    flex: 1;
  }

  .select,
  .select select {
    width: 100%;
  }
}

/* --- Carousel -------------------------------------------------------------
   A real scroll container with snap points, not a JS slider. It works without
   the script, scrolls by touch and trackpad natively, and keyboard users get
   correct behaviour because tabbing to a card scrolls it into view. */

.carousel {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Contained, not full-bleed. It scrolls within the container so both its
     first and last cards sit on the page's own margins — a track running off
     the viewport edge read as a layout accident rather than as a carousel. */
  padding-block: var(--space-2xs) var(--space-md);
}

.carousel::-webkit-scrollbar {
  display: none;
}

/* A whole number of cards per view, never a sliver.

   The cards used to be a fixed clamp(14rem, 20vw, 17rem): at 1920 that is 272px
   and five of them plus their gaps came to 1456 of the container's 1500, so a
   sixth card showed 20px of itself at the right edge and read as a rendering
   fault rather than as an invitation to scroll. The arrows are the scroll
   affordance; a cropped card is not.

   The track is the grid container at the scrollport's own width, so the auto
   columns can be a percentage of it. Percentages could not do this while the
   track was `width: max-content`, because the track's width was itself derived
   from the cards. --per is the number of cards on screen, whole at every
   breakpoint. */
.carousel__track {
  --per: 1;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc(
    (100% - (var(--per) - 1) * var(--space-md)) / var(--per)
  );
  gap: var(--space-md);
}

@media (min-width: 34rem) {
  .carousel__track {
    --per: 2;
  }
}

@media (min-width: 46rem) {
  .carousel__track {
    --per: 3;
  }
}

@media (min-width: 62rem) {
  .carousel__track {
    --per: 4;
  }
}

@media (min-width: 80rem) {
  .carousel__track {
    --per: 5;
  }
}

.carousel__item {
  scroll-snap-align: start;
  /* Cards in a row do not stretch on their own; without this each card is only
     as tall as its own text and the row's baselines go ragged. */
  display: flex;
}

.carousel__item > .card {
  width: 100%;
}

.carousel__controls {
  display: flex;
  gap: var(--space-3xs);
}

.carousel__arrow {
  border: 1px solid var(--rule-strong);
}

.carousel__arrow:hover {
  border-color: var(--ink);
}

.carousel__arrow[disabled] {
  opacity: 0.3;
  cursor: not-allowed;
}

/* The prev arrow is the next arrow, mirrored. */
[data-carousel-prev] .icon {
  transform: scaleX(-1);
}

/* No arrows on touch: the track scrolls by swipe, and --per is 1 there so a
   single whole card fills the view. */
@media (max-width: 46rem) {
  .carousel__controls {
    display: none;
  }
}

/* --- Box builder ----------------------------------------------------------
   Picker on the left, a summary that sticks alongside it on the right. */

/* .section-head carries the flex row, the hairline and the bottom margin. The
   only thing left to say is how wide each half runs. `ch` resolves against the
   element's own font size, which is why the old 52ch on the wrapper — set in
   body type but applied to a 68px heading — came out at 457px and squeezed
   both halves into a third of the row. */
/* Capped in em on the heading itself, so the measure resolves against the
   heading's own size and tracks its clamp. On the wrapper the same number was
   resolving against 17px body type and wrapped a 68px display line after two
   words. */
.box__head .section-head__text > :is(h1, h2) {
  max-width: 9em;
}

.standard__head .section-head__text > .h2 {
  max-width: 9em;
}

.box__lede {
  max-width: 44ch;
}

.box__layout {
  display: grid;
  gap: clamp(var(--space-lg), 4vw, var(--space-2xl));
  align-items: start;
}

@media (min-width: 62rem) {
  .box__layout {
    grid-template-columns: minmax(0, 1fr) 22rem;
  }

  .box__summary {
    position: sticky;
    top: calc(var(--header-height) + var(--space-md));
  }
}

.box__step {
  display: block;
  color: var(--ink-faint);
  letter-spacing: var(--tracking-wide);
  margin-bottom: var(--space-sm);
}

/* --- Tier selector --- */

.box__tiers {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-xl);
}

.box__tier-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.tier {
  position: relative;
}

.tier input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.tier__body {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: 3rem;
  padding-inline: var(--space-md);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  font-size: var(--size-sm);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.tier input:checked + .tier__body {
  border-color: var(--ink);
  background: var(--ink);
  color: #fff;
}

.tier input:focus-visible + .tier__body {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.tier__save {
  font-size: 0.66rem;
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-control);
  background: var(--signal-ok);
  color: #fff;
}

/* --- Picker grid --- */

.box__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--space-2xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* On a phone the picker is a single column, so eight compounds is eight full
   rows to scroll past before the summary. Four, and the rest behind a control.

   Capped at the width where the rail takes over: above 46rem the picker is a
   horizontal scroller that already shows everything, so there is nothing to
   collapse and no button. */
/* Named for what it does rather than as a sibling of .box__more, which already
   exists in this section as the summary's "bigger boxes" link — reusing that
   name applied this display:none to it and quietly hid it on the home page.

   Hidden by default in both directions: by the attribute until the script has
   counted the compounds and found more than four, and by display above the
   breakpoint where the rail makes the control pointless. */
.box__reveal {
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  width: 100%;
  margin-top: var(--space-2xs);
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-small);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--size-sm);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}

.box__reveal:hover {
  border-color: var(--ink);
}

.box__reveal[aria-expanded='true'] .box__reveal-show,
.box__reveal:not([aria-expanded='true']) .box__reveal-hide {
  display: none;
}

@media (max-width: 45.99rem) {
  .box__picker:not([data-expanded='true']) .pick:nth-child(n + 5) {
    display: none;
  }

  .box__reveal {
    display: inline-flex;
  }
}

.pick__btn {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  width: 100%;
  padding: var(--space-2xs);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-small);
  text-align: left;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.pick__btn:hover:not([disabled]) {
  border-color: var(--acc-deep);
}

.pick__btn[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* --- Chosen ----------------------------------------------------------------
   The colour lands on the PRODUCT, not in a ring around the card.

   Selection used to be a coloured border plus a tinted fill on a rounded
   rectangle, which is the default shape every selectable card takes and reads
   as such. What changed here is the object: the vial's ground goes from a pale
   wash to its compound's real colour, so the thing that is in the box looks
   like it is in the box. The card's own border never changes.

   That leaves the border free to mean one thing only — hover and focus — and
   it leaves the colour where it carries information. */
.pick:has([data-chosen]:not([data-chosen=''])) .pick__media {
  background: linear-gradient(
    160deg,
    color-mix(in srgb, var(--acc) 42%, #fff),
    color-mix(in srgb, var(--acc) 78%, #fff)
  );
}

/* --- The stepper -----------------------------------------------------------
   Takes the plus sign's corner once the compound is in the box, so one control
   becomes the other rather than a second one appearing beside it. Solid in the
   compound's deep tone: it is the only saturated thing on the card, which is
   what a control that is currently doing something should be. */
.pick {
  position: relative;
}

.pick:has([data-chosen]:not([data-chosen=''])) .pick__add {
  visibility: hidden;
}

.pick__qty {
  position: absolute;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.1rem;
  padding: 0.15rem;
  border-radius: var(--radius-control);
  background: var(--acc-deep);
  color: #fff;
  box-shadow: 0 2px 8px rgb(19 19 21 / 22%);
}

.pick__step {
  display: grid;
  place-items: center;
  width: 1.4rem;
  height: 1.4rem;
  padding: 0;
  border: 0;
  border-radius: calc(var(--radius-control) - 2px);
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}

.pick__step:hover:not([disabled]) {
  background: rgb(255 255 255 / 20%);
}

.pick__step[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

.pick__step:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -2px;
}

.pick__count {
  min-width: 1ch;
  text-align: center;
  font-size: var(--size-xs);
  line-height: 1;
}

.pick__media {
  flex: none;
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3.5rem;
  border-radius: var(--radius-small);
  background: color-mix(in srgb, var(--acc) 22%, #fff);
  overflow: hidden;
}

.pick__media img {
  height: 90%;
  width: auto;
  object-fit: contain;
}

.pick__meta {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.pick__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-sm);
  letter-spacing: var(--tracking-tight);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pick__sub {
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.pick__add {
  flex: none;
  margin-left: auto;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-control);
  background: var(--acc);
  color: var(--acc-deep);
}

/* --- Summary --- */

.box__summary {
  padding: var(--space-md);
  background: var(--paper-sunk);
  border-radius: var(--radius-surface);
}

.box__summary-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--rule-strong);
}

.box__count {
  color: var(--ink-soft);
}

.box__slots {
  list-style: none;
  margin: var(--space-sm) 0;
  padding: 0;
  display: grid;
  gap: var(--space-3xs);
}

.slot {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  /* One height whether the slot is empty or full. A filled slot is 4px of
     padding around a 44px thumbnail and an empty one was 8px around a line of
     text — 52px against 37px, so every pick grew the summary by 15px and the
     page shifted under the cursor that caused it. The slot is a fixed space
     waiting to be occupied, so it should measure the same either way. */
  min-height: 3.25rem;
  padding: var(--space-3xs);
  background: var(--paper-raised);
  border-radius: var(--radius-small);
}

.slot--empty {
  background: transparent;
  border: 1px dashed var(--rule-strong);
  border-left-width: 1px;
  padding-inline: var(--space-xs);
  color: var(--ink-faint);
  font-size: var(--size-sm);
}

.slot__index {
  color: var(--ink-faint);
  margin-right: var(--space-2xs);
}

/* The compound's colour, on the compound — the same move the picker makes. It
   was a 3px coloured stripe down the left edge of a rounded rectangle, which
   is the generic "accent card" and said nothing about which vial was in the
   slot. */
.slot__media {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.75rem;
  border-radius: 6px;
  background: linear-gradient(
    160deg,
    color-mix(in srgb, var(--acc) 38%, #fff),
    color-mix(in srgb, var(--acc) 72%, #fff)
  );
  overflow: hidden;
}

.slot__media img {
  height: 92%;
  width: auto;
}

.slot__meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.slot__name {
  font-size: var(--size-sm);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.slot__sub {
  font-size: 0.66rem;
  color: var(--ink-soft);
}

.slot__remove {
  margin-left: auto;
  width: 1.75rem;
  height: 1.75rem;
  display: grid;
  place-items: center;
  border-radius: var(--radius-control);
  color: var(--ink-faint);
  font-size: 1.1rem;
  line-height: 1;
}

.slot__remove:hover {
  background: var(--rule);
  color: var(--ink);
}

.box__free {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs);
  background: color-mix(in srgb, var(--signal-ok) 10%, #fff);
  border-radius: var(--radius-small);
  font-size: var(--size-sm);
}

.box__free-media {
  flex: none;
  width: 2rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  overflow: hidden;
}

.box__free-media img {
  height: 100%;
  width: auto;
}

.box__free-text {
  min-width: 0;
}

.box__free-flag {
  margin-left: auto;
  font-size: 0.6rem;
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-control);
  background: var(--signal-ok);
  color: #fff;
}

.box__totals {
  margin: var(--space-md) 0 var(--space-sm);
  display: grid;
  gap: var(--space-3xs);
}

.box__totals > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
  font-size: var(--size-sm);
}

.box__totals dt {
  color: var(--ink-soft);
}

.box__totals dd {
  margin: 0;
}

.box__totals-final {
  padding-top: var(--space-2xs);
  border-top: 1px solid var(--rule-strong);
}

.box__totals-final dt {
  color: var(--ink);
  font-weight: 500;
}

.box__note {
  margin-block: var(--space-2xs) var(--space-md);
  text-align: center;
}

/* --- Box teaser -----------------------------------------------------------
   Copy one side, a worked example of a filled box the other. */

.box-teaser__inner {
  display: grid;
  gap: clamp(var(--space-lg), 4vw, var(--space-2xl));
  align-items: center;
}

@media (min-width: 56rem) {
  .box-teaser__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

.box-teaser__list {
  list-style: none;
  margin: var(--space-lg) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-2xs);
}

.box-teaser__list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.box-teaser__check {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.4rem;
  height: 1.4rem;
  margin-top: 0.1rem;
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--signal-ok) 14%, #fff);
  color: var(--signal-ok);
}

.box-teaser__card {
  padding: var(--space-md);
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  box-shadow: var(--lift-card);
}

.box-teaser__card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xs);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--rule);
}

.box-teaser__tag {
  color: var(--ink-soft);
}

.box-teaser__grid {
  list-style: none;
  margin: var(--space-sm) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2xs);
}

.box-teaser__tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.box-teaser__tile-media {
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  border-radius: var(--radius-small);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--acc) 16%, #fff),
    color-mix(in srgb, var(--acc) 40%, #fff)
  );
  overflow: hidden;
}

.box-teaser__tile-media img {
  height: 88%;
  width: auto;
  object-fit: contain;
}

.box-teaser__tile-name {
  font-size: var(--size-xs);
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Box builder: compact layout -----------------------------------------
   The homepage variant. Copy across the top, the summary and a horizontally
   scrolling picker side by side beneath it, so the whole thing is interactive
   without taking a full screen. */

.box--compact .box__head .section-head__text > :is(h1, h2) {
  max-width: 11em;
}

@media (min-width: 62rem) {
  .box--compact .box__layout {
    grid-template-columns: minmax(0, 1fr) 21rem;
  }

  .box--compact .box__summary {
    position: static;
  }
}

/* The picker becomes a two-row horizontal scroller rather than a tall grid.
   The rail is what lets the section carry thirteen compounds without becoming
   a full screen, so it stays — but it stretches to the summary's height and
   the cards turn vertical to fill it.

   Row-height cards left a 407px void: 217px of picker beside a 624px summary,
   which was the largest empty area on the page. A rail of vertical cards fills
   the same column with the same eight products and shows each vial at a size
   worth looking at. */
@media (min-width: 46rem) {
  .box--compact .box__layout {
    align-items: stretch;
  }

  /* So the grid below can take a percentage height from the column. */
  .box--compact .box__picker {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .box--compact .box__grid {
    grid-template-columns: none;
    grid-auto-flow: column;
    /* Four cards per view at any column width, with a fifth peeking once the
       catalogue outgrows eight — which is the only affordance telling anyone
       the rail scrolls. A fixed 10.5rem left 280px of empty track beside
       today's eight products. */
    grid-auto-columns: minmax(10.5rem, 24%);
    /* minmax(0, 1fr), not auto: the rows have to divide the column's height
       between them, and `auto` would size them to their content instead. */
    grid-template-rows: repeat(2, minmax(0, 1fr));
    flex: 1;
    min-height: 15rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
    padding-bottom: var(--space-2xs);
  }

  .box--compact .pick {
    scroll-snap-align: start;
  }

  /* Vertical: the vial above its name, the way the catalogue cards read. */
  .box--compact .pick__btn {
    flex-direction: column;
    align-items: stretch;
    height: 100%;
  }

  .box--compact .pick__media {
    position: relative;
    flex: 1;
    width: auto;
    height: auto;
    min-height: 0;
  }

  /* Taken out of flow, which is the whole point. Left in flow the render's own
     1920x4952 aspect drove the flex item's height: each card grew to 637px,
     the two rows to 1290, and the summary stretched to match a 1326px row —
     the layout sized itself from the photograph instead of from the page. Out
     of flow the card takes its height from the grid row, the row from the
     column, and the column from the summary beside it.

     Centred by auto margins against a zero inset, and bounded by max-width and
     max-height so the vial scales down inside whatever box it lands in without
     ever being the thing that sets the box. */
  .box--compact .pick__media img {
    position: absolute;
    inset: 0;
    margin: auto;
    width: auto;
    height: auto;
    max-width: 82%;
    max-height: 88%;
  }

  /* The plus button leaves the flow and pins to the media's corner, so it
     cannot push the name out of the card at this narrower width. */
  .box--compact .pick__qty {
    top: var(--space-xs);
    right: var(--space-xs);
  }

  .box--compact .pick__add {
    position: absolute;
    /* space-xs, not 2xs, and the same value the catalogue card's chips use.
       The card's corners are rounded, so at 8px the button's own corner sat
       against the curve — and unlike a chip this one is a target, so it wants
       clear ground around it as well as clearance from the edge. */
    top: var(--space-xs);
    right: var(--space-xs);
  }

  .box--compact .pick__meta {
    padding-inline: var(--space-3xs);
  }
}

.box--compact .box__slots {
  margin-block: var(--space-sm);
}

/* Two lines reserved. The note swings between "Choose 1 more compound to
   complete your box." and "Your box is ready." — two lines against one — and
   the summary jumped by a line at the exact moment the visitor completed the
   box, which is the worst possible moment for the button under it to move. */
.box__note {
  /* Two lines reserved. The note swings between "Choose 1 more compound to
     complete your box." and "Your box is ready." — two lines against one — and
     the summary jumped by a line at the exact moment the visitor completed the
     box, which is the worst possible moment for the button under it to move.

     `lh` is the unit that means what this needs: two line boxes of whatever
     this element's line-height currently is. The em fallback ahead of it is
     3.1em for today's 1.55 leading — an earlier guess of 2.6em came out at
     39px against two lines of 46.5px, so it clamped the short message and
     still let the long one push past. */
  min-height: 3.1em;
  min-height: 2lh;
}

.box--compact .box__more {
  margin-top: var(--space-sm);
  text-align: center;
}

/* Slots read as a filling row, so an empty one should look like a waiting
   space rather than a list item. */
.box--compact .slot--empty {
  justify-content: center;
  gap: var(--space-2xs);
}

/* --- Proof band -----------------------------------------------------------
   The page's dark moment. Copy on one side, the figures on the other, with a
   chromatogram trace behind both. */

.proof {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* One centred column: statement, then the evidence, then the figures the
   evidence supports. Not a two-column copy-and-panel split, because the hero
   and the blend above it are already exactly that and a third made the
   left-right alternation feel mechanical. */
/* Centred by auto margins, not by a grid on a wrapper. The wrapper is gone
   with the two-column layout, and a capped block is left-aligned by default —
   which is how a head that was meant to be centred ended up sitting over the
   left third of a full-width band. */
.proof__head {
  max-width: 54ch;
  margin-inline: auto;
  margin-bottom: clamp(var(--space-lg), 4vw, var(--space-xl));
  /* No rule under the head. The trace's baseline and the stages' top border
     are already two horizontal lines within 100px of each other; a third made
     the section look ruled rather than composed. */
  border-bottom: 0;
  padding-bottom: 0;
}

/* The head is the WIDER measure and the heading is capped inside it, not the
   other way round. Capping the head at the heading's measure also capped the
   paragraph, which then wrapped at 34ch into a tall narrow block — a max-width
   on a child cannot exceed its parent's. */
.proof__head {
  text-align: center;
}

/* In em, so the measure tracks the heading's own clamp rather than the body
   size this element's ch would resolve against. */
.proof__head .h2 {
  max-width: 9em;
  margin-inline: auto;
}

.proof__lede {
  margin-top: var(--space-md);
}

/* An element in the flow at the container's width, not a background. That is
   what lets the figures below line up with it — and at 0.13 opacity behind
   text it was invisible anyway, so the drawing was doing no work. */
.proof__trace {
  display: block;
  width: 100%;
  height: clamp(4.5rem, 8vw, 7rem);
  color: #fff;
  opacity: 0.3;
  pointer-events: none;
}

.proof__trace svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Four stages in order, hanging off the trace's baseline. The rule along the
   top IS that baseline continued, which is what makes the sequence read as one
   run rather than as four separate columns — and it is a rule, not a node per
   stage, because a dot at each step is exactly the shape this theme does not
   use.

   No trailing rule and no outer border: a closed box would turn the sequence
   back into the figures table this replaced. */
.stages {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--ink-ground-rule);
}

.stage {
  padding: var(--space-md) var(--space-md) 0 0;
}

/* Hairlines between, never outside. */
.stage + .stage {
  padding-left: var(--space-md);
  border-left: 1px solid var(--ink-ground-rule);
}

/* The icon in the compound's pastel, hard against the rule above it — it is
   the marker on the line, so it sits tight to the line rather than floating in
   the middle of the cell.

   No disc behind it. Four filled circles in a row would be four dots, and the
   glyph alone at 1.5px reads cleanly on the dark ground. */
.stage__icon {
  display: block;
  margin-bottom: var(--space-sm);
  line-height: 0;
  color: var(--acc);
}

.stage__title {
  margin: 0 0 var(--space-2xs);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
  color: #fff;
}

.stage__body {
  margin: 0;
  font-size: var(--size-sm);
  color: var(--ink-ground-soft);
}

@media (max-width: 46rem) {
  /* One per row: the vertical hairline becomes a horizontal one, or the four
     stages run together into a single column of text. */
  .stage + .stage {
    padding-left: 0;
    border-left: 0;
    border-top: 1px solid var(--ink-ground-rule);
  }

  .stage {
    padding-bottom: var(--space-md);
  }
}

/* Centred with the head it belongs to, not left with the container. */
.proof__cta {
  margin-top: clamp(var(--space-lg), 4vw, var(--space-xl));
  text-align: center;
}

.proof__value {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-lg);
  letter-spacing: var(--tracking-tight);
  color: #fff;
}

.proof__label {
  margin: var(--space-3xs) 0 0;
  font-size: var(--size-sm);
  color: var(--ink-ground-soft);
}

/* Left from the pill treatment, which filled the emphasised word with a
   pastel and needed dark text on it. There is no fill now, so painting the
   word --ink here would put near-black on the site's one near-black ground.
   The dark-ground case is handled once, on .section--ink .em. */
.proof {
  --em-color: #f8d7a7;
}

/* --- Decorative marks ------------------------------------------------------
   Contour is the GLOW label's own topographic pattern; sparkles are used
   sparsely as punctuation. Both are aria-hidden and inert. */

.decor {
  display: block;
  pointer-events: none;
}

.decor--contour {
  width: 100%;
  height: 100%;
}

/* --- Hero panel: composed vial cluster ------------------------------------
   Several silos at different scales, angles and depths rather than one product
   centred in a box. Positions come from nth-child so the composition survives
   whatever the collection returns. */

/* Red and blue, placed rather than striped.

   The two bottles are TB-500 in coral and GLOW in sky, and they sit at 25% and
   53% across the panel. So instead of a linear ramp from one hue to the other,
   each bottle gets its own soft light centred roughly where it stands: a blush
   halo under the coral one, a sky halo under the blue one, over a base that
   cools from left to right.

   Two reasons it is built this way rather than as a two-stop ramp:

     - A direct red-to-blue blend passes through mauve, which goes muddy across
       a panel this size. Separate radial fields never mix at full strength, so
       the middle stays clean.
     - A ramp is a background; light behind an object is a setting. The vials
       read as standing IN something rather than in front of something.

   The colours are the compounds' own pastels from the palette, dropped to
   around 60% alpha so the glass still separates from what is behind it. */
.hero__panel--cluster {
  background:
    radial-gradient(
      52% 62% at 26% 58%,
      rgba(236, 185, 185, 0.62) 0%,
      rgba(236, 185, 185, 0) 70%
    ),
    radial-gradient(
      58% 68% at 60% 44%,
      rgba(183, 217, 252, 0.66) 0%,
      rgba(183, 217, 252, 0) 72%
    ),
    linear-gradient(112deg, #fdf6f3 0%, #fbfaf9 38%, #f1f7fd 100%);
}

.hero__contour {
  position: absolute;
  /* Exactly the panel box. It used to be 220% of it and up to 88rem wide,
     which is what put a horizontal scrollbar on the whole document. The
     cropping that makes the pattern read as a fragment rather than a target
     now happens inside the SVG, whose clusters already run past its viewBox
     and are sliced by it. */
  inset: 0;
  color: var(--ink);
  opacity: 0.06;
}

.hero__contour svg {
  display: block;
  width: 100%;
  height: 100%;
}

.cluster {
  position: absolute;
  /* Anchored to the TOP of the panel and capped, not stretched to fill it.

     The panel's height is set by the grid row, which is driven by the taller
     copy column — once the copy gained a figure row and a bigger headline the
     panel grew to 969px inside an 863px viewport, putting its bottom 252px
     below the fold. Every vial is positioned from the panel's bottom, so the
     focal vial's base sat at 90.8% of a panel you could not see the end of, and
     the composition read as truncated no matter how it was arranged.

     Capping the cluster keeps the whole arrangement above the fold while the
     panel colour still fills the full row. */
  inset: 0 0 auto 0;
  height: min(38rem, 66vh);
}

.cluster__item {
  position: absolute;
  bottom: 0;
  display: block;
}

.cluster__vial {
  height: 100%;
  width: auto;
  max-width: none;
  display: block;
  transition: transform var(--dur-slow) var(--ease-out);
}

/* Two bottles, standing and splayed, measured off the client's reference.

   The reference is not three bottles lying down — it is two, near-upright,
   tilted in OPPOSITE directions so their caps converge and their bases spread.
   Measured off the image, the axes run at +19deg and -15deg from vertical, the
   nearer bottle is about 15% taller than the far one, it overlaps the far one's
   right edge, and both are cropped by the bottom of the frame rather than
   standing clear of it. Each of those is load-bearing: matched rotations read
   as a pair of identical objects, and bases sitting inside the frame read as
   two product shots on a shelf.

     rotation  -14deg front, +17deg back — opposed, so the pair converges
     size      92% and 80% of the cluster, the front genuinely nearer
     overlap   the front vial's left edge crosses the back vial's body
     crop      both run past the cluster's bottom, so the panel cuts them

   Placement uses the `rotate` property, not a transform, leaving `transform`
   free for the entrance and the float. */

/* A slot is a place on the surface: it holds one bottle and the shadow that
   bottle casts. It exists because the two cannot be nested — the bottle is
   rotated 14 to 17 degrees and its shadow lies flat on the ground, so a shadow
   inside the bottle's own box would tilt with the glass. The slot is the
   cluster's exact box, so every percentage inside it resolves against the same
   thing it did before the shadows were added. */
.cluster__slot {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.cluster__slot:nth-child(1) {
  z-index: 2;
}

.cluster__slot:nth-child(2) {
  z-index: 1;
}

/* A third vial is no longer part of the composition, but the section still
   renders whatever the collection returns if the pickers are cleared. Anything
   past the second is hidden rather than stacked somewhere arbitrary. */
.cluster__slot:nth-child(n + 3) {
  display: none;
}

/* Front — right, larger, crisp, and the one that overlaps.

   The two `left` values are set from the pair's measured bounding box rather
   than picked: at 6% and 38% the pair spanned -6px to 634px of a 953px panel,
   hanging off the left edge with 319px of dead ground on the right, and the two
   bottles missed each other by 8px instead of overlapping. At 25% and 53% the
   span is 175px to 777px — 176px of margin either side — and the front bottle
   crosses the back one by about 30px.

   The attached shadow is much smaller than it was. On something meant to be
   hovering, a big soft shadow welded to the object is what makes it read as a
   sticker; the mass of the shadow belongs on the ground, and all that is left
   here is enough to give the glass some weight. */
.cluster__slot:nth-child(1) .cluster__item {
  left: 53%;
  bottom: 5%;
  height: 82%;
  rotate: -14deg;
  filter: drop-shadow(-6px 10px 14px rgb(19 19 21 / 14%));
}

/* Back — left, smaller, and softened just enough to sit behind.

   Both `bottom` values were raised so the pair sits centred in the panel rather
   than cropped against its bottom edge: the front bottle's box now has 40px of
   margin above and below it, up from 107px above and 27px of overflow below,
   which had the whole composition sitting low with dead space over the caps.

   The back bottle is 4% higher than the front, not level with it. Its base
   finishing above the front's and its cap finishing below is the depth cue —
   level bases would flatten the pair into one plane. */
.cluster__slot:nth-child(2) .cluster__item {
  left: 25%;
  bottom: 9%;
  height: 71%;
  rotate: 17deg;
  opacity: 0.94;
  filter: blur(0.6px) drop-shadow(0 8px 12px rgb(19 19 21 / 10%));
}

/* --- Cast shadows ----------------------------------------------------------
   An ellipse on the surface under each bottle, with a visible gap between the
   base and the shadow. The gap is the whole effect: a shadow touching the
   object reads as contact, and one clearly separated from it reads as height.

   Positions are measured, not guessed. Each bottle's real base was found by
   sampling the rendered PNG's alpha channel for its lowest opaque row and that
   row's horizontal centre, then mapping that point through the element's own
   rotation — which is not something you can eyeball, since the front bottle's
   base centre sits 71.3% across the cluster while its box centre sits at 63%.

     front  base centre 68.2% across, 3.0% above the cluster's bottom edge
     back   base centre 27.0% across, 7.2% above

   Re-measured after the bottles were made smaller and raised: shrinking them
   moved both bases up and inward, and a shadow left at the old coordinates is
   a shadow in the wrong place. Every one of these numbers is downstream of the
   bottle's height, so the two have to move together.

   Drawn as a radial gradient rather than a blurred solid. Both look the same;
   a gradient is one texture the compositor can scale, while filter: blur()
   repaints every frame of the float below it.

   Wider than the base it belongs to, because the light in this scene is broad
   and soft — a shadow the exact width of the object implies a point source. */
.cluster__cast {
  position: absolute;
  translate: -50% 0;
  border-radius: 50%;
  /* Stronger than the first pass, which was too faint to read at all against
     the panel's own gradient — a shadow nobody can see does not say the object
     is hovering, it just says nothing. */
  background: radial-gradient(
    closest-side,
    rgb(19 19 21 / 48%) 0%,
    rgb(19 19 21 / 32%) 46%,
    rgb(19 19 21 / 0%) 100%
  );
}

.cluster__slot:nth-child(1) .cluster__cast {
  left: 68.2%;
  /* 2.6% lower than the base sits, which is about 15px — the gap IS the
     effect, and at 5px from the base the shadow was close enough to read as
     contact rather than height. */
  bottom: -1.4%;
  width: 16%;
  height: 5.2%;
}

/* Smaller, fainter and softer: it is further from the surface's light and
   further from the viewer, and a matched pair of shadows would undo the depth
   the bottles themselves establish. */
.cluster__slot:nth-child(2) .cluster__cast {
  left: 27%;
  bottom: 3.5%;
  width: 16.5%;
  height: 4.2%;
  opacity: 0.66;
}

.hero__sparkle {
  position: absolute;
  z-index: 5;
  color: var(--ink);
  opacity: 0.28;
}

.hero__sparkle--a {
  top: 11%;
  left: 22%;
}

.hero__sparkle--b {
  top: 24%;
  right: 12%;
}

.hero__sparkle--c {
  top: min(31rem, 54vh);
  left: 8%;
}

@media (max-width: 55.999rem) {
  .hero__panel--cluster {
    min-height: 24rem;
  }

  /* The cluster has to shrink with the panel, and did not.

     Its height is min(38rem, 66vh) — 594px on a 900px-tall phone — while the
     panel here is 24rem. Every bottle is sized as a percentage OF THE CLUSTER,
     so they were built against a box 210px taller than the one they are
     clipped to: the front bottle came out 506px tall in a 384px panel, ran
     200px past its bottom edge, and the hero showed two bottle caps. */
  .cluster {
    height: 100%;
  }

  /* Both bottles hang off the panel's centre line and are offset by a share of
     THEIR OWN width, so the pair holds its arrangement at any width in this
     range instead of drifting apart as the panel grows.

     A percentage `left` cannot do that: the bottles are sized from the panel's
     height, which is fixed here, while `left` tracks its width — so values
     tuned at 390 had the two separated entirely by 600. Offsetting each from
     50% keeps the front crossing the back by about 25px whatever the width,
     and keeps the pair centred as a group. */
  .cluster__slot:nth-child(1) .cluster__item {
    left: 50%;
    translate: -2% 0;
    bottom: 14%;
    height: 76%;
  }

  /* The coral bottle is the far one, so it takes a bigger step down in size
     than "behind" strictly needs — at 65% the two read as one bottle at two
     distances only just apart, and depth in a pair this small has to be stated
     rather than implied. */
  .cluster__slot:nth-child(2) .cluster__item {
    left: 50%;
    translate: -118% 0;
    bottom: 18%;
    height: 58%;
  }

  /* The shadows stay on the ground while the bottles lift off it — that gap IS
     the effect, and the bottles were sitting low enough to cover their own.

     Each cast is centred on its bottle's BASE, which is not its box centre: a
     bottle rotated about its own middle swings its base sideways by
     (height / 2) x sin(angle). At these sizes that is +35px for the front at
     -14deg and -33px for the back at +17deg, which puts the two bases at
     almost exactly +/-92px from the panel's centre line.

     Offsets in px rather than percent, deliberately: the bottles are sized
     from the panel's HEIGHT, so their bases sit a fixed distance from the
     centre whatever its width, and a percentage would slide the shadows out
     from under them as the screen widened. The calc also restores the -50%
     the base rule relies on to centre each cast on its own `left` — the
     previous overrides replaced that outright, which is what put the shadows
     in the wrong place to begin with. */
  .cluster__slot:nth-child(1) .cluster__cast {
    left: 50%;
    translate: calc(-50% + 92px) 0;
    bottom: 4%;
    /* Sized in px for the same reason the offsets are: the bottle above it is
       a fixed size here, so a shadow measured against the panel's width grew
       from 57px at 320 to 107px at 600 under a base that never changed. These
       hold the 2.7:1 ellipse the desktop composition uses. */
    width: 70px;
    height: 26px;
  }

  .cluster__slot:nth-child(2) .cluster__cast {
    left: 50%;
    translate: calc(-50% - 92px) 0;
    bottom: 10%;
    width: 55px;
    height: 20px;
  }
}

/* --- Hero cluster: the entrance -------------------------------------------
   One orchestrated moment on load, not an effect per element. The back rank
   arrives first and the front vial lands last, so the composition assembles
   from depth forward and the hero reads as being built rather than revealed.

   Positioning uses the individual `rotate` and `translate` properties, which
   leaves `transform` free for animation on the same element — no risk of the
   entrance clobbering a vial's placement. */

@keyframes vial-land {
  from {
    opacity: 0;
    transform: translateY(3rem) scale(0.94);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes contour-bloom {
  from {
    opacity: 0;
    scale: 0.88;
  }
}

@keyframes sparkle-in {
  0% {
    opacity: 0;
    scale: 0.2;
  }
  70% {
    scale: 1.15;
  }
  100% {
    opacity: 0.28;
    scale: 1;
  }
}

/* Every vial breathes, not just the back rank — the cluster should feel alive
   before anyone touches it. Each gets its own period and its own start offset,
   because identical timings make five objects look mechanically linked rather
   than independently suspended. Amplitude stays near 1% so it is noticed
   second, not first. */
@keyframes vial-float {
  50% {
    transform: translateY(-1.1%) rotate(0.5deg);
  }
}

@keyframes vial-float-alt {
  50% {
    transform: translateY(-0.8%) rotate(-0.6deg);
  }
}

.hero__panel--cluster .cluster__item {
  animation: vial-land 900ms var(--ease-out) both;
}

/* Depth order: the far bottle first, so the pair assembles back to front. */
.hero__panel--cluster .cluster__slot:nth-child(2) .cluster__item { animation-delay: 120ms; }
.hero__panel--cluster .cluster__slot:nth-child(1) .cluster__item { animation-delay: 300ms; }

/* The float starts after the entrance has finished so the two never fight. */
.hero__panel--cluster .cluster__vial {
  animation: vial-float 8s var(--ease-in-out) infinite;
}

/* A different period and offset per bottle. With two objects rather than five
   matched timings are far more obvious — the pair would swing as one rigid
   piece. */
.hero__panel--cluster .cluster__slot:nth-child(1) .cluster__vial {
  animation-duration: 7.5s;
  animation-delay: 1.4s;
}

.hero__panel--cluster .cluster__slot:nth-child(2) .cluster__vial {
  animation-name: vial-float-alt;
  animation-duration: 9.2s;
  animation-delay: 1.9s;
}

/* The shadow breathes against its bottle, and this is the part that actually
   sells the hover: as the bottle rises the shadow shrinks and fades, because
   the object is further from the surface. A static shadow under a moving
   object reads as a printed mark.

   Duration and delay are copied from each bottle's float so the two stay in
   phase — they are separate elements, so nothing keeps them in step
   automatically, and a shadow drifting out of phase with its object is worse
   than no animation at all. */
@keyframes cast-breathe {
  50% {
    transform: scale(0.9);
    opacity: 0.62;
  }
}

@keyframes cast-in {
  from {
    opacity: 0;
    transform: scale(1.16);
  }
}

.hero__panel--cluster .cluster__cast {
  animation:
    cast-in 900ms var(--ease-out) both,
    cast-breathe 7.5s var(--ease-in-out) 1.4s infinite;
}

.hero__panel--cluster .cluster__slot:nth-child(2) .cluster__cast {
  animation:
    cast-in 900ms var(--ease-out) 120ms both,
    cast-breathe 9.2s var(--ease-in-out) 1.9s infinite;
}

.hero__panel--cluster .cluster__slot:nth-child(1) .cluster__cast {
  animation-delay: 300ms, 1.4s;
}

.hero__contour {
  animation: contour-bloom 1400ms var(--ease-out) both;
}

/* Two animations: the entrance once, then a twinkle forever.

   They are declared together rather than swapped by script because the
   entrance ends on exactly the values the twinkle starts from — opacity 0.28,
   scale 1 — so the hand-off is invisible. The twinkle carries no fill mode, so
   during its delay it leaves the entrance's held end state alone; giving it
   `both` would clobber the entrance from the first frame.

   Delays and durations are set per sparkle, and the durations are deliberately
   unrelated (5.2s, 6.6s, 4.4s). Three stars pulsing in unison reads as a
   flashing banner; three drifting out of phase reads as light catching. They
   re-sync only every few minutes, by which time nobody is watching.

   NOTE the delay must be per animation. `animation-delay: 700ms` with two
   animations in the list applies 700ms to both, which would start the twinkle
   underneath the entrance. */
.hero__sparkle {
  opacity: 0;
  animation:
    sparkle-in 700ms var(--ease-out) var(--spark-in) both,
    twinkle-a var(--spark-dur) linear var(--spark-delay) infinite;
}

.hero__sparkle--a {
  --spark-in: 700ms;
  --spark-delay: 1200ms;
  --spark-dur: 11000ms;
  animation-name: sparkle-in, twinkle-a;
}

.hero__sparkle--b {
  --spark-in: 840ms;
  --spark-delay: 3400ms;
  --spark-dur: 14000ms;
  animation-name: sparkle-in, twinkle-b;
}

.hero__sparkle--c {
  --spark-in: 960ms;
  --spark-delay: 2200ms;
  --spark-dur: 9500ms;
  animation-name: sparkle-in, twinkle-c;
}

/* A GLINT, NOT A PULSE.

   The first version faded each star up and down on a sine, which is breathing:
   the star is always doing something, and three of them doing it at once reads
   as a mechanism. A real catch of light is the opposite shape — long dormant,
   then a fast flare and a slower fall.

   So each star rests at a low opacity for most of a long cycle and spends about
   a second flaring. Three things happen in that second:

     - opacity spikes to roughly three times the resting value
     - the star scales up
     - it ROTATES a few degrees, which is what sells it as light catching an
       edge rather than a lamp being turned up. A four-point star turning 14
       degrees reads as a glint; the same star brightening in place does not.

   The attack is eased out and the fall eased in, set per keyframe, so the
   flare arrives faster than it leaves.

   Cycles are long and share no common factor (11s, 14s, 9.5s) and each flare
   sits at a different point in its own cycle, so two stars almost never flare
   together and the pattern does not repeat for minutes. */
@keyframes twinkle-a {
  0%,
  19% {
    opacity: 0.2;
    scale: 1;
    rotate: 0deg;
  }

  23% {
    opacity: 0.66;
    scale: 1.34;
    rotate: -13deg;
    animation-timing-function: ease-in;
  }

  32%,
  100% {
    opacity: 0.2;
    scale: 1;
    rotate: 0deg;
  }
}

/* Two flares close together — the star that catches twice. */
@keyframes twinkle-b {
  0%,
  47% {
    opacity: 0.22;
    scale: 1;
    rotate: 0deg;
  }

  50% {
    opacity: 0.58;
    scale: 1.24;
    rotate: 9deg;
    animation-timing-function: ease-in;
  }

  54% {
    opacity: 0.26;
    scale: 1.04;
    rotate: 4deg;
    animation-timing-function: ease-out;
  }

  58% {
    opacity: 0.7;
    scale: 1.38;
    rotate: 14deg;
    animation-timing-function: ease-in;
  }

  68%,
  100% {
    opacity: 0.22;
    scale: 1;
    rotate: 0deg;
  }
}

@keyframes twinkle-c {
  0%,
  70% {
    opacity: 0.24;
    scale: 1;
    rotate: 0deg;
  }

  74% {
    opacity: 0.62;
    scale: 1.3;
    rotate: -16deg;
    animation-timing-function: ease-in;
  }

  85%,
  100% {
    opacity: 0.24;
    scale: 1;
    rotate: 0deg;
  }
}

/* Motion here is decorative, so it goes entirely rather than being shortened:
   the composition must still be correct with no animation at all. */
@media (prefers-reduced-motion: reduce) {
  .hero__panel--cluster .cluster__item,
  .hero__panel--cluster .cluster__vial,
  .hero__panel--cluster .cluster__cast,
  .hero__contour,
  .hero__sparkle {
    animation: none !important;
  }

  .hero__sparkle {
    opacity: 0.28;
  }
}

/* --- Hero figure row -------------------------------------------------------
   Fills the copy column so it carries weight comparable to the panel opposite.
   Hairline dividers rather than boxes, so it reads as a footer to the copy
   rather than a second component. */

.hero__figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

.hero__figure {
  min-width: 6rem;
}

.hero__figure-value {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-lg);
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
}

.hero__figure-label {
  margin: var(--space-3xs) 0 0;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.hero__lede {
  max-width: 50ch;
}
/* --- Standard concepts -----------------------------------------------------
   Four candidate replacements for the certificate section, sharing a head and
   a footer so they can be compared on their layout rather than on incidental
   differences in their typography.

   All four are LEFT ALIGNED: the head is a stack, not the flex row that
   .section-head uses, because that row bottom-aligns a trailing element
   against the heading and there is nothing trailing here. */
.conc-head {
  max-width: 54ch;
  padding-bottom: var(--space-md);
  margin-bottom: clamp(var(--space-lg), 4vw, var(--space-xl));
  border-bottom: 1px solid var(--rule);
}

.conc-head--tight {
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: clamp(var(--space-lg), 3.4vw, var(--space-xl));
}

.conc-head .h2 {
  margin: 0;
}

/* The eyebrow pairing rule sets the gap above the heading; this only has to
   say how far the supporting line sits below it. */
.conc-head__lede {
  margin: var(--space-md) 0 0;
  max-width: 52ch;
}

.conc-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

.conc-foot__note {
  margin: 0;
  max-width: 62ch;
}

/* The reviews variant. Everywhere else this foot is a caption sitting opposite
   a call to action, so it hangs off the left margin like the rest of the page.
   Under the review wall there is no CTA and the note is a disclaimer about the
   cards above it, so it centres under them.

   It also steps down a size. At the body size, directly under four quotes set
   in the same face, it read as a fifth opinion rather than as the line that
   keeps the section from claiming an outcome. One step to --size-xs and no
   further, keeping --ink-soft, because it still has to be read. */
.conc-foot--centred {
  justify-content: center;
  text-align: center;
  /* The rule moves under the note rather than over it. Above, it read as a
     divider cutting the disclaimer off from the cards it is about; below, it
     closes the section — the note stays attached to the wall it qualifies and
     the line marks the end of the whole block. */
  padding-top: 0;
  border-top: 0;
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--rule);
}

.conc-foot--centred .conc-foot__note {
  margin-inline: auto;
  /* No measure. 62ch is right for a paragraph hanging off the left margin, but
     centred under the cards it split one sentence pair across two lines with
     the full width standing empty either side. Uncapped it runs as one line on
     a desktop, and still wraps on its own once the screen is narrower than the
     sentence. */
  max-width: none;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
}

/* --- 1. Index --------------------------------------------------------------
   A reference table. One grid template shared by the legend and every row, so
   the column names and the values they name cannot drift apart — the usual
   failure of a "table" built from a stack of divs. */
.cindex {
  --cindex-cols: 1.5rem minmax(12rem, 1.4fr) 6rem 6rem 7rem 1.75rem;
}

.cindex__legend,
.cindex__link {
  display: grid;
  grid-template-columns: var(--cindex-cols);
  align-items: center;
  gap: var(--space-md);
}

.cindex__legend {
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--rule-strong);
  font-size: 0.6875rem;
  letter-spacing: var(--tracking-wide);
  color: var(--ink-faint);
}

.cindex__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.cindex__row + .cindex__row {
  border-top: 1px solid var(--rule);
}

.cindex__link {
  padding-block: var(--space-sm);
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-small);
  transition: background-color var(--dur-fast) var(--ease-out);
}

/* The row tints in its own compound's colour on hover. Very low mix on
   purpose: eight rows that each light up strongly would make the list feel
   like a set of buttons rather than a document. */
.cindex__link:hover {
  background: color-mix(in srgb, var(--chip) 26%, var(--paper));
}

.cindex__link:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

.cindex__mark {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 4px;
  background: linear-gradient(150deg, color-mix(in srgb, var(--chip) 55%, #fff), var(--chip));
}

.cindex__names {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.cindex__title {
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

.cindex__common {
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.cindex__cell {
  color: var(--ink-soft);
}

.cindex__cell--price {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  color: var(--ink);
}

/* Supplied by the legend on a wide screen. Shown only once the columns stack. */
.cindex__cell-label {
  display: none;
}

.cindex__from {
  color: var(--ink-faint);
}

.cindex__go {
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  transition: color var(--dur-fast) var(--ease-out), translate var(--dur-fast) var(--ease-out);
}

.cindex__link:hover .cindex__go {
  color: var(--deep);
  translate: 3px 0;
}

/* Below this the six columns cannot hold their values without wrapping, so the
   row becomes a small block and each figure takes its name back. */
@media (max-width: 60rem) {
  .cindex__legend {
    display: none;
  }

  .cindex__link {
    grid-template-columns: 1.5rem 1fr auto;
    grid-template-areas:
      'mark names go'
      '.    cells cells';
    row-gap: var(--space-2xs);
  }

  .cindex__mark { grid-area: mark; align-self: start; margin-top: 0.35rem; }
  .cindex__names { grid-area: names; }
  .cindex__go { grid-area: go; }

  .cindex__cell {
    grid-area: auto;
  }

  .cindex__link .cindex__cell:nth-of-type(1) { grid-column: 2; }

  .cindex__cell-label {
    display: inline;
    margin-right: 0.4em;
    color: var(--ink-faint);
    font-size: 0.6875rem;
    letter-spacing: var(--tracking-wide);
  }
}

/* --- 2. Matrix -------------------------------------------------------------
   A real table, so the headers are announced and the cells keep their
   relationships. Its own scroller means a narrow screen scrolls the matrix
   rather than the page. */
.matrix__scroll {
  overflow-x: auto;
  scrollbar-width: thin;
}

.matrix {
  width: 100%;
  min-width: 44rem;
  border-collapse: collapse;
  text-align: left;
}

.matrix th,
.matrix td {
  padding: var(--space-sm) var(--space-md);
  vertical-align: middle;
}

.matrix thead th {
  padding-top: 0;
  border-bottom: 1px solid var(--rule-strong);
}

.matrix__head {
  display: grid;
  justify-items: center;
  gap: var(--space-2xs);
  text-decoration: none;
  color: inherit;
}

/* The vial stands in a field of its own compound's colour, the same device the
   product card uses, so a column head reads as that product and not as a
   generic table header. */
.matrix__vial {
  /* Absolutely positioned child rather than a grid one. A percentage height
     inside a box whose own height comes from `aspect-ratio` resolves to auto —
     the track depends on the image and the image depends on the track — so the
     tall render overflowed and the column head showed nothing but the cap.
     An absolute child resolves against the padding box, which is definite. */
  position: relative;
  width: 100%;
  max-width: 10rem;
  min-width: 6.5rem;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-small);
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 42%, #fff), var(--chip));
  overflow: hidden;
}

/* `contain` rather than a width percentage. These renders are tall with a lot
   of transparent margin, so sizing them by width made them far taller than the
   box and the column head showed nothing but the cap. */
.matrix__vial img {
  position: absolute;
  inset: 6% 8%;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: calc(100% - 16%);
  block-size: calc(100% - 12%);
  object-fit: contain;
}

.matrix__name {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  text-align: center;
}

.matrix__head:hover .matrix__name {
  color: var(--deep);
}

.matrix tbody tr + tr th,
.matrix tbody tr + tr td {
  border-top: 1px solid var(--rule);
}

.matrix__spec {
  display: table-cell;
  min-width: 13rem;
  padding-left: 0;
}

.matrix__spec-key {
  display: block;
  color: var(--ink);
}

.matrix__spec-detail {
  display: block;
  margin-top: 2px;
  font-size: var(--size-sm);
  font-weight: 400;
  color: var(--ink-soft);
}

.matrix__cell {
  color: var(--ink-soft);
}

/* The spanning row. Tinted and ticked so the eye reads "this one does not
   vary" before it reads the words. */
.matrix__cell--all {
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}

.matrix__all {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--ink);
}

/* The disc takes the accent and the tick is knocked out of it in the ground
   colour. Setting both to the accent — which is what this did first — draws a
   plain filled circle with an invisible tick inside it. */
.matrix__tick {
  display: grid;
  place-items: center;
  flex: none;
  color: var(--accent);
  --check-ink: var(--paper);
}

.matrix__na {
  color: var(--ink-faint);
}

/* --- 3. Mosaic -------------------------------------------------------------
   Deliberately unequal tiles. The statement is four times the area of anything
   else, and the rest pack around it — the one section on the page whose parts
   are not the same size as each other. */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-sm);
}

.bento__tile {
  display: flex;
  flex-direction: column;
  padding: var(--space-lg);
  border-radius: var(--radius-surface);
  background: var(--paper-sunk);
  min-height: 11rem;
}

.bento__tile--stmt {
  grid-column: span 2;
  grid-row: span 2;
  justify-content: center;
  background: color-mix(in srgb, var(--accent) 12%, var(--paper));
}

.bento__heading {
  margin: 0;
}

.bento__body {
  margin: var(--space-md) 0 0;
  max-width: 40ch;
  color: var(--ink-soft);
}

.bento__cta {
  margin-top: var(--space-lg);
  align-self: start;
}

/* Product tiles. Colour field plus the vial standing in it, cropped by the
   tile — the product card's device, so the mosaic still belongs to the
   catalogue. */
.bento__tile--product {
  position: relative;
  justify-content: flex-end;
  padding: 0;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 38%, #fff), var(--chip));
}

.bento__vial {
  position: absolute;
  inset: 0 0 2.2rem;
}

.bento__vial img {
  position: absolute;
  inset: 8% 20% 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 60%;
  block-size: 92%;
  object-fit: contain;
  transition: scale var(--dur-slow, 420ms) var(--ease-out);
}

.bento__tile--product:hover .bento__vial img {
  scale: 1.04;
}

.bento__ptext {
  position: relative;
  display: grid;
  gap: 1px;
  padding: var(--space-sm) var(--space-md);
  background: linear-gradient(to top, color-mix(in srgb, var(--chip) 88%, #fff), transparent);
}

.bento__pname {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.bento__pmeta {
  color: var(--deep);
}

.bento__tile--figure {
  justify-content: flex-end;
  gap: var(--space-2xs);
}

.bento__icon {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  margin-bottom: auto;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink-soft);
}

.bento__figure {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-xl);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
}

.bento__label {
  margin: 0;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* With the statement holding a 2x2 block, four tiles fill the two rows beside
   it and anything after that starts a fresh row on the left — leaving half of
   that row empty. The tiles from the fifth on span two columns so the bottom
   row closes instead of trailing off. */
@media (min-width: 64.01rem) {
  .bento__tile:nth-child(n + 6) {
    grid-column: span 2;
  }
}

@media (max-width: 64rem) {
  .bento {
    grid-template-columns: repeat(2, 1fr);
  }

  .bento__tile--stmt {
    grid-column: span 2;
    grid-row: auto;
  }
}

@media (max-width: 38rem) {
  .bento {
    grid-template-columns: 1fr;
  }

  .bento__tile--stmt {
    grid-column: auto;
  }
}

/* --- 4. Figures ------------------------------------------------------------
   No boxes anywhere. Hairlines and whitespace only, which is the whole reason
   this one is in the set: if the page feels crowded, the answer is not another
   arrangement of panels. */
.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-lg) 0;
  margin: 0;
  padding: 0;
}

.figures__item {
  padding-inline: var(--space-lg);
  border-left: 1px solid var(--rule-strong);
}

/* The first figure sits on the page's own left margin. A rule there would put
   a line in front of the section's alignment edge, which is the one edge every
   other section lines up against. */
.figures__item:first-child {
  padding-left: 0;
  border-left: 0;
}

.figures__value {
  font-family: var(--font-display);
  font-size: clamp(2.75rem, 6vw, 4.25rem);
  letter-spacing: -0.03em;
  line-height: 0.95;
  color: var(--ink);
  font-variant-numeric: tabular-nums lining-nums;
}

.figures__label {
  margin: var(--space-sm) 0 0;
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
}

.figures__detail {
  display: block;
  margin-top: 2px;
  font-weight: 400;
  color: var(--ink-soft);
}

@media (max-width: 44rem) {
  .figures__item {
    padding-inline: 0;
    border-left: 0;
    padding-bottom: var(--space-md);
    border-bottom: 1px solid var(--rule);
  }
}

/* The compound row. Chips, not cards: at this size product cards would
   out-shout the figures, and the figures are the section. */
.chips {
  margin-top: clamp(var(--space-lg), 4vw, var(--space-xl));
}

.chips__lead {
  margin: 0 0 var(--space-sm);
  font-size: 0.6875rem;
  letter-spacing: var(--tracking-wide);
  color: var(--ink-faint);
}

.chips__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 0.4rem 0.85rem 0.4rem 0.6rem;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  font-size: var(--size-sm);
  color: var(--ink-soft);
  text-decoration: none;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.chip:hover {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--chip) 70%, var(--rule-strong));
  background: color-mix(in srgb, var(--chip) 24%, var(--paper));
}

.chip__dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 999px;
  background: linear-gradient(150deg, color-mix(in srgb, var(--chip) 55%, #fff), var(--chip));
}

/* The render standing on its own colour, cropped to a rounded square inside
   the pill — the same treatment the thumbnail marker uses in the footer, so a
   compound looks the same wherever it is listed small. The vial is portrait
   and the well is square, so it is sized by its height and centred rather
   than fitted, which would letterbox it into a sliver. */
.chip__thumb {
  position: relative;
  flex: none;
  display: block;
  width: 1.25rem;
  height: 1.25rem;
  margin-inline-start: -0.1rem;
  border-radius: 6px;
  overflow: hidden;
  background: linear-gradient(160deg, color-mix(in srgb, var(--chip) 40%, #fff), var(--chip));
}

.chip__thumb img {
  position: absolute;
  inset: 8% 0 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 46%;
  block-size: 88%;
  object-fit: contain;
  margin-inline: auto;
}


/* --- 5. Guide --------------------------------------------------------------
   One row per research area: the plain-language answer on the left, the
   compounds belonging to it on the right. The left column is the thing being
   read, so it gets the fixed track and the products flow in what remains. */
.guide {
  display: grid;
  gap: 0;
}

.guide__area {
  display: grid;
  grid-template-columns: minmax(14rem, 20rem) 1fr;
  gap: var(--space-lg) var(--space-xl);
  padding-block: var(--space-lg);
}

.guide__area + .guide__area {
  border-top: 1px solid var(--rule);
}

.guide__aside {
  position: relative;
  padding-left: var(--space-md);
}

/* A colour bar rather than a colour panel. The area's hue has to be readable
   at a glance down the left edge without tinting a band of the page — the page
   already steps through these hues at section scale. */
.guide__aside::before {
  content: '';
  position: absolute;
  inset: 0.35rem auto 0.35rem 0;
  width: 3px;
  border-radius: 999px;
  background: var(--area);
}

.guide__label {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-lg);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

.guide__blurb {
  margin: var(--space-2xs) 0 0;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.guide__all {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2xs);
  margin-top: var(--space-sm);
  font-size: var(--size-sm);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 2px;
}

.guide__all:hover {
  border-bottom-color: var(--area);
}

.guide__count {
  color: var(--ink-faint);
}

.guide__items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.guide__item {
  display: grid;
  gap: var(--space-2xs);
  text-decoration: none;
  color: inherit;
}

.guide__vial {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius-small);
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 36%, #fff), var(--chip));
  overflow: hidden;
}

/* Absolute rather than a percentage-sized flow child: inside a box whose
   height comes from `aspect-ratio`, a percentage height resolves to auto and
   these tall renders overflow to show nothing but the cap. */
.guide__vial img {
  position: absolute;
  inset: 8% 18% 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 64%;
  block-size: 92%;
  object-fit: contain;
  transition: scale var(--dur-fast) var(--ease-out);
}

.guide__item:hover .guide__vial img {
  scale: 1.04;
}

.guide__itext {
  display: grid;
  gap: 1px;
}

.guide__iname {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.guide__icommon {
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.guide__imeta {
  margin-top: 2px;
  color: var(--deep);
}

@media (max-width: 52rem) {
  .guide__area {
    grid-template-columns: 1fr;
    gap: var(--space-md);
  }
}

/* --- Guide, compact ---------------------------------------------------------
   Areas side by side instead of stacked, and the square product tile becomes a
   44px thumbnail on a text row.

   The stacked version measured 1969px — two and a third viewports, and more
   than twice the height of any other candidate for this slot. A section whose
   job is "here is where to start" must not be the longest thing on the page,
   and the height was nearly all product tile: a 155px square plus three lines
   of text, times four areas. */
.guide--columns {
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  /* Two named tracks so the areas can share them: one for the answer, one for
     the compounds under it. */
  grid-template-rows: auto auto;
  gap: var(--space-xl) var(--space-lg);
}

/* Subgrid, so every column's product list begins on the SAME line regardless
   of how many lines its blurb ran to. Without it each area sized its own two
   rows and the four lists started at four different heights — which reads as
   four unrelated blocks rather than one row of choices. Browsers without
   subgrid fall back to the old independent sizing, which is untidy but works. */
.guide--columns .guide__area {
  position: relative;
  grid-row: span 2;
  grid-template-columns: 1fr;
  grid-template-rows: subgrid;
  gap: var(--space-md);
  padding-block: 0;
  padding-left: var(--space-md);
}

/* The bar belongs to the whole column here, not just the text at the top of
   it. Stopping it above the products made the column look truncated. */
.guide--columns .guide__area::before {
  content: '';
  position: absolute;
  inset: 0.35rem auto 0.35rem 0;
  width: 3px;
  border-radius: 999px;
  background: var(--area);
}

.guide--columns .guide__aside {
  padding-left: 0;
}

.guide--columns .guide__aside::before {
  display: none;
}

/* The rule between areas is what separated one full-width band from the next.
   Side by side they are separated by the gap and by their own colour bars. */
.guide--columns .guide__area + .guide__area {
  border-top: 0;
}

.guide--columns .guide__label {
  font-size: var(--size-md);
}

/* Two lines, so one long blurb cannot push its column — and therefore the
   whole row — taller than the rest. */
.guide--columns .guide__blurb {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.guide--columns .guide__items {
  grid-template-columns: 1fr;
  gap: var(--space-3xs);
  align-content: start;
}

.guide--columns .guide__item {
  grid-template-columns: 2.75rem 1fr;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-3xs);
  margin-left: calc(var(--space-3xs) * -1);
  border-radius: var(--radius-small);
  transition: background-color var(--dur-fast) var(--ease-out);
}

.guide--columns .guide__item:hover {
  background: color-mix(in srgb, var(--chip) 24%, transparent);
}

.guide--columns .guide__vial {
  aspect-ratio: 1;
  border-radius: 8px;
}

.guide--columns .guide__vial img {
  inset: 6% 16% 0;
  inline-size: 68%;
  block-size: 94%;
}

/* The common name is the first thing to go. At this size the compound's own
   name plus its size and price is the whole useful row, and the descriptor is
   on the product page one click away. */
.guide--columns .guide__icommon {
  display: none;
}

.guide--columns .guide__iname {
  font-size: var(--size-xs);
}

.guide--columns .guide__imeta {
  margin-top: 0;
  font-size: 0.6875rem;
}

.guide--columns .guide__all {
  margin-top: 0;
  font-size: var(--size-xs);
}

/* --- Stars ------------------------------------------------------------------
   Two stacked rows, the top one clipped to the exact percentage. Drawing each
   star as full/half/empty would round every rating to the nearest half, so 4.3
   and 4.7 would look the same. */
/* `width: max-content` matters more than it looks. As a child of a column
   flex container the box was stretched to the container's full width, so the
   overlay's `width: var(--fill)` resolved against ~250px instead of the 83px
   the stars actually occupy — every rating rendered as five full stars, and a
   four-star review silently showed as five. */
.stars {
  position: relative;
  display: inline-block;
  width: max-content;
  max-width: 100%;
  line-height: 0;
  white-space: nowrap;
}

.stars__track {
  display: inline-flex;
  gap: 2px;
  color: var(--rule-strong);
}

.stars__fill {
  position: absolute;
  inset: 0;
  display: inline-flex;
  gap: 2px;
  width: var(--fill);
  overflow: hidden;
  color: var(--star, #E8A93C);
}

/* The clip depends on these NOT shrinking. As ordinary flex items the five
   stars in the overlay squeezed themselves into its narrowed width instead of
   overflowing it, so a four-star rating drew five 12px gold stars sitting on
   top of five 15px grey ones — every partial rating came out smaller and
   visibly out of register with the row behind it. Full ratings looked fine,
   which is what made it hard to see. */
.stars__track svg,
.stars__fill svg {
  flex: none;
}

/* --- Reviews ----------------------------------------------------------------
   The head carries the aggregate on the right, because a number that large is
   the first thing read and it should not sit in the column that the eye uses
   to find the heading. */
.reviews__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-md) var(--space-xl);
  max-width: none;
}

.reviews__headtext {
  max-width: 46ch;
}

.reviews__agg {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* Both figures here are counted from the reviews present, never typed. */
.reviews__avg {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 5vw, 3.5rem);
  letter-spacing: -0.03em;
  line-height: 0.9;
  font-variant-numeric: tabular-nums lining-nums;
}

.reviews__aggmeta {
  display: grid;
  gap: var(--space-3xs);
}

.reviews__count {
  margin: 0;
  color: var(--ink-soft);
}

.reviews {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A card, but with the compound's colour on one edge rather than as a fill: a
   wall of pastel panels would be louder than anything else on the page, and the
   colour is here to say which compound, not to decorate. */
.review {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--chip);
  border-radius: var(--radius-small);
  background: var(--paper);
  overflow: hidden;
}

.review--nochip {
  border-left-color: var(--rule-strong);
}

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

.review__avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  overflow: hidden;
  background: color-mix(in srgb, var(--chip) 55%, var(--paper));
  color: var(--deep);
}

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

.review__initials {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Two lines are reserved whether or not the review is verified. A review
   without the verified line gave a 23px name block against its neighbours' 44,
   which lifted its stars 4px and its quote 8px — across a row of cards that
   reads as one card being broken rather than as one review being unverified.
   The space is held; only the label is conditional. */
.review__ident {
  display: grid;
  gap: 1px;
  min-width: 0;
  min-height: 2.75rem;
  align-content: center;
  margin-right: auto;
}

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

.review__verified {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.review__vtick {
  display: grid;
  place-items: center;
  color: var(--signal-ok, #4B7F52);
  --check-ink: var(--paper);
}

.review__quote {
  margin: 0;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink);
}

/* Pushed to the bottom and always on its own line. Letting it share a row with
   the name when it happened to fit meant one card in four laid its footer out
   differently from its neighbours. */
.review__product {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-top: auto;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  color: var(--ink-soft);
  text-decoration: none;
}

.review__product:hover {
  color: var(--deep);
}

/* --- Layout 1: Card -------------------------------------------------------
   The person leads. No photograph of the product at all — the compound is
   named on the closing line instead. Quietest of the three, and the only one
   that stays honest at a glance when a reviewer has no picture. */
.reviews--card .review__shot {
  display: none;
}

/* --- Layout 2: Shot -------------------------------------------------------
   The compound photographed across the top of the card, standing in its own
   colour. Most visual of the three, and the one that makes it obvious the
   review is attached to a specific thing you can buy. */
.reviews--shot .review {
  padding: 0;
  border-left-width: 1px;
  border-left-color: var(--rule);
}

.reviews--shot .review__shot {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 42%, #fff), var(--chip));
}

/* Absolute, because a percentage height inside an `aspect-ratio` box resolves
   to auto and these tall renders would show nothing but the cap. */
.reviews--shot .review__shot img {
  position: absolute;
  inset: 8% 0 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 34%;
  block-size: 88%;
  object-fit: contain;
  margin-inline: auto;
}

.reviews--shot .review__person,
.reviews--shot .review__quote {
  padding-inline: var(--space-md);
}

/* The card's own padding is zero here so the photograph can reach the edges,
   which means every inset is declared per element — and the foot of the card
   was being held open by the product line. With that gone the quote ended
   flush against the border. This is the inset it lost, plus a little: the
   quote is the last thing in the card now and wants room to finish. */
.reviews--shot .review__quote {
  padding-bottom: var(--space-lg);
}

.reviews--shot .review__person {
  padding-top: var(--space-md);
}

.reviews--shot .review__product {
  margin-inline: var(--space-md);
  margin-bottom: var(--space-md);
}

/* --- Layout 3: Split ------------------------------------------------------
   Avatar on the left, the compound as a small square on the right, the quote
   under both. Says "this person bought this" in one line without giving a
   photograph a third of the card. */
/* Avatar and name on one line, stars on their own beneath. As a single flex
   row the three had to share a column already 3rem narrower than the others,
   which wrapped "Reviewer name" onto two lines and squeezed the stars. */
.reviews--split .review__person {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--space-2xs);
}

.reviews--split .review__person .stars {
  grid-column: 1 / -1;
}

.reviews--split .review__shot {
  position: relative;
  flex: none;
  order: 2;
  width: 3rem;
  height: 3rem;
  border-radius: 10px;
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 42%, #fff), var(--chip));
  overflow: hidden;
}

.reviews--split .review__shot img {
  position: absolute;
  inset: 10% 0 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 46%;
  block-size: 90%;
  object-fit: contain;
  margin-inline: auto;
}

/* The shot is a sibling of the person row, so it is lifted onto that row and
   pinned right rather than being wrapped in another element. */
.reviews--split .review {
  display: grid;
  grid-template-columns: 1fr 3rem;
  align-content: start;
  gap: var(--space-sm) var(--space-sm);
}

.reviews--split .review__shot {
  grid-column: 2;
  grid-row: 1;
}

.reviews--split .review__person {
  grid-column: 1;
  grid-row: 1;
}

.reviews--split .review__quote,
.reviews--split .review__product {
  grid-column: 1 / -1;
}

.reviews--split .review--nochip .review__person {
  grid-column: 1 / -1;
}

.reviews__empty {
  margin: 0;
  max-width: 60ch;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* --- Compound markers -------------------------------------------------------
   Ten presentations of one link. Every marker element is in the markup and an
   `mk-<name>` class on an ancestor reveals exactly one, so the ten cannot drift
   apart the way ten templates would.

   No squares: the rounded square was the previous default and is out. */
.cmk {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  text-decoration: none;
  color: var(--ink-soft);
  transition: color var(--dur-fast) var(--ease-out);
}

.cmk:hover {
  color: var(--deep);
}

.cmk--review { font-size: var(--size-xs); }
.cmk--footer { font-size: var(--size-sm); }

/* Everything hidden by default; each mk- rule turns one back on. */
.cmk__dot,
.cmk__ring,
.cmk__bar,
.cmk__bracket,
.cmk__tag,
.cmk__vial,
.cmk__thumb {
  display: none;
  flex: none;
}

/* 01 Dot — a filled circle. */
.mk-dot .cmk__dot {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 999px;
  background: linear-gradient(150deg, color-mix(in srgb, var(--chip) 55%, #fff), var(--chip));
}

/* 02 Ring — the same circle left hollow, in the deep tone so a 1.5px stroke
   actually registers against paper. */
.mk-ring .cmk__ring {
  display: block;
  width: 0.6rem;
  height: 0.6rem;
  border: 1.5px solid var(--deep);
  border-radius: 999px;
}

/* 03 Bar — echoes the coloured edge down the side of a review card and the
   guide's area columns, so it is a shape the page already speaks. */
.mk-bar .cmk__bar {
  display: block;
  width: 3px;
  height: 0.95rem;
  border-radius: 999px;
  background: var(--chip);
}

/* 04 Rule — no shape; the colour becomes an underline. */
.mk-rule .cmk__name {
  padding-bottom: 2px;
  border-bottom: 2px solid var(--chip);
}

/* 05 Tag — the lot icon. The only marker carrying meaning beyond colour. */
.mk-tag .cmk__tag {
  display: grid;
  place-items: center;
  color: var(--deep);
}

/* 06 Vial — the product reduced to a mark. */
.mk-vial .cmk__vial {
  display: grid;
  place-items: center;
  color: var(--deep);
}

/* 07 Tint — no furniture at all, the name in its own deep tone. */
.mk-tint .cmk__name {
  color: var(--deep);
  font-weight: 600;
}

/* 08 Thumbnail — the only one that shows what actually arrives, and the only
   one that costs a request per row. */
.mk-thumb .cmk__thumb {
  position: relative;
  display: block;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 6px;
  overflow: hidden;
  background: linear-gradient(160deg, color-mix(in srgb, var(--chip) 40%, #fff), var(--chip));
}

.mk-thumb .cmk__thumb img {
  position: absolute;
  inset: 10% 0 0;
  width: auto;
  height: auto;
  max-width: none;
  inline-size: 46%;
  block-size: 88%;
  object-fit: contain;
  margin-inline: auto;
}

/* 09 Pill — the whole link tinted. */
.mk-pill .cmk {
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--chip) 32%, var(--paper));
  color: var(--deep);
}

.mk-pill .cmk:hover {
  background: color-mix(in srgb, var(--chip) 52%, var(--paper));
}

/* 10 Bracket — typographic rather than graphic. */
.mk-bracket .cmk__bracket {
  display: block;
  color: var(--deep);
  font-family: var(--font-data);
  font-size: 1em;
  line-height: 1;
}

/* --- The specimen sheet ---------------------------------------------------- */
.msheet {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-md);
}

.msheet__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-md);
  border: 1px solid var(--rule);
  border-radius: var(--radius-small);
  background: var(--paper);
}

.msheet__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
}

.msheet__num {
  margin: 0;
  color: var(--ink-faint);
}

.msheet__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
}

.msheet__note {
  margin: 0;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
  min-height: 5.4em;
}

.msheet__spec {
  padding: var(--space-sm);
  border-radius: 10px;
  background: var(--paper-sunk);
  margin-top: auto;
}

.msheet__spec--dark {
  margin-top: var(--space-2xs);
}

.msheet__label {
  margin: 0 0 var(--space-2xs);
  font-size: 0.5625rem;
  letter-spacing: var(--tracking-wide);
  color: var(--ink-faint);
}

.msheet__list {
  display: grid;
  gap: var(--space-3xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The review's closing line. It carries the rule and the bottom alignment; the
   marker inside it is the shared component, so a compound marked one way here
   is marked the same way in the footer. */
/* --- Scroll reveal ----------------------------------------------------------
   Content rises and fades in as it enters the viewport, once.

   THE TARGET LIST IS PUBLISHED AS A CUSTOM PROPERTY and read back by the
   script, so there is exactly one copy of it. The hidden state must be applied
   at first paint — otherwise the page shows its content and then hides it —
   which is why CSS owns the list rather than the script.

   The hero is excluded deliberately. It already has a choreographed entrance
   of its own, and two systems animating the same element is how a first
   impression ends up stuttering. Feature panels are scoped to `.feature` for
   the same reason: they share the `.split` markup with the hero.

   The marquee is excluded too — it is already moving. */
:root {
  --reveal-targets: '.section-head, .conc-head, .assurance__item, .carousel__item, .feature .split__copy, .feature .split__panel, .box__layout, .review, .faq__item, .figures__item, .guide__area, .bento__tile, .msheet__card, .cindex__row, .matrix__scroll, .footer__brand, .footer__col, .footer__signup';
}

@media (prefers-reduced-motion: no-preference) {
  /* `.js` is set in the head before first paint. With scripting off nothing
     below is hidden, so the page can never be left blank under the fold.

     `:where()` IS LOAD-BEARING, not tidiness. It contributes zero specificity,
     which keeps this whole rule at `.js` — one class — so `.js .is-in` at two
     classes always wins. Written as a plain list, `.js .feature .split__copy`
     scores three classes and beats the reveal, and those two panels stay at
     opacity 0 forever while every other target on the page reveals normally.
     That is exactly what happened, and it is invisible until you look at a
     section that uses a descendant selector. */
  .js
    :where(
      .section-head,
      .conc-head,
      .assurance__item,
      .carousel__item,
      .feature .split__copy,
      .feature .split__panel,
      .box__layout,
      .review,
      .faq__item,
      .figures__item,
      .guide__area,
      .bento__tile,
      .msheet__card,
      .cindex__row,
      .matrix__scroll,
      .footer__brand,
      .footer__col,
      .footer__signup
    ) {
    opacity: 0;
    /* Kept off `transform`: several of these elements already use transform
       for their own effects, and a shared shorthand would clobber it. */
    translate: 0 var(--reveal-rise, 14px);
    transition:
      opacity 620ms var(--ease-out),
      translate 620ms var(--ease-out);
    transition-delay: calc(var(--reveal-i, 0) * 70ms);
  }

  .js .is-in {
    opacity: 1;
    translate: 0 0;
  }
}

/* Smaller elements travel less. A 14px rise on a row in a list of eight is a
   ripple; on a full section panel it is barely visible, which is correct —
   the bigger the block, the less it should need to move to be noticed. */
.cindex__row,
.faq__item,
.footer__col {
  --reveal-rise: 8px;
}

.feature .split__copy,
.feature .split__panel,
.box__layout {
  --reveal-rise: 20px;
}

/* --- Press feedback ---------------------------------------------------------
   Buttons and cards give slightly under the pointer. 1% is nothing to look at
   and everything to feel: without it a click on a large card registers only
   when the next page paints. */
@media (prefers-reduced-motion: no-preference) {
  .btn,
  .card,
  .cmk,
  .icon-btn {
    transition: scale 120ms var(--ease-out);
  }

  .btn:active,
  .icon-btn:active {
    scale: 0.97;
  }

  .card:active {
    scale: 0.99;
  }
}

/* --- Marquee: readout -------------------------------------------------------
   A second style, for the band that runs above the catalogue.

   A scrolling band of claims is a fashion-brand device and this is a
   laboratory supplier, so this one is dressed as an instrument strip instead:
   paper rather than ink, hairline rules top and bottom, a fine tick ruler along
   the base, and the claims set in the data face like readings coming off a
   chart recorder. Same mechanism, completely different voice — which is the
   point of having two, since the page now carries one above the catalogue and
   one further down.

   The ground setting is overridden deliberately. The strip only reads as an
   instrument on paper. */
.marquee--readout {
  padding-block: var(--space-sm);
  background: var(--paper);
  color: var(--ink);
  border-block: 1px solid var(--rule);
  position: relative;
}

/* AN HPLC CHROMATOGRAM, not a ruler.

   The first version drew a tick scale along the base, which read as a ruler —
   a measuring tool, but the wrong instrument and nothing to do with what this
   company does. This is the trace the claims are actually about: the output of
   the reversed-phase HPLC run every lot goes through, with one dominant peak
   for the compound, a shoulder just after it, and a few impurity peaks barely
   off the baseline.

   The proportions are the claim. "Purity >= 99%" is what it looks like when
   one peak carries essentially all the area on a trace like this, so the strip
   is not decoration — it is a picture of the sentence scrolling over it.

   Drawn as a tiling background rather than markup: one paint, no DOM, and the
   path meets the baseline at both edges so it repeats seamlessly. It stays put
   while the claims scroll, which is right — on a chart recorder the paper
   moves and the trace is already printed on it. */
.marquee--readout::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 34px;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='320' height='34' viewBox='0 0 320 34' fill='none'><path d='M0 31L46.5 31L48 30.9L49.5 30.6L51 30.3L54 29.1L55.5 28.8L57 28.9L58.5 29.3L61.5 30.4L63 30.7L66 31L85.5 30.9L87 30.8L88.5 30.4L90 30L91.5 29.6L93 29.7L96 30.6L97.5 30.9L132 30.9L133.5 30.8L135 30.6L136.5 30.2L138 29.3L139.5 27.9L141 25.6L142.5 22.5L144 18.7L145.5 14.5L147 10.7L148.5 8L150 7L151.5 8L153 10.7L154.5 14.5L156 18.7L157.5 22.5L159 25.6L160.5 27.7L162 28.9L163.5 29.2L165 28.7L166.5 28.1L168 27.8L169.5 28.2L172.5 30L174 30.6L175.5 30.9L223.5 31L225 30.9L226.5 30.7L228 30.3L231 29.3L232.5 29.2L234 29.6L237 30.6L238.5 30.8L261 31L262.5 30.9L264 30.7L267 30L268.5 29.9L271.5 30.6L273 30.9L319.5 31' stroke='%23b9c0cc' stroke-width='1.1' stroke-linejoin='round' stroke-linecap='round'/></svg>");
  background-repeat: repeat-x;
  background-position: left bottom;
  opacity: 0.85;
  pointer-events: none;
}

.marquee--readout .marquee__run {
  gap: clamp(var(--space-md), 3.4vw, var(--space-xl));
  padding-left: clamp(var(--space-md), 3.4vw, var(--space-xl));
}

.marquee--readout .marquee__item {
  gap: var(--space-xs);
  /* Clear of the trace: the claims sit above the peaks rather than across
     them, so neither has to be faded to keep the other readable. */
  padding-bottom: var(--space-md);
}

/* The separator becomes a tick in the compound palette — the same colour
   system as the square it replaces, but the shape of a scale mark. */
.marquee--readout .marquee__mark {
  width: 2px;
  height: 0.95rem;
  border-radius: 1px;
  background: var(--mark);
}

.marquee--readout .marquee__text {
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  color: var(--ink-soft);
}

/* --- Marquee concepts -------------------------------------------------------
   Five alternatives to the solid band, all running the same mechanism. */

.marquee__label {
  margin: var(--space-lg) 0 var(--space-2xs);
  font-size: 0.625rem;
  letter-spacing: var(--tracking-wide);
  color: var(--ink-faint);
}

/* 1. Rule — no band at all. Hairlines top and bottom and the page's own
      ground showing through, so it divides two sections without adding a
      third colour between them. The quietest option by a distance. */
.marquee--rule {
  background: transparent;
  color: var(--ink-soft);
  border-block: 1px solid var(--rule);
  padding-block: var(--space-sm);
}

.marquee--rule .marquee__mark {
  width: 0.35rem;
  height: 0.35rem;
  border-radius: 999px;
}

/* 2. Chips — each claim outlined. Reads as a row of tags rather than one
      continuous sentence, which suits a list of separate, checkable facts. */
.marquee--chips {
  background: var(--paper-sunk);
  color: var(--ink);
  padding-block: var(--space-md);
}

.marquee--chips .marquee__run {
  gap: var(--space-2xs);
  padding-left: var(--space-2xs);
}

.marquee--chips .marquee__item {
  gap: var(--space-2xs);
  padding: 0.4rem 0.9rem 0.4rem 0.7rem;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  background: var(--paper);
}

.marquee--chips .marquee__mark {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
}

/* 3. Display — oversized type, held low in contrast. Not a claims band but a
      graphic divider: at this size it is read as texture first and words
      second, which is why the words have to be short. */
.marquee--display {
  background: var(--paper);
  color: var(--ink);
  padding-block: var(--space-lg);
}

.marquee--display .marquee__run {
  gap: var(--space-lg);
  padding-left: var(--space-lg);
}

.marquee--display .marquee__text {
  font-family: var(--font-display);
  font-size: clamp(2rem, 5.5vw, 4rem);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  text-transform: none;
  color: color-mix(in srgb, var(--ink) 22%, var(--paper));
}

.marquee--display .marquee__mark {
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 999px;
}

/* 4. Spectrum — the announcement bar's gradient, so the page opens, divides
      and closes on the same band of colour. Held far fainter than the bar,
      for the same reason the footer is: a 40px strip and a full-width divider
      are not the same amount of colour. */
.marquee--spectrum {
  color: var(--ink);
  padding-block: var(--space-md);
  background-image: linear-gradient(
    to right,
    rgba(248, 215, 167, 0.5) 0%,
    rgba(237, 232, 187, 0.5) 14%,
    rgba(206, 232, 215, 0.5) 28%,
    rgba(183, 198, 184, 0.5) 42%,
    rgba(192, 216, 220, 0.5) 56%,
    rgba(183, 217, 252, 0.5) 70%,
    rgba(186, 181, 243, 0.5) 82%,
    rgba(198, 182, 196, 0.5) 91%,
    rgba(236, 185, 185, 0.5) 100%
  );
}

.marquee--spectrum .marquee__mark {
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 45%, transparent);
}

/* 5. Ticker — the catalogue itself, scrolling. Each compound in its own
      colour with its size beside it, and every item is a link, so the band
      stops being decoration and becomes a way into the shop. The only concept
      here that carries live data rather than copy. */
.marquee--ticker {
  background: var(--paper);
  color: var(--ink);
  border-block: 1px solid var(--rule);
  padding-block: var(--space-sm);
  user-select: auto;
}

.marquee--ticker .marquee__run {
  gap: var(--space-lg);
  padding-left: var(--space-lg);
}

.marquee--ticker .marquee__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  text-decoration: none;
  color: var(--ink);
}

.marquee--ticker .marquee__link:hover {
  color: var(--deep);
}

.marquee--ticker .marquee__mark {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 999px;
  background: linear-gradient(150deg, color-mix(in srgb, var(--mark) 55%, #fff), var(--mark));
}

.marquee--ticker .marquee__text {
  font-family: var(--font-display);
  font-size: var(--size-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.marquee--ticker .marquee__meta {
  color: var(--deep);
}

/* A moving element that is also a set of links must be reachable. Pausing on
   hover and on keyboard focus is the difference between a ticker you can use
   and one you can only watch. */
.marquee--ticker:hover .marquee__track,
.marquee--ticker:focus-within .marquee__track {
  animation-play-state: paused;
}

/* --- Marquee: scroll-linked -------------------------------------------------
   No band, no perpetual motion. Hairlines top and bottom, the page's own
   ground showing through, and type big enough to read at a glance but held
   well back in contrast so it divides two sections without competing with
   either.

   The movement is the feature, so the dressing gets out of its way.

   The animation is removed rather than paused: the script drives `transform`
   directly, and a running animation on the same property would overwrite every
   frame it writes. */
.marquee--scroll {
  background: transparent;
  color: var(--ink);
  border-block: 1px solid var(--rule);
  padding-block: clamp(var(--space-sm), 2vw, var(--space-md));
}

.marquee--scroll .marquee__track {
  animation: none;
  will-change: transform;
}

.marquee--scroll .marquee__run {
  gap: clamp(var(--space-md), 4vw, var(--space-2xl));
  padding-left: clamp(var(--space-md), 4vw, var(--space-2xl));
}

.marquee--scroll .marquee__text {
  font-family: var(--font-display);
  font-size: clamp(1.125rem, 2.4vw, 1.75rem);
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: color-mix(in srgb, var(--ink) 46%, var(--paper));
}

.marquee--scroll .marquee__mark {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
}

/* Without script the track never gets a transform, so it must already read
   correctly sitting still. It does: the first run fills the width. */

/* --- Rounded panels ---------------------------------------------------------
   The hero panel and the two feature panels round the corners that face page
   content and leave the bleed edge square.

   Rounding all four would put a sliver of paper outside the viewport edge,
   where the panel runs off the screen — the curve would be cropped and read as
   a mistake. Only the inner edge has content beside it to be rounded against.

   Which pair curves follows the layout hand. `.split--reverse` puts the panel
   on the left, so it bleeds left and curves right; without it the panel sits
   right, bleeds right and curves left. The hero is always panel-right.

   No overflow change: .feature__panel is deliberately `overflow: visible` so
   the vial can cross the join, and clipping it to get a curve would undo a
   decision made on purpose. The background rounds; the vial still crosses. */
@media (min-width: 56rem) {
  .hero__panel,
  .feature:not(.split--reverse) .feature__panel {
    border-radius: var(--radius-panel) 0 0 var(--radius-panel);
  }

  .feature.split--reverse .feature__panel {
    border-radius: 0 var(--radius-panel) var(--radius-panel) 0;
  }
}

/* Stacked, the panel spans the full width with no content beside it, so there
   is no inner edge to round against. */
@media (max-width: 55.999rem) {
  .hero__panel,
  .feature__panel {
    border-radius: 0;
  }
}

/* --- Icon specimen sheet ---------------------------------------------------- */
.isheet__group {
  margin: var(--space-xl) 0 var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
}

.isheet {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

.isheet__cell {
  display: grid;
  gap: var(--space-2xs);
  justify-items: start;
  padding: var(--space-sm);
  border: 1px solid var(--rule);
  border-radius: var(--radius-small);
}

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

.isheet__btn {
  width: 2.75rem;
  height: 2.75rem;
}

/* The hover state the header gives these, shown standing still. */
.isheet__btn--hover {
  background: var(--paper-sunk);
  color: var(--ink);
}

/* Reversed. An outline that is crisp on paper can close up at this size once
   it is light-on-dark, which is the failure this column exists to catch. */
.isheet__btn--ink {
  background: var(--ink-ground, #17171a);
  color: #fff;
  border-radius: var(--radius-control);
}

.isheet__name {
  color: var(--ink-soft);
}

/* --- Header, pushed to the page edges ---------------------------------------
   The bar drops the content measure and runs on the viewport gutter instead,
   so the wordmark sits hard left and the controls hard right rather than being
   pulled in to the 1500px column the body copy uses.

   This deliberately breaks the alignment the header used to keep with the hero
   headline. That alignment was worth having while the bar was a contained
   object; now the bar is chrome spanning the window, and chrome that stops
   200px short of the edge on a wide screen looks like a mistake rather than a
   measure. The inner padding still nets out to exactly one gutter, so the
   wordmark lands on the page edge margin, not against the glass. */
.header > .container {
  max-width: none;
}


/* --- Hero height ------------------------------------------------------------
   The hero and the bars above it together fill exactly one screen: the
   announcement bar and header (and the WordPress toolbar, for signed-in
   staff) take their height, and the hero takes the rest. It used to be 90%
   of the viewport on its own, which with the bars on top ran past the bottom
   of the screen and cut the photograph off.

   --chrome is the height of everything above <main>, measured in the head
   script of index.php because the announcement bar's height depends on its
   message and the toolbar is only there when signed in. Before the script
   runs, 6.5rem is a close estimate, so there is no visible jump.

   `svh` rather than `vh` so mobile browsers measure against the small viewport
   and the hero does not sit taller than the visible area while the address
   bar is showing. The `vh` line above it is the fallback. */
@media (min-width: 56rem) {
  .hero .split__panel {
    min-height: calc(100vh - var(--chrome, 6.5rem));
    min-height: calc(100svh - var(--chrome, 6.5rem));
  }
}

/* --- Icon style families ----------------------------------------------------
   Six idioms applied to the same geometry. Weight and corner treatment are
   done in CSS rather than by redrawing each glyph six times: `stroke-width`,
   `stroke-linecap` and `stroke-linejoin` are presentation attributes, and a
   CSS declaration beats a presentation attribute — so one set of paths can
   wear every family without six copies drifting apart.

   Solid is the exception. A filled icon is different geometry, not a heavier
   stroke, so it uses the -solid glyphs. */
.fams {
  display: grid;
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.fams__row {
  display: grid;
  grid-template-columns: minmax(16rem, 1fr) auto;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--rule);
  border-radius: var(--radius-small);
}

.fams__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
}

.fams__note {
  margin: var(--space-3xs) 0 0;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.fams__key {
  margin: var(--space-2xs) 0 0;
  color: var(--ink-faint);
}

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

.fams__btn {
  width: 3rem;
  height: 3rem;
  color: var(--ink);
}

.fams__btn--ink {
  background: var(--ink-ground, #17171a);
  color: #fff;
}

/* 1. Monoline — the house set, unchanged. */

/* 2. Fine — hairline. Precise and expensive, and the first to disappear. */
.fam-fine :is(path, circle, rect, line) {
  stroke-width: 1.05;
}

/* 3. Bold — heavier with rounded ends. The commerce default. */
.fam-bold :is(path, circle, rect, line) {
  stroke-width: 2.2;
}

/* 4. Sharp — flat caps, mitred corners, nothing rounded. Instrument-like. */
.fam-sharp :is(path, circle, rect, line) {
  stroke-width: 1.75;
  stroke-linecap: butt;
  stroke-linejoin: miter;
}

.fam-sharp rect {
  rx: 0;
}

/* 5. Solid — uses the filled glyphs; nothing to restyle. */

/* 6. Tile — the glyph knocked out of a filled square, so the control reads as
      a button rather than as a mark. */
.fam-tile .fams__btn {
  background: var(--ink);
  color: var(--paper);
  border-radius: 12px;
}

.fam-tile .fams__btn--ink {
  background: var(--paper-sunk);
  color: var(--ink);
}

@media (max-width: 46rem) {
  .fams__row {
    grid-template-columns: 1fr;
  }
}


/* --- Account: register and sign in -----------------------------------------
   One layout for both pages, because they are one pair and a visitor bouncing
   between them should not feel the page rebuild underneath. The form is the
   wide column and always first in the source, so a phone gets the fields
   before it gets the argument for filling them in. */

.enrol {
  display: grid;
  gap: clamp(var(--space-xl), 5vw, var(--space-3xl));
  align-items: start;
}

@media (min-width: 56rem) {
  .enrol {
    /* The form column is capped rather than fluid. A password field 700px wide
       is not more usable than one at 420 — it just looks like the page could
       not think of anything else to put there. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
  }

  /* Sign in has two fields to register's six, so its aside sits closer in. */
  .enrol--tight {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.6fr);
  }
}

.enrol__form-col {
  max-width: 34rem;
}

.enrol__heading {
  margin: var(--space-2xs) 0 0;
  max-width: 14ch;
  text-wrap: balance;
}

.enrol__lede {
  margin: var(--space-md) 0 0;
  max-width: 44ch;
}

.enrol__form {
  display: grid;
  gap: var(--space-md);
  margin-top: var(--space-xl);
}

/* First and last name share a line on anything wider than a phone: they are
   one piece of information asked in two boxes, and stacking them makes the
   form look twice as long as it is. */
.enrol__pair {
  display: grid;
  gap: var(--space-md);
}

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

.enrol__alert {
  margin: 0;
}

.enrol__submit {
  justify-self: start;
}

.enrol__terms {
  margin: 0;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

.enrol__alt {
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* The mailing-list opt-in reuses .check from the researcher gate rather than
   restating it. An earlier version of this block redefined .check and
   .check input here, which did not just duplicate the gate's checkbox — being
   later in the file it overrode it, quietly changing the gap and the control
   size on the one dialog that stands between a visitor and the catalogue. */

/* Password recovery, as a real disclosure. */
.enrol__recover {
  margin-top: var(--space-lg);
  border-top: 1px solid var(--rule);
}

.enrol__recover-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-md);
  font-size: var(--size-sm);
  color: var(--ink-soft);
  cursor: pointer;
  list-style: none;
}

.enrol__recover-q::-webkit-details-marker {
  display: none;
}

.enrol__recover-q:hover {
  color: var(--ink);
}

.enrol__recover-body {
  margin: 0;
  max-width: 44ch;
}

.enrol__recover-panel {
  padding-bottom: var(--space-lg);
}

.enrol__recover .enrol__form {
  margin-top: var(--space-md);
}

/* The column beside the form. Ruled, not boxed: it is a list of what the
   account holds, and a card around it would make three sentences look like a
   pricing table. */
.enrol__aside {
  position: relative;
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

@media (min-width: 56rem) {
  .enrol__aside {
    /* Aligned to the heading rather than to the top of the column, so the
       caption and the eyebrow sit on one line across the page. */
    margin-top: var(--space-2xl);
  }
}

.enrol__aside-caption {
  margin: 0;
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
}

.enrol__aside-body {
  margin: var(--space-md) 0 0;
  max-width: 42ch;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.enrol__holds {
  display: grid;
  gap: var(--space-lg);
  margin: var(--space-lg) 0 0;
}

.enrol__hold-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-base);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.enrol__hold-body {
  margin: var(--space-3xs) 0 0;
  max-width: 40ch;
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

.enrol__vial {
  position: relative;
  display: block;
  width: min(42%, 7.5rem);
  margin-top: var(--space-xl);
  rotate: -7deg;
}

.enrol__vial::before {
  content: '';
  position: absolute;
  inset: 8% -14% 0;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(
    54% 46% at 50% 52%,
    color-mix(in srgb, var(--acc) 70%, transparent),
    transparent 72%
  );
  filter: blur(12px);
}

.enrol__vial img {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 16px 20px rgb(19 19 21 / 14%));
}


/* --- Account: the file -----------------------------------------------------
   A record rather than a dashboard. The usual customer page is a grid of
   rounded tiles, which is the wrong shape twice over here: the site's whole
   visual argument is documentary, and someone opening this page has come to
   look something up. */

.dossier__head {
  display: grid;
  gap: var(--space-lg);
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 46rem) {
  .dossier__head {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
  }
}

.dossier__heading {
  margin: var(--space-2xs) 0 0;
}

.dossier__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs);
  margin: var(--space-sm) 0 0;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

.dossier__sep {
  color: var(--ink-faint);
}

.dossier__email {
  /* A long address on a narrow phone should wrap inside the line rather than
     widen the document. */
  overflow-wrap: anywhere;
}

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

.dossier__body {
  display: grid;
  gap: clamp(var(--space-xl), 4vw, var(--space-2xl));
  margin-top: clamp(var(--space-xl), 4vw, var(--space-2xl));
  align-items: start;
}


.dossier__caption {
  margin: 0 0 var(--space-md);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
}

/* The ledger. Ruled rows, figures in the data face, the order number set as
   the reference it is — the same furniture the specification tables use, so a
   customer who has read a product page already knows how to scan this. */
.ledger {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--size-sm);
}

.ledger th,
.ledger td {
  padding-block: var(--space-sm);
  text-align: left;
  vertical-align: baseline;
  border-bottom: 1px solid var(--rule);
}

.ledger thead th {
  font-family: var(--font-data);
  font-weight: 400;
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border-bottom-color: var(--rule-strong);
}

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

.ledger__ref {
  font-family: var(--font-data);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ledger__ref a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
}

.ledger__ref a:hover {
  border-bottom-color: currentColor;
}

.ledger__date {
  color: var(--ink-soft);
  white-space: nowrap;
}

/* Right-aligned and tabular, so the column reads down its decimals. */
.ledger__num {
  text-align: right;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* State as a word with a mark beside it, never as colour alone — the status
   has to survive being read aloud and being printed. */
.ledger__state {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--ink-soft);
}

.ledger__state::before {
  content: '';
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 999px;
  background: var(--ink-faint);
}

.ledger__state--paid::before,
.ledger__state--fulfilled::before {
  background: var(--signal-ok);
}

.ledger__state--pending::before,
.ledger__state--partial::before,
.ledger__state--authorized::before {
  background: var(--accent);
}

.ledger__state--refunded::before,
.ledger__state--voided::before {
  background: var(--signal-stop);
}

/* Below 46rem the table stops being a table: each order becomes a block with
   its column names carried in as data-label pseudo-content. Five columns
   cannot be squeezed into 320px, and a horizontally scrolling table is a
   table most people never see the right-hand half of. */
@media (max-width: 45.999rem) {
  .ledger thead {
    /* Visually hidden rather than display:none — the header cells are still
       what the row cells are associated with. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .ledger tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-3xs) var(--space-md);
    padding-block: var(--space-md);
    border-bottom: 1px solid var(--rule);
  }

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

  .ledger th,
  .ledger td {
    padding-block: 0;
    border-bottom: 0;
  }

  .ledger__ref {
    grid-column: 1;
    font-size: var(--size-base);
  }

  .ledger__num {
    grid-column: 2;
    grid-row: 1;
    font-size: var(--size-base);
    color: var(--ink);
  }

  .ledger__date {
    grid-column: 1 / -1;
  }

  .ledger tbody td:nth-child(3),
  .ledger tbody td:nth-child(4) {
    grid-column: 1 / -1;
  }
}

.dossier__empty {
  display: grid;
  justify-items: start;
  gap: var(--space-md);
  padding-block: var(--space-lg);
  border-top: 1px solid var(--rule);
}

.dossier__empty-line {
  margin: 0;
  max-width: 46ch;
  color: var(--ink-soft);
}

.dossier__aside {
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

.dossier__address {
  font-size: var(--size-sm);
  line-height: var(--leading-normal);
  color: var(--ink-soft);
}

/* format_address emits its own paragraph, which would otherwise carry the
   document's default paragraph spacing into a four-line address. */
.dossier__address p {
  margin: 0;
}

.dossier__address-empty {
  margin: 0;
  font-size: var(--size-sm);
  color: var(--ink-faint);
}

.dossier__address-action {
  margin: var(--space-md) 0 0;
  font-size: var(--size-sm);
}

.dossier__note {
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  color: var(--ink-faint);
}

/* --- Certificate register (sections/coa-index.php) ---------------------------
   The Lab Results table: compound, size, lot, purity, certificate. Set like
   the concepts matrix — hairline rows, data in mono, the compound's own
   colour behind its vial — so it reads as the same document system. A
   pending row stays in the table at full weight except for its action,
   because "not on file yet" is information, not an error. */
.register__head {
  display: grid;
  gap: var(--space-xs);
  max-width: 44rem;
  margin-bottom: var(--space-lg);
}

.register__lede {
  color: var(--ink-soft);
}

.register__count {
  justify-self: start;
  margin-top: var(--space-2xs);
  padding: var(--space-3xs) var(--space-xs);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-control);
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.register__table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.register__table thead th {
  padding: 0 var(--space-md) var(--space-xs);
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--font-data);
  font-size: var(--size-xs);
  font-weight: 400;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.register__table thead th:first-child {
  padding-left: 0;
}

.register__row > * {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}

.register__row > :first-child {
  padding-left: 0;
}

.register__row > :last-child {
  padding-right: 0;
  text-align: right;
}

.register__product {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: inherit;
  text-decoration: none;
}

/* The same device as the matrix head and the product card: the vial stands
   in a field of its own compound colour. */
.register__vial {
  position: relative;
  flex: none;
  width: 3rem;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-small);
  background: linear-gradient(170deg, color-mix(in srgb, var(--chip) 42%, #fff), var(--chip));
  overflow: hidden;
}

.register__vial img {
  position: absolute;
  inset: 8% 10%;
  width: calc(100% - 20%);
  height: calc(100% - 16%);
  max-width: none;
  object-fit: contain;
}

.register__names {
  display: grid;
  gap: 2px;
}

.register__name {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.register__product:hover .register__name {
  color: var(--deep);
}

.register__common {
  font-size: var(--size-sm);
  font-weight: 400;
  color: var(--ink-soft);
}

.register__cell {
  color: var(--ink-soft);
  white-space: nowrap;
}

.register__na {
  color: var(--ink-faint);
}

.register__download {
  min-height: 2.5rem;
  padding-inline: var(--space-md);
  white-space: nowrap;
}

.register__pending {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--ink-faint);
  white-space: nowrap;
}

.register__pending::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  border: 1.5px solid currentColor;
}

/* Arriving from a product page's link to #coa-<handle>. */
.register__row:target > * {
  background: color-mix(in srgb, var(--chip) 18%, transparent);
}

.register__note {
  margin-top: var(--space-md);
  max-width: 44rem;
}

.register__note .link-action {
  margin-left: var(--space-3xs);
}

/* Phones: each row becomes a card, its cells labelled from the header. */
@media (max-width: 47.99rem) {
  .register__table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .register__table,
  .register__table tbody {
    display: block;
  }

  .register__row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-xs) var(--space-sm);
    padding: var(--space-md) 0;
    border-bottom: 1px solid var(--rule);
  }

  .register__row > * {
    padding: 0;
    border: 0;
  }

  .register__compound,
  .register__action {
    grid-column: 1 / -1;
  }

  .register__row > .register__action {
    text-align: left;
  }

  .register__cell::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 2px;
    font-family: var(--font-data);
    font-size: var(--size-xs);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--ink-faint);
  }

  .register__download {
    width: 100%;
  }
}

/* --- Silo shadow -------------------------------------------------------------
   One shadow for every product silo, taken from the home page's vial cluster
   (-6px 10px 14px at 14%): cast down and to the left, as if lit from the
   upper right, so a vial on a card, a product page or in the cart sits on
   the same surface as the ones on the home page. Small enough that the vial
   reads as standing, not floating — the cast ellipses on the feature
   sections carry any sense of height.

   Thumbnails get a scaled-down version: at 40px the full shadow is a smudge.
   Places with a shadow tuned to their own composition (the cluster, feature
   vials, 404, sign-in, referral, certificate) keep theirs. */
:root {
  --silo-shadow: drop-shadow(-6px 10px 14px rgb(19 19 21 / 14%));
  --silo-shadow-sm: drop-shadow(-2px 3px 4px rgb(19 19 21 / 16%));
}

/* The card's vial runs off the bottom of its frame, so a shadow cast straight
   down was clipped away; the sideways component is what makes it visible. */
.card__vial,
.product__stage img,
.vial__media img,
.matrix__vial img,
.bento__vial img,
.guide__vial img,
.pick__media img,
.box__free-media img,
.box-teaser__tile-media img {
  filter: var(--silo-shadow);
}

.product__thumb img,
.register__vial img,
.attach__thumb img,
.line-item__media img,
.chip__thumb img,
.cmk__thumb img {
  filter: var(--silo-shadow-sm);
}

/* --- WordPress toolbar ---------------------------------------------------------
   Signed-in staff see WordPress's toolbar (32px, 46px on small screens), fixed
   above the page. WordPress moves the page down for it; the sticky header has
   to stop below it too, or it slides underneath once the page scrolls. Under
   600px the toolbar scrolls away with the page, so the header sticks at 0. */
.admin-bar .site-header {
  top: 32px;
}

@media screen and (max-width: 782px) {
  .admin-bar .site-header {
    top: 46px;
  }
}

@media screen and (max-width: 600px) {
  .admin-bar .site-header {
    top: 0;
  }
}

/* --- Certificate card (product page) ------------------------------------------
   Sits under the research-use line, next to the buying decision. Tinted with
   the compound's own wash like the rest of the page, so it reads as part of
   this product rather than a generic badge. Pending is quieter: no fill, and
   a link to Lab Results in place of the button. */
.coa-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
  border: 1px solid color-mix(in srgb, var(--peptide) 60%, var(--rule));
  border-radius: var(--radius-small);
  background: var(--peptide-wash);
}

.coa-card.is-pending {
  border-color: var(--rule);
  background: transparent;
}

.coa-card__icon {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background: var(--paper-raised);
  color: var(--peptide-deep);
}

.coa-card.is-pending .coa-card__icon {
  background: var(--paper-sunk);
  color: var(--ink-faint);
}

.coa-card__text {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.coa-card__title {
  font-weight: 600;
  font-size: var(--size-sm);
  color: var(--ink);
}

.coa-card__meta {
  font-family: var(--font-data);
  font-size: var(--size-xs);
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}

.coa-card__button {
  min-height: 2.5rem;
  padding-inline: var(--space-md);
  background: var(--paper-raised);
  white-space: nowrap;
}

.coa-card__link {
  margin-right: var(--space-2xs);
  white-space: nowrap;
}

@media (max-width: 30rem) {
  .coa-card {
    grid-template-columns: auto 1fr;
  }

  .coa-card__button,
  .coa-card__link {
    grid-column: 1 / -1;
    justify-self: stretch;
    text-align: center;
  }
}

/* --- Certificate viewer (snippets/coa-viewer.php) ------------------------------
   A large sheet over a dimmed page: the document is the point, so it gets
   nearly the whole window, with a slim header carrying what it is and what
   you can do with it. A native <dialog>, so the backdrop is ::backdrop. */
.coa-viewer {
  width: min(64rem, calc(100vw - 2 * var(--gutter)));
  height: min(56rem, calc(100dvh - 2 * var(--space-lg)));
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  border-radius: var(--radius-surface);
  background: var(--paper-raised);
  color: var(--ink);
  box-shadow: var(--lift-drawer);
  overflow: hidden;
}

.coa-viewer::backdrop {
  background: rgb(19 19 21 / 45%);
  backdrop-filter: blur(4px);
}

.coa-viewer[open] {
  animation: coa-in var(--dur-base) var(--ease-out);
}

@keyframes coa-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.99);
  }
}

.coa-viewer__panel {
  display: grid;
  grid-template-rows: auto 1fr auto;
  height: 100%;
}

.coa-viewer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--rule);
}

.coa-viewer__titles {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.coa-viewer__eyebrow {
  margin: 0;
  font-size: var(--size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.coa-viewer__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-md);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
}

.coa-viewer__lot {
  margin: 0;
  font-size: var(--size-xs);
  color: var(--ink-soft);
}

.coa-viewer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

.coa-viewer__action {
  min-height: 2.5rem;
  padding-inline: var(--space-md);
}

.coa-viewer__close {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
}

.coa-viewer__close:hover {
  background: var(--paper-sunk);
  color: var(--ink);
}

.coa-viewer__body {
  min-height: 0;
  background: var(--paper-sunk);
}

.coa-viewer__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.coa-viewer__foot {
  margin: 0;
  padding: var(--space-xs) var(--space-md);
  border-top: 1px solid var(--rule);
}

@media (max-width: 40rem) {
  .coa-viewer__action {
    display: none;
  }
}

/* --- Type roles ----------------------------------------------------------------
   See the note at --font-title in pryme-tokens.css.

   Names and small titles: Hanken, semibold, lightly tightened. Familjen stays
   on the headings (.h1–.h3, the hero) and the large display figures, where it
   was chosen to be. */
.product__title,
.card__name,
.register__name,
.coa-viewer__title,
.h4,
.vial__name,
.faq__q,
.bento__pname,
.guide__iname,
.matrix__name,
.pick__name,
.cindex__title,
.coa__step-title,
.enrol__hold-title,
.stage__title,
.ledger__spec,
.offer__heading {
  font-family: var(--font-title);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* The product title is the one large name; it can take a little more
   tightening at that size. */
.product__title {
  font-weight: 650;
  letter-spacing: -0.02em;
}

/* Data in Hanken reads best a step heavier than body text, with tabular
   figures so prices and lots still line up. */
.data,
.product__price,
.card__price,
.attach__price,
.spec__value,
.register__cell,
.register__table thead th,
.coa-card__meta,
.qty input {
  font-variant-numeric: tabular-nums;
}

.data,
.spec__value,
.coa-card__meta {
  font-weight: 500;
}

/* Where the monospace is the point: codes you type, and the drawn
   certificate and receipt slip, whose letterforms are part of the picture. */
.code-pill,
.coa__caption,
.coa__value,
.coa__page,
.pact__slip-caption,
.pact__slip-row dd,
.mk-bracket .cmk__bracket {
  font-family: var(--font-code);
}

/* --- Research-use banner ---------------------------------------------------
   The storefront policy's site-wide banner (snippets/ruo-banner.php). Above
   everything, ink on paper inverted, small but never hidden: a statement of
   what the store is, not a promotion, so it gets none of the announcement
   bar's colour. */
.ruo-banner {
  background: var(--ink);
  color: var(--paper);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
}

.ruo-banner__text {
  margin: 0 auto;
  max-width: var(--page-width, 80rem);
  padding: 0.45rem 16px;
  text-align: center;
}

.ruo-banner__text strong {
  font-weight: 600;
}

.ruo-banner__who {
  opacity: 0.8;
  margin-left: 0.5em;
}


/* Below desktop: the required sentence only, on one line where it fits. The
   "sold exclusively to" statement is still on every product page, in the
   footer, and on sign-in and register. */
@media (max-width: 55.99rem) {
  .ruo-banner__who {
    display: none;
  }
}

@media (max-width: 40rem) {
  .ruo-banner {
    font-size: 0.6875rem;
  }

  .ruo-banner__text {
    padding: 0.35rem 16px;
  }
}

/* Sign in: a square photograph in place of the caption and text, with the
   corner radius the product gallery uses. */
.enrol__aside--image {
  padding-top: 0;
  border-top: 0;
}

.enrol__image {
  display: block;
  width: 100%;
  max-width: 26rem;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-panel);
  background: var(--paper-sunk);
}

@media (max-width: 55.99rem) {
  .enrol__aside--image {
    display: none;
  }
}

/* --- Catalogue, signed out --------------------------------------------------
   Stands in for a product list on a public page (snippets/catalog-locked.php):
   a quiet bordered panel, centred, so the page still reads as complete. */
.catalog-locked {
  max-width: 40rem;
  margin: 0 auto;
  padding: clamp(var(--space-xl), 5vw, var(--space-3xl)) var(--space-lg);
  border: 1px solid var(--rule);
  border-radius: var(--radius-panel);
  text-align: center;
}

.catalog-locked__heading {
  margin: var(--space-2xs) 0 0;
}

.catalog-locked__body {
  margin: var(--space-md) auto 0;
  max-width: 34ch;
}

.catalog-locked__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
  margin-top: var(--space-lg);
}

/* Signed out: where the product price would be (inc/compliance.php). */
.product__price--signin {
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--ink-soft);
}

/* --- Account modal -------------------------------------------------------------
   snippets/auth-gate.php. The modal frame (.gate, .gate__panel) with two tabs,
   Google above them, and the two short forms. Creating an account takes a
   date of birth, so this is the age check as well. */
body.auth-required {
  overflow: hidden;
}

/* Sized to fit a laptop or phone browser whole (about 600px tall) without a
   scrollbar of its own: at the age check's spacing it needed 770px, so on
   most screens it scrolled inside its rounded corners with the small print
   cut off. Shorter screens than that scroll the overlay, not the panel; the
   panel's auto margins centre it and give way to zero when it is taller,
   so its top is never out of reach. */
.gate--auth {
  place-items: initial;
  padding: clamp(0.75rem, 2.5vw, 1.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.gate--auth .gate__panel {
  margin: auto;
  max-height: none;
  overflow: visible;
  padding: clamp(1.25rem, 4vw, 2.25rem) clamp(1.25rem, 5vw, 2.5rem);
}

.gate--auth .gate__body {
  margin-top: var(--space-2xs);
}

.auth__google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  margin-top: var(--space-lg);
}

.auth__or {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 0;
  color: var(--ink-faint);
  font-size: var(--size-xs);
}

.auth__or::before,
.auth__or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

.auth__tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3xs, 0.25rem);
  margin-top: var(--space-sm);
  padding: var(--space-3xs, 0.25rem);
  border-radius: var(--radius-control);
  background: var(--paper-sunk);
}

.auth__tab {
  padding: 0.6rem var(--space-2xs);
  white-space: nowrap;
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--size-sm);
  cursor: pointer;
}

.auth__tab[aria-selected='true'] {
  background: #fff;
  color: var(--ink);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
}

.auth__tab:focus-visible {
  outline: var(--focus-ring);
}

.auth__pane[hidden] {
  display: none;
}

/* The age check centres its text; forms read left-aligned. */
.auth__form {
  text-align: left;
  display: grid;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

.auth__errors {
  padding: var(--space-xs) var(--space-sm);
  border-left: 3px solid var(--signal-stop);
  background: color-mix(in srgb, var(--signal-stop) 6%, #fff);
  font-size: var(--size-sm);
}

.auth__errors ul {
  margin: 0;
  padding-left: 1.1em;
}

.auth__fine {
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
}

.auth__fine a {
  color: var(--ink-soft);
}

/* The account modal's two halves: the product photograph beside the forms on
   a wide screen, a slim strip of it above them on a phone. The frame's own
   padding moves onto the forms' half so the photograph runs to the panel's
   rounded edge. */
.gate--auth .gate__panel.auth {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  width: min(54rem, 100%);
  padding: 0;
  overflow: hidden;
}

.auth__art {
  position: relative;
  margin: 0;
  background: var(--paper-sunk);
}

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

.auth__art-caption {
  position: absolute;
  inset: auto 0 0;
  display: grid;
  gap: var(--space-3xs);
  padding: var(--space-xl) var(--space-md) var(--space-md);
  background: linear-gradient(to top, rgb(14 18 28 / 0.78) 30%, transparent);
  color: #fff;
  text-align: left;
}

.auth__art-mark {
  font-family: var(--font-display);
  font-size: var(--size-base);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.auth__art-line {
  max-width: 26ch;
  font-size: var(--size-xs);
  line-height: var(--leading-normal);
  opacity: 0.88;
}

.auth__body {
  padding: clamp(1.25rem, 4vw, 2.25rem) clamp(1.25rem, 5vw, 2.5rem);
}

/* Phone and small tablet: the photograph is a strip, its caption left out. */
@media (max-width: 47.99rem) {
  .gate--auth .gate__panel.auth {
    width: min(33rem, 100%);
  }

  .auth__art {
    height: clamp(4.5rem, 18vw, 7rem);
  }

  .gate--auth .gate__title {
    font-size: var(--size-lg);
  }

  .auth__art img {
    object-position: 50% 38%;
  }

  .auth__art-caption {
    display: none;
  }
}

@media (min-width: 48rem) {
  .gate--auth .gate__panel.auth {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  }

  .auth__body {
    align-self: center;
  }
}

/* Title and lead follow the tab. Set on the server too, so this holds with
   scripting off. */
.gate--auth[data-mode='create'] .auth__title-signin,
.gate--auth[data-mode='signin'] .auth__title-create {
  display: none;
}

/* Date of birth: month, day and year on one row. */
.auth__birth {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.auth__birth legend {
  margin-bottom: var(--space-3xs);
  padding: 0;
  color: var(--ink-soft);
  font-size: var(--size-sm);
}

.auth__birth-row {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: var(--space-2xs);
}

.auth__birth :is(select, input) {
  padding-inline: 0.875rem;
}

.auth__birth select {
  appearance: none;
  padding-right: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%235A6274' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  text-overflow: ellipsis;
  cursor: pointer;
}

.auth__birth select:invalid {
  color: var(--ink-faint);
}

.auth__birth select option {
  color: var(--ink);
}

.auth__birth select:hover {
  border-color: var(--rule-strong);
}

.auth__birth select:focus-visible {
  background-color: #fff;
  border-color: var(--peptide-deep);
}

/* --- Account: setup, details, address ----------------------------------------
   Sign-up takes an email and a password only; the rest is asked for on the
   account page. While it is missing, a panel at the top holds the form; once
   given, it is a short record under "Your details" with an Edit toggle. */
.account-setup {
  display: grid;
  gap: var(--space-lg);
  margin-top: clamp(var(--space-lg), 3vw, var(--space-xl));
  padding: clamp(var(--space-md), 3vw, var(--space-xl));
  border: 1px solid var(--rule);
  border-radius: var(--radius-surface);
  background: var(--paper-raised);
}

@media (min-width: 56rem) {
  .account-setup {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.4fr);
    align-items: start;
  }
}

.account-setup__title {
  margin: 0;
}

.account-setup__body {
  margin: var(--space-xs) 0 0;
  max-width: 34ch;
  color: var(--ink-soft);
}

.account-setup__skip {
  margin: var(--space-md) 0 0;
  font-size: var(--size-sm);
}

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

/* Two by two: first and last name, then institution and phone. */
@media (min-width: 36rem) {
  .account-form {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.account-form .auth__errors,
.account-form__actions {
  grid-column: 1 / -1;
}

.account-grid {
  display: grid;
  gap: clamp(var(--space-xl), 4vw, var(--space-2xl));
  margin-top: clamp(var(--space-xl), 4vw, var(--space-2xl));
}

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

.account-block {
  padding-top: var(--space-md);
  border-top: 1px solid var(--rule);
}

.account-block__saved {
  margin: 0 0 var(--space-sm);
  font-size: var(--size-sm);
  color: var(--signal-ok);
}

/* The record, set like the specification table: label, then value. */
.account-facts {
  margin: 0;
  font-size: var(--size-sm);
}

.account-facts > div {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--space-sm);
  padding-block: var(--space-2xs);
}

.account-facts dt {
  color: var(--ink-faint);
}

.account-facts dd {
  margin: 0;
  color: var(--ink);
}

.account-edit {
  margin-top: var(--space-md);
}

.account-edit > summary {
  display: inline-block;
  cursor: pointer;
  font-size: var(--size-sm);
  list-style: none;
}

.account-edit > summary::-webkit-details-marker {
  display: none;
}

.account-edit[open] > summary {
  margin-bottom: var(--space-md);
}
