/* MJ Design 2026-09-05: the PREVIOUS front end's stylesheet, kept for the engine pages the client did not
   design (checkout, auth, account, orders, track, gate, certificates). It lives in a cascade LAYER so that
   every rule in the client's own catharos.css — loaded unlayered on the same pages — wins on any selector
   both define, and this sheet only fills what his does not (the page-specific components). The first cut
   had it the other way round, and a generic rule in here beat his nav on every one of these pages. */
@layer legacy {
/* ==========================================================================
   CATHAROS LABS — "ASSAY SHEET"
   Authored 2026-08-12. Layers on /theme.css tokens only. No new colour, no
   remote font, no external runtime.

   CONCEPT. A laboratory document, not a storefront: near-white paper, hairline
   rules that behave like a printed form, tabular numerals everywhere a number
   appears, and generous vertical air. The market default in this vertical is
   dark-glassy with a badge cluster; this is the opposite on every axis, and the
   opposition is deliberate rather than incidental.

   THE TIER-0 DECISION THAT SHAPES EVERYTHING. This client has zero certificates
   on file, so every proof surface in the codex is forbidden — no certificate
   plate, no evidence panel, no lookup, no purity figure, no accreditation mark.
   Rather than leave a trust vacuum, the design makes CANDOR the trust surface:
   a standard block ("every vial leaves here with a file behind it")
   carries the weight a certificate would carry elsewhere. It is the one
   high-trust pattern that needs no document to be true, and nothing in this
   market does it.

   MOTIF. The client's logo is a node graph — dots joined by hairlines. That
   becomes the connective rule system (.rule-node), used at section seams only.
   It is never scaled up into a molecular-network background; that is precisely
   the market fingerprint this build must not resemble.
   ========================================================================== */

:root {
  /* Deep navy from the client's own mark, reachable as a token. --sf-accent is
     the lifted interactive tone; --sf-accent-1 is the deepest ramp step and is
     effectively #22167A, which is his wordmark. Headings use the deep one so the
     page reads in his brand, buttons use the lifted one so they read as
     controls. */
  /* THE HEADING INK IS THE CLIENT'S (2026-08-19). His reference sets every
     heading in --ink-900 #0F1729, a near-black navy, and reserves the mid-blue
     --blue-600 for the one accented hero line (styles.css:145,148). This used to
     read var(--sf-accent-5) with a comment calling that "#22167A — the client's
     wordmark navy". IT IS NOT, and had not been for some time: the solver emits
     accent-5 as #1A406B (deltaEok 0.1624 from #22167A), so all 45 rules that
     take this token were setting type in a mid navy nobody had chosen. The
     comment was true when written and went false when the ramp was re-solved,
     which is the same failure mode as the --ca-teal contrast note above.
     Now a literal, because it is a stated brand value and not a derived one.
     Measured: 16.37:1 on --sf-bg, 17.09 on --sf-surface, 17.87 on white,
     14.97 on --sf-band — against 9.67 / 10.09 / 10.55 / 8.84 before. */
  /* 2026-09-10: his new --ink-900 (#1C2B4A), so no page on the site is darker than his; was #0F1729 */
  --ca-ink: #1C2B4A;
  --ca-line: color-mix(in srgb, var(--sf-accent-5) 18%, transparent);
  --ca-line-strong: color-mix(in srgb, var(--sf-accent-5) 38%, transparent);
  --ca-wash: color-mix(in srgb, var(--sf-accent) 4%, transparent);
  /* OWNER DECISION 2026-08-18: product photography sits on PURE WHITE, not on
     the 4% accent wash. The vial masters stay genuine cut-outs — the home hero
     overlaps three of them with alpha and drop shadows, and a baked white box
     would turn that bundle into three stacked rectangles — so the ground is a
     SURFACE token, painted by every frame a product photo actually lands in.
     One token, because the grid tile, the related strip and the PDP figure
     have to be the same shoot or the catalogue reads as two. */
  --ca-photo-bg: #FFFFFF;
  --ca-measure: 68ch;
  --ca-gutter: clamp(1.15rem, 4vw, 2.75rem);
  --ca-seam: clamp(3.25rem, 8vw, 6.5rem);

  /* THE CLIENT SPECIFIED HIS OWN TYPE SYSTEM and all three faces now ship from
     this repo. His README names them: display Familjen Grotesk, body Inter, data
     IBM Plex Mono, loaded in his file by a fonts.googleapis.com @import that he
     himself flagged for self-hosting. Vendored 2026-08-19 with the new
     factory/tools/vendor-font.mjs (all three OFL, latin + latin-ext, metric-
     matched fallback faces so the swap is a repaint and not a reflow).
     THIS RETIRES THE LITERATA PIN of 2026-08-14. That was an owner elevation
     pass toward an editorial-warm document-of-record voice, and the concept it
     served — the ASSAY SHEET brief at the top of this file — is the concept the
     client's reference replaces. A serif display under a clinical navy
     identity would read as neither.
     NB themes/fonts.mjs bans Inter and IBM Plex from the CURATED PAIR LIST on
     distinctiveness grounds ("lookalike stores all use those"). That list
     governs what the SEED may pick for a client who has not chosen; it does not
     govern a client who has. Vendoring the bytes does not make either family
     seed-selectable — that would be a separate edit to FONT_PAIRS, and it is
     deliberately not made. */
  --sf-font-display: 'Familjen Grotesk', 'Familjen Grotesk Fallback', 'Segoe UI', system-ui, sans-serif;
  --sf-font-body: 'Inter', 'Inter Fallback', 'Segoe UI', system-ui, sans-serif;
  --ca-font-mono: 'IBM Plex Mono', 'IBM Plex Mono Fallback', ui-monospace, monospace;

  /* THE PASS STATE. The client's own coded design reference (2026-08-19,
     factory/clients/catharos/client-design-reference/) names one:
     "Verified/pass state: green #2f8f5b — used consistently for anything
     confirmed/passed". His HUE ships; his LIGHTNESS does not, because #2f8f5b
     measures 3.70:1 on this site's own ground and fails AA as text.
     Solved with the repo's own ensureContrastAll(hex, [bg, surface, white,
     band], 4.5) — lightness walked, hue and chroma preserved (deltaEok 0.0724):
       #0D7947 → 5.00 : 1 on --sf-bg      #F1F5FE
                 5.22 : 1 on --sf-surface #F7FAFF
                 5.46 : 1 on white
                 4.58 : 1 on --sf-band    #E5EBF9
     THE TOKEN THIS REPLACES WAS A LATENT DEFECT. --ca-teal was #0B8F7F and its
     comment read "deepened for 4.5:1 on paper" — true when written against the
     old ground, and 3.66:1 against the ground the solver emits today. The
     ground moved and nothing re-checked it, because no gate sampled this token.
     It is sampled now: qa.mjs CONTRAST_TARGETS 'pass state'. */
  --ca-pass: #0D7947;
  --ca-teal: var(--ca-pass); /* retained alias — 40+ rules still name it */

  /* THE CHROME. The client's reference paints the nav and the footer near-black
     navy (--navy-950 #0a0f1c, styles.css:71 and :372) on a light page. That is
     HIS ground and it wins on authority 3 (client's own evidence) over this
     file's default — BUILD-DEFAULTS.md §1. What does NOT come from his file is
     the INK: chrome de-emphasis here is a SOLVED SECOND COLOUR, never an alpha
     fade toward transparent (a faded ink walks back toward its own ground and
     composites whatever scrolls under it, so no gate can measure it), and the
     bar is OPAQUE for the same reason. CLAUDE.md design doctrine.
     Measured on #0A0F1C:
       --ca-chrome-bg      17.52 : 1 against the page ground (visibility floor 1.25)
       --ca-chrome-ink     19.13 : 1   white, as he supplied it
       --ca-chrome-link     9.34 : 1   his .nav-links a #a9b6cc, passes as supplied
       --ca-chrome-accent   9.91 : 1   his --blue-300 #9dbde8, the active underline
       --ca-chrome-ink-dim  4.97 : 1   SOLVED. His footer fine print #6b7690 is
                                       4.21:1 and fails AA; ensureContrast lifted
                                       it to #77829C (deltaEok 0.0406).
     NB --sf-accent #3E6798 is 3.28:1 on this bar. Any nav- or footer-scoped rule
     inheriting the page's link colour is a legibility failure — every one of them
     is overridden below. */
  /* SECONDARY INK, from the client's own file (--ink-600, styles.css:22). His
     is DARKER than the theme's --sf-muted #536383, and the difference is not
     cosmetic: #536383 measures 5.53:1 on this page ground but only 4.50:1 where
     qa.mjs samples the age gate's exit control and 4.17:1 under the home page's
     high-variance hero — i.e. it was already generating five standing contrast
     warnings before this session and tipped to a FAILURE the moment the wider
     body face moved the sampled pixels. #4b5768 clears every one of them:
     6.72 on --sf-bg, 7.01 on --sf-surface, 7.34 on white, 6.14 on --sf-band,
     5.47 and 5.07 on the two grounds qa actually sampled. */
  --sf-muted: #4b5768;

  --ca-chrome-bg: #0A0F1C;
  --ca-chrome-ink: #FFFFFF;
  --ca-chrome-ink-dim: #77829C;
  --ca-chrome-link: #A9B6CC;
  --ca-chrome-accent: #9DBDE8;
  --ca-chrome-rule: color-mix(in srgb, var(--ca-chrome-ink) 12%, transparent);
}

/* Re-tuned for the grotesque (2026-08-19). The -0.012em / 550 pair below was
   measured for Literata; a serif carries tracking that a grotesque at the same
   value reads loose in. The client's own display sizes are the reference:
   h1 -1.5px at 68px = -0.022em at weight 700, h2 -0.8px at 42px = -0.019em.
   550 also does not exist as a Familjen cut (the variable range is 400..700, so
   it interpolated); 600/700 are real instances. */
h1, h2, h3, h4, .display { letter-spacing: -0.02em; font-weight: 600; }
h1 { font-weight: 700; }
.eyebrow, .panel-n, .pcard-cat, .tag, .pill, .btn, .tab, .co-steps button,
.specimen-head, .foot .foot-h, .foot-eyebrow, .acct-nav a, .cat-cell .c,
.trail-what, .record-id, .snav-word, .stat .v {
  font-family: var(--sf-font-display);
}
/* THE DATA FACE. In the client's reference the mono is not decoration — it is
   what marks a value as read off a document rather than written by a marketer:
   the eyebrow, the category line, the lot number, the purity label and every
   figure (styles.css .eyebrow/.product-cat/.verify-foot .lot/.verify-purity-label).
   Those surfaces take it here; buttons and the nav wordmark stay on the display
   face, as they do in his file. */
.pcard-cat, .record-id, .stat .v, .specimen-head {
  font-family: var(--ca-font-mono);
  font-feature-settings: "tnum" 1;
}
.eyebrow .tick { width: .45rem; height: .45rem; border-radius: 999px; background: var(--ca-teal); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ca-teal) 18%, transparent); flex: none; }
.pill-on, .msg-ok { color: var(--ca-teal); }
.pill-on { border-color: color-mix(in srgb, var(--ca-teal) 55%, transparent); }

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

body {
  margin: 0;
  background: var(--sf-bg);
  color: var(--sf-text);
  font-family: var(--sf-font-body);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, .display {
  font-family: var(--sf-font-display);
  color: var(--ca-ink);
  line-height: 1.08;
  letter-spacing: -0.017em;
  margin: 0 0 0.5em;
  font-weight: 600;
  text-wrap: balance;
}
h1 { font-size: clamp(2.1rem, 1.35rem + 3.4vw, 3.9rem); }
h2 { font-size: clamp(1.55rem, 1.15rem + 1.6vw, 2.4rem); }
h3 { font-size: clamp(1.12rem, 1rem + 0.55vw, 1.4rem); }
p  { margin: 0 0 1.05em; max-width: var(--ca-measure); }
a  { color: var(--sf-accent); text-underline-offset: 0.18em; }
a:hover { color: var(--sf-accent-4); }
img, svg, video { max-width: 100%; height: auto; display: block; }

/* Every numeral on this site is tabular. On a page whose whole argument is
   "we show you the data", digits that shift width read as sloppy. */
.num, table, .price, .spec-v, td, th, time { font-variant-numeric: tabular-nums; }

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

.wrap { width: 100%; max-width: 76rem; margin-inline: auto; padding-inline: var(--ca-gutter); }
.wrap-tight { max-width: 54rem; }
.section { padding-block: var(--ca-seam); }
.stack > * + * { margin-top: 1rem; }
.lede { font-size: clamp(1.06rem, 1rem + 0.4vw, 1.25rem); color: var(--sf-text); max-width: 58ch; }
.muted { color: var(--sf-muted); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- eyebrow: the form-field label of the system ------------------------- */
/* THE EYEBROW IS THE DATA FACE, and this is the rule that decides it — the
   grouped one further up sets the same property at the same specificity and
   loses on source order, which is why an earlier pass at this appeared to do
   nothing (measured: computed font-family came back Familjen Grotesk and IBM
   Plex Mono never even downloaded, because nothing on the page asked for it).
   Colour follows the client too: his .eyebrow is --blue-600, the accent, not a
   muted grey (styles.css:127). */
.eyebrow {
  font-family: var(--ca-font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--sf-accent);
  margin: 0 0 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
.eyebrow::after {
  content: ""; flex: 1; height: 1px;
  background: var(--ca-line);
}

/* --- node rule: the logo motif as a section seam ------------------------- */
.rule-node {
  border: 0; height: 9px; margin: 0;
  background:
    radial-gradient(circle at 50% 50%, var(--ca-line-strong) 0 2px, transparent 2.5px) 0 0/22px 9px repeat-x,
    linear-gradient(var(--ca-line), var(--ca-line)) 0 50%/100% 1px no-repeat;
}

/* --- buttons ------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 0.5rem;
  font-family: var(--sf-font-display);
  font-size: 0.9rem; font-weight: 600; letter-spacing: 0.01em;
  padding: 0.82rem 1.5rem;
  border-radius: var(--sf-radius);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background-color .16s ease, color .16s ease, border-color .16s ease, transform .16s ease;
}
.btn-primary { background: var(--sf-accent); color: var(--sf-accent-text); }
.btn-primary:hover { background: var(--sf-accent-4); color: var(--sf-accent-text); transform: translateY(-1px); }
.btn-ghost { background: transparent; color: var(--ca-ink); border-color: var(--ca-line-strong); }
.btn-ghost:hover { background: var(--ca-wash); color: var(--ca-ink); border-color: var(--sf-accent); }
/* Disabled reads as ink-on-surface at full opacity, not a faded accent: an
   opacity fade dropped "Awaiting stock" to 2.43:1 and the QA contrast check
   (rightly) failed it — a state label is still text someone has to read. */
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--sf-surface); color: var(--sf-text);
  border-color: var(--ca-line-strong);
  cursor: not-allowed; transform: none;
}
.btn[disabled]:hover, .btn[aria-disabled="true"]:hover { background: var(--sf-surface); color: var(--sf-text); transform: none; }
.btn-sm { padding: 0.55rem 0.95rem; font-size: 0.82rem; }

/* --- nav ---------------------------------------------------------------- */
/* THE BAR IS OPAQUE AND BRAND-COLOURED (client's design reference, 2026-08-19:
   .nav { background: var(--navy-950) }, styles.css:71). It was a 92% wash over
   --sf-bg with a backdrop blur: a translucent bar composites whatever scrolls
   under it, so its real contrast is a function of the page content and no gate
   can measure it. The blur goes with it — there is nothing to see through. */
.snav {
  position: sticky; top: 0; z-index: 60;
  background: var(--ca-chrome-bg);
  border-bottom: 1px solid var(--ca-chrome-rule);
}
/* THREE COLUMNS, and the outer two are equal-weight `1fr` — that is the whole
   trick. A flex row with `margin-right:auto` on the logo only puts the links
   BETWEEN the logo and the actions, which is not the same as centred: the row
   drifts by half the difference between the logo's width and the actions' width,
   and with a 220px lockup on one side and 120px of icons on the other it drifted
   visibly. Under `1fr auto 1fr` the middle cell is centred on the header itself,
   independent of what either side weighs.
   Mobile drops to `1fr auto` (logo | actions) because `.snav-links` is
   `display:none` there and an unplaced grid item occupies no column. */
.snav-in {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: .5rem;
  min-height: 4.7rem;
}
@media (min-width: 62rem) {
  .snav-in {
    grid-template-columns: 1fr auto 1fr;
    gap: clamp(.75rem, 1.8vw, 1.6rem);
    /* Grown to seat the taller lockup: 64px of artwork plus breathing room. */
    min-height: 6.1rem;
  }
}
.snav-logo { display: flex; align-items: center; gap: .6rem; text-decoration: none; justify-self: start; }
/* One rule owns the mark. Pages used to carry inline heights that had drifted
   to four different values (2.1 / 2.4 / 2.9 / 3.5rem) depending on which wave
   last touched them; those inline styles are gone. */
.snav-logo img { height: 2.1rem; width: auto; }
/* LOGO-NAV.png is the full horizontal lockup — mark PLUS wordmark, 1240x361, so
   3.43:1. The old nav art was the mark alone at 59px and this has to read as a
   deliberate step up: ~64px tall (≈220px wide) on desktop, easing to ~44px
   (≈151px) at 375px. The track above is sized so neither end can collide with
   the centred links at 1280 or at 375.
   `filter: none` is not decoration. The engine's logo convention composites
   `var(--sf-logo-filter, brightness(0) invert(1))` onto nav marks so a one-tone
   silhouette can flip per theme mode; this artwork is already the correct navy
   with a real alpha channel, and any such filter would flatten it to a slab.
   Stated explicitly here so a future theme emission cannot reach it. */
/* Stepped DOWN ~10% on 2026-08-15 (owner: "a tiny bit too big"): 64px → 57.6px
   at 1280 and up, 44.3px → 40.3px at 375. Still a deliberate step up from the
   old mark-alone nav art, just no longer the loudest object in the bar — the
   account and cart glyphs grew in the same pass to rebalance the track. */
/* THE NAV LOGO NOW INVERTS TOO. The "filter: none" this replaces was correct
   for exactly as long as the bar was near-white paper — see the footer note
   below, which measured this same artwork as 44% #001070 by opaque pixel count,
   i.e. its own darkest tone. On the client's #0A0F1C bar that is invisible.
   Same engine convention as the footer (--sf-logo-filter), one flat #fff. */
.snav .snav-logo img {
  height: clamp(2.5rem, 2.03rem + 2vw, 3.6rem);
  width: auto;
  filter: brightness(0) invert(1);
}
/* Grown from 3rem on 2026-08-15 (owner: "increase size of catharos logo on
   footer"). The artwork behind it also changed that day — brand.logo used to be
   a 156x148 file that was 92% opaque WHITE, so it sat on a visible plate in the
   footer; it is now a real cut-out at 603x549 (factory/tools/logo-cutout.mjs),
   which is what makes a larger size worth having. */
/* The footer logo INVERTS as of round 6 (2026-08-15), because the footer is now
   the brand navy — see `.foot`. The rule above says `filter: none` is
   deliberate on the nav, and it still is: there the artwork's own navy is
   correct against near-white paper and a silhouette would throw away a shaded
   mark for nothing. On a #22167A ground the same artwork is 44% #001070 by
   opaque pixel count, which is the mark's own darkest tone sitting on a darker
   version of itself — invisible. Measured alternatives, both rejected:
   `brightness(2.6)` drives the navy to a saturated #0029FF that reads as a
   different brand; `invert(1) hue-rotate(180deg)` (the usual dark-mode trick)
   maps the mark's HIGHLIGHTS to #284868, i.e. it prints the shading as a
   negative. `brightness(0) invert(1)` is the engine's own dark-ground logo
   convention (`--sf-logo-filter`), gives one flat #fff at 12.5:1, and loses
   only a gradient that is not legible at 68px anyway. */
.foot .snav-logo img { height: 4.25rem; filter: brightness(0) invert(1); }
.snav-word {
  font-family: var(--sf-font-display); font-weight: 600; font-size: 1.02rem;
  letter-spacing: -0.01em; color: var(--ca-chrome-ink); white-space: nowrap;
}
.snav-links { display: none; gap: clamp(.8rem, 1.7vw, 1.6rem); align-items: center; }
@media (min-width: 62rem) { .snav-links { display: flex; justify-self: center; } }
/* NAV-SCOPED RULES TAKE NAV COLOURS. --sf-text and the global
   "a { color: var(--sf-accent) }" are solved against the PAGE, not the bar:
   --sf-accent is 3.28:1 on #0A0F1C. Both are overridden here, explicitly. */
.snav-links a {
  font-size: 0.875rem; color: var(--ca-chrome-link); text-decoration: none;
  padding: .4rem 0; position: relative; white-space: nowrap;
  transition: color .15s ease;
}
.snav-links a:hover, .snav-links a[aria-current="page"] { color: var(--ca-chrome-ink); }
.snav-links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
  background: var(--ca-chrome-accent); transform: scaleX(0); transform-origin: left;
  transition: transform .18s ease;
}
.snav-links a:hover::after, .snav-links a[aria-current="page"]::after { transform: scaleX(1); }
.snav-acts { display: flex; align-items: center; gap: .25rem; justify-self: end; }
.snav-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; border-radius: var(--sf-radius);
  color: var(--ca-ink); background: none; border: 0; cursor: pointer;
  text-decoration: none; position: relative;
}
.snav-ico:hover { background: var(--ca-wash); }
/* Scoped to the BAR, so the cart drawer's own close button (which reuses
   .snav-ico on a light panel) keeps the page ink. */
.snav .snav-ico { color: var(--ca-chrome-ink); }
.snav .snav-ico:hover { background: color-mix(in srgb, var(--ca-chrome-ink) 12%, transparent); }
/* The account and cart glyphs were 20px inside a 40px box and read as an
   afterthought beside the lockup (owner, 2026-08-15: "make the profile, and the
   card icons a little bigger"). 23px of glyph in a 43px box — the hit target
   grows with the mark, and the nav track is sized off `min-height`, so the bar
   does not move.
   Set HERE and not on the inline width/height attributes: all 21 pages carry
   their own copy of the nav block, an author-origin CSS rule beats a
   presentational attribute, and one rule means the copies cannot drift apart
   again the way the logo heights once did.
   Scoped to `.snav-acts` deliberately — `.snav-ico` is reused by the cart
   drawer's close button, which is correct at its current size. */
.snav-acts .snav-ico { width: 2.7rem; height: 2.7rem; }
.snav-acts .snav-ico svg { width: 1.45rem; height: 1.45rem; }
.snav-cart-badge {
  position: absolute; top: .3rem; right: .28rem; min-width: 1.05rem; height: 1.05rem;
  padding: 0 .22rem; border-radius: 999px;
  background: var(--ca-chrome-accent); color: var(--ca-chrome-bg);
  font-size: .66rem; font-weight: 700; line-height: 1.05rem; text-align: center;
}
.snav-burger { display: inline-flex; }
@media (min-width: 62rem) { .snav-burger { display: none; } }
#mnav { display: none; border-top: 1px solid var(--ca-chrome-rule); background: var(--ca-chrome-bg); }
#mnav[data-open="1"] { display: block; }
#mnav a {
  display: block; padding: .95rem var(--ca-gutter);
  border-bottom: 1px solid var(--ca-chrome-rule);
  color: var(--ca-chrome-link); text-decoration: none; font-size: .95rem;
}
#mnav a:hover { background: color-mix(in srgb, var(--ca-chrome-ink) 10%, transparent); color: var(--ca-chrome-ink); }

/* --- RUO disclaimer bar -------------------------------------------------- */
/* `#site-disclaimer` is GONE from this client, element and all (owner,
   2026-08-15, third time of asking: "I want that completely taken off all
   pages. I don't know why that's still there").
   It survived the first two passes because of a belief recorded in the comment
   that used to sit here — that the id is load-bearing and "deleting it fails
   the build". It is not, and it does not. Checked before removing:
     · factory/tools/qa.mjs lists it in CONTRAST_TARGETS only, and that loop
       does `if (!el) { dispose(); continue; }` — an absent target emits NO
       check at all, so nothing fails and nothing even warns.
     · copy/compliance/scan.mjs deliberately does not look for missing
       disclaimers (see its header): it fires on prohibited CLAIMS, not on
       absent boilerplate.
     · The RUO position is unchanged on every page regardless — the footer's
       `.foot-fine` still carries "Not for Human or Veterinary Use. Not a Drug."
       in bold plus the full FDA paragraph, which is a stronger disclosure than
       the strip ever was and is what a regulator would actually read.
   engine/FRONTEND-CONTRACT.md §D still lists the element as a convention; that
   entry has been amended rather than left to re-seed this on the next build. */

/* --- launch-offer bar ---------------------------------------------------- */
/* Seated directly under the nav. The code is real and server-validated; there
   is deliberately NO countdown, because LAUNCH20 carries no expiry in the data
   and a fabricated deadline is a false claim, not a design flourish. */
.promo-bar {
  /* clip, not hidden alone: `hidden` on its own makes the bar a SCROLL
     CONTAINER, and the figures below deliberately overhang both ends of it.
     Same hygiene as `.constellation` and `.vfield` — see the note at
     `.constellation` for the probe this cost us once. */
  display: block; position: relative; overflow: hidden; overflow: clip;
  background: linear-gradient(100deg, var(--ca-ink) 0%, #1E2C55 42%, #0E1836 100%);
  color: #EDEFFA;
  border-bottom: 1px solid color-mix(in srgb, #fff 12%, transparent);
}
/* A slow specular sweep — the one piece of motion the bar gets. */
.promo-bar::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, color-mix(in srgb, #fff 13%, transparent) 50%, transparent 62%);
  transform: translate3d(-60%, 0, 0);
  animation: promo-sheen 7.5s ease-in-out 1.5s infinite;
}
@keyframes promo-sheen { 0% { transform: translate3d(-60%,0,0); } 55%, 100% { transform: translate3d(160%,0,0); } }
@media (prefers-reduced-motion: reduce) { .promo-bar::after { animation: none; opacity: 0; } }
/* The brand's node-graph motif, floating across the bar. This ornament is on
   its THIRD form and every round is recorded here, because each one was an
   owner correction and re-deriving a dead one would cost the same argument
   twice.
     · ROUND 5 (owner, 2026-08-15) killed a 22rem square of two TILING dot
       lattices. The bar is 59px tall, so what actually shipped was a thin
       horizontal slice through a repeating pattern — the tile boundaries landed
       wherever the viewport put them, the connecting geometry that makes the
       mark a *graph* was absent entirely, and it read as scattered specks ("the
       dots on the right side … are kind of confusing. I don't know what you're
       really trying to do there"). It was redrawn as ONE FINITE FIGURE: six
       nodes joined by hairline edges, an open run that echoes the mark rather
       than tiling a texture. An inline SVG data-URI — no network request, no
       404 surface, and unlike a repeating gradient the geometry is exact and
       what is drawn is what ships at every viewport width.
       THAT FIGURE IS STILL HERE, node for node, as `.promo-fig-a`. An approved
       figure does not get redrawn because a later round adds company.
     · ROUND 6 (owner, 2026-08-15) made it TRAVEL: "the dots structure thing …
       must be moving, travelling across the launch-offer header horizontally."
     · ROUND 7 (owner, 2026-08-16) is this one: "I want more of the dot
       structures maybe in different shapes like the constellations or
       something. Spread them out and also have them floating across the launch
       offer section." One figure crossing an empty bar is a procession of one.
       Three different grammars — an open run, a closed cluster, a branching
       spine — at three speeds, spread across the whole bar from the first
       frame, is a field.
   How it moves, and why this way. Round 6's three reasons all still hold:
     · TRANSFORM ONLY, no per-frame paint. Animating `background-position` would
       repaint the whole bar 60 times a second on every page of the site, which
       is the opposite of what the rest of this stylesheet does.
     · each figure stays ONE FINITE FIGURE. Making any of them `repeat-x` so it
       could travel seamlessly would put round 5's scattered-specks lattice
       straight back. Three finite graphs cross and re-enter; nothing tiles.
     · the mask fades each figure's OWN edges rather than the bar's right side.
       Carried on the travelling element it moves with the figure, so a graph
       dissolves in and out at its own extremities and never arrives or leaves
       on a hard clip line. It also keeps the ornament soft where it passes
       behind the copy, which is the job round 5's one-sided mask was doing.
   What round 7 changed:
     · `::before` is no longer the figure. It keeps only the radial glow it
       always also carried — the depth layer, and the one piece of this ornament
       that is alive with JS off. Its `will-change: transform` died with the
       travel and is NOT replaced: a running animation promotes its own layer
       for exactly as long as it runs, which is the same lesson the
       `.constellation` block below paid for once already.
     · the figures are `<span class="promo-fig">` elements mounted by
       catharos.js. One pseudo-element cannot be three things, and a bar does
       not have three more pseudo-elements to spend.
     · the crossing animates the `translate` PROPERTY while the bob animates
       `transform: translateY()`. Individual transform properties compose with
       `transform` in spec order (translate, then rotate, then scale, then
       transform), so both animations run on one element and neither clobbers
       the other. That is precisely what lets a figure FLOAT while it crosses,
       with no wrapper element and no second layer per figure.
     · spread comes from NEGATIVE animation-delays, not from staggered starts:
       at first paint A is ~18% through its crossing, B ~52%, C ~92%, so the bar
       is populated on frame one and nothing waits off-screen for its turn. The
       three durations are co-prime enough that the composition never repeats.
       Crossing speeds at 1280w: A ≈52px/s, B ≈34px/s, C ≈41px/s — round 6's
       approved drift register (~68px/s), calmed one step now that three of them
       share the bar.
   Reduced motion parks a finished, balanced still; see the block below. */
.promo-bar::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(130% 160% at 100% 50%,
    color-mix(in srgb, var(--sf-accent-1) 24%, transparent), transparent 72%);
}
.promo-fig {
  position: absolute; top: 0; bottom: 0; left: 0; z-index: 0; pointer-events: none;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 26%, #000 74%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 26%, #000 74%, transparent 100%);
  animation-name: promo-cross, promo-bob;
  animation-timing-function: linear, ease-in-out;
  animation-iteration-count: infinite, infinite;
}
/* `%` in translate resolves against the element's own width, `vw` against the
   viewport — mixing them is what lets a fixed-width figure cross a full-bleed
   bar whose width is not known to the stylesheet. Unchanged from round 6
   except that it now drives `translate` rather than `transform`, to leave
   `transform` free for the bob. */
@keyframes promo-cross { from { translate: -100% 0; } to { translate: 100vw 0; } }
@keyframes promo-bob   { 0%, 100% { transform: translateY(2px); } 50% { transform: translateY(-3px); } }

/* Figure A — "the open run". Round 5's owner-approved six-node figure, copied
   here verbatim. */
.promo-fig-a {
  width: min(30rem, 48%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 60'%3E%3Cg fill='none' stroke='%23fff' stroke-opacity='.24' stroke-width='1'%3E%3Cpath d='M24 46L74 22L128 40L182 16L232 36L276 20'/%3E%3Cpath d='M24 46L128 40'/%3E%3Cpath d='M128 40L232 36'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Ccircle cx='24' cy='46' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='74' cy='22' r='2.9' fill-opacity='.72'/%3E%3Ccircle cx='128' cy='40' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='182' cy='16' r='2.9' fill-opacity='.72'/%3E%3Ccircle cx='232' cy='36' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='276' cy='20' r='2.5' fill-opacity='.6'/%3E%3C/g%3E%3C/svg%3E");
  background-size: auto 62%; background-position: center 50%;
  animation-duration: 34s, 8s;   animation-delay: -6s, 0s;
}
/* Figure B — "the closed cluster": a pentagon loop with one spur, the compact
   counterpoint to A's open run. Its edges are a step fainter (.2 against A's
   .24) because a closed loop carries more ink over the same span. */
.promo-fig-b {
  width: min(19rem, 32%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 60'%3E%3Cg fill='none' stroke='%23fff' stroke-opacity='.2' stroke-width='1'%3E%3Cpath d='M28 40L58 14L104 20L126 42L74 50Z'/%3E%3Cpath d='M104 20L160 16'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Ccircle cx='28' cy='40' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='58' cy='14' r='2.9' fill-opacity='.72'/%3E%3Ccircle cx='104' cy='20' r='3.1' fill-opacity='.78'/%3E%3Ccircle cx='126' cy='42' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='74' cy='50' r='2.5' fill-opacity='.6'/%3E%3Ccircle cx='160' cy='16' r='2.5' fill-opacity='.6'/%3E%3C/g%3E%3C/svg%3E");
  background-size: auto 52%; background-position: center 32%;
  animation-duration: 46s, 11s;  animation-delay: -24s, -3s;
}
/* Figure C — "the branch": a spine that forks at a hub, so it reads as lineage
   rather than as a run or a ring. The third grammar. */
.promo-fig-c {
  width: min(23rem, 38%);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 60'%3E%3Cg fill='none' stroke='%23fff' stroke-opacity='.24' stroke-width='1'%3E%3Cpath d='M16 32L64 38L110 26L152 12L198 20'/%3E%3Cpath d='M110 26L148 46L192 38'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Ccircle cx='16' cy='32' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='64' cy='38' r='2.5' fill-opacity='.6'/%3E%3Ccircle cx='110' cy='26' r='3.1' fill-opacity='.78'/%3E%3Ccircle cx='152' cy='12' r='2.9' fill-opacity='.72'/%3E%3Ccircle cx='198' cy='20' r='2.2' fill-opacity='.46'/%3E%3Ccircle cx='148' cy='46' r='2.5' fill-opacity='.6'/%3E%3Ccircle cx='192' cy='38' r='2.9' fill-opacity='.72'/%3E%3C/g%3E%3C/svg%3E");
  background-size: auto 56%; background-position: center 66%;
  animation-duration: 40s, 9.5s; animation-delay: -37s, -5s;
}
/* Reduced motion: a finished, BALANCED still, never a frozen half-state. A is
   parked at the right-hand end — exactly where round 5 had it fixed and where
   round 6's reduced state already parked it — C is mirrored at the left and
   dropped to .55 so the two do not read as equal weights, and B is removed
   because at rest it would sit dead behind the LAUNCH20 pill. */
@media (prefers-reduced-motion: reduce) {
  .promo-fig { animation: none; transform: none; }
  .promo-fig-a { translate: calc(100vw - 100%) 0; }
  .promo-fig-c { translate: 0 0; opacity: .55; }
  .promo-fig-b { display: none; }
}
/* Same width gate as round 5/6's single figure: below 52rem the bar is copy
   only. The `::before` glow is NOT gated — it is depth, not a figure. */
@media (max-width: 52rem) { .promo-fig { display: none; } }

/* ROUND 6 (owner, 2026-08-15): "20% off your order", the LAUNCH20 pill, "Ends
   in" and the countdown must sit on ONE LINE, collinear. They did not, and the
   cause was three separate things rather than a missing `align-items`:
     · every child inherited the page's 1.6 body line-height, so each item's box
       was taller than its ink by a DIFFERENT amount (0.66rem eyebrow vs 1.16rem
       display italic) — `align-items: center` then centred five boxes of five
       heights, which puts five baselines in five places;
     · `.promo-timer` aligned its own contents on `baseline` while `.promo-code`
       aligned on `center`, so the two pills disagreed internally as well;
     · the two pills carried different vertical padding (.34rem vs .28rem) and
       so measured different heights, which reads as one sitting low.
   The fix is a row whose height is declared once and children whose boxes hug
   their ink: `line-height: 1` everywhere, both pills on one explicit height,
   every child an inline-flex box centred on the same axis. `min-height` on the
   row means the countdown appearing (it is `hidden` until /api/promo returns a
   real expiry) cannot change the bar's height — verified with the timer forced
   visible at 1280.
   Mobile still WRAPS on purpose: the timer is display:none below 46rem and the
   CTA below 40rem, and whatever is left wraps to a second row. The rules above
   are per-row, so each row is aligned on its own. */
.promo-bar-in {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: .45rem clamp(.65rem, 1.7vw, 1.15rem);
  padding-block: .72rem; position: relative; z-index: 1;
  line-height: 1; min-height: 3.34rem;
}
.promo-bar-in > * { display: inline-flex; align-items: center; line-height: 1; }
.promo-bar-eyebrow {
  font-family: var(--sf-font-display); font-size: .66rem; font-weight: 600;
  letter-spacing: .17em; text-transform: uppercase;
  color: color-mix(in srgb, #fff 62%, var(--sf-accent-1));
}
/* Hairline rules either side of the offer, the way a sale bar sets its headline
   apart from its label without shouting. */
.promo-bar-msg::before,
.promo-bar-msg::after {
  content: ""; display: inline-block; vertical-align: middle;
  width: 1px; height: 1.05em; margin-inline: clamp(.6rem, 1.6vw, 1.05rem);
  background: color-mix(in srgb, #fff 26%, transparent);
}
.promo-bar-msg { font-size: .95rem; letter-spacing: .01em; display: inline-flex; align-items: center; }
/* "20% off" is italic display serif at 18.6px; "your order" is upright sans at
   15.2px. At `margin-right: .3rem` the measured ink gap was 4.8px — tighter
   than the word spacing inside "your order" itself, so the two read as one
   collided word (owner, 2026-08-15: "the spacing between the twenty percent off
   and the your order … looks messed up"). The italic also leans its last glyph
   INTO that gap, which costs another pixel or so of apparent space. */
.promo-bar-msg strong {
  font-family: var(--sf-font-display); font-weight: 700; font-style: italic;
  color: #fff; font-size: 1.16rem; letter-spacing: -.005em; margin-right: .62rem;
}
/* Both pills carry ONE height (round 6), so neither can sit low against the
   other or against the text either side of them. Padding is inline-only now —
   the height is the height. */
.promo-code {
  display: inline-flex; align-items: center; gap: .5rem;
  height: 1.9rem; padding: 0 .8rem 0 .68rem; border-radius: 999px; cursor: pointer;
  background: color-mix(in srgb, #fff 14%, transparent);
  border: 1px solid color-mix(in srgb, #fff 34%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 22%, transparent);
  color: #fff; font: inherit;
  transition: background .18s ease, border-color .18s ease, transform .18s ease;
}
.promo-code:hover { background: color-mix(in srgb, #fff 22%, transparent); border-color: #fff; transform: translateY(-1px); }
.promo-code-label { font-size: .6rem; letter-spacing: .15em; text-transform: uppercase; opacity: .68; }
.promo-code-value { font-family: var(--sf-font-display); font-weight: 700; letter-spacing: .09em; font-size: .86rem; }
.promo-code[data-copied="1"] { background: color-mix(in srgb, var(--ca-teal) 42%, transparent); border-style: solid; border-color: var(--ca-teal); }
/* The countdown pill. Deliberately quieter than the code pill — the code is the
   thing to act on; the deadline is context. Tabular figures so the seconds
   digit does not jog the whole bar sideways once a second. */
.promo-timer {
  display: inline-flex; align-items: center; gap: .45rem;
  height: 1.9rem; padding: 0 .7rem; border-radius: 999px;
  background: color-mix(in srgb, #000 18%, transparent);
  border: 1px solid color-mix(in srgb, #fff 16%, transparent);
  color: #fff;
}
.promo-timer[hidden] { display: none; }
.promo-timer-label { font-size: .6rem; letter-spacing: .15em; text-transform: uppercase; opacity: .66; }
.promo-timer-value {
  font-family: var(--sf-font-display); font-weight: 600; font-size: .82rem;
  letter-spacing: .02em; font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1; min-width: 10.5ch; text-align: right;
}
@media (max-width: 46rem) { .promo-timer { display: none; } }

.promo-bar-cta { color: #fff; font-size: .84rem; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, #fff 45%, transparent); padding-bottom: 1px; }
.promo-bar-cta:hover { border-bottom-color: #fff; }
@media (max-width: 40rem) { .promo-bar-cta { display: none; } }

/* --- hero ---------------------------------------------------------------- */
/* Tall enough that the powder fill has somewhere to go and the vial can stand
   at full height. The section is a flex column so the meaning line can sit on
   the floor of it rather than trailing the left copy block. */
.hero {
  padding-block: clamp(1.5rem, 4vw, 3rem) clamp(1.25rem, 3vw, 2rem);
  min-height: min(94vh, 61rem);
  display: flex; flex-direction: column; justify-content: center;
}
/* `.hero-grid` is SHARED — the home hero, `my-account` (statement | account
   record) and `track` (statement + lookup form | lifecycle rail) all use it as
   a two-column masthead. It keeps that job unchanged. The column gap is a named
   custom property because `.hero-mark`'s top margin is derived from the same
   rhythm; two hand-matched clamps would drift the first time one is touched. */
.hero-grid { --hero-gap: clamp(1.5rem, 4vw, 3.5rem); display: grid; gap: var(--hero-gap); align-items: center; flex: 1 1 auto; }
@media (min-width: 60rem) {
  .hero-grid { grid-template-columns: 1.04fr 0.96fr; }
  /* `flex: 0 0 auto` is not cosmetic. `.hero-grid` is a flex child of the hero
     with `flex: 1 1 auto`, so it was being stretched to the hero's whole
     min-height; content-height keeps the copy vertically centred by the hero's
     own justify-content. */
  .hero-grid { align-items: stretch; flex: 0 0 auto; }
}
/* ROUND 6 (owner, 2026-08-15) made the HOME hero ONE CENTRED COLUMN with the
   vial stacked beneath the copy — class `.hero-centered`, replaced here.
   ROUND 7 — owner, 2026-08-16: "when I said I wanted the vial centered I meant
   IN LINE with the section to the left, not centered under it." So the hero is
   SIDE-BY-SIDE again: copy column left (left-aligned, as round 5), the vial
   right and centred within its own half, VERTICALLY CENTRED on the copy block,
   at round 6's size.
   `align-items: center` is the entire mechanism for "vertically centred on the
   copy": both items are at most as tall as the row, so each is centred in it
   and their centre-lines ARE the row's centre-line. Nothing hand-tuned, so
   nothing to drift when the copy rewraps at a new width.
   The vial stays IN FLOW — round 6's other change, kept deliberately. QA
   samples hero text contrast from real pixels, and an absolute vial layer
   behind hero copy is exactly the sample that once measured 1.73:1. Two grid
   cells cannot overlap.
   Still a MODIFIER, never a change to `.hero-grid` itself: `my-account.html`
   and `track.html` use that class as a real two-column masthead, and the first
   cut of round 6 edited the shared rule and silently collapsed both (QA stayed
   PASS through it). The two-class selector (0-2-0) also outranks the
   `min-width: 60rem` rule above (0-1-0), so it wins at every width without
   needing to be nested in one. */
.hero-grid.hero-split { align-items: center; flex: 0 0 auto; }
.hero-copy { max-width: 46rem; }
.hero h1 { margin-bottom: 1rem; }
/* The client sets his second headline line in the accent and breaks it onto its
   own line (client-design-reference/styles.css:148, .hero h1 .accent). The
   headline spans here are already block-level from the .tl cascade, so this
   carries the colour only. Both candidates clear AA on this ground — his literal
   --blue-600 #3a5fa0 at 5.79:1, the solved --sf-accent #3E6798 at 5.34:1 — so
   this is not a contrast rescue. The TOKEN wins for a different reason: it is
   re-solved whenever the palette moves, and a pasted hex is not. That is the
   same failure the --ca-teal and --sf-accent-5 notes above record. */
.hero h1 .hero-accent { color: var(--sf-accent); }
.hero-cta { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 1.6rem; }
/* Below 60rem the grid has no second track to sit in, so the composition
   stacks: copy above, vial below, both centred (owner, 2026-08-16, signed that
   off for phones). Side-by-side is the contract from 1024px up and this
   breakpoint is 960px, so it clears it with room. */
@media (max-width: 59.99rem) {
  .hero-grid.hero-split { grid-template-columns: minmax(0, 1fr); justify-items: center; }
  .hero-split .hero-copy { text-align: center; }
  .hero-split .lede { margin-inline: auto; }
  .hero-split .hero-cta { justify-content: center; }
  .hero-split .hero-mark { margin-top: clamp(1rem, 3vw, 2.25rem); }
}

/* The powder stage. `isolation: isolate` keeps the canvas, the floaters and the
   content in one predictable order without any of them needing to out-index
   the others. */
/* `overflow: clip`, with `hidden` as the fallback for engines that predate it.
   `hidden` makes the section a SCROLL CONTAINER: anything that overflows it
   below (the vial layer's derivative runs 4% past the floor by design) becomes
   scrollable overflow, and `scrollIntoView` / focus / an anchor jump on the CTA
   then scrolls the HERO internally — the copy jumped up 47px in the QA
   contrast probe and the sample landed on powder instead of the button
   (1.73:1). `clip` clips identically and can never be scrolled. */
.powder-stage { position: relative; overflow: hidden; overflow: clip; isolation: isolate; }
.powder-stage > .powder { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.powder-stage > .wrap { position: relative; z-index: 2; }

/* No `.powder-media` rule any more: the ground is a STILL (owner, 2026-08-15
   night — "a still of the endpoint of the animation"), painted by catharos.js
   as the host's background-image from the asset map's poster. The video-element
   rules that used to live here (cover, centre-bottom, the settle fade) are at
   git 4625221 with the play-once script they served. */

/* Scrim. The preset asks the model to keep the upper field clean and the lane
   MEASURES overlay contrast for it (0.1377 on this clip against a 0.22 soft
   threshold) — but "measured once on this clip" is not "guaranteed for this
   headline at this size on this viewport". A pale wash over the top and the
   left costs nothing and makes the headline's contrast independent of whatever
   a regenerated clip happens to come back with. */
.powder-stage > .powder::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(180deg,
      var(--sf-bg) 0%,
      color-mix(in srgb, var(--sf-bg) 70%, transparent) 30%,
      transparent 58%),
    linear-gradient(90deg,
      color-mix(in srgb, var(--sf-bg) 62%, transparent) 0%,
      color-mix(in srgb, var(--sf-bg) 22%, transparent) 38%,
      transparent 62%);
}

/* --- the flagship vial, standing the full height of the hero ------------- */
/* HISTORY, kept short because it explains the shape. The vial arrived here on
   2026-08-15 by swapping places with the C mark, and inherited the mark's cell:
   a grid cell whose height was BOUND to the copy column (a "height: 1px" flex
   trick so the row would be sized by the copy and never by the artwork). That
   was the right machine for a badge-sized mark and the wrong one for a
   product: the cell was ~330px wide, `max-width: 100%` capped the square
   derivative at that width, and the vial stood ~370px tall in a 680–980px hero.
   Two rounds of "make it bigger" then bolted a `transform: scale()` from the
   footing onto it (1.14, then 1.3), which grew the glass into the section's
   `overflow: hidden` at some viewports — cap sliced off — while a footing mask
   dissolved the last 12% of the image, i.e. the whole base. Owner, 2026-08-15
   night: "ghk cu vial is butchered and needs to be 2x size".

   ROUND 5 (superseded below). `.hero-mark` became an absolute LAYER of the
   section pinned to `left: 50%`, sized against the HERO's height (figure 100%,
   img 96%), so the vial stood the full height of the hero on the right half.

   ROUND 6 — owner, 2026-08-15: "−⅓ size, centered composition". Two changes,
   and the second is what makes the first safe:
     · SIZE. The vial renders at ~66% of the height it had. Measured on the
       previous build at 1280x800: img 722px tall (0.96 x a 752px hero); it is
       472px now. The scale factor holds at 390 / 768 / 1280 / 1920 — see the
       two `height` declarations below, which are the only dials.
     · SHAPE. It is IN FLOW again, as the second flex child of the hero column,
       centred on the same axis as the copy. NOT an absolute layer any more:
       centring a layer horizontally would have put glass directly behind a
       centred headline, and the QA gauntlet samples hero text contrast from
       real pixels — a translucent vial under the h1 is exactly the sample that
       measured 1.73:1 the last time something drifted under hero copy. In flow
       the two can never overlap, at any viewport, without a media query.
   DOM order is unchanged (copy first, mark second), so the visual order and the
   reading order agree and no `order` hack is needed. The hero grows past its
   min-height to seat the stack, which is what min-height is for.

   ROUND 7 — owner, 2026-08-16: "when I said I wanted the vial centered I meant
   IN LINE with the section to the left, not centered under it." Round 6 read
   "centred" as a centred STACK; it means centred ON THE COPY. So:
     · PLACE. `.hero-mark` moves back INSIDE `.wrap` as the grid's second cell
       (it was a sibling of the wrap while the composition was stacked), which
       is what makes "in line with the section to the left" a fact of the
       layout rather than a matched pair of margins. `align-items: center` on
       `.hero-grid.hero-split` centres it against the copy block.
     · SIZE. Unchanged from round 6 — the two `height` clamps below are the
       only dials and neither is touched. 472px at 1280x800, 608px at
       1920x1080, re-measured after the move.
     · FLOW. Unchanged from round 6. Still an in-flow grid cell, never an
       absolute layer, for the contrast-probe reason above. */
.hero-mark {
  position: relative; z-index: 1; pointer-events: none;
  display: flex; justify-content: center; align-items: center;
  flex: 0 0 auto; padding: 0; margin-top: 0;
  /* ROUND 7. As a grid item its automatic minimum size would be its min-content
     WIDTH, and the vial's derivative is a square: at 608px tall it is 608px
     wide, wider than half of a 76rem wrap (514px at 1920). Left at `auto` that
     minimum widens the vial track until the grid overflows the wrap and the
     page scrolls sideways. `min-width: 0` pins the track to its 0.96fr share
     and lets the square frame overhang it symmetrically instead — which is
     free, because the overhang is transparent: measured on the shipped
     cl-3-rt-10mg-1200.webp, the SOLID vial is x=345..854, i.e. 42.5% of the
     frame's width, centred, so a 608px box carries 258px of glass with 175px
     of empty frame each side. The ~47px per-side overhang at 1920 is a quarter
     of that margin, and the copy column's edge is 56px of grid gap further
     away again. */
  min-width: 0;
}
.hero-copy { position: relative; z-index: 1; }
/* The figure carries a DEFINITE height and the image derives its width from it,
   so the vial is sized by the viewport rather than by whatever the copy happens
   to measure — the lesson of the "height: 1px" cell this replaced. Below 60rem
   the constraint is width-side (30vw), above it height-side (59vh), because
   that is which axis is scarce at each end. */
.hero-mark > figure {
  margin: 0;
  display: flex; align-items: center; justify-content: center;
  width: auto; height: clamp(7rem, 30vw, 12rem);
  /* Round 7, and it is the pair to `min-width: 0` above: as a flex item the
     figure's default `flex-shrink: 1` would let it be squeezed to the narrower
     track while `height` stayed fixed, and an `img` with `height: 100%` and a
     capped width is a SQUASHED vial, not a smaller one. Refusing to shrink is
     what keeps the overhang the transparent-frame kind. */
  flex: 0 0 auto;
}
.hero-mark img {
  display: block; width: auto; height: 100%; max-width: 100%;
  filter: drop-shadow(0 30px 44px rgb(var(--sf-shadow) / .22));
  /* "a little more faded" (owner, 2026-08-15 evening) — the one dial that
     survives from the old rule. A touch of transparency lets the powder ground
     breathe through the glass instead of the cut-out reading as a sticker. */
  opacity: .9;
}

/* --- ROUND 8: the hero carries a GROUP, and the group floats -------------- */
/* Owner, 2026-08-16: "I want CL-3 RT, GHK-Cu and NAD+, all three of those up in
   front of the powder hero … floating around kind of like the floating animated
   vials around the bottom of the page … all three of them kind of bundled up or
   next to each other in that animated hero section."
   So the flagship stops being ONE still specimen and becomes three, adrift on
   the same `ca-hover` the mid-page floaters use — the idiom he named, not a new
   one invented for the hero.

   WHAT IS DELIBERATELY UNCHANGED, because each of these is a decision already
   paid for in owner rounds (see the history above this block):
     · The figure stays IN FLOW as the grid's second cell (round 7). It is NOT
       promoted to an absolute layer over the section. That is the contrast-probe
       rule: glass drifting under hero copy is what measured 1.73:1 once, and
       three drifting vials would be three chances to do it again. In flow the
       cluster and the headline cannot overlap at any viewport, with no media
       query holding them apart.
     · The height clamps (round 6, re-measured in round 7) are untouched — 472px
       at 1280x800, 608px at 1920x1080. They size the FRAME; the vials take
       their share of it below.
     · `min-width: 0` on `.hero-mark` and the 44vw guard still bind, so the
       square frame still overhangs its track by ~47px a side at 1920 and no
       more than it did with one vial.

   THE FRAME STAYS SQUARE, and that is the load-bearing choice. The obvious way
   to seat three vials is a wider box — but the overhang arithmetic in the round-7
   comment above is written against a 1:1 frame, and widening it to 1.18:1 would
   push ~101px a side past the track at 1920 while ALSO filling the frame edge to
   edge with glass. The old overhang was free precisely because it was empty
   frame. So the box is unchanged and the vials shrink into it instead: measured
   on the shipped derivatives the solid glass is 42.5% of a frame's width, which
   at these three heights puts the group's ink between 11.6% and 88.8% of the
   frame — inside the visible track at every viewport the size was set from.

   Depth: the centre vial is largest and in front, the flankers smaller, lower
   in opacity and behind, overlapping it. That is what makes it read as one
   bundle rather than as three vials in a row. */
.hero-cluster { position: relative; aspect-ratio: 1 / 1; display: block; }
.hero-cluster img {
  position: absolute;
  height: var(--v-h); width: auto;
  left: var(--v-x); bottom: var(--v-y);
  z-index: var(--v-z, 1);
  opacity: var(--v-op, .9);
  /* Each vial keeps its own drop shadow: one shadow on the group would light
     three vials from one plane and flatten the depth the sizes just built. */
  filter: drop-shadow(0 22px 34px rgb(var(--sf-shadow) / .2));
  max-width: none;   /* the flankers overhang the frame by design; the inherited
                        `max-width: 100%` would squash them instead */
}
/* The stacked copies that make the front vial opaque (index.html `layers`).
   Identical to their sibling in every way that positions or animates them, and
   carrying NO shadow: `drop-shadow` is per-element, so three copies painted
   three shadows and the vial grew a dark aura around its cap and shoulder. The
   bottom copy keeps the shadow; these two only supply alpha. */
.hero-cluster img.v-back { filter: none; }
/* Positions. `--v-x` is the image's LEFT EDGE as a percentage of the square
   frame, derived from where its visible glass should sit, not eyeballed: glass
   centre minus half the square. Centres are 25.5% of the frame apart, which is
   the spacing that overlaps the cakes without hiding a label.
   MEASURED, not eyeballed: at 23.5% spacing the centre vial's shoulder covered
   the last letter of GHK-Cu's printed name. 25.5% clears every compound name on
   all three labels and still overlaps the cakes. It also stays inside the track:
   the group's ink then runs 9.9%–90.6% of the frame, which at the widest case
   (1920, a 608px frame overhanging a 514px track by 47px a side) puts the
   outermost ink 13px INSIDE the track — it never reaches the 56px grid gap, let
   alone the copy column beyond it.

   ROUND 8b — owner, 2026-08-16, holding up a reference site: "what is going on
   with the sizing and orientation. i was thinking something more like this."
   Two separate things were wrong and only one of them was a design choice:
     · SIZING was a BUG, not a composition — a malformed comment above this
       block closed early, the CSS parser's error recovery swallowed the
       `.v-mid` rule whole, and the centre vial fell back to `.hero-mark img`'s
       `height: 100%` on an absolutely-positioned element with no height to
       resolve against. It rendered at its natural 1200px inside a ~455px frame,
       four times its neighbours. Fixed by closing the comment. The scan that
       catches this class is in the verification notes below.
     · ORIENTATION was the real ask. Three vials standing upright in a row read
       as a shelf. The reference floats them TILTED and near-equal in size, so
       they read as suspended rather than stood up. Hence the base rotations and
       the much closer size spread (76/86/80 where it was 70/80/72).
   The tilt lives on `rotate:`, NOT inside the keyframe, and that is deliberate:
   `ca-hover` drives `transform`, and the two individual transform properties
   COMPOSE. Baking the tilt into `--f-rot-a/b` instead would have made the
   vials snap upright for a reduced-motion visitor, who gets no animation at
   all — the composition would only exist for people who can see it move.

   ROUND 8c — owner, 2026-08-16: "can we move ghk to match the positioning style
   of nad except mirrored on the other side? so cl-3 rt in middle and ghk on left
   and nad on right."
   So the two flankers are now a TRUE MIRRORED PAIR about the frame's centre
   line, and the only thing that differs between them is the sign:
     · same size (80%), where GHK was the runt at 76%;
     · same standing height (16%), where GHK sat on the floor at 0% and read as
       a third vial that had been put down rather than one of a matched pair;
     · same opacity and the same depth plane (z 2), both behind CL-3 RT;
     · equal and opposite tilt (±15deg) and mirrored x.
   The x values are the mirror arithmetic, not a guess: `--v-x` is a left EDGE,
   so the glass centre is `--v-x + --v-h/2`. NAD+ sits at 39 + 40 = 79% of the
   frame; reflecting that about 50% gives 21%, so GHK's left edge is 21 − 40 =
   −19%. Reflect either one and you get the other exactly.
   CL-3 RT stays larger (86%) and LOWER (8%) than both — it is the front of the
   group, and a front object that sits higher than the two behind it reads as
   floating away from them instead of in front of them. */
.hero-cluster .v-mid  { --v-h: 86%; --v-x: 7%;   --v-y: 8%;  --v-z: 3; --v-op: .94; rotate: -3deg; }
.hero-cluster .v-left { --v-h: 80%; --v-x: -19%; --v-y: 16%; --v-z: 2; --v-op: .90; rotate: -15deg; }
.hero-cluster .v-right{ --v-h: 80%; --v-x: 39%;  --v-y: 16%; --v-z: 2; --v-op: .90; rotate: 15deg; }
/* The drift itself — `ca-hover`, the mid-page floaters' own keyframe, on three
   different clocks so the bundle breathes instead of pulsing as one block. The
   rise is dialled down via `--f-rise` (see the keyframe): far enough to read as
   floating, not so far that the group comes apart. Durations are coprime-ish so
   the three do not re-sync on any short cycle. */
@media (prefers-reduced-motion: no-preference) {
  .hero-cluster img { animation: ca-hover var(--f-dur, 30s) ease-in-out var(--f-delay, 0s) infinite; }
  /* The rotations here are now a small WOBBLE ON TOP of the base tilt above,
     not the tilt itself — ±2deg, because the composition is already carried by
     `rotate:` and a second large rotation would swing each vial through the
     others. */
  /* DELAYS ARE NEGATIVE, and `backwards` is not decoration. Owner, 2026-08-16:
     "upon initial load the three vials dont move - then jitter a bit - and then
     they start cleanly floating."
     Both halves of that were one mechanism, and it was MEASURED in headless
     Chrome by sampling each vial's box every 50ms for five seconds:

       v-mid   first movement  647ms   0 discontinuities   (delay was 0s)
       v-left  first movement 1517ms   ONE 7.64px jump     (delay was 1.5s)
       v-right first movement 3034ms   ONE 7.64px jump     (delay was 3s)

     During a POSITIVE `animation-delay` an element renders with its ordinary,
     un-animated style — and `ca-hover`'s 0% keyframe is not that style: it
     carries `rotate(var(--f-rot-a))`. So each flanker sat dead at 0deg for its
     whole delay and then SNAPPED ±2deg the instant the animation became active.
     That snap is the jitter, and the delay is the dead time in front of it.
     Two fixes, and either alone is insufficient:
       · `backwards` makes the 0% pose apply DURING the delay, so there is no
         style to jump from — this is what kills the snap;
       · NEGATIVE delays start each vial already partway through its cycle, so
         all three are in motion on the first painted frame and still out of
         phase with each other — which is what the staggered delays were for in
         the first place. Phases: mid 24%, right 39%, left 51% of their cycles.
     Re-measured after the change: 0 discontinuities on all three, first
     movement inside the first sampled frame. */
  .hero-cluster img     { animation-fill-mode: backwards; }
  .hero-cluster .v-mid  { --f-dur: 29s; --f-delay: -7s;  --f-sway: .45rem; --f-rise: -1.5rem; --f-rot-a: -1.5deg; --f-rot-b: 1.5deg; }
  .hero-cluster .v-left { --f-dur: 37s; --f-delay: -19s; --f-sway: -.6rem; --f-rise: -1.9rem; --f-rot-a: 2deg;    --f-rot-b: -2deg; }
  .hero-cluster .v-right{ --f-dur: 33s; --f-delay: -13s; --f-sway: .7rem;  --f-rise: -1.2rem; --f-rot-a: -2deg;   --f-rot-b: 2deg; }
}
/* Under reduced motion the cluster is SHOWN and held still. It is not a `.floaters`
   layer — those are ambient decoration and are removed outright — these three are
   the hero's subject, and a hero with no product in it is a broken page, not a
   calm one. Same call the gate's vials already make. */

/* DESKTOP (round 6). The one dial: the figure's height.
   Where the number comes from. The hero is `min-height: min(94vh, 61rem)` and
   the old layer rendered the image at 96% of it — 0.96 x 94vh below 1038px of
   viewport height, 0.96 x 61rem above it. Two thirds of that is 0.62vh-ish and
   0.66 x 58.6rem, which is what `min(59vh, 38rem)` reproduces to within 2% at
   every height the hero can take. Written as literal clamp terms rather than as
   `calc()` off the hero so the vial keeps its size when the hero grows past its
   min-height to seat the stack (which it now does — a percentage would have
   grown the vial with it and eaten the reduction the owner asked for).
   Verified 2026-08-15: 472px at 1280x800 (was 722), 608px at 1920x1080
   (was 937) — 0.654 and 0.649 of round 5.
   ROUND 7 (owner, 2026-08-16) changed the vial's PLACE, not its size: this
   clamp is untouched and re-measured at 472 / 608 after the move. It is
   viewport-derived on purpose — a percentage of the cell would have re-coupled
   the vial to whatever the copy column happens to measure, which is the exact
   failure ("~370px tall in a 980px hero") the history above opens with. */
/* The `44vw` term is ROUND 7's, and it is a GUARD, not a size. The vial's
   derivative is square, so a 608px-tall box is 608px wide — wider than the
   0.96fr track (514px at 1920), and the frame overhangs it by ~47px a side.
   That is free while the overhang is smaller than the 56px grid gap. At a
   TALL, NARROW desktop it is not: 1024x1366 keeps the full 608 (59vh = 806,
   clipped by the 38rem cap) against a 432px track and a 41px gap, so the
   frame's edge crossed the copy column by 47px — measured, not feared. 44vw
   is just under half the wrap's inner width, so it binds only in that band
   (960-1300px) and never at the two viewports the size was set from:
   1280x800 takes 59vh = 472 (44vw = 563), 1920x1080 takes the 38rem cap = 608
   (44vw = 845). Both re-measured unchanged after adding it. */
@media (min-width: 60rem) {
  .hero-mark > figure { height: min(clamp(12rem, 59vh, 38rem), 44vw); }
}

/* The entrance is GONE (2026-08-15). It armed the group hidden — opacity 0,
   scale .955, blur 9px — from SCRIPT, then faded it in on the powder clip's
   timeline. Two problems, and the first one is fatal on its own:
     · the stylesheet painted the group VISIBLE, the script then armed it
       hidden, and it faded back in. Every reload therefore showed it, hid it,
       and showed it again (owner: "pop up, disapear then re pop up"). A hidden
       state armed after first paint can only ever look like that;
     · the powder is a static settled frame now, so there is no timeline left to
       cue from — only the wall-clock fallback could fire.
   The group is simply present from first paint, which is exactly what every
   failure path of the old code already fell back to. Nothing references
   data-mark-armed or data-mark-in any more; do not reintroduce them without
   arming the hidden state in CSS from the start, before paint. */

/* The meaning line, on the floor of the hero and spanning it. */
.hero-foot { padding-top: clamp(1.25rem, 3vw, 2.25rem); }

/* Specimen panel — a labelled figure, the way a datasheet presents a sample. */
.specimen {
  border: 1px solid var(--ca-line-strong);
  border-radius: var(--sf-radius);
  background: var(--sf-elevated);
  overflow: hidden;
}
.specimen-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .8rem 1.1rem; border-bottom: 1px solid var(--ca-line);
  font-family: var(--sf-font-display); font-size: .7rem; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--sf-muted);
}
.specimen-body { padding: clamp(1.1rem, 3vw, 2rem); }
.specimen figure { margin: 0; }

/* --- product grid -------------------------------------------------------- */
.grid-products { display: grid; gap: 1px; background: var(--ca-line); border: 1px solid var(--ca-line); }
@media (min-width: 34rem) { .grid-products { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 58rem) { .grid-products { grid-template-columns: repeat(3, 1fr); } }
/* The QA harness requires each tile to be an <article data-variants>, so the
   article is the grid cell and the card stretches to fill it. The 1px-gap
   hairline grid works by letting the container colour show through, so the
   article stays transparent and only .pcard paints. */
.pcell { display: flex; min-width: 0; }
.pcell > .pcard { flex: 1 1 auto; min-width: 0; }
.pcard {
  background: var(--sf-bg); padding: clamp(1rem, 2.5vw, 1.5rem);
  display: flex; flex-direction: column; gap: .55rem;
  text-decoration: none; color: inherit; position: relative;
  transition: background-color .16s ease;
}
.pcard:hover { background: var(--sf-surface); }
.pcard-fig {
  aspect-ratio: 4 / 3; display: grid; place-items: center;
  background: var(--ca-photo-bg); border-radius: calc(var(--sf-radius) * .6);
  margin-bottom: .35rem; overflow: hidden;
}
.pcard-fig img { max-height: 100%; width: auto; object-fit: contain; }
.pcard-cat { font-size: .68rem; letter-spacing: .13em; text-transform: uppercase; color: var(--sf-muted); font-family: var(--sf-font-display); font-weight: 600; }
.pcard-name { font-family: var(--sf-font-display); font-weight: 600; font-size: 1.06rem; color: var(--ca-ink); line-height: 1.2; }
.pcard-foot { display: flex; justify-content: space-between; align-items: baseline; gap: .6rem; margin-top: auto; padding-top: .6rem; }
.price { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); }
.pcard-size { font-size: .82rem; color: var(--sf-muted); }

/* --- spec table: the datasheet primitive -------------------------------- */
.spec { width: 100%; border-collapse: collapse; }
.spec caption {
  text-align: left; font-family: var(--sf-font-display); font-size: .7rem;
  font-weight: 600; letter-spacing: .13em; text-transform: uppercase;
  color: var(--sf-muted); padding-bottom: .7rem;
}
.spec th, .spec td { text-align: left; padding: .72rem .9rem; border-bottom: 1px solid var(--ca-line); font-size: .93rem; vertical-align: top; }
.spec th { font-weight: 500; color: var(--sf-muted); width: 42%; }
/* overflow-wrap:anywhere, NOT break-word: only "anywhere" is taken into account
   when the browser computes min-content, which is the number that was making the
   table too wide to fit. An address, a lot code or a tracking number is exactly
   the kind of value this table carries, and none of them have break points. */
.spec td { color: var(--sf-text); overflow-wrap: anywhere; }
.spec tr:last-child th, .spec tr:last-child td { border-bottom: 0; }

/* --- candor block: the trust surface, in place of a certificate ---------- */
.candor {
  border-left: 3px solid var(--sf-accent);
  background: var(--ca-wash);
  padding: clamp(1.1rem, 3vw, 1.75rem) clamp(1.1rem, 3vw, 2rem);
  border-radius: 0 var(--sf-radius) var(--sf-radius) 0;
}
.candor h2, .candor h3 { margin-top: 0; }
.candor ul { margin: 0; padding-left: 1.15rem; }
.candor li { margin-bottom: .5rem; max-width: 62ch; }
.candor li::marker { color: var(--sf-accent); }

/* --- refusal plaque ----------------------------------------------------- */
.plaque { border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius); padding: clamp(1.1rem, 3vw, 1.75rem); }
.plaque ul { margin: 0 0 1rem; padding-left: 1.15rem; }
.plaque li { margin-bottom: .45rem; }

/* --- steps / panels ---------------------------------------------------- */
/* minmax(0, 1fr), not the implicit 1fr. A grid item's automatic minimum size
   is its MIN-CONTENT, so an item containing something that cannot wrap makes the
   whole track wider than the viewport instead of scrolling inside itself. On
   contact.html that item is the .spec table below, whose email cell is one
   unbreakable 24-character token; the wider body face tipped it 17px past 375.
   Stated for the single-column case too, which is where it actually bit. */
.panels { display: grid; gap: 1.15rem; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 48rem) { .panels-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
@media (min-width: 56rem) { .panels-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.panel { border: 1px solid var(--ca-line); border-radius: var(--sf-radius); padding: clamp(1rem, 2.5vw, 1.5rem); background: var(--sf-surface); }
.panel h3 { margin-bottom: .4rem; }
.panel p:last-child { margin-bottom: 0; }
/* ONE panel/step heading scale, and one sub-heading under it. Ten pages were
   setting this inline at 1.02 / 1.15 / 1.18 / 1.2 / 1.25 / 1.3rem for the same
   role, which is what made the account, checkout and order pages read as a
   different site from the brand pages (2026-08-15, WS-B). The scale is the
   site's own third step — these are h2/h3 for the document outline, sized like
   the panel headings they are. */
.panel-h { font-size: clamp(1.12rem, 1rem + 0.55vw, 1.35rem); margin: 0 0 .45rem; }
.panel-sub { font-size: 1.02rem; margin: 0 0 .4rem; }
/* One page-head block: the eyebrow / title / lede stack that opens a page.
   Four pages spelled its spacing inline (`style="padding-bottom:2rem"`). */
.page-head { padding-block: var(--ca-seam) 2rem; }

.panel-n {
  font-family: var(--sf-font-display); font-size: .7rem; font-weight: 700;
  letter-spacing: .12em; color: var(--sf-accent); display: block; margin-bottom: .55rem;
}

/* --- forms -------------------------------------------------------------- */
.field { display: block; margin-bottom: 1.05rem; }
.field > span, .field-label {
  display: block; font-family: var(--sf-font-display); font-size: .78rem;
  font-weight: 600; letter-spacing: .04em; color: var(--ca-ink); margin-bottom: .38rem;
}
/* min-width:0 for the same reason the fieldset below carries min-inline-size:0.
   A text input has an INTRINSIC preferred width from its size attribute (20
   characters by default), and width:100% does not lower the automatic minimum
   size a flex or grid item gets from it. Switching the body face to the
   client's Inter — 5.6% wider than the Source Sans 3 it replaced — grew those
   20 characters enough to push contact.html's form 17px past a 375px viewport.
   The measurement, not the guess: form.panel#cform rendered L18 R392 W373 in a
   339px content box. */
input[type=text], input[type=email], input[type=password], input[type=tel],
input[type=search], input[type=number], select, textarea {
  width: 100%; min-width: 0; padding: .72rem .85rem;
  font-family: var(--sf-font-body); font-size: .97rem; color: var(--sf-text);
  background: var(--sf-elevated);
  border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius);
}
input:focus, select:focus, textarea:focus { border-color: var(--sf-accent); outline: 2px solid color-mix(in srgb, var(--sf-accent) 30%, transparent); outline-offset: 0; }
textarea { min-height: 8.5rem; resize: vertical; }
.hint { font-size: .82rem; color: var(--sf-muted); margin-top: .3rem; }
.err { font-size: .85rem; color: #a3121a; margin-top: .35rem; }
/* A fieldset carries the UA default min-inline-size: min-content, and min-content
   here is set by the widest white-space:nowrap descendant. .buy-box on the PDP
   wraps the upsell list, whose .upsell-name is nowrap by design (it ellipsises),
   so the LONGEST SIBLING PRODUCT TITLE became the buy-box floor: 441px against a
   375px viewport = the 84px horizontal overflow that failed 7 of 9 PDPs on
   2026-08-19 (QA overflow family, 7 FAIL). .upsell-body already carries
   min-width: 0, which fixes FLEX shrinking and does nothing for a fieldset s
   intrinsic floor. Reset it here rather than on .buy-box: every fieldset on the
   site has the same trap, and the next long SKU name would re-open it. */
fieldset { border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius); padding: 1.1rem 1.15rem; margin: 0 0 1.15rem; min-inline-size: 0; }
legend { font-family: var(--sf-font-display); font-weight: 600; font-size: .85rem; color: var(--ca-ink); padding-inline: .4rem; }
.check { display: flex; gap: .65rem; align-items: flex-start; margin-bottom: .8rem; font-size: .92rem; line-height: 1.5; }
.check input { margin-top: .28rem; flex: none; width: 1.05rem; height: 1.05rem; accent-color: var(--sf-accent); }
.check a { text-decoration: underline; color: var(--sf-accent); }

/* --- cart drawer -------------------------------------------------------- */
#cart-overlay {
  position: fixed; inset: 0; z-index: 90;
  background: rgb(var(--sf-shadow) / .42);
  opacity: 0; visibility: hidden; transition: opacity .2s ease, visibility .2s ease;
}
#cart-overlay.open { opacity: 1; visibility: visible; }
#cart-panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 91;
  width: min(26rem, 100%); background: var(--sf-bg);
  border-left: 1px solid var(--ca-line-strong);
  display: flex; flex-direction: column;
  transform: translateX(100%); transition: transform .24s cubic-bezier(.22,.61,.36,1);
}
#cart-overlay.open ~ #cart-panel, #cart-panel.open { transform: translateX(0); }
#cart-panel header { display: flex; justify-content: space-between; align-items: center; padding: 1.05rem var(--ca-gutter); border-bottom: 1px solid var(--ca-line); }
#cart-items { flex: 1; overflow-y: auto; padding: 1rem var(--ca-gutter); }
#cart-footer { border-top: 1px solid var(--ca-line); padding: 1.05rem var(--ca-gutter); }

/* --- footer ------------------------------------------------------------- */
/* THE FOOTER IS DARK BLUE (owner, 2026-08-15, round 6: "make all footers the
   dark blue"). The token is `--ca-ink`, which is `--sf-accent-5` / #22167A —
   the client's own wordmark navy and the SAME ground `.band-ink` already uses
   for the seven-checks band, so the page closes on a colour it has already
   established rather than on a second navy invented for the footer.
   Inverted text tokens are spelled out here, not inherited: theme.css only
   knows light mode, which is why `.band-ink` inverts by hand too. Measured
   against #22167A (relative luminance 0.0232):
     · body/link #EDEFFA ........................ 12.5:1  (needs 4.5)
     · muted + headings #B4B2D6 .................. 7.0:1  (needs 4.5)
     · hairline rules #736DAD .................... 3.1:1  (needs 3.0)
   Those two are OPAQUE literals, not `color-mix(… , transparent)`, and that is
   deliberate. #B4B2D6 is exactly what #EDEFFA at 72% composites to on this
   ground and #736DAD is #EDEFFA at 40% — identical pixels — but a translucent
   colour serialises as `color(srgb r g b / a)`, which the QA gauntlet's
   `parseCssColor` does not read (it handles hex and rgb()). A contrast target
   whose colour cannot be parsed is a check that quietly stops measuring, which
   is worse on a legal disclaimer than a colour that is one hex step off.
   `.foot a:hover` moves to plain white rather than to `--sf-accent`, which is
   a 4041AB that would have been a 2.3:1 link on this ground.
   One rule does all 27 pages because the markup is one included part — the
   auth and gate pages picked up `.foot` in the WS-B unification and inherit
   this with it. */
/* THE FOOTER IS THE SAME BAR AS THE NAV. It was already dark, but on
   var(--ca-ink) (= --sf-accent-5, which the solver emits as #1A406B today) with
   five BAKED hexes for its inks — #736DAD / #EDEFFA / #B4B2D6 / #fff — a
   lavender set left over from an earlier palette. A baked ink does not move when
   the palette moves, so it cannot be solved and no gate can measure it. Every
   one of them is now a chrome token, and the ground is the client's own
   --navy-950, so the nav and the footer are one surface rather than two. */
.foot {
  border-top: 1px solid var(--ca-chrome-rule);
  background: var(--ca-chrome-bg); color: var(--ca-chrome-ink);
  padding-block: var(--ca-seam) 2rem; margin-top: var(--ca-seam);
}
.foot-grid { display: grid; gap: 2rem; }
/* Four columns as of 2026-08-15: the legal pages moved out of the fine print
   and into a shelf of their own to the right of Account & support (owner).
   Two steps, not one — at 50rem four columns would be 8rem each and the link
   text would wrap mid-phrase, so the brand block sits over a 2x2 until there is
   width for a single row. */
@media (min-width: 50rem) { .foot-grid { grid-template-columns: 1.5fr 1fr 1fr; } }
@media (min-width: 68rem) { .foot-grid { grid-template-columns: 1.5fr 1fr 1fr 1fr; } }
.foot .foot-h { font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ca-chrome-ink-dim); margin-bottom: .8rem; }
.foot ul { list-style: none; margin: 0; padding: 0; }
.foot li { margin-bottom: .45rem; }
.foot .muted { color: var(--ca-chrome-ink-dim); }
.foot a { color: var(--ca-chrome-link); text-decoration: none; font-size: .9rem; }
.foot a:hover { color: var(--ca-chrome-ink); text-decoration: underline; }
.foot-fine { margin-top: 2.4rem; padding-top: 1.4rem; border-top: 1px solid var(--ca-chrome-rule); font-size: .78rem; color: var(--ca-chrome-ink-dim); }
.foot-fine p { max-width: 72ch; margin-bottom: .55rem; }
.foot-fine strong { color: var(--ca-chrome-ink); }
/* --sf-accent is 3.28:1 on the chrome ground; the eyebrow takes the chrome accent. */
.foot-eyebrow { font-family: var(--sf-font-display); font-size: .7rem; font-weight: 600; letter-spacing: .15em; text-transform: uppercase; color: var(--ca-chrome-accent); margin-bottom: .7rem; }

/* ==========================================================================
   TRANSACTIONAL SURFACES — checkout, gate, auth, account, order records.
   Same document logic as the content pages: a form on this site is a form on
   a printed sheet. Ruled rows, tabular numerals, no card shadows, no colour
   that is not already a token.
   ========================================================================== */

/* --- minimal nav: the auth/gate pages carry no catalog chrome --------- */
.snav-min .snav-in { min-height: 3.9rem; }
.snav-min .snav-links { display: none; }

/* --- centred form shell ------------------------------------------------- */
.sheet { padding-block: clamp(2.25rem, 6vw, 4.5rem) var(--ca-seam); }
.sheet-in { width: min(30rem, 100%); margin-inline: auto; }
.sheet-wide { width: min(46rem, 100%); }
.form-card {
  border: 1px solid var(--ca-line-strong);
  border-radius: var(--sf-radius);
  background: var(--sf-elevated);
  padding: clamp(1.35rem, 4vw, 2.15rem);
}
.form-card h1 { font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2rem); }
.form-card .btn { width: 100%; }
.form-card .btn + .btn { margin-top: .6rem; }
.form-foot { margin-top: 1.35rem; padding-top: 1.1rem; border-top: 1px solid var(--ca-line); font-size: .9rem; }
.form-foot p { margin-bottom: .4rem; }
.msg { font-size: .9rem; margin-top: .9rem; min-height: 1.2em; }
.msg-ok { color: var(--sf-accent); }
.msg-bad { color: #a3121a; }

/* --- tabs (gate: attest / sign in) --------------------------------------- */
.tabs { display: flex; border-bottom: 1px solid var(--ca-line-strong); margin: 0 0 1.4rem; }
.tab {
  flex: 1; background: none; border: 0; cursor: pointer;
  font-family: var(--sf-font-display); font-size: .74rem; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--sf-muted);
  padding: .85rem .4rem; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--ca-ink); }
.tab[aria-selected="true"] { color: var(--ca-ink); border-bottom-color: var(--sf-accent); }

/* --- checkout: two columns, the summary is a ruled ledger ---------------- */
.co { display: grid; gap: clamp(1.75rem, 4vw, 3rem); align-items: start; }
@media (min-width: 62rem) { .co { grid-template-columns: minmax(0, 1fr) 21.5rem; } }
.co-steps { counter-reset: step; display: flex; gap: 0; margin: 0 0 2rem; border: 1px solid var(--ca-line); border-radius: var(--sf-radius); overflow: hidden; }
.co-steps button {
  flex: 1; background: var(--sf-surface); border: 0; cursor: pointer; text-align: left;
  padding: .7rem .85rem; font-family: var(--sf-font-display); font-size: .74rem;
  font-weight: 600; letter-spacing: .06em; color: var(--sf-muted);
  border-right: 1px solid var(--ca-line);
}
.co-steps button:last-child { border-right: 0; }
.co-steps button[disabled] { cursor: not-allowed; opacity: .55; }
.co-steps button.done { color: var(--ca-ink); }
.co-steps button.cur { background: var(--ca-ink); color: #fff; }
.co-step[hidden] { display: none; }
.co-actions { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 1.5rem; }
.co-inline { display: grid; gap: 0 1rem; }
@media (min-width: 36rem) { .co-inline { grid-template-columns: 1fr 1fr; } }

/* radio rows — shipping methods and payment rails share one anatomy */
.optrow {
  display: flex; gap: .8rem; align-items: flex-start;
  border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius);
  padding: .85rem 1rem; margin-bottom: .6rem; cursor: pointer;
}
.optrow:hover { border-color: var(--sf-accent); background: var(--ca-wash); }
.optrow input { margin-top: .25rem; flex: none; accent-color: var(--sf-accent); width: 1.05rem; height: 1.05rem; }
.optrow-body { flex: 1; min-width: 0; }
.optrow-name { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); display: flex; flex-wrap: wrap; gap: .5rem; align-items: baseline; }
.optrow-note { font-size: .84rem; color: var(--sf-muted); display: block; margin-top: .15rem; }
.optrow-cost { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); white-space: nowrap; }
.tag {
  font-family: var(--sf-font-display); font-size: .66rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--sf-accent); color: var(--sf-accent);
  border-radius: 999px; padding: .1rem .5rem;
}

/* code entry: input + button on one line */
.inline-apply { display: flex; gap: .6rem; align-items: flex-end; }
.inline-apply .field { flex: 1; margin-bottom: 0; }
.inline-apply .btn { flex: none; }

/* the ledger */
.ledger {
  border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius);
  background: var(--sf-surface); padding: 1.15rem 1.2rem;
  position: sticky; top: 5.25rem;
}
.ledger-item { display: grid; grid-template-columns: 2.6rem 1fr auto; gap: .65rem; align-items: center; padding: .55rem 0; border-bottom: 1px solid var(--ca-line); }
.ledger-item img { width: 2.6rem; height: 2.6rem; object-fit: contain; background: var(--ca-wash); border-radius: calc(var(--sf-radius) * .5); }
.ledger-item-name { font-size: .86rem; line-height: 1.35; min-width: 0; }
.ledger-rows { margin-top: .9rem; }
.ledger-row { display: flex; justify-content: space-between; gap: 1rem; font-size: .9rem; padding: .3rem 0; }
.ledger-row[hidden] { display: none; }
.ledger-row .v { font-variant-numeric: tabular-nums; }
.ledger-total { border-top: 1px solid var(--ca-line-strong); margin-top: .5rem; padding-top: .65rem; font-family: var(--sf-font-display); font-weight: 600; font-size: 1.02rem; color: var(--ca-ink); }

/* --- attestation block: the three propositions -------------------------- */
.attest { border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius); padding: 1.15rem 1.2rem; background: var(--sf-elevated); }
.attest .check { margin-bottom: 1rem; }
.attest .check:last-of-type { margin-bottom: .35rem; }
.attest-note { font-size: .92rem; line-height: 1.5; color: var(--sf-text); margin: 1rem 0 0; padding-top: .85rem; border-top: 1px solid var(--ca-line); max-width: none; }

/* --- record tables: orders, tracking, account --------------------------- */
.record { border: 1px solid var(--ca-line-strong); border-radius: var(--sf-radius); overflow: hidden; margin-bottom: 1.15rem; }
.record-head { display: flex; flex-wrap: wrap; gap: .5rem 1rem; justify-content: space-between; align-items: baseline; padding: .9rem 1.1rem; background: var(--sf-surface); border-bottom: 1px solid var(--ca-line); }
.record-id { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); font-variant-numeric: tabular-nums; }
.record-body { padding: 1.05rem 1.1rem; }
.record-lines { list-style: none; margin: 0 0 .9rem; padding: 0; }
.record-lines li { display: flex; justify-content: space-between; gap: 1rem; padding: .35rem 0; border-bottom: 1px solid var(--ca-line); font-size: .9rem; }
.record-lines li:last-child { border-bottom: 0; }
.pill {
  font-family: var(--sf-font-display); font-size: .66rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; border-radius: 999px;
  padding: .18rem .6rem; border: 1px solid var(--ca-line-strong); color: var(--sf-muted);
  white-space: nowrap;
}
.pill-on { border-color: var(--sf-accent); color: var(--sf-accent); }
.pill-wait { border-color: color-mix(in srgb, #8a6d00 55%, transparent); color: #7a5f00; }
.pill-off { border-color: color-mix(in srgb, #a3121a 45%, transparent); color: #a3121a; }

/* --- progress trail (tracking) ------------------------------------------ */
.trail { list-style: none; margin: 0; padding: 0 0 0 1.35rem; border-left: 1px solid var(--ca-line-strong); }
.trail li { position: relative; padding: 0 0 1.25rem; }
.trail li:last-child { padding-bottom: 0; }
.trail li::before {
  content: ""; position: absolute; left: -1.72rem; top: .42rem;
  width: .55rem; height: .55rem; border-radius: 999px;
  background: var(--sf-bg); border: 1px solid var(--ca-line-strong);
}
.trail li.on::before { background: var(--sf-accent); border-color: var(--sf-accent); }
.trail-when { display: block; font-size: .78rem; color: var(--sf-muted); font-variant-numeric: tabular-nums; }
.trail-what { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); }

/* --- account section switcher ------------------------------------------- */
.acct { display: grid; gap: clamp(1.5rem, 4vw, 2.5rem); align-items: start; }
@media (min-width: 56rem) { .acct { grid-template-columns: 14rem minmax(0, 1fr); } }
.acct-nav { list-style: none; margin: 0; padding: 0; border: 1px solid var(--ca-line); border-radius: var(--sf-radius); overflow: hidden; }
.acct-nav a {
  display: block; padding: .75rem 1rem; font-size: .9rem; text-decoration: none;
  color: var(--sf-text); border-bottom: 1px solid var(--ca-line); background: var(--sf-surface);
}
.acct-nav li:last-child a { border-bottom: 0; }
.acct-nav a:hover { background: var(--ca-wash); color: var(--ca-ink); }
.acct-nav a[aria-current="true"] { background: var(--ca-ink); color: #fff; }

/* --- payment instruction block (order confirmation) --------------------- */
.paybox { border: 1px solid var(--ca-line-strong); border-left: 3px solid var(--sf-accent); border-radius: 0 var(--sf-radius) var(--sf-radius) 0; padding: clamp(1.1rem, 3vw, 1.6rem); background: var(--ca-wash); }
.paybox pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: var(--sf-font-body); font-size: .95rem; line-height: 1.6; }
.paylogo { height: 1.6rem; width: auto; display: inline-block; vertical-align: middle; }

/* --- empty states -------------------------------------------------------- */
.empty { border: 1px dashed var(--ca-line-strong); border-radius: var(--sf-radius); padding: clamp(1.5rem, 5vw, 2.75rem); text-align: center; }
.empty p { margin-inline: auto; }

/* ==========================================================================
   ATMOSPHERE — the layer that separates "correct" from "alive".
   Same tokens, same document logic; what changes is that colour now WASHES
   between sections instead of every surface being flat paper, and the
   client's node-graph mark breathes quietly behind the hero. Everything here
   is transform/opacity/background only, honours prefers-reduced-motion, and
   never adds a colour the theme ramp does not already own.
   ========================================================================== */

/* --- section washes: the ramp's pale end, used as fields ----------------- */
.wash      { background: color-mix(in srgb, var(--sf-accent-1) 22%, var(--sf-bg)); }
.wash-soft { background: color-mix(in srgb, var(--sf-accent-1) 11%, var(--sf-bg)); }
.wash-deep {
  background:
    radial-gradient(120% 90% at 85% 10%, color-mix(in srgb, var(--sf-accent) 16%, transparent), transparent 60%),
    linear-gradient(175deg, color-mix(in srgb, var(--sf-accent-1) 30%, var(--sf-bg)), var(--sf-bg) 70%);
}
/* Navy band — an inverted section for statements that should land heavy.
   Text tokens are inverted by hand because theme.css only knows light mode. */
.band-ink { background: var(--ca-ink); color: #EDEFFA; }
.band-ink h2, .band-ink h3 { color: #fff; }
.band-ink .eyebrow { color: color-mix(in srgb, #fff 65%, var(--sf-accent-1)); }
.band-ink .eyebrow::after { background: color-mix(in srgb, #fff 25%, transparent); }
.band-ink .muted { color: color-mix(in srgb, #EDEFFA 72%, transparent); }
/* :not(.btn) matters — `.band-ink a` (0-2-0) outranks `.btn-primary` (0-1-0),
   so without it every primary button inside the band inherited pale accent-1
   text on the accent fill and measured 3.83:1. The QA contrast check caught
   it; the exclusion is the fix, not a lighter token. */
.band-ink a:not(.btn) { color: var(--sf-accent-1); }

/* --- constellation: the client's mark as section atmosphere -------------- */
/* Two planes of tiled radial-gradient dots — HIS logo motif as texture.
   Deliberately NOT a molecular-network wallpaper: sparse, low-alpha, no mesh.
   (The comment that stood here claimed "drawn once as an SVG background …
   sub-3%-opacity lines". Neither was ever true of this code: the planes are
   radial gradients and they have no lines at all. Corrected, not carried.)

   THIS BLOCK SPLIT IN TWO on 2026-08-16, owner: "this dot animation is really
   laggy, and it's not really fluid. All of the dots don't really move. Make it
   fluid and smooth so the dots float around."
     · A PLAIN `.constellation` — the gate, login, register, reset- and
       forgot-password, the checkout top, the order-success masthead, the
       products / track / faq / about heroes and about's own ink band: TWELVE
       surfaces across eleven pages, counted 2026-08-16 — keeps these planes,
       as a STILL. (A thirteenth, index's `.band-ink.constellation.vfield`, has
       had no planes at all since the `.vfield` rule below.) They crawled 1.6–2.4% of
       their own size over 64–88s, which is exactly what "the dots don't really
       move" describes: a translate that slow is under the noticeability floor,
       so the layer paid full compositing cost and bought nothing anyone could
       see. As stills they keep every bit of their texture at zero animation
       cost, and both drift keyframes are gone from the sheet entirely — grep
       `ca-drift` and you will find nothing.
     · A `.constellation[data-starfield]` — the top section of coa-library,
       standards, wholesale, partner-program and contact — drops these planes
       and takes a live canvas instead. See the starfield block below. The
       reason it cannot be more CSS: a hairline drawn BETWEEN two independently
       moving dots is the one part of this motif that is meaning rather than
       ornament, and no CSS layer can draw it. Transforms can float a dot; they
       cannot connect two. */
/* clip, not hidden: `hidden` makes a section a SCROLL CONTAINER, so a focus or
   anchor jump inside it shifts the section under the reader (and puppeteer's
   scrollIntoView sampled the wrong pixels — see .powder-stage above). Same
   pattern everywhere a section clips decoration: hidden first as the fallback
   for engines that predate clip. (2026-08-15, WS-B) */
.constellation { position: relative; overflow: hidden; overflow: clip; }
.constellation::before,
.constellation::after {
  content: ""; position: absolute; inset: -12% -6%;
  background-image:
    radial-gradient(circle, color-mix(in srgb, var(--sf-accent) 42%, transparent) 0 2.5px, transparent 3px),
    radial-gradient(circle, color-mix(in srgb, var(--sf-accent) 30%, transparent) 0 2px, transparent 2.5px),
    radial-gradient(circle, color-mix(in srgb, var(--sf-accent) 22%, transparent) 0 1.5px, transparent 2px);
  background-size: 340px 300px, 260px 380px, 190px 240px;
  background-position: 12% 18%, 70% 60%, 40% 85%;
  pointer-events: none;
  z-index: 0;
}
.constellation::after {
  background-size: 420px 360px, 300px 300px, 230px 320px;
  background-position: 85% 25%, 25% 70%, 60% 40%;
  opacity: .6;
}
/* `will-change: transform` was declared here unconditionally and has been
   REMOVED (round 6, 2026-08-15). It promoted two section-sized layers on every
   `.constellation` on the site — thirteen pages, twenty-six layers — and held
   them promoted forever, including for reduced-motion visitors who never see
   the drift at all. Nothing needed it: a `transform` keyframe animation is
   composited by the engine on its own, and `will-change` only ever bought the
   first frame of a 64-second loop. What it cost was the rasterised backing
   store for three tiled radial gradients per layer, kept resident for the life
   of the page, inside the backdrop root of every effect stacked above it. */
.constellation > * { position: relative; z-index: 1; }
/* A section that is BOTH a constellation and an ambient video field ran three
   ambient systems at once — a decoding 1280x720 clip, plus two drifting
   gradient planes over the top of it. The planes are drawn in `--sf-accent`
   at 22–42% alpha, which over an ink-dark band under a veil is at the edge of
   visible and is not worth what it costs to composite above live video. The
   clip is the motion in these sections. */
.vfield.constellation::before, .vfield.constellation::after { content: none; }

/* --- starfield: the same constellation, genuinely afloat ----------------- */
/* One live canvas per `[data-starfield]` section (mounted by catharos.js), in
   place of the two tiled planes above. The attribute selector below is 0-2-0
   and outranks `.constellation > *` (0-1-0), so neither this rule nor the
   canvas's own depends on source order to win.
   WHY CANVAS AND NOT FORTY ANIMATED ELEMENTS: forty individually animated CSS
   dots are "composited for free" only in the sense that each becomes its own
   promoted layer — which is precisely the 26-resident-layers mistake recorded
   just above, paid forty times over and on five pages. One canvas is a single
   paint surface whose cost is bounded, measured, and PAUSED when the section
   is off-screen (IntersectionObserver, in the JS).
   WHY THIS IS CSS AND NOT A JS CLASS TOGGLE: with scripting disabled no canvas
   is ever created, and `content: none` here still suppresses the planes, so the
   section's own ground — coa-library's `wash-deep` gradient, the page-heads'
   paper — stands by itself. Finished, with no dots at all. That is the blessed
   JS-off fallback, and it only works because the suppression is a stylesheet
   fact rather than something a script has to arrive to state. */
.constellation[data-starfield]::before,
.constellation[data-starfield]::after { content: none; }

[data-starfield] > .starfield {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 0; pointer-events: none;
  /* Ground-aware by construction, with zero page knowledge. `currentColor`
     inside the `color` property resolves to the SECTION's own inherited text
     colour, so the field asks the cascade what ground it is standing on
     instead of this stylesheet keeping a list of filenames that another
     session can invalidate. On all five of today's sections the inherited ink
     is #192534 and this mixes to rgb(43,50,106) — deep accent navy on paper.
     Invert any of them (`.band-ink`, pale #EDEFFA text) and the identical rule
     resolves to rgb(159,161,214), pale periwinkle on navy, with no edit here.
     catharos.js reads this back through getComputedStyle and picks its alpha
     profile from the resolved luminance.
     No `will-change`: a canvas IS its own backing store and needs no promotion
     hint. The lesson recorded above is not being paid a second time. */
  color: color-mix(in srgb, var(--sf-accent) 45%, currentColor);
}

/* --- floating vials: the catalog itself as ambient life ---------------- */
/* Real, gate-passed product stills drifting at low opacity behind hero and
   band content. GPU transforms only, slow cycles, and the whole layer is
   removed (not merely paused) under prefers-reduced-motion. */
.floaters { position: absolute; inset: 0; overflow: hidden; overflow: clip; pointer-events: none; z-index: 0; display: none; }
@media (prefers-reduced-motion: no-preference) { .floaters { display: block; } }
/* These used to lean on mix-blend-mode: multiply to drop the white sweep. It
   never fired — the host is position:absolute + z-index:0, which is its own
   stacking context, so the blend had an empty backdrop and every floater
   rendered as a pale white RECTANGLE. The derivatives now carry real alpha
   (factory/tools/cutout.mjs), so no blending is involved at all and the same
   asset works over the pale wash and the navy band alike. */
/* The host section. `isolation: isolate` keeps the layer and the content in one
   predictable order without either needing to out-index the other — the same
   arrangement the hero uses for the powder plate. */
/* `overflow: hidden` on the HOST as well as on .floaters: the slots sit at
   negative offsets by design (they hang into the page gutter), so the host is
   the backstop that guarantees a floater can never widen the document. */
.floater-host { position: relative; isolation: isolate; overflow: hidden; overflow: clip; }
.floater-host > .floaters { z-index: 0; }
.floater-host > .wrap { position: relative; z-index: 1; }

/* Section floaters do not traverse. A vial sailing a full viewport height
   through a paragraph is distracting, and on a 40s cycle it also leaves the
   section empty for most of the visit. These hold their placement and breathe:
   a small rise, a few degrees of roll, constant low opacity. Transform and
   nothing else, so it stays on the compositor. */
/* Full opacity by default now. These sit in the page gutter, clear of the
   content column, and the derivatives carry real alpha — so at full strength
   they read as specimens standing beside the copy rather than as a watermark. */
/* The width IS the space available, rather than a guess that has to be checked
   against it. The clear strip outside `.wrap` is (100vw - 76rem)/2 plus the
   wrap's own gutter; take that, less a little breathing room, and a floater can
   never overlap the copy NOR be clipped by the edge — the two failure modes
   this has had. Capped so it stays ambient on a very wide screen. */
.floater-sec {
  position: absolute;
  /* 2x (owner, 2026-08-15 evening: "make floating vials 2x size"), then "a
     little bit smaller" (same night, with the cl-3 hero swap): cap 22rem→18rem
     and the gutter multiplier ×2→×1.7 — about 18% off the 2x, still well above
     the original 11rem. A floater may still reach INTO the content column
     deliberately; it stays under the copy (z-index 0 vs the wrap's 1,
     pointer-events none), so the overlap is visual depth, not interference. */
  width: min(18rem, max(0px, calc(((100vw - 76rem) / 2 + var(--ca-gutter) - 1.4rem) * 1.7)));
  opacity: var(--f-op, 1);
  filter: saturate(.94) contrast(1.02) drop-shadow(0 18px 26px rgb(var(--sf-shadow) / .18));
  animation: ca-hover var(--f-dur, 30s) ease-in-out var(--f-delay, 0s) infinite;
  will-change: transform;
}
/* 82rem is where the formula above still leaves a vial worth drawing (~86px).
   A previous pass put this at 90rem and the owner's window is narrower than
   that, so every floater vanished — "did you delete the floating vials?".
   Below 82rem the clear strip is under ~5rem and anything drawn in it is either
   a sliver or on top of the copy, so nothing is drawn. */
@media (max-width: 82rem) { .floater-sec { display: none; } }
.band-ink .floater-sec { filter: saturate(.75) brightness(1.35) contrast(.95); }
/* `--f-rise` was factored out of the 50% keyframe on 2026-08-16 so the HERO
   CLUSTER can breathe less than a mid-page floater does. The default IS the
   old literal, so every `.floater-sec` and the gate's vials are byte-identical
   in behaviour — this adds a dial, it does not retune anything that existed.
   Why the hero wants a smaller one: a mid-page floater is alone in a gutter and
   a 2.6rem rise reads as life; three vials standing as ONE GROUP that each rose
   2.6rem on different clocks would pull the bundle apart, which is the opposite
   of what was asked for. */
@keyframes ca-hover {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(var(--f-rot-a, -6deg)); }
  50%      { transform: translate3d(var(--f-sway, .6rem), var(--f-rise, -2.6rem), 0) rotate(var(--f-rot-b, 4deg)); }
}

/* --- glass: controls and chrome with depth ------------------------------- */
.btn-glass, .snav.glass {
  background: color-mix(in srgb, var(--sf-bg) 55%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.5);
  backdrop-filter: blur(14px) saturate(1.5);
  border: 1px solid color-mix(in srgb, #fff 55%, var(--ca-line));
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent),
    0 10px 30px -18px rgb(var(--sf-shadow) / .5);
}
.btn-glass { color: var(--ca-ink); }
.btn-glass:hover { background: color-mix(in srgb, var(--sf-bg) 78%, transparent); color: var(--ca-ink); transform: translateY(-1px); }
.band-ink .btn-glass {
  background: color-mix(in srgb, #fff 12%, transparent);
  border-color: color-mix(in srgb, #fff 28%, transparent);
  color: #fff;
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 30%, transparent), 0 10px 30px -18px rgb(0 0 0 / .6);
}
.band-ink .btn-glass:hover { background: color-mix(in srgb, #fff 20%, transparent); color: #fff; }
/* THE JANK, and its fix (round 6, owner 2026-08-15: the seven-checks band
   "looks really laggy and pretty bad — it needs to be moving [smoothly]").
   `.btn-glass` carries `backdrop-filter: blur(14px) saturate(1.5)`, and on the
   home page exactly one `.btn-glass` exists: "The testing standard", sitting
   inside `.band-ink.vfield` — i.e. directly on top of a PLAYING VIDEO. A
   backdrop filter is not a paint of the element, it is a read-back and re-blur
   of everything behind it, and its backdrop is invalidated by every decoded
   frame of the clip underneath. So the compositor re-read and 14px-gaussian'd
   that region 24 times a second, and the video lost the direct-composite path
   it would otherwise have taken. One `backdrop-filter` over one video is enough
   to make a whole band stutter, and this was that.
   The button keeps its glass READING — the fill is raised to an opaque-enough
   translucency and the inset highlight and border are untouched — it simply
   stops sampling what is behind it. Scoped to `.vfield` so the same class over
   a static ground (the PDP, the gate) is unchanged. */
.vfield .btn-glass {
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.band-ink.vfield .btn-glass { background: color-mix(in srgb, #fff 20%, transparent); }
.band-ink.vfield .btn-glass:hover { background: color-mix(in srgb, #fff 30%, transparent); }

/* --- lighting: a source behind the subject ------------------------------- */
.lit { position: relative; }
.lit::before {
  content: ""; position: absolute; inset: -18%;
  background: radial-gradient(50% 42% at 50% 38%, color-mix(in srgb, var(--sf-accent) 26%, transparent), transparent 70%);
  pointer-events: none; z-index: 0;
}
.lit > * { position: relative; z-index: 1; }
.band-ink .lit::before {
  background: radial-gradient(52% 45% at 50% 40%, color-mix(in srgb, var(--sf-accent-1) 30%, transparent), transparent 72%);
}

/* --- hero object: the product, lit, with no chrome around it ------------- */
/* No mix-blend-mode any more. The derivatives carry a real alpha channel
   (factory/tools/cutout.mjs), which is what lets the vial stand IN the powder.
   multiply could never have worked here: it only blends against its own
   stacking context — and the figure's own z-index silently made it a no-op,
   which is why this rendered as a white rectangle — and even when it does
   blend it lets a textured ground show through the label.
   The still is a square frame with transparent margins around the vial, so it
   is scaled past 100% and the empty margins simply overflow the section. */
.hero-object { display: grid; place-items: center; padding: 0; }
.hero-object figure { margin: 0; width: 100%; max-width: none; display: grid; place-items: center; }
.hero-object img {
  width: min(34rem, 112%); max-width: none; height: auto; display: block;
  filter: drop-shadow(0 38px 55px rgb(var(--sf-shadow) / .26));
}
@media (min-width: 60rem) {
  .hero-object { justify-self: end; }
  .hero-object img { width: min(52rem, 148%); margin-right: -6%; }
}
/* The glow reads as a source behind the glass. Its core is transparent so it
   haloes the vial instead of tinting the label. */
.hero-object.lit::before {
  inset: -22%;
  background: radial-gradient(46% 40% at 50% 46%,
    transparent 0 24%,
    color-mix(in srgb, var(--sf-accent) 22%, transparent) 46%,
    transparent 72%);
}
/* --- the specimen's seat beside "The standard" ------------------------- */
/* Moved out of the hero on 2026-08-15 (owner). The hero's powder fill carries
   that section alone; here the vial has copy to stand beside and a plain ground
   to stand on. */
.stand-grid { display: grid; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
@media (min-width: 60rem) { .stand-grid { grid-template-columns: 1.05fr 0.95fr; } }
/* `overflow: hidden` is load-bearing, not tidiness. In the hero this figure was
   free to scale past 100% because .powder-stage clipped it; here the parent is
   an ordinary .section, so an image at 118% pushed the document wider than the
   viewport and the QA overflow check failed the build at both 1280 and 375.
   The figure now stays inside its column AND the box clips, so neither the
   scaled art nor the .lit glow (inset:-18%) can reach the page edge. */
.stand-object { display: grid; place-items: center; position: relative; overflow: hidden; overflow: clip; }
.stand-object figure { margin: 0; width: 100%; max-width: none; display: grid; place-items: center; }
.stand-object img {
  width: 100%; max-width: 34rem; height: auto; display: block;
  filter: drop-shadow(0 34px 48px rgb(var(--sf-shadow) / .24));
}
.stand-object.lit::before {
  inset: -18%;
  background: radial-gradient(46% 40% at 50% 46%,
    transparent 0 24%,
    color-mix(in srgb, var(--sf-accent) 20%, transparent) 46%,
    transparent 72%);
}

/* --- the mark + definition, in the seat the specimen vacated -------------- */
/* Swapped down from the hero, 2026-08-15 (owner). This is an ordinary section
   over a plain surface, so the mark needs none of the machinery it carried in
   the hero: no scrim to out-argue a video frame, no timed entrance, no absolute
   layer. It is just artwork above a caption.
   `overflow: hidden` is INHERITED from .stand-object and is load-bearing there
   — see the note above it; do not drop it. */
.stand-mark {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(.75rem, 1.6vw, 1.35rem);
  /* NO `place-items` reset here. There used to be a `place-items: initial`
     after `align-items: center`, meant to undo .stand-object's grid
     `place-items: center` — but place-items is the SHORTHAND for align-items
     + justify-items, so declared later in the same rule it silently reset
     align-items back to `normal`, and in a column flex an intrinsically-sized
     img under `normal` sits at flex-start: the C mark rendered ~150px LEFT of
     the caption's centre (owner, 2026-08-15 night: "i dont know why the c logo
     is off center"). Measured, not guessed: img cx 800 vs column cx 947 at
     1280. .stand-object's own `place-items: center` is harmless in flex — it
     resolves to the same align-items: center — so it simply stands. */
}
/* VERTICAL SEAT (owner, 2026-08-16: "move up the c logo here its too low").
   .stand-grid centres its two cells, and this column (mark + plate, ~387px) is
   much shorter than the candor block beside it (~676px at 1280), so centring
   parked the mark's top 100px BELOW the heading's top and its centre well
   under the copy's midline. Measured, not guessed: img top 1401 vs h2 top
   1301 at 1280. Now the column sits at the START of the row and is inset by
   the candor's own top padding, so the mark's top lands on the heading's top
   line — mark and copy read as one row that starts together. Only where the
   grid is two-up; stacked (below 60rem) it is ordinary flow, and neither the
   alignment nor the inset applies. */
@media (min-width: 60rem) {
  .stand-mark { align-self: start; padding-top: clamp(1.1rem, 3vw, 1.75rem); }
}
.stand-mark img {
  width: clamp(9rem, 17vw, 14rem);
  max-width: 100%; height: auto; display: block;
  filter: none;                          /* never tint the mark — see .snav-logo img */
}
/* Barely there. The old hero halo was 86% opaque page colour at its core, which
   over a plain surface reads as a lit disc rather than as air (owner: "too
   strong"). This is a breath of accent at a tenth of that, and it sits BEHIND
   the artwork only — the caption plate below has its own opaque ground. */
.stand-mark::after {
  content: ""; position: absolute; z-index: -1;
  left: 50%; top: 0; width: min(22rem, 92%); aspect-ratio: 1;
  transform: translateX(-50%);
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--sf-accent) 7%, transparent) 0 42%,
    transparent 78%);
  pointer-events: none;
}
/* The definition is a PLATE — a white card with real weight — not a caption
   floating on the wash (owner, 2026-08-15 night: "make the catharos definition
   be on a white boxed background with some weight so it stands out more").
   The vocabulary is the candor block's, one column over: an ink rule on one
   edge (top here, left there), a hairline border, and the page's elevated
   surface. Type steps up from muted to text colour and gains a little size so
   the sentence carries the box rather than sitting in it. */
.stand-mark .hero-meaning-plate {
  position: relative;
  margin: 0; width: 100%; max-width: 30rem;
  background: var(--sf-elevated);
  border: 1px solid var(--ca-line);
  border-top: 3px solid var(--ca-ink);
  border-radius: var(--sf-radius, 8px);
  padding: clamp(1rem, 2vw, 1.4rem) clamp(1.15rem, 2.4vw, 1.75rem);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--ca-ink) 6%, transparent),
              0 14px 34px -12px rgb(var(--sf-shadow) / .22),
              0 4px 12px -6px rgb(var(--sf-shadow) / .12);
  color: var(--sf-text); font-size: 1.02rem; font-weight: 500;
  text-align: center; text-wrap: pretty;
}

/* --- wholesale band ------------------------------------------------------ */
/* The CTA used to be a right-aligned paragraph in a borrowed .hero-grid, which
   left it stranded in white space with the next section's plaque crowding up
   underneath it. It sits in its own column now and the band keeps a full seam
   below it. */
.wholesale-band { padding-block: clamp(2.75rem, 6vw, 4.25rem); }
.wholesale-grid { display: grid; gap: clamp(1.25rem, 3vw, 2.5rem); align-items: center; }
@media (min-width: 60rem) { .wholesale-grid { grid-template-columns: 1fr auto; } }
.wholesale-cta { margin: 0; }
@media (min-width: 60rem) { .wholesale-cta { justify-self: end; } }

.hero-meaning {
  max-width: 60ch; font-size: .95rem; line-height: 1.65;
  color: var(--sf-muted); padding-top: 1rem;
  border-top: 1px solid var(--ca-line);   /* was a teal left rule — owner cut the green */
}
.hero-meaning span { font-family: var(--sf-font-display); color: var(--ca-ink); font-weight: 600; }
.hero-meaning em { font-style: italic; color: var(--ca-ink); }

/* Specimen panel gets glass + inner glow so the hero object sits in light. */
.specimen { position: relative; }
.specimen-body { position: relative; }
.specimen-body::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(60% 45% at 50% 32%, color-mix(in srgb, var(--sf-accent-1) 38%, transparent), transparent 75%);
  pointer-events: none;
}
.specimen-body > * { position: relative; }

/* --- ambient video field: full-bleed motion behind a section -------------
   Named `.vfield`, NOT `.field`: `.field` is the form-label wrapper 500 lines
   up, so this block's `overflow: hidden` was clipping the focus ring of every
   input on the site — every auth page, checkout, contact, account (found
   2026-08-15, WS-B). Two meanings for one class name is not a naming quibble
   when one of them is a keyboard-accessibility surface. */
.vfield { position: relative; overflow: hidden; overflow: clip; }
.vfield > .field-media {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; pointer-events: none;
}
.vfield > .field-veil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, color-mix(in srgb, var(--sf-bg) 78%, transparent), color-mix(in srgb, var(--sf-bg) 30%, transparent) 45%, color-mix(in srgb, var(--sf-bg) 85%, transparent));
}
.vfield > .wrap { position: relative; z-index: 2; }
.band-ink.vfield > .field-veil {
  background: linear-gradient(180deg, color-mix(in srgb, var(--ca-ink) 82%, transparent), color-mix(in srgb, var(--ca-ink) 55%, transparent) 50%, color-mix(in srgb, var(--ca-ink) 88%, transparent));
}

/* --- richer product cards ------------------------------------------------ */
.pcard-alias { font-size: .78rem; color: var(--sf-muted); line-height: 1.4; }
.dose-pills { display: flex; flex-wrap: wrap; gap: .3rem; }
.dose-pill {
  font-variant-numeric: tabular-nums; font-size: .72rem; font-weight: 600;
  font-family: var(--sf-font-display);
  border: 1px solid var(--ca-line-strong); border-radius: 999px;
  padding: .14rem .55rem; color: var(--ca-ink); white-space: nowrap;
}
.pcard:hover .pcard-fig img, .pcard:hover .pcard-fig video { transform: scale(1.045); }
.pcard-fig img, .pcard-fig video { transition: transform .35s cubic-bezier(.22,.61,.36,1); }

/* --- category strip ------------------------------------------------------ */
.cat-strip { display: grid; gap: 1px; background: var(--ca-line); border: 1px solid var(--ca-line); }
@media (min-width: 44rem) { .cat-strip { grid-template-columns: repeat(3, 1fr); } }
.cat-cell {
  background: var(--sf-bg); padding: clamp(1.1rem, 3vw, 1.6rem);
  text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: .3rem;
  transition: background-color .16s ease;
}
.cat-cell:hover { background: var(--ca-wash); }
.cat-cell .n { font-family: var(--sf-font-display); font-weight: 600; font-size: 1.05rem; color: var(--ca-ink); }
.cat-cell .d { font-size: .85rem; color: var(--sf-muted); }
.cat-cell .c { font-family: var(--sf-font-display); font-size: .72rem; font-weight: 700; letter-spacing: .1em; color: var(--sf-accent); margin-top: .4rem; }

/* --- stat row: tabular confidence without a fake badge ------------------- */
.stat-row { display: grid; gap: 1px; background: var(--ca-line); border: 1px solid var(--ca-line); }
@media (min-width: 40rem) { .stat-row { grid-template-columns: repeat(4, 1fr); } }
.stat { background: var(--sf-bg); padding: 1rem 1.1rem; }
.stat .v { font-family: var(--sf-font-display); font-weight: 600; font-size: clamp(1.2rem, 1rem + 1vw, 1.7rem); color: var(--ca-ink); font-variant-numeric: tabular-nums; }
.stat .k { font-size: .78rem; color: var(--sf-muted); letter-spacing: .05em; }

/* ==========================================================================
   CHOREOGRAPHY — codex patterns, wired as a system.
   Every entrance is IntersectionObserver-triggered (.in), staggered by the
   --i custom property, GPU transforms + opacity only, and the whole layer
   collapses to "visible, still" under prefers-reduced-motion.
   Patterns: Typeline Cascade, Inkline Draw, Ledger Rise, Quorum Stagger,
   Blur-to-Focus Arrival, Lift-and-Shade, Glass Tilt, Border Circuit,
   Ghost Fill Sweep, Badge Pop & Tally, Press Compression, Second Seal.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  /* Ledger Rise — the base entrance. --i staggers siblings. */
  .rise { opacity: 0; transform: translateY(16px); transition: opacity .55s ease, transform .55s cubic-bezier(.22,.61,.36,1); transition-delay: calc(var(--i, 0) * 90ms); }
  .rise.in { opacity: 1; transform: none; }
  .rise:nth-child(2) { --i: 1; }
  .rise:nth-child(3) { --i: 2; }
  .rise:nth-child(4) { --i: 3; }

  /* Typeline Cascade — headline lines surface one after another from behind
     a baseline mask, like lines of a form being typed. */
  /* The mask is the HEADING box, and a heading box ends at the last line's
     baseline-plus-descent — which is NOT where the ink ends. "isn't worth
     selling" hung its `g` 11.6px past the bottom edge at 62px type and the mask
     shaved it clean off (owner, 2026-08-15). A descender's worth of padding
     gives the ink somewhere to live; the span's start offset grows by the same
     amount so a line still begins fully outside the mask and no sliver of the
     next line is visible before it rises. Measured at .186em on desktop and
     .184em on mobile, so .22em clears both with room to spare.
     No compensating negative margin: the 11.6px that used to be cut is now ink
     sitting inside the padding, so the perceived gap to the lede moves ~2px. */
  .tl { display: block; overflow: hidden; padding-bottom: .22em; }
  .tl > span { display: block; transform: translateY(calc(110% + .22em)); transition: transform .7s cubic-bezier(.19,.7,.22,1); transition-delay: calc(var(--i, 0) * 130ms); }
  .in .tl > span, .tl.in > span { transform: none; }

  /* Inkline Draw — hairline rules draw themselves left to right. */
  .inkline { position: relative; }
  .inkline::after { content: ""; position: absolute; left: 0; bottom: -6px; height: 1px; width: 100%; background: var(--ca-line-strong); transform: scaleX(0); transform-origin: left; transition: transform .9s cubic-bezier(.22,.61,.36,1) .15s; }
  .in .inkline::after, .inkline.in::after { transform: scaleX(1); }
  .rule-node { transform-origin: left; }
  .rule-node.rise { transform: scaleX(.2); }
  .rule-node.rise.in { transform: scaleX(1); }

  /* Blur-to-Focus Arrival — the hero object resolves like an instrument
     being brought into focus. */
  .focus-in { opacity: 0; filter: blur(10px); transform: scale(.97); transition: opacity .8s ease, filter .8s ease, transform .8s cubic-bezier(.22,.61,.36,1); }
  .focus-in.in { opacity: 1; filter: none; transform: none; }

  /* Quorum Stagger — grid children arrive as a roll call. */
  .quorum > * { opacity: 0; transform: translateY(14px); transition: opacity .5s ease, transform .5s cubic-bezier(.22,.61,.36,1); }
  .quorum.in > * { opacity: 1; transform: none; }
  .quorum.in > *:nth-child(1) { transition-delay: .02s; } .quorum.in > *:nth-child(2) { transition-delay: .1s; }
  .quorum.in > *:nth-child(3) { transition-delay: .18s; } .quorum.in > *:nth-child(4) { transition-delay: .26s; }
  .quorum.in > *:nth-child(5) { transition-delay: .34s; } .quorum.in > *:nth-child(6) { transition-delay: .42s; }
  .quorum.in > *:nth-child(7) { transition-delay: .5s; }  .quorum.in > *:nth-child(8) { transition-delay: .58s; }

  /* Second Seal — a notice stamps itself when it enters the record.
     Settles FLAT: a residual rotation left body text visibly tilted. */
  .stamp { opacity: 0; transform: scale(1.14); transition: opacity .34s cubic-bezier(.34,1.4,.64,1), transform .34s cubic-bezier(.34,1.4,.64,1); }
  .stamp.in { opacity: 1; transform: none; }

  /* Constellation drift — REMOVED 2026-08-16 (owner: "all of the dots don't
     really move"). `ca-drift-a`/`ca-drift-b` translated the two gradient planes
     1.6–2.4% of their own size over 64 and 88 seconds, on every `.constellation`
     in the site — seventeen surfaces across sixteen pages. At that rate a dot
     crosses about five pixels a minute, which is below the threshold at which a
     human reads anything as moving, so every one of those surfaces ran a
     perpetual composited animation that nobody could perceive.
     The planes stay, as stills (see the `.constellation` block). The five
     sections that genuinely needed motion took a canvas instead, and the dots
     there move at 6–16 px/s, which is roughly two hundred times faster and is
     the actual answer to the complaint. Do not reinstate a slow drift here to
     "bring the texture back" — the texture never left; only the crawl did. */
}
/* Reduced motion: everything above must READ perfectly as a still. */
@media (prefers-reduced-motion: reduce) {
  .tl > span { transform: none; }
  .focus-in { opacity: 1; filter: none; transform: none; }
  .quorum > * { opacity: 1; transform: none; }
  .stamp { opacity: 1; transform: none; }
}

/* --- Border Circuit / Ghost Fill Sweep: button anatomy (all modes) ------- */
.btn { position: relative; overflow: hidden; }
.btn-primary::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 38%, color-mix(in srgb, #fff 26%, transparent) 50%, transparent 62%);
  transform: translateX(-120%);
}
@media (prefers-reduced-motion: no-preference) {
  .btn-primary:hover::before { transition: transform .7s ease; transform: translateX(120%); }
  .btn:active { transform: translateY(0) scale(.985); } /* Press Compression */
}
/* Underlines that draw themselves (body links in prose surfaces). */
.candor a, .plaque a, .foot-fine a {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1px; background-position: 0 100%; background-repeat: no-repeat;
  text-decoration: none;
}
@media (prefers-reduced-motion: no-preference) {
  .candor a, .plaque a, .foot-fine a { background-size: 0 1px; transition: background-size .35s ease; }
  .candor a:hover, .plaque a:hover, .foot-fine a:hover { background-size: 100% 1px; }
}

/* --- Glass Tilt: cards get depth under the pointer ----------------------- */
@media (prefers-reduced-motion: no-preference) and (pointer: fine) {
  .pcard, .panel, .cat-cell { transition: transform .28s cubic-bezier(.22,.61,.36,1), box-shadow .28s ease, background-color .16s ease; }
  .pcard:hover, .cat-cell:hover { transform: translateY(-4px); box-shadow: 0 18px 40px -22px rgb(var(--sf-shadow) / .45); }
  .panel:hover { transform: translateY(-2px); box-shadow: 0 14px 34px -24px rgb(var(--sf-shadow) / .4); }
}
@media (prefers-reduced-motion: reduce) {
  .rise { opacity: 1; transform: none; }
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* --- the researcher gate ------------------------------------------------- */
/* The sitewide age/research attestation is built by the ENGINE (age-gate.js,
   a protected file — engine/FRONTEND-CONTRACT.md §B): it injects `#lag-root`
   with a `.lag-modal` dialog, two `.lag-check` boxes, `#lag-confirm` and
   `#lag-exit`, plus its own <style id="lag-style">, and owns the logic
   (localStorage TTL, the disabled-until-both-checked button, the exit). None
   of that is touched. What follows RE-DRESSES it in this site's own terms —
   owner, 2026-08-15 night, holding up Amino Club's /age-verify: "i need this
   on the catharos site as well. take inspiration from it and make something
   similar". Studied as a pattern, not copied (design-research doctrine): a
   full-page light interstitial, the brand lockup standing above a white
   verification card, two boxed affirmations, one full-width enter control
   that wakes when both are ticked, fine print, a quiet way out, and the
   catalog itself tumbling softly in the field behind. Every one of those is
   redrawn in the assay-sheet vocabulary — paper ground, hairline rules, ink
   rule on the card, Literata title, tabular quiet — and the vials are OUR
   cut-outs from the truthful asset map, placed by catharos.js.

   Specificity, on purpose: the engine's stylesheet is appended to <head> at
   inject time and so wins every tie, so every rule here carries `#lag-root`
   (1-1-0 beats the engine's .lag-* at 0-1-0), and the root itself is addressed
   as `html #lag-root` (1-0-1 beats `#lag-root` at 1-0-0). Nothing uses
   !important.

   NOT `body > #lag-root`, which is what this was until 2026-08-18 and what
   broke it. engine commit ed52c49 moved the scrim off <body> and onto <html>
   — correctly: theme.css leaves a permanent transform on <body>, which made
   <body> the containing block for the fixed-position scrim and put Confirm
   below the fold on every phone. But #lag-root is now a SIBLING of <body>,
   so a `body >` child selector matches nothing, and all 42 rules in this
   block died at once — the gate fell back to the engine default inside a
   container this file had styled for a look it could no longer reach.
   `html #lag-root` is the same 1-0-1 and is mount-agnostic: it holds whether
   the root is parented to <html> or back on <body>. The custom properties it
   reads (--ca-*, --sf-*) are declared on :root, so they still inherit. */
html #lag-root {
  /* Paper, not a blur over the page: the page behind is `visibility: hidden`
     until the gate closes anyway, so the frosted backdrop was blurring
     nothing. A soft light field, brand-tinted at the top, is the ground. */
  background:
    radial-gradient(120% 70% at 50% -10%, color-mix(in srgb, var(--sf-accent-1) 34%, transparent), transparent 60%),
    linear-gradient(180deg, var(--sf-surface) 0%, var(--sf-bg) 100%);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  /* Column flex, centred by AUTO MARGINS on the first and last items rather
     than by justify-content: center — a centred column that outgrows a short
     viewport (landscape phone) clips its top unreachably inside the scroll
     container; auto margins collapse to zero first and keep everything
     scrollable. The bottom padding is the hanging exit line's room. */
  flex-direction: column; justify-content: flex-start; align-items: center;
  gap: clamp(1rem, 2.4vh, 1.5rem);
  padding: clamp(1.25rem, 4vh, 2.5rem) 1.25rem clamp(3.25rem, 8vh, 4.5rem);
  font-family: var(--sf-font-body);
  color: var(--sf-text);
  isolation: isolate;
}
/* The field: our vials, tumbling softly around the card. Placed by catharos.js
   from the asset map (never a guessed path); this only draws them. Behind
   everything (z-index 0 under the lockup and card at 1), inert, and — unlike
   the mid-page floaters — SHOWN under reduced motion, just held still: here
   they are the composition, not an ambient extra. Hidden on narrow screens
   where the card owns the whole width and glass under a checkbox is clutter. */
#lag-root .gate-field { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
#lag-root .gate-vial {
  position: absolute; display: block; height: auto; max-width: none;
  width: var(--g-w, 9rem);
  opacity: var(--g-op, 1);
  filter: saturate(.94) contrast(1.02) drop-shadow(0 22px 30px rgb(var(--sf-shadow) / .16));
  transform: rotate(var(--f-rot-a, 0deg));
  will-change: transform;
}
@media (prefers-reduced-motion: no-preference) {
  #lag-root .gate-vial { animation: ca-hover var(--f-dur, 34s) ease-in-out var(--f-delay, 0s) infinite; }
}
@media (max-width: 48rem) { #lag-root .gate-field { display: none; } }

/* The lockup stands ABOVE the card, on the field, the way a letterhead sits
   above a form. catharos.js moves the engine's own `.lag-logo` (LOGO.webp) up
   here, so there is one logo, not two, and it is not inverted — the engine's
   default filter exists for a dark header this gate no longer has. */
#lag-root .gate-brand { position: relative; z-index: 1; display: flex; justify-content: center; margin: auto 0 0; }
#lag-root .gate-brand .lag-logo { height: clamp(4.25rem, 11vh, 6rem); width: auto; margin: 0; filter: none; position: static; }

/* The card. White (elevated), a hairline, the ink rule the candor block and
   the definition plate carry, and a shadow with real weight. `overflow:
   visible` so the exit link can hang BELOW the card while staying inside the
   dialog's DOM for assistive tech (aria-modal hides everything else). */
#lag-root .lag-modal {
  position: relative; z-index: 1;
  max-width: 30rem; margin: 0 0 auto;
  background: var(--sf-elevated);
  border: 1px solid var(--ca-line);
  border-top: 3px solid var(--ca-ink);
  border-radius: var(--sf-radius, 8px);
  overflow: visible;
  box-shadow: 0 1px 0 color-mix(in srgb, var(--ca-ink) 6%, transparent),
              0 24px 60px -18px rgb(var(--sf-shadow) / .28),
              0 8px 22px -10px rgb(var(--sf-shadow) / .16);
}
/* The engine's dark, particle-lit header is retired: catharos.js removes its
   canvas and overlay and moves the logo out, and this makes what remains a
   plain title band on the card. */
#lag-root .lag-head { padding: clamp(1.4rem, 3.5vh, 2rem) clamp(1.25rem, 4vw, 2rem) 0; background: none; overflow: visible; }
#lag-root .lag-head-canvas, #lag-root .lag-head-overlay { display: none; }
#lag-root .lag-title {
  font-family: var(--sf-font-display); color: var(--ca-ink);
  font-size: clamp(1.45rem, 1.2rem + 1vw, 1.8rem); font-weight: 600; line-height: 1.12;
  letter-spacing: -0.017em; margin: 0;
}
#lag-root .lag-title span, #lag-root .lag-title strong { display: inline; font: inherit; color: inherit; }
#lag-root .gate-eyebrow {
  display: block; margin: 0 0 .55rem;
  font-family: var(--sf-font-display); font-size: .7rem; font-weight: 600;
  letter-spacing: .15em; text-transform: uppercase; color: var(--sf-muted);
}
#lag-root .lag-body { padding: .9rem clamp(1.25rem, 4vw, 2rem) clamp(1.4rem, 3.5vh, 2rem); }
#lag-root .lag-intro { margin: 0 0 1.1rem; font-size: .95rem; line-height: 1.6; color: var(--sf-muted); max-width: none; }

/* Each affirmation is a boxed FIELD, the way an assay sheet boxes the line a
   signatory initials: hairline border, page-colour ground, and the whole row
   is the hit target. Ticked, the border takes the ink and the box fills. */
#lag-root .lag-check {
  display: flex; align-items: flex-start; gap: .85rem;
  margin: 0 0 .7rem; padding: .85rem 1rem;
  border: 1px solid var(--ca-line); border-radius: var(--sf-radius, 8px);
  background: var(--sf-surface); cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
#lag-root .lag-check:hover { border-color: var(--ca-line-strong); background: var(--sf-elevated); }
#lag-root .lag-check:has(.lag-cb:checked) { border-color: var(--ca-ink); background: var(--sf-elevated); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ca-ink) 8%, transparent); }
#lag-root .lag-box {
  width: 1.3rem; height: 1.3rem; margin-top: .05rem;
  border: 1.5px solid var(--ca-line-strong); border-radius: 4px; background: var(--sf-elevated);
}
#lag-root .lag-check:hover .lag-box { border-color: var(--ca-ink); }
#lag-root .lag-cb:checked + .lag-box { background: var(--ca-ink); border-color: var(--ca-ink); }
#lag-root .lag-cb:focus-visible + .lag-box { outline: 2px solid var(--sf-accent); outline-offset: 2px; }
#lag-root .lag-txt { font-size: .93rem; line-height: 1.5; color: var(--sf-text); }
#lag-root .lag-txt strong { color: var(--ca-ink); font-weight: 700; }
#lag-root .lag-divider { display: none; }

/* The enter control. Asleep it is a pale plate; awake it is the ink. */
#lag-root .lag-btn-primary {
  margin-top: .5rem; padding: .95rem 1rem;
  border-radius: 999px; font-family: var(--sf-font-body); font-size: .95rem; font-weight: 700;
  background: color-mix(in srgb, var(--ca-ink) 10%, var(--sf-surface)); color: color-mix(in srgb, var(--ca-ink) 45%, var(--sf-muted));
  box-shadow: none; transition: background-color .18s ease, color .18s ease, transform .2s ease, box-shadow .18s ease;
}
#lag-root .lag-btn-primary.ready {
  background: var(--ca-ink); color: var(--sf-accent-text, #fff);
  box-shadow: 0 12px 26px -12px rgb(var(--sf-shadow) / .55);
}
#lag-root .lag-btn-primary.ready:hover { background: var(--sf-accent); transform: translateY(-1px); }
#lag-root .lag-btn-primary.ready:active { transform: none; }
/* Fine print, inside the card, under the control — the affirmation restated
   as a sentence with its negations in the same breath (copy/compliance/RULES §3.2). */
#lag-root .gate-fine { margin: 1rem 0 0; font-size: .74rem; line-height: 1.55; color: var(--sf-muted); max-width: none; }
#lag-root .gate-fine a { color: var(--sf-accent); font-weight: 600; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--sf-accent) 35%, transparent); }
#lag-root .gate-fine a:hover { border-bottom-color: var(--sf-accent); }
/* The way out hangs below the card, quiet: a sentence with a link in it. */
#lag-root .lag-btn-secondary {
  position: absolute; left: 0; right: 0; top: 100%; margin: 1rem auto 0;
  width: auto; padding: .35rem .6rem;
  border: 0; background: none; box-shadow: none;
  font-family: var(--sf-font-body); font-size: .82rem; font-weight: 500; color: var(--sf-muted);
  text-align: center;
}
#lag-root .lag-btn-secondary strong { color: var(--ca-ink); font-weight: 600; text-decoration: underline; text-underline-offset: .18em; }
#lag-root .lag-btn-secondary:hover { color: var(--sf-text); background: none; }
#lag-root .lag-btn-secondary:hover strong { color: var(--sf-accent); }
/* Room for the hanging exit line on short screens: the modal's own margin
   cannot grow past the flex container's padding, so the container gives it. */
html #lag-root { padding-bottom: clamp(3.25rem, 8vh, 4.5rem); }
@media (max-width: 520px) {
  #lag-root .lag-head { padding: 1.4rem 1.25rem 0; }
  #lag-root .lag-body { padding: .8rem 1.25rem 1.4rem; }
}

/* --- documents: legal pages + 404 ---------------------------------------
   The build re-shells every master-emitted document page in this site's chrome
   (factory/build.mjs → redressDocPages), dropping the rendered legal body into
   `.prose`. So the document set is styled HERE, from the same tokens as every
   other page, and the whole rendered vocabulary has to be covered — h2/h3, p,
   ul/ol, strong, links, rules, tables — because nobody hand-edits these pages.
   (2026-08-15: before this, privacy/terms/shipping/returns/RUO shipped in the
   master's tailwind dress inside an otherwise all-Catharos site.) */
.doc-sheet { padding-block: clamp(2.5rem, 6vw, 4.5rem) var(--ca-seam); }
.doc-head { margin-bottom: clamp(1.6rem, 4vw, 2.4rem); }
.doc-head h1 { margin-bottom: .4rem; }
.doc-updated { font-family: var(--sf-font-display); font-size: .88rem; margin: 0; }
.doc-lede { max-width: 52ch; }
.doc-actions { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 1.9rem; }

.prose { max-width: var(--ca-measure); overflow-wrap: break-word; }
.prose > :first-child { margin-top: 0; }
.prose h2 { font-size: clamp(1.18rem, 1.05rem + .6vw, 1.5rem); margin: 2.1em 0 .5em; }
.prose h3 { font-size: clamp(1.02rem, .97rem + .3vw, 1.16rem); margin: 1.7em 0 .45em; }
.prose p, .prose li { font-size: .97rem; line-height: 1.75; }
.prose ul, .prose ol { margin: 0 0 1.05em; padding-left: 1.3rem; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li { margin-bottom: .38em; }
.prose li::marker { color: var(--sf-accent); }
.prose strong { color: var(--ca-ink); font-weight: 600; }
.prose a { text-decoration: underline; }
.prose hr { border: 0; border-top: 1px solid var(--ca-line); margin: 2.2em 0; }
.prose table { width: 100%; border-collapse: collapse; margin: 0 0 1.2em; font-size: .92rem; }
.prose th, .prose td { border: 1px solid var(--ca-line); padding: .55rem .7rem; text-align: left; vertical-align: top; }
.prose th { font-family: var(--sf-font-display); font-weight: 600; color: var(--ca-ink); background: var(--ca-wash); }
/* A rate table must not push the page sideways on a phone. */
@media (max-width: 640px) { .prose table { display: block; overflow-x: auto; } }

/* --- print: a lab will print these pages -------------------------------- */
@media print {
  .snav, #mnav, #cart-overlay, #cart-panel, .foot nav, .hero-cta { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  h1, h2, h3 { color: #000; }
  .candor, .plaque, .panel { border-color: #999; background: none; }
  /* The footer is navy on screen as of round 6, and a printer drops background
     colour by default — which would have left #EDEFFA text on white paper, i.e.
     the RUO disclaimer and the FDA paragraph printing INVISIBLE on the pages a
     lab is most likely to print. Back to ink on paper here. */
  .foot { background: none !important; color: #000; border-top-color: #999; }
  .foot a, .foot .foot-h, .foot .muted, .foot-fine, .foot-fine strong { color: #000; }
  .foot-fine { border-top-color: #999; }
  .foot .snav-logo img { filter: none; }
}


/* --- PDP: commonly ordered alongside -------------------------------------
   OWNER DECISION 2026-08-18. Sits between the add-to-cart row and the stock
   line / waitlist, inside the order fieldset, because it is part of the order
   decision and not a browsing surface — the related-products strip further
   down the page is the browsing surface and stays where it is.

   Each row adds ONE unit and does not touch the quantity stepper: the stepper
   belongs to the item on the page. Rows are <button>, not <a>: they add to the
   cart rather than navigate, and a link that silently adds to a cart is the
   pattern that makes people distrust a checkout. */
.upsell { margin-top: 1rem; border-top: 1px solid var(--ca-line); padding-top: .9rem; }
.upsell-head {
  font-family: var(--sf-font-display); font-size: .68rem; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--sf-muted);
  margin-bottom: .6rem;
}
.upsell-list { display: flex; flex-direction: column; gap: .45rem; }
.upsell-row {
  display: flex; align-items: center; gap: .7rem; width: 100%; text-align: left;
  padding: .5rem .6rem; border: 1px solid var(--ca-line-strong);
  border-radius: calc(var(--sf-radius) * .55);
  background: var(--sf-elevated); color: inherit; cursor: pointer;
  font: inherit;
  transition: border-color .16s ease, background-color .16s ease;
}
.upsell-row:hover, .upsell-row:focus-visible { border-color: var(--sf-accent); background: var(--sf-surface); }
.upsell-row[disabled] { opacity: .55; cursor: default; }
.upsell-fig {
  flex: none; width: 2.9rem; height: 2.9rem; border-radius: calc(var(--sf-radius) * .4);
  background: var(--ca-photo-bg); display: grid; place-items: center; overflow: hidden;
}
.upsell-fig img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
.upsell-body { flex: 1 1 auto; min-width: 0; }
.upsell-name {
  display: block; font-family: var(--sf-font-display); font-weight: 600;
  font-size: .92rem; color: var(--ca-ink); line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.upsell-meta { display: block; font-size: .76rem; color: var(--sf-muted); margin-top: .1rem; }
.upsell-add {
  flex: none; display: flex; align-items: center; gap: .35rem;
  font-family: var(--sf-font-display); font-weight: 600; font-size: .82rem;
  color: var(--sf-accent);
}
.upsell-row[disabled] .upsell-add { color: var(--sf-muted); }
.upsell-price { font-variant-numeric: tabular-nums; }
@media (max-width: 30rem) {
  .upsell-row { gap: .55rem; padding: .45rem .5rem; }
  .upsell-add span:not(.upsell-price) { display: none; }
}

}
