/* ============================================================
   HALOTIR LABS · Design System v6 "Warm lab"
   ------------------------------------------------------------
   See design-system/MASTER.md for the reasoning behind every
   rule below. The short version:

     Warm tinted surfaces, not white. Fraunces italic for
     display accents, sans for everything that has to be read.
     Motion is a family, not an effect: one easing curve, one
     float, one reveal. Mobile first, always.

   The v4 "Swiss" rules this file used to open with are gone.
   That system banned serifs and shadows; v6 uses both
   deliberately. Tokens start at :root below, and the v6 layer
   begins at the tint block further down.
   ============================================================ */

:root {
  /* Ink */
  --ink: #1c1917;
  --ink-2: #57534e;
  --muted: #78716c;

  /* Ground */
  --bg: #ffffff;
  --tint: #fafaf9;
  --panel: #f5f5f4;
  --panel-2: #f5f5f4;
  --line: #e7e5e4;
  --line-2: #d6d3d1;

  /* Accent (4.9:1 on white) */
  --accent: #a16207;
  --accent-deep: #854d0e;
  --accent-tint: #fdf8ee;
  --accent-line: #efe0bf;
  --gold-foil: linear-gradient(90deg, #a5711f, #e3c085 22%, #c08a3a 50%, #e3c085 78%, #a5711f);

  --radius: 20px;
  --radius-sm: 12px;
  --gutter: 24px;
  --header-h: 64px;

  --font: "Anek Latin", system-ui, -apple-system, sans-serif;
  --font-num: "Poppins", system-ui, sans-serif;
}

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
a, button, input, select, textarea, label { touch-action: manipulation; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
body.gate-locked { overflow: hidden; }

img, svg { max-width: 100%; display: block; }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-deep); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

h1, h2, h3, h4 { font-family: var(--font); font-weight: 600; line-height: 1.15; color: var(--ink); }
h1 { font-size: 2.25rem; letter-spacing: -0.02em; }
h2 { font-size: 1.6rem; letter-spacing: -0.01em; }
h3 { font-size: 1.05rem; }
p { color: var(--ink-2); }
strong { font-weight: 600; color: var(--ink); }
.num { font-family: var(--font-num); }

.wrap { width: 100%; max-width: 1440px; margin: 0 auto; padding: 0 var(--gutter); }
.narrow { max-width: 760px; }

/* ---------- Announcement ---------- */
.announce {
  background: var(--accent-tint);
  border-bottom: 1px solid var(--accent-line);
  color: var(--accent-deep);
  text-align: center;
  font-size: 0.8rem;
  line-height: 1.45;
  padding: 10px 16px;
}

/* ---------- Header ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.header-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: var(--header-h); }

.logo { display: flex; align-items: center; min-height: 44px; gap: 9px; color: var(--ink); }
.logo:hover { color: var(--ink); }
.logo-mark { width: 26px; height: 26px; flex-shrink: 0; }
.logo-word {
  display: flex; align-items: baseline; gap: 7px;
  font-size: 1.1rem; font-weight: 600; letter-spacing: -0.01em; line-height: 1; white-space: nowrap;
}
.logo-word em { font-style: normal; font-weight: 400; font-size: 0.72rem; color: var(--muted); }
.logo-light { color: #fff; }
.logo-light:hover { color: #fff; }
.logo-light .logo-word em { color: var(--accent); }

/* wp_nav_menu emits bare <li> (items_wrap '%3$s'), which browsers render with
   list markers. .main-nav is the flex container, so the items just need their
   markers removed. */
.main-nav li { list-style: none; }
.main-nav { display: none; }
.header-actions { display: flex; align-items: center; gap: 8px; }

.icon-btn {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  border: none; background: none; color: var(--ink); cursor: pointer;
}
.icon-btn svg { width: 22px; height: 22px; }
.cart-count {
  display: none;
  position: absolute; top: 4px; right: 3px;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 9px;
  background: var(--ink); color: #fff;
  font-family: var(--font-num); font-size: 0.62rem; font-weight: 500;
  align-items: center; justify-content: center;
}
.cart-count.has-items { display: flex; }
.menu-btn { flex-direction: column; gap: 5px; }
.menu-btn span { display: block; width: 18px; height: 1.5px; background: var(--ink); border-radius: 2px; transition: transform .2s, opacity .2s; }
.menu-btn.open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.menu-btn.open span:nth-child(2) { opacity: 0; }
.menu-btn.open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 12px 26px;
  font-family: var(--font); font-size: 0.95rem; font-weight: 500;
  border: 1px solid transparent; border-radius: 999px;
  cursor: pointer; transition: background .18s, color .18s, border-color .18s;
}
.btn-primary { background: var(--ink); color: #fff; }
.btn-primary:hover { background: #000; color: #fff; }
.btn-primary:disabled { background: var(--line); color: var(--muted); cursor: not-allowed; }
.btn-outline { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-outline:hover { border-color: var(--ink); color: var(--ink); }
.btn-block { width: 100%; }
.btn-sm { min-height: 44px; padding: 10px 18px; font-size: 0.88rem; }

.text-link { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; font-weight: 500; color: var(--accent); }
.text-link::after { content: "→"; }

/* ---------- Section furniture ---------- */
.section { padding: 48px 0; }
.section-tint { background: var(--tint); }
.section-head { margin-bottom: 24px; max-width: 62ch; }
.section-head p { margin-top: 8px; color: var(--muted); }
.eyebrow { display: block; font-size: 0.85rem; font-weight: 500; color: var(--accent); margin-bottom: 8px; }
.center { text-align: center; margin-left: auto; margin-right: auto; }

/* ---------- Hero ---------- */
.hero { border-bottom: 1px solid var(--line); }
.hero-inner { display: grid; gap: 28px; padding-block: 32px 40px; }
.hero h1 { font-size: 2.4rem; margin-bottom: 14px; }
.hero .lede { font-size: 1.05rem; max-width: 46ch; margin-bottom: 24px; }
.hero-ctas { display: flex; flex-wrap: wrap; gap: 12px; }
.hero-points { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 24px; }
.hero-point { display: flex; align-items: center; gap: 7px; font-size: 0.88rem; color: var(--ink-2); }
.hero-point svg { width: 16px; height: 16px; color: var(--ink-2); flex-shrink: 0; }
.hero-visual { background: var(--panel); border-radius: var(--radius); padding: 28px; display: flex; align-items: center; justify-content: center; aspect-ratio: 4 / 3; min-width: 0; }
/* The vial art is portrait and the panel is landscape, so size the drawing
   off the panel's height. Sizing it off the width made the SVG taller than
   the box, which inflated the grid track and pushed the page into
   horizontal scroll on phones. */
.hero-visual .vial { width: auto; height: 100%; max-height: 100%; max-width: min(76%, 290px); }

/* ============================================================
   PRODUCT MEDIA
   A neutral vial placeholder until photography exists. Swap by
   adding `image:` to a product in js/products.js.
   ============================================================ */
.product-media {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--panel);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
.product-media .vial { width: auto; height: 100%; max-height: 100%; max-width: min(62%, 168px); transition: transform .3s ease; }
.product-media img.photo { width: 100%; height: 100%; max-width: none; object-fit: cover; }

/* ---------- Product grid ---------- */
.product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.product-card {
  display: flex; flex-direction: column;
  background: var(--panel);
  border-radius: var(--radius);
  overflow: hidden;
  color: var(--ink);
}
.product-card:hover { color: var(--ink); }
.product-card:hover .vial { transform: scale(1.06); }
.card-body { background: transparent; padding: 12px; display: flex; flex-direction: column; flex: 1; }
.card-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.card-row h3 { font-size: 1rem; font-weight: 600; line-height: 1.3; }
.card-price { font-family: var(--font-num); font-size: 0.92rem; font-weight: 400; color: var(--ink-2); white-space: nowrap; }
.card-meta { font-size: 0.8rem; color: var(--muted); margin-top: 2px; }

/* ---------- Feature cards ---------- */
.feature-grid { display: grid; gap: 12px; }
.feature-card { background: var(--panel-2); border-radius: var(--radius); padding: 24px; }
.feature-card svg { width: 24px; height: 24px; color: var(--accent); margin-bottom: 12px; stroke-width: 1.6; }
.feature-card h3 { margin-bottom: 6px; }
.feature-card p { font-size: 0.92rem; color: var(--muted); }

/* ---------- Stat row ---------- */
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 24px; }
.stat { text-align: center; padding: 20px 8px; background: var(--panel-2); border-radius: var(--radius-sm); }
.stat-v { display: block; font-family: var(--font-num); font-size: 1.45rem; font-weight: 500; color: var(--ink); }
.stat-k { display: block; margin-top: 2px; font-size: 0.78rem; color: var(--muted); }

/* ---------- Checklist ---------- */
.check-list { list-style: none; }
.check-list li { position: relative; padding-left: 28px; margin-bottom: 10px; color: var(--ink-2); }
.check-list li::before {
  content: ""; position: absolute; left: 0; top: 6px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent-tint); border: 1px solid var(--accent-line);
}
.check-list li::after {
  content: ""; position: absolute; left: 5px; top: 10px;
  width: 6px; height: 3px;
  border-left: 1.5px solid var(--accent); border-bottom: 1.5px solid var(--accent);
  transform: rotate(-45deg);
}

/* ---------- Store page ---------- */
.page-head { padding-block: 28px 20px; }
.page-head h1 { margin-bottom: 8px; }
.page-head p { color: var(--muted); max-width: 62ch; }

.store-tools { display: grid; gap: 10px; padding-bottom: 16px; }
.search-field { position: relative; }
.search-field svg { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--muted); }
.search-field input {
  width: 100%; min-height: 46px; padding: 10px 18px 10px 44px;
  font-family: var(--font); font-size: 16px; color: var(--ink);
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 999px;
}
.search-field input:focus { outline: none; border-color: var(--ink); }
.sort-field { display: flex; align-items: center; gap: 8px; }
.sort-field label { font-size: 0.88rem; color: var(--muted); white-space: nowrap; }
.sort-field select {
  flex: 1; min-height: 46px; padding: 10px 16px;
  font-family: var(--font); font-size: 16px; color: var(--ink);
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 999px;
}

.chip-row {
  display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none;
  margin: 0 calc(var(--gutter) * -1); padding: 4px var(--gutter) 16px;
}
.chip-row::-webkit-scrollbar { display: none; }
.chip {
  flex: 0 0 auto; min-height: 44px; padding: 10px 18px;
  font-family: var(--font); font-size: 0.9rem; font-weight: 500;
  color: var(--ink-2); background: var(--bg);
  border: 1px solid var(--line-2); border-radius: 999px;
  cursor: pointer; white-space: nowrap;
}
.chip:hover { border-color: var(--ink); color: var(--ink); }
.chip.active { background: var(--ink); border-color: var(--ink); color: #fff; }
.result-count { font-size: 0.88rem; color: var(--muted); padding-bottom: 12px; }
.no-results { padding: 48px 0; text-align: center; color: var(--muted); }

/* ---------- Product detail ---------- */
.ruo-banner {
  background: var(--ink); color: #fff;
  text-align: center; font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 10px 16px; line-height: 1.5;
}
.ruo-banner strong { color: #fbbf24; font-weight: 600; }

.pdp { display: grid; gap: 20px; padding-block: 20px 8px; }
.pdp > * { min-width: 0; }
.pdp-media { background: var(--panel); border-radius: var(--radius); padding: 32px; display: flex; align-items: center; justify-content: center; aspect-ratio: 4 / 3; }
.pdp-media .vial { width: auto; height: 100%; max-height: 100%; max-width: min(62%, 300px); }
.pdp-info { display: flex; flex-direction: column; }
.pdp-cat { font-size: 0.85rem; color: var(--muted); }
.pdp h1 { font-size: 1.9rem; margin: 6px 0 10px; }
.pdp-tagline { font-size: 0.98rem; margin-bottom: 14px; }
.pdp-verify { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.85rem; color: var(--ink-2); margin-bottom: 18px; }
.pdp-verify span { display: flex; align-items: center; gap: 6px; }
.pdp-verify svg { width: 15px; height: 15px; color: var(--ink-2); flex-shrink: 0; }

.price-row { display: flex; align-items: baseline; gap: 10px; margin-bottom: 16px; }
.price { font-family: var(--font-num); font-size: 1.75rem; font-weight: 500; }
.price-note { font-size: 0.85rem; color: var(--muted); }

.buy-row { display: flex; gap: 10px; margin-bottom: 14px; }
.buy-row .btn { flex: 1; }
.qty {
  display: inline-flex; align-items: center; flex-shrink: 0;
  border: 1px solid var(--line-2); border-radius: 999px; overflow: hidden;
}
.qty button { width: 44px; height: 48px; background: none; border: none; font-size: 1.1rem; color: var(--ink); cursor: pointer; }
.qty input {
  width: 36px; border: none; text-align: center; background: transparent;
  font-family: var(--font-num); font-size: 16px; color: var(--ink); -moz-appearance: textfield;
}
.qty input::-webkit-outer-spin-button, .qty input::-webkit-inner-spin-button { -webkit-appearance: none; }

.pdp-note { font-size: 0.82rem; color: var(--muted); }

.trust-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 18px; padding: 14px; background: var(--panel-2); border-radius: var(--radius-sm); }
.trust-strip div { text-align: center; font-size: 0.74rem; color: var(--ink-2); line-height: 1.4; }
.trust-strip svg { width: 18px; height: 18px; color: var(--ink-2); margin: 0 auto 5px; stroke-width: 1.6; }

/* Certificate of Analysis
   Drawn from the physical vial label: cream stock, a gold foil rule at the
   top and bottom edge, a gold-outlined seal, and small uppercase technical
   type. The rules are pseudo-elements rather than borders so the card keeps
   the system radius; overflow clips them into the rounded corners. */
.coa-panel { margin-top: 28px; }
.coa-cert {
  position: relative; overflow: hidden; text-align: center;
  background: var(--accent-tint); border: 1px solid var(--accent-line);
  border-radius: var(--radius); padding: 30px 20px 26px;
}
.coa-cert::before, .coa-cert::after {
  content: ""; position: absolute; left: 0; right: 0; height: 2px; background: var(--gold-foil);
}
.coa-cert::before { top: 0; }
.coa-cert::after { bottom: 0; }

.coa-seal {
  display: inline-block; border: 1px solid var(--accent); border-radius: 999px;
  padding: 6px 16px; font-family: var(--font-num); font-size: 0.66rem; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-deep); line-height: 1.4;
}
.coa-cert h2 { font-size: 1.375rem; margin: 16px 0 8px; }
.coa-cert p { font-size: 0.92rem; color: var(--ink-2); max-width: 52ch; margin: 0 auto; }
.coa-cert .btn { margin-top: 20px; }

.coa-meta {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 32px;
  margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--accent-line);
}
.coa-meta dt { font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.coa-meta dd { font-family: var(--font-num); font-size: 0.9rem; font-weight: 500; color: var(--ink); margin-top: 3px; }
.coa-fine { font-size: 0.78rem; color: var(--muted); margin-top: 14px; }

/* Catalog reference list on the COA page. A list, not a table: there is no
   longer a grid of results to compare, only a name and a catalog number. */
.coa-list { border-top: 1px solid var(--line); margin-top: 20px; }
.coa-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 6px 0; border-bottom: 1px solid var(--line);
}
.coa-item-name { font-weight: 500; font-size: 0.95rem; line-height: 1.3; }
.coa-item-sub { font-family: var(--font-num); font-size: 0.76rem; color: var(--muted); margin-top: 3px; }
.coa-item a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 0.86rem; font-weight: 500; white-space: nowrap;
}

/* Numbered request steps. Selectors are scoped to .prose so its generic
   list indents and margins do not fight the counter layout. */
.step-list, .prose ol.step-list {
  list-style: none; counter-reset: step; display: grid; gap: 18px;
  margin: 18px 0 0; padding-left: 0;
}
.step-list li, .prose .step-list li {
  position: relative; padding-left: 44px; margin-bottom: 0;
  font-size: 0.94rem; color: var(--ink-2);
}
.prose ul.check-list { padding-left: 0; }
.step-list li::before {
  counter-increment: step; content: counter(step);
  position: absolute; left: 0; top: -1px;
  width: 30px; height: 30px; border-radius: 999px;
  border: 1px solid var(--accent-line); background: var(--accent-tint); color: var(--accent-deep);
  font-family: var(--font-num); font-size: 0.82rem; font-weight: 500;
  display: flex; align-items: center; justify-content: center;
}
.step-list strong { color: var(--ink); font-weight: 500; }

/* ============================================================
   PDP: class eyebrow, identifier chips, mass + bundle selectors
   ============================================================ */
.pdp-eyebrow {
  display: block; font-style: italic; font-size: 0.88rem;
  color: var(--muted); margin-bottom: 4px;
}
.pdp-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 14px; }
.id-chip {
  font-family: var(--font-num); font-size: 0.72rem; color: var(--ink-2);
  background: var(--tint); border: 1px solid var(--line);
  border-radius: 999px; padding: 5px 12px; white-space: nowrap;
}

.mass-row { display: flex; align-items: center; gap: 14px; margin: 16px 0 4px; }
.opt-label { font-size: 0.85rem; font-weight: 500; color: var(--ink); min-width: 56px; }
.mass-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.mass-opt {
  min-height: 44px; padding: 10px 18px; cursor: pointer;
  font-family: var(--font-num); font-size: 0.85rem; font-weight: 500;
  color: var(--ink); background: var(--bg);
  border: 1px solid var(--line-2); border-radius: var(--radius-sm);
  transition: background .15s, color .15s, border-color .15s;
}
.mass-opt:hover { border-color: var(--ink); }
.mass-opt.active { background: var(--ink); color: #fff; border-color: var(--ink); }

/* Price line, restyled when a bundle tier is chosen. */
.price-row .was { font-size: 1rem; color: var(--muted); text-decoration: line-through; margin-right: 10px; }
.price-row .bundle-flag {
  display: block; font-family: var(--font-num); font-size: 0.62rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-deep);
}
.price-row .now { font-size: 2rem; font-weight: 600; color: var(--ink); }

.bundle-box { margin-top: 20px; }
.bundle-title {
  display: block; font-family: var(--font-num); font-size: 0.66rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px;
}
.bundle-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.bundle-card {
  position: relative; cursor: pointer; text-align: left;
  display: flex; flex-direction: column; gap: 4px;
  padding: 16px 14px 14px; min-height: 88px;
  background: var(--bg); border: 1.5px solid var(--line);
  border-radius: var(--radius-sm);
  transition: border-color .15s, background .15s;
}
.bundle-card:hover { border-color: var(--line-2); }
.bundle-card.active { border-color: var(--accent); background: var(--accent-tint); }
.bundle-badge {
  position: absolute; top: -9px; left: 12px;
  font-family: var(--font-num); font-size: 0.58rem; font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  background: var(--ink); color: #fff;
}
.bundle-badge.b1 { background: var(--accent); }
.bundle-badge.b2 { background: var(--accent-deep); }
.bundle-vials { display: flex; gap: 3px; margin-bottom: 2px; }
.bundle-vials i {
  width: 9px; height: 18px; border-radius: 2px 2px 3px 3px;
  background: var(--panel); border: 1px solid var(--line-2);
}
.bundle-card.active .bundle-vials i { border-color: var(--accent-line); background: #fff; }
.bundle-qty { font-size: 0.9rem; font-weight: 600; color: var(--ink); }
.bundle-off { font-family: var(--font-num); font-size: 0.76rem; color: var(--accent-deep); }
.bundle-card:not(.active) .bundle-off { color: var(--muted); }
.bundle-note { font-size: 0.76rem; color: var(--muted); margin-top: 12px; }

/* Dispatch cutoff strip. Green reads as "go" without importing a second
   accent: it is semantic status, not brand colour. */
.dispatch {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 16px; padding: 12px 14px;
  background: #f0f7f1; border: 1px solid #cfe3d3; border-radius: var(--radius-sm);
}
.dispatch-dot {
  width: 8px; height: 8px; border-radius: 50%; background: #15662f;
  flex-shrink: 0; margin-top: 7px;
}
.dispatch-main { font-size: 0.86rem; font-weight: 500; color: #14532d; margin: 0; }
.dispatch-sub { font-size: 0.74rem; color: #3f6b4c; margin: 2px 0 0; }

/* CoA button sits beside Add to cart. */
.buy-row .coa-btn, .coa-btn {
  flex: 0 0 auto; min-width: 84px; padding-inline: 20px;
}
@media (prefers-reduced-motion: reduce) { .dispatch { transition: none; } }

@media (min-width: 640px) {
  .bundle-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1024px) {
  .bundle-grid { grid-template-columns: repeat(5, 1fr); }
}

/* Detail sections */
.pdp-sections { padding-bottom: 32px; }
.pdp-sections section { padding: 28px 0; border-top: 1px solid var(--line); }
.pdp-sections h2 { font-size: 1.5rem; margin-bottom: 12px; }
.pdp-sections p, .pdp-sections .check-list, .pdp-sections .notice { max-width: 70ch; }
.spec-table, .coa-panel { max-width: 760px; }
.pdp-sections p { margin-bottom: 12px; }
.fine { font-size: 0.82rem; color: var(--muted); }

.spec-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.spec-table th, .spec-table td { text-align: left; padding: 11px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.spec-table th { width: 44%; font-weight: 400; color: var(--muted); padding-right: 12px; }
.spec-table td { font-family: var(--font-num); font-size: 0.86rem; color: var(--ink-2); }

.notice {
  background: var(--accent-tint); border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm); padding: 16px 18px;
  font-size: 0.86rem; color: #6b4708; line-height: 1.6;
}
.notice strong { color: #533606; }

/* Sticky mobile buy bar */
.buy-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: none; align-items: center; gap: 12px;
  background: var(--bg); border-top: 1px solid var(--line);
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
  box-shadow: 0 -4px 20px rgba(0,0,0,.06);
  transform: translateY(110%); transition: transform .28s ease;
}
.buy-bar.show { transform: translateY(0); }
.buy-bar .bb-info { flex: 1; min-width: 0; }
.buy-bar .bb-name { font-size: 0.92rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.buy-bar .bb-price { font-family: var(--font-num); font-size: 0.82rem; color: var(--muted); }
body.has-buy-bar { padding-bottom: 76px; }

/* ---------- Cart ---------- */
.cart-wrap { display: grid; gap: 24px; padding-block: 20px 56px; }
.cart-row { display: grid; grid-template-columns: 84px 1fr; grid-template-areas: "media info" "media qty"; gap: 4px 14px; padding: 16px 0; border-bottom: 1px solid var(--line); position: relative; }
.cart-media { grid-area: media; width: 84px; aspect-ratio: 1; background: var(--panel); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; }
.cart-media .vial { width: 58%; }
.cart-info { grid-area: info; padding-right: 36px; }
.cart-info h3 { font-size: 0.98rem; }
.cart-info h3 a { color: var(--ink); }
.cart-info .card-meta { margin-top: 2px; }
.cart-row .qty { grid-area: qty; justify-self: start; margin-top: 8px; }
.cart-row .qty button { height: 44px; width: 42px; }
.cart-line-price { position: absolute; right: 0; bottom: 22px; font-family: var(--font-num); font-weight: 500; }
.cart-remove { position: absolute; top: 12px; right: -8px; width: 44px; height: 44px; background: none; border: none; color: var(--muted); font-size: 1.2rem; cursor: pointer; }
.cart-remove:hover { color: var(--ink); }

.cart-summary { background: var(--panel-2); border-radius: var(--radius); padding: 22px; }
.cart-summary h2 { font-size: 1.15rem; margin-bottom: 14px; }
.sum-row { display: flex; justify-content: space-between; font-size: 0.92rem; padding: 9px 0; border-bottom: 1px solid var(--line); }
.sum-row.total { font-weight: 600; font-size: 1.05rem; border-bottom: none; padding-top: 14px; }
.sum-row .num { font-family: var(--font-num); }
.ship-note { font-size: 0.85rem; color: var(--muted); margin: 12px 0; }
.ship-note.ok { color: var(--accent-deep); }
.empty-state { text-align: center; padding: 60px 0 80px; }
.empty-state p { margin: 10px 0 22px; color: var(--muted); }

/* ---------- Attestation checkbox ---------- */
.attest {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px; margin: 14px 0;
  background: var(--bg); border: 1px solid var(--line-2); border-radius: var(--radius-sm);
  font-size: 0.86rem; line-height: 1.5; cursor: pointer;
}
.attest:hover { border-color: var(--accent); }
.attest input { margin-top: 2px; width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }

/* ---------- FAQ ---------- */
.faq-item { border-bottom: 1px solid var(--line); }
.faq-item:first-of-type { border-top: 1px solid var(--line); }
.faq-q {
  width: 100%; min-height: 56px; padding: 16px 0;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: none; border: none; text-align: left; cursor: pointer;
  font-family: var(--font); font-size: 1rem; font-weight: 500; color: var(--ink);
}
.faq-q::after { content: "+"; font-size: 1.3rem; font-weight: 300; color: var(--accent); flex-shrink: 0; transition: transform .2s; }
.faq-item.open .faq-q::after { transform: rotate(45deg); }
.faq-a { display: none; padding-bottom: 18px; font-size: 0.94rem; color: var(--ink-2); max-width: 70ch; }
.faq-item.open .faq-a { display: block; }

/* ---------- Prose ---------- */
.prose { padding-block: 28px 56px; }
.prose > * { max-width: 70ch; }
.prose h2 { font-size: 1.3rem; margin: 30px 0 10px; }
.prose h3 { margin: 22px 0 8px; }
.prose p, .prose li { color: var(--ink-2); margin-bottom: 12px; }
.prose ul, .prose ol { padding-left: 20px; margin-bottom: 16px; }
.prose li { margin-bottom: 8px; }
.prose li::marker { color: var(--accent); }
.prose .callout { background: var(--accent-tint); border: 1px solid var(--accent-line); border-radius: var(--radius-sm); padding: 16px 18px; color: #6b4708; margin: 20px 0; }
.prose-meta { display: block; font-size: 0.82rem; color: var(--muted); }

/* ---------- Tables ---------- */

/* ---------- Newsletter / closing band ---------- */
.closing { background: var(--tint); padding: 48px 0; text-align: center; }
.closing h2 { margin-bottom: 8px; }
.closing p { color: var(--muted); max-width: 46ch; margin: 0 auto 22px; }
.subscribe { display: grid; gap: 10px; max-width: 460px; margin: 0 auto; }
.subscribe input {
  min-height: 48px; padding: 12px 20px;
  font-family: var(--font); font-size: 16px; color: var(--ink);
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 999px;
}
.subscribe input:focus { outline: none; border-color: var(--ink); }

/* ---------- Contact ---------- */
.contact-grid { display: grid; gap: 20px; padding-block: 24px 56px; }
.contact-card { background: var(--panel-2); border-radius: var(--radius); padding: 22px; }
.contact-card h3 { margin-bottom: 6px; }
.contact-card p { font-size: 0.92rem; color: var(--muted); margin-bottom: 14px; }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 0.88rem; font-weight: 500; margin-bottom: 6px; }
.field input, .field select, .field textarea {
  width: 100%; min-height: 48px; padding: 12px 14px;
  font-family: var(--font); font-size: 16px; color: var(--ink);
  background: var(--bg); border: 1px solid var(--line-2); border-radius: var(--radius-sm);
}
.field textarea { min-height: 130px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--ink); }

/* ---------- Age gate ---------- */
.age-gate {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(28,25,23,.6); backdrop-filter: blur(6px);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 16px; overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.age-gate.closing-anim { opacity: 0; transition: opacity .25s; }
.gate-card { background: var(--bg); border-radius: var(--radius); max-width: 460px; width: 100%; margin: auto; padding: 28px 22px 22px; }
.gate-card .logo { justify-content: center; margin-bottom: 16px; }
.gate-card h2 { text-align: center; font-size: 1.35rem; margin-bottom: 6px; }
.gate-sub { text-align: center; font-size: 0.9rem; color: var(--muted); margin-bottom: 18px; }
.gate-card .btn-block { margin-top: 12px; }
.gate-exit { display: block; width: 100%; min-height: 44px; background: none; border: none; color: var(--muted); font-family: var(--font); font-size: 0.85rem; cursor: pointer; text-decoration: underline; margin-top: 6px; }
.gate-fine { font-size: 0.76rem; color: var(--muted); text-align: center; margin-top: 12px; line-height: 1.5; }

/* ---------- Toast ---------- */
.toast {
  position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%) translateY(14px);
  background: var(--ink); color: #fff; padding: 12px 22px; border-radius: 999px;
  font-size: 0.88rem; opacity: 0; pointer-events: none; transition: opacity .25s, transform .25s;
  z-index: 300; max-width: calc(100vw - 32px); text-align: center;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
body.has-buy-bar .toast { bottom: 90px; }

/* ---------- Footer ---------- */
.footer-contact { margin-bottom: 18px; font-size: 0.84rem; line-height: 1.6; color: #a8a29e; }
.footer-contact address { font-style: normal; margin-bottom: 6px; }
.footer-contact p { margin: 0; color: #a8a29e; }
.footer-contact a { color: #d6d3d1; }
.footer-contact a:hover { color: #fff; }

.site-footer { background: var(--ink); color: #a8a29e; padding: 40px 0 28px; border-top: 2px solid transparent; border-image: var(--gold-foil) 1; }
.footer-grid { display: grid; gap: 26px; padding-bottom: 26px; border-bottom: 1px solid rgba(255,255,255,.12); }
.footer-brand p { font-size: 0.88rem; color: #a8a29e; margin-top: 14px; max-width: 40ch; }
.site-footer h4 { font-size: 0.78rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #e3c085; margin-bottom: 10px; }
.site-footer a { display: block; color: #a8a29e; font-size: 0.9rem; padding: 11px 0; }
.site-footer a:hover { color: #fff; }
.footer-legal { padding-top: 22px; }
.footer-legal p { font-size: 0.78rem; line-height: 1.65; color: #8a8580; }
.footer-legal strong { color: #a8a29e; }
.footer-legal a { display: inline; color: #b3aca4; text-decoration: underline; padding: 0; }
.footer-copy { margin-top: 14px; font-size: 0.78rem; }

/* ============================================================
   BREAKPOINTS (additive)
   ============================================================ */
@media (min-width: 640px) {
  .product-grid { gap: 16px; }
  .feature-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }
  .subscribe { grid-template-columns: 1fr auto; }
  .contact-grid { grid-template-columns: 1fr 1fr; }
  .store-tools { grid-template-columns: 1fr auto; align-items: center; }
  .hero-inner { grid-template-columns: 1.05fr .95fr; align-items: center; gap: 32px; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .cart-row { grid-template-columns: 96px 1fr auto; grid-template-areas: "media info qty"; align-items: center; padding-right: 168px; }
  .cart-media { width: 96px; }
  .cart-row .qty { margin-top: 0; }
  .cart-line-price { top: 50%; bottom: auto; transform: translateY(-50%); right: 48px; }
  .cart-remove { top: 50%; transform: translateY(-50%); right: 0; }
}

@media (min-width: 768px) {
  .product-grid { grid-template-columns: repeat(3, 1fr); }
  h1 { font-size: 3rem; }
  h2 { font-size: 2.375rem; }
  .hero h1 { font-size: 3.6rem; }
  .section { padding: 64px 0; }
  .section-head { margin-bottom: 32px; }
  .stat-row { gap: 16px; }
  .stat-v { font-size: 1.8rem; }
}

@media (min-width: 1024px) {
  :root { --header-h: 70px; }
  .product-grid { grid-template-columns: repeat(4, 1fr); gap: 24px; }
  .feature-grid { grid-template-columns: repeat(3, 1fr); gap: 24px; }
  .feature-grid.two-up { grid-template-columns: repeat(2, 1fr); }
  .main-nav { display: flex; gap: 26px; }
  .main-nav a { font-size: 0.92rem; font-weight: 500; color: var(--ink-2); padding: 6px 0; border-bottom: 2px solid transparent; }
  .main-nav a:hover { color: var(--ink); }
  .main-nav a.active { color: var(--ink); border-bottom-color: var(--accent); }
  .menu-btn { display: none; }

  .hero-inner { padding-block: 56px 64px; gap: 56px; }
  .hero .lede { font-size: 1.12rem; }
  .hero-visual { padding: 48px; }

  .card-body { padding: 16px; }
  .card-row h3 { font-size: 1.12rem; }
  
  .pdp { grid-template-columns: 1fr 1fr; gap: 48px; padding-block: 32px 8px; }
  .pdp h1 { font-size: 2.4rem; }
  .pdp-media { aspect-ratio: 1; padding: 40px; }
  .buy-row .btn { flex: 0 0 auto; min-width: 220px; }
  .buy-bar { display: none !important; }
  body.has-buy-bar { padding-bottom: 0; }

  .cart-wrap { grid-template-columns: 1fr 350px; gap: 40px; align-items: start; padding-block: 32px 72px; }
  .cart-summary { position: sticky; top: calc(var(--header-h) + 20px); }

  .footer-grid { grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 36px; }
  .site-footer { padding: 56px 0 32px; }
  .spec-table th { width: 240px; }
  .prose { padding-block: 40px 80px; }
  .contact-grid { gap: 32px; padding-block: 32px 72px; }
  .page-head { padding-block: 40px 24px; }
  .closing { padding: 64px 0; }
}

@media (max-width: 1023px) {
  .main-nav.open {
    display: flex; flex-direction: column; gap: 0;
    position: absolute; top: var(--header-h); left: 0; right: 0;
    background: var(--bg); border-bottom: 1px solid var(--line);
    padding: 8px var(--gutter) 16px;
  }
  .main-nav.open a { padding: 14px 0; border-bottom: 1px solid var(--line); color: var(--ink); font-weight: 500; }
  .main-nav.open a:last-child { border-bottom: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  html { scroll-behavior: auto; }
}

/* faq.html groups questions into categories */
.faq-cat { margin-bottom: 32px; }
.faq-cat > h2 { font-size: 1.2rem; margin-bottom: 8px; }

/* Below 360px a title plus a nowrap price cannot share one row. */
@media (max-width: 359px) { .card-row { flex-direction: column; gap: 2px; } }

/* ============================================================
   v5 — CONVERSION SURFACES
   ------------------------------------------------------------
   Structure and interaction modelled on the category leader;
   palette stays Halotir. Green is reserved for earned/free
   states only (shipping unlocked, savings, in stock) because
   it has to read instantly against the gold accent.
   Mobile first. Every drawer is a right/left slide, never a
   modal, because thumbs live at the bottom of the screen.
   ============================================================ */

:root {
  --success: #15803d;
  --success-tint: #f0fdf4;
  --success-line: #bbf7d0;
  --success-bar: #22c55e;

  --radius-lg: 24px;
  --radius-pill: 9999px;

  --ease: cubic-bezier(.32, .72, 0, 1);
  --dur: 300ms;
  --dur-fast: 180ms;

  --z-backdrop: 90;
  --z-drawer: 100;
  --z-sticky: 80;
  --z-toast: 120;
}

/* ---------- Scroll reveal ---------- */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity 520ms var(--ease), transform 520ms var(--ease); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }

/* ---------- Backdrop shared by both drawers ---------- */
.backdrop {
  position: fixed; inset: 0; background: rgba(28, 25, 23, .44);
  opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) linear;
  z-index: var(--z-backdrop);
}
.backdrop.show { opacity: 1; pointer-events: auto; }
body.drawer-open { overflow: hidden; }

/* ---------- Cart drawer ---------- */
.cart-drawer {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: min(89vw, 440px);
  background: var(--bg);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform var(--dur) var(--ease);
  z-index: var(--z-drawer);
  box-shadow: -8px 0 40px rgba(28, 25, 23, .10);
}
.cart-drawer.open { transform: none; }

.cd-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px 20px; border-bottom: 1px solid var(--line); flex: none;
}
.cd-head h2 { font-size: 1.05rem; font-weight: 600; }
.cd-head .cd-count { color: var(--muted); font-weight: 400; margin-left: 4px; }
.cd-close {
  width: 40px; height: 40px; border-radius: var(--radius-pill);
  border: none; background: none; cursor: pointer; color: var(--ink);
  display: grid; place-items: center; transition: background var(--dur-fast) ease;
}
.cd-close:hover { background: var(--panel); }

.cd-body { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 14px 16px; background: var(--tint); }
.cd-foot { flex: none; border-top: 1px solid var(--line); padding: 14px 16px calc(14px + env(safe-area-inset-bottom)); background: var(--bg); }

/* Line item */
.cd-item {
  display: grid; grid-template-columns: 68px 1fr auto; gap: 12px; align-items: start;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 12px; margin-bottom: 10px; position: relative;
}
.cd-thumb { width: 68px; height: 68px; border-radius: 10px; background: var(--panel); display: grid; place-items: center; overflow: hidden; }
.cd-thumb .vial { height: 84%; width: auto; }
.cd-item h3 { font-size: .95rem; font-weight: 600; line-height: 1.25; padding-right: 18px; }
.cd-variant { font-size: .8rem; color: var(--muted); margin-top: 2px; }
.cd-item-price { font-family: var(--font-num); font-weight: 600; font-size: .95rem; white-space: nowrap; }
.cd-was { font-size: .78rem; color: var(--muted); text-decoration: line-through; display: block; text-align: right; font-weight: 400; }
.cd-x {
  position: absolute; top: 8px; right: 8px; width: 26px; height: 26px;
  border: none; background: none; color: var(--muted); cursor: pointer; border-radius: 50%;
  display: grid; place-items: center; font-size: 15px; line-height: 1;
}
.cd-x:hover { background: var(--panel); color: var(--ink); }
.cd-qty { margin-top: 8px; display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line-2); border-radius: var(--radius-pill); padding: 3px 4px; }
.cd-qty button { width: 26px; height: 26px; border: none; background: none; cursor: pointer; border-radius: 50%; font-size: 15px; color: var(--ink); }
.cd-qty button:hover { background: var(--panel); }
.cd-qty button:disabled { opacity: .35; cursor: not-allowed; }
.cd-qty span { font-family: var(--font-num); font-size: .88rem; min-width: 16px; text-align: center; }

/* Assurance row */
.cd-assure {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 12px; margin-bottom: 10px; font-size: .88rem;
}
.cd-assure svg { color: var(--success); flex: none; }
.cd-assure b { margin-left: auto; color: var(--success); font-weight: 600; }

/* Order bump */
.cd-bump {
  position: relative;
  display: grid; grid-template-columns: 56px 1fr auto; gap: 12px; align-items: center;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 12px; margin-bottom: 10px;
}
.cd-bump-media { width: 56px; height: 56px; border-radius: 10px; background: var(--accent-tint); display: grid; place-items: center; overflow: hidden; }
.cd-bump-media .vial { height: 84%; width: auto; }
.cd-bump-eyebrow {
  font-family: var(--font-num); font-size: .62rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--success);
}
.cd-bump h4 { font-size: .84rem; font-weight: 600; line-height: 1.25; margin-top: 1px; }
.cd-bump p { font-size: .76rem; color: var(--muted); margin-top: 1px; }
.cd-bump p b { color: var(--ink); font-weight: 600; }
.cd-bump-add {
  height: 40px; padding: 0 18px; border-radius: var(--radius-pill);
  background: var(--ink); color: #fff; border: none; cursor: pointer;
  font-family: var(--font-num); font-size: .85rem; font-weight: 500;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.cd-bump-add:hover { opacity: .88; }
.cd-bump-add:active { transform: scale(.97); }
.cd-bump-dismiss {
  position: absolute; top: -8px; right: -8px; width: 24px; height: 24px;
  border-radius: 50%; border: 1px solid var(--line); background: var(--bg);
  color: var(--muted); cursor: pointer; font-size: 13px; line-height: 1; display: grid; place-items: center;
}
.cd-bump-dismiss:hover { color: var(--ink); }

/* Promo code */
.cd-promo { padding: 4px 2px 10px; }
.cd-promo-toggle {
  display: inline-flex; align-items: center; gap: 8px; background: none; border: none;
  cursor: pointer; font-size: .92rem; font-weight: 500; color: var(--ink); padding: 8px 0;
}
.cd-promo-toggle svg { color: var(--muted); }
.cd-promo-form { display: none; gap: 8px; margin-top: 6px; }
.cd-promo-form.show { display: flex; }
.cd-promo-form input {
  flex: 1; min-width: 0; height: 44px; padding: 0 14px; font-size: 16px;
  border: 1px solid var(--line-2); border-radius: var(--radius-sm); background: var(--bg); font-family: var(--font);
}
.cd-promo-form input:focus { outline: none; border-color: var(--ink); }
.cd-promo-form button { height: 44px; padding: 0 16px; border-radius: var(--radius-sm); border: 1px solid var(--ink); background: none; cursor: pointer; font-weight: 500; }

/* Free-shipping progress */
.ship-progress { margin-bottom: 12px; }
.ship-progress-label { display: flex; align-items: center; gap: 8px; font-size: .88rem; margin-bottom: 8px; }
.ship-progress-label svg { color: var(--success); flex: none; }
.ship-progress-label b { color: var(--success); font-family: var(--font-num); font-weight: 600; }
.ship-progress-track { height: 6px; border-radius: var(--radius-pill); background: var(--success-line); overflow: hidden; }
.ship-progress-fill { height: 100%; border-radius: var(--radius-pill); background: var(--success-bar); width: 0; transition: width 520ms var(--ease); }
.ship-progress.done .ship-progress-label { color: var(--success); font-weight: 500; }

/* Dispatch urgency chip */
.cd-urgency {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--success-tint); border: 1px solid var(--success-line);
  border-radius: var(--radius-pill); padding: 9px 14px; font-size: .84rem; text-align: center;
}
.cd-urgency svg { color: var(--success); flex: none; }
.cd-urgency b { font-family: var(--font-num); font-weight: 600; }
.cd-eta { text-align: center; font-size: .8rem; color: var(--muted); margin-top: 8px; }
.cd-eta b { color: var(--ink); font-weight: 500; }

.cd-totals { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 12px; }
.cd-total-row { display: flex; align-items: center; justify-content: space-between; font-size: .95rem; }
.cd-total-row + .cd-total-row { margin-top: 6px; }
.cd-total-row b { font-family: var(--font-num); font-weight: 600; font-size: 1.15rem; }
.cd-total-row.saving { color: var(--success); font-size: .85rem; }
.cd-total-row.saving span:last-child { font-family: var(--font-num); font-weight: 600; }
.cd-tax-note { font-size: .75rem; color: var(--muted); margin-top: 6px; }

.cd-actions { display: grid; gap: 8px; margin-top: 12px; }
.cd-checkout {
  height: 52px; border-radius: var(--radius-pill); background: var(--ink); color: #fff;
  border: none; cursor: pointer; font-family: var(--font-num); font-size: .98rem; font-weight: 500;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.cd-checkout:hover { opacity: .9; color: #fff; }
.cd-checkout:active { transform: scale(.99); }
.cd-viewcart { height: 44px; border-radius: var(--radius-pill); border: 1px solid var(--line-2); background: none; cursor: pointer; font-size: .92rem; font-weight: 500; color: var(--ink); display: grid; place-items: center; }
.cd-viewcart:hover { border-color: var(--ink); color: var(--ink); }

.cd-empty { text-align: center; padding: 56px 20px; }
.cd-empty p { margin-bottom: 16px; }

/* ---------- Mobile nav drawer ---------- */
.nav-drawer {
  position: fixed; top: 0; left: 0; bottom: 0; width: min(84vw, 340px);
  background: var(--bg); z-index: var(--z-drawer);
  transform: translateX(-100%); transition: transform var(--dur) var(--ease);
  display: flex; flex-direction: column;
  box-shadow: 8px 0 40px rgba(28, 25, 23, .10);
}
.nav-drawer.open { transform: none; }
.nd-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--line); }
.nd-links { flex: 1; overflow-y: auto; padding: 8px 0; }
.nd-links a {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 15px 20px; color: var(--ink); font-size: 1.02rem; font-weight: 500;
  border-bottom: 1px solid var(--line);
}
.nd-links a:hover { background: var(--tint); color: var(--ink); }
.nd-links a svg { color: var(--line-2); flex: none; }
.nd-group { padding: 18px 20px 6px; font-family: var(--font-num); font-size: .66rem; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.nd-foot { padding: 16px 20px calc(16px + env(safe-area-inset-bottom)); border-top: 1px solid var(--line); font-size: .8rem; color: var(--muted); }

.badge-new {
  display: inline-block; margin-left: 7px; padding: 2px 6px; border-radius: 5px;
  background: var(--accent-tint); border: 1px solid var(--accent-line); color: var(--accent-deep);
  font-family: var(--font-num); font-size: .58rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; vertical-align: middle;
}

/* ---------- Category pill strip ---------- */
.cat-strip { position: relative; margin-bottom: 20px; }
.cat-pills {
  display: flex; gap: 8px; overflow-x: auto; scroll-behavior: smooth;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  padding: 2px var(--gutter) 6px; margin: 0 calc(var(--gutter) * -1);
  scroll-snap-type: x proximity;
}
.cat-pills::-webkit-scrollbar { display: none; }
.cat-pill {
  flex: none; scroll-snap-align: start;
  padding: 9px 16px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-2); background: var(--bg); color: var(--ink-2);
  font-size: .88rem; font-weight: 500; white-space: nowrap; cursor: pointer;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.cat-pill:hover { border-color: var(--ink); color: var(--ink); }
.cat-pill.active { background: var(--ink); border-color: var(--ink); color: #fff; }

/* ---------- Quick add on product card ---------- */
.qa-btn {
  width: 100%; height: 42px; margin-top: 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--ink); background: none; color: var(--ink); cursor: pointer;
  font-family: var(--font-num); font-size: .86rem; font-weight: 500;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.qa-btn:hover { background: var(--ink); color: #fff; }
.qa-btn:disabled { opacity: .5; cursor: default; }
.qa-btn.added { background: var(--success); border-color: var(--success); color: #fff; }

/* ---------- Build a Box ---------- */
.box-intro-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-block: 24px; }
.box-tile { border-radius: var(--radius); padding: 20px 18px; min-height: 150px; display: flex; flex-direction: column; justify-content: flex-end; }
.box-tile h3 { font-size: .98rem; margin-bottom: 3px; }
.box-tile p { font-size: .82rem; color: var(--ink-2); line-height: 1.4; }
.box-tile .tile-big { font-family: var(--font-num); font-size: 3rem; font-weight: 600; line-height: 1; letter-spacing: -.03em; margin-bottom: 10px; }
.box-tile .tile-big sup { font-size: 1.2rem; font-weight: 500; }
.box-tile.t-gold { background: var(--accent-tint); }
.box-tile.t-green { background: var(--success-tint); }
.box-tile.t-stone { background: var(--panel); }
.box-tile.t-plain { background: var(--bg); border: 1px solid var(--line); }
.tile-pill { display: inline-block; align-self: flex-start; padding: 4px 10px; border-radius: var(--radius-pill); background: var(--bg); font-family: var(--font-num); font-size: .64rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--success); margin-bottom: auto; }
.tile-inset { background: var(--bg); border-radius: var(--radius-sm); padding: 12px 14px; margin-bottom: auto; display: inline-flex; align-items: center; gap: 10px; align-self: flex-start; }
.tile-inset .tile-num { font-family: var(--font-num); font-size: 1.3rem; font-weight: 600; }
.tile-inset .tile-pass { padding: 3px 9px; border-radius: var(--radius-pill); background: var(--success-tint); color: var(--success); font-family: var(--font-num); font-size: .62rem; font-weight: 600; letter-spacing: .08em; }

/* Box picker card */
.box-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.box-card { background: var(--tint); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; }
.box-card-media { position: relative; aspect-ratio: 1 / 1; display: grid; place-items: center; background: var(--panel); }
.box-card-media .vial { height: 78%; width: auto; }
.box-coa {
  position: absolute; left: 10px; bottom: 10px;
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--bg); border-radius: var(--radius-pill); padding: 5px 10px;
  font-family: var(--font-num); font-size: .72rem; font-weight: 600;
  box-shadow: 0 1px 4px rgba(28, 25, 23, .12); color: var(--ink);
}
.box-coa svg { color: var(--success); }
.box-coa span { color: var(--muted); font-weight: 500; text-decoration: underline; }
.box-card-body { padding: 12px; display: flex; flex-direction: column; flex: 1; }
.box-card-body h3 { font-size: .92rem; line-height: 1.2; }
.box-cat { font-size: .76rem; color: var(--muted); margin-top: 2px; margin-bottom: 10px; }
.box-dose {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 9px 14px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-2); background: var(--bg); cursor: pointer;
  font-size: .84rem; font-weight: 600; color: var(--ink); margin-bottom: 6px;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.box-dose .box-dose-price { font-family: var(--font-num); font-weight: 500; color: var(--muted); }
.box-dose.active { background: var(--ink); border-color: var(--ink); color: #fff; }
.box-dose.active .box-dose-price { color: rgba(255, 255, 255, .72); }
.box-price { display: flex; align-items: baseline; gap: 7px; margin: 8px 0 10px; flex-wrap: wrap; }
.box-price .now { font-family: var(--font-num); font-size: 1.08rem; font-weight: 600; }
.box-price .was { font-family: var(--font-num); font-size: .84rem; color: var(--muted); text-decoration: line-through; }
.box-price .in-box { font-size: .76rem; color: var(--muted); }
.box-add {
  margin-top: auto; width: 100%; height: 44px; border-radius: var(--radius-pill);
  background: var(--ink); color: #fff; border: none; cursor: pointer;
  font-family: var(--font-num); font-size: .88rem; font-weight: 500;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.box-add:hover { opacity: .9; }
.box-add:active { transform: scale(.98); }
.box-add.in-cart { background: var(--success); }

/* Sticky slot bar */
.slot-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-sticky);
  background: var(--bg); border-top: 1px solid var(--line);
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 12px;
  transform: translateY(100%); transition: transform var(--dur) var(--ease);
  box-shadow: 0 -4px 24px rgba(28, 25, 23, .07);
}
.slot-bar.show { transform: none; }
.slots { display: flex; gap: 6px; flex: 1; min-width: 0; }
.slot {
  width: 38px; height: 38px; border-radius: 50%; flex: none;
  border: 1.5px dashed var(--line-2); display: grid; place-items: center;
  font-family: var(--font-num); font-size: .82rem; color: var(--muted);
  transition: all var(--dur-fast) var(--ease); overflow: hidden; background: var(--bg);
}
.slot.filled { border-style: solid; border-color: var(--ink); background: var(--panel); color: var(--ink); }
.slot.filled .vial { height: 76%; width: auto; }
.slot.free { border-style: solid; border-color: var(--success-line); background: var(--success-tint); color: var(--success); font-size: .56rem; font-weight: 600; letter-spacing: .04em; }
.slot-info { text-align: right; flex: none; }
.slot-info strong { display: block; font-size: 1.02rem; font-weight: 600; line-height: 1.15; }
.slot-info span { font-size: .76rem; color: var(--muted); }
.slot-cta {
  height: 46px; padding: 0 20px; border-radius: var(--radius-pill); background: var(--ink);
  color: #fff; border: none; cursor: pointer; font-family: var(--font-num); font-size: .9rem; font-weight: 500; flex: none;
}
.slot-cta:disabled { opacity: .4; cursor: not-allowed; }

/* ---------- Partner program ---------- */
.partner-hero { background: linear-gradient(165deg, #fdf8ee 0%, #f7f5f0 55%, #f5f5f4 100%); padding: 32px 0 40px; }
.earnings-card {
  background: var(--bg); border-radius: var(--radius); padding: 18px 20px;
  box-shadow: 0 8px 30px rgba(28, 25, 23, .09); max-width: 300px; margin-bottom: 24px;
}
.earnings-card .ec-label { font-size: .8rem; color: var(--muted); line-height: 1.3; }
.earnings-card .ec-total { font-family: var(--font-num); font-size: 2rem; font-weight: 600; letter-spacing: -.02em; margin-top: 4px; }
.earnings-card hr { border: none; border-top: 1px solid var(--line); margin: 12px 0; }
.ec-row { display: flex; align-items: center; justify-content: space-between; font-size: .85rem; }
.ec-row + .ec-row { margin-top: 6px; }
.ec-row span:first-child { color: var(--muted); }
.ec-row b { font-family: var(--font-num); font-weight: 600; color: var(--success); }
.ec-row.recurring b { color: var(--accent); }

.partner-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px 20px; margin-top: 28px; }
.partner-stat strong { display: block; font-family: var(--font-num); font-size: 1.9rem; font-weight: 600; letter-spacing: -.02em; line-height: 1.1; }
.partner-stat span { font-size: .86rem; color: var(--muted); }

.step-cards { display: grid; gap: 12px; }
.step-card { background: var(--tint); border-radius: var(--radius); padding: 20px; }
.step-card .step-n {
  width: 32px; height: 32px; border-radius: 50%; background: var(--ink); color: #fff;
  display: grid; place-items: center; font-family: var(--font-num); font-size: .88rem; font-weight: 600; margin-bottom: 12px;
}
.step-card h3 { font-size: 1rem; margin-bottom: 4px; }
.step-card p { font-size: .9rem; }

/* ---------- Social proof toast ---------- */
.proof {
  position: fixed; left: 16px; bottom: 16px; z-index: var(--z-toast);
  display: flex; align-items: center; gap: 11px; max-width: min(90vw, 330px);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 11px 13px; box-shadow: 0 10px 34px rgba(28, 25, 23, .13);
  transform: translateY(130%); opacity: 0;
  transition: transform 420ms var(--ease), opacity 420ms var(--ease);
}
.proof.show { transform: none; opacity: 1; }
.proof-media { width: 40px; height: 40px; border-radius: 8px; background: var(--panel); display: grid; place-items: center; flex: none; overflow: hidden; }
.proof-media .vial { height: 82%; width: auto; }
.proof p { font-size: .82rem; line-height: 1.35; color: var(--ink); }
.proof p b { font-weight: 600; }
.proof span { font-size: .72rem; color: var(--muted); }
.proof-x { border: none; background: none; color: var(--muted); cursor: pointer; font-size: 15px; align-self: flex-start; padding: 0 2px; }

/* ---------- Desktop ---------- */
@media (min-width: 768px) {
  .box-grid { grid-template-columns: repeat(3, 1fr); gap: 16px; }
  .box-intro-grid { grid-template-columns: repeat(4, 1fr); }
  .partner-stats { grid-template-columns: repeat(4, 1fr); }
  .step-cards { grid-template-columns: repeat(2, 1fr); }
  .slot { width: 42px; height: 42px; }
}

@media (min-width: 1024px) {
  .box-grid { grid-template-columns: repeat(4, 1fr); }
  .step-cards { grid-template-columns: repeat(4, 1fr); }
  .nav-drawer { display: none; }
  .partner-hero { padding: 56px 0 64px; }
  .earnings-card { max-width: 330px; }
  .partner-stat strong { font-size: 2.3rem; }
}

/* ---------- Product card as article with a stretched link ---------- */
.product-card { position: relative; }
.card-link { position: absolute; inset: 0; z-index: 1; }
.card-link::after { content: ""; position: absolute; inset: 0; }
.product-card .card-body { position: relative; }
.product-card .qa-btn { position: relative; z-index: 2; }
.product-card:focus-within { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius); }
.card-link:focus-visible { outline: none; }

/* ---------- Toast error state ---------- */
.toast.error { background: #7f1d1d; color: #fff; }

/* ---------- Store toolbar spacing with pills ---------- */
.store-tools + .cat-strip { margin-top: 14px; }

/* ---------- v5 corrections ----------
   Every inline SVG in this stylesheet is sized explicitly; the v5
   components above set colour but not size, which let the shield in
   the cart drawer render at its intrinsic box and blow the panel out
   to a horizontal scroll. Size them all, in one place.            */
.cd-assure svg,
.ship-progress-label svg,
.cd-urgency svg { width: 18px; height: 18px; }
.cd-promo-toggle svg { width: 18px; height: 18px; }
.cd-close svg { width: 20px; height: 20px; }
.nd-links a svg { width: 16px; height: 16px; }
.cd-checkout svg { width: 18px; height: 18px; }
.qa-btn svg, .box-add svg, .box-coa svg { width: 15px; height: 15px; }
.slot-cta svg { width: 16px; height: 16px; }

/* mediaHTML() wraps the vial in its own div, so the artwork is a
   grandchild of the thumb, not a child. Without this the wrapper has
   no height and the vial collapses to nothing. */
.cd-thumb > div,
.cd-bump-media > div,
.box-card-media > div,
.slot.filled > div,
.proof-media > div { width: 100%; height: 100%; display: grid; place-items: center; }
.cd-thumb .vial, .cd-bump-media .vial { height: 86%; width: auto; }
.box-card-media .vial { height: 78%; width: auto; }
.slot.filled .vial { height: 76%; width: auto; }

/* A drawer must never scroll sideways. */
.cart-drawer, .nav-drawer { overflow-x: hidden; }
.cd-body, .cd-foot { max-width: 100%; }
.cd-item, .cd-bump, .cd-assure { min-width: 0; }
.cd-item > *, .cd-bump > * { min-width: 0; }
.cd-item h3, .cd-bump h4, .cd-bump p { overflow-wrap: anywhere; }

/* The bump has ~110px of text column inside a 334px drawer, so the
   eyebrow has to fit on one line at that width and the price must not
   be separated from its label by a line break. */
.cd-bump { grid-template-columns: 52px 1fr auto; gap: 10px; padding: 11px; }
.cd-bump-media { width: 52px; height: 52px; }
.cd-bump-eyebrow { font-size: .58rem; letter-spacing: .07em; white-space: nowrap; }
.cd-bump p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cd-bump-add { padding: 0 15px; }

/* .lede was only ever scoped under .hero. The partner hero is its own
   block, so the intro paragraph there fell back to plain body text. */
.partner-hero .lede { font-size: 1.05rem; max-width: 46ch; margin-top: 12px; color: var(--ink-2); }
.partner-hero h1 { max-width: 14ch; }

/* Slot bar: five 38px slots, a label and a CTA cannot share one row at
   375px, they collide. Stack into two rows on mobile and only go
   single-row once there is width for it. */
.slot-bar { flex-wrap: wrap; row-gap: 10px; }
.slots { order: 1; flex: 1 1 100%; justify-content: space-between; }
.slot-info { order: 2; flex: 1 1 auto; text-align: left; }
.slot-cta { order: 3; }

@media (min-width: 560px) {
  .slot-bar { flex-wrap: nowrap; }
  .slots { order: 0; flex: 0 0 auto; justify-content: flex-start; }
  .slot-info { order: 0; flex: 1 1 auto; text-align: right; }
  .slot-cta { order: 0; }
}

/* ============================================================
   Phone sign-in
   ------------------------------------------------------------
   This is the highest-stakes screen on the site: everything the
   customer has chosen is behind it. So it is one column, one
   field, one button, and nothing else competing for the tap.
   ============================================================ */
.auth-wrap { max-width: 460px; padding-bottom: 72px; }
.auth-head { padding-block: 28px 16px; }
.auth-head h1 { font-size: 2rem; }
.auth-head p { color: var(--muted); }

.auth-card {
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 28px 22px;
}
.auth-intro { text-align: center; color: var(--muted); font-size: .92rem; margin-bottom: 26px; }
.auth-card h2 { font-size: 1.45rem; text-align: center; }
.auth-sub { text-align: center; color: var(--muted); font-size: .92rem; margin-top: 4px; margin-bottom: 22px; }
.auth-sub b { color: var(--ink); font-weight: 600; }

/* 56px tall and 16px text. Anything under 16px makes iOS Safari zoom
   the whole page on focus, which on a checkout gate reads as a bug. */
.auth-input {
  display: block; width: 100%; height: 56px; padding: 0 16px;
  font-family: var(--font); font-size: 16px; color: var(--ink);
  background: var(--panel); border: 1px solid transparent;
  border-radius: var(--radius-sm);
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.auth-input::placeholder { color: var(--line-2); }
.auth-input:focus { outline: none; background: var(--bg); border-color: var(--ink); }

.auth-code {
  text-align: center; font-family: var(--font-num);
  font-size: 1.6rem; font-weight: 600; letter-spacing: .38em;
  text-indent: .38em; /* letter-spacing pads the right, this recentres it */
}

.auth-fine { font-size: .76rem; color: var(--muted); line-height: 1.45; margin-top: 12px; }
.auth-error { font-size: .85rem; color: #b91c1c; margin-top: 12px; }

.auth-submit {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; height: 56px; margin-top: 18px;
  border: none; border-radius: var(--radius-pill); cursor: pointer;
  background: var(--ink); color: #fff;
  font-family: var(--font-num); font-size: 1rem; font-weight: 500;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.auth-submit svg { width: 18px; height: 18px; }
.auth-submit:hover:not(:disabled) { opacity: .9; }
.auth-submit:active:not(:disabled) { transform: scale(.99); }
.auth-submit:disabled { opacity: .45; cursor: not-allowed; }

.auth-link {
  display: block; margin: 16px auto 0; padding: 4px;
  background: none; border: none; cursor: pointer;
  font-family: var(--font); font-size: .92rem; color: var(--ink);
  text-decoration: underline; text-underline-offset: 3px;
}
.auth-link:disabled { color: var(--muted); cursor: default; text-decoration: none; }
.auth-sub .auth-link { display: inline; margin: 0; font-size: inherit; }

.auth-legal { margin-top: 24px; text-align: center; font-size: .78rem; color: var(--muted); line-height: 1.5; }
.auth-legal a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.auth-legal a:hover { color: var(--ink); }

@media (min-width: 768px) {
  .auth-card { padding: 34px 30px; }
  .auth-head { padding-block: 40px 20px; }
}

/* .card-btn was used in the product card from the start but never had a
   rule, so it rendered as plain body text. It reads as an affordance now:
   an outlined pill matching .qa-btn, but lighter, because the whole card
   is already the tap target and this is a cue, not a second control. */
.card-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; height: 42px; margin-top: 10px;
  border: 1px solid var(--line-2); border-radius: var(--radius-pill);
  font-family: var(--font-num); font-size: .86rem; font-weight: 500; color: var(--ink);
  transition: border-color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.product-card:hover .card-btn { border-color: var(--ink); background: var(--bg); }

/* ============================================================
   v6 — "WARM LAB"
   ------------------------------------------------------------
   v4 of this system opened with "Sans only. No card borders, no
   shadows." That austerity was a deliberate choice and it is the
   reason the site read flat beside a competitor built on colour
   and motion. This layer keeps the Halotir palette, ink and gold,
   and adds the three things that were missing: tinted surfaces,
   real elevation, and a motion family.

   The motion is one family, not a grab bag: everything floats on
   the same ease-in-out curve at 4 to 6 seconds, with staggered
   delays and different amplitudes so no two elements ever sync
   up. That is what reads as "alive" rather than "animated".
   ============================================================ */

:root {
  /* Tinted surfaces. Muted on purpose: a lab supplier that looks
     like a sweet shop stops looking like a lab supplier. */
  --tint-gold:   #fdf6e9;
  --tint-sage:   #e9f2ec;
  --tint-steel:  #eaeff5;
  --tint-clay:   #f8efe8;
  --tint-violet: #f0edf6;

  /* Saturated counterparts, for numerals and small marks only. */
  --vivid-gold:  #a16207;
  --vivid-sage:  #2f7a52;
  --vivid-steel: #2c5c8a;
  --vivid-clay:  #a4562a;

  /* Elevation, tinted warm so shadows never look grey on cream. */
  --shadow-sm:   0 1px 2px rgba(28, 25, 23, .06);
  --shadow-md:   0 4px 16px rgba(28, 25, 23, .07);
  --shadow-lg:   0 18px 44px rgba(28, 25, 23, .11);
  --shadow-gold: 0 18px 44px rgba(161, 98, 7, .14);
  --shadow-lift: 0 10px 30px rgba(28, 25, 23, .10);

  --font-display: "Fraunces", Georgia, serif;

  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.4, 0, .2, 1);
}

/* ---------- The motion family ---------- */
@keyframes h-float       { 0%, 100% { transform: translateY(0); }        50% { transform: translateY(-10px); } }
@keyframes h-float-slow  { 0%, 100% { transform: translateY(0); }        50% { transform: translateY(-8px); } }
@keyframes h-float-deep  { 0%, 100% { transform: translateY(0); }        50% { transform: translateY(-14px); } }
@keyframes h-drift       { 0%, 100% { transform: translate(0, 0); }      50% { transform: translate(8px, -6px); } }
@keyframes h-bob         { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-6px) rotate(1.5deg); } }
@keyframes h-sway {
  0%, 100% { transform: translateY(0) translateX(0); }
  25% { transform: translateY(-8px) translateX(4px); }
  50% { transform: translateY(-14px) translateX(0); }
  75% { transform: translateY(-8px) translateX(-4px); }
}
@keyframes h-shimmer     { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
@keyframes h-glow        { 0%, 100% { opacity: .28; transform: scale(1); } 50% { opacity: .5; transform: scale(1.06); } }
@keyframes h-marquee     { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }
@keyframes h-rain        { 0% { transform: translateY(-460px) rotate(var(--tilt, -6deg)); } 100% { transform: translateY(460px) rotate(var(--tilt, -6deg)); } }
@keyframes h-pill-pulse  { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.045); } }

.f-float      { animation: h-float 4s ease-in-out infinite; }
.f-float-slow { animation: h-float-slow 5s ease-in-out .5s infinite; }
.f-float-deep { animation: h-float-deep 4.5s ease-in-out 1s infinite; }
.f-drift      { animation: h-drift 7s ease-in-out infinite; }
.f-sway       { animation: h-sway 6s ease-in-out infinite; }
.f-bob        { animation: h-bob 3s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .f-float, .f-float-slow, .f-float-deep, .f-drift, .f-sway, .f-bob,
  .promo-glow, .rain-tile, .marquee-track { animation: none !important; }
}

/* ---------- Display numerals ---------- */
.display-num {
  font-family: var(--font-display);
  font-weight: 600;
  font-variation-settings: "SOFT" 0, "WONK" 1;
  letter-spacing: -.02em;
  font-style: normal;
}
.accent-serif { font-family: var(--font-display); font-style: italic; font-weight: 500; }

/* ============================================================
   Promo bar
   ============================================================ */
.promo-bar {
  position: relative; overflow: hidden;
  background: linear-gradient(105deg, #241c14 0%, #3b2a15 45%, #241c14 100%);
  border-bottom: 1px solid rgba(227, 192, 133, .32);
  padding: 11px var(--gutter);
  display: flex; align-items: center; justify-content: center;
  gap: 10px 14px; flex-wrap: wrap; text-align: center;
}
.promo-bar::after {
  /* The sheen that travels across the bar. Slow enough to notice
     only once, which is the point. */
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 35%, rgba(255, 244, 214, .13) 50%, transparent 65%);
  animation: h-shimmer 7s ease-in-out infinite;
  pointer-events: none;
}
.promo-sun {
  position: absolute; right: -26px; bottom: -34px;
  width: 116px; height: 116px; border-radius: 50%;
  background: radial-gradient(circle, rgba(227, 192, 133, .5), rgba(227, 192, 133, 0) 68%);
  animation: h-glow 6s ease-in-out infinite; pointer-events: none;
}
.promo-eyebrow {
  font-family: var(--font-num); font-size: .64rem; font-weight: 500;
  letter-spacing: .26em; text-transform: uppercase; color: #e6d5ae;
}
.promo-offer {
  font-family: var(--font-display); font-style: italic; font-weight: 600;
  font-size: 1.12rem; letter-spacing: -.01em;
  background: var(--gold-foil); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: #e3c085;
}
.promo-code {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 15px; border-radius: var(--radius-pill);
  background: var(--gold-foil); color: #2b1f10;
  font-family: var(--font-num); font-size: .76rem; font-weight: 600; letter-spacing: .16em;
  border: none; cursor: pointer; animation: h-pill-pulse 3.4s ease-in-out infinite;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .28);
}
.promo-code:active { transform: scale(.97); }
.promo-timer {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 13px; border-radius: var(--radius-pill);
  border: 1px solid rgba(227, 192, 133, .34); color: #f0e3c6;
  font-family: var(--font-num); font-size: .78rem; font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* Must exclude the sun: it is absolutely positioned decoration, and a
   blanket `position: relative` on children (same specificity, declared
   later) dragged it back into flow where it claimed a whole grid row. */
.promo-bar > *:not(.promo-sun) { position: relative; z-index: 1; }

/* ============================================================
   Hero
   ============================================================ */
.hero-v6 {
  position: relative; overflow: hidden;
  background:
    radial-gradient(120% 90% at 82% 8%, var(--tint-gold) 0%, transparent 58%),
    radial-gradient(90% 70% at 8% 92%, var(--tint-steel) 0%, transparent 60%),
    linear-gradient(170deg, #ffffff 0%, var(--tint) 100%);
  padding: 34px 0 44px;
}
.hero-v6 .wrap { position: relative; z-index: 2; }

.hero-collage {
  position: relative; height: 300px; margin-bottom: 22px;
  display: flex; align-items: center; justify-content: center;
}
.hero-vial {
  position: absolute; display: grid; place-items: center;
  filter: drop-shadow(0 16px 30px rgba(28, 25, 23, .16));
}
.hero-vial .vial { height: 100%; width: auto; }
/* Four vials, four amplitudes, four delays. Nothing lines up. */
.hero-vial.v1 { height: 210px; z-index: 4; transform: rotate(-7deg);  left: 50%; margin-left: -46px; }
.hero-vial.v2 { height: 156px; z-index: 3; transform: rotate(9deg);   left: 12%;  top: 24%; }
.hero-vial.v3 { height: 132px; z-index: 2; transform: rotate(-14deg); right: 10%; top: 14%; }
.hero-vial.v4 { height: 116px; z-index: 1; transform: rotate(6deg);   right: 20%; bottom: 4%; }
.hero-halo {
  position: absolute; left: 50%; top: 50%; width: 300px; height: 300px;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: radial-gradient(circle, rgba(161, 98, 7, .13), transparent 66%);
  animation: h-glow 7s ease-in-out infinite; z-index: 0;
}

.hero-v6 h1 { font-size: 2.6rem; line-height: 1.03; letter-spacing: -.03em; }
.hero-v6 h1 em { font-family: var(--font-display); font-style: italic; font-weight: 500; color: var(--accent); }
.hero-v6 .lede { font-size: 1.04rem; max-width: 44ch; margin-top: 14px; color: var(--ink-2); }
.hero-ctas-v6 { display: flex; gap: 10px; margin-top: 22px; flex-wrap: wrap; }
.hero-ctas-v6 .btn { border-radius: var(--radius-pill); height: 52px; padding: 0 26px; display: inline-flex; align-items: center; }
.hero-ctas-v6 .btn-primary { box-shadow: var(--shadow-gold); }

.hero-trust { display: flex; gap: 8px; margin-top: 22px; flex-wrap: wrap; }
.hero-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .8); border: 1px solid var(--line);
  font-size: .82rem; font-weight: 500; color: var(--ink-2);
  box-shadow: var(--shadow-sm); backdrop-filter: blur(6px);
}
.hero-chip svg { width: 15px; height: 15px; color: var(--accent); }

/* ============================================================
   Marquee
   ============================================================ */
.marquee {
  overflow: hidden; border-block: 1px solid var(--line);
  background: var(--bg); padding: 13px 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}
.marquee-track { display: flex; width: max-content; animation: h-marquee 34s linear infinite; }
.marquee:hover .marquee-track { animation-play-state: paused; }
.marquee-item {
  display: inline-flex; align-items: center; gap: 9px; padding: 0 26px;
  font-family: var(--font-num); font-size: .78rem; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted); white-space: nowrap;
}
.marquee-item svg { width: 14px; height: 14px; color: var(--accent); }
.marquee-item::after { content: ""; width: 4px; height: 4px; border-radius: 50%; background: var(--accent-line); margin-left: 26px; }

/* ============================================================
   Product carousel
   ============================================================ */
.rail-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.rail {
  display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  padding: 4px var(--gutter) 18px; margin: 0 calc(var(--gutter) * -1);
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { flex: 0 0 clamp(206px, 62vw, 250px); scroll-snap-align: start; }

.rail-card {
  position: relative; display: flex; flex-direction: column;
  border-radius: var(--radius); overflow: hidden; background: var(--bg);
  border: 1px solid var(--line); box-shadow: var(--shadow-sm);
  transition: transform 260ms var(--ease-out), box-shadow 260ms var(--ease-out);
}
.rail-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.rail-media {
  aspect-ratio: 4 / 5; display: grid; place-items: center; overflow: hidden;
  transition: background 300ms var(--ease-soft);
}
.rail-media > div { width: 100%; height: 100%; display: grid; place-items: center; }
.rail-media .vial { height: 74%; width: auto; transition: transform 420ms var(--ease-out); }
.rail-card:hover .rail-media .vial { transform: scale(1.06); }
/* Each card sits on its own tint, cycled so a row never repeats. */
.rail-card:nth-child(5n+1) .rail-media { background: var(--tint-gold); }
.rail-card:nth-child(5n+2) .rail-media { background: var(--tint-sage); }
.rail-card:nth-child(5n+3) .rail-media { background: var(--tint-steel); }
.rail-card:nth-child(5n+4) .rail-media { background: var(--tint-clay); }
.rail-card:nth-child(5n+5) .rail-media { background: var(--tint-violet); }

.rail-body { padding: 13px; display: flex; flex-direction: column; flex: 1; }
.rail-body h3 { font-size: .96rem; line-height: 1.2; }
.rail-cat { font-size: .76rem; color: var(--muted); margin-top: 2px; }
.rail-price { margin-top: 9px; font-family: var(--font-num); font-weight: 600; font-size: 1.02rem; }
.rail-price small { font-weight: 400; color: var(--muted); font-size: .74rem; }
.rail-actions { display: grid; gap: 6px; margin-top: 11px; }
.rail-badge {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  padding: 4px 10px; border-radius: var(--radius-pill);
  font-family: var(--font-num); font-size: .62rem; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
  background: var(--ink); color: #fff; box-shadow: var(--shadow-sm);
}
.rail-badge.gold { background: var(--gold-foil); color: #2b1f10; }

/* ============================================================
   Bento grid
   ============================================================ */
.bento { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.bento-cell {
  position: relative; overflow: hidden;
  border-radius: var(--radius); padding: 20px 18px; min-height: 156px;
  display: flex; flex-direction: column; justify-content: flex-end;
  transition: transform 260ms var(--ease-out), box-shadow 260ms var(--ease-out);
}
.bento-cell:hover { transform: translateY(-3px); }
.bento-cell h3 { font-size: 1rem; margin-bottom: 4px; }
.bento-cell p { font-size: .84rem; line-height: 1.45; }
.bento-cell .bento-num {
  font-family: var(--font-display); font-size: 2.7rem; font-weight: 600;
  line-height: 1; letter-spacing: -.03em; margin-bottom: 10px;
}
.bento-cell .bento-num sup { font-size: 1.1rem; font-weight: 500; }
.bento-cell svg { width: 22px; height: 22px; margin-bottom: auto; }
.b-gold   { background: var(--tint-gold);   box-shadow: 0 10px 26px rgba(161, 98, 7, .10); }
.b-gold .bento-num, .b-gold svg { color: var(--vivid-gold); }
.b-sage   { background: var(--tint-sage);   box-shadow: 0 10px 26px rgba(47, 122, 82, .10); }
.b-sage .bento-num, .b-sage svg { color: var(--vivid-sage); }
.b-steel  { background: var(--tint-steel);  box-shadow: 0 10px 26px rgba(44, 92, 138, .10); }
.b-steel .bento-num, .b-steel svg { color: var(--vivid-steel); }
.b-clay   { background: var(--tint-clay);   box-shadow: 0 10px 26px rgba(164, 86, 42, .10); }
.b-clay .bento-num, .b-clay svg { color: var(--vivid-clay); }
.b-ink    { background: #1c1917; box-shadow: var(--shadow-lg); }
.b-ink h3 { color: #fff; }
.b-ink p  { color: rgba(255, 255, 255, .68); }
.b-ink .bento-num { color: #e3c085; }
.b-ink svg { color: #e3c085; }
.bento-wide { grid-column: 1 / -1; }

/* ============================================================
   Promo panels (box + bulk) with falling tiles
   ============================================================ */
.promo-panel {
  position: relative; overflow: hidden; border-radius: var(--radius-lg);
  padding: 28px 22px; min-height: 210px;
  display: flex; flex-direction: column; justify-content: center;
  box-shadow: var(--shadow-md);
}
.promo-panel.p-dark { background: linear-gradient(150deg, #241c14, #3b2a15); }
.promo-panel.p-dark h3 { color: #fff; font-size: 1.5rem; }
.promo-panel.p-dark p { color: rgba(255, 255, 255, .7); }
.promo-panel.p-light { background: var(--tint-sage); }
.promo-panel > * { position: relative; z-index: 2; }
.promo-panel .btn { align-self: flex-start; margin-top: 16px; border-radius: var(--radius-pill); }

.rain { position: absolute; inset: 0; z-index: 1; opacity: .5; pointer-events: none; }
.rain-tile {
  position: absolute; top: 0; width: 46px; height: 46px; border-radius: 12px;
  display: grid; place-items: center; overflow: hidden;
  background: rgba(255, 255, 255, .07); border: 1px solid rgba(227, 192, 133, .16);
  animation: h-rain linear infinite;
}
.rain-tile .vial { height: 78%; width: auto; opacity: .85; }

/* ============================================================
   Section rhythm
   ============================================================ */
.section-v6 { padding: 44px 0; }
.section-head-v6 { margin-bottom: 20px; }
.section-head-v6 .eyebrow {
  font-family: var(--font-num); font-size: .66rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--accent);
}
.section-head-v6 h2 { font-size: 1.85rem; letter-spacing: -.02em; margin-top: 6px; }
.section-head-v6 p { margin-top: 8px; max-width: 52ch; }
.link-more {
  font-family: var(--font-num); font-size: .86rem; font-weight: 500;
  color: var(--ink); white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
}
.link-more svg { width: 15px; height: 15px; transition: transform 220ms var(--ease-out); }
.link-more:hover { color: var(--accent); }
.link-more:hover svg { transform: translateX(3px); }

/* Stagger children of a revealed group. */
.reveal-group > * { opacity: 0; transform: translateY(16px); transition: opacity 560ms var(--ease-out), transform 560ms var(--ease-out); }
.reveal-group.in > * { opacity: 1; transform: none; }
.reveal-group.in > *:nth-child(1) { transition-delay: 0ms; }
.reveal-group.in > *:nth-child(2) { transition-delay: 70ms; }
.reveal-group.in > *:nth-child(3) { transition-delay: 140ms; }
.reveal-group.in > *:nth-child(4) { transition-delay: 210ms; }
.reveal-group.in > *:nth-child(5) { transition-delay: 280ms; }
.reveal-group.in > *:nth-child(6) { transition-delay: 350ms; }
@media (prefers-reduced-motion: reduce) { .reveal-group > * { opacity: 1; transform: none; } }

/* Product cards inherit the new lift. */
.product-card {
  border-radius: var(--radius); overflow: hidden;
  transition: transform 260ms var(--ease-out), box-shadow 260ms var(--ease-out);
}
.product-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.product-media .vial { transition: transform 420ms var(--ease-out); }
.product-card:hover .product-media .vial { transform: scale(1.05); }

/* ============================================================
   Desktop
   ============================================================ */
@media (min-width: 768px) {
  .bento { grid-template-columns: repeat(4, 1fr); }
  .bento-cell { min-height: 178px; }
  .bento-wide { grid-column: span 2; }
  .hero-v6 h1 { font-size: 3.4rem; }
  .hero-collage { height: 360px; }
  .promo-panel { padding: 34px 30px; }
}

@media (min-width: 1024px) {
  .hero-v6 { padding: 60px 0 72px; }
  .hero-v6 .hero-inner { display: grid; grid-template-columns: 1.05fr .95fr; gap: 40px; align-items: center; }
  .hero-collage { height: 440px; margin-bottom: 0; order: 2; }
  .hero-v6 h1 { font-size: 3.9rem; }
  .hero-vial.v1 { height: 300px; }
  .hero-vial.v2 { height: 214px; }
  .hero-vial.v3 { height: 182px; }
  .hero-vial.v4 { height: 158px; }
  .hero-halo { width: 420px; height: 420px; }
  .section-v6 { padding: 68px 0; }
  .section-head-v6 h2 { font-size: 2.3rem; }
}

/* The promo bar wrapped to four stacked lines at 375px and ate 221px of an
   812px viewport, pushing the hero entirely below the fold. Two rows on
   mobile: offer above, code and timer below. */
@media (max-width: 639px) {
  .promo-bar {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: center;
    align-items: center;
    gap: 6px 10px;
    padding: 9px var(--gutter);
  }
  /* The eyebrow is decoration. At 375px it costs a whole row that the
     offer, the code and the timer all need more than it does. */
  .promo-eyebrow { display: none; }
  .promo-offer { grid-column: 1 / -1; font-size: 1.02rem; line-height: 1.1; }
  .promo-code { font-size: .7rem; padding: 5px 13px; letter-spacing: .12em; }
  .promo-timer { font-size: .72rem; padding: 4px 11px; }
  .promo-sun { width: 84px; height: 84px; right: -20px; bottom: -26px; }
}

/* The falling tiles were drifting straight across the headline and the CTA,
   and at .5 they competed with the text instead of sitting behind it.
   Confine them to the right of the panel and take them down. */
.rain { left: auto; right: 0; width: 46%; opacity: .26; }
@media (min-width: 768px) { .rain { width: 38%; } }

/* A dark primary button on a dark panel is invisible. On p-dark the
   primary action goes gold, which is also the one place the foil gets to
   act as a button rather than as text. */
.promo-panel.p-dark .btn-primary {
  background: var(--gold-foil); color: #2b1f10; border-color: transparent;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .3);
}
.promo-panel.p-dark .btn-primary:hover { color: #2b1f10; opacity: .93; }
.promo-panel h3 { margin-bottom: 6px; }

/* ============================================================
   v6 — PAGE SURFACES
   Restyles the existing PDP, shop, cart and prose classes onto
   the v6 tokens rather than inventing parallel ones, so there is
   still one class per thing.
   ============================================================ */

/* ---------- Research-use banner ---------- */
.ruo-banner {
  background: linear-gradient(100deg, #241c14, #3b2a15);
  border: none; color: rgba(255, 255, 255, .8);
  padding: 11px var(--gutter); font-size: .8rem; line-height: 1.45; text-align: center;
}
.ruo-banner strong { color: #f0b4a8; font-weight: 600; letter-spacing: .04em; }

/* ---------- PDP layout ---------- */
.pdp { padding-block: 20px 8px; }
.pdp-media {
  position: relative; background: var(--tint-gold);
  border-radius: var(--radius-lg); aspect-ratio: 1 / 1;
  display: grid; place-items: center; overflow: hidden;
}
.pdp-media::after {
  content: ""; position: absolute; left: 50%; top: 52%;
  width: 72%; aspect-ratio: 1; transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(161, 98, 7, .14), transparent 68%);
  animation: h-glow 7s ease-in-out infinite; pointer-events: none;
}
.pdp-media .vial { position: relative; z-index: 1; height: 78%; width: auto; max-width: none; animation: h-float-slow 5s ease-in-out infinite; }
.pdp-media .photo { position: relative; z-index: 1; }

.pdp-info { padding-top: 20px; }
.pdp-eyebrow { font-family: var(--font-display); font-style: italic; font-size: 1rem; color: var(--accent); }
.pdp h1 { font-size: 2.15rem; letter-spacing: -.03em; margin-top: 4px; }
.pdp-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.id-chip {
  padding: 6px 13px; border-radius: var(--radius-pill);
  border: 1px solid var(--line-2); background: var(--bg);
  font-family: var(--font-num); font-size: .74rem; color: var(--ink-2);
}
.pdp-tagline { margin-top: 14px; font-size: 1rem; line-height: 1.6; }

.price-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.price-row .price, .price-row .now { font-family: var(--font-display); font-size: 2rem; font-weight: 600; letter-spacing: -.02em; color: var(--ink); }
.price-row .was { font-family: var(--font-num); font-size: 1rem; color: var(--muted); text-decoration: line-through; }
.price-row .bundle-flag {
  padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--tint-sage); color: var(--vivid-sage);
  font-family: var(--font-num); font-size: .68rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
}
.price-note { font-size: .82rem; color: var(--muted); margin-top: 4px; }

/* Dose pills */
.mass-row { margin-top: 22px; }
.opt-label {
  display: block; font-family: var(--font-num); font-size: .68rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin-bottom: 9px;
}
.mass-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.mass-opt {
  min-width: 84px; padding: 12px 18px; border-radius: var(--radius-pill);
  border: 1.5px solid var(--line-2); background: var(--bg); cursor: pointer;
  font-family: var(--font-num); font-size: .92rem; font-weight: 600; color: var(--ink);
  transition: all 200ms var(--ease-soft);
}
.mass-opt:hover { border-color: var(--ink); }
.mass-opt.active { background: var(--ink); border-color: var(--ink); color: #fff; }

/* Volume tier cards */
.bundle-box { margin-top: 24px; }
.bundle-title {
  font-family: var(--font-num); font-size: .68rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px;
}
.bundle-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.bundle-card {
  position: relative; padding: 14px 12px; border-radius: var(--radius-sm);
  border: 1.5px solid var(--line); background: var(--bg); cursor: pointer; text-align: left;
  transition: all 200ms var(--ease-soft);
}
.bundle-card:hover { border-color: var(--line-2); transform: translateY(-2px); }
.bundle-card.active { border-color: var(--accent); background: var(--tint-gold); box-shadow: 0 6px 18px rgba(161, 98, 7, .13); }
.bundle-qty { font-family: var(--font-num); font-size: .92rem; font-weight: 600; }
.bundle-off { font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; color: var(--accent); line-height: 1.1; margin-top: 2px; }
.bundle-note { font-size: .74rem; color: var(--muted); margin-top: 2px; }
.bundle-badge {
  position: absolute; top: -8px; right: 8px; padding: 3px 9px; border-radius: var(--radius-pill);
  font-family: var(--font-num); font-size: .58rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  background: var(--ink); color: #fff;
}
.bundle-badge.b1 { background: var(--gold-foil); color: #2b1f10; }
.bundle-badge.b2 { background: var(--vivid-sage); color: #fff; }

/* Dispatch */
.dispatch {
  display: flex; align-items: flex-start; gap: 10px; margin-top: 18px;
  padding: 13px 15px; border-radius: var(--radius-sm);
  background: var(--success-tint); border: 1px solid var(--success-line);
}
.dispatch-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success-bar); margin-top: 6px; flex: none; animation: h-glow 2.6s ease-in-out infinite; }
.dispatch-main { font-size: .9rem; font-weight: 600; color: var(--ink); }
.dispatch-sub { font-size: .78rem; color: var(--ink-2); margin-top: 2px; }

/* ---------- Verified results panel ---------- */
.verify-panel {
  position: relative; overflow: hidden;
  background: linear-gradient(155deg, #1c1917 0%, #2b2118 100%);
  border-radius: var(--radius-lg); padding: 28px 22px; margin-top: 32px;
}
.verify-panel::before {
  content: ""; position: absolute; right: -60px; top: -60px; width: 220px; height: 220px;
  border-radius: 50%; background: radial-gradient(circle, rgba(227, 192, 133, .18), transparent 70%);
  animation: h-glow 8s ease-in-out infinite;
}
.verify-panel > * { position: relative; z-index: 1; }
.verify-panel .eyebrow { color: #e3c085; font-family: var(--font-num); font-size: .66rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; }
.verify-panel h2 { color: #fff; font-size: 1.6rem; margin-top: 6px; }
.verify-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; margin-top: 22px; }
.verify-stat strong { display: block; font-family: var(--font-display); font-size: 2rem; font-weight: 600; color: #e3c085; line-height: 1; }
.verify-stat span { display: block; font-size: .8rem; color: rgba(255, 255, 255, .62); margin-top: 5px; }
.verify-pills { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 20px; }
.verify-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .07); border: 1px solid rgba(227, 192, 133, .2);
  color: rgba(255, 255, 255, .85); font-family: var(--font-num); font-size: .72rem; font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase;
}
.verify-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success-bar); }
.verify-panel .btn { margin-top: 22px; background: #fff; color: var(--ink); border-color: #fff; border-radius: var(--radius-pill); }
.verify-panel .btn:hover { background: #fff; color: var(--ink); opacity: .92; }

/* ---------- Spec cards ---------- */
.spec-cards { display: grid; gap: 12px; margin-top: 16px; }
.spec-card { background: var(--tint); border-radius: var(--radius); padding: 18px; border: 1px solid var(--line); }
.spec-card h3 { font-size: .95rem; margin-bottom: 10px; }
.spec-card dl { display: grid; grid-template-columns: auto 1fr; gap: 7px 14px; font-size: .86rem; }
.spec-card dt { color: var(--muted); }
.spec-card dd { color: var(--ink); font-weight: 500; text-align: right; }

/* ---------- Sticky buy bar ---------- */
.buy-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-sticky);
  display: none; align-items: center; gap: 12px;
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, .94); backdrop-filter: blur(12px);
  border-top: 1px solid var(--line); box-shadow: 0 -4px 24px rgba(28, 25, 23, .08);
  transform: translateY(100%); transition: transform 300ms var(--ease-out);
}
.buy-bar.show { transform: none; }
.buy-bar .bb-info { flex: 1; min-width: 0; }
.buy-bar .bb-name { font-size: .88rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.buy-bar .bb-price { font-family: var(--font-num); font-size: .9rem; color: var(--ink-2); }
.buy-bar .btn { flex: none; height: 46px; padding: 0 22px; border-radius: var(--radius-pill); }

/* ---------- Shop hero ---------- */
.shop-hero {
  background:
    radial-gradient(110% 80% at 88% 0%, var(--tint-gold) 0%, transparent 60%),
    linear-gradient(170deg, #fff, var(--tint));
  padding: 30px 0 22px;
}
.shop-hero h1 { font-size: 2.3rem; letter-spacing: -.03em; }
.shop-hero h1 em { font-family: var(--font-display); font-style: italic; color: var(--accent); }
.shop-hero p { margin-top: 10px; max-width: 52ch; }

/* Promo tiles between the pills and the grid */
.shop-promos { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 20px; }
.shop-promo {
  position: relative; overflow: hidden; border-radius: var(--radius);
  padding: 16px; min-height: 104px; display: flex; flex-direction: column; justify-content: flex-end;
  transition: transform 240ms var(--ease-out);
}
.shop-promo:hover { transform: translateY(-3px); }
.shop-promo strong { display: block; font-size: .96rem; color: var(--ink); }
.shop-promo span { font-size: .78rem; color: var(--ink-2); }
.shop-promo .tag {
  align-self: flex-start; margin-bottom: auto; padding: 3px 9px; border-radius: var(--radius-pill);
  font-family: var(--font-num); font-size: .58rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  background: var(--bg); color: var(--accent);
}
.shop-promo.sp-gold { background: var(--tint-gold); }
.shop-promo.sp-sage { background: var(--tint-sage); }

/* ---------- Editorial prose ---------- */
.prose { padding-block: 8px 72px; }
.prose h2 {
  font-size: 1.5rem; letter-spacing: -.02em; margin-top: 40px; margin-bottom: 10px;
  padding-top: 20px; border-top: 1px solid var(--line);
}
.prose h2:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.prose h3 { font-size: 1.05rem; margin-top: 24px; margin-bottom: 6px; }
.prose p, .prose li { font-size: 1rem; line-height: 1.72; color: var(--ink-2); }
.prose p + p { margin-top: 14px; }
.prose ul, .prose ol { margin: 12px 0 12px 20px; display: grid; gap: 7px; }
.prose .callout {
  background: var(--tint-gold); border: 1px solid var(--accent-line);
  border-radius: var(--radius); padding: 18px 20px; margin: 22px 0;
}
.prose .callout p { color: var(--ink); }
.prose a { border-bottom: 1px solid var(--accent-line); }

/* First paragraph of a policy page reads as a standfirst. */
.page-head + .prose > p:first-child,
.prose > p.lede { font-size: 1.1rem; line-height: 1.65; color: var(--ink); }

.page-head { padding-block: 34px 18px; }
.page-head h1 { font-size: 2.3rem; letter-spacing: -.03em; }
.page-head h1 em { font-family: var(--font-display); font-style: italic; color: var(--accent); }
.page-head p { margin-top: 10px; max-width: 56ch; font-size: 1.02rem; }

/* ---------- Cart page ---------- */
.cart-wrap { padding-block: 8px 72px; }
.cart-row {
  display: grid; grid-template-columns: 88px 1fr auto; gap: 14px; align-items: start;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px; margin-bottom: 10px;
}
.cart-media { width: 88px; height: 88px; border-radius: var(--radius-sm); background: var(--tint-gold); display: grid; place-items: center; overflow: hidden; }
.cart-media > div { width: 100%; height: 100%; display: grid; place-items: center; }
.cart-media .vial { height: 84%; width: auto; }
.cart-summary {
  background: var(--tint); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px; margin-top: 18px;
}
.cart-summary h2 { font-size: 1.15rem; margin-bottom: 14px; }
.sum-row { display: flex; justify-content: space-between; font-size: .95rem; padding: 7px 0; }
.sum-row.total { border-top: 1px solid var(--line); margin-top: 8px; padding-top: 14px; font-weight: 600; }
.sum-row.total .num { font-family: var(--font-display); font-size: 1.5rem; font-weight: 600; }

/* ---------- FAQ ---------- */
.faq-item { border-bottom: 1px solid var(--line); }
.faq-q {
  width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 18px 34px 18px 0; position: relative;
  font-family: var(--font); font-size: 1rem; font-weight: 600; color: var(--ink);
}
.faq-q::after {
  content: ""; position: absolute; right: 6px; top: 50%; width: 9px; height: 9px;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: translateY(-70%) rotate(45deg); transition: transform 240ms var(--ease-out);
}
.faq-item.open .faq-q::after { transform: translateY(-30%) rotate(-135deg); }
.faq-a { display: none; padding-bottom: 18px; }
.faq-item.open .faq-a { display: block; }
.faq-a p { line-height: 1.7; }

/* ---------- Desktop ---------- */
@media (min-width: 768px) {
  .verify-stats { grid-template-columns: repeat(4, 1fr); }
  .spec-cards { grid-template-columns: repeat(2, 1fr); }
  .bundle-grid { grid-template-columns: repeat(4, 1fr); }
  .shop-promos { gap: 14px; }
  .shop-hero h1 { font-size: 3rem; }
  .page-head h1 { font-size: 2.9rem; }
}

@media (min-width: 1024px) {
  .pdp { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; align-items: start; padding-block: 34px 12px; }
  .pdp-media { position: sticky; top: 88px; }
  .pdp-info { padding-top: 0; }
  .pdp h1 { font-size: 2.7rem; }
  .verify-panel { padding: 40px 36px; }
  .verify-panel h2 { font-size: 2.1rem; }
  .cart-wrap { display: grid; grid-template-columns: 1fr 360px; gap: 32px; align-items: start; }
  .cart-summary { margin-top: 0; position: sticky; top: 88px; }
  /* The sticky buy bar is a phone affordance; on desktop the real button
     is still on screen, so a second one is noise. */
  .buy-bar { display: none !important; }
}

/* ============================================================
   Wordmark
   ------------------------------------------------------------
   Painted as a mask rather than an <img> so the single cached
   SVG can render ink on cream and white on the dark footer from
   one file. Inlining it three times per page would have cost
   36KB of markup to achieve the same thing.
   ============================================================ */
:root { --logo-url: url("../images/halotir-logo.svg"); }

.logo, .logo-light { display: inline-flex; align-items: center; }
/* Ratio is the new file's viewBox, 1175.04 x 579.09, so 2.029:1. Sized up
   from 34px: at 40px the wordmark occupies about 62% of the 64px header,
   which reads as the brand rather than as a favicon. */
.logo-img {
  display: block; height: 40px; width: 81px;
  background-color: currentColor;
  -webkit-mask-image: var(--logo-url); mask-image: var(--logo-url);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.logo { color: var(--ink); }
.logo:hover { color: var(--accent); }
.logo-light { color: #fff; }
.logo-light:hover { color: #fff; opacity: .85; }

.nd-head .logo-img { height: 34px; width: 69px; }
.gate-card .logo-img { height: 54px; width: 110px; margin: 0 auto 18px; }
.site-footer .logo-img { height: 44px; width: 89px; }

@media (min-width: 1024px) { .logo-img { height: 46px; width: 93px; } }

/* .site-footer a (0,1,1) outranks .logo-light (0,1,0), so the wordmark
   in the footer was picking up the muted link colour instead of white. */
.site-footer .logo-light { color: #fff; }
.site-footer .logo-light:hover { color: #fff; opacity: .82; }

/* wp_nav_menu here uses items_wrap '%3$s', which emits bare <li> with no
   <ul> around them. They still compute to display:list-item and draw a
   marker, which is the same bug the main nav had. */
.site-footer li { list-style: none; }
.site-footer ul { margin: 0; padding: 0; }


/* Reveal only hides when we know JS is running to un-hide it. Without this
   gate a failed main.js leaves every revealed section at opacity 0, which
   is a blank page. See the inline script in header.php. */
.reveal { opacity: 1; transform: none; }
.reveal-group > * { opacity: 1; transform: none; }
.js .reveal:not(.in) { opacity: 0; transform: translateY(14px); }
.js .reveal-group:not(.in) > * { opacity: 0; transform: translateY(16px); }

/* The reference bar is separated by a solid 2px gold rule, not a hairline.
   It also sits below the sticky header now, so it must never paint above
   it: position:relative with no z-index keeps it under the header's z-50. */
.promo-bar { border-bottom: 2px solid #c99a4e; z-index: auto; }

/* Three chips at 375px are three full rows, roughly 200px of hero spent on
   badges. One horizontally scrollable line instead, the same pattern the
   category pills already use, so they read as a strip rather than a stack. */
.hero-trust {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 2px var(--gutter) 4px;
  margin: 22px calc(var(--gutter) * -1) 0;
  scroll-snap-type: x proximity;
}
.hero-trust::-webkit-scrollbar { display: none; }
.hero-chip { flex: none; scroll-snap-align: start; padding: 7px 13px; font-size: .8rem; }

@media (min-width: 768px) {
  .hero-trust { flex-wrap: wrap; overflow: visible; margin-inline: 0; padding-inline: 0; }
}

/* The promo code pill is a real button (it copies the code) but the compact
   mobile bar left it 27px tall, under any sane touch minimum. Keep the
   visual size, extend the hit area past the pill with a transparent
   overlay so the bar does not grow. */
.promo-code { position: relative; min-height: 30px; }
.promo-code::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}

/* ============================================================
   Disclaimer tiers 2 and 3
   ------------------------------------------------------------
   Four tiers, each doing a different job. The footer block is the
   only place on the site that cites the statute, which is what
   lets the product banner stay one line. The notice card is the
   only place that states the condition of sale on the page
   itself rather than at the checkout.
   ============================================================ */
.fda-disclaimer {
  border: 1px solid rgba(227, 192, 133, .22);
  border-radius: var(--radius-sm);
  padding: 16px 18px;
  margin-bottom: 20px;
  background: rgba(255, 255, 255, .03);
}
.fda-disclaimer h4 {
  font-family: var(--font-num); font-size: .66rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: #e3c085; margin-bottom: 8px;
}
.fda-disclaimer p { font-size: .78rem; line-height: 1.6; color: rgba(255, 255, 255, .78); }
.fda-disclaimer strong { color: #fff; font-weight: 600; }
.fda-disclaimer .text-link { display: inline-block; margin-top: 10px; font-size: .78rem; color: #e3c085; }
.fda-disclaimer .text-link:hover { color: #fff; }

.research-notice {
  border: 1px solid var(--line);
  border-left: 3px solid #b45309;
  border-radius: var(--radius-sm);
  background: var(--tint);
  padding: 20px;
  margin-top: 32px;
}
.research-notice h3 {
  font-family: var(--font-num); font-size: .72rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: #b45309; margin-bottom: 10px;
}
.research-notice p { font-size: .88rem; line-height: 1.62; }
.research-notice p + p { margin-top: 10px; }
.research-notice strong { color: var(--ink); font-weight: 600; }
/* ============================================================
   v7 · Shared foundation
   ------------------------------------------------------------
   Loads after the v1-v6 base. Everything every surface agrees on
   lives here so no surface has to invent it locally.

   The three decisions carried over from the teardown of the
   category leader, because each one is doing real work:

   1. One motion pairing. 200ms on cubic-bezier(.4,0,.2,1) for
      every state change. Their site uses that exact pairing 4,966
      times. Consistency at this scale stops reading as animation
      and starts reading as responsiveness.

   2. Stadium controls. Every button, input, chip and badge is a
      pill. They use `rounded-full` 7,193 times against 1,287 for
      the next radius down. No squared interactive control exists
      anywhere on their site, and that single choice defines the
      aesthetic more than any colour does.

   3. Flat at rest. Cards carry no shadow until touched, then take
      a hairline ring plus a soft two-layer lift. Nothing "pops"
      until you reach for it.

   What we deliberately do NOT copy: their pastel quartet. A lab
   supplier that looks like a sweet shop stops reading as a lab
   supplier. Our warm tints stay, and gold stays the one accent.
   ============================================================ */

:root {
  /* Motion. --ease-soft already is cubic-bezier(.4,0,.2,1); this
     names the duration that goes with it so the pair travels
     together and nobody reaches for 300ms out of habit. */
  --dur-ui: 200ms;
  --motion: var(--dur-ui) var(--ease-soft);

  /* Elevation. Flat at rest, so there is no --shadow-rest. */
  --lift: 0 0 0 1px rgba(28, 25, 23, .06),
          0 1px 2px -1px rgba(28, 25, 23, .10),
          0 8px 20px -12px rgba(28, 25, 23, .22);
  /* Bars pinned to the bottom of the viewport cast upward. */
  --lift-up: 0 -4px 20px rgba(28, 25, 23, .08);

  /* Focus. Ink at 20%, never blue, never the accent: the accent
     already means "this is the action", so reusing it for focus
     makes a focused link look like a button. */
  --ring: 0 0 0 2px var(--bg), 0 0 0 4px rgba(28, 25, 23, .22);

  /* The bottom bar is 1 to 2 rows plus the home indicator. Pages
     that carry one add this to their padding so the last element
     is never trapped underneath it. */
  --bar-h: 76px;
  --safe-b: env(safe-area-inset-bottom, 0px);

  /* Every full-width strip that sits under the header is this tall.
     There were three of them, each with its own padding and its own
     font size, so a product page opened with a 90px promo slab, a
     72px legal slab and a header, and the bottle started below the
     fold on a phone. One height is what makes them read as a stack of
     bands rather than as three unrelated accidents.

     54px would have been tidier, but the promo bar's code pill is a
     real button and stays at the 44px touch minimum; 44 plus the
     band's own padding and border is 58, and matching the others up
     to it beats shrinking a tap target to hit a round number. */
  --strip-h: 58px;
}

/* ---------- Stadium controls ---------- */
.btn, .qty, .mass-opt, .box-add, .slot-cta, .icon-btn, .chip,
.verify-pill, .id-chip, .coa-btn, .box-coa, .filter-chip,
input[type="search"], input[type="text"], input[type="email"],
input[type="tel"], select {
  border-radius: var(--radius-pill);
}

/* ---------- iOS will zoom any input under 16px ---------- */
/* This is not a preference. Safari zooms the viewport on focus and
   does not zoom back out, which strands the user mid-form. The
   quantity field was 15.2px and did exactly that. */
input, select, textarea,
.qty input, .qty input.qty, .qty input[type="number"] {
  font-size: 16px;
}

/* ---------- Focus ---------- */
:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--radius-pill);
}
.card:focus-visible, .box-card:focus-visible, .rail-card:focus-visible {
  border-radius: var(--radius-lg);
}

/* ---------- Full-bleed snap scrollers keep their gutter ----------
   Every horizontal rail on the site is built the same way: negative
   margins to reach the screen edges, matching padding to put the first
   item back on the text margin, and scroll snapping. That combination
   has one trap, and all four of these fell into it. scroll-padding
   defaults to auto, so snapping measures from the scrollport edge and
   not from the padding edge; the browser pulls the first item's
   leading edge flush to the bezel, scrolls the rail by exactly the
   gutter, and the item sits clipped with its rounded corner cut off.
   .rail had it worst because its snap type is mandatory and the
   misalignment was there on load, but proximity only defers the same
   result to the first swipe. One declaration fixes the class of bug. */
.rail,
.chip-row,
.cat-pills,
.hero-trust {
  scroll-padding-inline: var(--gutter);
}

/* ---------- Flat at rest ---------- */
.box-card, .rail-card, .spec-card, .coa-item, .step-card, .product-card {
  box-shadow: none;
  transition: box-shadow var(--motion), transform var(--motion);
}
@media (hover: hover) {
  .box-card:hover, .rail-card:hover, .spec-card:hover,
  .coa-item:hover, .step-card:hover, .product-card:hover {
    box-shadow: var(--lift);
  }
}
/* Touch has no hover, so it needs a press state instead. Without
   one a tap on a card gives no feedback at all. */
.box-card:active, .rail-card:active, .product-card:active,
.btn:active, .box-add:active, .mass-opt:active {
  transform: scale(.985);
}

/* ---------- One transition pairing ---------- */
.btn, .mass-opt, .box-add, .slot-cta, .icon-btn, .chip, .filter-chip,
.bundle-card, .box-coa, .coa-btn, a, button {
  transition: background-color var(--motion), border-color var(--motion),
              color var(--motion), box-shadow var(--motion),
              transform var(--motion), opacity var(--motion);
}

/* ---------- Numerals that must not jitter ---------- */
/* Any figure that changes in place (counters, running totals, the
   purity number) needs fixed-width digits or it visibly reflows. */
.display-num, .price, .now, .was, .verify-stat strong,
.slot-info strong, .box-price .now, .bb-price, .cart-count {
  font-variant-numeric: tabular-nums;
}

/* ---------- Bottom-bar clearance ---------- */
/* Every page carrying a fixed bottom bar sets this class on body,
   so the bar can never sit on top of the last row of content. */
body.has-bottom-bar {
  padding-bottom: calc(var(--bar-h) + var(--safe-b));
}

/* ---------- Shared bottom bar shell ---------- */
.bottom-bar {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: var(--z-sticky);
  background: rgba(255, 255, 255, .94);
  backdrop-filter: blur(12px) saturate(1.4);
  border-top: 1px solid var(--line);
  box-shadow: var(--lift-up);
  padding: 10px var(--gutter) calc(10px + var(--safe-b));
  transform: translateY(calc(100% + var(--safe-b)));
  transition: transform 300ms var(--ease-out);
}
.bottom-bar.show { transform: none; }
@media (min-width: 1024px) { .bottom-bar { display: none !important; } }

/* ---------- Type floor ----------
   Two floors, not one, because two different jobs are involved.

   11px is the floor for uppercase tracked eyebrows and badges. That
   is not a compromise: the category leader uses 10 and 11px for
   these thousands of times, and a tracked uppercase label reads
   larger than its point size suggests. Anything under 11px is where
   it stops being small and starts being unreadable, and a 9.28px
   badge was over that line.

   13px is the floor for anything a buyer actually has to read:
   prices, notes, spec values, captions. */
.badge-new, .badge, .promo-eyebrow, .nd-group, .eyebrow,
.proof-eyebrow, .stat-cap span, .proof-card dt,
/* These three are single-class rules losing to descendant selectors in
   the base (0,2,0 beats 0,1,0), which is how .eyebrow kept rendering at
   10.56px after the floor was set. Matching their specificity is what
   makes the floor actually apply; the parts layer loads last, so equal
   specificity resolves our way. */
.section-head-v6 .eyebrow, .partner-hero .eyebrow, .verify-panel .eyebrow {
  font-size: .6875rem;   /* 11px */
  letter-spacing: .08em;
}

/* A link laid over a card whose primary action is a different button
   100px below it. At 31px it was the smallest real target on the page
   and the easiest to hit by accident on the way to the card. */
.box-coa { min-height: 44px; padding-inline: 12px; }

/* The sitewide FDA block is tier 2 of four, and per docs/COMPLIANCE.md
   it is the load-bearing one: the only place citing the Federal Food,
   Drug and Cosmetic Act, and what buys the shorter tiers the right to
   be short. Its heading was rendering at 10.56px, the smallest text on
   the page. A disclaimer nobody can read is a disclaimer that is not
   really there, and it is read by processors and regulators before
   anyone else. */
.fda-disclaimer h4 { font-size: .8125rem; letter-spacing: .06em; }
.fda-disclaimer p  { font-size: .8125rem; line-height: 1.55; }

/* Sits after the 11px rule so it wins for the readable tier. */
.slot.free, .bundle-badge, .verify-panel .eyebrow, .coa-seal,
.coa-meta dt, .bundle-title, .opt-label { font-size: .75rem; }
.verify-pill, .box-coa, .trust-strip div, .bundle-note, .id-chip,
.box-cat, .box-price .in-box, .slot-info span, .dispatch-sub,
.coa-fine, .ruo-banner, .verify-stat span, .price-note,
.promo-code, .promo-timer,
.box-price .was, .spec-card dl { font-size: .8125rem; }

/* ---------- Tap targets ----------
   A link is a target too. These measured under 44px on a live phone:
   a text link at 22px is a coin-flip for a thumb. Padding rather
   than height so inline links keep their baseline. */
.link-more, .cd-close, .cd-x, .promo-code {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* Icon-only buttons must centre on BOTH axes. The rule above replaced
   the base's `display: grid; place-items: center` with inline-flex and
   only re-stated the cross axis, which left every glyph pinned to the
   left edge of its 44px button: an X floating off-centre inside a grey
   circle. align-items alone is never enough when you change display. */
.cd-close, .cd-x { justify-content: center; }
/* The quantity numeral is flanked by two 44px buttons, so it does not
   need to be a target itself, but it must not be a 23px sliver
   either: it is the thing being read. */
.qty input { min-height: 44px; }

/* ---------- Motion off ---------- */
@media (prefers-reduced-motion: reduce) {
  .bottom-bar { transition: none; }
  .box-card:active, .rail-card:active, .product-card:active,
  .btn:active, .box-add:active, .mass-opt:active { transform: none; }
}
/* ============================================================
   v7 · Signature components
   ------------------------------------------------------------
   The two display patterns the teardown singled out as the
   category leader's most recognisable moves, translated into our
   palette rather than copied out of theirs.

   Their version and ours differ on purpose. They run a quartet of
   candy pastels; a lab supplier that looks like a sweet shop stops
   reading as a lab supplier, so ours runs the warm tints. Their
   accent stripe is a pastel gradient; ours is --gold-foil, which
   is the same gradient printed on the physical vial label. Same
   structural idea, our own brand carrying it.

   Both live here rather than inside one surface's file because
   they appear on several surfaces and must not drift apart.
   ============================================================ */

/* ---------- The stat strip ----------
   Huge display serif numeral, small sans caption underneath. The
   research calls this "the single most distinctive visual
   signature of the whole component". The contrast is the whole
   trick: the numeral has to be big enough to read as typography
   rather than as data.

   2x2 on a phone, 4-up from 768. Fixed tint order so the rhythm
   is the same everywhere it appears. */
.stat-strip {
  display: grid;
  /* minmax(0, 1fr), not 1fr. A 1fr track floors at the item's
     min-content, and a 44px display numeral over a two-word label
     measured 155px per cell: two of those plus the gap ran 16px past
     a 320px screen and put a horizontal scrollbar on the page. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
/* Below 350 the two-up cells are too narrow to carry a display
   numeral and a label without the label breaking mid-word. */
@media (max-width: 349px) {
  .stat-strip { grid-template-columns: minmax(0, 1fr); }
}
.stat-cell {
  border-radius: var(--radius);
  padding: 16px;
  min-height: 132px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--tint-gold);
}
.stat-cell:nth-child(4n + 2) { background: var(--tint-sage); }
.stat-cell:nth-child(4n + 3) { background: var(--tint-steel); }
.stat-cell:nth-child(4n + 4) { background: var(--tint-clay); }

.stat-fig {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 1;
  font-weight: 600;
  font-size: 2.75rem;
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* The unit steps down rather than matching, so the number reads as
   the subject and the unit as an annotation on it. */
.stat-fig sup,
.stat-fig .unit {
  font-size: .55em;
  font-weight: 500;
  vertical-align: baseline;
  margin-left: .04em;
}
.stat-cap { margin-top: 12px; }
.stat-cap strong {
  display: block;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink);
}
.stat-cap span {
  display: block;
  font-size: .75rem;
  line-height: 1.35;
  color: var(--ink-2);
  margin-top: 2px;
}

@media (min-width: 768px) {
  .stat-strip { grid-template-columns: repeat(4, 1fr); gap: 12px; }
  .stat-cell { min-height: 156px; padding: 20px; }
  .stat-fig { font-size: 3.5rem; }
}

/* ---------- The proof card ----------
   One dark panel carrying a single very large figure. Its job is
   to be the only place on the page where a number is the hero, so
   never put two on one screen.

   The 3px foil rule pinned to the top edge is the same gradient as
   the vial label's foil, which is what ties a flat web panel back
   to a physical object. */
.proof-card {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--ink);
  color: #fff;
  padding: 24px 20px;
}
.proof-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--gold-foil);
}
.proof-eyebrow {
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: rgba(255, 255, 255, .55);
}
.proof-fig {
  display: flex;
  align-items: baseline;
  gap: 2px;
  margin: 10px 0 4px;
}
.proof-fig b {
  font-family: var(--font-num);
  font-size: 3.5rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -.03em;
  /* A figure that updates in place reflows without this, and the
     jitter is visible at 56px. */
  font-variant-numeric: tabular-nums;
}
.proof-fig i {
  font-style: normal;
  font-family: var(--font-num);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--accent-line);
}
.proof-card dl {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, .12);
}

/* Each cell is a two-row grid with the label pushed up and the value
   pinned to the bottom. Without this the columns are three independent
   stacks, so "Analyses per lot" wrapping to two lines on a phone
   dropped its "2" a whole line below "HPLC · MS" and "Independent",
   and three figures that are meant to be read across came out on two
   different baselines. */
.proof-card dl > div {
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: start;
}
.proof-card dt {
  font-size: .6875rem;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: rgba(255, 255, 255, .5);
}
.proof-card dd {
  font-size: .8125rem;
  font-weight: 500;
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}

@media (min-width: 768px) {
  .proof-card { padding: 30px 28px; }
  .proof-fig b { font-size: 4rem; }
}

/* ---------- Section ground rhythm ----------
   Depth without borders and without shadows: alternate a warm
   near-white against the page white. Two neighbouring sections
   should never carry the same ground. */
.ground-tint { background: var(--tint); }
.ground-gold { background: var(--tint-gold); }
.ground-ink  { background: var(--ink); color: #fff; }
.ground-ink h2, .ground-ink h3 { color: #fff; }
.ground-ink p { color: rgba(255, 255, 255, .72); }

/* ---------- Edge-bleeding chip row ----------
   The iOS-native pattern: a horizontally scrollable row that runs
   past the page gutter to both screen edges, so it reads as
   continuing off-screen rather than as a cut-off list. Hidden
   scrollbar, snap points, and no wrapping.

   Shared because three surfaces need it and each one hand-rolling
   it is how they end up 2px apart. */
.chip-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block: 2px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.chip-row::-webkit-scrollbar { display: none; }
.chip-row > * { flex: 0 0 auto; scroll-snap-align: start; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 44px outright, not 36px plus a padded ::after. The pseudo-element
     trick is the usual way to buy hit area without visual bulk, but
     .chip-row is an overflow-x scroll container, and it clips the
     vertical overflow that trick depends on. Measured on a phone, the
     extension caught nothing: the real target stayed 36px. The
     category leader uses 36px here; our own floor is 44px, and on a
     filter row a mis-tap costs the shopper the thing they were
     reaching for. */
  min-height: 44px;
  padding: 0 16px;
  font-size: .8125rem;
  font-weight: 500;
  color: var(--ink-2);
  background: var(--bg);
  border: 1px solid var(--line);
}
.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

@media (prefers-reduced-motion: reduce) {
  .chip-row { scroll-behavior: auto; }
}

/* ---------- The italic run inside a display heading ----------
   Every headline on the site puts one phrase into Fraunces italic
   and the rest into Anek roman, and where the italic came first the
   two words were touching. Measured with canvas text metrics on the
   shop headline at 390px, "Every vial, verified before it ships.":

     "verified"  Fraunces italic 600 32px
                 advance 116.77, ink right 121.48, so the ink
                 overhangs its own advance box by 4.71px
     "before"    Anek roman 600 32px
                 ink starts 1.79px left of its origin
     word space  5.61px of advance between the two runs

   5.61 minus 4.71 minus 1.79 is negative: the d of the italic and
   the b of the roman overlap by 0.89px at 32px, and by 1.3px at the
   48px desktop size. The same space after the roman "vial," yields
   6.26px of clear ink, which is why one gap in the line looks right
   and the other looks like a missing space. Nothing is missing. The
   space is there and the italic is standing in it.

   .18em restores about 4.9px of ink gap at 32px, a little under the
   roman-to-roman 6.26, which is where an italic-to-roman join wants
   to sit: the slant does some of the separating on its own. Being
   in em it holds at every heading size without a second rule.

   The rule is opted into with .em-midline on the heading, not hung
   off "h1 em". A blanket selector looked tidier and was wrong: on a
   centred heading a trailing margin is still part of the line box,
   so the line centres around it and the visible words sit half the
   margin off axis. Measured at 390px, the home hero "Research-grade
   peptides you can trust." drifted 2.9px left of the line above it.
   Only two headings on the site put roman text after the italic and
   need separating at all: the shop hero and the research library.
   Everywhere else the italic ends the heading.

   So the class marks the case rather than the tag: a heading whose
   italic run has words after it. A new headline written that way
   adds .em-midline and needs no new rule here.

   Punctuation goes inside the italic rather than after it, which is
   what .ruo-title already does with "confirmation.", so no rule has
   to special-case a period.
   ------------------------------------------------------------ */
h1.em-midline em,
h2.em-midline em {
  margin-right: .18em;
}
/* ============================================================
   The product page
   ------------------------------------------------------------
   Rebuilt against the category leader, surface by surface. The
   shape of the argument is theirs because it is a good argument:
   one bottle lit on a coloured ground, then a block that answers
   price, how many, what it costs at each quantity, and what
   happens after you pay, without the eye leaving the block.

   The palette is not theirs. Their accent is a teal that means
   "saving" and a crimson that means "was"; ours is one gold that
   already means "this is the action", so borrowing their colours
   would have given the page two competing accents. Savings speak
   in sage, the discounted figure in deep gold, and the gold stays
   the only thing that asks to be pressed.

   Everything here overrides css/styles.css, which loads first.
   Where a rule looks redundant it is usually beating a v1 or v6
   declaration in the base sheet; those are called out.
   ============================================================ */

/* ------------------------------------------------------------
   Skeleton
   ------------------------------------------------------------ */

.pdp {
  gap: 22px;
}

/* ------------------------------------------------------------
   Media: one bottle, floating, lit from behind
   ------------------------------------------------------------
   Three layers back to front: a tinted tile, a radial glow where
   the bottle stands, and the cutout carrying its own shadow off
   its alpha.

   The glow is the whole trick. A transparent PNG dropped on a
   flat tile reads as a sticker, because nothing in the picture
   agrees that the bottle occupies space. Putting light behind
   the glass is the one cue that fixes it, and it costs a single
   pseudo-element.
   ------------------------------------------------------------ */

/* All three resets here are corrections, not choices. The v6 block in
   the base sheet makes .pdp-media a centred grid with a 1:1 ratio,
   sized for a single drawn vial, and the tile now holds a stage plus a
   chip row instead.

   place-items:normal is the one that is easy to miss. Dropping the tile
   back to display:block does not neutralise place-items: box alignment
   applies to block layout too, so justify-items:center kept handing the
   stage justify-self:center, which means shrink-to-fit. The stage's only
   children are absolutely positioned, so its fit-content width is zero,
   and it laid out as a 0x0 box centred on the tile with the bottle
   invisible inside it. The ratio then had nothing to resolve against. */
.pdp-media {
  position: relative;
  display: block;
  place-items: normal;
  aspect-ratio: auto;
  gap: 0;
  padding: 0;
  /* A vignette, not a flat fill. --tint-gold on its own is #fdf6e9,
     which is close enough to white that a glow painted on top of it has
     nowhere to be brighter than. Lifting the centre and dropping the
     corners gives the bloom below something to sit against, and it is
     what turns the tile from a swatch into a lit space. */
  background: radial-gradient(
    112% 96% at 50% 42%,
    #fffdf8 0%,
    var(--tint-gold) 46%,
    #f7ecd6 100%
  );
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

/* The base sheet paints a radial wash on .pdp-media::after sized for
   the drawn vial. The stage below does that job properly now, and two
   washes on one tile just muddies the ground. */
.pdp-media::after { content: none; }

/* width:100% is stated rather than left to auto so the stage survives
   any future ancestor that aligns its children. A box whose contents
   are all out of flow has a fit-content width of zero, so every
   shrink-to-fit path collapses it silently. */
.pdp-stage {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
}

.pdp-glow {
  position: absolute;
  /* Low and centred: light comes from behind the bottle's body, not
     from the middle of an empty square. Sitting it at 58% down puts
     the brightest part behind the label, which is what the eye reads
     as the bottle being lit rather than the tile being painted. */
  left: 50%;
  top: 56%;
  width: 88%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  /* Two layers, because a single amber wash is the wrong model. Light
     behind glass is brighter than the surface it falls on and only
     picks up colour at its edges; one translucent brown circle is
     darker than the ground everywhere, which the eye reads as a shadow
     under the bottle rather than a lamp behind it.

     So: a warm white core does the lighting, and a wider amber ring
     underneath does the colour. The reference gets this for free by
     glowing a saturated green on a dark tile. Ours is gold on cream, so
     the same effect has to be built out of two stops instead of one. */
  background:
    radial-gradient(
      circle,
      rgba(255, 253, 245, .92) 0%,
      rgba(255, 249, 234, .58) 32%,
      rgba(255, 249, 234, 0) 62%
    ),
    radial-gradient(
      circle,
      rgba(186, 122, 22, .26) 0%,
      rgba(186, 122, 22, .15) 44%,
      rgba(186, 122, 22, 0) 74%
    );
  animation: h-glow 7s ease-in-out infinite;
  pointer-events: none;
}

/* Out of flow and filling the stage, with the breathing room expressed
   as padding rather than insets.

   Out of flow first, because an in-flow image restarts the circular
   measurement this file keeps running into: the stage would size to the
   bottle and the bottle would size against the stage.

   Padding second, and this one is a correction. The obvious spelling is
   inset:26px 26px 46px with width and height auto, but an absolutely
   positioned *replaced* element does not stretch to its insets the way a
   div does. width:auto means intrinsic width, which over-constrains the
   box, so the bottom inset is dropped and the height falls back to
   whatever max-height says. That handed the bottle the base sheet's
   max-height:100%, made it as tall as the whole stage, and pushed it
   26px out through the bottom of the tile and across the chips.
   inset:0 with a 100% box and border-box padding is immune: the padding
   is subtracted inside the box, object-fit:contain letterboxes the
   bottle in what is left, and the bottom stays deeper than the sides so
   the chips have somewhere to sit. */
.pdp-stage img.vial,
.pdp-stage .vial {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  padding: 14px 14px 40px;
  max-width: none;
  max-height: none;
  object-fit: contain;
  /* Longer and softer than the hero throw. This is the largest single
     bottle on the site and it sits on a tint, so a short hard shadow
     would read as a cut-out pasted down. */
  filter: drop-shadow(0 22px 34px rgba(83, 56, 12, .22));
  animation: h-float-slow 6s ease-in-out infinite;
}

/* The base sheet kills the float on .pdp-media .vial, from back when
   the product page showed a rectangular photograph and a floating
   rectangle looked absurd. It is a cutout now, so it floats. */
.pdp-media .vial { animation: h-float-slow 6s ease-in-out infinite; }

/* Chips ride the bottom edge of the tile, half on the ground and half
   off it. Overlapping is what keeps them attached to the photograph
   instead of reading as the first row of the text block below. */
.pdp-media-chips {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  padding: 0 14px 14px;
  margin-top: -26px;
}

.pm-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-sm);
  font-size: .75rem;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}

.pm-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--vivid-sage);
  flex: none;
}

/* ------------------------------------------------------------
   Head
   ------------------------------------------------------------ */

.pdp-info { padding-top: 4px; }

.pdp-eyebrow { margin-bottom: 6px; }

.pdp-chips { margin: 10px 0 0; }

.pdp-tagline { margin: 12px 0 0; }

/* A hairline between the description and the controls. Everything above
   it describes the compound, everything below it transacts, and without
   the rule the two read as one undifferentiated column. */
.mass-row,
.buy-head {
  border-top: 1px solid var(--line);
  padding-top: 18px;
  margin-top: 18px;
}

/* Except when they are adjacent: two rules stacked 18px apart is a
   ladder, not a separator. */
.mass-row + .cart .buy-head,
.mass-row + form.cart .buy-head {
  border-top: 0;
  padding-top: 0;
}

.mass-opt {
  border-radius: 10px;
  min-height: 44px;
  padding: 0 18px;
  font-weight: 650;
}

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

/* ------------------------------------------------------------
   Price and quantity, on one row
   ------------------------------------------------------------ */

.buy-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.buy-head-price { min-width: 0; }

.opt-label {
  display: block;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 7px;
}

/* The base sheet sets this row to space-between for a note-then-figure
   layout that no longer exists; the note moved into .buy-assure. */
.price-row {
  display: block;
  margin: 0;
}

.price-row .price {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 2.1rem;
  line-height: 1;
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--ink);
}

/* assets/pdp.js rewrites the inside of .price when a tier is chosen,
   into was + flag + now. The struck figure leads so the eye lands on
   the live one, which is the order the reference uses too. */
.price-row .was {
  font-size: 1.15rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
  order: 1;
}

.price-row .now {
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: -.02em;
  /* Deep gold, not crimson. Crimson is the category convention and it
     does read as "reduced", but on a page whose only accent is gold it
     arrives as a second accent and an error colour at once. */
  color: var(--accent-deep);
  order: 2;
}

.price-row .bundle-flag {
  order: 3;
  align-self: center;
  padding: 4px 9px;
  border-radius: var(--radius-pill);
  background: var(--success-tint);
  border: 1px solid var(--success-line);
  color: var(--vivid-sage);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  text-decoration: none;
}

.qty-row { margin: 0; }

/* ------------------------------------------------------------
   The offer, one tap from the price
   ------------------------------------------------------------ */

.price-coupon {
  margin: 12px 0 0;
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--ink-2);
}

.price-coupon b {
  font-weight: 700;
  color: var(--ink);
  letter-spacing: .03em;
}

.coupon-tap {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 650;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1.5px;
  cursor: pointer;
  /* The tap target is bigger than the ink. An 11-character link is
     about 70px wide and 18px tall, which fails the 44px floor in
     every direction that matters on a phone. */
  min-height: 44px;
  padding-inline: 2px;
}

.coupon-tap:hover { color: var(--accent-deep); }

.price-coupon.is-applied .coupon-tap {
  color: var(--vivid-sage);
  text-decoration: none;
  cursor: default;
}

/* Replaces the link text with whatever the handler decided actually
   happened: applied now, or applied at checkout. */
.price-coupon.is-applied .coupon-tap::after {
  content: "✓ " attr(data-note);
}
.price-coupon.is-applied .coupon-tap { font-size: 0; }
.price-coupon.is-applied .coupon-tap::after { font-size: .8125rem; }

/* ------------------------------------------------------------
   Bundle tiers
   ------------------------------------------------------------
   A horizontal rail on a phone and a four-across grid on a
   desktop. Stacked full-width rows were the old layout and they
   were fine, but they cost four screen-heights of the buy box and
   they hid the one comparison that matters: the tiers side by
   side, drawn as the number of bottles each one is.
   ------------------------------------------------------------ */

.bundle-box {
  display: none;
  margin-top: 20px;
}

.bundle-box.revealed { display: block; }

.bundle-title {
  display: block;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
}

/* Two by two on a phone, four across on a desktop. Never a scroller.
 *
 * This started as an edge-to-edge horizontal rail, and on a 375px
 * screen it fit exactly 1.9 cards. Two of the four tiers were off the
 * right edge, including the one that saves the most, and mandatory snap
 * pulled the first card's gutter away so it sat clipped against the
 * bezel. A rail is the right pattern for a catalogue nobody has to read
 * to the end of. It is the wrong pattern for a four-option control,
 * where the whole point is comparing the options against each other:
 * anything you have to swipe to find, most people never priced. */
.bundle-rows {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Row gap is the wider one because the ribbons overhang the top edge
     of the second row by 9px and would otherwise sit on the first. */
  gap: 20px 10px;
  padding: 14px 0 6px;
}

.bundle-card {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  justify-items: center;
  align-content: center;
  gap: 6px;
  min-height: 132px;
  padding: 16px 10px 14px;
  background: var(--bg);
  border: 1.5px solid var(--line);
  border-radius: 14px;
  text-align: center;
  cursor: pointer;
}

.bundle-card.active {
  border-color: var(--accent);
  background: var(--tint-gold);
  box-shadow: var(--shadow-gold);
}

.bundle-card:active { transform: scale(.985); }

/* The radio dot is gone. The whole card is the control now, and its
   selected state is a tint and a gold edge, which is legible at a
   glance across a rail in a way a 22px mark never was. */
.bundle-mark { display: none; }

/* ------------------------------------------------------------
   The bottle count, drawn as bottles
   ------------------------------------------------------------
   Overlapped rather than spaced, so three bottles read as a group
   and still fit the width one bottle had. The negative margin is
   what does it; the drop shadow on each cutout is what keeps them
   from collapsing into one silhouette.
   ------------------------------------------------------------ */

.bundle-vials {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  flex: none;
  height: 46px;
}

.bundle-vials img.vial,
.bundle-vials .vial {
  width: auto;
  height: 100%;
  max-width: none;
  object-fit: contain;
  filter: drop-shadow(0 2px 4px rgba(83, 56, 12, .3));
  animation: none;
}

.bundle-vials img.vial + img.vial,
.bundle-vials .vial + .vial { margin-left: -11px; }

/* Each bottle behind the one in front, so the stack reads front to
   back rather than as a row of flat stickers. */
.bundle-vials img.vial:nth-child(1) { z-index: 4; }
.bundle-vials img.vial:nth-child(2) { z-index: 3; }
.bundle-vials img.vial:nth-child(3) { z-index: 2; }
.bundle-vials img.vial:nth-child(4) { z-index: 1; }

/* Four bottles overlap harder than three so the bulk stack stays the
   width of the others. Widening the tile instead would break the
   two-column grid it sits in. */
.bundle-vials[data-count="4"] img.vial + img.vial,
.bundle-vials[data-count="4"] .vial + .vial { margin-left: -14px; }

.bundle-main {
  min-width: 0;
  text-align: center;
}

.bundle-qty {
  display: block;
  font-size: .9rem;
  font-weight: 700;
  color: var(--ink);
  line-height: 1.25;
}

.bundle-sub {
  display: block;
  margin-top: 2px;
  font-size: .8125rem;
  font-weight: 650;
  color: var(--vivid-sage);
}

.bundle-card:first-child .bundle-sub {
  color: var(--muted);
  font-weight: 500;
}

/* The per-unit price is the honest comparison between four tiers, and
   the stacked card finally has the vertical room to carry it. It used
   to be hidden below 1024px because the horizontal rail was 78px tall
   and there was nowhere to put it. */
.bundle-figs {
  display: block;
  text-align: center;
}

.bundle-unit {
  display: block;
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--ink);
}

.bundle-each {
  display: block;
  margin-top: 1px;
  font-size: .6875rem;
  color: var(--muted);
}

.bundle-badge {
  position: absolute;
  top: -9px;
  /* left:auto is load-bearing: css/styles.css:452 sets left:12px, and
     without this the badge anchors to both edges and stretches. */
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  /* 11px, not 10. This badge is the only thing distinguishing the
     three bundle tiers from each other, and it was the smallest type
     anywhere on the site. */
  font-size: .6875rem;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--accent);
  color: #fff;
  border: 0;
}

/* Three ribbons, three jobs, so they must not look alike: gold is the
   one most people pick, sage is the one that saves most per bottle,
   ink is the one that is a different kind of order entirely. */
.bundle-card:nth-child(3) .bundle-badge { background: var(--vivid-sage); }
.bundle-card:nth-child(4) .bundle-badge { background: var(--ink); }

.bundle-note {
  margin: 12px 0 0;
  font-size: .75rem;
  color: var(--muted);
}

/* ------------------------------------------------------------
   Buy row
   ------------------------------------------------------------ */

.buy-row {
  display: flex;
  gap: 10px;
  align-items: stretch;
  margin-top: 16px;
}

.buy-row .single_add_to_cart_button {
  flex: 1 1 auto;
  min-height: 54px;
  font-size: 1rem;
  font-weight: 700;
  border-radius: var(--radius-pill);
}

.buy-row .coa-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 54px;
  padding: 0 22px;
  border-radius: var(--radius-pill);
  font-weight: 650;
}

/* ------------------------------------------------------------
   What happens after you pay
   ------------------------------------------------------------
   One bordered list, five rows, hairline separated. The first row
   is tinted because it is the only one that is time-sensitive:
   everything else is true all day, that one stops being true at
   three o'clock.
   ------------------------------------------------------------ */

.buy-assure {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
  background: var(--bg);
}

.ba-row {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 12px;
  align-items: start;
  padding: 13px 15px;
  border-top: 1px solid var(--line);
}

.ba-row:first-child { border-top: 0; }

.ba-row[hidden] { display: none; }

.ba-row > svg {
  width: 20px;
  height: 20px;
  margin-top: 1px;
  color: var(--vivid-sage);
  flex: none;
}

.ba-live {
  background: var(--success-tint);
  border-color: var(--success-line);
}

.ba-main {
  margin: 0;
  font-size: .875rem;
  font-weight: 650;
  line-height: 1.35;
  color: var(--ink);
}

.ba-sub {
  margin: 2px 0 0;
  font-size: .8125rem;
  line-height: 1.4;
  color: var(--muted);
}

.ba-secure > svg { color: var(--muted); }
.ba-secure .ba-main {
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The old dispatch chip and three-cell trust strip are both gone; the
   list above says everything they said, once each. */
.dispatch, .trust-strip { display: none; }

/* ------------------------------------------------------------
   More ways to save
   ------------------------------------------------------------ */

.mws-grid {
  display: grid;
  gap: 12px;
  margin-top: 20px;
}

.mws-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 16px;
  align-items: center;
  padding: 18px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--bg);
  text-decoration: none;
  color: inherit;
}

.mws-gold { background: var(--tint-gold); border-color: var(--accent-line); }
.mws-sage { background: var(--tint-sage); border-color: #cfe3d6; }

.mws-fig {
  display: grid;
  justify-items: center;
  line-height: 1;
  flex: none;
}

.mws-fig b {
  font-family: var(--font-display);
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: -.02em;
}

.mws-gold .mws-fig b { color: var(--vivid-gold); }
.mws-sage .mws-fig b { color: var(--vivid-sage); }

.mws-fig small {
  margin-top: 2px;
  font-size: .625rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

.mws-body { min-width: 0; }

.mws-body strong {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  margin-bottom: 3px;
}

.mws-body span {
  display: block;
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--ink-2);
}

.mws-card > svg {
  width: 20px;
  height: 20px;
  color: var(--muted);
  flex: none;
  transition: transform var(--motion);
}

.mws-card:hover > svg { transform: translateX(3px); }

/* ------------------------------------------------------------
   Research updates
   ------------------------------------------------------------ */

.updates-card {
  padding: 30px 22px;
  border-radius: var(--radius-lg);
  background: var(--tint);
  border: 1px solid var(--line);
  text-align: center;
}

.updates-card h2 { margin: 6px 0 10px; }

.updates-card > p {
  max-width: 46ch;
  margin: 0 auto 18px;
  color: var(--ink-2);
  font-size: .9375rem;
  line-height: 1.6;
}

.updates-card .subscribe {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  max-width: 460px;
  margin: 0 auto;
}

.updates-card .subscribe input[type="email"] {
  flex: 1 1 200px;
  min-height: 50px;
  padding: 0 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  background: var(--bg);
  font: inherit;
  /* 16px floor, or iOS zooms the whole page on focus. */
  font-size: 16px;
}

.updates-card .subscribe input:focus-visible { box-shadow: var(--ring); outline: none; }

.updates-card .subscribe .btn {
  flex: 0 0 auto;
  min-height: 50px;
  padding: 0 26px;
  border-radius: var(--radius-pill);
}

.subscribe-msg {
  flex: 1 0 100%;
  margin: 2px 0 0;
  font-size: .8125rem;
  min-height: 1.2em;
}

.subscribe-msg.is-ok { color: var(--vivid-sage); }
.subscribe-msg.is-err { color: var(--vivid-clay); }

.subscribe.is-done input,
.subscribe.is-done .btn { display: none; }

.updates-card .fine {
  margin: 14px 0 0;
  font-size: .75rem;
  color: var(--muted);
}

/* The display italic on the closing word of a section heading. Used on
   exactly three headings on this page and nowhere else, so it reads as
   punctuation between blocks rather than as decoration. */
.section-head-v6 h2 em,
.updates-card h2 em {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
}

/* ------------------------------------------------------------
   Sticky buy bar
   ------------------------------------------------------------ */

.pdp-bar { display: grid; gap: 8px; }

.pdp-bar-info {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.pdp-bar-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .875rem;
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The offer, again, at bar size.
   Once the buy box has scrolled away this bar is the only price and the
   only button on screen, and the discount was living on two strips the
   buyer had already scrolled past. Same class, same data-coupon, same
   handler as the line in the buy box; the two settle together.

   Drawn as a pill rather than as the buy box's underlined link, because
   next to a filled Add button an underline reads as a footnote. It keeps
   the 44px target the link version was given: this is a secondary
   control but it is on a phone, sitting 8px from the primary one, and a
   near miss here taps Add. The name is what yields the width for it. */
.price-coupon-sm {
  flex: none;
  margin: 0;
  line-height: 1;
}
.price-coupon-sm .coupon-tap {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 12px;
  border-radius: var(--radius-pill);
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--accent-deep);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-decoration: none;
}
.price-coupon-sm.is-applied .coupon-tap {
  background: var(--tint-sage);
  box-shadow: inset 0 0 0 1px rgba(47, 122, 82, .25);
  color: var(--vivid-sage);
}
.price-coupon-sm.is-applied .coupon-tap::after { font-size: .6875rem; }

.pdp-bar-price {
  font-weight: 700;
  white-space: nowrap;
}

.pdp-bar-price .was {
  font-weight: 500;
  color: var(--muted);
  text-decoration: line-through;
  margin-right: 6px;
}

.pdp-bar-price .now { color: var(--accent-deep); }

.pdp-bar-row { display: flex; gap: 8px; align-items: stretch; }

.pdp-bar-masses { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.pdp-bar-masses::-webkit-scrollbar { display: none; }

.pdp-bar-masses .mass-opt {
  min-height: 46px;
  padding: 0 14px;
  font-size: .8125rem;
}

.pdp-bar-add {
  flex: 0 0 auto;
  min-height: 46px;
  padding: 0 22px;
  border-radius: var(--radius-pill);
  font-weight: 700;
}

body.single-product { --bar-h: 104px; }

body.single-product.has-bottom-bar .toast {
  bottom: calc(var(--bar-h) + var(--safe-b) + 14px);
}

/* ------------------------------------------------------------
   Desktop
   ------------------------------------------------------------ */

@media (min-width: 768px) {
  .mws-grid { grid-template-columns: 1fr 1fr; }
  /* One card left. The grid pairs the box offer with the volume ladder, so
     while Build a Box is switched off the ladder is the only card and a
     two-column track would strand it in the left half with dead space
     beside it. The card is a horizontal row at every width, so spanning
     the wrap reads as deliberate rather than as a missing sibling. */
  .mws-grid.mws-solo { grid-template-columns: 1fr; }
  .updates-card { padding: 44px 34px; }
}

@media (min-width: 1024px) {
  .pdp { gap: 52px; }

  .pdp-media { position: sticky; top: 96px; }

  .pdp-stage img.vial,
  .pdp-stage .vial { padding: 22px 22px 52px; }

  body.single-product.has-bottom-bar { padding-bottom: 0; }

  /* Two rows become one. The card itself is unchanged from mobile;
     only the column count moves, which is the whole reason the stacked
     card was promoted out of this block in the first place. */
  .bundle-rows {
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    padding: 14px 0 4px;
  }

  .bundle-vials { height: 42px; }

  .bundle-unit { font-size: 1.15rem; }

  /* Labels move inline beside their controls, which is what buys the
     vertical room for the tier cards to grow. */
  .mass-row,
  .buy-head {
    display: grid;
    grid-template-columns: 86px 1fr;
    align-items: center;
    gap: 14px;
  }

  .mass-row .opt-label,
  .buy-head .opt-label { margin-bottom: 0; }

  .buy-head {
    grid-template-columns: 1fr auto;
    align-items: end;
  }

  .buy-head-price .opt-label { margin-bottom: 6px; }

  .price-row .price,
  .price-row .now { font-size: 2.4rem; }
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .pdp-glow,
  .pdp-media .vial,
  .pdp-stage img.vial { animation: none; }
  .mws-card:hover > svg { transform: none; }
}

/* ============================================================
   SYNONYMS UNDER THE TITLE
   A researcher may know this compound as GSH or as ACTH(4-7)-PGP
   and not by the name in the heading. Set quiet: it is an index
   entry, not a claim, and it must not compete with the tagline
   directly under it.
   ============================================================ */
.pdp-syn {
  margin: 8px 0 0;
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--ink-2);
  letter-spacing: .01em;
}

/* ============================================================
   PUBLISHED LITERATURE
   ------------------------------------------------------------
   A numbered reference list, styled as a catalogue rather than a
   marketing section: the number is the anchor, the journal and
   year lead, the title carries the weight, authors trail.
   ============================================================ */
.lit-sec { padding: 48px 0 8px; }
.lit-sec .eyebrow { margin-bottom: 10px; }
.lit-sec h2 { margin: 0 0 12px; }
.lit-lede {
  max-width: 60ch;
  margin: 0 0 28px;
  color: var(--ink-2);
  line-height: 1.65;
}

.lit-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
  border-top: 1px solid var(--line);
}
.lit-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 16px;
  padding: 18px 0;
  border-bottom: 1px solid var(--line);
}
/* Tabular figures so 01/02/10 sit on one axis down the column. */
.lit-num {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
  padding-top: 2px;
  min-width: 24px;
}
.lit-body { min-width: 0; }
.lit-meta {
  margin: 0 0 4px;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.lit-meta span { margin: 0 4px; opacity: .5; }
.lit-title {
  margin: 0 0 4px;
  font-size: .9375rem;
  line-height: 1.5;
  color: var(--ink);
  font-weight: 500;
}
.lit-authors {
  margin: 0;
  font-size: .8125rem;
  color: var(--ink-2);
  font-style: italic;
}

@media (min-width: 768px) {
  .lit-sec { padding: 64px 0 16px; }
  .lit-item { grid-template-columns: 48px minmax(0, 1fr); gap: 20px; padding: 22px 0; }
}

/* Formula and sequence are data, not prose: monospace keeps the
   subscripts aligned and stops a long sequence from looking like a
   sentence that failed to wrap. */
.spec-formula, .spec-seq {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8125rem;
  letter-spacing: 0;
  word-break: break-word;
}
.spec-formula sub { font-size: .7em; }
/* ============================================================
   v7 · Build a Box
   ------------------------------------------------------------
   The category leader's one genuinely valuable piece of UI
   architecture, rebuilt on our tokens: main grid + sticky aside
   at >=1024px, and below that a fixed bottom tray (one full-width
   button) that opens a bottom sheet holding the slot list and CTA.

   Loads after styles.css and 10-tokens.css, so pills, motion,
   flat-at-rest and the .bottom-bar shell all come from there.
   The old .slot-bar rules in styles.css are left alone; the
   markup that used them no longer exists on the WooCommerce page.
   ============================================================ */

/* ---------- Stat strip spacing on this page ---------- */
/* The strip sits between the page head and the picker section,
   which brings its own 44px of padding, so only a little air is
   needed above. */
.box-stats { margin-block: 4px 8px; }

/* ---------- Two-column skeleton ---------- */
.box-layout { display: block; }
.box-aside { display: none; }

@media (min-width: 1024px) {
  .box-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;
    gap: 32px;
    align-items: start;
  }
  .box-aside { display: block; }

  /* styles.css goes 4-up at 1024 assuming a full-width grid. Next
     to a 400px aside that means ~150px cards, so hold 3-up until
     the main column is genuinely wide enough for four. */
  .box-layout .box-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1400px) {
  .box-layout .box-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ---------- Filter chips ---------- */
.box-filter { margin-bottom: 16px; }
/* The shared .chip is 36px tall for visual parity across surfaces.
   The tap minimum here is 44px, so the hit area is extended past
   the pill instead of growing the pill. */
.box-filter .chip { position: relative; }
.box-filter .chip::after { content: ""; position: absolute; inset: -4px 0; }

/* ---------- Sticky panel (desktop aside) ---------- */
.box-panel {
  position: sticky;
  top: calc(var(--header-h) + 20px);
  max-height: calc(100vh - var(--header-h) - 40px);
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--bg);
  padding: 20px;
}
.box-panel-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 14px;
}
.box-panel-head h2 { font-size: 1.15rem; }
.box-count {
  font-size: .8125rem;
  color: var(--muted);
  /* The count swaps digits in place; without this it reflows. */
  font-variant-numeric: tabular-nums;
}

/* ---------- Slot rows ---------- */
.box-slot-list { display: grid; gap: 6px; }
.box-slot {
  min-height: 56px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border: 1px dashed var(--line-2);
  border-radius: 16px;
  background: var(--bg);
}
.box-slot-token {
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: 50%;
  background: var(--panel);
  color: var(--muted);
  display: grid;
  place-items: center;
  font-family: var(--font-num);
  font-size: .75rem;
  font-weight: 600;
}
.box-slot-hint { font-size: .875rem; color: var(--muted); }

/* A filled slot is a real button whose whole 56px row removes the
   item, because hunting back up the grid to un-pick a card is the
   defect this rebuild exists to fix. */
button.box-slot {
  width: 100%;
  font: inherit;
  text-align: left;
  cursor: pointer;
  border-style: solid;
  border-color: var(--line-2);
  background: var(--tint);
  transition: border-color var(--motion), background-color var(--motion), transform var(--motion);
}
button.box-slot:hover { border-color: var(--ink); }
button.box-slot:active { transform: scale(.985); }
/* The global focus style rounds to a pill; these rows are 16px. */
button.box-slot:focus-visible { border-radius: 16px; }

.box-slot-thumb {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.box-slot-thumb > div { width: 100%; height: 100%; display: grid; place-items: center; }
.box-slot-thumb .vial { height: 82%; width: auto; }
.box-slot-thumb img { width: 100%; height: 100%; max-width: none; object-fit: cover; }
.box-slot-name {
  flex: 1;
  min-width: 0;
  font-size: .875rem;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.box-slot-x { flex: none; color: var(--muted); display: grid; place-items: center; }
.box-slot-x svg { width: 16px; height: 16px; }
button.box-slot:hover .box-slot-x { color: var(--ink); }

/* ---------- Free-gift row ---------- */
.box-gift {
  margin-top: 6px;
  border-color: var(--success-line);
  background: var(--success-tint);
  /* The 500ms unlock is the one place this page is allowed to move
     slower than --motion: it is a reward, not a state change. */
  transition: border-color 500ms var(--ease-soft), background-color 500ms var(--ease-soft);
}
.box-gift .box-gift-thumb,
.box-gift .box-slot-text,
.box-gift .box-gift-price {
  transition: opacity 500ms var(--ease-soft);
}
.box-gift[data-unlocked="false"] .box-gift-thumb,
.box-gift[data-unlocked="false"] .box-slot-text,
.box-gift[data-unlocked="false"] .box-gift-price { opacity: .5; }
.box-gift[data-unlocked="true"] { border-style: solid; }

.box-gift-thumb {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 12px;
  background: rgba(255, 255, 255, .7);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.box-gift-thumb > div { width: 100%; height: 100%; display: grid; place-items: center; }
.box-gift-thumb .vial { height: 82%; width: auto; }
.box-gift-thumb img { width: 100%; height: 100%; max-width: none; object-fit: cover; }

.box-slot-text { flex: 1; min-width: 0; }
.box-slot-text strong { display: block; font-size: .875rem; line-height: 1.25; }
.box-slot-text span { display: block; font-size: .75rem; color: var(--muted); line-height: 1.3; }

.box-gift-price { flex: none; text-align: right; }
.box-gift-price b {
  display: block;
  font-family: var(--font-num);
  font-size: .875rem;
  font-weight: 600;
  color: var(--muted);
}
.box-gift[data-unlocked="true"] .box-gift-price b { color: var(--success); }
.box-gift-price s {
  font-family: var(--font-num);
  font-size: .75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---------- CTA ---------- */
.box-cta {
  width: 100%;
  height: 48px;
  margin-top: 14px;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  cursor: pointer;
  font-family: var(--font-num);
  font-size: .92rem;
  font-weight: 600;
}
.box-cta:hover { background: var(--accent-deep); }
/* Disabled is a state of its own, not the enabled button dimmed:
   a washed-out gold still reads as "tap me". */
.box-cta:disabled {
  background: var(--panel);
  color: var(--muted);
  cursor: not-allowed;
}

/* ---------- Mobile tray ---------- */
/* The .bottom-bar shell (position, blur, border-top, safe area,
   show/hide transform) comes from 10-tokens.css. This layer only
   turns it into a single tappable button. */
.box-tray {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  cursor: pointer;
  color: var(--ink);
  font: inherit;
  /* Reset the UA button border; the shell's own border-top stays. */
  border-width: 1px 0 0;
}
.box-tray:focus-visible { border-radius: 0; }

.box-tray-tokens { display: flex; align-items: center; gap: 5px; flex: none; }
.tray-token {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 50%;
  border: 1.5px dashed var(--line-2);
  background: var(--bg);
  color: var(--muted);
  display: grid;
  place-items: center;
  overflow: hidden;
  font-family: var(--font-num);
  font-size: .75rem;
  font-weight: 600;
  transition: border-color var(--motion), background-color var(--motion);
}
.tray-token.filled {
  border-style: solid;
  border-color: var(--ink);
  background: var(--panel);
  color: var(--ink);
}
.tray-token > div { width: 100%; height: 100%; display: grid; place-items: center; }
.tray-token .vial { height: 78%; width: auto; }
.tray-token img { width: 100%; height: 100%; max-width: none; object-fit: cover; }
/* "Free" does not fit a 36px circle at the 12px type floor, so this
   one token is a stadium instead of a circle. */
.tray-token.tray-free {
  width: auto;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  border-color: var(--success-line);
  background: var(--success-tint);
  color: var(--success);
  transition: border-style var(--motion);
}
.tray-token.tray-free.filled {
  border-style: solid;
  border-color: var(--success-line);
  background: var(--success-tint);
  color: var(--success);
}

.box-tray-label { flex: 1; min-width: 0; text-align: right; }
.box-tray-label strong { display: block; font-size: 1rem; font-weight: 600; line-height: 1.2; }
.box-tray-sub {
  display: block;
  font-size: .8125rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.box-tray-chev { flex: none; color: var(--ink); }
/* The shared chevron glyph points right; the tray opens upward. */
.box-tray-chev svg { width: 20px; height: 20px; transform: rotate(-90deg); }

/* Five tokens, a label and a chevron share one row down to 320px
   only if the tokens give a little first. */
@media (max-width: 359px) {
  .tray-token { width: 32px; height: 32px; }
  .box-tray-tokens { gap: 4px; }
}

/* ---------- Bottom sheet ---------- */
.box-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  background: rgba(28, 25, 23, .44);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.box-scrim.show { opacity: 1; }

.box-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-drawer);
  background: var(--bg);
  border-radius: 20px 20px 0 0;
  box-shadow: var(--lift-up);
  padding: 10px var(--gutter) calc(16px + var(--safe-b));
  max-height: 82vh;
  overflow-y: auto;
  transform: translateY(102%);
  transition: transform var(--dur) var(--ease);
}
.box-sheet.open { transform: none; }

.box-sheet-grab {
  display: block;
  width: 36px;
  height: 4px;
  margin: 2px auto 10px;
  border-radius: var(--radius-pill);
  background: var(--line-2);
}
.box-sheet-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
.box-sheet-head h2 { font-size: 1.15rem; }
.box-sheet-close {
  margin-left: auto;
  width: 44px;
  height: 44px;
  flex: none;
  border: none;
  background: none;
  color: var(--muted);
  cursor: pointer;
  display: grid;
  place-items: center;
}
.box-sheet-close:hover { color: var(--ink); }
.box-sheet-close svg { width: 20px; height: 20px; }

body.box-sheet-open { overflow: hidden; }

/* Everything mobile-only dies at the desktop breakpoint, and the
   clearance padding must die with it: 10-tokens.css hides
   .bottom-bar there, so the page would otherwise keep 76px of
   padding for a bar that no longer exists. */
@media (min-width: 1024px) {
  .box-scrim, .box-sheet { display: none !important; }
  body.has-bottom-bar { padding-bottom: 0; }
}

/* ---------- COA chip ---------- */
/* Visually a small trust chip, but it is a live link sitting on a
   card whose primary action is elsewhere, so it gets a real 44px
   hit area without growing the pill. */
.box-coa::after { content: ""; position: absolute; inset: -9px -6px; }

/* ---------- Promo panel rain ---------- */
/* Six infinitely falling vials are a desktop garnish, not something
   a phone should spend its battery compositing. */
.box-promo .rain { display: none; }
@media (min-width: 1024px) {
  .box-promo .rain { display: block; }
}

/* ---------- Motion off ---------- */
@media (prefers-reduced-motion: reduce) {
  .box-scrim, .box-sheet, .box-gift,
  .box-gift .box-gift-thumb, .box-gift .box-slot-text, .box-gift .box-gift-price,
  .tray-token, button.box-slot { transition: none; }
  button.box-slot:active { transform: none; }
}
/* ============================================================
   v7 · Shop listing + homepage
   ------------------------------------------------------------
   Loads after styles.css, 10-tokens.css and 15-signature.css, so
   everything here is either new surface work or a deliberate
   override of the v6 layer. The reference is the category
   leader's mobile build (docs/research/02 + 03): their grid
   maths, their two-shade card, their one-hover-effect rule,
   carried on our warm tints instead of their pastels.

   90% of traffic is phones. Every unscoped rule below IS the
   phone layout; desktop only ever adds.
   ============================================================ */

/* ============================================================
   PLP · toolbar
   ============================================================ */

/* 44px matches the competitor's h-11 search pill exactly, and it
   doubles as the tap-target floor, so the field needs no hit-area
   tricks. Resting on the warm near-white rather than pure white
   separates it from the page without a heavier border. */
.search-field input {
  height: 44px;
  min-height: 44px;
  padding: 0 44px;
  background: var(--tint);
  border-color: var(--line);
}
.search-field input:focus {
  background: var(--bg);
  border-color: var(--ink);
}

/* The clear control only exists while there is something to
   clear; plp.js flips the hidden attribute. 40px visual inside a
   44px row, right where iOS puts its own field-clear affordance. */
.search-clear {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  border-radius: var(--radius-pill);
  color: var(--muted);
  cursor: pointer;
}
.search-clear:hover { color: var(--ink); background: var(--panel); }
.search-clear svg { width: 15px; height: 15px; }
.search-clear[hidden] { display: none; }

.sort-field select { min-height: 44px; }

/* Below 640 the bare select has to speak for itself; the label
   stays in the DOM for its accessible name (the reference site
   ships a nameless select here, an a11y gap we do not copy). */
@media (max-width: 639px) {
  .sort-field label {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .sort-field { position: relative; }
  .sort-field select { width: 100%; }
}
@media (min-width: 640px) {
  /* The competitor caps search at max-w-md so the pill never
     stretches into a canoe on wide screens. */
  .search-field { width: 100%; max-width: 448px; }
}

/* ============================================================
   PLP · category chips
   ------------------------------------------------------------
   The row itself is the shared .chip-row from 15-signature.css;
   the markup carries both class sets so main.js's .cat-pill
   contract keeps working. Only the tap-target extension lives
   here: the shared chip is 36px visually, our floor is 44, so
   the hit area grows past the pill instead of the pill growing
   past the system.
   ============================================================ */
.cat-pill.chip { position: relative; }
.cat-pill.chip::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
/* main.js toggles .active; signature styles [aria-pressed]. Alias
   them so the selected pill looks right even before plp.js has
   synced the attribute. */
.cat-pill.chip.active {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}
.cat-strip { margin-bottom: 18px; }

/* ============================================================
   PLP · promo banners
   ------------------------------------------------------------
   Was a 2-up grid of ~157px tiles at 375px, which is where copy
   goes to die. The competitor stacks these full-width on mobile
   and only goes 2-up at lg, as one-row banners: sticker disc,
   copy, chevron. Whole banner is the tap target.
   ============================================================ */
.shop-promos {
  grid-template-columns: 1fr;
  gap: 10px;
  margin-bottom: 18px;
}
.shop-promo {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 14px;
  min-height: 0;
  padding: 14px 16px;
}
/* The slight rotation is what makes the disc read as a placed
   sticker rather than another badge. Opposite tilts so the two
   banners do not look stamped from one template. */
.promo-disc {
  flex: none;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: var(--shadow-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  transform: rotate(-6deg);
  font-family: var(--font-num);
  color: var(--ink);
}
.sp-sage .promo-disc { transform: rotate(5deg); }
.promo-disc b { font-size: 1.05rem; font-weight: 600; line-height: 1; }
.promo-disc small { font-size: .75rem; font-weight: 500; color: var(--ink-2); line-height: 1.1; }
.promo-disc svg { width: 20px; height: 20px; color: var(--vivid-sage); }
.promo-txt { flex: 1; min-width: 0; }
.promo-txt strong { display: block; font-size: .95rem; color: var(--ink); }
.promo-txt span { display: block; font-size: .8125rem; color: var(--ink-2); line-height: 1.4; }
.promo-chev { flex: none; width: 18px; height: 18px; color: var(--ink-2); }
@media (min-width: 1024px) {
  .shop-promos { grid-template-columns: 1fr 1fr; gap: 14px; }
}

/* ============================================================
   PLP · grid
   ------------------------------------------------------------
   The competitor's measured ramp: 2-up with 12px gaps all the
   way to 768 (they never go 1-up on small phones; two narrow
   cards beat one huge one for scan speed), 3-up/16 at 768,
   4-up/24 at 1024. Restated in full here so the base file's
   640px gap bump cannot reorder it.
   ============================================================ */
.product-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
@media (min-width: 768px)  { .product-grid { grid-template-columns: repeat(3, 1fr); gap: 16px; } }
@media (min-width: 1024px) { .product-grid { grid-template-columns: repeat(4, 1fr); gap: 24px; } }

/* ============================================================
   PLP · the card
   ------------------------------------------------------------
   Two-shade construction: the card ground is a warm tint, the
   content panel a warm near-white. The tone step is what gives
   the card depth with no border and no resting shadow. Tints
   cycle in the same fixed order as the rail so a row never
   repeats.
   ============================================================ */
.product-card {
  height: 100%;
  background: var(--tint-gold);
}
.product-grid > .product-card:nth-child(5n + 2) { background: var(--tint-sage); }
.product-grid > .product-card:nth-child(5n + 3) { background: var(--tint-steel); }
.product-grid > .product-card:nth-child(5n + 4) { background: var(--tint-clay); }
.product-grid > .product-card:nth-child(5n + 5) { background: var(--tint-violet); }

/* v6 gave cards a translateY pop on hover. The system is now:
   flat at rest, hairline lift on hover (10-tokens), press on
   touch, and the image zoom is the only movement on the card
   body. The zoom stays; the pop goes. */
.product-card:hover,
.rail-card:hover { transform: none; }
@media (hover: hover) {
  /* Restated from 10-tokens because the unconditional reset
     above would otherwise outrank it in the cascade. */
  .product-card:hover,
  .rail-card:hover { box-shadow: var(--lift); }
}

/* The media tile must take a real photograph the day it lands:
   an img.photo fills the 4/5 box edge to edge (cover comes from
   the base layer), the drawn vial sits centred on the tint until
   then. Same zoom for both, 300ms, the competitor's number. */
.product-media { background: transparent; }
.product-media .vial {
  height: 76%;
  max-width: 70%;
  transition: transform 300ms var(--ease-soft);
}
.product-media img.photo {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  transition: transform 300ms var(--ease-soft);
}
@media (hover: hover) {
  .product-card:hover .product-media .vial,
  .product-card:hover .product-media img.photo { transform: scale(1.05); }
}

.card-body {
  background: var(--tint);
  padding: 12px;
}
/* Title over price, not beside it.
   Two cards fit across a 375px phone, so the row had about 134px to
   split between a name and a price. "Tesamorelin" is a single word 95px
   wide that cannot wrap, so flex shrank it under its own minimum and
   the -webkit-box clipped it to "Tesamorel" with no ellipsis to even
   signal it. Every SKU longer than about nine characters had the same
   problem waiting for it. Stacking removes the competition outright,
   and it is what the shelf this is modelled on does. */
.card-row { display: block; margin-bottom: 2px; }
.card-row h3 {
  font-size: 1rem;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.card-price {
  display: block;
  margin-top: 3px;
  font-weight: 600;
  font-size: 1rem;
  color: var(--ink);
  line-height: 1.25;
  text-align: left;
}
.card-meta {
  font-size: .8125rem;
  margin-bottom: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* CTA pinned with margin-top:auto so every card in a row
   bottom-aligns no matter how its title wrapped. 36px visual
   height per the competitor's h-9; the pseudo-element stretches
   the hit area back to the 44px floor without growing the pill. */
.product-card .qa-btn,
.product-card .card-btn,
.rail-card .qa-btn,
.rail-card .card-btn {
  position: relative;
  height: 36px;
  margin-top: auto;
  font-size: .8125rem;
}
.product-card .qa-btn::before,
.product-card .card-btn::before,
.rail-card .qa-btn::before,
.rail-card .card-btn::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
@media (min-width: 1024px) {
  .product-card { border-radius: var(--radius-lg); }
  .product-card .qa-btn,
  .product-card .card-btn,
  .rail-card .qa-btn,
  .rail-card .card-btn { height: 40px; }
}

/* ---------- Empty state ---------- */
/* Shared by the no-category-match state (#catEmpty) and the
   server-side no-search-results paragraph. Padded tall so the
   page does not collapse to a footer when the grid empties. */
.no-results { padding: 44px 0 56px; }
.no-results p { color: var(--muted); }
.no-results .btn { margin-top: 16px; }
.no-results[hidden] { display: none; }

/* ============================================================
   HOME · proof strip, as a marquee
   ------------------------------------------------------------
   The static version cut its sixth promise off at the viewport
   edge with nothing to say it had. See front-page.php for why
   this went back to moving.

   The mechanics: a viewport that clips, a track that holds two
   identical runs, and a translate of exactly -50%. When the
   first run has fully left, the second is pixel-for-pixel where
   the first began, so the reset is invisible. The gap between
   items has to live inside each run rather than between the two
   runs, or the seam gains a hole once per cycle.
   ============================================================ */
.proof-strip {
  background: var(--bg);
  border-block: 1px solid var(--line);
  padding: 12px 0;
  overflow: hidden;
}

.proof-viewport {
  overflow: hidden;
  /* Fade both ends so items dissolve rather than being guillotined at
     the edge. This is what a clipped static row could never do: the cut
     stops reading as a mistake and starts reading as continuation. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}

.proof-track {
  display: flex;
  width: max-content;
  animation: proof-scroll 38s linear infinite;
  /* Promoted so the scroll runs off the main thread. Without this the
     track repaints text every frame, which on a mid-range phone is
     exactly the cost that got the marquee removed the first time. */
  will-change: transform;
}

/* Pausing offscreen is handled by the shared [data-pause-offscreen].is-off
   rule further down this file, which main.js now actually drives. */

.proof-run {
  display: flex;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
  flex: none;
}

.proof-run li {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  font-family: var(--font-num);
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.proof-run li svg { width: 14px; height: 14px; color: var(--accent); }

/* The separator carries the spacing, and it is on every item including
   the last one. A :last-child exception would close the gap exactly at
   the seam between the two runs, which is the one place a gap is
   load-bearing. */
.proof-run li::after {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent-line);
  margin-inline: 18px;
}

@keyframes proof-scroll {
  to { transform: translate3d(-50%, 0, 0); }
}

/* Reduced motion gets the swipeable row back rather than a frozen
   marquee: a stopped track is the truncation bug again, and someone who
   has asked for less motion still deserves the sixth promise. */
@media (prefers-reduced-motion: reduce) {
  .proof-viewport {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .proof-viewport::-webkit-scrollbar { display: none; }
  .proof-track { animation: none; padding-inline: var(--gutter); }
  .proof-run[aria-hidden="true"] { display: none; }
}

/* ============================================================
   HOME · product rail
   ------------------------------------------------------------
   Same two-shade card as the PLP so the rail reads as a preview
   of the shop, not a third card design. Tint moves from the
   media div to the card itself (the body panel needs to sit on
   it), so the base layer's per-media tints are zeroed out.
   ============================================================ */
/* The heading takes the full width on a phone and the link drops
   beneath it, right-aligned. Side by side, a two-line display heading
   with a trailing italic ran within a few pixels of "View all" and the
   two read as one collided lump. */
@media (max-width: 767px) {
  .rail-head { flex-wrap: wrap; align-items: flex-start; gap: 0; }
  .rail-head > .section-head-v6 { flex: 1 1 100%; margin-bottom: 6px; }
  .rail-head > .link-more { margin-left: auto; }
}

.rail-card {
  background: var(--tint-gold);
  border: 0;
  box-shadow: none;
}
.rail-card:nth-child(5n + 2) { background: var(--tint-sage); }
.rail-card:nth-child(5n + 3) { background: var(--tint-steel); }
.rail-card:nth-child(5n + 4) { background: var(--tint-clay); }
.rail-card:nth-child(5n + 5) { background: var(--tint-violet); }
.rail-card:nth-child(5n + 1) .rail-media,
.rail-card:nth-child(5n + 2) .rail-media,
.rail-card:nth-child(5n + 3) .rail-media,
.rail-card:nth-child(5n + 4) .rail-media,
.rail-card:nth-child(5n + 5) .rail-media { background: transparent; }

.rail-media .vial { height: 76%; transition: transform 300ms var(--ease-soft); }
.rail-media img.photo {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  transition: transform 300ms var(--ease-soft);
}

/* ---------- Cutout cards ----------
   A cutout dropped straight onto a flat tint reads as a sticker, the
   same way it did on the product page, and the fix is the same two
   cues: light behind the glass and a shadow under it. The bloom is
   white rather than gold because the rail cycles five tints and a warm
   wash only works on one of them; white lifts sage, steel, clay and
   violet identically. */
/* Out of flow, and this is the third time this file's layout has hit
   the same shape, so it is worth naming. .rail-media is a grid with an
   auto row and a 4:5 ratio. An in-flow child asking for height:100%
   inside an auto track is circular, so the browser drops it to auto,
   the image falls back to its own 518:1100 ratio, and the track grows
   to 441px inside a 291px tile with overflow:hidden. The result is a
   bottle cropped top and bottom that looks like an object-fit bug and
   is actually a track-sizing one. Absolute positioning contributes
   nothing to track sizing, which ends it.

   width and height are stated rather than left to the insets because
   .rail-media carries place-items:center, and a centred abs-positioned
   box with auto size shrink-to-fits to nothing. */
.rail-media { position: relative; }

/* Keyed on .is-cut alone rather than on each wrapper class, because
   four surfaces now want this exact tile: the home rail, the related
   rail, the shop grid and the box picker. Whatever the outer box is
   called, it is positioned and carries the ratio; whatever the inner
   one is called, it is this. */
.is-cut {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: transform 300ms var(--ease-soft);
}
.is-cut::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 56%;
  width: 78%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(255, 255, 255, .58) 0%,
    rgba(255, 255, 255, .24) 42%,
    rgba(255, 255, 255, 0) 70%
  );
  pointer-events: none;
}
/* Absolute for the same reason the wrapper is, and definite for a
   second one worth writing down: height:76% with width:auto gives an
   image that has not downloaded yet a computed width of 0. Zero area
   means the element never intersects the viewport, lazy loading never
   fires, the image never downloads, and its width stays 0. The rail
   rendered five tinted tiles with no bottles in them and no network
   request to explain it. Sizing the box from the tile instead of from
   the file breaks both loops at once.

   The wrapper is still a grid with an auto row, so an in-flow image
   asking for height:100% would restart the cycle one level down. It
   did, once: the tile and the wrapper came out right at 233x291 and
   the bottle inside them was 449 tall. */
.is-cut .vial {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  padding: 11% 11% 13%;
  object-fit: contain;
  filter: drop-shadow(0 10px 16px rgba(83, 56, 12, .2));
  animation: h-float-slow 6s ease-in-out infinite;
}

/* Staggered, or five bottles bob in perfect unison and the rail looks
   mechanical rather than alive. */
.rail-card:nth-child(5n + 2) .is-cut .vial,
.box-grid > .box-card:nth-child(5n + 2) .is-cut .vial { animation-delay: -.9s; }
.rail-card:nth-child(5n + 3) .is-cut .vial,
.box-grid > .box-card:nth-child(5n + 3) .is-cut .vial { animation-delay: -1.8s; }
.rail-card:nth-child(5n + 4) .is-cut .vial,
.box-grid > .box-card:nth-child(5n + 4) .is-cut .vial { animation-delay: -2.7s; }
.rail-card:nth-child(5n + 5) .is-cut .vial,
.box-grid > .box-card:nth-child(5n + 5) .is-cut .vial { animation-delay: -3.6s; }

/* The shop grid gets the identical treatment, and deliberately the
   identical markup: .product-media is already relative with the same
   4:5 ratio and the same overflow, so once content-product.php nests
   the cutout wrapper inside it, every rule above applies untouched and
   there is one implementation of the tile rather than two.
   The tint underneath comes from .product-card, not the tile.

   Worth saying why the grid moved off photographs at all. The set
   photograph is 3:4 and the tile is 4:5, so cover cropped the bottle's
   base off; worse, the photograph carries its own tan backdrop, which
   painted over the five-tint rotation and made a row of four cards read
   as four identical tan rectangles. */
.product-grid > .product-card:nth-child(5n + 2) .is-cut .vial { animation-delay: -.9s; }
.product-grid > .product-card:nth-child(5n + 3) .is-cut .vial { animation-delay: -1.8s; }
.product-grid > .product-card:nth-child(5n + 4) .is-cut .vial { animation-delay: -2.7s; }
.product-grid > .product-card:nth-child(5n + 5) .is-cut .vial { animation-delay: -3.6s; }

@media (hover: hover) {
  .rail-card:hover .rail-media img.photo { transform: scale(1.05); }
  /* Scaled on the wrapper, not the bottle. The float animates the
     bottle's own transform, and an animation beats a plain declaration,
     so a hover rule on .vial would simply never apply. */
  .rail-card:hover .is-cut,
  .product-card:hover .is-cut,
  .box-card:hover .is-cut { transform: scale(1.05); }
}

.rail-body { background: var(--tint); padding: 12px; }
.rail-body h3 {
  font-size: 1rem;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* One price treatment for both rails and the shop card: figure first at
   full weight, unit label after it and quiet. The home rail used to put
   a "Per vial" caption above a right-aligned figure inside a flex row
   shared with the title, which is the arrangement that clips a long
   single-word name, and it read differently from the same product's
   card two screens away. */
.rail-price {
  display: flex;
  align-items: baseline;
  gap: 5px;
  margin-top: auto;
  padding-top: 9px;
  line-height: 1.2;
}
.rail-price small { font-size: .75rem; color: var(--muted); font-weight: 400; }
.rail-price .amount { font-family: var(--font-num); font-weight: 600; font-size: 1rem; color: var(--ink); }
.rail-cat {
  font-size: .8125rem;
  margin-bottom: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The card link is the whole card; only the CTA sits above it. */
.rail-card .qa-btn,
.rail-card .card-btn { z-index: 2; }

/* Badge: one per card, top-left over the image, pill with a
   hairline ring. 12px, not the competitor's 10, because nothing
   on a phone goes below 12. */
.rail-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-family: var(--font-num);
  font-size: .75rem;
  font-weight: 600;
  box-shadow: 0 0 0 1px rgba(28, 25, 23, .08), var(--shadow-sm);
}
@media (min-width: 1024px) {
  .rail-card { border-radius: var(--radius-lg); }
  .rail-body { padding: 16px; }
}

/* ============================================================
   HOME · hero, and one padding instead of two
   ------------------------------------------------------------
   .hero-inner still carries the padding-block it was given back
   when it was the hero: the band and the content were the same
   box. In .hero-v6 they are not, and the band pads itself, so
   the two stacked. At desktop that was 116px above the headline
   and 136px below the collage, a third of a 692px hero spent on
   nothing, and it read as the page trailing off before the first
   section rather than as breathing room.

   The band keeps the padding, the content gives its up, and the
   totals land slightly tighter than either was alone.
   ============================================================ */
.hero-v6 .hero-inner { padding-block: 0; }
.hero-v6 { padding-block: 44px 52px; }
@media (min-width: 1024px) {
  .hero-v6 { padding-block: 76px 84px; }
}

/* ============================================================
   HOME · offscreen animation pause
   ------------------------------------------------------------
   The hero floats and the promo rain are infinite animations.
   Fine while on screen, pure battery drain once scrolled past.
   plp.js flips .is-off from an IntersectionObserver.
   ============================================================ */
[data-pause-offscreen].is-off,
[data-pause-offscreen].is-off * { animation-play-state: paused !important; }

/* ============================================================
   Motion off
   ------------------------------------------------------------
   The press states are restated unconditionally above the base
   layers, so their reduced-motion guards need restating too.
   ============================================================ */
.product-card:active,
.rail-card:active { transform: scale(.985); }
@media (prefers-reduced-motion: reduce) {
  .product-card:active,
  .rail-card:active { transform: none; }
  .product-media .vial,
  .product-media img.photo,
  .rail-media .vial,
  .rail-media img.photo { transition: none; }
}

/* ------------------------------------------------------------
   Card proof row
   ------------------------------------------------------------
   The purity figure and the certificate, on the tile people
   actually choose from. Sits between the category line and the
   price on every card surface: home rail, shop grid, related
   rail. See halotir_card_proof().

   The COA link needs z-index. Every card here is an <article>
   with a stretched ::after link covering the whole tile, so a
   child that is meant to be separately clickable has to be
   lifted above it or the tile swallows the click.
   ------------------------------------------------------------ */
.card-proof {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 7px;
}
/* 30px, not 22. The certificate chip is a real link to the COA page,
   and at 22px tall with 10.4px type it was both under any reasonable
   thumb target and under the system's own floor of "nothing on a phone
   goes below 12". The purity chip matches it so the pair still reads as
   one row of proof rather than two different objects. */
.cp-purity,
.cp-coa {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding-inline: 10px;
  border-radius: var(--radius-pill);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}
.cp-purity {
  background: var(--tint-sage);
  box-shadow: inset 0 0 0 1px rgba(47, 122, 82, .2);
  color: var(--vivid-sage);
}
.cp-coa {
  position: relative;
  z-index: 2;
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line-2);
  color: var(--ink-2);
  text-decoration: none;
  transition:
    box-shadow var(--motion),
    color var(--motion);
}
@media (hover: hover) {
  .cp-coa:hover { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-deep); }
}
.cp-coa:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A 233px rail card cannot hold both chips on one line at 375px.
   Wrapping is fine; both chips keeping their own line is not, so
   the purity chip leads and the certificate follows it. */
@media (max-width: 380px) {
  /* Padding, not type size. Shrinking the font here is what produced
     10.4px chips on a phone, which is the one place they are read. */
  .cp-purity, .cp-coa { padding-inline: 8px; }
}

/* ------------------------------------------------------------
   Bulk panel · the arithmetic
   ------------------------------------------------------------
   Inside the dark promo panel, so the palette is the inverted
   one: light ink on the panel's own ground, and the savings
   line takes the gold rather than the sage, because sage on
   near-black reads as a system message.
   ------------------------------------------------------------ */
.bulk-math {
  position: relative;
  z-index: 1;
  margin: 16px 0 20px;
  padding: 14px 16px;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, .06);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.bm-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.bm-name { font-size: .9375rem; font-weight: 600; color: #fff; }
.bm-x { margin-left: 5px; font-weight: 400; color: rgba(255, 255, 255, .62); }
.bm-pct {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: 22px;
  padding-inline: 9px;
  border-radius: var(--radius-pill);
  background: rgba(227, 192, 133, .18);
  box-shadow: inset 0 0 0 1px rgba(227, 192, 133, .34);
  color: #e3c085;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.bm-figs { display: flex; align-items: baseline; gap: 10px; }
.bm-was {
  font-family: var(--font-num);
  font-size: 1rem;
  color: rgba(255, 255, 255, .5);
  text-decoration-thickness: 1px;
}
.bm-now {
  font-family: var(--font-num);
  font-size: 1.5rem;
  font-weight: 600;
  color: #fff;
  letter-spacing: -.01em;
}
.bm-save { margin: 4px 0 0; font-size: .8125rem; color: #e3c085; }
/* wc_price() wraps the amount in its own span, which would otherwise
   inherit the paragraph size and undo the emphasis. */
.bm-was .amount, .bm-now .amount, .bm-save .amount { font: inherit; color: inherit; }

@media (min-width: 768px) {
  .bulk-math { max-width: 340px; }
}

/* ============================================================
   Home FAQ
   ------------------------------------------------------------
   The objection block, sitting between the promo bento and the
   updates feed. Head and support CTA down the left, the
   accordion down the right, and on a phone the questions come
   before the "still stuck" line rather than after it.

   The panel animates on grid-template-rows, 0fr to 1fr, so it
   opens to whatever height its own answer needs with no
   measured pixel value in the JS and nothing to recompute on
   resize. The collapsed panel also goes visibility:hidden, on
   a delay that matches the collapse, because a zero-height
   overflow:hidden box still hands its links to the tab order.
   ============================================================ */

.hfaq-section { padding-block: 44px 48px; }
.hfaq-aside h2 { margin: 6px 0 0; font-size: 1.85rem; letter-spacing: -.02em; }
.hfaq-lede {
  margin: 10px 0 0;
  max-width: 46ch;
  color: var(--ink-2);
  line-height: 1.65;
}

.hfaq-list { margin-top: 24px; }
.hfaq-item { border-bottom: 1px solid var(--line); }
.hfaq-item:first-child { border-top: 1px solid var(--line); }
.hfaq-h { margin: 0; font-size: inherit; font-weight: inherit; letter-spacing: normal; }

.hfaq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  min-height: 62px;
  padding: 15px 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -.01em;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: color var(--motion);
}
@media (hover: hover) {
  .hfaq-q:hover { color: var(--accent-deep); }
  .hfaq-q:hover .hfaq-ico { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-deep); }
}
.hfaq-q:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

/* Plus to minus: the vertical stroke scales out of the horizontal
   one. A rotation would have been the obvious move and it is the
   wrong one, because rotating the chip takes the remaining stroke
   round with it. */
.hfaq-ico {
  position: relative;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line-2);
  color: var(--ink-2);
  transition: background var(--motion), box-shadow var(--motion), color var(--motion);
}
.hfaq-ico::before,
.hfaq-ico::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 11px;
  height: 1.6px;
  margin: -.8px 0 0 -5.5px;
  border-radius: 1px;
  background: currentColor;
  transition: transform 300ms var(--ease-out), opacity 180ms linear;
}
.hfaq-ico::after { transform: rotate(90deg); }
.hfaq-item.open .hfaq-ico {
  background: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  color: #fff;
}
.hfaq-item.open .hfaq-ico::after { transform: rotate(90deg) scaleX(0); opacity: 0; }

.hfaq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 340ms var(--ease-out);
}
.hfaq-item.open .hfaq-a { grid-template-rows: 1fr; }
.hfaq-a-in {
  overflow: hidden;
  visibility: hidden;
  transition: visibility 0s linear 340ms;
}
.hfaq-item.open .hfaq-a-in { visibility: visible; transition-delay: 0s; }
.hfaq-a-body {
  padding-bottom: 20px;
  max-width: 62ch;
  font-size: .9375rem;
  line-height: 1.75;
  color: var(--ink-2);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 180ms linear, transform 300ms var(--ease-out);
}
.hfaq-item.open .hfaq-a-body { opacity: 1; transform: none; transition-delay: 90ms; }
.hfaq-a-body p { margin: 0 0 10px; }
.hfaq-a-body p:last-child { margin-bottom: 0; }
.hfaq-a-body a {
  color: var(--accent-deep);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

.hfaq-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 18px;
  margin-top: 24px;
}
.hfaq-all {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  color: var(--ink-2);
  font-size: .9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: color var(--motion);
}
.hfaq-all svg { width: 15px; height: 15px; transition: transform 220ms var(--ease-out); }
.hfaq-all:hover { color: var(--ink); }
.hfaq-all:hover svg { transform: translateX(3px); }

@media (min-width: 900px) {
  .hfaq-section { padding-block: 72px 78px; }
  .hfaq-grid {
    display: grid;
    grid-template-columns: minmax(0, .86fr) minmax(0, 1.14fr);
    column-gap: clamp(40px, 6vw, 84px);
    align-items: start;
  }
  /* All three share one row, and the left column stacks by
     alignment rather than by a second row.

     The obvious version put the CTA in row 2 and spanned the list
     across both. A grid item that spans auto rows hands its height
     to those rows to divide up, so a 520px list made row 1 298px
     tall and left a 145px hole between a 153px aside and its own
     button. One row, top and bottom aligned, has no hole to leave:
     the heading sits against the first question and the CTA sits
     against the last. Safe because the list is always taller than
     the aside and the CTA stacked. */
  .hfaq-aside { grid-column: 1; grid-row: 1; align-self: start; }
  .hfaq-list { grid-column: 2; grid-row: 1; margin-top: 0; }
  .hfaq-cta { grid-column: 1; grid-row: 1; align-self: end; margin-top: 0; }
  .hfaq-aside h2 { font-size: 2.05rem; }
  .hfaq-q { font-size: 1.125rem; min-height: 68px; padding: 18px 0; }
  .hfaq-a-body { font-size: .96875rem; padding-bottom: 24px; }
}

/* Decoration, so it only exists where there is room for it. Below
   900 the grid is a single column and the bottle would be an image
   wedged between a paragraph and a button. */
.hfaq-deco { display: none; }
@media (min-width: 900px) {
  .hfaq-deco {
    display: block;
    position: relative;
    grid-column: 1;
    grid-row: 1;
    align-self: center;
    justify-self: start;
    width: clamp(150px, 15vw, 210px);
    aspect-ratio: 1 / 1;
    margin-left: 6%;
    /* Centred in the whole cell, then pushed clear of the lede: the
       glow is wider than the bottle and its top fringe was landing on
       the last line of the paragraph. */
    transform: translateY(30px);
    pointer-events: none;
  }
  .hfaq-deco::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 145%;
    aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(circle, var(--tint-sage) 0%, rgba(226, 240, 231, .42) 46%, rgba(226, 240, 231, 0) 70%);
  }
  .hfaq-deco .vial {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: contain;
    filter: drop-shadow(0 14px 24px rgba(83, 56, 12, .18));
    animation: h-float-slow 6.5s ease-in-out infinite;
  }
}

/* Two white sections in a row otherwise read as one long one. */
.hfaq-section + .ru-section { border-top: 1px solid var(--line); }

@media (prefers-reduced-motion: reduce) {
  .hfaq-a, .hfaq-a-body, .hfaq-ico, .hfaq-ico::before, .hfaq-ico::after, .hfaq-all svg {
    transition-duration: 1ms;
  }
  .hfaq-a-in { transition-delay: 0s; }
}

/* ============================================================
   Home · the sitewide offer, on the card
   ------------------------------------------------------------
   Top right, opposite the "Popular" badge so a featured product
   can carry both without them stacking on each other.
   ============================================================ */

.promo-badge {
  left: auto;
  right: 10px;
  background: var(--gold-foil);
  color: #2b1f10;
  letter-spacing: .07em;
}
/* Applied. Sage rather than gold, matching every other "this is
   already done" state on the site, and the tick makes it a
   receipt rather than an offer. */
.promo-badge.is-on {
  background: var(--tint-sage);
  color: var(--vivid-sage);
  box-shadow: 0 0 0 1px rgba(47, 122, 82, .25), var(--shadow-sm);
}
.promo-badge.is-on::before { content: "✓ "; }
@media (max-width: 380px) {
  /* Was .58rem, which computed to 9.28px: smaller than anything else
     on the site and smaller than the rule three blocks up says any
     badge may be. The string is two words, so tightening the padding
     and the tracking buys back the width without touching the type. */
  .promo-badge { right: 8px; padding-inline: 8px; letter-spacing: .04em; }
}

/* ============================================================
   Home · how a lot is released
   ------------------------------------------------------------
   The method behind the two numerals in the stat strip above.
   Read out of the quality page so the sequence on the home page
   and the sequence on the page that documents it cannot drift.
   ============================================================ */

.rel-section { padding-block: 44px 48px; }
.rel-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 22px;
}
.rel-head .section-head-v6 { margin-bottom: 0; }
.rel-head .link-more { flex: none; white-space: nowrap; }
@media (max-width: 719px) {
  .rel-head { display: block; }
  .rel-head .link-more { margin-top: 12px; }
}

.rel-rail {
  display: flex;
  gap: 12px;
  margin: 0 calc(var(--gutter) * -1);
  padding: 4px var(--gutter) 18px;
  list-style: none;
  counter-reset: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.rel-rail::-webkit-scrollbar { display: none; }
.rel-rail > .rel-card {
  flex: 0 0 clamp(232px, 74vw, 292px);
  scroll-snap-align: start;
}

.rel-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 20px 18px 18px;
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: box-shadow var(--motion), transform var(--motion);
}
@media (hover: hover) {
  .rel-card:hover { box-shadow: inset 0 0 0 1px var(--line-2), var(--lift); }
}
.rel-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-bottom: 12px;
  border-radius: 50%;
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  font-family: var(--font-num);
  font-size: .875rem;
  font-weight: 700;
  color: var(--accent-deep);
}
.rel-title {
  margin: 0 0 6px;
  font-size: 1rem;
  font-weight: 650;
  line-height: 1.3;
  letter-spacing: -.01em;
  color: var(--ink);
}
.rel-body {
  margin: 0;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--ink-2);
}

@media (min-width: 768px) {
  .rel-rail {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 14px;
    margin: 0;
    padding: 0;
    overflow: visible;
    scroll-snap-type: none;
  }
  .rel-rail > .rel-card { flex: none; }
  .rel-section { padding-block: 66px 72px; }
}
@media (min-width: 1100px) {
  .rel-rail { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; }
  .rel-card { padding: 18px 15px 16px; }
  .rel-title { font-size: .9375rem; }
  .rel-body { font-size: .8125rem; }
}

/* ============================================================
   Home · what happens after checkout
   ------------------------------------------------------------
   Four facts about the parcel rather than the vial. Every one
   of them lived only in a policy page or inside the cart
   drawer, which is to say nobody read them until after they
   had already decided.
   ============================================================ */

.ofact-section { padding-block: 44px 48px; }
.ofact-grid { display: grid; gap: 12px; }

.ofact-card {
  position: relative;
  display: block;
  padding: 20px 18px;
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--ink);
  text-decoration: none;
  transition: box-shadow var(--motion), transform var(--motion), background var(--motion);
}
@media (hover: hover) {
  .ofact-card:hover {
    background: var(--bg);
    box-shadow: inset 0 0 0 1px var(--accent-line), var(--lift);
    transform: translateY(-2px);
  }
  .ofact-card:hover .ofact-ico { background: var(--accent); color: #fff; }
}
.ofact-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.ofact-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-bottom: 13px;
  border-radius: 50%;
  background: var(--tint-gold);
  color: var(--accent-deep);
  transition: background var(--motion), color var(--motion);
}
.ofact-ico svg { width: 19px; height: 19px; }
.ofact-title {
  margin: 0 0 6px;
  font-size: 1rem;
  font-weight: 650;
  line-height: 1.3;
  letter-spacing: -.01em;
}
.ofact-body {
  margin: 0;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--ink-2);
}

@media (min-width: 600px) {
  .ofact-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .ofact-section { padding-block: 66px 72px; }
  .ofact-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
  .ofact-card { padding: 22px 20px 24px; }
}
/* ============================================================
   v7 · Cart drawer, cart page, checkout
   ------------------------------------------------------------
   Loads after styles.css, 10-tokens.css and 15-signature.css,
   so everything here is either new surface work or a deliberate
   override of the v6 base. The reference is the category
   leader's cart and checkout (docs/research/06 + 07): their
   structure and rigor, carried on our warm tints and our gold,
   never their pastels.

   90% of traffic is phones. Every unscoped rule below IS the
   phone layout; desktop only ever adds.

   woo.css is enqueued after app.css, so any rule here that
   fights one of its selectors is written to out-rank it, not to
   out-shout it. No !important except inside the autofill hack,
   where the browser's own UA priority forces it.
   ============================================================ */

/* ============================================================
   Drawer · chrome
   ============================================================ */

/* Edge to edge on a phone: at 375px a 89vw panel leaves a 41px
   sliver of page peeking through, which reads as a rendering
   mistake, not a scrim. From 480px up the panel takes the
   competitor's standard sheet width and the scrim does its job. */
.cart-drawer {
  width: min(100%, 480px);
}

.cd-head { padding: 16px 20px; }
.cd-head h2 { font-size: 1.1rem; }

/* The footer is sticky over a scrolling list, so it needs the
   upward cast the tokens define for bottom-pinned bars, or the
   list appears to slide under nothing. */
.cd-foot { box-shadow: var(--lift-up); }

/* ============================================================
   Drawer · line items
   ============================================================ */

/* Price moves to the bottom corner so the top corner belongs to
   the remove control alone. With both stacked at the top, a
   full-size remove target sat directly on the price. */
.cd-item {
  grid-template-columns: 68px minmax(0, 1fr) auto;
}
.cd-item-price {
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

/* Remove earns a true 44px target, not a 26px dot with hope.
   Visual weight stays low: no border, muted glyph, quiet hover. */
.cd-x {
  top: 0;
  right: 0;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  font-size: 17px;
}

/* Media tile must hold three things without breaking: the inline
   vial SVG (sized by the base layer), a future product photo,
   and the wrapper div halotir_product_media() puts around both. */
.cd-thumb img.photo,
.cd-bump-media img.photo {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

/* Per-unit price under the variant line. 12px is the floor and
   this sits exactly on it. */
.cd-unit { font-size: .75rem; color: var(--muted); margin-top: 2px; }
.cd-unit .woocommerce-Price-amount { font-family: var(--font-num); }

/* Stepper: 32px visuals inside an invisible 44px hit area. The
   pseudo-element is part of the button, so the extra ring is
   clickable without inflating the pill. */
.cd-qty { margin-top: 10px; padding: 2px; gap: 2px; }
.cd-qty button {
  width: 32px;
  height: 32px;
  position: relative;
  font-size: 16px;
}
.cd-qty button::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: inherit;
}
.cd-qty span { min-width: 22px; }

/* ============================================================
   Drawer · order bump
   ============================================================ */

/* The one card in the drawer allowed to carry the brand ground.
   Cream stock and a gold eyebrow mark it as ours; the competitor
   runs the same mechanic on a grey offwhite. The Add button
   stays ink because gold never becomes a large surface and the
   primary action colour is already spoken for. */
.cd-bump {
  background: var(--accent-tint);
  border-color: var(--accent-line);
}
.cd-bump-media { background: var(--bg); }

/* The base layer set this eyebrow at 9px, below the 12px floor.
   12px with modest tracking still fits the narrow column now
   that the drawer runs full width on phones. */
.cd-bump-eyebrow {
  font-size: .75rem;
  letter-spacing: .08em;
  color: var(--accent-deep);
}
.cd-bump h4 { font-size: .9rem; }
/* The v5 layer ellipsized this line to survive a 334px panel.
   The drawer is wider now, and an ellipsis here eats the price,
   which is the one thing the line exists to show. Let it wrap. */
.cd-bump p { font-size: .8125rem; white-space: normal; }

/* Dismiss is deliberately secondary, but a 24px circle is a
   miss on a phone. Same trick as the stepper: modest visual,
   full-size invisible target. */
.cd-bump-dismiss {
  width: 30px;
  height: 30px;
  position: absolute;
  /* Inside the card, not straddling its corner. The base pinned this at
     top/right -8px, so a 28px control hung 7px outside the panel on two
     sides and read as a sticker someone had dropped on the layout
     rather than part of it. */
  top: 6px;
  right: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
}
/* Modest visual, full-size target. The bump card is not a scroll
   container, so unlike .chip-row it does not clip this bleed: verified
   with elementFromPoint rather than assumed. */
.cd-bump-dismiss::after {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: var(--radius-pill);
}

/* ============================================================
   Drawer · promo code
   ============================================================ */

/* Input and submit merged into ONE pill: the wrapper carries the
   border and the focus state, the input carries neither. This is
   the competitor's only form pattern and the single squared
   input we still had. */
.cd-promo-form {
  align-items: center;
  gap: 0;
  padding: 3px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  background: var(--bg);
  transition: border-color var(--motion);
}
.cd-promo-form:focus-within { border-color: var(--ink); }
.cd-promo-form input {
  border: 0;
  height: 44px;
  background: transparent;
  padding: 0 16px;
  border-radius: var(--radius-pill);
}
.cd-promo-form input:focus { border: 0; }
/* The wrapper announces focus; a second ring on the inner input
   would draw a pill inside a pill. */
.cd-promo-form input:focus-visible { box-shadow: none; }
.cd-promo-form button {
  height: 38px;
  padding: 0 18px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-weight: 500;
  font-size: .9rem;
  flex: none;
}
.cd-promo-form button:active { transform: scale(.97); }

/* ============================================================
   Drawer · free-shipping progress
   ============================================================ */

/* Progress runs in our gold while there is distance to cover,
   and only flips to the semantic green once the threshold is
   met. The competitor's grey-on-offwhite bar is the structure;
   the tint is the identity. */
.ship-progress-label { color: var(--ink-2); }
.ship-progress-label svg { color: var(--accent); }
.ship-progress-label b { color: var(--accent-deep); }
.ship-progress-track { height: 4px; background: var(--accent-line); }
.ship-progress-fill { background: var(--accent); }

.ship-progress.done .ship-progress-label,
.ship-progress.done .ship-progress-label svg { color: var(--success); }
.ship-progress.done .ship-progress-track { background: var(--success-line); }
.ship-progress.done .ship-progress-fill { background: var(--success-bar); }

/* ============================================================
   Drawer · footer actions
   ============================================================ */

/* 56px matches the competitor's h-14 primary CTA. It is the one
   button the whole drawer exists to serve, so it gets the full
   height and everything else stays at the 44px floor. */
.cd-checkout { height: 56px; font-size: 1rem; }
.cd-checkout:active { transform: scale(.985); }
.cd-viewcart { min-height: 44px; }

/* ============================================================
   Drawer · empty state
   ============================================================ */

.cd-empty {
  padding: 64px 24px;
  display: grid;
  justify-items: center;
  gap: 6px;
}
.cd-empty-badge {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-pill);
  background: var(--panel);
  display: grid;
  place-items: center;
  margin-bottom: 10px;
}
/* Unsized inline SVGs have blown this drawer into horizontal
   scroll before. Every icon this surface adds is sized here. */
.cd-empty-badge svg { width: 26px; height: 26px; color: var(--muted); }
.cd-empty h3 { font-size: 1.05rem; }
.cd-empty p { font-size: .9rem; color: var(--muted); margin-bottom: 14px; }

/* ============================================================
   Cart page · line rows
   ============================================================ */

/* Photography arrives later; the tile has to hold either the
   inline vial or a real photograph without relayout. The wrapper
   div halotir_product_media() emits gets explicit size, because
   an unsized wrapper is how thumbnails collapsed to zero. */
.cart-media {
  border-radius: 16px;
  overflow: hidden;
}
.cart-media > div {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}
.cart-media img.photo { width: 100%; height: 100%; max-width: none; object-fit: cover; }

.cart-info h3 { font-size: 1rem; }

/* Match the drawer's stepper feel without shrinking the page's
   44px buttons: rows are roomier here, so full-size is right. */
.cart-row .qty { background: var(--bg); }
/* right:0 on an absolutely positioned child resolves against the row's
   PADDING box, not its content box, so the price rendered hard against
   the card border with none of the card's 14px padding and read as
   clipped. The remove control keeps its -8px: that is a 44px hit target
   deliberately overhanging so its small glyph sits optically inset. */
.cart-row .cart-line-price { right: 14px; }
/* From 640 the price and the remove control share a vertical centre
   line, so the price has to clear the remove's 44px box as well as the
   padding: 14 + 44 clears it with room for the gap. */
@media (min-width: 640px) {
  .cart-row .cart-line-price { right: 58px; }
}

/* ============================================================
   Cart page · coupon
   ============================================================ */

/* Same merged pill as the drawer. The outer div keeps woo.css's
   block spacing job; the pill inside carries the border. */
body.woocommerce-cart .coupon {
  display: block;
  margin: 18px 0 0;
}
.coupon-pill {
  display: flex;
  align-items: center;
  padding: 3px;
  max-width: 420px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  background: var(--bg);
  transition: border-color var(--motion);
}
.coupon-pill:focus-within { border-color: var(--ink); }
body.woocommerce-cart .coupon .coupon-pill input[type="text"] {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  height: 44px;
  min-height: 44px;
  padding: 0 16px;
  border-radius: var(--radius-pill);
}
.coupon-pill input:focus-visible { box-shadow: none; }
.coupon-apply {
  height: 38px;
  padding: 0 20px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-weight: 500;
  font-size: .9rem;
  cursor: pointer;
  flex: none;
}
.coupon-apply:active { transform: scale(.97); }

/* The no-JS fallback; cart.js hides it once quantity changes
   submit themselves. */
.cart-update { margin-top: 14px; }

/* ============================================================
   Cart page · summary
   ============================================================ */

/* White card with a hairline, flat at rest, like the competitor's
   rounded-[20px] bordered summary. The grey panel it replaces
   read as a disabled state next to the white line rows. */
.cart-summary {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 20px;
}

/* The shipping note is the same message as the drawer's progress
   line, so it wears the same ground: gold while short of the
   threshold, green once free shipping is earned. */
.cart-summary .ship-note {
  background: var(--accent-tint);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  color: var(--accent-deep);
  font-size: .85rem;
  margin: 0 0 14px;
}
.cart-summary .ship-note.ok {
  background: var(--success-tint);
  border-color: var(--success-line);
  color: var(--success);
}

.cart-summary .cart-checkout-link {
  min-height: 56px;
  margin-top: 14px;
}

.cart-continue {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 8px;
  font-size: .9rem;
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cart-continue:hover { color: var(--ink); }

/* ============================================================
   Cart page · sticky checkout bar (phones)
   ============================================================ */

/* Rides the shared .bottom-bar shell from the tokens layer,
   which already handles the safe area, the slide-in, and hiding
   itself from 1024 up where the sticky summary column exists. */
.cart-bar {
  display: flex;
  align-items: center;
  gap: 16px;
}
/* An author display beats the UA's [hidden] rule, so the pre-JS
   hidden state has to be restated or the bar flashes on load. */
.cart-bar[hidden] { display: none; }
.cart-bar-sum {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}
.cart-bar-sum span { font-size: .75rem; color: var(--muted); }
.cart-bar-sum b { font-family: var(--font-num); font-size: 1.1rem; font-variant-numeric: tabular-nums; }
.cart-bar-cta { flex: 1; min-height: 48px; }

/* One primary CTA per phone screen: while the bar is pinned, the
   in-flow summary button stands down. Express buttons a plugin
   hooks in are untouched. */
@media (max-width: 1023px) {
  body.has-bottom-bar .cart-summary .cart-checkout-link { display: none; }
}

/* The tokens layer hides every .bottom-bar from 1024 up, but the
   body class that buys its clearance is set by JS regardless of
   viewport. Without this, desktop carries padding for a bar that
   is not there. */
@media (min-width: 1024px) {
  body.woocommerce-cart.has-bottom-bar { padding-bottom: 0; }
}

/* ============================================================
   Checkout · layout
   ============================================================ */

/* Single column is the base layout, which the markup already is
   once WooCommerce's floats are gone. Desktop adds the second column
   and pins the order summary.

   The grid goes on .woocommerce, not on form.checkout. The stepped
   checkout replaced WooCommerce's #customer_details / #order_review
   pair, and the summary now renders in .hl-osum as a SIBLING of the
   form, so the form itself has nothing left to split into columns.
   Gridding it anyway is what dealt the three step panels across two
   tracks and squeezed the name fields down to 70px. */
/* :not(.woocommerce-order-received) on every selector in here, because
   the order-received screen carries the woocommerce-checkout body class
   too. Without the exclusion it inherited the checkout's form-plus-
   summary grid, and the receipt rendered into an 812px first track with
   a 372px empty second track beside it: the whole confirmation squeezed
   into two thirds of the page to leave room for a summary that is not
   on this screen. */
@media (min-width: 1024px) {
  body.woocommerce-checkout .hl-focus-main { max-width: 1120px; }
  body.woocommerce-checkout:not(.woocommerce-order-received) .woocommerce {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 372px;
    column-gap: 48px;
    align-items: start;
  }
  /* Everything that is not the summary stacks down the first track.
     Naming the column explicitly rather than trusting auto-placement,
     because auto-placement is exactly what broke this before. */
  body.woocommerce-checkout:not(.woocommerce-order-received) .woocommerce > * { grid-column: 1; min-width: 0; }
  /* span 99, not 1 / -1: there are no explicit rows here, so -1 resolves
     back to line 1 and the summary lands in row 1 alone, stretching that
     one row to its full height and pushing the first step 450px down. */
  body.woocommerce-checkout:not(.woocommerce-order-received) .woocommerce > .hl-osum {
    grid-column: 2;
    grid-row: 1 / span 99;
    position: sticky;
    top: calc(var(--header-h) + 24px);
  }
  body.woocommerce-checkout .hl-osum .hl-sheet { margin-top: 0; }
}

body.woocommerce-checkout form.checkout h3 {
  font-size: 1.15rem;
  margin: 26px 0 16px;
}
body.woocommerce-checkout form.checkout h3:first-child { margin-top: 0; }

/* ============================================================
   Checkout · fields
   ============================================================ */

/* The pill is the system's only input shape, and the 16px size
   is a hard floor: Safari zooms anything smaller and strands the
   visitor mid-form. Spacing is generous because on a phone this
   form IS the page. */
body.woocommerce-checkout form .form-row { margin-bottom: 16px; }
body.woocommerce-checkout form .form-row label {
  font-size: .85rem;
  color: var(--ink-2);
  margin-bottom: 7px;
}
body.woocommerce-checkout form .form-row input.input-text,
body.woocommerce-checkout form .form-row select {
  min-height: 52px;
  padding: 12px 20px;
  font-size: 16px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line-2);
  background: var(--bg);
  transition: border-color var(--motion);
}
body.woocommerce-checkout form .form-row textarea {
  min-height: 120px;
  padding: 14px 20px;
  font-size: 16px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line-2);
  background: var(--bg);
}
body.woocommerce-checkout form .form-row input.input-text:focus,
body.woocommerce-checkout form .form-row select:focus,
body.woocommerce-checkout form .form-row textarea:focus {
  outline: none;
  border-color: var(--ink);
}

/* Native select keeps its own chevron; only the shape changes.
   Extra right padding stops long state names running under it. */
body.woocommerce-checkout form .form-row select { padding-right: 36px; }

/* Select2, when WooCommerce swaps the country/state fields. */
body.woocommerce-checkout .select2-container .select2-selection--single {
  min-height: 52px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  padding: 12px 20px;
  font-size: 16px;
}
body.woocommerce-checkout .select2-container .select2-selection__rendered {
  line-height: 26px;
  padding: 0;
}
body.woocommerce-checkout .select2-container .select2-selection__arrow {
  top: 50%;
  transform: translateY(-50%);
  right: 14px;
}

/* Inline validation: state lives on the row, colour on the
   control. Red only ever means "fix this field". */
body.woocommerce-checkout .form-row.woocommerce-invalid input.input-text,
body.woocommerce-checkout .form-row.woocommerce-invalid select {
  border-color: #dc2626;
}

/* ============================================================
   Checkout · order review
   ============================================================ */

body.woocommerce-checkout #order_review_heading { margin: 28px 0 14px; }

body.woocommerce-checkout #order_review {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 20px;
}

body.woocommerce-checkout #order_review table.shop_table th,
body.woocommerce-checkout #order_review table.shop_table td {
  padding: 12px 0;
}
body.woocommerce-checkout #order_review table.shop_table tfoot .order-total th,
body.woocommerce-checkout #order_review table.shop_table tfoot .order-total td {
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--ink);
  border-bottom: none;
}

/* Payment now sits inside the white review card, so its own grey
   panel becomes a divided section instead of a box in a box. */
body.woocommerce-checkout #payment {
  background: none;
  border-radius: 0;
  padding: 18px 0 0;
  margin-top: 6px;
  border-top: 1px solid var(--line);
}
body.woocommerce-checkout #payment ul.payment_methods { margin-bottom: 14px; }
body.woocommerce-checkout #payment ul.payment_methods li { padding: 6px 0; }
body.woocommerce-checkout #payment ul.payment_methods label {
  font-size: .95rem;
  font-weight: 500;
  cursor: pointer;
}
body.woocommerce-checkout #payment input[type="radio"] {
  width: 20px;
  height: 20px;
  margin-right: 8px;
  vertical-align: -4px;
  accent-color: var(--accent);
}
body.woocommerce-checkout #payment div.payment_box {
  background: var(--tint);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: .85rem;
  color: var(--ink-2);
  margin: 8px 0;
}

/* The researcher attestation used to render here as a lone
   WooCommerce checkbox field above the order button. It is now one of
   the two consents inside section 4 (.hl-consent, further down), so the
   rules that styled `.form-row.attest` have gone with the markup rather
   than lingering as selectors that match nothing. */

/* ============================================================
   Checkout · place order
   ============================================================ */

body.woocommerce-checkout #place_order {
  width: 100%;
  min-height: 56px;
  font-size: 1rem;
  border-radius: var(--radius-pill);
}
body.woocommerce-checkout #place_order:active { transform: scale(.985); }

/* Checkout's own coupon form, when a store enables it there. */
body.woocommerce-checkout form.checkout_coupon {
  display: flex;
  gap: 8px;
  align-items: flex-end;
  max-width: 420px;
  margin-bottom: 20px;
}
body.woocommerce-checkout form.checkout_coupon .form-row { margin-bottom: 0; flex: 1; }

/* ============================================================
   Autofill · kill the yellow flash
   ============================================================ */

/* Chrome and Safari paint autofilled fields yellow from UA-level
   styles that ordinary declarations cannot beat. The competitor's
   fix, verbatim: stall the background transition for 5000s and
   bury the paint under an inset white shadow. Kept in their own
   rules because an unknown pseudo-class voids a whole selector
   list in other engines. */
.cart-drawer input:-webkit-autofill,
.cart-drawer input:-webkit-autofill:hover,
.cart-drawer input:-webkit-autofill:focus,
body.woocommerce-cart input:-webkit-autofill,
body.woocommerce-cart input:-webkit-autofill:hover,
body.woocommerce-cart input:-webkit-autofill:focus,
body.woocommerce-checkout input:-webkit-autofill,
body.woocommerce-checkout input:-webkit-autofill:hover,
body.woocommerce-checkout input:-webkit-autofill:focus {
  transition: background-color 5000s ease-in-out;
  box-shadow: inset 0 0 0 1000px #fff !important;
  -webkit-text-fill-color: var(--ink) !important;
}
body.woocommerce-cart input:autofill,
body.woocommerce-checkout input:autofill {
  box-shadow: inset 0 0 0 1000px #fff !important;
}

/* ============================================================
   Motion off
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .cart-drawer,
  .backdrop,
  .ship-progress-fill,
  .cd-promo-form,
  .coupon-pill { transition: none; }
  .cd-checkout:active,
  .cd-promo-form button:active,
  .coupon-apply:active,
  body.woocommerce-checkout #place_order:active { transform: none; }
}

/* ============================================================
   WooCommerce notices
   ------------------------------------------------------------
   WooCommerce emits its notices as a block with a `float: right`
   button inside. A float does not extend its parent's height, so
   a 50px pill inside a 40px message escapes the box and lands on
   whatever follows: on checkout, "View cart" was sitting on top
   of the returning-customer notice.

   Making the notice a flex row fixes the whole family at once
   rather than nudging one button back into place.
   ============================================================ */
.woocommerce-message,
.woocommerce-info,
.woocommerce-error,
.woocommerce-noreviews {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 16px;
  margin: 0 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-size: .9375rem;
  color: var(--ink);
  list-style: none;
}
/* A coloured left edge carries the notice type without tinting the
   whole panel, which on a warm palette turns muddy. */
.woocommerce-message { border-left: 3px solid var(--success, #15803d); }
.woocommerce-info    { border-left: 3px solid var(--accent); }
.woocommerce-error   { border-left: 3px solid #b91c1c; }

.woocommerce-message .button,
.woocommerce-info .button,
.woocommerce-error .button {
  float: none;
  margin-left: auto;
  min-height: 40px;
  padding: 0 18px;
  font-size: .875rem;
  white-space: nowrap;
}
.woocommerce-error li { list-style: none; }

/* ---------- The notice on a phone ----------
   The flex row above is right up to the point where it runs out of
   width, and on a phone it always does. Measured at 390: the cart's
   "2 x GLP-3 have been added to your cart." is 290px of a 310px
   track, so the button wraps, and `margin-left: auto` then parks it
   against the right edge of a line it is alone on. The result is a
   106px box holding one sentence, with a pill hanging off the far
   corner like something that fell out of the paragraph.

   Below 640 the notice stops pretending it is a row. Text, then the
   action beneath it at full width, which is the shape a phone reads
   as deliberate.

   Scoped through .woocommerce-notices-wrapper rather than applied to
   the classes directly. The login and coupon toggles further down
   this file are also .woocommerce-info and they are built as a row on
   purpose, with the action at the end of the sentence; they live
   inside their own toggle wrapper, not in the notices wrapper, so
   naming the wrapper is what separates the two families. */
@media (max-width: 639px) {
  .woocommerce-notices-wrapper > .woocommerce-message,
  .woocommerce-notices-wrapper > .woocommerce-info,
  .woocommerce-notices-wrapper > .woocommerce-error {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  .woocommerce-notices-wrapper > .woocommerce-message .button,
  .woocommerce-notices-wrapper > .woocommerce-info .button,
  .woocommerce-notices-wrapper > .woocommerce-error .button {
    margin-left: 0;
    width: 100%;
  }
}

/* And it stops being a primary button at any width. On the cart the
   page's own commitment is "Checkout", in the sticky bar, drawn as a
   black pill; on checkout it is "Place order". A second black pill a
   few hundred pixels above either of them, offering to take the
   buyer somewhere they already are, is two primary actions on one
   screen. The notice keeps a real, full-height control and gives up
   the fill: outline, ink text, same pill. */
.woocommerce-notices-wrapper > .woocommerce-message .button,
.woocommerce-notices-wrapper > .woocommerce-info .button,
.woocommerce-notices-wrapper > .woocommerce-error .button {
  background: none;
  color: var(--ink);
  border: 1px solid var(--line-2);
  box-shadow: none;
}
@media (hover: hover) {
  .woocommerce-notices-wrapper > .woocommerce-message .button:hover,
  .woocommerce-notices-wrapper > .woocommerce-info .button:hover,
  .woocommerce-notices-wrapper > .woocommerce-error .button:hover {
    background: var(--tint);
    border-color: var(--ink-2);
    color: var(--ink);
  }
}

/* ---------- Order notes ----------
   WooCommerce's textarea ships resizable on both axes. The horizontal
   handle can drag the field past its own card, and on a phone the
   grab corner is a desktop affordance drawn into the one field on
   the page that has no use for it. Vertical only, everywhere. */
.hl-checkout #order_comments,
.hl-checkout textarea.input-text { resize: vertical; }
@media (pointer: coarse) {
  .hl-checkout #order_comments,
  .hl-checkout textarea.input-text { resize: none; }
}

/* ---------- Login and coupon toggles ---------- */
/* Both ship as a bare sentence with a link buried mid-phrase. On a
   phone that wraps across two lines and reads as body copy, not as
   something to tap. Each becomes one bordered row with the action
   at its end. */
.woocommerce-form-login-toggle .woocommerce-info,
.woocommerce-form-coupon-toggle .woocommerce-info {
  justify-content: space-between;
  background: var(--tint);
  border-left-width: 1px;
  border-left-color: var(--line);
}
.woocommerce-form-login-toggle a,
.woocommerce-form-coupon-toggle a {
  font-weight: 600;
  color: var(--accent-deep);
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
}

/* ---------- Section headings ---------- */
.woocommerce-billing-fields h3,
.woocommerce-shipping-fields h3,
.woocommerce-additional-fields h3,
#order_review_heading {
  font-size: 1.15rem;
  margin: 26px 0 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

/* ---------- Fields ---------- */
.woocommerce-checkout .form-row { margin-bottom: 14px; }
.woocommerce-checkout .form-row label {
  display: block;
  margin-bottom: 6px;
  font-size: .8125rem;
  font-weight: 500;
  color: var(--ink-2);
}
.woocommerce-checkout .form-row .required {
  color: var(--accent-deep);
  text-decoration: none;
}
/* Two-up only where both halves genuinely fit. This used to pair from
   380px, which on a 393px phone leaves 139px per field: about 107px of
   usable text once the padding is off, and a floating label shrunk to
   .72 sitting over it. Names stack on phones now. 560 is the width at
   which each half clears 250px.

   Grid on the wrapper rather than inline-block halves on the rows: every
   other row is form-row-wide, and a percentage width plus a margin has to
   be kept in sync by hand with whatever gap the column is using. The
   fields the pairing is for all live in these two wrappers. */
@media (min-width: 560px) {
  .woocommerce-checkout .woocommerce-billing-fields__field-wrapper,
  .woocommerce-checkout .woocommerce-shipping-fields__field-wrapper {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 14px;
  }
  .woocommerce-checkout .woocommerce-billing-fields__field-wrapper > .form-row,
  .woocommerce-checkout .woocommerce-shipping-fields__field-wrapper > .form-row {
    grid-column: 1 / -1;
    width: auto;
    margin-right: 0;
  }
  .woocommerce-checkout .woocommerce-billing-fields__field-wrapper > .form-row-first,
  .woocommerce-checkout .woocommerce-shipping-fields__field-wrapper > .form-row-first { grid-column: 1; }
  .woocommerce-checkout .woocommerce-billing-fields__field-wrapper > .form-row-last,
  .woocommerce-checkout .woocommerce-shipping-fields__field-wrapper > .form-row-last { grid-column: 2; }
}

/* ============================================================
   Checkout · layout and the collapsed summary
   ============================================================ */

/* Single column on a phone. The two-column grid only appears once
   there is genuinely room for a 380px aside beside a usable form,
   which is not true at 768. */
.hl-checkout-grid { display: grid; gap: 8px; }

/* ============================================================
   Order summary: fixed bar, plus a bottom sheet

   Replaces the collapsible block that used to sit at the top of
   checkout. A summary above the form is only visible while nobody
   is looking at it: the first tap into an address field scrolls it
   away and it never comes back. A bar pinned to the bottom is
   permanently in view, sits under the thumb, and costs one tap to
   expand.
   ============================================================ */
.hl-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px 16px calc(14px + var(--safe-b, 0px));
  background: var(--bg);
  border: 0;
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 20px rgba(28, 25, 23, .08);
  cursor: pointer;
  text-align: left;
}
/* Points up, because that is where the sheet comes from. */
.hl-bar-chev {
  display: block;
  line-height: 0;
  color: var(--accent-deep);
  transition: transform var(--motion);
}
.hl-bar-chev svg {
  width: 20px; height: 20px;
  stroke-width: 2.2;
  transform: rotate(-90deg);
}
.hl-osum.open .hl-bar-chev { transform: rotate(180deg); }

.hl-bar-figs {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  max-width: 520px;
}
.hl-bar-left, .hl-bar-right { display: inline-flex; align-items: center; gap: 8px; }
.hl-bar-count {
  display: grid; place-items: center;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--ink);
  color: #fff;
  font-size: .75rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.hl-bar-word { font-size: .875rem; color: var(--ink-2); }
.hl-bar-total {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 1;
  font-size: 1.125rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.hl-bar-total .amount { font: inherit; }

/* The bar is fixed, so the document has to reserve its height or the
   last control on every step ends up underneath it. */
body.hl-focus { padding-bottom: 96px; }

.hl-sheet-scrim {
  position: fixed; inset: 0;
  z-index: 70;
  background: rgba(28, 25, 23, .45);
  opacity: 0;
  transition: opacity var(--motion);
}
.hl-osum.open .hl-sheet-scrim { opacity: 1; }

.hl-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 71;
  display: flex;
  flex-direction: column;
  max-height: 85vh;
  max-height: 85dvh;
  background: var(--bg);
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -10px 40px rgba(28, 25, 23, .18);
  transform: translateY(100%);
  transition: transform 260ms var(--ease-out);
  /* Both axes named. Declaring only overflow-y computes overflow-x to
     auto, which turns any panel into a sideways scroller and widens the
     layout viewport on a phone. */
  overflow-x: hidden;
  overflow-y: hidden;
  overscroll-behavior: contain;
}
.hl-osum.open .hl-sheet { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .hl-sheet { transition: none; }
}

.hl-sheet-head {
  position: relative;
  flex: none;
  padding: 10px 16px 14px;
  border-bottom: 1px solid var(--line);
  text-align: center;
}
.hl-sheet-grab {
  display: block;
  width: 44px; height: 5px;
  margin: 0 auto 14px;
  border-radius: 999px;
  background: var(--line-2);
}
.hl-sheet-title { margin: 0; font-size: 1.0625rem; font-weight: 600; color: var(--ink); }
.hl-sheet-count { margin: 3px 0 0; font-size: .8125rem; color: var(--muted); }
.hl-sheet-x {
  position: absolute;
  top: 14px; right: 10px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: 0; border-radius: 50%;
  background: none;
  color: var(--ink-2);
  cursor: pointer;
}
.hl-sheet-x svg { width: 17px; height: 17px; }

.hl-sheet-body {
  flex: 1 1 auto;
  padding: 4px 16px 16px;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.hl-sheet-foot {
  flex: none;
  padding: 14px 16px calc(14px + var(--safe-b, 0px));
  border-top: 1px solid var(--line);
}
/* Square-cornered where everything else on this page is a pill, so the
   shape itself says dismiss rather than commit. */
.hl-sheet-done {
  width: 100%;
  min-height: 48px;
  border: 0;
  border-radius: 12px;
  background: var(--ink);
  color: #fff;
  font-size: .9375rem; font-weight: 500;
  cursor: pointer;
}

/* ---------- Delivery step ---------- */
.hl-sub { margin: 0 0 14px; font-size: .875rem; color: var(--ink-2); }

.hl-note {
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 12px;
  padding: 12px 14px;
  margin-bottom: 16px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  background: var(--tint-gold);
}
.hl-note svg { width: 18px; height: 18px; color: var(--accent-deep); margin-top: 1px; }
.hl-note-t { margin: 0; font-size: .8125rem; font-weight: 600; color: var(--ink); }
.hl-note-b { margin: 3px 0 0; font-size: .75rem; line-height: 1.5; color: var(--ink-2); }

/* The rates arrive as WooCommerce table rows. Rather than re-render
   them and lose the hooks other code attaches, they are re-parented
   into a table of our own and laid out as blocks. */
.hl-delivery-table,
.hl-delivery-table tbody,
.hl-delivery-table tr,
.hl-delivery-table td {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
}
.hl-delivery-table th { display: none; }
.hl-delivery-table ul#shipping_method,
.hl-delivery-table .woocommerce-shipping-methods {
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  gap: 10px;
}
.hl-delivery-table li { position: relative; margin: 0; }

.hl-delivery-table li input.shipping_method {
  position: absolute;
  left: 16px; top: 50%;
  transform: translateY(-50%);
  width: 20px; height: 20px;
  margin: 0;
  appearance: none; -webkit-appearance: none;
  border: 2px solid var(--line-2);
  border-radius: 50%;
  background: var(--bg);
  cursor: pointer;
  transition: border-color var(--motion), box-shadow var(--motion);
}
.hl-delivery-table li input.shipping_method:checked {
  border-width: 6px;
  border-color: var(--ink);
}
.hl-delivery-table li input.shipping_method:focus-visible { box-shadow: var(--ring); }

.hl-delivery-table li label {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name cost" "eta cost";
  align-items: center;
  gap: 2px 12px;
  min-height: 64px;
  margin: 0;
  padding: 12px 16px 12px 48px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
  transition: border-color var(--motion), background-color var(--motion);
}
/* Standalone rule on purpose. An unsupported selector inside a comma
   list invalidates the whole rule, so :has() never shares one. */
.hl-delivery-table li:has(input.shipping_method:checked) label {
  border-color: var(--ink);
  background: var(--tint);
}
.hl-ship-name { grid-area: name; font-size: .9375rem; font-weight: 500; color: var(--ink); }
.hl-ship-eta  { grid-area: eta;  font-size: .75rem; color: var(--muted); }
.hl-ship-cost {
  grid-area: cost;
  font-size: .9375rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-align: right;
}
.hl-free-pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--tint-gold);
  color: var(--accent-deep);
  font-size: .75rem; font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
}
.hl-delivery-empty { margin: 0; font-size: .875rem; color: var(--ink-2); }
/* A single available rate renders as plain text with no radio, so it
   needs the same row treatment without the indent for one. */
.hl-delivery-table td > .woocommerce-shipping-methods + p,
.hl-delivery-table td > p {
  margin: 0;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  color: var(--ink-2);
}

/* ---------- Payment step preamble ---------- */
.hl-ssl {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 16px;
  font-size: .75rem; color: var(--ink-2);
}
.hl-ssl svg { width: 14px; height: 14px; color: var(--accent-deep); }

/* The totals put back in front of someone at the exact moment they
   reach for a card, so they do not have to open the sheet to check. */
.hl-pay-total {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
  min-height: 52px;
  margin-bottom: 18px;
  padding: 10px 16px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  background: var(--tint-gold);
  cursor: pointer;
  font-size: .875rem;
  color: var(--ink-2);
}
.hl-pay-figure {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 1;
  font-size: 1.125rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ---------- Section 4: review and consents ---------- */
.hl-review { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--line); }
.hl-step-head.static { padding: 0 0 16px; cursor: default; min-height: 0; }

/* Over-qualified on purpose. Section 4 renders inside WooCommerce's
   `.form-row.place-order`, so `.woocommerce-checkout .form-row label`
   applies to these consents and would set them back to display:block,
   dropping the checkbox onto a line of its own above the text. */
body.woocommerce-checkout .hl-review .hl-consent {
  position: relative;
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 12px;
  margin-bottom: 16px;
  cursor: pointer;
}
/* Absolutely positioned so the grid sees two children, not three. A
   third in-flow child in a two-column grid starts a new row and the
   text ends up one word wide. */
.hl-consent input {
  position: absolute;
  width: 20px; height: 20px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.hl-box {
  display: grid; place-items: center;
  width: 20px; height: 20px;
  margin-top: 2px;
  border: 2px solid var(--line-2);
  border-radius: 4px;
  background: var(--bg);
  transition: background-color var(--motion), border-color var(--motion);
}
.hl-box svg { width: 13px; height: 13px; stroke-width: 3; color: #fff; opacity: 0; }
.hl-consent input:checked + .hl-box { background: var(--ink); border-color: var(--ink); }
.hl-consent input:checked + .hl-box svg { opacity: 1; }
.hl-consent input:focus-visible + .hl-box { box-shadow: var(--ring); }
.hl-consent-t { font-size: .8125rem; line-height: 1.6; color: var(--ink-2); }
.hl-consent-t strong { color: var(--ink); font-weight: 600; }

.hl-legal { margin: 0 0 20px; font-size: .75rem; line-height: 1.6; color: var(--muted); }
.hl-legal a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }

/* ---------- Reassurance beside the pay button ---------- */
.hl-checkout-trust {
  list-style: none;
  margin: 18px 0 0;
  padding: 14px 16px;
  display: grid;
  gap: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--tint);
}
.hl-checkout-trust li {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: .8125rem;
  color: var(--ink-2);
}
.hl-checkout-trust svg { width: 16px; height: 16px; flex: none; color: var(--accent); }

/* ---------- Desktop ---------- */
@media (min-width: 1024px) {
  /* A bar pinned to the bottom of a 1400px window is a phone idiom
     stranded on a desktop, so above 1024 the sheet stops being a sheet:
     the bar goes away and the same summary renders as a plain card at
     the end of the column. Same markup, same fragments, no second copy
     of the numbers to drift out of sync. */
  .hl-bar { display: none; }
  body.hl-focus { padding-bottom: 0; }
  .hl-sheet-scrim { display: none; }
  .hl-sheet {
    position: static;
    max-height: none;
    transform: none;
    margin-top: 28px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: none;
    overflow: visible;
  }
  .hl-sheet[hidden] { display: flex; }
  .hl-sheet-grab, .hl-sheet-x, .hl-sheet-foot { display: none; }
  .hl-sheet-head { text-align: left; padding: 18px 20px 14px; }
  .hl-sheet-body { overflow: visible; padding: 8px 20px 20px; }
  .hl-pay-total { display: none; }

  .hl-checkout-grid {
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 40px;
    align-items: start;
  }
  .hl-checkout-aside { position: sticky; top: 92px; }
}

/* ============================================================
   Stepped checkout
   Measured from the category leader at 375px:
   inputs 48px / 12px radius (NOT pills, a deliberate exception
   to the stadium rule: a pill reads casual for address entry
   and the rounded rectangle is what makes a form look like a
   form). Step submits are 48px auto-width centred pills; only
   the final commitment is a full-width 56px pill.
   ============================================================ */

/* ---------- Focus chrome ---------- */
.hl-focus { background: var(--tint); }
.hl-focus-head {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-top, 0px));
  background: rgba(255, 255, 255, .92);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.hl-focus-back {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px;
  font-size: .875rem; color: var(--ink-2);
}
.hl-focus-back svg { width: 16px; height: 16px; transform: rotate(180deg); }
/* The wordmark is painted as a mask filled with currentColor, and this
   is an <a>, which the base stylesheet colours with the gold accent. So
   without this the logo renders gold on the one page where it should be
   at its most sober. */
.hl-focus-logo { justify-self: center; display: block; color: var(--ink); }
.hl-focus-logo .logo-img { height: 24px; }
.hl-focus-secure {
  justify-self: end;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 0 12px;
  border-radius: var(--radius-pill);
  background: var(--success-tint, #f0fdf4);
  border: 1px solid var(--success-line, #bbf7d0);
  color: var(--success, #15803d);
  font-size: .75rem; font-weight: 500;
}
.hl-focus-secure svg { width: 13px; height: 13px; }
.hl-focus-secure span { display: none; }
.hl-focus-main { display: block; padding: 20px var(--gutter) 40px; }
/* The gutter was being paid twice. .wrap carries its own --gutter and
   sits inside this main, so at 375px the checkout column measured 279px:
   96px of padding, a quarter of the screen, and the reason a heading as
   short as "Shipping address" wrapped onto two lines. The outer gutter
   is the one that stays, because .wrap also carries the max-width the
   desktop layout needs. */
.hl-focus-main > .wrap { padding-left: 0; padding-right: 0; }
/* The page title was running at hero size and eating the top third of a
   phone screen, pushing the progress rail below the fold on the one
   screen where knowing how far in you are matters most. */
.hl-focus .page-title,
.hl-focus .entry-title,
.hl-focus h1 {
  font-size: 1.5rem;
  line-height: 1.15;
  margin: 0 0 16px;
}
.hl-focus-legal {
  padding: 0 var(--gutter) calc(28px + env(safe-area-inset-bottom, 0px));
  text-align: center;
}
.hl-focus-legal p { font-size: .75rem; color: var(--muted); }

/* ---------- Progress rail ----------
   Track and fill are pseudo-elements on the rail itself, not margins on
   the dots. The first attempt drew each connector as a ::before with a
   negative margin, which put a 75px line to the LEFT of the first dot
   and bunched all three together, because a flex row with gap:0 gives
   10px dots nothing to space them apart with.

   The fill width keys off aria-valuenow, so the value a screen reader
   announces and the line a sighted user sees are literally the same
   number and cannot drift. */
.hl-rail {
  position: relative;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 180px;
  margin: 4px auto 22px;
  padding: 0;
}
.hl-rail::before,
.hl-rail::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 5px;
  height: 2px;
  transform: translateY(-50%);
}
.hl-rail::before { right: 5px; background: var(--line-2); }
.hl-rail::after {
  width: 0;
  background: var(--ink);
  transition: width 320ms var(--ease-out);
}
.hl-rail[aria-valuenow="2"]::after { width: calc(50% - 5px); }
.hl-rail[aria-valuenow="3"]::after { width: calc(100% - 10px); }

.hl-rail-dot {
  position: relative;
  z-index: 1;
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: 50%;
  background: var(--line-2);
  transition: background-color var(--motion), box-shadow var(--motion);
}
.hl-rail-dot.done,
.hl-rail-dot.current { background: var(--ink); }
/* A halo rather than a bigger dot: growing it shifts the whole rail. */
.hl-rail-dot.current { box-shadow: 0 0 0 3px rgba(28, 25, 23, .14); }

/* ---------- Step panels ---------- */
.hl-step {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  margin-bottom: 12px;
  overflow: hidden;
  transition: border-color var(--motion), box-shadow var(--motion);
}
.hl-step.open { border-color: var(--line-2); box-shadow: var(--lift); }

/* Heading and Edit share a row. The heading is a button too, but "the
   heading is clickable" is not something anyone discovers, so a
   completed step carries a named control as well. */
.hl-step-bar { display: flex; align-items: center; }
.hl-step-head {
  display: flex; align-items: center; gap: 14px;
  flex: 1 1 auto; min-width: 0; min-height: 64px;
  padding: 14px 18px;
  background: none; border: 0;
  text-align: left; cursor: pointer;
}
.hl-step-head:disabled { cursor: default; opacity: .5; }
/* 44px of target, pulled flush by the same amount of negative margin,
   so it reads as tight to the edge without being a 24px tap. */
.hl-step-edit {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 18px; margin-right: 4px;
  background: none; border: 0;
  color: var(--ink); font-size: .8125rem; font-weight: 500;
  text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}
.hl-step-recap {
  margin: -6px 0 0;
  padding: 0 18px 14px;
  font-size: .8125rem; line-height: 1.5;
  color: var(--ink-2);
}
.hl-step-n {
  display: grid; place-items: center;
  width: 34px; height: 34px; flex: none;
  border-radius: 50%;
  background: var(--line);
  color: var(--ink-2);
  font-size: .9375rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  transition: background-color var(--motion), color var(--motion);
}
.hl-step.open .hl-step-n,
.hl-step.done .hl-step-n { background: var(--ink); color: #fff; }
.hl-step-title { font-size: 1.0625rem; font-weight: 600; color: var(--ink); }
.hl-step.open .hl-step-title { font-size: 1.25rem; }
/* A tick, not the word "Done". The badge already carries the number and
   the recap line already says what was entered, so a third piece of
   text on the same row is just more to read. */
.hl-step-tick { display: inline-flex; line-height: 0; color: var(--success, #15803d); }
/* Setting `display` on an element defeats the hidden attribute, which
   is why the tick and the Edit control both showed on the step that was
   still being filled in. Restated per class rather than globally: a
   blanket [hidden] override would reach into WooCommerce's markup too. */
.hl-step-tick[hidden], .hl-step-edit[hidden], .hl-step-recap[hidden] { display: none; }
.hl-step-tick svg { width: 17px; height: 17px; stroke-width: 2.4; }
.hl-step:not(.done) .hl-step-title { color: var(--ink-2); }
.hl-step.open .hl-step-title, .hl-step.done .hl-step-title { color: var(--ink); }

.hl-step-body { padding: 0 18px 20px; }

.hl-step-trust {
  list-style: none;
  display: flex; flex-wrap: wrap; gap: 8px 16px;
  margin: 0 0 18px; padding: 0;
}
.hl-step-trust li {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: .8125rem; color: var(--ink-2);
}
.hl-step-trust svg { width: 15px; height: 15px; color: var(--success, #15803d); }

/* ---------- Step actions ---------- */
/* A step submit is auto-width and centred. Only the final commitment
   gets the full width, so the shape itself says which button ends the
   process and which merely continues it. */
.hl-step-actions {
  display: flex; align-items: center; justify-content: center;
  gap: 12px; margin-top: 20px;
}
.hl-step-actions.single { justify-content: flex-start; margin-top: 14px; }
.hl-step-next {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 0 32px;
  border: 0; border-radius: var(--radius-pill);
  background: var(--ink); color: #fff;
  font-size: .875rem; font-weight: 500;
  cursor: pointer;
  transition: opacity var(--motion), transform var(--motion);
}
.hl-step-next:hover { opacity: .88; }
.hl-step-next:active { transform: scale(.98); }
.hl-step-next svg { width: 15px; height: 15px; }
.hl-step-back {
  min-height: 44px; padding: 0 14px;
  background: none; border: 0;
  color: var(--ink-2); font-size: .875rem;
  text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}

/* ---------- Place order ---------- */
.hl-place-order {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 56px;
  border: 0; border-radius: var(--radius-pill);
  background: var(--ink); color: #fff;
  font-size: 1rem; font-weight: 600;
  cursor: pointer;
}
.hl-place-order svg { width: 16px; height: 16px; }
.hl-place-order:disabled { opacity: .5; cursor: not-allowed; }

/* ---------- Floating labels ---------- */
/* The field carries the label inside it, so the row is one object
   instead of two and the column reads shorter. Needs the space
   placeholder set in inc/checkout-steps.php: :placeholder-shown is the
   only CSS-reachable signal for "this field is empty". */
.hl-checkout .form-row.hl-float { position: relative; margin-bottom: 14px; }
.hl-checkout .form-row.hl-float label {
  position: absolute;
  left: 17px; top: 15px;
  margin: 0;
  font-size: 1rem;
  color: var(--muted);
  pointer-events: none;
  transform-origin: left top;
  transition: transform 160ms var(--ease-out), color 160ms var(--ease-out);
}
/* Matched from the row, not from the input. WooCommerce wraps the input
   in a .woocommerce-input-wrapper span, so the input is never a sibling
   of the label and no combinator can reach across that: it has to be
   :has() on the row. */
.hl-checkout .form-row.hl-float:has(input.input-text:not(:placeholder-shown)) label,
.hl-checkout .form-row.hl-float:has(input.input-text:focus) label {
  transform: translateY(-9px) scale(.72);
  color: var(--ink-2);
}
.hl-checkout .form-row.hl-float input.input-text {
  width: 100%;
  min-height: 56px;
  padding: 22px 16px 6px;
  font-size: 16px;
  border: 1px solid #e8e8e8;
  border-radius: 12px;
  background: var(--bg);
}
.hl-checkout .form-row.hl-float input.input-text:focus {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(28, 25, 23, .08);
  outline: none;
}
/* No flex reversal here. The label is out of flow anyway, so reversing
   only moved .description above its own field. */
.hl-checkout .form-row .description {
  margin: 6px 2px 0;
  font-size: .75rem;
  color: var(--muted);
}

/* Selects keep a visible label above, because a floating label over a
   select that already shows a value is two labels arguing. It still has
   to line up with the floated ones, which sit 17px inside their box. */
/* Excludes labels that wrap their own control: "Create an account?" and
   the like are a checkbox and its text on one line, not a caption. */
.hl-checkout .form-row:not(.hl-float) > label:not(:has(input)) {
  display: block;
  margin: 0 0 6px 17px;
  font-size: .8125rem;
  color: var(--ink-2);
}
.hl-checkout .form-row select,
.hl-checkout .form-row .select2-container .select2-selection--single {
  min-height: 56px;
  border-radius: 12px;
  border: 1px solid #e8e8e8;
  font-size: 16px;
}

@media (min-width: 640px) {
  .hl-focus-secure span { display: inline; }
}
@media (min-width: 1024px) {
  .hl-focus-main { max-width: 720px; margin: 0 auto; }
}

/* ---------- Order review inside the summary sheet ----------
   The table used to render inside step 2, which is why every rule here
   was scoped to .hl-step. It now lives in the bottom sheet, and the
   shipping rows have been lifted out of it entirely into the delivery
   step, so the rules that styled #shipping_method inside this table are
   gone rather than rescoped. They were also the reason the new delivery
   rows laid out wrong: an id selector beats a class one, so
   `#shipping_method li label { display: flex }` quietly won over the
   grid the new rows are built on. */
.hl-sheet-body #order_review { margin: 0; }
.hl-sheet-body .shop_table {
  width: 100%;
  border-collapse: collapse;
  font-size: .9375rem;
}
.hl-sheet-body .shop_table thead { display: none; }
.hl-sheet-body .shop_table th,
.hl-sheet-body .shop_table td {
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-weight: 400;
  color: var(--ink-2);
}
.hl-sheet-body .shop_table tr:last-child th,
.hl-sheet-body .shop_table tr:last-child td { border-bottom: 0; }
.hl-sheet-body .shop_table .product-name { color: var(--ink); font-weight: 500; }
.hl-sheet-body .shop_table .product-total,
.hl-sheet-body .shop_table td:last-child,
.hl-sheet-body .shop_table th:last-child {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.hl-sheet-body .shop_table .product-quantity { color: var(--muted); font-weight: 400; }
.hl-sheet-body .shop_table .cart-subtotal th,
.hl-sheet-body .shop_table .cart-subtotal td { color: var(--ink-2); font-weight: 400; }
.hl-sheet-body .shop_table .order-total th { font-size: 1rem; font-weight: 600; color: var(--ink); }
.hl-sheet-body .shop_table .order-total td {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 1;
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--ink);
}
/* The shipping row is lifted out into step 2 and replaced by a plain
   line stating what was chosen, so the label column keeps a sane width
   rather than collapsing to the width of one word. */
.hl-sheet-body .shop_table th { width: 55%; }
.hl-sheet-body .hl-ship-line td { text-align: right; }
.hl-sheet-body .hl-ship-line .hl-free-pill { font-size: .6875rem; }

/* Place Order is the single full-width control on the site. Every other
   button is auto-width, so the shape alone says which one ends the
   process. WooCommerce nests it in .form-row.place-order, whose inline
   layout was capping it at its text width. */
.hl-step .form-row.place-order,
.hl-step #payment .form-row.place-order {
  display: block;
  width: 100%;
  padding: 0;
  margin-top: 18px;
}
.hl-step .hl-place-order { width: 100%; max-width: none; }


/* ============================================================
   Drawer · the running offer

   The reference shows the live promotion as a banner with a
   one-tap Apply, instead of assuming people already know the
   code. A code nobody types is a discount the store pays for in
   abandoned carts and gets nothing back for.

   Gold ground, because this is the one row in the drawer that is
   the brand talking rather than the cart reporting.
   ============================================================ */
.cd-offer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  margin-bottom: 12px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  background: var(--accent-tint);
}
.cd-offer-badge {
  align-self: start;
  padding: 4px 9px;
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  white-space: nowrap;
}
.cd-offer-t {
  margin: 0;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink);
}
.cd-offer-b {
  margin: 2px 0 0;
  font-size: .75rem;
  line-height: 1.45;
  color: var(--ink-2);
}
.cd-offer-b b { color: var(--accent-deep); font-weight: 600; letter-spacing: .02em; }
.cd-offer-apply,
.cart-offer-apply {
  flex: none;
  min-height: 36px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: .8125rem;
  font-weight: 500;
  cursor: pointer;
  transition: opacity var(--motion), transform var(--motion);
}
.cd-offer-apply:hover,
.cart-offer-apply:hover { opacity: .88; }
.cd-offer-apply:active,
.cart-offer-apply:active { transform: scale(.97); }
.cd-offer-apply:disabled,
.cart-offer-apply:disabled { opacity: .5; cursor: default; }

/* The same card on the cart page, where it is not living in a 400px
   drawer: it can breathe, and the button can be a full 44px target
   rather than the 36px the drawer's width argues for. */
.cart-offer {
  margin: 0 0 14px;
  padding: 14px 16px;
  background: var(--accent-tint, var(--tint));
}
.cart-offer-apply { min-height: 44px; padding: 0 20px; font-size: .875rem; }
@media (max-width: 480px) {
  /* Badge and copy stack, and the button takes the full width under
     them. At 320 the three-column grid gave the copy 96px and broke
     "Applies to this whole cart" across four lines. */
  .cart-offer { grid-template-columns: auto minmax(0, 1fr); gap: 10px 12px; }
  .cart-offer .cart-offer-apply { grid-column: 1 / -1; width: 100%; }
}

/* ------------------------------------------------------------
   Discounts, where the money is
   ------------------------------------------------------------
   Both the drawer and the cart page stopped at the subtotal, which
   WooCommerce computes before coupons. With HALO20 on a two-line
   cart that meant the drawer said $129.98 and the cart page said
   $103.98 for the same order, and the panel a buyer actually opens
   after adding was the one showing the higher number. There is only
   one reading of a code that appears to have done nothing.
   ------------------------------------------------------------ */
.cd-total-due {
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--line);
}
.cd-total-due b { font-size: 1.25rem; }

/* Money coming off is green wherever it appears, so the eye finds it
   in the same colour on the drawer and on the cart page. */
.sum-row.saving { color: var(--success); }
.sum-row.saving span:last-child {
  font-family: var(--font-num);
  font-weight: 600;
}

/* ------------------------------------------------------------
   Checkout · the offer, at the last step
   ------------------------------------------------------------
   Sits with the "Returning customer?" and "Have a coupon?" rows and
   matches their shape, so the three read as one stack of options
   rather than as a banner dropped on top of a form.
   ------------------------------------------------------------ */
.co-promo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  padding: 14px 16px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius);
  background: var(--tint-gold);
  font-size: .9375rem;
  color: var(--ink-2);
}
.co-promo-t b { color: var(--ink); font-weight: 700; letter-spacing: .02em; }

.co-promo-tap {
  flex: none;
  min-height: 40px;
  padding: 0 18px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: #fff;
  font-size: .875rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: opacity var(--motion), transform var(--motion);
}
.co-promo-tap:hover { opacity: .88; color: #fff; }
.co-promo-tap:active { transform: scale(.97); }
.co-promo-tap:disabled { opacity: .5; cursor: default; }

/* Applied: the row stops being an offer and becomes a receipt, so it
   drops the gold and takes the same green every other confirmed saving
   on the site uses. */
.co-promo.is-on {
  border-color: rgba(47, 122, 82, .28);
  background: var(--tint-sage);
  color: var(--vivid-sage);
  justify-content: flex-start;
}
.co-promo.is-on .co-promo-t b { color: var(--vivid-sage); }
.co-promo.is-on .co-promo-t::before {
  content: "✓ ";
  font-weight: 700;
}

/* ------------------------------------------------------------
   How payment works, before the typing starts
   ------------------------------------------------------------
   Prints on the cart and at the top of checkout step one, and
   only while the store cannot take a card. Deliberately a
   neutral note rather than a warning colour: it is a fact about
   the flow, not an error the buyer has made.
   ------------------------------------------------------------ */
.hl-pay-note {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: 0 0 16px;
  padding: 11px 13px;
  border-radius: var(--radius);
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  font-size: .8125rem;
  line-height: 1.55;
  color: var(--ink-2);
}
.hl-pay-note svg {
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  color: var(--accent-deep);
}
.hl-step-trust + .hl-pay-note { margin-top: 2px; }
.woocommerce-cart .hl-pay-note { margin-bottom: 14px; }

/* ============================================================
   Order received
   ------------------------------------------------------------
   The last screen of the funnel. Everything from `.ty-details`
   down is WooCommerce's own markup, so those rules target its
   class names rather than ours: the order table, the address
   columns and the gateway's bank-detail block all arrive from
   the plugin and had no styling at all before this block, which
   is why a completed order landed on a page that looked nothing
   like the six screens that led to it.
   ============================================================ */

/* A receipt does not want the full 1440. Capped here rather than on
   .wrap, which the footer and the rest of the site share. */
.ty {
  max-width: 1080px;
  margin-inline: auto;
  padding-bottom: 56px;
}

/* ---------- Head ---------- */

.ty-head {
  max-width: 640px;
  margin: 0 auto 32px;
  text-align: center;
}
.ty-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  height: 60px;
  margin-bottom: 16px;
  border-radius: 50%;
  background: var(--success-tint);
  box-shadow: inset 0 0 0 1px var(--success-line), 0 10px 26px -14px rgba(21, 128, 61, .5);
  color: var(--success);
  /* The one flourish on the screen. It draws the eye to the answer
     of the only question being asked here, then stops. */
  animation: ty-pop 520ms var(--ease-out) both;
}
.ty-mark svg { width: 28px; height: 28px; stroke-width: 2; }
.ty-mark.is-fail {
  background: #fef2f2;
  box-shadow: inset 0 0 0 1px #fecaca, 0 10px 26px -14px rgba(185, 28, 28, .45);
  color: #b91c1c;
}
@keyframes ty-pop {
  0%   { transform: scale(.7); opacity: 0; }
  60%  { transform: scale(1.06); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.ty-head .eyebrow { display: block; margin-bottom: 8px; }
.ty-head h1 {
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 6.4vw, 2.5rem);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -.02em;
  color: var(--ink);
}
.ty-lede {
  margin: 0;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink-2);
}
.ty-lede b { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }

/* The success notice is already said by the head above it, so when a
   gateway filters in something extra it renders as an aside rather
   than as a second announcement. */
.ty-extra {
  margin: 14px 0 0;
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  background: var(--accent-tint);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--accent-deep);
  border-left: 0;
}
.ty-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 20px 0 0;
  padding: 0;
  background: none;
  border: 0;
}
.ty-actions .btn { min-height: 48px; padding-inline: 24px; }

/* ---------- Layout ---------- */

/* min-width:0 on both columns, not just the grid.
   A grid item's automatic minimum size is its content, so the order
   table -- which carries a long right-aligned shipping line -- pushed
   the single-column mobile grid to 401px inside a 375px viewport and
   put a horizontal scrollbar on a receipt. */
.ty-grid { display: grid; gap: 28px; min-width: 0; }
.ty-main,
.ty-aside { min-width: 0; }

@media (min-width: 1000px) {
  .ty-grid {
    grid-template-columns: minmax(0, 1fr) 320px;
    align-items: start;
    gap: 40px;
  }
  /* Sticky only where there is vertical room for it to be useful.
     The order table is the tall column; the aside is four short
     cards and would otherwise leave a column of white. */
  .ty-aside { position: sticky; top: calc(var(--header-h) + 20px); }
}

/* ---------- Order facts ---------- */

.ty-facts {
  display: grid;
  gap: 0;
  margin: 0 0 28px;
  padding: 0;
  list-style: none;
  background: var(--bg);
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
}
/* Each cell draws its own top and left hairline, rather than a panel
   background showing through 1px gaps. The gap trick paints the empty
   grid area too, so four facts in a three-wide row left a grey slab
   where the fifth cell would have been. Drawing per cell has no such
   hole, and the leading edges land exactly on the panel's own ring. */
.ty-fact {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 18px;
  background: var(--bg);
  box-shadow: inset 1px 1px 0 var(--line);
}
.ty-fact-k {
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--muted);
}
.ty-fact-v {
  font-family: var(--font-num);
  font-size: .9375rem;
  font-weight: 600;
  color: var(--ink);
  text-align: right;
  overflow-wrap: anywhere;
}
.ty-fact-v .woocommerce-Price-amount { font-family: var(--font-num); }

@media (min-width: 620px) {
  .ty-facts { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .ty-fact {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 5px;
    padding: 16px 18px 18px;
  }
  .ty-fact-v { text-align: left; }
}

/* ---------- What happens next ---------- */

.ty-next { margin-bottom: 28px; }
.ty-next h2,
.ty-pay h2,
.ty-details .woocommerce-order-details__title,
.ty-details .woocommerce-column__title,
.ty-details .woocommerce-customer-details h2 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--ink);
}
.ty-pay h2 { display: flex; align-items: center; gap: 9px; }
.ty-pay h2 svg { width: 19px; height: 19px; color: var(--accent); }

.ty-steps {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ty-step {
  position: relative;
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  gap: 14px;
  padding-bottom: 20px;
}
.ty-step:last-child { padding-bottom: 0; }
/* The connector, drawn behind the numbers rather than between them, so
   it needs no knowledge of how tall any one step's copy runs. */
.ty-step:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 15px;
  top: 32px;
  bottom: 8px;
  width: 1px;
  background: var(--line);
}
.ty-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  font-family: var(--font-num);
  font-size: .875rem;
  font-weight: 700;
  color: var(--accent-deep);
}
.ty-step-body { padding-top: 3px; }
.ty-step-body h3 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 5px;
  font-size: 1rem;
  font-weight: 650;
  line-height: 1.3;
  letter-spacing: -.01em;
  color: var(--ink);
}
.ty-step-body h3 svg { width: 17px; height: 17px; flex: none; color: var(--accent); }
.ty-step-body p {
  margin: 0;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--ink-2);
}

@media (min-width: 760px) {
  .ty-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
  .ty-step {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;
    padding: 18px 16px 18px;
    border-radius: var(--radius);
    background: var(--bg);
    box-shadow: inset 0 0 0 1px var(--line);
  }
  /* Horizontal now, so the vertical rule would run across the copy. */
  .ty-step:not(:last-child)::before { display: none; }
  .ty-step-body { padding-top: 0; }
}

/* ---------- Gateway instructions ---------- */

.ty-pay {
  margin-bottom: 28px;
  padding: 20px 20px 22px;
  border-radius: var(--radius);
  background: var(--accent-tint);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.ty-pay p { margin: 0 0 10px; font-size: .9375rem; line-height: 1.65; color: var(--ink-2); }
.ty-pay p:last-child { margin-bottom: 0; }
.ty-pay h2:not(:first-child) { margin-top: 18px; }
/* The offline gateways print their account rows as an unstyled list. */
.ty-pay ul.order_details,
.ty-pay ul.wc-bacs-bank-details {
  display: grid;
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--accent-line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.ty-pay ul.order_details li,
.ty-pay ul.wc-bacs-bank-details li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding: 11px 15px;
  background: var(--bg);
  font-size: .875rem;
  color: var(--muted);
}
.ty-pay ul.order_details li strong,
.ty-pay ul.wc-bacs-bank-details li strong {
  font-family: var(--font-num);
  font-weight: 600;
  color: var(--ink);
  text-align: right;
  overflow-wrap: anywhere;
}

/* ---------- WooCommerce's own order table ---------- */

.ty-details { margin: 0; }
.ty-details .woocommerce-order-details,
.ty-details .woocommerce-customer-details { margin-bottom: 28px; }
.ty-details .woocommerce-customer-details:last-child,
.ty-details .woocommerce-order-details:last-child { margin-bottom: 0; }

.ty-details table.woocommerce-table--order-details {
  width: 100%;
  margin: 0;
  border: 0;
  border-collapse: separate;
  border-spacing: 0;
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
  font-size: .9375rem;
}
.ty-details table.woocommerce-table--order-details th,
.ty-details table.woocommerce-table--order-details td {
  padding: 13px 18px;
  border: 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}
.ty-details table.woocommerce-table--order-details thead th {
  background: var(--tint);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.ty-details table.woocommerce-table--order-details td:last-child,
.ty-details table.woocommerce-table--order-details th:last-child {
  text-align: right;
}
/* Prices only. The shipping row's value is "$8.00 via Standard (3-5
   business days)", and holding that on one line is what made the
   table wider than the phone it was being read on. */
.ty-details table.woocommerce-table--order-details tbody td:last-child { white-space: nowrap; }
.ty-details table.woocommerce-table--order-details .woocommerce-Price-amount { white-space: nowrap; }
.ty-details table.woocommerce-table--order-details tbody td a {
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid var(--line-2);
}
.ty-details table.woocommerce-table--order-details tbody td a:hover { border-bottom-color: var(--accent); }
.ty-details .product-quantity {
  font-family: var(--font-num);
  font-weight: 600;
  color: var(--muted);
}
.ty-details .woocommerce-Price-amount { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
.ty-details .wc-item-meta {
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  font-size: .8125rem;
  color: var(--muted);
}
.ty-details .wc-item-meta p { margin: 0; display: inline; }
.ty-details tfoot th {
  background: var(--bg);
  font-weight: 500;
  color: var(--ink-2);
}
.ty-details tfoot td { background: var(--bg); font-family: var(--font-num); font-weight: 600; color: var(--ink); }
.ty-details tfoot tr:last-child th,
.ty-details tfoot tr:last-child td {
  border-bottom: 0;
  background: var(--tint);
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--ink);
}

/* ---------- Addresses ---------- */

.ty-details .woocommerce-columns--addresses {
  display: grid;
  gap: 12px;
  margin: 0;
}
@media (min-width: 620px) {
  .ty-details .woocommerce-columns--addresses { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.ty-details .woocommerce-columns--addresses .woocommerce-column {
  padding: 18px 20px 20px;
  border-radius: var(--radius);
  background: var(--tint);
  box-shadow: inset 0 0 0 1px var(--line);
}
.ty-details .woocommerce-column__title { font-size: 1rem; margin-bottom: 8px; }
.ty-details address {
  margin: 0;
  font-style: normal;
  font-size: .9375rem;
  line-height: 1.7;
  color: var(--ink-2);
}
.ty-details address p { margin: 6px 0 0; }

/* ---------- Aside ---------- */

.ty-aside { display: grid; gap: 12px; }
.ty-card {
  padding: 18px 20px 20px;
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--line);
}
.ty-card h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 7px;
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -.01em;
  color: var(--ink);
}
.ty-card h2 svg { width: 17px; height: 17px; flex: none; color: var(--accent); }
.ty-card p {
  margin: 0;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--ink-2);
}
.ty-card-legal { background: var(--tint); }
.ty-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 12px;
  font-size: .875rem;
  font-weight: 600;
  color: var(--accent-deep);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.ty-link svg {
  width: 15px;
  height: 15px;
  flex: none;
  transition: transform var(--motion);
}
.ty-link:hover svg { transform: translateX(3px); }
.ty-continue { min-height: 48px; }

/* The vial from the order itself, floating over the aside. Hidden on
   phones: the aside stacks under a long order table there, and a
   decorative bottle at the bottom of a receipt is decoration nobody
   scrolls to. */
.ty-deco { display: none; }
@media (min-width: 1000px) {
  .ty-deco {
    display: block;
    position: relative;
    width: 100%;
    height: 190px;
    margin-bottom: 4px;
    pointer-events: none;
  }
  .ty-deco::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 200px;
    height: 200px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(circle, var(--tint-sage) 0%, rgba(226, 240, 231, .42) 46%, rgba(226, 240, 231, 0) 70%);
  }
  .ty-deco .vial {
    position: relative;
    z-index: 1;
    display: block;
    width: auto;
    height: 100%;
    margin: 0 auto;
    object-fit: contain;
    filter: drop-shadow(0 14px 24px rgba(83, 56, 12, .18));
    animation: h-float-slow 6.5s ease-in-out infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ty-mark { animation: none; }
  .ty-deco .vial { animation: none; }
  .ty-link:hover svg { transform: none; }
}

/* ------------------------------------------------------------
   Cart row: two targets that measured under the thumb minimum
   ------------------------------------------------------------ */

/* The remove button sat at right:-8px to optically centre its glyph,
   which put its box 4px past the viewport on a 375px phone and gave
   the cart a 3px horizontal scroll. It is a 44px square around a
   small glyph, so pulling the box back to the row edge moves the
   glyph by 8px and fixes the scroll. */
.cart-remove { right: 0; }

/* The product title in a cart row is an 18px-tall inline link. The
   negative margin cancels the padding for layout, so the hit box
   grows to 30px without moving anything on the row. */
.cart-info h3 a {
  display: inline-block;
  padding-block: 6px;
  margin-block: -6px;
}

/* The cart row's middle track was `1fr`, which is minmax(auto, 1fr):
   it floors at the content's min-content width. .cart-info measured
   186px there, so the row could not go below 332px and the cart page
   scrolled sideways on a 320px screen. minmax(0, 1fr) lets the track
   shrink; min-width:0 lets the block inside it agree. */
.cart-row { grid-template-columns: 88px minmax(0, 1fr) auto; }
.cart-info { min-width: 0; }
.cart-info h3,
.cart-info .card-meta { overflow-wrap: anywhere; }

@media (max-width: 380px) {
  /* At this width the price and the remove button both sit on the
     right of the row, so the reserved gutter can come in. */
  .cart-info { padding-right: 28px; }
}

/* ============================================================
   Checkout · WooCommerce's own checkboxes
   ------------------------------------------------------------
   Two controls on the checkout come from WooCommerce rather than
   from this theme: "Create an account?" and "Ship to a different
   address?". They were the last native 13x13 boxes in the funnel,
   sitting three inches under two custom 20px consent boxes that
   this file already styles, on the one screen where an unfamiliar
   control reads as a broken page.

   Styled on the input itself rather than by copying the .hl-consent
   pattern, because that pattern needs a sibling <span class="hl-box">
   and this markup is WooCommerce's. appearance:none plus a check
   painted as a background image needs no extra element, and the SVG
   is the same path halotir_icon('check') draws.
   ============================================================ */

body.woocommerce-checkout .woocommerce-form__label-for-checkbox {
  display: grid;
  grid-template-columns: 20px 1fr;
  align-items: start;
  gap: 12px;
  margin: 0;
  cursor: pointer;
  font-size: .8125rem;
  line-height: 1.6;
  color: var(--ink-2);
}
body.woocommerce-checkout .woocommerce-form__label-for-checkbox span { padding-top: 1px; }

body.woocommerce-checkout input[type="checkbox"].woocommerce-form__input-checkbox {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
  border: 2px solid var(--line-2);
  border-radius: 4px;
  background: var(--bg) no-repeat center / 13px 13px;
  cursor: pointer;
  transition: background-color var(--motion), border-color var(--motion);
}
body.woocommerce-checkout input[type="checkbox"].woocommerce-form__input-checkbox:checked {
  background-color: var(--ink);
  border-color: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.5l5 5 10-11'/%3E%3C/svg%3E");
}
/* 10-tokens.css rounds every :focus-visible to a pill, which on a
   20px square draws a circle around a rounded rectangle. */
body.woocommerce-checkout input[type="checkbox"].woocommerce-form__input-checkbox:focus-visible {
  box-shadow: var(--ring);
  border-radius: 4px;
}

/* WooCommerce wraps the ship-to label in an <h3>, so it rendered at
   18.4px display weight beside a checkbox: a heading that is really a
   toggle. It reads as the control it is. */
body.woocommerce-checkout h3#ship-to-different-address {
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-family: var(--font);
  font-size: .875rem;
  font-weight: 500;
  letter-spacing: 0;
}
body.woocommerce-checkout .woocommerce-account-fields { margin-top: 16px; }

/* ============================================================
   TAP TARGETS ON WOOCOMMERCE'S OWN LINKS
   The last four controls in the funnel measuring under 44px are
   links WooCommerce prints itself: the two toggles at the top of
   checkout, the coupon remove link in the cart totals, and the
   focus-header wordmark. All four clear the 24px WCAG AA minimum
   on their own, so none of them is a violation, but all four sit
   next to controls built to 44 and read as less tappable for it.
   Every fix here is padding plus a matching negative margin, so
   the hit area grows and nothing moves. Resizing them instead
   would push the notice rows and the sticky header taller, which
   is the wrong trade for a target that already passes.

   Revised after measuring on a phone rather than reasoning about
   it. min-height on a border box is a floor, not a total, and
   these line boxes are 19.6px, so 24 plus 20 of padding never
   bound anything: the coupon toggle came out 40px and the remove
   link 41.6px, both short of the 44 they were aiming at. The
   floors are now the figure they were meant to produce, and the
   negative margins absorb the difference so the rows still do
   not move: the toggle occupies 20px of flow where it occupied
   19.6, and the remove link 22px where it occupied 19.6.
   ============================================================ */
.woocommerce-form-login-toggle a,
.woocommerce-form-coupon-toggle a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: 10px;
  margin-block: -12px;
}

/* Inline inside the discount amount, so it cannot take block padding
   without splitting the line box. inline-flex gives it a box to pad. */
.woocommerce-remove-coupon {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 11px 4px;
  margin: -11px -4px;
}

/* Centred in a 1fr auto 1fr grid whose outer columns already hold a
   44px back link, so the header keeps its height either way. */
.hl-focus-logo {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 6px;
}

/* ============================================================
   .cart-wrap: the implicit column is auto, not 1fr
   ------------------------------------------------------------
   styles.css declares `.cart-wrap { display: grid }` with no
   columns for mobile, so the single implicit track is `auto`,
   and an auto track floors at the min-content width of the
   widest item in it. At 320 that made the track 282.52px inside
   a 257px container: the form, the summary, both cart rows, the
   coupon pill and the offer card all sat 1.5px past the right
   edge and the page scrolled sideways.

   Every child gets min-width: 0 as well. Without it a grid item
   refuses to shrink below its own min-content no matter what the
   track says, and the track would just be overrun again from the
   other direction. Same fix as .stat-strip and .cart-row.

   The 1024 rule restates the two-column layout with the same
   guard, because `1fr 360px` has exactly the same floor and
   would reintroduce this on a narrow desktop window.
   ============================================================ */
.cart-wrap { grid-template-columns: minmax(0, 1fr); }
.cart-wrap > * { min-width: 0; }

@media (min-width: 1024px) {
  .cart-wrap { grid-template-columns: minmax(0, 1fr) 360px; }
}

/* The cart page's own promo toggle. .cd-promo-toggle is styled for the
   drawer, where it sits on the tinted panel; on the cart page it needs
   a little air above it now that it follows the offer card, and a real
   tap target rather than the 8px block padding the drawer gives it. */
.cart-promo-toggle {
  min-height: 44px;
  margin-top: 4px;
  color: var(--ink-2);
}
.cart-promo-toggle:hover { color: var(--ink); }

/* body.woocommerce-cart .coupon sets display:block, and any author rule
   beats the UA sheet's [hidden] { display: none }. So cart.js could set
   the hidden attribute, the property would read back true, and the field
   stayed on screen underneath the toggle meant to replace it. Checking
   node.hidden in the console agrees with the code and disagrees with the
   page; only looking at it catches this. */
body.woocommerce-cart .coupon[hidden] { display: none; }

/* Same bug, older: cart.js has always set updateBtn.hidden = true, on
   the grounds that quantity changes submit themselves and so the button
   is "a second label for a step that no longer exists". It never went
   anywhere, because .btn sets display: inline-flex. Update cart has
   been sitting on the live cart page under a comment explaining its
   own absence.

   Still per-component rather than a blanket [hidden] override: the
   checkout sheet deliberately relies on .hl-sheet[hidden] resolving to
   display: flex so it can animate out, and an !important reset would
   take the animation with it. */
#cartUpdate[hidden], .cart-update[hidden] { display: none; }
/* ============================================================
   v7 · Global chrome, partner program, content pages
   ------------------------------------------------------------
   Loads after styles.css, 10-tokens and 15-signature, so it can
   restyle the chrome both sites already render without either of
   them changing markup shape. The theme (header.php/footer.php)
   and the static site (store.js injectChrome) emit the same
   classes in slightly different DOM order, which is why the
   mobile header below places by grid cell, not by source order.

   90% of traffic is phones. Every unscoped rule here IS the
   phone layout; desktop is additive.
   ============================================================ */

/* ------------------------------------------------------------
   Header · mobile: hamburger left, wordmark centred, cart right
   ------------------------------------------------------------
   Grid with three fixed cells rather than flex order. The theme
   puts the menu button before the logo, the static site puts it
   last inside .header-actions; explicit cell placement makes
   both DOMs land identically, so neither file has to change to
   match the other. `display: contents` dissolves the actions
   wrapper so its buttons can take cells of their own. */
@media (max-width: 1023px) {
  .header-inner {
    display: grid;
    grid-template-columns: 44px 1fr 44px;
    align-items: center;
  }
  .header-actions { display: contents; }
  .header-inner .menu-btn { grid-column: 1; grid-row: 1; }
  .header-inner > .logo   { grid-column: 2; grid-row: 1; justify-self: center; }
  .header-actions .icon-btn:not(.menu-btn) { grid-column: 3; grid-row: 1; justify-self: end; }
}

/* Flat until the page moves under it. The border alone reads as
   part of the page at rest; the shadow only earns its place once
   there is content behind the header to separate from.
   chrome.js toggles the class; without JS the border still does
   the job, so this is enhancement, not dependency. */
.site-header { transition: box-shadow var(--motion); }
.site-header.is-scrolled {
  box-shadow: 0 1px 0 rgba(28, 25, 23, .03), 0 4px 16px rgba(28, 25, 23, .07);
}

/* wp_nav_menu now wraps items in a <ul> so the markup is valid;
   dissolving the ul and li keeps each <a> a direct flex child of
   .main-nav, which is what the desktop nav layout expects. The
   fallback renderer emits bare <a>s, so both shapes align. */
.main-nav ul, .main-nav li { display: contents; }

/* The count badge must survive a two-digit cart. */
.cart-count { padding: 0 4px; }

/* ------------------------------------------------------------
   Strips · one height for all three
   ------------------------------------------------------------
   .promo-bar, .announce and .ruo-banner are all full-width bands
   under the header, and every one of them had been sized by its
   own padding and its own font. On a phone that produced a 90px
   promo slab on top of a 72px legal slab: 162px of banner before
   the product, on a 812px screen, and the bottle started below
   the fold.

   min-height rather than height, because the legal line is the
   one piece of copy on the page that must not be clipped to make
   a layout work. It gets a smaller face on a phone so it fits,
   and if a translation ever runs longer the band grows instead
   of hiding a word of it.
   ------------------------------------------------------------ */
.promo-bar,
.announce,
.ruo-banner {
  box-sizing: border-box;
  min-height: var(--strip-h);
  padding-block: 6px;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px 12px;
  text-align: center;
}

/* One line on a phone. The eyebrow is already hidden below 640px;
   this drops the grid the base sheet switches to, because a grid
   is what put the code and the timer on a second row. */
@media (max-width: 639px) {
  .promo-bar {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    padding-inline: 12px;
  }
  .promo-offer {
    grid-column: auto;
    font-size: .92rem;
    line-height: 1.1;
    white-space: nowrap;
  }
  /* 11px floor on both. The code is a string someone may have to
     read and retype, and the timer is the number the whole bar
     exists to communicate; 10.56px and 10.88px put them below
     anything else on the page. The tracking and the padding give
     the width back, so the band still measures --strip-h. */
  .promo-code {
    flex: none;
    font-size: .6875rem;
    letter-spacing: .08em;
    padding: 5px 10px;
  }
  .promo-timer {
    flex: none;
    font-size: .6875rem;
    padding: 4px 9px;
  }

  /* Three short lines inside the band instead of three long ones
     spilling past it. */
  .ruo-banner {
    font-size: .6875rem;
    line-height: 1.3;
    letter-spacing: 0;
  }
}

/* ------------------------------------------------------------
   Nav drawer · native feel
   ------------------------------------------------------------ */
.nav-drawer { width: min(86vw, 360px); }

/* A row you can actually hit with a thumb, top to bottom. */
.nd-links { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.nd-links a {
  min-height: 48px;
  padding: 13px 20px;
  border-bottom: 1px solid var(--tint);
}
.nd-links a:active { background: var(--panel); }
/* .nd-links is flex:1, so the list box is as tall as the drawer and
   the content is not. Measured at 390: about 535px of rows inside a
   695px box, which left the last row's divider drawn as a rule
   across 160px of empty white with nothing under it. A divider
   separates two things; with one thing above it and nothing below
   it is just a line. */
.nd-links a:last-child { border-bottom: 0; }
.nd-links a svg { color: var(--line-2); }
/* aria-current is set by chrome.js; the drawer should say where
   you already are so tapping it is never a surprise reload. */
.nd-links a[aria-current="page"] { color: var(--accent-deep); }
.nd-links a[aria-current="page"] svg { color: var(--accent); }

/* The close control is the escape hatch, so it gets a visible
   ground and the full 44px, not a bare glyph. */
.nav-drawer .cd-close {
  width: 44px; height: 44px;
  background: var(--panel);
}
.nd-head { padding-top: calc(14px + env(safe-area-inset-top, 0px)); }

/* The scrim slightly blurs the page it covers, which is what
   sells the drawer as a layer above the page rather than a box
   drawn on it. */
.backdrop {
  background: rgba(28, 25, 23, .4);
  backdrop-filter: blur(3px) saturate(1.2);
  -webkit-backdrop-filter: blur(3px) saturate(1.2);
}

/* ------------------------------------------------------------
   Footer · mobile stack
   ------------------------------------------------------------
   Brand block full width, then the three link columns pair up
   two across. One column each would scroll for two screens; the
   labels are short enough that two columns cost nothing at
   320px and halve the height of the footer. */
@media (max-width: 1023px) {
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 22px 20px; }
  .footer-brand { grid-column: 1 / -1; }
}
/* Full-row tap targets. 11px vertical padding measured out at
   ~43px rows; 12px clears the 44px floor. */
.site-footer .footer-grid a { padding: 12px 0; min-height: 44px; display: flex; align-items: center; }
.site-footer .footer-menu { margin: 0; padding: 0; list-style: none; }

/* Tier 2 does the site's regulatory heavy lifting, so it reads
   at 13px, not fine print. Never shrink or de-emphasise this
   block: it is what allows tier 1 to stay one line. */
.fda-disclaimer p { font-size: .8125rem; }

/* Trust row above the copyright, mirroring the category leader's
   bottom bar. Claims only, no links: each one is backed
   elsewhere on the site (quality page, COA card, checkout). */
.footer-trust {
  display: flex; flex-wrap: wrap; gap: 6px 20px;
  margin-top: 18px; padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, .12);
}
.footer-trust span {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 28px;
  font-size: .78rem; color: #a8a29e;
}
.footer-trust svg { width: 15px; height: 15px; color: #e3c085; flex: none; }

/* ------------------------------------------------------------
   Partner program
   ------------------------------------------------------------
   The page has one job on a phone: the two rates and the
   mechanic inside the first screen, then a single application
   CTA. Everything below the hero is supporting evidence. */
.partner-hero { padding: 32px 0 40px; }
.partner-hero .eyebrow {
  font-family: var(--font-num); font-size: .6875rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--accent);
}
/* styles.css caps the old hero h1 at 14ch for the earnings-card
   layout; this hero runs full measure again. */
.partner-hero h1 { font-size: 2.3rem; letter-spacing: -.03em; line-height: 1.05; margin-top: 8px; max-width: 18ch; }
.partner-hero h1 em { font-family: var(--font-display); font-style: italic; font-weight: 500; color: var(--accent); }

/* The signature stat cells, scaled up: on this page the numbers
   are the argument, so they get display size even on a phone. */
.partner-lead { margin-top: 24px; }
.partner-lead .stat-fig { font-size: 3.4rem; }
.partner-lead .stat-cell { min-height: 148px; }

.partner-cta { margin-top: 20px; }
/* Full-width primary on a phone: the thumb should not have to
   aim. Inline again once there is width to spare. */
.partner-cta .btn { width: 100%; height: 52px; border-radius: var(--radius-pill); box-shadow: var(--shadow-gold); }
.partner-fine { margin-top: 12px; font-size: .8125rem; color: var(--muted); }

/* The worked example. One dark panel, one hero figure; the
   proof-card rule says never two per screen and this page obeys
   it, so the hero strip carries no dark cell. */
.partner-proof { max-width: 560px; }
.proof-card .proof-note { margin-top: 14px; font-size: .75rem; line-height: 1.5; color: rgba(255, 255, 255, .55); }

/* Four steps as a connected vertical list, not four cards: on a
   phone the mechanic should be scannable in one glance, and
   stacked cards push step four two screens down. */
.partner-steps { max-width: 560px; }
.partner-steps .step-list li { font-size: .96rem; line-height: 1.6; }
.partner-steps .step-list strong { display: block; font-size: 1.02rem; font-weight: 600; margin-bottom: 2px; }

/* Sections alternate grounds; the shortcode marks every other
   one .ground-tint so no two neighbours match. */
.partner-page .section-v6 > .wrap > .check-list,
.partner-page .section-v6 > .wrap > .fine { max-width: 62ch; }
.partner-page .faq-a { max-width: 62ch; }

@media (min-width: 640px) {
  .partner-cta .btn { width: auto; padding: 0 30px; }
}
@media (min-width: 768px) {
  .partner-hero { padding: 56px 0 60px; }
  .partner-hero h1 { font-size: 3.2rem; }
  .partner-lead { max-width: 720px; }
  .partner-lead .stat-fig { font-size: 4.4rem; }
  .partner-lead .stat-cell { min-height: 190px; padding: 24px; }
  /* .stat-strip goes four across from 768 because everywhere else it
     holds four. This strip holds two, so the cells took the first two
     of four columns: 171px each inside a 720px box, with 378px of the
     strip empty beside them and "of the merchandise total on a referred
     researcher's first order" wrapping to four lines in a 123px column.
     Two cells, two columns. */
  .partner-lead { grid-template-columns: repeat(2, 1fr); }
}

/* ------------------------------------------------------------
   Content pages
   ------------------------------------------------------------ */
/* The title band takes the warm ground so the page opens with
   one deliberate surface change instead of a wall of white. The
   prose below returns to white, keeping the two-grounds rhythm. */
.page-head.ground-tint { padding-block: 36px 28px; margin-bottom: 4px; }

/* One bottle floating in the title band, out in the space the reading
   measure leaves empty. Absolute so it contributes nothing to the band's
   height: the band is sized by its heading at every width, and the bottle
   is allowed to overhang it. */
/* overflow-x, not overflow. The band is sized by its heading and is
   shorter than the bottle, so plain `hidden` clipped the bottle top and
   bottom and it read as a cropped photograph. Clipping only sideways lets
   it hang past the band's bottom edge onto the white below, which is the
   floating look and not a crop. `clip` rather than `hidden` because
   `hidden` on one axis forces a scroll container on the other. */
.page-head.has-deco { position: relative; overflow-x: clip; }
.head-deco {
  position: absolute;
  right: 4%;
  top: 70%;
  width: 116px;
  height: 116px;
  transform: translateY(-50%);
  pointer-events: none;
  opacity: .55;
}
.head-deco::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  /* Sized to stay inside the band. At 150% the halo cleared the band's
     top edge and smudged the dark promo bar above it. */
  width: 115%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .72) 0%, rgba(255, 255, 255, .3) 45%, rgba(255, 255, 255, 0) 70%);
}
.head-deco .vial {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: contain;
  filter: drop-shadow(0 10px 18px rgba(83, 56, 12, .18));
  animation: h-float-slow 6s ease-in-out infinite;
}
/* Below the wide breakpoints the reading measure fills the band edge to
   edge, so there is no empty space for the bottle to occupy and it ends
   up behind the last word of the heading. Fading it out only turns the
   collision into a smudge. The heading gives it a lane instead, which
   costs one wrapped line and keeps both things legible. */
@media (max-width: 899px) {
  .page-head.has-deco h1 { padding-right: 108px; }
}
@media (max-width: 599px) {
  .head-deco { right: 4px; top: 50%; width: 88px; height: 88px; opacity: .62; }
  .page-head.has-deco h1 { padding-right: 96px; }
}
@media (min-width: 900px) {
  .head-deco { right: 8%; width: 168px; height: 168px; opacity: .9; }
}
@media (prefers-reduced-motion: reduce) {
  .head-deco .vial { animation: none; }
}

/* Anchors land below the sticky header, not underneath it. */
.faq-cat, .prose h2 { scroll-margin-top: calc(var(--header-h) + 14px); }

/* FAQ section jump chips. The chips are 36px visual pills by
   default; as anchors they are primary tap targets here, so
   they take the 44px floor. */
/* The chips are navigation for the page; the first category heading is
   content. 16px between them read as one cramped block. */
.faq-nav { margin: 16px 0 34px; }
.faq-nav .chip { min-height: 44px; color: var(--ink-2); }
.faq-nav .chip:active { background: var(--panel); }

/* FAQ answers: comfortable reading, quiet colour, no wall. */
.faq-cat { margin-bottom: 36px; }
.faq-cat > h2 { border-top: none; padding-top: 0; }
.faq-a { font-size: .96rem; line-height: 1.7; color: var(--ink-2); }

/* Contact page: card spacing moves out of inline styles. */
.contact-stack { display: grid; gap: 20px; align-content: start; }

/* Policy pages: a little air between a heading band and the
   dense clause lists that follow it. */
.prose ul + h2, .prose ol + h2 { margin-top: 44px; }

@media (min-width: 768px) {
  .page-head.ground-tint { padding-block: 52px 36px; }
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .site-header { transition: none; }
  .backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ============================================================
   Entry gate
   ------------------------------------------------------------
   The first screen anyone sees, so it carries more design weight
   than its word count suggests. Two legal affirmations with no
   context read as an obstacle; a vial above them says what is
   actually sold here before a word is parsed, and the same two
   questions then read as reasonable.

   Phone first: the card is a bottom sheet on a phone and a
   centred dialog from 640px, because a 560px-tall centred box on
   a 375px screen either scrolls inside itself or pushes its own
   button off screen.
   ============================================================ */
.age-gate {
  position: fixed;
  inset: 0;
  overflow: hidden;
  overscroll-behavior: contain;
  touch-action: pan-y;
  z-index: var(--z-drawer, 100);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0;
  background: rgba(28, 25, 23, .55);
  backdrop-filter: blur(6px) saturate(1.2);
  -webkit-backdrop-filter: blur(6px) saturate(1.2);
  animation: gate-fade 260ms var(--ease-out) both;
}
@keyframes gate-fade { from { opacity: 0 } to { opacity: 1 } }

.gate-card {
  width: 100%;
  max-width: 460px;
  max-height: 94vh;
  /* Both axes named explicitly. Setting only overflow-y computes
     overflow-x to auto as well, so anything that escapes horizontally
     turns the card into a sideways scroller. It did: the glow below
     overshot by 20% a side and the card measured 419px inside a 375px
     viewport, which is what made the gate drag left and right and the
     whole page render as though it were desktop width. */
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--bg);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: 0 -8px 40px rgba(28, 25, 23, .28);
  animation: gate-rise 320ms var(--ease-out) both;
}
@keyframes gate-rise {
  from { transform: translateY(24px); opacity: 0 }
  to   { transform: none; opacity: 1 }
}
.age-gate.closing-anim { opacity: 0; transition: opacity 280ms var(--ease-out); }

/* ---------- The vial ---------- */
.gate-art {
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding: 30px 20px 6px;
  /* The glow is a pseudo-element that overshoots the box on every side
     rather than a background on the box itself. A gradient painted on
     the element stops dead at its edges, and because the card behind it
     is plain white that stop is visible as a rectangle with a seam
     across the bottom. Overshooting lets it fade out in open space. */
}
.gate-art::before {
  content: "";
  position: absolute;
  /* Vertical overshoot only. The horizontal -20% was what escaped the
     card; a glow does not need to leave the box sideways to look soft,
     it just needs to reach transparent before the edge, which the
     gradient stop below does on its own. */
  inset: -35% 0 -25%;
  background: radial-gradient(60% 55% at 50% 45%, var(--tint-gold) 0%, transparent 72%);
  pointer-events: none;
}
.gate-art .vial {
  position: relative;
  height: 158px;
  width: auto;
  filter: drop-shadow(0 12px 22px rgba(28, 25, 23, .18));
  animation: h-float-slow 5s ease-in-out infinite;
}

.gate-body { padding: 4px 22px calc(22px + env(safe-area-inset-bottom, 0px)); }
.gate-body .logo { display: block; margin: 0 auto 14px; }
.gate-body .logo-img { height: 26px; }

.gate-card h2 {
  font-size: 1.4rem;
  text-align: center;
  margin-bottom: 8px;
}
.gate-sub {
  text-align: center;
  font-size: .875rem;
  line-height: 1.5;
  color: var(--ink-2);
  margin-bottom: 18px;
}

/* ---------- The two affirmations ----------
   Separate boxes, not one combined checkbox. Age and researcher
   status are two different assertions and the buyer has to make
   each of them; merging them would let one tap stand in for both. */
.attest {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 12px;
  align-items: start;
  padding: 14px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  line-height: 1.45;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color var(--motion), background-color var(--motion);
}
/* Two rules, not one selector list, and that is load bearing. If a
   browser does not understand :has(), it discards the WHOLE rule the
   selector appears in, including the .attest.on half that it does
   understand. Pairing them meant the JS added the class and nothing
   happened. Split, the .on rule survives on its own. */
.attest.on {
  border-color: var(--accent);
  background: var(--tint-gold);
  color: var(--ink);
}
.attest:has(input:checked) {
  border-color: var(--accent);
  background: var(--tint-gold);
  color: var(--ink);
}
/* Drawn, not the browser's.
   accent-color recolours a stock checkbox; it cannot change the shape,
   the radius, the border weight or the way the tick arrives. This is the
   first control anyone on the site ever touches, sitting in an otherwise
   carefully set card, and a stock square next to that typography is the
   one element that says the page was assembled rather than designed.

   The tick is a background image rather than a ::after, because Firefox
   does not render generated content on an input at all, and a checkbox
   whose checked state is invisible in one browser is worse than a plain
   one everywhere. Growing it from zero is what makes the tick land
   rather than blink. */
.attest input {
  appearance: none;
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  margin: 0;
  flex: none;
  border: 1.5px solid var(--line);
  border-radius: 7px;
  background-color: var(--bg);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.5l3.3 3.4L12.8 5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0 0;
  cursor: pointer;
  transition:
    background-color var(--motion),
    border-color var(--motion),
    background-size 190ms var(--ease-out);
}
.attest:hover input { border-color: var(--accent); }
.attest input:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  background-size: 15px 15px;
}
/* The row is the target, so the ring belongs on the row. A ring drawn
   on a 22px box inside a 48px row points at the wrong thing. */
.attest input:focus-visible { outline: none; }
.attest:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.attest strong { color: var(--ink); font-weight: 600; }

.gate-card .btn-block { width: 100%; min-height: 52px; margin-top: 6px; }
.gate-card .btn-block:disabled { opacity: .45; cursor: not-allowed; }

.gate-exit {
  display: block;
  width: 100%;
  min-height: 44px;
  margin-top: 4px;
  background: none;
  border: 0;
  font-size: .8125rem;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.gate-fine {
  margin-top: 12px;
  font-size: .75rem;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
}
.gate-fine a { color: var(--accent-deep); text-decoration: underline; }

@media (min-width: 640px) {
  .age-gate { align-items: center; padding: 24px; }
  .gate-card { border-radius: var(--radius-lg); }
  .gate-art { padding: 34px 20px 8px; }
  .gate-art .vial { height: 176px; }
}

@media (prefers-reduced-motion: reduce) {
  .age-gate, .gate-card { animation: none; }
  .gate-art .vial { animation: none; }
}

/* ---------- Short viewports ----------
   The card caps at 94vh and scrolls inside itself, so on a short phone
   the Enter button sat below its own fold: the gate looked like it had
   no action. Measured at 320x720 the card came to 677px with the
   button out of reach.

   The vial is what pays for it. It is decoration, and decoration is the
   first thing to give up room when the primary action cannot be seen.
   Two steps rather than one so a 700px phone does not get the same
   treatment as a 600px one. */
@media (max-height: 760px) {
  .gate-art { padding: 18px 20px 4px; }
  .gate-art .vial { height: 104px; }
  .gate-card h2 { font-size: 1.25rem; }
  .gate-sub { margin-bottom: 14px; font-size: .8125rem; }
  .attest { padding: 12px; margin-bottom: 8px; }
  .gate-body .logo { margin-bottom: 10px; }
  .gate-fine { margin-top: 10px; }
}
@media (max-height: 640px) {
  .gate-art { padding: 12px 20px 2px; }
  .gate-art .vial { height: 74px; }
  .gate-body .logo { display: none; }
  .gate-sub { margin-bottom: 12px; }
}

/* Insurance for any content length: the action stays on screen even if
   the panel above it scrolls. A gate whose button cannot be found reads
   as broken, not as strict. */
.gate-card .btn-block {
  position: sticky;
  bottom: 0;
  z-index: 1;
}
.gate-body {
  /* The sticky button needs an opaque strip under it, or the attestation
     text scrolls through it. */
  background: var(--bg);
}

/* ------------------------------------------------------------
   Research updates
   ------------------------------------------------------------ */
/* Sits directly above the FDA disclaimer, so it is the last thing
   read before the legal block. A rail on a phone and a grid from
   768 up, off one list: the scroll container only exists below the
   breakpoint, so there is one source of order and no second copy
   of the cards to drift. */
.ru-section { padding-block: 44px 52px; }
.ru-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}
.ru-kicker {
  margin: 0 0 4px;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent-deep);
}
.ru-head h2 { margin: 0; }
.ru-all {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  min-height: 44px;
  color: var(--ink-2);
  font-size: .9375rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
.ru-all svg { width: 15px; height: 15px; transition: transform 220ms var(--ease-out); }
.ru-all:hover { color: var(--ink); }
.ru-all:hover svg { transform: translateX(3px); }

/* Same treatment the product rails already use below 640: the heading
   takes the whole row and the link drops under it, right aligned. Side
   by side, "Research updates" wraps to two lines to make room for a link
   that is not the point of the section. */
@media (max-width: 639px) {
  .ru-head { flex-wrap: wrap; align-items: flex-start; gap: 0; }
  .ru-head > div { flex: 1 1 100%; margin-bottom: 4px; }
  .ru-head > .ru-all { margin-left: auto; }
}

.ru-rail {
  display: flex;
  gap: 12px;
  margin: 0 calc(var(--gutter) * -1);
  padding: 4px var(--gutter) 18px;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.ru-rail::-webkit-scrollbar { display: none; }
.ru-rail > .ru-card {
  flex: 0 0 clamp(240px, 76vw, 300px);
  scroll-snap-align: start;
}

.ru-card {
  display: flex;
  flex-direction: column;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: var(--shadow-sm);
  transition:
    transform 260ms var(--ease-out),
    box-shadow 260ms var(--ease-out),
    border-color 260ms var(--ease-out);
}
@media (hover: hover) {
  .ru-card:hover {
    transform: translateY(-4px);
    border-color: var(--line-2);
    box-shadow: var(--shadow-lift);
  }
}
.ru-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
.ru-tag {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding-inline: 9px;
  border-radius: var(--radius-pill);
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--accent-deep);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
/* One tint per position, the same five-step rotation the product
   tiles use, so the row reads as part of the same system. */
.ru-rail > .ru-card:nth-child(5n + 2) .ru-tag {
  background: var(--tint-sage);
  box-shadow: inset 0 0 0 1px rgba(47, 122, 82, .2);
  color: var(--vivid-sage);
}
.ru-rail > .ru-card:nth-child(5n + 3) .ru-tag {
  background: var(--tint-steel);
  box-shadow: inset 0 0 0 1px rgba(44, 82, 122, .2);
  color: var(--vivid-steel);
}
.ru-date { font-size: .8125rem; color: var(--muted); }
.ru-title {
  margin: 0 0 8px;
  font-size: 1.0625rem;
  line-height: 1.3;
  font-weight: 600;
  color: var(--ink);
}
.ru-body {
  margin: 0 0 16px;
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--ink-2);
}
.ru-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: auto;
  min-height: 44px;
  color: var(--accent-deep);
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
}
.ru-link svg { width: 15px; height: 15px; transition: transform 220ms var(--ease-out); }
.ru-link:hover svg { transform: translateX(3px); }

@media (min-width: 768px) {
  /* The rail stops being a rail: same list, laid out as a grid. */
  .ru-rail {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px;
    margin: 0;
    padding: 0;
    overflow: visible;
    scroll-snap-type: none;
  }
  .ru-rail > .ru-card { flex: none; }
  .ru-card { padding: 22px; }
  .ru-section { padding-block: 68px 76px; }
}
/* ============================================================
   Floating vial photography
   ------------------------------------------------------------
   The card surfaces take a rectangular 3:4 photograph that fills
   its box edge to edge. The floating surfaces do not: the hero
   collage, the promo rain and the entry gate scatter a bottle
   over the page with nothing behind it, so they take a cutout on
   transparency instead.

   PHP emits those cutouts as <img class="vial vial-photo">, on
   purpose. Every surface in the system already sizes .vial with
   a height and width:auto, so the photograph inherits all of the
   existing geometry, rotations and float animations for free.
   This file therefore adds only the two things an <img> needs
   that an <svg> did not.
   ============================================================ */

/* Percentage heights fail on a photographic vial in a way the drawn one
   never did, and the failure is silent. The floating slots are grids with
   auto-sized rows, so the row measures the image at its natural 480px,
   the image then resolves its own height:100% against that 480px row, and
   the bottle renders at full size and blows out of its slot. An inline SVG
   dodged the whole thing because a viewBox gives it a resolvable ratio and
   no intrinsic pixel height to report.

   Taking the image out of flow breaks the loop: an absolutely positioned
   child contributes nothing to track sizing, and inset resolves against
   the slot's own definite height. The slot then needs its own width, which
   is what the aspect-ratio below is for. */
.hero-vial {
  aspect-ratio: 47 / 96;   /* the bottle's ratio, so height drives width */
}
.hero-vial img.vial {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* The rain tile is a 46px rounded chip and the vial sat at 78% inside it.
   Inset reproduces that breathing room without a percentage height. */
.rain-tile img.vial {
  position: absolute;
  inset: 5px;
  width: calc(100% - 10px);
  height: calc(100% - 10px);
}

img.vial {
  /* The drawing scaled to its viewBox. A raster needs telling. */
  object-fit: contain;

  /* drop-shadow, never box-shadow. box-shadow traces the element
     box, which on a transparent PNG paints a hard rectangle in
     mid-air around the bottle. drop-shadow follows the alpha, so
     the shadow is the bottle's own silhouette. */
  filter: drop-shadow(0 10px 20px rgba(83, 56, 12, .22));
}

/* A 46px tile cannot carry a 20px shadow; it just turns to mud.
   The rain also runs its vials at .85 opacity to sit them back
   behind the panel copy, which a photograph reads as washed out,
   so it comes back up. */
.rain-tile img.vial {
  opacity: 1;
  filter: drop-shadow(0 3px 7px rgba(83, 56, 12, .3));
}

/* The plate goes with it, for the same reason and one step later.
   styles.css:1635 draws every rain tile as a 46px chip with a 7%
   white fill and a gold hairline, and that was right when the
   thing inside it was a line drawing: the chip was the object and
   the drawing was its mark. A photograph is already an object. Put
   one inside a lit rounded rectangle and it stops being a bottle
   drifting behind the copy and becomes a thumbnail somebody left
   on the panel, which is exactly how it read at 390px: two small
   pale cards sitting on the paragraph, one of them clipped by the
   panel edge.

   So the tile keeps its geometry and its motion and gives up its
   surface. What drifts behind the sentence now is the silhouette
   of a vial and its own soft shadow, which is what the rain was
   always meant to be. The 26% opacity on .rain is doing the
   sitting-back that the .85 on the vial used to do badly. */
.rain-tile {
  background: none;
  border: 0;
}

/* The gate art is the largest single vial on the site and sits on
   near-white, so it can carry a longer, softer throw. */
.gate-art img.vial {
  filter: drop-shadow(0 16px 30px rgba(83, 56, 12, .2));
}

/* The hero collage overlaps four bottles at different depths. A
   little more lift separates the front ones from the back ones. */
.hero-vial img.vial {
  filter: drop-shadow(0 14px 26px rgba(83, 56, 12, .26));
}

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

/* ------------------------------------------------------------
   The same circular sizing, in every small token slot
   ------------------------------------------------------------
   The hero collage was not a special case. Every one of these
   slots is a centred grid holding an image at height:100%, so
   the auto row measures the photograph at its natural height,
   the photograph resolves its 100% against that row, and it
   renders oversized. A 40px box-slot chip was drawing its image
   at 40x53 and overflowing by a third. Out of flow fixes it: an
   absolutely positioned child contributes nothing to track
   sizing, and inset resolves against the slot's own definite box.

   These slots take the cutout, not the card photograph, which is
   why the fit below is `contain`. At 36-84px a centre-crop of a
   3:4 product shot is a square of blank label: no cap, no glass,
   no silhouette, nothing that reads as a vial at a glance. The
   cutout scaled down stays a recognisable little bottle, and it
   needs its whole height to do that.
   ------------------------------------------------------------ */
.box-slot-thumb,
.box-gift-thumb,
.tray-token,
.cd-thumb,
.cd-bump-media,
.cart-media,
.is-token,
*:has(> .is-token) { position: relative; }

/* Taking the image out of flow moves the collapse up a level rather
   than curing it. `.cart-media-inner` is a grid item whose only child
   is now absolute, so the auto column measured nothing and the track
   came out 0 wide inside an 88px box: the vial was there, correctly
   fitted, and one pixel across. The wrapper has to leave the flow for
   the same reason the image did. */
.is-token {
  position: absolute;
  inset: 0;
}

/* `img.vial`, not `img`, and the class is load-bearing. Every one of
   these slots already carries a legacy rule sizing the drawn vial by
   percentage height with width:auto, and `.cart-media .vial` at two
   classes outranks `.is-token > img` at one class and one type. The
   photograph was inheriting height:84% and width:auto, and width:auto
   on an out-of-flow image that has not decoded yet shrink-to-fits to
   nothing: an 88px cart thumbnail holding a correctly fitted vial
   zero pixels wide. Matching the class puts these back in front. */
.box-slot-thumb > img,
.box-gift-thumb > img,
.tray-token > img,
.is-token > img,
.box-slot-thumb > img.vial,
.box-gift-thumb > img.vial,
.tray-token > img.vial,
.is-token > img.vial {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: contain;
}

/* The generic vial shadow is a 20px throw sized for a 300px hero
   bottle. On a 40px chip that is wider than the vial itself and
   just reads as smudge, so it comes down to something the chip
   can actually carry. */
.box-slot-thumb > img.vial,
.box-gift-thumb > img.vial,
.tray-token > img.vial,
.is-token > img.vial {
  filter: drop-shadow(0 2px 4px rgba(83, 56, 12, .28));
  opacity: 1;
}

/* ------------------------------------------------------------
   The box picker card, and the last of the circular sizing
   ------------------------------------------------------------
   `.box-card-media` is a 1:1 tile and the photograph is 3:4, and
   until now nothing gave it an object-fit at all. The default is
   `fill`, so every vial in the picker grid was being squashed a
   third narrower than it was shot. Nobody caught it because the
   drawn SVG scaled to its viewBox and never stretched.

   The same tile also reproduced the grid-sizing failure at phone
   widths. `.box-media-inner` asks for height:100% of a parent
   whose aspect-ratio should make that definite, but as a grid
   item the tile carries min-height:auto, so it grew to its own
   content instead: a 158px column holding an 800px-tall image,
   the vial drawn one fifth as wide as it was tall. Clearing the
   min-height lets the aspect-ratio hold, and taking the image out
   of flow stops it reporting a height for anything to grow to.
   ------------------------------------------------------------ */
.box-card-media,
.proof-media { min-height: 0; }

.box-media-inner,
.proof-media > div { position: relative; }

.box-media-inner > img.photo,
.proof-media > div > img.photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

/* The picker tile was square while the shop card next door is 4:5 and
   the product page is 3:4, and square is the one that does not fit a
   bottle. A 1:1 crop of a 3:4 photograph takes a quarter of the height
   off, which lands on the cap at the top and the plinth at the base:
   the vial arrives beheaded and stood on nothing. Matching the shop
   card gets both ends back and costs about fifty pixels a row.

   Scoped to tiles that actually hold a photograph, so any product
   still falling back to the drawn vial keeps the square it was drawn
   for. */
.box-card-media:has(img.photo),
.box-card-media:has(.is-cut) { aspect-ratio: 4 / 5; }

/* The picker moved to cutouts alongside the shop grid and the rails, so
   it gets the ground they get. The photograph brought its own tan
   backdrop and hid whatever was painted underneath; without one, the
   tile is bare --panel and ten near-white squares in a row read as a
   spreadsheet. The rotation is the same five tints in the same order,
   so a compound sits on the same colour wherever it appears. */
.box-card-media:has(.is-cut) { background: var(--tint-gold); }
.box-grid > .box-card:nth-child(5n + 2) .box-card-media:has(.is-cut) { background: var(--tint-sage); }
.box-grid > .box-card:nth-child(5n + 3) .box-card-media:has(.is-cut) { background: var(--tint-steel); }
.box-grid > .box-card:nth-child(5n + 4) .box-card-media:has(.is-cut) { background: var(--tint-clay); }
.box-grid > .box-card:nth-child(5n + 5) .box-card-media:has(.is-cut) { background: var(--tint-violet); }


/* ============================================================
   Floating bottles as page furniture
   ------------------------------------------------------------
   The home hero has carried a four-bottle collage since v6, and
   it is what stops that page reading as a document. Every other
   landing surface was type on a gradient: the shop hero, the
   partner hero, the empty cart, the 404. Same motif, smaller
   scale, so the rest of the site belongs to the same product.

   Three rules the decoration must never break.

   It cannot take a click, because it lies across the copy.

   It cannot compete with the copy for contrast, which is why the
   phone gets exactly one bottle, faint and mostly outside the
   frame. The first pass put a second one at the top right and it
   landed directly on the word "verified" in the headline.

   And it may only leave the frame through the right-hand edge. A
   bottle crossing the bottom of the hero gets sliced flat by the
   overflow clip and reads as a rendering fault; the same bottle
   running off the side reads as a photograph that continues past
   the page. Every slot below is therefore positioned to sit
   fully inside the band vertically.
   ============================================================ */
/* One knob per surface. The three slots are sized off --hv-h rather
   than given their own pixel values, because the constraint that
   matters is the height of the band they sit in and that differs by
   page: the shop hero is one headline and one paragraph, the partner
   hero carries a stat strip and a button and is three times taller.
   Hard-coding a 250px bottle looked right on the partner page and hung
   out of both ends of the shop hero, where the clip cut it flat. */
.hero-vials {
  --hv-h: 156px;
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* The heroes are painted gradients that never needed a stacking
   context before. Without these the bottles escape to the page
   box and land somewhere else entirely. */
.shop-hero,
.partner-hero {
  position: relative;
  overflow: hidden;
}
.shop-hero > .wrap,
.partner-hero > .wrap { position: relative; z-index: 1; }

/* Height drives the slot and the ratio drives the width, the same
   contract .hero-vial uses. The image is out of flow inside it so
   nothing can measure the file and resize the box around it. */
.hv {
  position: absolute;
  display: block;
  aspect-ratio: 47 / 96;
}
.hv img.vial {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: contain;
  filter: drop-shadow(0 12px 22px rgba(83, 56, 12, .18));
}

/* Each slot's tilt lives in its own keyframes rather than in a
   transform declaration beside the animation. An animation beats a
   plain declaration on the same property, so `transform: rotate()`
   next to `animation: h-float` is simply discarded the moment the
   animation starts and the bottle stands up straight. */
@keyframes hv-drift-1 {
  0%, 100% { transform: rotate(-8deg) translateY(0); }
  50%      { transform: rotate(-8deg) translateY(-9px); }
}
@keyframes hv-drift-2 {
  0%, 100% { transform: rotate(11deg) translate(0, 0); }
  50%      { transform: rotate(11deg) translate(6px, -7px); }
}
@keyframes hv-drift-3 {
  0%, 100% { transform: rotate(-17deg) translateY(0); }
  50%      { transform: rotate(-17deg) translateY(-12px); }
}

/* Phone: one bottle, bottom right, three quarters of it outside
   the frame. Enough to say the page is about bottles, not enough
   to be read as content or to sit under a word. */
.hv1 {
  height: var(--hv-h);
  right: -30px;
  bottom: 8px;
  opacity: .6;
  animation: hv-drift-1 6s ease-in-out infinite;
}
.hv2 { height: calc(var(--hv-h) * .62); }
.hv3 { height: calc(var(--hv-h) * .46); }
.hv2,
.hv3 { display: none; }

/* Tablet up: the copy stops using the full width, so a second
   bottle has somewhere to be and the first can come into frame
   and gain contrast. */
@media (min-width: 768px) {
  .shop-hero .hero-vials { --hv-h: 186px; }
  .partner-hero .hero-vials { --hv-h: 216px; }

  .hv1 {
    right: 5%;
    bottom: 16px;
    opacity: .8;
  }
  .hv2 {
    display: block;
    right: 26%;
    top: 14px;
    opacity: .62;
    animation: hv-drift-2 9s ease-in-out infinite;
  }

  /* The shop hero is a headline and a paragraph, which on a wide
     screen is under 200px of content and left the band shorter than
     the bottle standing in it. The extra room is what the decoration
     needs, and a 1280px page wants it anyway. */
  .shop-hero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 268px;
  }
}

/* Desktop: the third bottle, small and tucked into the right
   margin, which is what turns two objects into a group. */
@media (min-width: 1024px) {
  .shop-hero .hero-vials { --hv-h: 208px; }
  .partner-hero .hero-vials { --hv-h: 260px; }

  .hv1 { right: 7%; }
  .hv2 { right: 27%; }
  .hv3 {
    display: block;
    right: -18px;
    top: 30%;
    opacity: .5;
    animation: hv-drift-3 7.5s ease-in-out 1s infinite;
  }
}

/* ---------- The solo variant ----------
   The empty cart and the 404 have no hero to decorate, so the
   bottle is the content: in flow, centred, above the sentence.
   Overriding position is what takes .hero-vials out of the
   absolute layout the heroes need. */
.hero-vials.hv-solo {
  position: relative;
  inset: auto;
  display: block;
  height: 190px;
  margin: 0 auto 18px;
  overflow: visible;
}
.hv-solo .hv1 {
  position: absolute;
  height: 190px;
  right: auto;
  bottom: auto;
  left: 50%;
  top: 0;
  opacity: 1;
  /* Centring by translate, so it has to travel inside the
     keyframes for the same reason the tilts do. */
  animation: hv-solo-float 5s ease-in-out infinite;
}
@keyframes hv-solo-float {
  0%, 100% { transform: translateX(-50%) translateY(0) rotate(-5deg); }
  50%      { transform: translateX(-50%) translateY(-10px) rotate(-5deg); }
}

@media (min-width: 768px) {
  .hero-vials.hv-solo,
  .hv-solo .hv1 { height: 240px; }
}
/* ==========================================================================
   80-pay.css — manual payment panel (Zelle, crypto)
   Rendered by inc/payment-gateways.php on the order-received screen.

   The email version of this panel does NOT use these rules. WooCommerce
   inlines only its own email stylesheet, so the email carries its own
   inline styles in render_email(). Changing a colour here changes the site
   and not the email; the two are matched by hand on purpose, because there
   is no way to share them.
   ========================================================================== */

.pay-panel {
  margin: 24px 0 0;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}

.pay-lede {
  margin: 0 0 16px;
  font-size: 1.0625rem;
  font-weight: 650;
  line-height: 1.35;
  color: var(--ink);
  letter-spacing: -0.01em;
}

/* Single column by default. minmax(0, …) rather than a bare 1fr or an
   implicit auto track: both floor at min-content, and the wallet address is
   a 42-character unbroken string that would push the panel wider than the
   phone. */
.pay-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

.pay-body > * {
  min-width: 0;
}

.pay-rows {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pay-rows li {
  display: flex;
  gap: 16px;
  align-items: baseline;
  justify-content: space-between;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}

.pay-rows li:first-child {
  padding-top: 0;
}

.pay-rows li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.pay-k {
  flex: none;
  font-size: 0.8125rem;
  color: var(--muted);
}

.pay-v {
  min-width: 0;
  font-size: 0.9375rem;
  font-weight: 650;
  color: var(--ink);
  text-align: right;
  overflow-wrap: anywhere;
}

/* The amount and the reference. These are the two things a buyer retypes
   into their banking app, so they get size rather than colour: a tinted
   row reads as a promotion, and this is an instruction. */
.pay-rows li.is-key .pay-v {
  font-family: var(--font-num);
  font-size: 1.125rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* The wallet address, full width and monospace. A proportional font makes
   0/O and 1/l indistinguishable, and there is no undo on a mistyped one. */
.pay-block-k {
  margin: 16px 0 6px;
  font-size: 0.8125rem;
  color: var(--muted);
}

.pay-block-v {
  margin: 0;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--ink);
  overflow-wrap: anywhere;
  /* Long-press to copy on mobile, double-click to select the whole thing on
     desktop. Neither works if the text is inside a flex row that wraps it
     mid-token, which is why this is its own block. */
  user-select: all;
}

.pay-qr {
  margin: 0;
  text-align: center;
}

.pay-qr img {
  display: block;
  width: 180px;
  height: 180px;
  max-width: 100%;
  margin: 0 auto;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  /* Always white, never var(--panel). A QR needs light modules under dark
     ones to scan, and a tinted panel colour is not a guarantee of that. */
  background: #fff;
}

.pay-qr figcaption {
  margin-top: 8px;
  font-size: 0.75rem;
  color: var(--muted);
}

.pay-note {
  margin: 16px 0 0;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-2);
}

.pay-note b {
  font-weight: 650;
  color: var(--ink);
}

.pay-note.is-quiet {
  margin-top: 8px;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* The QR moves alongside once there is room for it without squeezing the
   rows. 200px of QR plus a 24px gap needs roughly 520px of panel before the
   amount and the label stop fitting on one line together. */
@media (min-width: 560px) {
  .pay-panel {
    padding: 26px 28px;
  }

  .pay-body {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 28px;
  }

  .pay-qr img {
    width: 200px;
    height: 200px;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .pay-panel {
    animation: pay-panel-in var(--dur, 0.32s) var(--ease-out, ease-out) both;
  }
}

@keyframes pay-panel-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* The amount and the reference stop sharing a face.
   A price is read once and wants the display serif; a reference is retyped
   into a banking app and wants open counters, so 0 and O cannot trade
   places. Both are still larger than the rows around them, because size is
   what marks them as the two things to act on. */
.pay-rows li.is-money .pay-v {
  font-family: var(--font-display);
  font-size: 1.3rem;
  letter-spacing: -0.02em;
}

.pay-rows li.is-code .pay-v {
  font-family: var(--font-num);
  font-size: 1.125rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
}

/* Tracking on the buyer's order screen. Sits above the order table, because
   once a parcel is moving it is the only thing on that page anyone opens it
   for. */
.hl-tracking {
  margin: 0 0 24px;
  padding: 20px 22px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--accent-tint);
}

.hl-tracking-k {
  margin: 0 0 6px;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}

.hl-tracking-v {
  margin: 0;
  font-family: var(--font-num);
  font-size: 1.125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.hl-tracking-cta {
  margin: 14px 0 0;
}

/* ============================================================
   Email verification, before an order is shown
   woocommerce/checkout/form-verify-email.php

   WooCommerce puts this between a buyer and their receipt whenever
   it cannot identify them, which for a guest store is nearly every
   visit: the link arrives by email and gets opened later. It used to
   render as unstyled plugin markup at the end of a funnel where
   every other screen had been rebuilt, so it reads in the receipt's
   own language instead. Layout comes from .ty and .ty-head; only the
   gold mark and the form below the head are new.
   ============================================================ */

/* Not the green success disc. Nothing has succeeded yet, and a tick
   above a form asking for something is a mixed signal. Gold is the
   same "we need you" register the payment panel uses. */
.ty-mark.is-lock {
  background: var(--accent-tint);
  box-shadow: inset 0 0 0 1px var(--accent-line), 0 10px 26px -14px rgba(161, 98, 7, .45);
  color: var(--accent);
}

.ty-verify-form {
  max-width: 420px;
  margin-inline: auto;
  padding: 0;
  background: none;
  border: 0;
}

.ty-verify-actions { margin: 4px 0 0; }
.ty-verify-actions .btn {
  width: 100%;
  min-height: 52px;
  justify-content: center;
}

.ty-verify-help {
  margin: 16px 0 0;
  text-align: center;
  font-size: .875rem;
  line-height: 1.6;
  color: var(--muted);
}
.ty-verify-help a { color: var(--ink); font-weight: 600; }

/* The error WooCommerce prints on a failed attempt sits inside the
   form, where the plugin's own notice width would break the 420
   column. */
.ty-verify-form .woocommerce-error {
  margin: 0 0 16px;
  width: 100%;
}
/* ==========================================================================
   85-contact.css — the contact form
   Rendered by inc/contact.php via [halotir_contact_form].
   ========================================================================== */

.cf {
  margin: 24px 0 0;
  max-width: 640px;
}

/* The honeypot. Off-screen rather than display:none, because a share of
   bots skip anything that is display:none or hidden specifically to dodge
   this trick, and fill in what they can see in the DOM. aria-hidden and
   tabindex="-1" on the markup keep it away from screen readers and the tab
   order, so it is invisible to everyone it should be invisible to. */
.cf-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.cf-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}

.cf-field {
  margin: 0 0 16px;
  min-width: 0;
}

.cf-field label {
  display: block;
  margin: 0 0 6px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}

.cf-opt {
  margin-left: 4px;
  font-weight: 400;
  color: var(--muted);
}

.cf-field input,
.cf-field select,
.cf-field textarea {
  display: block;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  font: inherit;
  /* 16px minimum. Anything smaller makes iOS Safari zoom the viewport on
     focus, and it does not zoom back out, so the rest of the form is then
     being filled in on a page the user has to pinch to read. */
  font-size: 1rem;
  line-height: 1.4;
  color: var(--ink);
  -webkit-appearance: none;
  appearance: none;
  transition: border-color var(--dur-fast, 0.15s) var(--ease, ease),
              box-shadow var(--dur-fast, 0.15s) var(--ease, ease);
}

.cf-field textarea {
  min-height: 132px;
  resize: vertical;
}

/* The select loses its native arrow to -webkit-appearance: none, so it is
   drawn back on. Inline SVG as a data URI, since a strict CSP and an
   external asset request are both avoidable here. */
.cf-field select {
  padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
}

.cf-field input:focus,
.cf-field select:focus,
.cf-field textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.cf-field input::placeholder,
.cf-field textarea::placeholder {
  color: var(--muted);
}

.cf button[type="submit"] {
  margin-top: 4px;
}

.cf button[type="submit"]:disabled {
  opacity: 0.55;
  cursor: default;
}

/* The status line. Reserves no height when empty, so the form does not
   shift the moment it appears; it is the last element before the fine
   print, so growing downward pushes nothing anyone is reading. */
.cf-msg {
  margin: 12px 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
}

.cf-msg:empty {
  margin: 0;
}

.cf-msg.is-ok {
  color: var(--success);
  font-weight: 600;
}

.cf-msg.is-err {
  color: #b91c1c;
  font-weight: 600;
}

.cf-fine {
  margin: 12px 0 0;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--muted);
}

/* Name and email pair up once there is room for two fields without either
   dropping below a comfortable typing width. */
@media (min-width: 560px) {
  .cf-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0 16px;
  }
}
/* ============================================================
   HOME · hero, rebuilt for the phone
   ------------------------------------------------------------
   The v6 hero was designed at 1280 and inherited down. At 375 it
   measured 834px, which is a full screen and a half before a
   single product is visible, and it spent that budget in the
   wrong order: three lines of headline, four lines of lede, two
   buttons, a chip row clipped mid-word at the right edge, and
   only then the bottles.

   The reference does the opposite and is 575px. Product first,
   then a centred headline, one sentence, one button. You know
   what is being sold before you have read anything.

   So on a phone this reorders rather than redesigns: the same
   markup, the collage pulled to the top, the copy centred under
   it, the chip row dropped because the marquee immediately below
   already carries all six of those facts and carries them
   without clipping. Desktop keeps the two-column split, where
   there is room for all of it and the ordering never applied.
   ============================================================ */

@media (max-width: 767px) {
  .hero-v6 {
    /* 34/44 was tuned against a headline that led. Now that the
       bottles lead, the top pad is doing nothing but holding the
       product away from the promo bar. */
    padding: 8px 0 28px;
  }

  .hero-inner {
    gap: 0;
    padding-block: 0 8px;
    text-align: center;
  }

  /* The whole reorder, in one line. Grid children take `order`, so
     nothing about the source markup has to move and desktop is
     untouched. */
  .hero-collage { order: -1; }

  .hero-v6 h1 {
    font-size: 2.35rem;
    line-height: 1.04;
    letter-spacing: -0.032em;
    /* Two lines at this size on a 375 screen. Three lines was the
       single biggest block of the old 834px. */
    text-wrap: balance;
  }

  .hero-v6 .lede {
    margin: 12px auto 0;
    /* 44ch is a desktop measure. At 375 it never binds, and the
       paragraph ran the full gutter-to-gutter width at four lines.
       Pulling it in drops it to three and gives the centred block
       an axis. */
    max-width: 34ch;
    font-size: 1rem;
    line-height: 1.55;
  }

  .hero-ctas-v6 {
    display: grid;
    justify-items: center;
    gap: 4px;
    margin-top: 18px;
  }

  /* One decision, full width, thumb height. Two 50% buttons side by
     side at 375 give each about 150px, which is where "Shop all
     compounds" starts wrapping to two lines inside the pill. */
  .hero-ctas-v6 .btn-primary {
    width: 100%;
    min-height: 52px;
  }

  /* The second button becomes a link.

     Two stacked full-width pills cost 114px and, worse, read as two
     equal choices. They are not: almost everyone wants the catalogue,
     and Build a box is the thing you choose once you already know
     what is in it. Demoting it recovers the height and gives the hero
     a single obvious action, which is what makes the reference feel
     quick to move through.

     Still a 48px target, just without the outline. */
  .hero-ctas-v6 .btn-outline {
    width: auto;
    min-height: 48px;
    padding-inline: 12px;
    border: 0;
    background: none;
    color: var(--ink-2);
    font-size: 0.9375rem;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-color: var(--accent-line);
  }

  /* Dropped, not hidden-and-still-laid-out. These three chips were a
     horizontally scrollable row that nobody scrolls, so the third was
     permanently half visible at the right edge and read as a
     rendering fault. Every claim they made survives in the proof
     marquee directly below, which now actually moves through all six.
     display:none rather than a markup change so desktop, where they
     wrap onto one tidy line, keeps them. */
  .hero-trust { display: none; }
}

/* ============================================================
   HOME · the bottle collage
   ------------------------------------------------------------
   Four bottles at 210/156/132/116 with a 94px spread of scale
   between the largest and smallest, sitting at four similar
   vertical positions inside a 300px band. Rendered, that reads
   as a row of similar objects rather than a group with depth,
   which is the "too close together, not spread out" complaint.

   Depth comes from three things this had none of: a real size
   ratio between front and back, overlap so the front bottle
   occludes one behind it, and bottles that leave the frame. A
   cluster that fits neatly inside its box looks placed. One that
   runs off the edge looks photographed.
   ============================================================ */

@media (max-width: 767px) {
  .hero-collage {
    height: 268px;
    margin-bottom: 4px;
    /* The bottles are allowed past the left and right edges now, and
       .hero-v6 already clips, so the bleed resolves against the page
       rather than against this box. */
    overflow: visible;
  }

  /* Front bottle. Nearly 2.5x the smallest now rather than 1.8x, and
     pulled off centre so the group has a direction. */
  .hero-vial.v1 {
    height: 228px;
    left: 50%;
    margin-left: -74px;
    top: 8px;
    z-index: 4;
    transform: rotate(-8deg);
  }

  /* Mid left, overlapped by the front bottle's shoulder. The overlap
     is the depth cue; without it these are two bottles on a shelf. */
  .hero-vial.v2 {
    height: 150px;
    left: -18px;
    top: 74px;
    z-index: 3;
    transform: rotate(12deg);
  }

  /* Back right, small and high, a third of it past the edge. */
  .hero-vial.v3 {
    height: 116px;
    right: -26px;
    top: 26px;
    z-index: 2;
    transform: rotate(-16deg);
  }

  /* Smallest, lowest, furthest back. Sits in the gap between the
     front bottle's base and the right edge so the group reads as a
     triangle rather than a line. */
  .hero-vial.v4 {
    height: 94px;
    right: 34px;
    bottom: -6px;
    z-index: 1;
    transform: rotate(7deg);
    opacity: 0.92;
  }

  /* Scaled to the group rather than to the box. At 300px on a 268px
     band the halo was wider than the bottles and washed the whole
     area to a flat tint, which is what makes the cluster look like it
     is sitting on a panel instead of floating. */
  .hero-halo {
    width: 232px;
    height: 232px;
    top: 46%;
  }
}

/* ------------------------------------------------------------
   The tilts, on the image rather than the slot
   ------------------------------------------------------------
   Every bottle on this page has been standing perfectly upright,
   including before this pass, and the `transform: rotate()` in
   the base stylesheet has never once applied. The slots carry
   h-float / h-float-slow / h-float-deep / h-drift, all of which
   animate `transform`, and a running animation beats a plain
   declaration on the same property outright. The rotate was
   being discarded the moment the first frame played.

   70-vial-photo.css documents this exact trap and solves it for
   the .hv slots by baking the rotation into each keyframe. That
   fix does not scale here, because it would mean four more
   keyframe blocks that have to be kept in sync with four
   positions.

   The image is the better place: the slot owns motion, the image
   owns orientation, and the two compose because they are
   different elements. Nothing animates the image, so nothing can
   discard this.
   ------------------------------------------------------------ */
.hero-vial.v1 .vial { transform: rotate(-8deg); }
.hero-vial.v2 .vial { transform: rotate(12deg); }
.hero-vial.v3 .vial { transform: rotate(-15deg); }
.hero-vial.v4 .vial { transform: rotate(8deg); }

/* ------------------------------------------------------------
   Hold the float until the glass is actually there
   ------------------------------------------------------------
   The white rectangles that appear behind these bottles for the
   first second of every cold load are not a background and not
   the image's alpha. Both are clean: the corners of all four
   cutouts read rgba(0,0,0,0) and nothing in the cascade paints
   a box.

   They are compositor layers. A transform animation promotes
   each slot to its own layer at first paint, and a promoted
   layer that has nothing to raster yet is filled with the
   nearest opaque ancestor colour, which on this page is the
   white on body. Against the hero's cream gradient that reads as
   a hard-edged white card, axis-aligned, exactly the size of the
   bottle's bounding box. Once the decode lands the layer rasters
   properly and it never comes back, which is why it only ever
   shows up "when you load the page in".

   This is the whole fix now: no animation, so no layer, until
   every bottle in the group has decoded. Two frames of margin
   after that so the raster has landed before the promotion
   happens.

   It used to be paired with decoding="sync" in the markup. That
   half is gone, and good riddance: it only ever worked if the
   browser honoured the hint, it asked for a main-thread decode at
   the exact moment these animations wanted that thread, and it
   travelled with a fetchpriority="low" that put the hero imagery
   behind every script on the page. This rule needs none of that.
   It holds regardless, including on a slow connection where the
   images arrive long after first paint, because it keys off the
   decode actually completing rather than off a request that it
   should.

   Falls back to still bottles with no script, which is a fine
   thing to fall back to. ------------------------------------ */
.hero-collage:not(.is-lit) .hero-vial,
.hero-vials:not(.is-lit) .hv { animation: none; }

/* The home collage keeps its tilt through this, because that tilt lives
   on the image and only the slot is animated. The .hv furniture does not:
   70-vial-photo.css bakes its rotations into the keyframes, so switching
   the animation off stands those three bottles up straight and lighting
   them tips them over. Restating the same angles here means the only
   thing that changes at .is-lit is that they start moving. */
.hero-vials:not(.is-lit) .hv1 { transform: rotate(-8deg); }
.hero-vials:not(.is-lit) .hv2 { transform: rotate(11deg); }
.hero-vials:not(.is-lit) .hv3 { transform: rotate(-17deg); }

/* ------------------------------------------------------------
   Depth, at every width
   ------------------------------------------------------------
   One shadow for four bottles at four distances is what flattens
   the group. Near objects cast a tighter, darker shadow; far
   ones a longer, weaker one. Overriding per slot costs four
   rules and does more for the sense of depth than the positions
   do.

   drop-shadow, never box-shadow: these are transparent cutouts,
   and box-shadow would trace the element box and paint exactly
   the rectangle in mid-air that this whole pass is removing.
   ------------------------------------------------------------ */
.hero-vial.v1 .vial { filter: drop-shadow(0 18px 26px rgba(28, 25, 23, 0.26)); }
.hero-vial.v2 .vial { filter: drop-shadow(0 14px 24px rgba(28, 25, 23, 0.2)); }
.hero-vial.v3 .vial { filter: drop-shadow(0 12px 24px rgba(28, 25, 23, 0.16)); }
.hero-vial.v4 .vial { filter: drop-shadow(0 10px 22px rgba(28, 25, 23, 0.13)); }

/* The slot carried its own shadow as well as the image, so every
   bottle was drawing two. Stacked drop-shadows on a soft-edged alpha
   double the density at the silhouette and read as a grey halo
   hugging the glass. One shadow, on the image, where the alpha is. */
.hero-vial { filter: none; }

/* ------------------------------------------------------------
   Desktop: the same depth idea, more room to spend on it
   ------------------------------------------------------------ */
@media (min-width: 1024px) {
  .hero-vial.v1 { height: 320px; margin-left: -96px; }
  .hero-vial.v2 { height: 206px; }
  .hero-vial.v3 { height: 168px; }
  .hero-vial.v4 { height: 138px; }
}
/* ============================================================
   The promo bar, rebuilt
   ------------------------------------------------------------
   At 375 this was one squeezed line reading

       20% off sitewide   HALO20   28d 5h

   on a flat brown band about 60px tall, with the eyebrow hidden
   to make room and a decorative "sun" that rendered as a faint
   smudge nobody would name. Every element of a good offer bar
   was present and none of them had room to land.

   The reference runs 93px and two rows: a tracked serif eyebrow
   beside a gold italic offer, then a filled code pill beside an
   outlined countdown, over a deep burgundy gradient with a sun
   sitting on the bottom edge. It reads as an event. Ours read as
   a notice.

   So this spends the extra thirty pixels the same way, with one
   substitution. Their mark is a sun, and a sun on this site
   would be borrowed. The halo over the wordmark is already the
   brand's shape, so the thing rising off the bottom edge here is
   a halo: two concentric gold ellipses in perspective, glowing,
   half below the horizon.
   ============================================================ */

/* ------------------------------------------------------------
   The band
   ------------------------------------------------------------
   Deeper and warmer than the flat 105deg brown, with the light
   coming from the bottom right where the halo is, so the glow
   has somewhere to have come from. Layered rather than replaced:
   the base sheet's gradient stays underneath as the floor
   colour.
   ------------------------------------------------------------ */
.promo-bar {
  background:
    radial-gradient(120% 180% at 88% 118%, rgba(227, 192, 133, .28), rgba(227, 192, 133, 0) 62%),
    radial-gradient(90% 160% at 4% -30%, rgba(140, 62, 44, .34), rgba(140, 62, 44, 0) 64%),
    linear-gradient(105deg, #201811 0%, #3d2a15 46%, #241b12 100%);
}

/* The horizon: one hairline, the top edge of a very wide ellipse
   sitting mostly below the band, so it curves. The reference has
   waves under its sun and the band wants something for the halo
   to rise off, but two lines and a shadow read as a swoosh drawn
   across the countdown. One line at a tenth opacity, low enough
   to clear the pills, reads as a horizon and nothing else. */
.promo-bar::before {
  content: "";
  position: absolute;
  right: -24%;
  bottom: -46px;
  width: 92%;
  height: 74px;
  border-radius: 50%;
  border-top: 1px solid rgba(227, 192, 133, .13);
  pointer-events: none;
}

/* ------------------------------------------------------------
   The halo
   ------------------------------------------------------------
   .promo-sun by name only. Renaming it would drop it out of the
   base sheet's `.promo-bar > *:not(.promo-sun)` exclusion and
   straight back into flow, where it would claim a row of its
   own; that regression is documented at the rule itself and is
   not worth reopening for a class name nobody reads.

   The element is the glow; ::before is the ring. One ring, not
   two: concentric ellipses at this size read as a target, and
   the first pass at two of them plus a horizon line came out as
   scribble behind the countdown pill.

   It sits low enough that the band's own overflow:hidden takes
   the bottom off it, which is what makes it read as rising
   rather than as an oval someone drew. Flattened to about 3:1,
   the same proportion as the halo over the wordmark, because a
   circle here would be a sun and a sun here would be borrowed.
   ------------------------------------------------------------ */
.promo-sun {
  right: -34px;
  bottom: -56px;
  width: 152px;
  height: 152px;
  background: radial-gradient(circle, rgba(248, 229, 188, .62), rgba(230, 197, 141, .2) 38%, rgba(227, 192, 133, 0) 66%);
}

/* The ring is light, so it has to behave like light. It was drawn
   as a 2px near-white stroke at 95%, and composited over the band's
   own brown that lands at about rgb(242, 233, 214): brighter than
   the gold code pill beside it, which comes out around rgb(231,
   196, 133). The brightest object in the bar was the ornament, and
   the second brightest was the one thing in the bar a buyer is
   meant to act on. At that weight it also stops reading as glow
   and starts reading as an ellipse somebody drew, which is the
   note the first draft was already trying to avoid.

   Same shape, same place, same crop. The core comes down to a
   hairline and a little over half opacity, which composites to
   about rgb(167, 152, 124) and puts it a clear step under the
   pill, and the bloom grows to take over the work the hard edge
   was doing. What is left is the halo over the wordmark, at scale,
   rising out of the corner. */
.promo-sun::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 42%;
  width: 96px;
  height: 32px;
  border-radius: 50%;
  border: 1.5px solid rgba(247, 231, 197, .58);
  box-shadow:
    0 0 34px rgba(248, 226, 176, .5),
    inset 0 0 18px rgba(248, 226, 176, .3);
  transform: translateX(-50%) rotate(-8deg);
  pointer-events: none;
}

/* ------------------------------------------------------------
   Rows
   ------------------------------------------------------------ */
.promo-line,
.promo-act {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  margin: 0;
}

.promo-line { gap: 4px 12px; }
.promo-act { gap: 8px 10px; }

/* The hairline between eyebrow and offer, which is what stops
   "End of summer 20% off sitewide" from reading as one run-on
   phrase when they share a line. */
.promo-rule {
  width: 1px;
  height: 14px;
  background: rgba(232, 200, 126, .4);
  flex: none;
}

.promo-code-label {
  font-family: var(--font-num);
  font-size: .625rem;
  font-weight: 500;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: rgba(232, 200, 126, .75);
}

/* Tabular, because the seconds digit now moves. Without it every
   tick renumbers the pill a pixel or two wider or narrower and
   nudges the code pill beside it. */
.promo-timer {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  background: rgba(255, 248, 233, .05);
}

/* ------------------------------------------------------------
   Phone
   ------------------------------------------------------------
   Both older sheets have a say here: the base switches the bar
   to a two-column grid below 640 and hides the eyebrow, and
   60-chrome then forces it back to a nowrap flex row to fit one
   line. Neither applies now that the children are two groups, so
   both are restated rather than left to interact.
   ------------------------------------------------------------ */
@media (max-width: 639px) {
  .promo-bar {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 7px;
    padding-block: 10px;
    padding-inline: 12px;
    min-height: 0;
  }

  /* Back on. It was hidden because at one row it cost the offer,
     the code and the timer the width they needed. At two rows it
     costs nothing and it is the line that gives the bar an
     occasion instead of a percentage. */
  .promo-eyebrow {
    display: inline;
    font-size: .58rem;
    letter-spacing: .3em;
  }

  .promo-offer {
    grid-column: auto;
    font-size: 1.06rem;
    line-height: 1.05;
    white-space: nowrap;
  }

  .promo-rule { height: 12px; }

  /* Hidden here and only here: the code pill is legible as a code
     without being labelled one, and the row needs the width for
     four countdown units. */
  .promo-code-label { display: none; }

  .promo-code {
    flex: none;
    font-size: .6875rem;
    letter-spacing: .1em;
    padding: 5px 12px;
  }

  .promo-timer {
    flex: none;
    font-size: .6875rem;
    letter-spacing: .01em;
    padding: 4px 10px;
  }

  /* Further off the right edge than desktop, proportionally. There
     is no room to reserve a column for it here, so the halo has to
     clear the countdown pill by sitting mostly outside the band
     rather than beside it. */
  .promo-sun {
    width: 146px;
    height: 146px;
    right: -32px;
    bottom: -58px;
  }
  /* 84px wide, centred at about x336 on a 375 band, which starts
     just past where the countdown pill ends. Any wider and the ring
     crosses the pill; any narrower and it stops reading as a shape
     and goes back to being a smudge in the corner. */
  .promo-sun::before {
    width: 84px;
    height: 28px;
    border-width: 1.5px;
  }

  .promo-bar::before { right: -30%; bottom: -40px; height: 66px; }
}

/* One row again once there is room for one, which is the layout
   the desktop bar always wanted: eyebrow, offer, code, timer,
   halo off to the right. */
@media (min-width: 640px) {
  .promo-bar {
    flex-direction: row;
    gap: 10px 22px;
    padding-right: 132px;
  }
}

@media (min-width: 1024px) {
  .promo-bar { padding-right: 168px; }
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------
   The shimmer and the halo glow are decoration and stop. The
   countdown is information and does not: it is driven by
   setTimeout, not by an animation, so it keeps ticking here,
   which is correct. Reduced motion is a request about movement
   on the page, not a request to stop being told when the sale
   ends.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .promo-bar::after,
  .promo-sun,
  .promo-code { animation: none; }
}
/* ============================================================
   The entry gate, as a page
   ------------------------------------------------------------
   This was a bottom sheet: a white card sliding up over the
   blurred, dimmed storefront, with one bottle sitting on a cream
   radial blob about 286x246 behind a 75x158 cutout. Three things
   were wrong with it and only one was cosmetic.

   The blob was the visible one. A radial gradient painted on a
   plain white card stops dead at the card's edges, so it read as
   a grey-cream rectangle behind the glass rather than as a glow.

   The scrim was worse. Blurring the page behind a legal
   affirmation is the exact visual grammar of a cookie banner, and
   a cookie banner is a thing you dismiss without reading. This
   screen is a storefront door, not an interruption of a page
   already in progress.

   And the sheet said nothing about the shop. One bottle on a card
   is a decoration. A dozen running off all four edges is a
   catalogue, which is the thing behind the door and the only
   reason anyone would tick the boxes.

   So: an opaque page in the brand's own ivory, a scattered
   bottle field, wordmark above, card centred, exit below. The
   overlay is still injected client-side rather than served as a
   route, because the storefront is public-cached by LiteSpeed and
   a real /verify redirect would have to be decided server-side
   from a cookie the cache does not vary on. Nothing about that is
   visible from the outside: what renders is a page.
   ============================================================ */

.age-gate {
  /* A plain block scroller, not a flex box with an auto-margined
     child. Centring a flex item with `margin: auto` looks right
     until the card is taller than the window, at which point the
     auto margin resolves against negative free space and pushes
     the top of the card above the scroll origin, where nothing
     can reach it. On a 450px-tall window that ate the wordmark.
     The column below centres with min-height instead, which grows
     rather than overflows. */
  display: block;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0;
  /* Opaque, and the same ivory-to-warm gradient the hero sits on,
     so entering the site is a continuation rather than a cut. No
     backdrop-filter: there is nothing behind this to see through,
     and blurring a full-screen layer costs a GPU pass on the
     slowest frame of the whole session. */
  background:
    radial-gradient(120% 80% at 84% -6%, rgba(253, 246, 233, .95), rgba(253, 246, 233, 0) 58%),
    radial-gradient(90% 60% at 6% 104%, rgba(246, 238, 224, .8), rgba(246, 238, 224, 0) 60%),
    var(--bg, #fdfcfa);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ------------------------------------------------------------
   The bottle field
   ------------------------------------------------------------
   Twelve slots at percentage positions, so the arrangement holds
   its shape from 320 to 1600 instead of collapsing into a corner
   at one end and drifting off screen at the other.

   Four rules make it read as depth rather than as a pattern: no
   two adjacent slots are the same size, every slot is tilted and
   no two by the same amount, four of them cross an edge, and the
   ones behind the card are the large ones so the card occludes
   something rather than floating on empty ground.
   ------------------------------------------------------------ */
/* fixed, not absolute. The gate scrolls on a short window, and an
   absolutely positioned field is sized to the container's padding
   box but still scrolls with its content, so it would slide away
   and leave bare ground under the card. .age-gate is itself fixed
   and makes a stacking context, so this stays inside the overlay
   and stays put. */

/* An explicit height, NOT `inset: 0`, and this is the whole reason
   the field used to shake on a phone.

   Every slot below is placed at a percentage of this box. `inset: 0`
   on a fixed element resolves against the DYNAMIC viewport, which on
   iOS and Android is the one that grows and shrinks by the height of
   the browser's own URL bar as it collapses and returns. The gate is
   a scroller (`overflow-y: auto` above), so dragging it is exactly
   what triggers that transition, and every percentage in the field
   recomputed against a box that was changing under it. Measured at
   390x844: a 60px viewport change moved nine of the twelve bottles,
   the lowest of them by 62px, continuously, while the finger was
   still on the glass. A dozen bottles sliding different distances at
   once does not read as parallax, it reads as the page having lost
   its grip.

   `vh` is the large-viewport unit and does not track the URL bar, so
   either declaration below is stable; `svh` is preferred where it is
   understood because it matches the state the gate is FIRST seen in,
   URL bar showing, page not yet scrolled. That is the composition
   worth being exact about. When the bar then collapses, a band of
   bare ivory opens at the bottom and nothing in the field moves by a
   pixel, which is the trade being made on purpose: the arrival frame
   is the one a visitor judges, and stillness beats coverage.

   If a slot is ever re-anchored, keep it a percentage of THIS box.
   Reaching for `dvh` here would reintroduce the bug exactly. */
.gate-scatter {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.gs {
  position: absolute;
  display: block;
}

.gs .vial {
  display: block;
  width: 100%;
  height: auto;
  /* drop-shadow, never box-shadow: these are transparent cutouts
     and box-shadow traces the element box, which would paint a
     rectangle in mid-air around every one of them. */
  filter: drop-shadow(0 16px 28px rgba(83, 56, 12, .16));
}

/* Held still until every bottle has decoded. Twelve animated slots
   promote twelve compositor layers at once, and a promoted layer
   with nothing rastered in it is filled with the page background,
   which here would be twelve pale rectangles as the entire first
   impression. main.js adds .is-lit once decode() has settled. */
.gate-scatter:not(.is-lit) .gs { animation: none; }

/* Widths, because the cutouts are 122x260 and a height would have
   to be restated every time that ratio changes. These land the
   bottles between 77 and 136px tall on a phone, which is the size
   at which a dozen of them still read as individual objects. The
   first pass ran 117 to 216 and the field stopped being a
   background: at that scale the bottles compete with the card for
   the screen instead of sitting behind it. */
.gs1  { top: -3%;  left: 4%;   width: 48px; animation: h-float 6s ease-in-out infinite; }
.gs2  { top: 5%;   left: 62%;  width: 36px; animation: h-drift 9s ease-in-out .4s infinite; }
.gs3  { top: 1%;   right: -5%; width: 58px; animation: h-float-slow 7.5s ease-in-out .9s infinite; }
.gs4  { top: 17%;  left: -7%;  width: 42px; animation: h-float-deep 8s ease-in-out .2s infinite; }
.gs5  { top: 24%;  left: 33%;  width: 64px; animation: h-float-slow 6.5s ease-in-out 1.3s infinite; }
.gs6  { top: 33%;  right: 2%;  width: 38px; animation: h-drift 10s ease-in-out .7s infinite; }
.gs7  { top: 46%;  left: 6%;   width: 55px; animation: h-float 7s ease-in-out 1.1s infinite; }
.gs8  { top: 55%;  right: -6%; width: 60px; animation: h-float-deep 8.5s ease-in-out .5s infinite; }
.gs9  { top: 68%;  left: 40%;  width: 37px; animation: h-drift 11s ease-in-out 1.6s infinite; }
.gs10 { bottom: 4%; left: -4%; width: 51px; animation: h-float-slow 7s ease-in-out .3s infinite; }
.gs11 { bottom: -4%; left: 52%; width: 44px; animation: h-float 8s ease-in-out 1.4s infinite; }
.gs12 { bottom: 12%; right: 6%; width: 35px; animation: h-drift 9.5s ease-in-out 1s infinite; }

/* Tilt on the image, never on the slot. The slot is running a
   transform animation and a running animation beats a plain
   declaration on the same property outright, so a rotate() here
   would be discarded on the first frame and every bottle would
   stand up straight. The image is animated by nothing, so the two
   compose. Same trap, same fix, as the home collage. */
.gs1  .vial { transform: rotate(-9deg); }
.gs2  .vial { transform: rotate(14deg); }
.gs3  .vial { transform: rotate(-17deg); }
.gs4  .vial { transform: rotate(8deg); }
.gs5  .vial { transform: rotate(-6deg); }
.gs6  .vial { transform: rotate(19deg); }
.gs7  .vial { transform: rotate(-13deg); }
.gs8  .vial { transform: rotate(7deg); }
.gs9  .vial { transform: rotate(-21deg); }
.gs10 .vial { transform: rotate(11deg); }
.gs11 .vial { transform: rotate(-5deg); }
.gs12 .vial { transform: rotate(16deg); }

/* Depth by distance. The near bottles are opaque and sharply
   shadowed; the far ones lose contrast, which is what stops
   twelve identical cutouts from reading as a repeated sticker. */
.gs2, .gs6, .gs9, .gs12 { opacity: .5; }
.gs4, .gs11 { opacity: .72; }
.gs2 .vial, .gs6 .vial, .gs9 .vial, .gs12 .vial {
  filter: drop-shadow(0 10px 20px rgba(83, 56, 12, .1));
}

/* ------------------------------------------------------------
   Wordmark, card, exit
   ------------------------------------------------------------ */
/* Fills the window and centres its own contents, so a card that
   outgrows the viewport simply makes the column taller and the
   overlay scrolls to it. The width limit moves to the three
   children, because the column itself is now full width. */
.gate-stage {
  position: relative;
  z-index: 1;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 26px 16px calc(24px + env(safe-area-inset-bottom, 0px));
}

.gate-mark,
.gate-card,
.gate-exit {
  width: 100%;
  max-width: 452px;
}

.gate-mark {
  display: flex;
  justify-content: center;
  margin-bottom: 18px;
}
.gate-mark .logo-img {
  height: 40px;
  width: 132px;
}

.gate-card {
  width: 100%;
  max-width: none;
  max-height: none;
  overflow: visible;
  /* All four corners. It was a bottom sheet, so it had two. */
  border-radius: var(--radius-lg);
  border: 1px solid rgba(28, 25, 23, .06);
  /* Wide and soft rather than tight and dark: the card is lifted
     off a light ground here, not pinned to the bottom edge of a
     dimmed one. */
  box-shadow:
    0 1px 2px rgba(28, 25, 23, .04),
    0 18px 50px rgba(28, 25, 23, .1);
  animation: gate-card-in 380ms var(--ease-out) both;
}
@keyframes gate-card-in {
  from { transform: translateY(12px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

.gate-body {
  padding: 26px 22px 22px;
}

.gate-card h2 {
  text-align: left;
  font-size: 1.6rem;
  line-height: 1.12;
  letter-spacing: -.02em;
  margin-bottom: 10px;
}

.gate-sub {
  text-align: left;
  font-size: .9375rem;
  line-height: 1.55;
  margin-bottom: 18px;
}

.gate-card .btn-block {
  margin-top: 14px;
  min-height: 54px;
}

.gate-fine {
  text-align: left;
  margin-top: 16px;
}

/* Out of the card and under it, which is where a way out belongs:
   inside, between the button and the fine print, it read as a
   second option of equal weight to entering. */
.gate-exit {
  display: block;
  width: 100%;
  min-height: 48px;
  margin: 14px 0 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-family: var(--font);
  font-size: .875rem;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.gate-exit b {
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ------------------------------------------------------------
   Small phones
   ------------------------------------------------------------
   The field thins out rather than shrinking uniformly. Twelve
   bottles on a 320px screen is a texture, not a scatter, and the
   ones that go are the small faint ones whose only job was depth
   they no longer have room to describe.
   ------------------------------------------------------------ */
/* Both of these sit dead centre, and on a phone the card runs
   gutter to gutter, so both are behind it with not a pixel showing.
   They exist to be occluded at tablet and up, where the card stops
   filling the width and a bottle disappearing behind its edge is
   the cue that says the field has depth. Here that cue has nothing
   to read, so they are two decodes and two compositor layers
   painting nothing. */
@media (max-width: 767px) {
  .gs5,
  .gs9 { display: none; }
}

@media (max-height: 720px), (max-width: 359px) {
  .gs2, .gs9, .gs12 { display: none; }
  .gate-mark { margin-bottom: 14px; }
  .gate-mark .logo-img { height: 34px; width: 112px; }
  .gate-body { padding: 22px 18px 18px; }
  .gate-card h2 { font-size: 1.42rem; }
  .gate-sub { font-size: .875rem; margin-bottom: 14px; }
}

/* ------------------------------------------------------------
   Tablet and up
   ------------------------------------------------------------ */
@media (min-width: 768px) {
  .gate-stage { padding-block: 40px; }
  .gate-mark,
  .gate-card,
  .gate-exit { max-width: 496px; }
  .gate-mark .logo-img { height: 46px; width: 150px; }
  .gate-body { padding: 34px 32px 28px; }
  .gate-card h2 { font-size: 1.85rem; }
  .gate-sub { font-size: 1rem; }

  /* About 1.7x the phone widths. There is far more open ground
     here, so the field can carry more of it without crowding a
     card that no longer fills the screen. */
  .gs1  { width: 82px; }
  .gs2  { width: 62px; }
  .gs3  { width: 96px; }
  .gs4  { width: 70px; }
  .gs5  { width: 106px; top: 20%; left: 24%; }
  .gs6  { width: 66px; }
  .gs7  { width: 91px; }
  .gs8  { width: 102px; }
  .gs9  { width: 63px; }
  .gs10 { width: 87px; }
  .gs11 { width: 74px; }
  .gs12 { width: 59px; }
}

@media (min-width: 1200px) {
  /* Pulled off the middle at desktop, where the card no longer
     fills the width and the centre column would otherwise be a
     stack of bottles behind a form. */
  .gs2  { left: 70%; }
  .gs5  { left: 16%; }
  .gs9  { left: 74%; }
  .gs11 { left: 26%; }
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .gs { animation: none; }
  .gate-card { animation: none; }
}
/* ============================================================
   PDP · the first screen
   ------------------------------------------------------------
   Tapping a card on the home page landed on this:

     header            62px
     promo bar         91px   dark brown
     research banner   58px   dark brown, uppercase, three lines
     ---------------------------------------------------------
                      211px of chrome before the product

   The two brown bands touch, so they read as one 149px slab with
   a gold hairline through the middle, and the second one is set
   in shouting caps. That is the "opens up to a page that doesn't
   even look right" complaint, and none of it is about the product
   page itself: the page below is fine, it just could not be seen.

   The reference solves this by not having the second band. We
   cannot: the research-use line is the one compliance statement
   that has to sit next to the price rather than in a footer.

   So it stays and stops shouting. Sentence case on a warm ivory
   ground with a hairline under it, which reads as the legal note
   it is instead of a hazard strip, and gives the eye one dark
   band rather than two.
   ============================================================ */

.ruo-banner {
  background: linear-gradient(180deg, #fdf8ee, #fbf6ee);
  border-bottom: 1px solid rgba(161, 98, 7, .16);
  color: var(--ink-2);
  /* The caps came from the v1 sheet and survived four redesigns.
     Forty characters of uppercase at .69rem is slower to read
     than the same sentence in sentence case, which is a strange
     thing to do to the one line on the page that is there to be
     understood. */
  text-transform: none;
  letter-spacing: 0;
  font-size: .75rem;
  line-height: 1.45;
  padding-block: 9px;
}

.ruo-banner strong {
  color: var(--accent-deep);
  font-weight: 600;
  letter-spacing: 0;
}

@media (max-width: 639px) {
  .ruo-banner {
    font-size: .6875rem;
    line-height: 1.4;
    letter-spacing: 0;
    text-align: center;
    /* Released from --strip-h. That floor exists so three dark bands
       under the header measure the same and do not look ragged
       stacked on each other. This one is no longer dark and no
       longer stacks against anything, so holding it at 58px just
       pads two lines of small print with 20px of nothing and pushes
       the bottle further down the screen. */
    min-height: 0;
  }
}

/* ------------------------------------------------------------
   The sticky buy bar
   ------------------------------------------------------------
   The second row holds the mass pills and the Add button, with
   Add at flex: 0 0 auto so the pills get the width. On a
   single-mass SKU the pills are not rendered at all, so Add was
   a 96px pill sitting alone at the left of a 375px bar with 260
   pixels of empty white beside it, under a row that was already
   full. The bar is 119px tall; half of it was blank.

   :only-child rather than a modifier class, because "is there
   anything else on this row" is exactly the condition, and the
   markup already answers it.
   ------------------------------------------------------------ */
.pdp-bar-row > .pdp-bar-add:only-child {
  flex: 1 1 auto;
}

/* ------------------------------------------------------------
   The title block
   ------------------------------------------------------------
   Eyebrow, name, identifiers, synonyms, tagline: five things,
   and on some SKUs three of them were the same string. The
   duplicate chip is gone in PHP. What is left here is the
   spacing, which was tuned when the tagline was wrapped in two
   stray empty paragraphs and so had two paragraphs of margin
   doing the work a rule should have been doing.
   ------------------------------------------------------------ */
.pdp-tagline {
  margin-top: 12px;
}
.pdp-tagline p {
  margin: 0;
}
.pdp-tagline p + p {
  margin-top: 8px;
}

@media (max-width: 767px) {
  .pdp-chips { margin-top: 12px; }
  .pdp-syn { margin-top: 8px; }
  .pdp-tagline { margin-top: 10px; }
}
/* ============================================================
   The rest of the payment panel
   ------------------------------------------------------------
   80-pay.css styles the instructions: where to send, how much,
   what memo. That is only half of a hand-settled payment. The
   other half is the buyer telling us they sent it, because
   nothing here watches a bank feed or a chain, and an order
   sits on-hold until a human is told to go and look.

   Three things live here, in the order the buyer needs them:

     .pay-hold      how long the order is held
     .pay-confirm   the one action left to take
     .pay-alts      the other method, if this one is awkward

   Deliberately after the notes rather than before. The notes
   carry the warnings that stop money going to the wrong place,
   and nothing should sit between those and the address.
   ============================================================ */

.pay-hold {
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--muted);
}

.pay-hold b {
  color: var(--ink);
  font-weight: 650;
}

/* ------------------------------------------------------------
   The confirmation step
   ------------------------------------------------------------
   Given its own ground rather than another paragraph in the
   stack. By this point the panel is six blocks of text and a QR
   code, and the single thing the buyer still has to do was
   reading as the seventh note nobody finishes.
   ------------------------------------------------------------ */
.pay-confirm {
  margin: 18px 0 0;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}

.pay-confirm-h {
  margin: 0 0 6px;
  font-size: 0.9375rem;
  font-weight: 650;
  line-height: 1.3;
  color: var(--ink);
  letter-spacing: -0.005em;
}

.pay-confirm-p {
  margin: 0 0 14px;
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--ink-2);
}

/* Full width on a phone. The label is five words and centring it in a
   375px panel leaves a tap target that reads as secondary, which is the
   opposite of what it is. */
.pay-confirm-btn {
  display: block;
  width: 100%;
  text-align: center;
}

.pay-confirm-alt {
  margin: 12px 0 0;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--muted);
}

.pay-confirm-alt a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  /* An email address is one unbroken token and will not wrap on its own,
     so on a 320px screen it pushes the panel sideways. */
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------
   The other way to pay
   ------------------------------------------------------------
   Quieter than everything above it. A buyer who is mid-transfer
   should not be given a second decision at the same weight as
   the instructions they are following.
   ------------------------------------------------------------ */
.pay-alts {
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.pay-alts-h {
  margin: 0 0 8px;
  font-size: 0.875rem;
  font-weight: 650;
  line-height: 1.3;
  color: var(--ink);
}

.pay-alts-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pay-alts-list li {
  margin: 0 0 8px;
  font-size: 0.8125rem;
  line-height: 1.65;
  color: var(--ink-2);
}

.pay-alts-list b {
  color: var(--ink);
  font-weight: 650;
}

/* The wallet address arrives here as a <code>, so it gets the same
   treatment as .pay-block-v without the panel around it: same monospace,
   same anywhere-wrap, same select-all for a long-press copy. */
.pay-alts-list code {
  display: inline-block;
  padding: 2px 6px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--panel);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--ink);
  overflow-wrap: anywhere;
  user-select: all;
}

.pay-alts-note {
  margin: 10px 0 0;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--muted);
}

@media (min-width: 560px) {
  .pay-confirm {
    padding: 20px 22px;
  }

  /* Once there is room, the button stops being a full-width bar and
     goes back to being sized by its label. */
  .pay-confirm-btn {
    display: inline-block;
    width: auto;
    min-width: 240px;
  }
}
/* ============================================================
   v8 · the mobile pass
   ------------------------------------------------------------
   Loads last, so it settles arguments the earlier parts started.
   Everything here was found on a real 375px viewport, measured
   rather than eyeballed, and each block records what the
   measurement was so the next person does not re-derive it.

   Scope: the offer surfaces (promo bar, card badges, bundle
   tiers, box builder) and the long-form pages. Nothing here
   changes desktop unless the desktop value was itself the bug.
   ============================================================ */

/* ============================================================
   1 · The product card, three faults
   ------------------------------------------------------------
   Measured on the home rail at 375px:

     elementFromPoint(card centre, +120px) -> IMG.vial
     gap between .rail-price bottom and .qa-btn top -> 0px
     .qa-btn rendered height -> 36px

   So: tapping the bottle did nothing, the price sat on the
   button, and the button was eight pixels under the touch
   floor. All three are the same card and all three are below.
   ============================================================ */

/* ------------------------------------------------------------
   1a · The whole card is the link again
   ------------------------------------------------------------
   .card-link is `position:absolute; inset:0; z-index:1`, and
   .is-cut .vial is `position:absolute; inset:0; z-index:1`.
   Neither .rail-media nor .is-cut establishes a stacking
   context (both are positioned with z-index:auto), so the
   bottle and the link are siblings at the same level in the
   card's context and the later one in source order paints on
   top. The bottle is later. It won, and it is not a link.

   Raising the link one step fixes the tile; the two children
   that are meant to be separately clickable move up with it.
   The badges are decoration and now say so, which also means
   they can never eat a tap near the top of the card.
   ------------------------------------------------------------ */
.rail-card .card-link,
.product-card .card-link { z-index: 2; }

.rail-card .qa-btn,
.rail-card .card-btn,
.product-card .qa-btn,
.product-card .card-btn,
.card-proof .cp-coa { z-index: 3; }

.rail-badge,
.promo-badge { z-index: 4; pointer-events: none; }

/* ------------------------------------------------------------
   1b · Air between the price and the button
   ------------------------------------------------------------
   .rail-price carries `margin-top:auto` on the button below it
   to bottom-align the CTA across a row. When the card has no
   slack left, auto resolves to 0 and the two lock together.
   A floor on the price's own bottom padding survives that,
   because padding is not what auto is distributing.

   14px, not 10: the button's 44px hit area is centred on the
   pill, so anything under 12px puts the price inside the
   button's tap zone even when it looks clear.
   ------------------------------------------------------------ */
.rail-price { padding-bottom: 14px; }
.product-card .card-meta { margin-bottom: 12px; }

/* ------------------------------------------------------------
   1c · A real 44px button
   ------------------------------------------------------------
   The 36px pill with a 44px ::before was a reasonable trick
   when the card was dense, but it spends its overhang on the
   price line above and it reads as a weak control next to the
   black pill on every other surface. On a phone the card has
   the room. Give the button the height, and retire the
   pseudo-element so nothing invisible overlaps the price.
   ------------------------------------------------------------ */
@media (max-width: 1023px) {
  .product-card .qa-btn,
  .product-card .card-btn,
  .rail-card .qa-btn,
  .rail-card .card-btn {
    height: 44px;
    font-size: .875rem;
    font-weight: 600;
  }
  .product-card .qa-btn::before,
  .product-card .card-btn::before,
  .rail-card .qa-btn::before,
  .rail-card .card-btn::before { content: none; }
}

/* ------------------------------------------------------------
   1d · One badge over the image, not two
   ------------------------------------------------------------
   The card carried "Popular" top-left and "20% off" top-right,
   both on the same gold foil, both the same size, both the
   same weight. Two objects of identical rank read as one
   striped lump, and they landed on a bottle whose own label is
   printed in the same gold.

   The fix is hierarchy, not colour-matching. The discount is
   live, time-boxed and actionable, so it keeps the position
   over the image and takes solid ink: it now contrasts with
   all five card tints instead of blending into one of them,
   and it rhymes with the black Add-to-cart pill rather than
   with the label art.

   "Popular" stops being a sticker. In the home rail it is
   deleted outright in front-page.php, because the rail's own
   heading is "Most requested" and a Popular badge on eight of
   eight cards inside it is a badge that says nothing. On the
   shop grid, where it does discriminate, it survives as a
   quiet ink-on-white marker in the card body.
   ------------------------------------------------------------ */
.rail-badge,
.promo-badge {
  top: 12px;
  padding: 5px 11px;
  font-family: var(--font);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
  box-shadow: none;
}
.promo-badge {
  left: auto;
  right: 12px;
  background: var(--ink);
  color: #fff;
}
/* Applied. Sage and a tick, matching every other done-state on
   the site. Kept from the previous pass because it was right. */
.promo-badge.is-on {
  background: var(--tint-sage);
  color: var(--vivid-sage);
  box-shadow: inset 0 0 0 1px rgba(47, 122, 82, .22);
}
/* The featured marker, shop grid only, in the body rather than
   over the art. A dot and a word: it reads as an annotation,
   which is what it is, and it can never be mistaken for the
   offer. */
.card-flag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  font-size: .75rem;
  font-weight: 600;
  color: var(--accent-deep);
  letter-spacing: .01em;
}
.card-flag::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
@media (max-width: 380px) {
  .rail-badge, .promo-badge { padding-inline: 9px; letter-spacing: 0; }
}

/* ------------------------------------------------------------
   1e · The rail card itself
   ------------------------------------------------------------
   233px at 375, which left a 130px peek of the next card. A
   peek is the point of a rail, but at that size the second
   card shows its whole bottle and half its title and reads as
   a broken two-up grid rather than as "there is more this
   way". Narrowing the card widens the peek in relative terms
   while making it unambiguously a peek.
   ------------------------------------------------------------ */
@media (max-width: 767px) {
  .rail > * { flex: 0 0 min(70vw, 258px); }
  .rail-body { padding: 14px 14px 16px; }
}

/* ============================================================
   2 · The promo bar
   ------------------------------------------------------------
   Measured at 375: header 65px, band 91px, so 156px of chrome
   before the hero on an 812px viewport. That is not a disaster
   and the band is not broken, which is why it reads as "fine"
   rather than as wrong. What makes it look unfinished is that
   its second row holds two capsules of near-identical size and
   weight, one filled with a metallic gradient and one
   outlined, and only one of them is a control. The eye reads
   two buttons, tries the second one, and nothing happens.

   So: keep the two rows, keep the halo, and fix the rank.
   The code pill is the only thing here you can do something
   with, so it is the only filled object. The countdown becomes
   text. The gradient goes flat, because a metallic sweep on a
   34px pill is the same trick as the gold foil on the proof
   card rule and using it twice in one viewport spends it.
   ------------------------------------------------------------ */
@media (max-width: 639px) {
  .promo-bar {
    gap: 6px;
    padding-block: 9px;
  }

  /* Row one. The hairline stays, because without it the eyebrow
     and the offer read as one run-on phrase. */
  .promo-eyebrow { font-size: .5625rem; letter-spacing: .26em; opacity: .78; }
  .promo-rule { height: 11px; opacity: .7; }
  .promo-offer { font-size: 1.125rem; }

  /* Row two. One object, not two. */
  .promo-act { gap: 12px; }
  .promo-code {
    background: #e7c485;
    background-image: none;
    color: #241a0c;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .12em;
    padding: 6px 15px;
    box-shadow: 0 1px 0 rgba(255, 255, 255, .35) inset;
  }
  /* The pill is 32px tall, so the hit area is stretched rather
     than the pill grown: nothing sits close enough to it to
     lose a tap to the overhang. */
  .promo-code::after {
    content: "";
    position: absolute;
    left: -8px; right: -8px;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }

  /* Text, not a capsule. A countdown is information; it was
     wearing the costume of a control. */
  .promo-timer {
    background: none;
    border: 0;
    padding: 0;
    min-height: 0;
    font-size: .75rem;
    letter-spacing: .01em;
    color: rgba(247, 236, 214, .78);
  }

  /* Atmosphere, at the level where it stops being an object.
     At full strength the ring reads as an ellipse somebody
     drew behind the countdown; at half it reads as light. */
  .promo-sun { opacity: .55; }
  .promo-bar::before { opacity: .6; }
}

/* ============================================================
   3 · Bundle and save
   ------------------------------------------------------------
   The 2x2 grid is right. Four things on top of it were not.
   ============================================================ */

/* ------------------------------------------------------------
   3a · The ribbons were three unrelated objects
   ------------------------------------------------------------
   Gold, sage and ink, assigned by :nth-child, on three tiles
   sitting inside one 327px block. Three saturated colours in
   a four-tile grid is not a hierarchy, it is a colour wheel,
   and the bound-to-position colouring meant unhiding the 50+
   tier would silently recolour all of them.

   One family: ink, at one weight, for every tier that carries
   a ribbon. The tier that matters is signalled by selection
   state, which is the gold-bordered tint the active card
   already has, and by the discount figure itself. A ribbon's
   job here is to name the tier, not to out-shout the tier next
   to it.
   ------------------------------------------------------------ */
.bundle-badge,
.bundle-card:nth-child(3) .bundle-badge,
.bundle-card:nth-child(4) .bundle-badge {
  background: var(--ink);
  color: #fff;
  font-weight: 600;
  font-size: .6875rem;
  letter-spacing: .02em;
  text-transform: none;
  box-shadow: none;
  /* The ribbon was nowrap and centred with no ceiling, so a
     108px "Most popular" on a 158px card overhung 25px each
     side into the 10px column gap and over its neighbour.
     Clamping it to the card ends that for every string,
     including a translated one. */
  max-width: calc(100% - 16px);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The selected tier is the one that gets colour. It already
   had the gold ring and the gold tint; the ribbon joins it so
   selection reads as one object rather than a card wearing
   somebody else's hat. */
.bundle-card.active .bundle-badge {
  background: var(--accent);
  color: #fff;
}

/* ------------------------------------------------------------
   3b · Prices that line up
   ------------------------------------------------------------
   .bundle-unit is repainted live by pdp.js and was the one
   price treatment on the site left off the tabular-numerals
   list, so four figures in a 2x2 grid agreed on nothing.
   ------------------------------------------------------------ */
.bundle-unit,
.bundle-each { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------
   3c · Four tiles the same height
   ------------------------------------------------------------
   "each · save $195.00" is 19 characters at 11px against
   135px of content box, which wraps on some devices and not
   others. When it wrapped, the row grew, `align-content:
   center` re-centred the neighbour, and two cards that are
   meant to be read as a set had their price baselines a line
   apart. Reserving the second line unconditionally costs 15px
   and buys a grid that is the same on every phone.
   ------------------------------------------------------------ */
.bundle-rows { align-items: stretch; }
.bundle-card { min-height: 142px; }
.bundle-each { min-height: 2.4em; }

/* ------------------------------------------------------------
   3d · The vial stack
   ------------------------------------------------------------
   Four 21px bottles overlapped at -14px is 43px of grey mush
   at the top of a 158px tile, and at that size nobody counts
   them. Three is the point at which the repetition still
   reads as "more than one" and the shapes stay separable.
   ------------------------------------------------------------ */
.bundle-vials { height: 42px; }
.bundle-vials img.vial:nth-child(n + 4),
.bundle-vials .vial:nth-child(n + 4) { display: none; }

.bundle-title { letter-spacing: .01em; }
.bundle-note { color: var(--muted); }

/* Reduced motion missed this one control. */
@media (prefers-reduced-motion: reduce) {
  .bundle-card:active { transform: none; }
}

/* ============================================================
   4 · Build a box
   ------------------------------------------------------------
   The page reads as complicated because of what it asks you to
   pass before you can start, not because the mechanic is hard.
   The mechanic is one tap per compound, four times.

   inc/build-a-box.php now puts the picker directly under the
   headline and moves the four-figure stat strip below it. What
   is left here is the seam that reorder exposes, and the one
   genuine layout fault on the page: the fixed tray.
   ============================================================ */
@media (max-width: 767px) {
  /* The picker follows the page head directly now, so it does
     not need a section's worth of lead-in on top of the head's
     own bottom margin. */
  .box-pick { padding-top: 18px; }
  .box-pick .section-head-v6 { margin-bottom: 14px; }
  /* And the strip, now that it sits between the picker and the
     how-it-works cards, is a divider rather than an opener. */
  .box-stats { margin-top: 8px; }

  /* ----------------------------------------------------------
     The tray label could wrap, and take the fixed bar with it
     ----------------------------------------------------------
     Budget at 375: 327px usable inside the bar's gutters, of
     which four 36px tokens plus their gaps take 164px, the
     Free token about 46px, the row's two gaps 24px, and the
     chevron 20px. That leaves roughly 73px for a label holding
     "Box complete" at 16px over "+1 free Halotir H2O" at 13px.

     The sub-line had nowrap and an ellipsis. The strong above
     it had neither, so it wrapped to two lines, the fixed bar
     grew past the 76px of clearance --bar-h reserves for it,
     and the bar covered the last row of the page.

     Two changes, because either alone still leaves it tight:
     give the label the same ellipsis contract as its sibling,
     and buy back 24px by shrinking the tokens at every phone
     width rather than only below 359px.
     ---------------------------------------------------------- */
  .box-tray-label { min-width: 0; }
  .box-tray-label strong {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: .9375rem;
  }
  .tray-token { width: 30px; height: 30px; font-size: .75rem; }
  .box-tray-tokens { gap: 4px; }
  .tray-free { padding: 0 9px; }
  .box-tray { gap: 10px; }
}

/* The COA chip on a picker tile is a link sitting inside a card
   whose whole surface is a control, so it needs to be above the
   card, and it needs to not run the width of a 158px tile. */
.box-coa { z-index: 3; font-size: .6875rem; }
.box-card .box-coa span { font-weight: 600; }

/* ============================================================
   5 · Long-form pages
   ------------------------------------------------------------
   terms, privacy, shipping, returns, disclaimer, research-use.
   Measured: terms is 8106px tall at 375px, eighteen h2
   sections, sixteen pixel body type at 1.72, and no way to get
   anywhere. Nothing overflows. It is not broken, it is
   unnavigable, which on a phone is the same outcome.
   ============================================================ */

/* ------------------------------------------------------------
   5a · The heading was squeezed by decoration
   ------------------------------------------------------------
   .page-head.has-deco h1 takes `padding-right: 96px` below
   600px to clear a floating vial. At 375 that leaves 231px for
   a 36.8px heading, so "Certificates of Analysis" breaks to
   three lines beside a 90px bottle. The bottle is decoration
   and the heading is the page. Shrink the reserve, and let the
   type step down with it.
   ------------------------------------------------------------ */
@media (max-width: 600px) {
  .page-head.has-deco h1 { padding-right: 64px; }
  .page-head h1 { font-size: 2rem; line-height: 1.12; letter-spacing: -.022em; }
}
/* This block used to zero the reserve and fade the decoration at
   the same time, so that a short heading could use the full
   measure while the bottle stepped out of its way. Only half of
   that ever ran: the fade named .page-deco, and the element
   functions.php:472 actually prints is .head-deco, so the
   selector never matched anything. The reserve went, the bottle
   stayed at full strength, and on /certificates-of-analysis/ the
   result was measurable: the h1 ran to 351px and the art starts
   at 283, so the last letters of "Certificates of Analysis" sat
   underneath a vial.

   Corrected to the real class, and the trade is rebalanced. A
   long title should wrap rather than collide; two clean lines
   are a heading, one line with a bottle through it is a fault.
   The reserve stays, smaller, and the art actually recedes now. */
@media (max-width: 380px) {
  .page-head.has-deco h1 { padding-right: 56px; }
  .page-head.has-deco .head-deco {
    opacity: .55;
    transform: scale(.8);
    transform-origin: 100% 50%;
  }
}

/* ------------------------------------------------------------
   5b · Section rhythm
   ------------------------------------------------------------
   40px margin plus 20px padding plus a rule above every h2 is
   61px of separator eighteen times over. On desktop that is
   generous. On a phone it is 1,098px of the document spent on
   dividers. Tightened, and the rule kept, because the rule is
   what makes the sections countable while scrolling.
   ------------------------------------------------------------ */
@media (max-width: 767px) {
  .prose { padding-block: 4px 56px; }
  .prose h2 {
    font-size: 1.25rem;
    margin-top: 28px;
    padding-top: 16px;
    line-height: 1.22;
  }
  .prose h3 { font-size: 1rem; margin-top: 20px; }
  .prose p, .prose li { line-height: 1.65; }
  .prose ul, .prose ol { margin-left: 18px; gap: 6px; }
  .page-head + .prose > p:first-child,
  .prose > p.lede { font-size: 1.0625rem; line-height: 1.58; }
}

/* ------------------------------------------------------------
   5c · A way to get somewhere
   ------------------------------------------------------------
   Built as a details element so it works with no JS and so
   the closed state costs one row. Rendered by page.php from
   the h2s already in the document, which means it can never
   disagree with the page the way a hand-maintained list would.
   ------------------------------------------------------------ */
.doc-toc {
  margin: 18px 0 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--tint);
  overflow: hidden;
}
.doc-toc > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 52px;
  padding: 0 16px;
  cursor: pointer;
  font-weight: 600;
  font-size: .9375rem;
  color: var(--ink);
  list-style: none;
}
.doc-toc > summary::-webkit-details-marker { display: none; }
.doc-toc > summary::after {
  content: "";
  width: 9px;
  height: 9px;
  flex: none;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--motion);
}
.doc-toc[open] > summary::after { transform: translateY(2px) rotate(-135deg); }
.doc-toc-count {
  margin-left: auto;
  margin-right: 4px;
  font-family: var(--font-num);
  font-size: .8125rem;
  font-weight: 500;
  color: var(--muted);
}
.doc-toc ol {
  margin: 0;
  padding: 4px 0 8px;
  list-style: none;
  border-top: 1px solid var(--line);
  display: block;
  counter-reset: toc;
}
.doc-toc li { margin: 0; }
.doc-toc a {
  display: flex;
  gap: 10px;
  align-items: baseline;
  min-height: 44px;
  padding: 10px 16px;
  font-size: .9375rem;
  line-height: 1.35;
  color: var(--ink-2);
  border-bottom: 0;
  text-decoration: none;
}
.doc-toc a::before {
  counter-increment: toc;
  content: counter(toc);
  flex: none;
  min-width: 18px;
  font-family: var(--font-num);
  font-size: .75rem;
  font-weight: 600;
  color: var(--accent);
}
@media (hover: hover) {
  .doc-toc a:hover { color: var(--ink); background: var(--bg); }
}
.doc-toc a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The prose sets `scroll-margin-top` on h2 already; the anchor
   ids are added by page.php to the same elements, so a jump
   lands under the sticky header rather than behind it. */

/* ------------------------------------------------------------
   5d · Back to the top
   ------------------------------------------------------------
   Eighteen sections deep on a phone, the only route back is a
   long scroll or the browser chrome. A footer-anchored link is
   the cheapest fix and needs no script.
   ------------------------------------------------------------ */
.doc-top {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-top: 28px;
  font-size: .875rem;
  font-weight: 600;
  color: var(--ink-2);
  border-bottom: 0;
}
.doc-top::before {
  content: "";
  width: 8px;
  height: 8px;
  border-left: 2px solid var(--accent);
  border-top: 2px solid var(--accent);
  transform: translateY(2px) rotate(45deg);
}

/* ============================================================
   6 · Certificates, said the same way everywhere
   ------------------------------------------------------------
   The COA page is the strongest page on the site and it says
   "Available on request". Elsewhere the same fact appeared as
   a bare "CoA" button, as "View certificates of analysis", and
   in the seed content as "published" and "posted", which is
   both off-policy and untrue. The strings are fixed at their
   sources; what is left here is making the request affordance
   look like an affordance.
   ============================================================ */
/* ------------------------------------------------------------
   The buy row
   ------------------------------------------------------------
   Naming the action on the certificate button cost it width,
   and .buy-row is a flex row of two items with no growth rule,
   so both settled near half: 187px for "Add to cart · $24.99"
   and 130px for a two-word secondary. The primary wrapped to
   two lines and came out 61px tall beside a 44px pill.

   The secondary is secondary. It gets its content width and no
   more; the primary takes the rest and keeps its label on one
   line, which is the arrangement it always wanted.
   ------------------------------------------------------------ */
.buy-row .coa-btn,
.coa-btn,
.pdp-coa-btn {
  flex: 0 0 auto;
  white-space: nowrap;
  font-weight: 600;
  padding-inline: 16px;
  font-size: .875rem;
}
.buy-row .single_add_to_cart_button,
.buy-row .btn-primary {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
}
/* The COA panel on /certificates-of-analysis/ centred six
   lines of body copy at 16px, which is the one setting that
   makes a short paragraph feel long. Left-aligned inside a
   centred panel keeps the composition and returns the ragged
   edge that tells the eye where the next line starts. */
@media (max-width: 767px) {
  .coa-panel { padding: 22px 18px 24px; }
  .coa-panel p,
  .coa-cert p { text-align: left; }
  .coa-panel .coa-fine,
  .coa-fine { text-align: center; }
  .coa-list { gap: 8px; }
  .coa-item a { padding: 12px 14px; }
}

/* ============================================================
   7 · Chrome budget on the product page
   ------------------------------------------------------------
   Measured at 375: header 65px and promo band 91px stack on
   every page, and the product page adds a research-use notice
   on top of them, so the first thing a buyer sees of the
   product is roughly 250px down an 812px viewport.

   The notice does not get cut. It is the load-bearing piece of
   compliance chrome on the one page where a buyer is deciding,
   and every word in it is doing work. What it can lose is the
   third line: at 14px over 1.6 the sentence wraps to three
   rows, and at 13px over 1.45 it wraps to two with the same
   words and the same emphasis.
   ============================================================ */
@media (max-width: 767px) {
  .ruo-banner,
  .ruo-banner p {
    padding-block: 9px;
    font-size: .8125rem;
    line-height: 1.45;
  }
}

/* ============================================================
   8 · Build a box, picker tile
   ------------------------------------------------------------
   Measured at 375: each tile is 158 x 389, so a phone shows
   two and a bit of a nine-product grid and the picker reads as
   five screens of scrolling before a decision is possible.

   The art keeps its full 4:5 tile. It is the reason the page
   works and the one thing on it nobody asked to be smaller.
   The 192px of body underneath it is where the height actually
   went, and most of that is air:

     .box-cat carries 10px of bottom margin into a price that
     already carries 8px of top margin, and the price row is
     flex-wrap with three children measuring 62 + 48 + 40 plus
     14 of gap against 134px of content width, so "in box"
     drops to a line of its own and costs a full row.

   "in box" is also the least useful word on the page. Every
   price in this grid is a box price, the section header says
   so, and the struck original beside it already explains what
   kind of number it is. It goes on phones and stays on desktop
   where the row has the width to hold it.
   ============================================================ */
@media (max-width: 767px) {
  .box-card-body { padding: 10px; }
  .box-card-body h3 { font-size: .9375rem; }
  .box-cat { margin-bottom: 6px; }
  .box-price { margin: 2px 0 8px; gap: 6px; }
  .box-price .in-box { display: none; }

  /* Tabular figures so $51.99 and $124.99 keep the struck
     original in the same place from tile to tile. */
  .box-price .now,
  .box-price .was { font-variant-numeric: tabular-nums; }

  /* The pill sits over the bottom of a photograph whose lower
     third is the palest part of the frame. 600 on the numeral
     is what keeps it legible there. */
  .box-coa { font-weight: 600; }
}

/* ============================================================
   9 · Five defects found by sweeping the surfaces nobody looks at
   ------------------------------------------------------------
   Each one is a rule that exists and works everywhere except in
   the one place a second class lands on the same element, or in
   the one place a class that usually travels in a pair arrives
   alone. That is why they survived every visual pass: on the
   page the author was looking at, the rule was correct.
   ============================================================ */

/* 9.1 · Partner program, three sections with no side gutter.

   inc/partner.php:457, :501 and :568 write <div class="wrap
   section-v6">, putting both classes on one element. Both set
   `padding` with the shorthand at equal specificity, and
   styles.css:1646 (.section-v6, 44px 0) is written after
   styles.css:80 (.wrap, 0 var(--gutter)), so it wins and
   zeroes the inline padding. The sections above these use the
   correct nesting and stay inset, which is what makes the
   three flush ones look like a rendering fault rather than a
   choice. page.php skips its own .wrap for this shortcode, so
   there is no parent to fall back on.

   A compound selector is (0,2,0) and settles it in both
   directions without touching the 44px block padding that is
   doing its job. */
.wrap.section-v6 {
  padding-inline: var(--gutter);
}

/* 9.2 · 404, same mechanism.

   404.php:2 is <div class="wrap empty-state"> and
   styles.css:556 restates the padding shorthand, so the
   headline, the sentence and the button all sit against both
   bezels. The h1 there is bare, so it also misses the mobile
   step-down that .page-head h1 gets in section 5 above, and
   arrives at 36px on a 375px screen.

   Scoped to the compound deliberately: cart-empty.php writes a
   bare .empty-state inside page.php's own .wrap, and a blanket
   rule would gutter that one twice. */
.wrap.empty-state {
  padding-inline: var(--gutter);
}
@media (max-width: 600px) {
  .empty-state h1 { font-size: 1.875rem; }
}

/* 9.3 · Checkout, the summary bar eats the bottom of the page.

   50-cart.css:881 reserves a flat 96px for .hl-bar, but the
   bar pads its own bottom by calc(14px + var(--safe-b)). On a
   phone with a home indicator that is 34px the browser never
   accounted for, so the last stretch of every checkout step,
   the Back link and the legal line, sits under the bar. In a
   375px desktop frame --safe-b is 0 and the bug does not
   exist, which is exactly why it shipped.

   10-tokens.css:152 already writes this correctly for the
   shared bottom bar. This is the same idiom, applied to the
   one bar that forgot it, and kept under 1024 so it cannot
   undo the desktop zero-out at 50-cart.css:1187. */
@media (max-width: 1023px) {
  body.hl-focus { padding-bottom: calc(96px + var(--safe-b, 0px)); }
}

/* 9.4 · Research library, filter pills 8px under the floor.

   archive-product.php:92 emits class="cat-pill chip" and picks
   up .chip's 44px minimum plus the invisible extension at
   40-plp-home.css:90. research.php:122 and :128 emit .cat-pill
   alone, so they get neither. With no line-height reset on
   button in this theme, .88rem text in 9px of padding lands
   around 36px. These sit in a horizontally scrolling row,
   where a tap that lands low is read as the start of a scroll
   and the filter the reader reached for simply does not
   happen.

   Harmless on the archive pills, which already measure 44. */
.cat-pill {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* 9.5 · Sign in, the two controls you need when the code
   does not arrive are the two smallest things on the screen.

   phone-auth.php:523 is the Resend button: styles.css:1295
   gives it 4px of padding and nothing else, so it is about
   27px tall. phone-auth.php:504 is the Change link, and
   styles.css:1302 forces display:inline on it, which means it
   cannot take a height at all and its target is the ~19px
   line box it happens to occupy mid-sentence. The submit
   buttons beside them are a correct 56px, so this is an
   oversight rather than a density choice.

   display:flex with width:max-content rather than inline-flex,
   because the centring at styles.css:1295 is `margin: 16px
   auto 0` and only a block-level box honours it. */
.auth-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: max-content;
  min-height: 44px;
  padding: 0 12px;
}
.auth-sub .auth-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 6px;
  vertical-align: middle;
}

/* 9.6 · Checkout step actions, a three pixel margin.

   Back plus Continue to payment measure roughly 292px of
   button inside about 289px of content width at 375. That is
   inside the uncertainty of font metrics rather than a proven
   overflow, so the fix is the cheap one that costs nothing if
   the buttons already fit: let them shrink instead of forcing
   the label to wrap. */
@media (max-width: 400px) {
  .hl-step-actions .hl-step-next,
  .hl-step-actions .hl-step-back { min-width: 0; }
  .hl-step-actions .hl-step-next { padding-inline: 18px; }
}
/* ============================================================
   A1 · HOME HERO · signature motion and graphics layer
   ------------------------------------------------------------
   Loads last: the build sorts parts by codepoint and digits sort
   before uppercase, so A1- lands after 99-mobile.css and wins
   every tie on equal specificity. Nothing here needs !important.

   Scope is .hero-v6 (front-page.php:34) and only .hero-v6. Every
   animated property is transform, opacity, filter or
   background-position; nothing moves layout, so this layer can
   not cause CLS. Everything that moves is stilled in the motion
   off block at the bottom of the file.

   What this adds, back to front:

     1. Film grain over the hero's gradient ground.
     2. The halo, re-keyed: its base keyframes drop the centering
        translate, which put the glow half its own size low and
        right whenever it animated. Fixed here.
     3. A caustic pool of light and shadow under the lead bottle,
        breathing in phase with the existing float.
     4. A specular sweep across the glass of each bottle: one
        light source, passing left to right, with a long rest
        between passes.
     5. Entrance choreography for the copy column, and a one-time
        foil glint on the emphasised word.
     6. Scroll parallax at four depths, gated behind
        @supports (animation-timeline: view()).

   Considered and deliberately dropped:

     - Any motion on the CTAs. The motion family pulses exactly
       one thing sitewide, the promo code pill, and a storefront
       whose buy button breathes reads as anxious, not premium.
     - Animated grain. Flickering noise reads as video artefact,
       and a full-bleed layer repainting forever is exactly the
       cost the is-off machinery exists to avoid. The grain is
       static.
     - Parallax on the copy column. The copy is the reference
       plane the depth is measured against, and .reveal already
       owns its transform via a transition during entrance
       (styles.css:789), so a scroll-driven transform on the same
       element would mask the reveal.
     - A true alpha mask for the sweep. mask-image with the
       per-SKU cutout would need the image URL, which this static
       file cannot know and which would be a second fetch anyway.
       The approximation used instead is documented at section 4.
   ============================================================ */

/* ------------------------------------------------------------
   1 · Film grain
   ------------------------------------------------------------
   The hero ground is three stacked gradients (styles.css:1469)
   and at phone width most of the band is that flat wash. The
   grain sits at z-index 1, under .wrap which the base pins at
   z-index 2 (styles.css:1477), so the texture lands on the
   ground and never dirties the type or the photography.

   The tile is an inline data: SVG running feTurbulence through a
   desaturate, so it costs no request. soft-light rather than
   multiply because multiply can only darken, which reads as a
   grey veil on cream; soft-light modulates both ways around the
   ground colour and reads as paper. Static on purpose, see the
   header note on animated grain.
   ------------------------------------------------------------ */
.hero-v6::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 240px 240px;
  opacity: .32;
  mix-blend-mode: soft-light;
}

/* ------------------------------------------------------------
   2 · The halo, re-keyed
   ------------------------------------------------------------
   .hero-halo (front-page.php:72) centres itself with
   transform: translate(-50%, -50%) (styles.css:1494) and then
   runs h-glow, whose keyframes declare transform: scale() alone
   (styles.css:1383). An animation replaces a plain declaration
   on the same property outright, so the moment h-glow starts the
   centering translate is discarded and the glow rides low and
   right by half its own size: 116px at the mobile 232px, 210px
   at the desktop 420px. This is the same trap 90-home-hero.css
   documents for the slot tilts, on one more element. The fix is
   the same fix: carry the translate in every keyframe.

   The base motion-off sweep (styles.css:1396) never listed the
   halo either, so it kept glowing under reduced motion. It stops
   in the motion off block below.
   ------------------------------------------------------------ */
.hero-v6 .hero-halo {
  animation: a1-halo 7s ease-in-out infinite;
}
@keyframes a1-halo {
  0%, 100% { opacity: .28; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: .5;  transform: translate(-50%, -50%) scale(1.06); }
}

/* ------------------------------------------------------------
   3 · The caustic pool
   ------------------------------------------------------------
   A transparent bottle over a gradient is a PNG on a page. What
   makes it an object in a room is the floor answering it: a
   pool of warmed light where the glass focuses the light down,
   inside a soft shadow where the bottle blocks it. One squashed
   radial does both, gold-bright at the core falling to the same
   warm brown the vial drop-shadows already use
   (70-vial-photo.css:56).

   Vertical placement is derived from the declared geometry, not
   eyeballed: at phone width v1 sits at top 8px and 228px tall
   in a 268px band (90-home-hero.css:140), so its base lands at
   236px and the pool centres there. The tablet and desktop
   steps re-derive the same way from the 360px and 440px bands
   with the bottle flex-centred.

   It breathes with the float: a1-pool runs 4s ease-in-out, the
   exact pairing of v1's f-float (styles.css:1388), and both are
   released by the same .is-lit recalc, so they start in phase.
   The pool contracts and dims as the bottle lifts, which is
   what couples the object to the floor. Gating the breath
   behind .is-lit also means it never animates before there is a
   bottle to answer; the pool itself is static furniture and is
   present from first paint.

   z-index 0 with the halo, and a pseudo paints after the
   element's children at the same level, so the stack reads
   halo, then pool, then bottles. pointer-events: none because
   it lies inside an aria-hidden decoration (front-page.php:71).
   ------------------------------------------------------------ */
.hero-v6 .hero-collage::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 12px;
  z-index: 0;
  width: 190px;
  height: 40px;
  transform: translateX(-50%);
  pointer-events: none;
  background: radial-gradient(closest-side,
      rgba(227, 192, 133, .34),
      rgba(161, 98, 7, .12) 46%,
      rgba(83, 56, 12, .06) 66%,
      transparent 82%);
  opacity: .8;
}
.hero-v6 .hero-collage.is-lit::after {
  animation: a1-pool 4s ease-in-out infinite;
}
@keyframes a1-pool {
  0%, 100% { opacity: .9;  transform: translateX(-50%) scale(1, 1); }
  50%      { opacity: .62; transform: translateX(-50%) scale(.93, .86); }
}
@media (min-width: 768px) {
  .hero-v6 .hero-collage::after { bottom: 52px; width: 240px; height: 48px; }
}
@media (min-width: 1024px) {
  .hero-v6 .hero-collage::after { bottom: 44px; width: 300px; height: 56px; }
}

/* ------------------------------------------------------------
   3b · Hold the pose, not just the position
   ------------------------------------------------------------
   90-home-hero.css:250 states the intent for the .is-lit hold:
   the only thing that changes when the bottles light is that
   they start moving. The code does not quite deliver it. The
   slots still carry their own rotate declarations, at
   styles.css:1489 for desktop and in the 90-home-hero mobile
   block, and those only ever render while the float is NOT
   running, because a running float replaces the slot transform.
   So during the pre-lit hold the slot rotation stacked on the
   image rotation from 90-home-hero.css:211 and every bottle
   waited at roughly double tilt, then stood up by its own slot
   angle the moment the float began. Neutralising the slot
   transform in the held state makes the pose identical before
   and after .is-lit, which is the documented intent. The image
   keeps its tilt; position comes from left/top/margin, so
   nothing moves.
   ------------------------------------------------------------ */
.hero-v6 .hero-collage:not(.is-lit) .hero-vial { transform: none; }

/* ------------------------------------------------------------
   4 · The specular sweep
   ------------------------------------------------------------
   One pass of light across the glass, then a long rest. The
   sweep lives on each slot's ::after (.hero-vial,
   front-page.php:85), a rounded lozenge inset from the slot box.
   The slot is the bottle's bounding box because 70-vial-photo
   gives it the bottle's own 47/96 ratio (70-vial-photo.css:31),
   so an inset lozenge hugs the glass body closely.

   Reading as light on glass rather than a shine rectangle takes
   four things together: the lozenge's radius clips the band to a
   bottle-ish silhouette; an elliptical gradient mask fades the
   band out before it can touch the lozenge's edges, which is
   what curvature does to a reflection; mix-blend-mode: screen
   makes it brighten the photograph instead of sitting on it,
   and turns near-invisible over the white label the way a real
   highlight dies on matte paper; and the pseudo is rotated to
   the exact angle the image is (90-home-hero.css:211-214), so
   the streak runs down the glass axis, not down the page.

   The band travels via background-position, which rule 1
   permits. The trade is that a background-position animation
   repaints the pseudo rather than riding a compositor
   transform, and the honest price of clipping by transform
   instead would be overflow: hidden on the slot, which slices
   the image's drop-shadow at the box edge. The pseudos are
   small, the pass lasts under six seconds of a fourteen second
   cycle, and the is-off observer stops the lot offscreen.

   One light source: the delays run west to east across the
   group, v2 left, v1 centre, v3 then v4 on the right, matching
   the slot positions in 90-home-hero.css and the desktop map in
   styles.css:1489. All four share one period so they stay phase
   locked as a single pass. The travel is 42% of a 14s cycle,
   about 5.9s, inside the 4 to 7s target, with an 8s rest.

   Gated behind .is-lit: light on glass needs the glass decoded,
   and the parked band (background-position 130%) is fully
   outside the mask, so the static composition simply has no
   sweep and loses nothing.
   ------------------------------------------------------------ */
.hero-v6 .hero-vial::after {
  content: "";
  position: absolute;
  inset: 4% 14% 3%;
  border-radius: 26% / 7%;
  pointer-events: none;
  background-image: linear-gradient(105deg,
      transparent 40%,
      rgba(255, 255, 255, .22) 47%,
      rgba(255, 255, 255, .5) 50%,
      rgba(255, 255, 255, .22) 53%,
      transparent 60%);
  background-size: 340% 100%;
  background-repeat: no-repeat;
  background-position: 130% 0;
  mix-blend-mode: screen;
  -webkit-mask-image: radial-gradient(ellipse 62% 46% at 50% 44%, #000 52%, transparent 94%);
          mask-image: radial-gradient(ellipse 62% 46% at 50% 44%, #000 52%, transparent 94%);
}
/* The image tilts, restated on the pseudo. If 90-home-hero
   retunes its angles these four follow it by hand; they are the
   same numbers for the same reason. */
.hero-v6 .hero-vial.v1::after { transform: rotate(-8deg); }
.hero-v6 .hero-vial.v2::after { transform: rotate(12deg); }
.hero-v6 .hero-vial.v3::after { transform: rotate(-15deg); }
.hero-v6 .hero-vial.v4::after { transform: rotate(8deg); }

.hero-v6 .hero-collage.is-lit .hero-vial::after {
  animation: a1-sweep 14s ease-in-out 2.4s infinite;
}
.hero-v6 .hero-collage.is-lit .hero-vial.v1::after { animation-delay: 2.8s; }
.hero-v6 .hero-collage.is-lit .hero-vial.v3::after { animation-delay: 3.2s; }
.hero-v6 .hero-collage.is-lit .hero-vial.v4::after { animation-delay: 3.35s; }
@keyframes a1-sweep {
  0%   { background-position: 130% 0; }
  42%  { background-position: -30% 0; }
  100% { background-position: -30% 0; }
}

/* ------------------------------------------------------------
   5 · Entrance choreography
   ------------------------------------------------------------
   The copy column already reveals as one block: .hero-copy
   carries .reveal (front-page.php:36) and main.js adds .in from
   its observer, with a 3s failsafe and a no-observer fallback.
   This keeps that system and sequences the children inside it:
   headline, lede, CTAs, then the trust row on desktop, 60ms
   apart on the family ease-out, translate and opacity only.

   Rule 4, taken literally: opacity on these children exists
   nowhere but inside the keyframes, so an animation that never
   starts leaves them at their static opacity of 1, and the
   keyframes themselves start from .35 and .3, never zero, so
   even a frame frozen at 0% is readable. The whole thing is
   additionally gated behind .js (header.php:27) and .in, both
   set by the same script that guards .reveal itself.

   The emphasised word gets its own beat: it arrives last, as a
   focus pull on filter: blur rather than a translate, because
   the em is an inline box (front-page.php:50) and a transform
   would need inline-block, which opens a break opportunity
   before the full stop that follows it and risks an orphaned
   period at 375px. Blur and opacity apply to inline boxes as
   they are.
   ------------------------------------------------------------ */
@keyframes a1-rise {
  from { opacity: .35; transform: translateY(12px); }
  to   { opacity: 1;   transform: none; }
}
/* Was a blur focus-pull from opacity .3 and blur(7px). Measured on
   the live page at 375: the claim word is painted with
   background-clip: text over a transparent fill, so a blurred gold
   gradient on a cream ground is not a soft word, it is no word. For
   the ~940ms of delay plus duration the headline read "Research-grade
   peptides you can ." with a smudge in the gap, while every line
   around it was already sharp. A missing word is worse than a still
   one, so the em now only carries the foil sweep, which passes
   through the letterforms without ever emptying them. */
@keyframes a1-focus {
  from { opacity: .82; }
  to   { opacity: 1; }
}
.js .hero-v6 .hero-copy.in > h1           { animation: a1-rise 560ms var(--ease-out) both; }
.js .hero-v6 .hero-copy.in > .lede        { animation: a1-rise 560ms var(--ease-out) 60ms both; }
.js .hero-v6 .hero-copy.in > .hero-ctas-v6 { animation: a1-rise 560ms var(--ease-out) 120ms both; }
.js .hero-v6 .hero-copy.in > .hero-trust  { animation: a1-rise 560ms var(--ease-out) 180ms both; }
.js .hero-v6 .hero-copy.in h1 em          { animation: a1-focus 360ms var(--ease-out) 120ms both; }

/* ------------------------------------------------------------
   5b · Foil on the claim word
   ------------------------------------------------------------
   The bottle's label prints its type in gold; the headline's
   claim word should be the same material. Not --gold-foil
   as-is: its light stops sit around #e3c085, which against this
   hero's cream ground is nowhere near the 3:1 large-text floor,
   and the site only ever runs that gradient on dark grounds
   (styles.css:1442 onward). This variant keeps the same
   family, #854d0e through #a16207 with one #c9973f glint stop,
   and it is sized at 300% so the visible slice at rest, either
   end of the gradient, is the dark range only. The accent
   itself is documented at 4.9:1 on white (styles.css:33).

   The glint is entrance-only: one pass of the highlight through
   the word as it focuses in, then it rests dark and static.
   Motion carries the foil moment so the resting state never has
   to trade contrast for shine.

   Gated behind @supports for background-clip: text, because the
   fallback chain matters: without support the em keeps the
   base's solid accent colour (styles.css:1501) and nothing is
   transparent over nothing.
   ------------------------------------------------------------ */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .hero-v6 h1 em {
    background-image: linear-gradient(105deg,
        #854d0e 0%, #a16207 35%, #c9973f 50%, #a16207 65%, #854d0e 100%);
    background-size: 300% 100%;
    background-repeat: no-repeat;
    background-position: 0 0;
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
    color: var(--accent);
  }
  .js .hero-v6 .hero-copy.in h1 em {
    animation: a1-focus 360ms var(--ease-out) 120ms both,
               a1-foil 1100ms var(--ease-soft) 300ms both;
  }
}
@keyframes a1-foil {
  from { background-position: 0% 0; }
  to   { background-position: 100% 0; }
}

/* ------------------------------------------------------------
   6 · Scroll parallax, four depths
   ------------------------------------------------------------
   As the hero scrolls away, the layers leave at different
   rates: the front bottle slightly faster than the page, the
   back bottles, the halo and the pool progressively slower, and
   the copy column exactly with the page as the reference plane.
   Small offsets on purpose; depth is felt at 12 to 46px, and
   anything bigger reads as an effect.

   All of it is scroll-driven CSS inside this @supports gate, so
   a browser without animation-timeline gets the full static
   composition and loses nothing. Inside the gate,
   animation-composition is safely available, which is what lets
   the parallax ADD to the floats instead of replacing them: the
   float shorthand must be restated to grow the list, so the
   pairings below restate styles.css:1388-1391 exactly, per the
   slot-to-float table in front-page.php:74-79. If the float
   family retunes, these four lines follow it by hand.

   The .is-lit gate stays in the selector for the same reason it
   exists at all (90-home-hero.css:247): these shorthands would
   otherwise out-specify the hold and re-promote undecoded
   layers at first paint.

   One keyframe block serves every layer: the offset lives in
   --a1-px, resolved per element. The range runs over exit, so
   progress is zero while the hero is fully in view and the
   motion only exists in the leave.
   ------------------------------------------------------------ */
@supports (animation-timeline: view()) {
  @keyframes a1-px {
    from { transform: translateY(0); }
    to   { transform: translateY(var(--a1-px, 0px)); }
  }

  .hero-v6 .hero-collage.is-lit .hero-vial.v1 {
    --a1-px: -16px;
    animation: h-float 4s ease-in-out infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
  .hero-v6 .hero-collage.is-lit .hero-vial.v2 {
    --a1-px: 12px;
    animation: h-float-slow 5s ease-in-out .5s infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
  .hero-v6 .hero-collage.is-lit .hero-vial.v3 {
    --a1-px: 24px;
    animation: h-float-deep 4.5s ease-in-out 1s infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
  .hero-v6 .hero-collage.is-lit .hero-vial.v4 {
    --a1-px: 32px;
    animation: h-drift 7s ease-in-out infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
  .hero-v6 .hero-halo {
    --a1-px: 40px;
    animation: a1-halo 7s ease-in-out infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
  .hero-v6 .hero-collage.is-lit::after {
    --a1-px: 46px;
    animation: a1-pool 4s ease-in-out infinite, a1-px linear both;
    animation-timeline: auto, view();
    animation-range: normal, exit 0% exit 100%;
    animation-composition: replace, add;
  }
}

/* ------------------------------------------------------------
   7 · The is-off pause reaches the pseudos
   ------------------------------------------------------------
   The base pause rule (40-plp-home.css:647) matches the marked
   element and its descendants, but * never matches a
   pseudo-element and animation-play-state does not inherit, so
   the sweep and the pool this file added would keep compositing
   offscreen while everything around them paused. Restated here
   for the two pseudos. No !important, unlike the base rule:
   these sit after every animation shorthand above at equal or
   greater specificity, so the cascade resolves it on order,
   which is the point of the A1- prefix.
   ------------------------------------------------------------ */
.hero-v6 .hero-collage.is-off::after,
.hero-v6 .hero-collage.is-off .hero-vial::after {
  animation-play-state: paused;
}

/* ------------------------------------------------------------
   8 · Motion off
   ------------------------------------------------------------
   Reduced motion keeps the whole composition and loses only the
   movement: grain, pool, halo glow, foil word and the collage
   all render in their resting states. The floats themselves are
   already stopped by the base sweep with !important
   (styles.css:1396); everything this file added stops here, and
   the halo joins the list it was never on. The halo's static
   opacity is pinned between its animated extremes of .28 and
   .5, because with the animation gone it would otherwise render
   the gradient at full strength for the first time.

   The last rule is the reduced-motion half of 3b: with the
   floats stopped, the slot rotations would come back and double
   the image tilts, so the slot transform is neutralised here
   too and the bottles hold the exact pose the moving version
   floats around.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .hero-v6 .hero-halo {
    animation: none;
    transform: translate(-50%, -50%);
    opacity: .38;
  }
  .hero-v6 .hero-collage::after,
  .hero-v6 .hero-collage.is-lit::after {
    animation: none;
  }
  .hero-v6 .hero-vial::after,
  .hero-v6 .hero-collage.is-lit .hero-vial::after {
    animation: none;
  }
  .hero-v6 .hero-collage.is-lit .hero-vial.v1,
  .hero-v6 .hero-collage.is-lit .hero-vial.v2,
  .hero-v6 .hero-collage.is-lit .hero-vial.v3,
  .hero-v6 .hero-collage.is-lit .hero-vial.v4 {
    animation: none;
  }
  .js .hero-v6 .hero-copy.in > h1,
  .js .hero-v6 .hero-copy.in > .lede,
  .js .hero-v6 .hero-copy.in > .hero-ctas-v6,
  .js .hero-v6 .hero-copy.in > .hero-trust,
  .js .hero-v6 .hero-copy.in h1 em {
    animation: none;
  }
  .hero-v6 .hero-vial { transform: none; }
}
/* ============================================================
   A2 · Motion, the card and the rails
   ------------------------------------------------------------
   Loads after 99-mobile.css (digits sort before "A" in the
   codepoint sort build.sh uses), so everything in this file wins
   ties against the entire system. That is a loaded gun, so the
   rules here add states and timing only. Nothing restates a
   z-index, a height, a padding or a snap value that an earlier
   part already fixed; section 1 of 99-mobile.css in particular
   (link at z 2, controls at 3, badges at 4 and inert, 44px
   buttons) is treated as law and never re-declared.

   Scope, verified against the markup before styling:
     .product-card / .card-link / .qa-btn / .card-btn
        woocommerce/content-product.php:29, 30, 72, 77
     .rail / .rail-card / .rail-media / .rail-price
        front-page.php:169, 235, 257, 278; base .rail in
        css/styles.css:1540
     .product-media    content-product.php:40 (outer box passed
        to halotir_rail_media), base css/styles.css:194
     .is-cut           functions.php:647 (halotir_rail_media)
     .card-proof / .cp-purity / .cp-coa
        functions.php:427-439 (halotir_card_proof)
     .product-grid.reveal-group
        woocommerce/archive-product.php:145
     .qa-btn.added     assets/main.js:594 adds it on a quick add,
        main.js:597 removes it 1600ms later. main.js also swaps
        the label to "Added" via textContent, which deletes the
        plus icon, so the added state styles a text-only pill.
     related rail      functions.php:1756 (.rail-media),
        functions.php:1770 (.qa-btn), same classes as the home
        rail, so every rule here covers it for free.

   Only transform, opacity, filter, background-position and
   clip-path are ever animated. The one mask in this file is
   static. Every moving thing is answered in the reduced motion
   block at the end.
   ============================================================ */

/* ============================================================
   A2.1 · The press, made physical
   ------------------------------------------------------------
   Phones have no hover, so the pressed state is the only
   feedback a card ever gives. The system already has the bones:
   10-tokens.css:128 presses the card to scale(.985) and
   40-plp-home.css:657 restates it above the base layer. What is
   missing is feel. The shipped press inherits the one 200ms
   pairing, so the card eases DOWN over 200ms, and a press that
   arrives late reads as lag, not as contact.

   Two changes. Press-in gets its own short duration, so contact
   registers inside a frame or two; release falls back to the
   base 200ms, which is the "returns fast" half of the contract
   and needs no new rule. And the art answers slightly more than
   the frame: the frame goes to .985 while the tile art goes to
   .97, so the two layers separate by about 1.5% and the surface
   reads as flexing under the thumb rather than the whole card
   shrinking as one flat sticker.

   The art rule targets the wrapper (.is-cut) and never the vial,
   for the reason 40-plp-home.css:553 records: the float
   animation owns the bottle's own transform, and an animation
   beats a plain declaration, so a rule on .vial would silently
   never apply. The bare .vial selectors below cover the drawn
   SVG fallback, which has no float animation and takes the
   transform directly (base transitions at styles.css:201 and
   styles.css:1560).

   Trade-off: on hover+touch hybrids a press momentarily wins
   over the hover zoom (scale 1.05 from 40-plp-home.css:558).
   That is correct; the thumb outranks the pointer.
   ============================================================ */
.product-card:active,
.rail-card:active {
  transition-duration: 90ms;
}

/* Pressing the quick-add button, or the COA chip, must not squish
   the card underneath it. The two targets overlap visually, and if
   both compress together the button reads as painted onto the card
   instead of resting on it. :active reaches every ancestor of the
   pressed element, so without this carve-out the shipped card
   scale fires on every button press. In a browser without :has()
   the rule drops silently and the shipped behaviour continues,
   which is a safe floor. */
.product-card:active:has(.qa-btn:active, .cp-coa:active),
.rail-card:active:has(.qa-btn:active, .cp-coa:active) {
  transform: none;
}

@media not all and (prefers-reduced-motion: reduce) {
  /* The art presses deeper than the frame. Guarded by the same
     :has() carve-out so the bottle stays still while the button
     is the thing being pressed. */
  .product-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .is-cut,
  .rail-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .is-cut,
  .product-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .product-media .vial,
  .rail-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .rail-media .vial {
    transform: scale(.97);
    transition-duration: 110ms;
  }
}

/* ============================================================
   A2.2 · Light on the glass
   ------------------------------------------------------------
   The press moment carries one more cue: a soft band of warm
   light crossing the tile, masked to an ellipse sitting where
   the bottle sits (the same 50%/56% centre the standing bloom
   uses, 40-plp-home.css:481), so it reads as light catching
   glass rather than as a banner shine across the whole tile.

   Mechanics: press-in fades the band up in 90ms and sends it
   across on a 460ms ease-out, so even a 120ms tap shows a
   believable glimmer starting to move. On release the band
   fades out in 140ms where it stands, and only snaps back to
   its start 200ms later, after it is already invisible; that
   delayed zero-duration return is what makes a short tap end
   clean instead of visibly rewinding.

   Layering is the part that could break the site, so it is
   explicit: the pseudo sits at z-index 1 inside the card's
   stacking context, the same layer as the bottle (which paints
   first, being earlier in the tree), and BELOW .card-link at 2
   and .qa-btn at 3 per 99-mobile.css:44-51. pointer-events is
   none on top of that, so it can never eat a tap even if the
   layering ever changes around it. Both tiles clip: overflow
   hidden at styles.css:199 and styles.css:1555.

   The whole feature lives inside the motion media query, so a
   reduced motion user never has the pseudo at all. */
@media not all and (prefers-reduced-motion: reduce) {
  .product-media::after,
  .rail-media::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(105deg, transparent 40%, rgba(255, 251, 240, .55) 50%, transparent 60%);
    -webkit-mask-image: radial-gradient(ellipse 72% 64% at 50% 56%, #000 38%, transparent 74%);
    mask-image: radial-gradient(ellipse 72% 64% at 50% 56%, #000 38%, transparent 74%);
    opacity: 0;
    transform: translateX(-65%);
    transition: opacity 140ms linear, transform 0s linear 200ms;
  }
  .product-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .product-media::after,
  .rail-card:active:not(:has(.qa-btn:active, .cp-coa:active)) .rail-media::after {
    opacity: .85;
    transform: translateX(65%);
    transition: opacity 90ms linear, transform 460ms var(--ease-out);
  }
}

/* ============================================================
   A2.3 · The button is its own object
   ------------------------------------------------------------
   The quick-add pill (base at styles.css:1001) hovers to a full
   ink fill on desktop but had no press state at all, so on a
   phone the only acknowledgment of the site's single most
   important tap was the card squish behind it, which is exactly
   the confusion the carve-out above removes. The press is the
   hover fill arriving instantly plus a .96 compression on a
   70ms in-stroke: filled, small, fast, which is a different
   gesture vocabulary from the card's soft .985, so thumb and
   eye agree there are two objects.

   .96, no deeper. Hit testing follows transforms, so a pressed
   44px pill is momentarily about 42px; that is under a pixel a
   side, the same order as the shipped card scale, and well
   inside touch slop. A stronger squash would start gambling
   with edge taps.

   The COA chip gets a colour snap only. It is a 30px text chip
   over a busy card; making it jump would out-shout the button. */
.qa-btn:active {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
  transform: scale(.96);
  transition-duration: 70ms;
}
.cp-coa:active {
  color: var(--accent-deep);
}

/* ============================================================
   A2.4 · The add-to-cart moment
   ------------------------------------------------------------
   main.js:594 adds .added, swaps the label to "Added", and
   holds the state for 1600ms (main.js:597). The base layer
   already turns the pill success green (styles.css:1010). Two
   things were missing.

   First, a fault: the button is disabled for the whole added
   window, and .qa-btn:disabled (styles.css:1009) dims it to
   half opacity, so the confirmation was being shown at 50%.
   The dim is right for "Adding" and wrong for "Added"; the
   moment gets its full opacity back.

   Second, the ceremony. A check draws itself in ahead of the
   word: an L of two borders rotated into a tick, revealed by a
   clip-path wipe that runs in the element's own pre-rotation
   space, so the reveal travels along the stroke, short arm
   first, the way a pen would draw it. Under it the pill
   settles: it starts at the pressed .96 the thumb just left it
   at, overshoots to 1.015 and lands at rest. No confetti; a
   receipt, not a celebration. The green itself still arrives
   through the base 180ms background transition
   (styles.css:1006), which is enough.

   "Adding" itself gets nothing new on purpose. It is a sub-
   second wait state and the shipped dim already says "busy";
   giving it motion would make the wait feel longer. */
.qa-btn.added:disabled {
  opacity: 1;
}
.qa-btn.added {
  animation: a2-qa-settle 340ms var(--ease-out);
}
/* position: static is load-bearing. This same pseudo is the old
   44px hit-area stretcher: 40-plp-home.css:276 declares it
   absolute with left/right 0 and top 50% at every width, and
   99-mobile.css:91 only removed its content on phones. The
   moment content comes back, that stray geometry would come
   back with it and pin a full-width 44px ghost over the price;
   static opts out of all of it and lets the check ride the
   button's own flex row, sitting ahead of the word with the
   base gap (styles.css:1005). */
.qa-btn.added::before {
  content: "";
  position: static;
  flex: none;
  width: 12px;
  height: 7px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-1px) rotate(-45deg);
  animation: a2-check-draw 240ms var(--ease-out) 60ms both;
}
@keyframes a2-qa-settle {
  0%   { transform: scale(.96); }
  60%  { transform: scale(1.015); }
  100% { transform: scale(1); }
}
@keyframes a2-check-draw {
  from { clip-path: inset(-2px 100% -2px -2px); }
  to   { clip-path: inset(-2px); }
}

/* ============================================================
   A2.5 · Entrance
   ------------------------------------------------------------
   The shop grid already animates in: archive-product.php:145
   wraps it in .reveal-group, main.js:980 observes it, and
   styles.css:1663 staggers children at 70ms steps. But the
   stagger stops at nth-child(6), so in a 4-up desktop grid
   cards seven and eight fire at 0ms, BEFORE cards two through
   six, and the sweep reads as broken. Re-timed here for the
   grid only: 40ms steps, capped at 360ms from nth-child(10)
   on, so a 12-card grid never leaves a card waiting long and
   the order is monotonic. These delays outrank the base rule
   on specificity and only matter to the transition path.

   Where scroll-driven animations exist, each card instead
   drives its own entrance off its position in the scrollport:
   grid cards rise as they enter from below, rail cards resolve
   as they are swiped in from the right. Progress is position,
   not time, so a card revealed by scrolling animates exactly
   as it appears, a refresh mid-page renders every visible card
   already settled, and nothing ever waits at opacity 0 for an
   observer that failed (main.js:986 keeps a 3s safety net for
   the fallback path; the scroll-driven path needs none).

   Two techniques worth recording because they are load-bearing:

   The "to" keyframes deliberately omit transform. A missing
   keyframe value resolves to the underlying computed value, so
   at full progress the animation stops holding transform and
   the :active press scale and desktop hover lift keep working.
   Naming transform in the "to" frame would pin it forever
   (scroll-driven animations never end) and silently kill every
   press state above.

   Inside the @supports block the reveal-group's waiting state
   (.js .reveal-group:not(.in) > *, styles.css:2067) is
   neutralised for grid cards, because it is the underlying
   value the trick above resolves to; without this, a fully
   scrolled-in card would sit 16px low until the observer
   fired.

   Cost accepted knowingly: in supporting browsers, cards
   already on screen at load do not play the time-based load
   stagger, because their scroll progress is already complete.
   Position-honest entrances for everything below the fold are
   worth more than a one-time flourish above it.

   The rail peek is the second dividend: at 375px the next card
   peeks about 90px into frame, which puts it mid-entrance,
   visibly lighter than the settled cards. A card that is
   obviously mid-arrival says "this row continues" better than
   any arrow. The fallback rail gets no time-based entrance;
   with no observer hook on .rail, a load-time animation would
   finish before the rail is ever seen. */
.product-grid.reveal-group.in > .product-card:nth-child(2) { transition-delay: 40ms; }
.product-grid.reveal-group.in > .product-card:nth-child(3) { transition-delay: 80ms; }
.product-grid.reveal-group.in > .product-card:nth-child(4) { transition-delay: 120ms; }
.product-grid.reveal-group.in > .product-card:nth-child(5) { transition-delay: 160ms; }
.product-grid.reveal-group.in > .product-card:nth-child(6) { transition-delay: 200ms; }
.product-grid.reveal-group.in > .product-card:nth-child(7) { transition-delay: 240ms; }
.product-grid.reveal-group.in > .product-card:nth-child(8) { transition-delay: 280ms; }
.product-grid.reveal-group.in > .product-card:nth-child(9) { transition-delay: 320ms; }
.product-grid.reveal-group.in > .product-card:nth-child(n + 10) { transition-delay: 360ms; }

@supports (animation-timeline: view()) {
  .js .product-grid.reveal-group:not(.in) > .product-card {
    opacity: 1;
    transform: none;
  }
  .product-grid > .product-card {
    animation: a2-card-rise linear both;
    animation-timeline: view(block);
    animation-range: entry 0% entry 45%;
  }
  /* Alternate columns complete a beat later, so a row arrives as
     a slight diagonal rather than a slab. Range offset, not
     animation-delay: time delays do not apply to progress-based
     timelines. */
  .product-grid > .product-card:nth-child(2n) {
    animation-range: entry 8% entry 53%;
  }
  .rail > .rail-card {
    animation: a2-rail-in linear both;
    animation-timeline: view(inline);
    animation-range: entry 0% entry 85%;
  }
}
/* Starts at .3, not 0.

   The sibling rule in A3 used an identical construction on the box
   picker with a `from` of opacity 0, and on the live page cards that
   were sitting fully inside the viewport, 71px from the top, still
   reported opacity 0: the view() progress was not advancing for those
   subjects, so `from` was simply what the browser painted. On a shop
   grid that means the products are gone.

   A non-zero floor makes that failure survivable. A stalled timeline
   now costs a slightly faint card instead of no card, and the visible
   travel from .3 to 1 is enough that nothing is lost when it does
   run. This is the same floor A1 and A4 use, and it is the reason
   neither of them failed the way A3 did. */
@keyframes a2-card-rise {
  from { opacity: .3; transform: translateY(16px); }
  to   { opacity: 1; }
}
@keyframes a2-rail-in {
  from { opacity: .3; transform: translateX(18px); }
  to   { opacity: 1; }
}

/* ============================================================
   A2.6 · The rail says it scrolls
   ------------------------------------------------------------
   Checked before touching: snapping is already complete.
   styles.css:1541 sets scroll-snap-type: x mandatory,
   styles.css:1546 aligns each card to start, and
   10-tokens.css:112 fixed the scroll-padding so the snap
   respects the gutter. None of that is restated here.

   What was missing is the edge story. Cards were guillotined
   flat at the bezel, which is the same fault the proof strip
   had before 40-plp-home.css:330 masked it. Same cure: a fade
   sized to the gutter at both ends. The geometry makes it
   honest at the extremes with a purely static mask: the rail's
   own gutter padding (styles.css:1543) means that at either end
   of travel the terminal card rests inside the fade zone's
   inner edge and reads at full strength, so nothing is ever
   permanently washed out; mid-scroll, whatever crosses the
   bezel dissolves instead of being cut, which is the
   continuation cue. Together with the peek entrance above, the
   rail now states its own affordance twice without an arrow.

   overscroll-behavior-x keeps a hard swipe at the rail's end
   from chaining into the browser's own horizontal gestures, so
   the snap stop feels like the rail's edge and not the page
   giving way. */
.rail {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 var(--gutter), #000 calc(100% - var(--gutter)), transparent 100%);
  overscroll-behavior-x: contain;
}

/* ============================================================
   A2.7 · What deliberately does not move
   ------------------------------------------------------------
   Decided, not forgotten:

   The price. It is the number a buyer checks twice, it already
   has tabular numerals so it cannot jitter, and a price that
   moves reads as a price that changed. Static.

   The proof row. "≥99% purity" and "COA on request" are the
   trust surface of the card; evidence that animates reads as
   marketing. The COA chip keeps only the colour snap in A2.3.

   The badges. .rail-badge and .promo-badge are pointer-inert
   decoration by decree of 99-mobile.css:53, and the promo badge
   already owns a state change (is-on, functions.php:1003) that
   swaps it to the sage receipt; motion on top of that state
   machine would blur which change means something.

   The card-flag and card-meta lines: annotations. Annotations
   hold still.
   ============================================================ */

/* ============================================================
   A2.8 · Motion off
   ------------------------------------------------------------
   Everything this file animates, answered. The press keeps a
   non-motion acknowledgment: the hairline lift ring the system
   already uses for hover (10-tokens.css:38), applied instantly,
   so a reduced motion user still sees the card respond to touch
   as a static state change. 10-tokens.css:242 and
   40-plp-home.css:660 already zero the card scale itself; the
   art press and the light sweep never exist under reduced
   motion because A2.1/A2.2 wrap them in the motion query.

   The last rule also settles a pre-existing gap for the grid:
   under reduced motion the base guard (styles.css:1671) is
   outspecified by the .js waiting state (styles.css:2067), so
   grid cards still faded in over 560ms. For product cards the
   waiting state is neutralised outright, which makes the added
   delays above moot and the grid simply present.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .product-card:active,
  .rail-card:active {
    transition: none;
    box-shadow: var(--lift);
  }
  .qa-btn:active {
    transform: none;
    transition: none;
  }
  .qa-btn.added {
    animation: none;
  }
  .qa-btn.added::before {
    animation: none;
  }
  .product-grid > .product-card,
  .rail > .rail-card {
    animation: none;
  }
  .product-grid.reveal-group.in > .product-card:nth-child(n) {
    transition-delay: 0s;
  }
  .js .product-grid.reveal-group:not(.in) > .product-card {
    opacity: 1;
    transform: none;
  }
}
/* ============================================================
   A3 · BUILD A BOX · motion and micro-interaction layer
   ------------------------------------------------------------
   Loads last: the build sorts parts by codepoint, digits sort
   before uppercase, so A3- lands after 99-mobile.css and wins
   every tie at equal specificity. Nothing here needs !important
   and nothing here restates a rule 99-mobile.css already
   shipped (tile body padding, .in-box hidden under 768, the
   30px phone tokens all stay exactly as that file left them).

   Scope is the Build a Box page only: everything hangs off
   .box-grid, .box-filter, .box-tray, .box-sheet, .box-scrim,
   .box-slot or .box-gift, none of which render anywhere else.

   Every animated property is transform, opacity, filter or
   background-position. Nothing animates a box dimension or an
   offset, so this layer cannot cause CLS, and no keyframe
   translates an element sideways past its own container, so
   nothing can widen the page at 375px. Every state selector
   below is one the JS actually sets; the ledger:

     .box-add.in-cart            box.js:196
     .box-add[disabled]          box.js:201
     .box-card[hidden]           box.js:259
     .tray-token.filled          box.js:176
     .tray-token.tray-free.filled  box.js:184
     .box-sheet.open             box.js:276 and 286
     .box-scrim.show             box.js:277 and 287
     .box-tray[aria-expanded]    box.js:278 and 288
     .chip[aria-pressed]         box.js:253-255
     .box-gift[data-unlocked]    box.js:164
     .box-cta[disabled]          box.js:187, php:361 and 524
     .box-slot / .box-slot-token rebuilt in box.js:105-146

   Selection has no card-level class, so the card states ride on
   :has() over .box-add.in-cart, gated behind @supports. In a
   browser without :has() the button and the tray still confirm;
   only the tile ring and the spent dimming are lost.

   One grammar decision runs through the whole file: selection
   is ink, completion is gold. The tray's filled tokens already
   border in ink (30-box.css:261), so the selected tile ring is
   ink to match, and gold appears exactly once, at the moment
   the fourth slot fills. A gold ring on every picked tile would
   spend the payoff four times before it is earned.
   ============================================================ */

/* ------------------------------------------------------------
   1 · The ADD moment
   ------------------------------------------------------------
   Observed: tapping Add flips the button ink to green under the
   thumb that is covering it, and nothing else on the tile moves.
   Scrolled back past later, a picked tile is distinguishable
   only by reading the button copy. Three answers, all driven by
   the one class the JS sets (box.js:196):

   The button confirms with a compressed pop. The colour swap to
   var(--success) is already handled by styles.css:1068 and the
   sitewide colour transition in 10-tokens.css:134, so this file
   adds only the beat the eye catches at the tile's edges when
   the centre is under a thumb.

   The artwork stamps. The media block compresses about 4% and
   settles. It only shrinks, never grows, because the cutout
   photograph covers its tile edge to edge and any upscale would
   shear the vial cap against the card's overflow clip, which is
   exactly the rendering fault 70-vial-photo.css exists to bury.
   Targeted as the media's first element child so it catches the
   .box-media-inner wrapper (functions.php:647) and the drawn
   SVG fallback (functions.php:644) alike.

   The tile keeps a persistent record: an inset ink ring plus a
   faint ink wash on the media, fading on opacity both ways. The
   fade OUT is the remove path's answer on the grid: un-picking
   releases the tile over 200ms instead of blinking.
   ------------------------------------------------------------ */
.box-grid .box-add.in-cart {
  animation: a3-add-confirm 300ms var(--ease-out);
}
@keyframes a3-add-confirm {
  0% { transform: scale(.94); }
  55% { transform: scale(1.04); }
  100% { transform: none; }
}

/* The ring lives on every media block at opacity 0 so that both
   directions of the state change are a pure opacity transition. */
.box-grid .box-card-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2; /* under .box-coa, which 99-mobile.css:412 pins at 3 */
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--ink);
  background: rgba(28, 25, 23, .05);
  opacity: 0;
  transition: opacity 200ms var(--ease-soft);
}

@supports selector(:has(*)) {
  .box-grid .box-card:has(.box-add.in-cart) .box-card-media::after {
    opacity: 1;
  }
  .box-grid .box-card:has(.box-add.in-cart) .box-card-media > :first-child {
    animation: a3-vial-stamp 420ms var(--ease-soft);
  }

  /* Once the box is full the JS spends every unpicked button
     (box.js:201). The grid should read the same way: the four
     chosen tiles hold full colour, the rest recede. Filter and
     opacity only, and it reverses the moment a slot reopens. */
  .box-grid .box-card:has(.box-add:disabled:not(.in-cart)) .box-card-media {
    opacity: .55;
    filter: saturate(.55);
  }
}

@keyframes a3-vial-stamp {
  0% { transform: scale(1); }
  38% { transform: scale(.955); }
  75% { transform: scale(1.006); }
  100% { transform: scale(1); }
}

/* The spent button itself. State styling, not motion: the fade
   between states comes from the sitewide button transition. */
.box-grid .box-add:disabled:not(.in-cart) {
  opacity: .45;
  cursor: not-allowed;
}

/* ------------------------------------------------------------
   2 · The SLOT FILL
   ------------------------------------------------------------
   Observed: .tray-token gets .filled and its innerHTML swapped
   in the same synchronous render pass (box.js:174-182), so the
   vial thumb appears the same frame the finger lifts, with no
   sense that anything travelled or landed.

   The land: the token holds invisible for 90ms, the travel time
   the eye needs to move from the tile to the tray, then drops
   in from 7px above at 112% and settles with one overshoot.
   fill-mode backwards is what holds the from-frame through the
   delay. 7px of upward start pokes past the tray's top border
   because .bottom-bar clips nothing, and that crossing is what
   sells "dropped in" rather than "appeared".

   The land animation is keyed to the class, not the element:
   render() rewrites every filled token's innerHTML on every
   change (box.js:178), so any first-render hook on the img
   would replay across all filled tokens on each add. The class
   only changes on the token whose state actually changed, so
   the animation fires exactly once per fill.

   The reopen: an emptied token replays its entrance, a short
   rise from faint, which is the remove path made visible in the
   tray. The same keyframes run once per token at page load with
   a small left-to-right stagger, so the choreography is
   established as the tray's own idiom the first time it slides
   up, not invented mid-session.
   ------------------------------------------------------------ */
.tray-token:not(.tray-free):not(.filled) {
  animation: a3-slot-open 260ms var(--ease-out) backwards;
}
/* The :not(.filled) guard keeps these delay rules from ever
   touching the land animation's own 90ms hold below, whatever
   the source order says. */
.box-tray-tokens .tray-token:nth-child(2):not(.filled):not(.tray-free) { animation-delay: 45ms; }
.box-tray-tokens .tray-token:nth-child(3):not(.filled):not(.tray-free) { animation-delay: 90ms; }
.box-tray-tokens .tray-token:nth-child(4):not(.filled):not(.tray-free) { animation-delay: 135ms; }
@keyframes a3-slot-open {
  from {
    opacity: .2;
    transform: scale(.82);
  }
}

.tray-token.filled:not(.tray-free) {
  animation: a3-token-land 340ms cubic-bezier(.22, 1.2, .36, 1) 90ms backwards;
}
@keyframes a3-token-land {
  0% {
    opacity: 0;
    transform: translateY(-7px) scale(1.12);
  }
  62% {
    opacity: 1;
    transform: translateY(1px) scale(.97);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------
   3 · COMPLETION
   ------------------------------------------------------------
   Observed: the fourth fill and the first are identical events.
   The free token flips .filled (box.js:184), the heading text
   swaps (box.js:172), and the page's entire payoff is a border
   going from dashed to solid at 30px wide.

   The sequence, timed to follow the fourth land (which settles
   at about 430ms after the tap: 90ms hold plus 340ms of land):

     380ms  the Free pill pops once, scale only, it never
            disappears so there is nothing to fade
     500ms  a single gold glint crosses the pill, clipped to
            the stadium by the token's own overflow
     560ms  a gold-foil hairline lights along the tray's top
            edge and stays lit while the box is complete
            (state, not loop: it reverts the moment a slot
            reopens because :has stops matching)

   The heading goes deep gold by state. It is a text swap the JS
   makes instantly, so colouring it instantly is coherent where
   easing it would smear a change that already happened.

   The desktop aside gets the same glint on the gift row, keyed
   to data-unlocked (box.js:164), riding on top of the 500ms
   unlock transition 30-box.css:155 already ships. And the CTA,
   which enables only now, announces itself with one small pulse
   rather than sitting there hoping to be noticed.

   Nothing loops. A lab brand earns the moment and then stands
   still.
   ------------------------------------------------------------ */
.tray-token.tray-free {
  position: relative;
}
.tray-token.tray-free.filled {
  animation: a3-free-pop 420ms var(--ease-out) 380ms;
}
@keyframes a3-free-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

/* Absolute, because .tray-token is a grid container and an
   in-flow pseudo would push the word Free into a second row. */
.tray-token.tray-free::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg,
    rgba(227, 192, 133, 0) 35%,
    rgba(227, 192, 133, .85) 50%,
    rgba(227, 192, 133, 0) 65%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}
.tray-token.tray-free.filled::before {
  animation: a3-free-glint 800ms var(--ease-soft) 500ms both;
}
@keyframes a3-free-glint {
  0% {
    background-position: 130% 0;
    opacity: 0;
  }
  25% { opacity: 1; }
  100% {
    background-position: -30% 0;
    opacity: 0;
  }
}

@supports selector(:has(*)) {
  .box-tray:has(.tray-free.filled)::after {
    content: "";
    position: absolute;
    top: -1px;
    left: 0;
    right: 0;
    height: 2px;
    pointer-events: none;
    background: var(--gold-foil);
    background-size: 220% 100%;
    animation: a3-tray-foil 1200ms var(--ease-soft) 560ms both;
  }
  .box-tray:has(.tray-free.filled) .box-tray-label strong {
    color: var(--accent-deep);
  }
}
@keyframes a3-tray-foil {
  0% {
    opacity: 0;
    background-position: 0% 0;
  }
  30% { opacity: 1; }
  100% {
    opacity: 1;
    background-position: 100% 0;
  }
}

/* Aside and sheet: the gift row glints once when it unlocks. */
.box-gift {
  position: relative;
}
.box-gift::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg,
    rgba(227, 192, 133, 0) 35%,
    rgba(227, 192, 133, .6) 50%,
    rgba(227, 192, 133, 0) 65%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}
.box-gift[data-unlocked="true"]::after {
  animation: a3-free-glint 900ms var(--ease-soft) 400ms both;
}

/* Fires when disabled is removed, which only happens at four of
   four (box.js:187). At load the PHP ships the button disabled
   (build-a-box.php:361), so this cannot fire on page load. */
.box-cta:not(:disabled) {
  animation: a3-cta-arm 340ms var(--ease-out);
}
@keyframes a3-cta-arm {
  0% { transform: scale(1); }
  50% { transform: scale(1.02); }
  100% { transform: scale(1); }
}

/* ------------------------------------------------------------
   4 · The REMOVE path in the slot lists
   ------------------------------------------------------------
   Observed: render() rebuilds every slot row from scratch on
   every change (box.js:152-157), so a removed item's row does
   not transition to empty, it is replaced by a new empty row.
   No exit animation is possible without JS changes, and any
   entrance hook on whole rows would replay across all rows on
   every render, which reads as the panel flickering.

   What is animated instead is the one element only empty rows
   contain: the numbered circle. When a removal rebuilds the
   list, the reopened slot's number breathes back in, which is
   the visible undo. The cost accepted: on an add, the numbers
   of the slots still empty replay the same soft fade, which
   reads as the remaining count acknowledging the change. Filled
   rows carry no .box-slot-token, so picked items never blink.

   Plus one affordance: the X on a filled row turns crosswise a
   quarter turn on hover, so the row telegraphs removal before
   it is committed. Its colour shift is already in 30-box:146.
   ------------------------------------------------------------ */
.box-slot-token {
  animation: a3-num-in 320ms var(--ease-soft) backwards;
}
@keyframes a3-num-in {
  from { opacity: .2; }
}

.box-slot-x {
  transition: transform 200ms var(--ease-out);
}
button.box-slot:hover .box-slot-x {
  transform: rotate(90deg);
}

/* ------------------------------------------------------------
   5 · The BOTTOM SHEET
   ------------------------------------------------------------
   Observed: the sheet already slides from its trigger's edge
   (30-box.css:330, translateY 102%), but it moves as one slab
   with everything painted on, enter and exit take the same
   300ms, and the JS unhides content 320ms after close
   (box.js:296), so a slower exit would get display-noned mid
   flight.

   Enter is retimed to 320ms on the page's decelerating curve,
   and the contents cascade up behind the slab in four steps, so
   opening reads as the tray unpacking rather than a screenshot
   arriving. Exit drops to 200ms, just over 60% of enter, which
   both matches how dismissal should feel and clears the JS
   cleanup timer with margin. The delays live only on the .open
   state, so on exit the contents drop away in 120ms with no
   stagger, the polite way to leave.

   The chevron on the tray rotates to point down while the sheet
   is up, keyed to aria-expanded (box.js:278 and 288), and dips
   under the finger on press.
   ------------------------------------------------------------ */
.box-sheet {
  transition: transform 200ms var(--ease-soft);
}
.box-sheet.open {
  transition: transform 320ms var(--ease);
}

.box-scrim {
  transition: opacity 160ms var(--ease-soft);
}
.box-scrim.show {
  transition: opacity 260ms var(--ease-soft);
}

.box-sheet .box-sheet-head,
.box-sheet .box-slot-list,
.box-sheet .box-gift,
.box-sheet .box-cta {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 120ms var(--ease-soft), transform 120ms var(--ease-soft);
}
.box-sheet.open .box-sheet-head,
.box-sheet.open .box-slot-list,
.box-sheet.open .box-gift,
.box-sheet.open .box-cta {
  opacity: 1;
  transform: none;
  transition: opacity 260ms var(--ease-out), transform 300ms var(--ease-out);
}
.box-sheet.open .box-sheet-head { transition-delay: 40ms; }
.box-sheet.open .box-slot-list { transition-delay: 90ms; }
.box-sheet.open .box-gift { transition-delay: 130ms; }
.box-sheet.open .box-cta { transition-delay: 160ms; }

.box-tray-chev {
  transition: transform 200ms var(--ease-soft);
}
.box-tray:active .box-tray-chev {
  transform: translateY(2px);
}
/* The glyph inside is already rotated to point up (30-box:298);
   turning the wrapper flips it to point down at the open sheet. */
.box-tray[aria-expanded="true"] .box-tray-chev {
  transform: rotate(180deg);
}

/* Transform is already in the sitewide button transition set
   (10-tokens.css:134), so a state alone is enough here. */
.box-sheet-close:active {
  transform: scale(.88);
}

/* ------------------------------------------------------------
   6 · FILTER CHIPS and the re-shown grid
   ------------------------------------------------------------
   Observed: filtering sets card.hidden (box.js:259), which is
   display none. Hiding cannot be animated from CSS without
   holding the dying cards in layout past the click, and letting
   both the leaving and the arriving set occupy the grid at once
   makes the reflow churn worse than the jump it would soften,
   so the hide stays instant on purpose. The reveal is where the
   eye lands, and reveals CAN be animated: a card coming back
   from display none gets a first-render frame, and
   @starting-style supplies what that frame looks like.

   The crossfade rides on the card's children rather than the
   card, because section 7 permanently owns the card's own
   opacity and transform where scroll timelines are supported,
   and an animation silences a transition on the same property.
   Even columns run 60ms behind odd, a checker stagger cheap
   enough to read as a ripple instead of a popple. The same
   frames run once at first paint, a quiet page entrance for
   browsers that will not run section 7.

   The pressed chip ticks. aria-pressed moves between chips
   (box.js:253-255), the fill swap is 15-signature.css:238, and
   the animation restarts because the selector match is new. The
   All chip ships pressed (build-a-box.php:301) so one whisper
   of a tick happens at load; at scale .94 for a quarter second
   that is below the threshold of complaint.
   ------------------------------------------------------------ */
.box-grid .box-card-media,
.box-grid .box-card-body {
  transition: opacity 260ms var(--ease-soft),
              transform 260ms var(--ease-out),
              filter 300ms var(--ease-soft);
}
.box-grid .box-card:nth-child(2n) .box-card-media,
.box-grid .box-card:nth-child(2n) .box-card-body {
  transition-delay: 60ms;
}
@starting-style {
  .box-grid .box-card-media,
  .box-grid .box-card-body {
    opacity: 0;
  }
  .box-grid .box-card-media {
    transform: translateY(6px);
  }
}

.box-filter .chip[aria-pressed="true"] {
  animation: a3-chip-tick 240ms var(--ease-out);
}
@keyframes a3-chip-tick {
  0% { transform: scale(.94); }
  60% { transform: scale(1.02); }
  100% { transform: none; }
}

/* ------------------------------------------------------------
   7 · Scroll-driven entrance
   ------------------------------------------------------------
   Tiles below the fold rise 16px and fade as they cross into
   view, scrubbed by scroll, not time. Even columns complete a
   little later than odd so rows arrive with a diagonal grain
   instead of in ranks. The range ends well before the tile is
   halfway in, so nothing sits half-faded above the tray line.

   Cost, accepted knowingly: while this animation is applied its
   fill holds the card's transform, so the .box-card:active
   press scale from 10-tokens.css:128 stops applying on this
   grid. The card surface takes no taps here, its actions are
   the Add button and the COA link, both of which keep their own
   press states, so what is lost is feedback on a dead surface.

   Cards re-shown by a filter that are already inside the range
   render at 100% progress, which is why section 6's crossfade
   lives on the children and still runs.
   ------------------------------------------------------------ */
/* The scroll-driven entrance that used to live here is gone.

   Measured on the live page at 375 after deploy: with the grid
   scrolled so that cards 3 and 4 sat at 71px from the top, fully
   inside the viewport, both were still reporting opacity 0, and
   cards 5 and 6 with them. Only the first row ever resolved. The
   view() progress was not advancing for those subjects, so the
   keyframe's `from` state was what the page rendered, and the
   `from` state was opacity 0.

   That is the one failure this grid cannot absorb. build-a-box.php
   paints the picker server-side on purpose, with its own comment
   saying it must survive every script failing, and an entrance that
   can hide the products is a worse outcome than no entrance at all.
   The other layers in this build start their reveals at opacity .25
   or higher for exactly this reason; this one started at 0.

   It is deleted rather than repaired because the repair is not worth
   what it buys. The picker is a grid of photographs the buyer came
   to look at. It should be there when they arrive. Everything else
   in this file, the add moment, the slot fill, the completion beat,
   the sheet and the filter crossfade, is untouched and all of it is
   driven by state the JS actually sets rather than by scroll. */

/* ------------------------------------------------------------
   8 · Motion off
   ------------------------------------------------------------
   Every animation and transition this file introduces, stilled.
   A media query adds no specificity, so a flat low-specificity
   list here would lose to the very state rules it is meant to
   silence; each override below therefore restates the exact
   selector of the rule it stills, and wins on source order
   alone. The :has() restatements sit in their own @supports so
   a parser without :has() cannot invalidate the plain list.

   States keep their values because nothing in this file relies
   on an animation fill to stay visible: sheet contents are
   opacity 1 under .open, the completion hairline rests at
   opacity 1 with its shimmer removed, the glints rest at 0, and
   the chevron still flips (instantly) while the sheet is open.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .box-grid .box-add.in-cart,
  .box-grid .box-card-media::after,
  .box-grid .box-card-media,
  .box-grid .box-card-body,
  .box-grid .box-card,
  .tray-token:not(.tray-free):not(.filled),
  .tray-token.filled:not(.tray-free),
  .tray-token.tray-free.filled,
  .tray-token.tray-free.filled::before,
  .box-gift[data-unlocked="true"]::after,
  .box-cta:not(:disabled),
  .box-slot-token,
  .box-slot-x,
  .box-scrim,
  .box-scrim.show,
  .box-sheet,
  .box-sheet.open,
  .box-sheet .box-sheet-head,
  .box-sheet .box-slot-list,
  .box-sheet .box-gift,
  .box-sheet .box-cta,
  .box-sheet.open .box-sheet-head,
  .box-sheet.open .box-slot-list,
  .box-sheet.open .box-gift,
  .box-sheet.open .box-cta,
  .box-tray-chev,
  .box-filter .chip[aria-pressed="true"] {
    animation: none;
    transition: none;
  }
  .box-tray:active .box-tray-chev,
  .box-sheet-close:active {
    transform: none;
  }
  @supports selector(:has(*)) {
    .box-grid .box-card:has(.box-add.in-cart) .box-card-media > :first-child,
    .box-tray:has(.tray-free.filled)::after {
      animation: none;
      transition: none;
    }
  }
}
/* ============================================================
   A4 · Motion system, scroll and chrome
   ------------------------------------------------------------
   The sitewide layer: the vocabulary the surface files share,
   the scroll-driven reveal path, the header on scroll, the cart
   badge, drawer exit timing, section rhythm, anchor offsets,
   overscroll containment and cross-document view transitions.

   Loads last: build.sh sorts parts by codepoint, digits sort
   before uppercase, so A4- lands after 99-mobile.css and wins
   every tie against the whole system. Everything in this file
   therefore only ADDS states and timing. It never restates a
   z-index, a height, a padding an earlier part fixed (the one
   padding rule in section A4.6 is a deliberate, measured
   normalisation, recorded there), and no declaration in it is flagged important.

   Division of labour with the other motion files: A1 owns
   .hero-v6, A2 owns the product card, the home rail and
   .product-grid.reveal-group. This file owns everything generic:
   bare .reveal, every .reveal-group that is not the product
   grid, and the page chrome. A surface file that needs to retune
   a specific group outranks this layer by adding one element
   type or class to its selector, exactly as A2 already does.

   Hard rules obeyed throughout: only transform, opacity, filter,
   backdrop-filter and background-position are ever animated;
   translates are Y-only so nothing can widen the page at 375px;
   every moving thing is answered in the motion-off block at the
   end of the file.
   ============================================================ */

/* ============================================================
   A4.1 · The vocabulary
   ------------------------------------------------------------
   The system already speaks with a small set of numbers; this
   block names them so the next file reaches for a word instead
   of a number. Nothing below invents a new value: each token is
   an existing, shipped duration observed in the cascade.

   Durations:
     enter  520ms  the reveal transition (styles.css:789). The
                   longest thing the site does; content arriving
                   is allowed to take its time.
     exit   340ms  roughly 65% of enter. Exits must be faster
                   than entrances: the user has already decided
                   to leave, and matching the enter duration on
                   the way out reads as the page dragging its
                   feet. Nothing shipped uses this number yet;
                   it is the pair rule, stated once.
     sheet  300ms  the drawer slide, --dur (styles.css:779,
                   used at styles.css:809 and :958).
     sheet-exit 210ms  70% of sheet, applied in A4.5 below.
     press  90ms   contact feedback, set by A2 for the card
                   press (A2-motion-cards.css:75). Press-in is
                   nearly instant; release rides the base 200ms.
     pop    360ms  a badge or counter announcing a change.
     sweep  1100ms a foil or light pass (A1's headline glint,
                   A1-motion-hero.css:347). Decorative sweeps
                   are the only thing allowed to run over a
                   second, and only ever once.
     ui     200ms  already tokenised as --dur-ui with --motion
                   (10-tokens.css:34); listed for completeness.

   Easings, all shipped, none new:
     --ease      cubic-bezier(.32,.72,0,1)  weight settling,
                 sheets and reveals (styles.css:778).
     --ease-out  cubic-bezier(.22,1,.36,1)  entrances and
                 lifts (styles.css:1366).
     --ease-soft cubic-bezier(.4,0,.2,1)    state changes, the
                 4,966-use pairing (styles.css:1367,
                 10-tokens.css:31).

   No spring, no bounce, anywhere: the brand brief is weight and
   glass, and glass does not overshoot by more than a hair.
   ============================================================ */
:root {
  --a4-enter: 520ms;
  --a4-exit: 340ms;
  --a4-sheet: var(--dur);
  --a4-sheet-exit: 210ms;
  --a4-press: 90ms;
  --a4-pop: 360ms;
  --a4-sweep: 1100ms;
}

/* ============================================================
   A4.2 · Scroll-driven reveals
   ------------------------------------------------------------
   The shipped system is three cooperating pieces:

     styles.css:789 and :1663   the .reveal / .reveal-group > *
                                transitions, waiting at opacity
                                0 and translateY(14 or 16px);
     styles.css:2062-2067       the waiting state gated behind
                                html.js so failed JS can never
                                blank the page (header.php:27
                                sets the class inline);
     main.js:980-1007           the IntersectionObserver adding
                                .in, plus a no-observer fallback
                                at main.js:984 and a 3 second
                                failsafe at main.js:995.

   Where the browser has scroll-driven animations, this section
   replaces time with position: an element animates by where it
   stands in the viewport, so a mid-page refresh renders every
   visible element already settled, entrance progress tracks the
   finger exactly, and nothing ever waits on an observer.

   How the two paths avoid double-animating: inside the
   @supports gate the JS waiting state is neutralised (restated
   at equal or higher specificity; this file loads after
   styles.css:2066-2067, so the tie resolves here). From then on
   the .in class that main.js keeps adding is visually a no-op:
   the transitioned properties' underlying values never change,
   so the transition has no delta to run, and the scroll
   animation alone owns the entrance. Outside the gate nothing
   here applies and the shipped JS path runs exactly as before.

   Two inherited techniques, both load-bearing, both from A2
   (A2-motion-cards.css:277-283): the "to" keyframe omits
   transform, so at full progress the animation stops holding it
   and hover or press transforms on the same element keep
   working (a scroll animation never "ends"); and the entrance
   starts at opacity .25, never 0, so even a frame frozen by a
   stalled timeline is readable. The group timeline below lives
   on the group element, which always sits in the root scroller,
   so children of the two horizontal rails that carry
   reveal-group (.rel-rail, 40-plp-home.css:1081; .ru-rail,
   60-chrome.css:685) are driven by their rail's vertical
   position and never by the rail's own inline scroller. That is
   what keeps every timeline in this section provably active.

   Ranges are percentages of entry, not time delays, because
   time delays do not apply to progress-based timelines. Entry
   is safe for any element height: for a subject taller than the
   viewport, entry 100% resolves at contain 0%, the moment the
   subject's top reaches the top of the screen, so the range
   always completes within one screen of scrolling.

   .product-grid.reveal-group is excluded everywhere: A2 already
   drives it per-card (A2-motion-cards.css:315-333) and its own
   neutraliser covers it.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* The waiting states, retired on this path. */
    .js .reveal:not(.in) {
      opacity: 1;
      transform: none;
    }
    .js .reveal-group:not(.product-grid):not(.in) > * {
      opacity: 1;
      transform: none;
    }

    /* Single reveals ride their own position. */
    .reveal {
      animation: a4-rise linear both;
      animation-timeline: view(block);
      animation-range: entry 0% entry 60%;
    }

    /* Groups share one timeline, the group's own, and stagger
       by range offset: each child starts 8% of entry later and
       finishes on the same slope. The cap at nth-child(6)
       matches the JS path's cap so both paths read as the same
       choreography, and no child in a long list is ever left
       waiting: by entry 90% the whole set is settled. */
    .reveal-group:not(.product-grid) {
      view-timeline-name: --a4-grp;
      view-timeline-axis: block;
    }
    .reveal-group:not(.product-grid) > * {
      animation: a4-rise linear both;
      animation-timeline: --a4-grp;
      animation-range: entry 0% entry 50%;
    }
    .reveal-group:not(.product-grid) > :nth-child(2) {
      animation-range: entry 8% entry 58%;
    }
    .reveal-group:not(.product-grid) > :nth-child(3) {
      animation-range: entry 16% entry 66%;
    }
    .reveal-group:not(.product-grid) > :nth-child(4) {
      animation-range: entry 24% entry 74%;
    }
    .reveal-group:not(.product-grid) > :nth-child(5) {
      animation-range: entry 32% entry 82%;
    }
    .reveal-group:not(.product-grid) > :nth-child(n + 6) {
      animation-range: entry 40% entry 90%;
    }
  }
}
@keyframes a4-rise {
  from { opacity: .25; transform: translateY(14px); }
  to   { opacity: 1; }
}

/* ------------------------------------------------------------
   A4.2b · The JS path's stagger, capped
   ------------------------------------------------------------
   styles.css:1665-1670 delays group children at 70ms steps and
   stops writing delays at nth-child(6), so in any group longer
   than six the seventh child inherits 0ms and fires BEFORE
   children two through six. A2 recorded the fault for the shop
   grid (A2-motion-cards.css:255-263) and fixed it there; the
   same fault is live for every other long group, the partner
   check-list (inc/partner.php:462) being the clearest case.
   One rule makes the order monotonic sitewide: everything from
   seven on shares the cap the sixth child already set. Equal
   specificity with the base rules, later in the cascade, so it
   wins; A2's grid delays are more specific and keep their own
   tuning.
   ------------------------------------------------------------ */
.reveal-group.in > :nth-child(n + 7) {
  transition-delay: 350ms;
}

/* ============================================================
   A4.3 · The header earns its shadow
   ------------------------------------------------------------
   60-chrome.css:42 designed a scrolled state for the header, a
   soft separation shadow under .site-header.is-scrolled, and
   its comment assumes "chrome.js toggles the class". No such
   file exists: the theme ships main.js, cart.js, box.js, pdp.js
   and checkout.js, and none of them writes is-scrolled, so the
   shadow has never fired. Verified by grepping every asset.

   Rebuilt here in pure CSS. A pseudo-element carries the exact
   shadow the design intended, at opacity 0, and a scroll()
   timeline fades it in across the first hundred pixels of
   scroll, which is about the distance the 89px promo band takes
   to pass underneath (99-mobile.css:188 measured the band at
   375px; the verified mobile pass leaves it at 89px). Only
   opacity animates; the shadow itself is a static declaration.
   The pseudo sits at z-index -1 inside the header's own
   stacking context (styles.css:96 makes the header sticky at
   z-index 50), so it paints above the page but under the
   header's controls, and pointer-events: none keeps it inert.

   At rest the range starts at 8px, not 0, so rubber-band
   overscroll at the top of the page cannot flicker it.

   If a chrome.js ever appears and starts toggling is-scrolled,
   the two shadows would stack; whoever adds it should delete
   this block in the same change.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .site-header::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      box-shadow: 0 1px 0 rgba(28, 25, 23, .03), 0 4px 16px rgba(28, 25, 23, .07);
      opacity: 0;
      animation: a4-header-shade linear both;
      animation-timeline: scroll(root block);
      animation-range: 8px 96px;
    }
  }
}
@keyframes a4-header-shade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ============================================================
   A4.4 · The cart badge lands with weight
   ------------------------------------------------------------
   The badge is #cartCount.cart-count (header.php:115), hidden
   until it has something to say: styles.css:130 sets display:
   none and styles.css:139 flips it to flex under .has-items.
   Two writers toggle that class: main.js:295 on load, from the
   halotir_cart_count cookie, and main.js:476 after any cart
   mutation. classList.toggle with a force argument only mutates
   when the state actually changes, so this animation fires
   exactly when the badge goes from absent to present: first
   item added, and on landing with a non-empty cart. A count
   moving 2 to 3 repaints textContent only, which CSS cannot
   see, and re-popping on every quantity tick would read as
   nagging anyway; the drawer already confirms those.

   The scale settles through 1.08, the same restraint as A2's
   quick-add settle (A2-motion-cards.css:241, peak 1.015 from a
   pressed start): a hair of overshoot to feel physical, nothing
   that reads as bounce. Ends on scale(1) with no fill so the
   underlying state is exactly the shipped one.
   ============================================================ */
.cart-count.has-items {
  animation: a4-badge-pop var(--a4-pop) var(--ease-out);
}
@keyframes a4-badge-pop {
  0%   { opacity: 0; transform: scale(.55); }
  70%  { opacity: 1; transform: scale(1.08); }
  100% { transform: scale(1); }
}

/* ============================================================
   A4.5 · Sheets leave faster than they arrive
   ------------------------------------------------------------
   Both drawers slide on transform over --dur 300ms
   (styles.css:809 cart, :958 nav) in BOTH directions, and a
   symmetric close is the single clearest "the UI is slow" tell:
   opening is an arrival and may take its time, closing is the
   user asking to have their page back. Transitions read their
   timing from the destination state, so putting a shorter
   duration on the closed state changes only the close. 210ms is
   70% of the open, the exit rule from A4.1 applied.

   main.js:183-188 keeps the panel in the tree for 320ms after
   removing .open before setting hidden; a 210ms slide is long
   gone by then, so the earlier finish costs nothing. The
   backdrop already runs 180ms (styles.css:796) and needs no
   help.
   ============================================================ */
.cart-drawer:not(.open),
.nav-drawer:not(.open) {
  transition-duration: var(--a4-sheet-exit);
}

/* ============================================================
   A4.6 · Section rhythm at 375
   ------------------------------------------------------------
   Read from the cascade, mobile values, top padding then
   bottom:

     .hero-v6        44 / 52   40-plp-home.css:636
     .section-v6     44 / 44   styles.css:1646
     .hfaq-section   44 / 48   40-plp-home.css:817
     .rel-section    44 / 48   40-plp-home.css:1066
     .ofact-section  44 / 48   40-plp-home.css:1171
     .ru-section     44 / 52   60-chrome.css:642

   The top edge is unanimous at 44px, which is why the page
   already scans as one system on the way down. The bottoms run
   48 to 52 except .section-v6 at 44, the one section class that
   appears between the others on the home page (front-page.php
   156, 302, 359), so the home page alternates 44 and 48 gaps
   for no reason a reader could name. Normalised to the 48px the
   three purpose-built sections agreed on. Scoped to the phone
   breakpoint: at 1024px and up styles.css:1704 sets 68px and
   that desktop decision is left exactly alone.
   ============================================================ */
@media (max-width: 767px) {
  .section-v6 {
    padding-bottom: 48px;
  }
}

/* ============================================================
   A4.7 · Anchors land clear of the header
   ------------------------------------------------------------
   The chrome above a page at load is 154px at 375px: the 65px
   sticky header plus the 89px promo band. Only the header
   STAYS: grep across styles.css and every part shows no sticky
   or fixed rule for .promo-bar or .announce, so by the time an
   anchor jump lands, the band has scrolled away and the header
   is the only thing an anchor can hide under.

   Coverage before this rule: 60-chrome.css:335 offsets
   .faq-cat and .prose h2, which carries every doc-toc target
   (doc-nav.php:150 builds the list from .prose h2 ids,
   page.php:97 applies .prose). What it misses is every other
   id on the site, and at least one is a live in-page link
   today: the PDP's "Request COA" button (functions.php:1140)
   jumps to id="certificate" (functions.php:1238), a bare .wrap
   with no offset, so the certificate panel lands with its head
   under the header. An attribute selector closes the class of
   bug rather than the instance: scroll-margin only ever acts
   during anchor and :target scrolling, it cannot move layout,
   and no theme script calls scrollIntoView (verified across
   every script in assets; checkout.js:112 computes its own scrollTo top),
   so a blanket rule is safe. At (0,1,0) it loses to the tuned
   .prose h2 rule on specificity, which keeps the shipped doc
   pages exactly as verified.

   16px of breathing room past --header-h (64px, styles.css:42;
   the rendered 65px includes the 1px border) puts a jumped-to
   heading 15px clear of the hairline.
   ============================================================ */
[id] {
  scroll-margin-top: calc(var(--header-h) + 16px);
}

/* ============================================================
   A4.8 · Scroll stays where the finger put it
   ------------------------------------------------------------
   Nested scrollers, both axes. Vertically: reaching the end of
   the nav drawer's link list (.nd-links, styles.css:964), the
   cart drawer body (.cd-body, styles.css:828) or the age gate
   (.age-gate, built at main.js:83,
   scrolling per styles.css:629) must not hand the remaining
   momentum to the page behind the scrim; body scrolling is
   locked while a drawer is open (styles.css:800) but the gate
   locks via its own class and iOS chains enthusiastically
   either way. Horizontally: the four full-bleed snap rails
   named at 10-tokens.css:107-112 plus the two rail-shaped
   reveal groups (.rel-rail, 40-plp-home.css:1081; .ru-rail,
   60-chrome.css:685) should swallow the end-of-rail bounce
   rather than let a hard flick start navigating the page.
   Contain, not none: the rubber-band inside the scroller is
   kept, only the chaining is cut.
   ============================================================ */
.nd-links,
.cd-body,
.age-gate {
  overscroll-behavior-y: contain;
}
.rail,
.chip-row,
.cat-pills,
.hero-trust,
.rel-rail,
.ru-rail {
  overscroll-behavior-x: contain;
}

/* ============================================================
   A4.9 · Cross-document view transitions
   ------------------------------------------------------------
   Judged safe here, enabled, and the judgement is recorded:

   LiteSpeed's public page cache is a non-issue: the transition
   is composed entirely on the client between two same-origin
   documents, cached or not, and both documents carry this
   stylesheet, which is the only opt-in the feature needs. The
   fixed chrome is the real question. The header is sticky and
   identical on every page, so it gets a view-transition-name:
   snapshotted as its own group, it holds perfectly still while
   the root crossfades beneath it, which is precisely the
   app-like read a multi-page store wants. The promo band and
   the announce strip are NOT named: header.php renders one or
   the other from a sitewide option so consecutive pages always
   agree, both sit in normal flow at the top of the root
   snapshot, and identical pixels crossfading into identical
   pixels is indistinguishable from stillness. Naming them would
   buy nothing and costs a failure mode: a duplicate
   view-transition-name on one page skips the transition
   entirely.

   Checkout is the one asymmetric hop: header.php:42 swaps the
   whole chrome for the focus header, so site-header exists on
   one side only and the header simply crossfades away with the
   root group. Acceptable, and arguably correct for entering a
   focused flow.

   The crossfade runs at --dur-ui, the system's 200ms, rather
   than the UA default: page-to-page should feel like a state
   change, not a scene change. The whole feature sits inside
   the reduced-motion query, so those users keep instant
   navigations; browsers without the at-rule ignore all of it.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
  .site-header {
    view-transition-name: site-header;
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: var(--dur-ui);
  }
  ::view-transition-group(site-header) {
    animation-duration: var(--dur-ui);
  }
}

/* ============================================================
   A4.10 · Motion off
   ------------------------------------------------------------
   Everything this file animates, answered, plus one repair the
   system has needed since v6. The base sheet's global nuke
   (styles.css:746) collapses every duration to .01ms, but it
   cannot stop scroll-driven animations, whose progress comes
   from position, not time, so A4.2 and A4.3 are switched off
   by the media queries wrapping them AND re-answered here as
   belt and braces.

   The repair: under reduced motion the base guards at
   styles.css:791 and :1671 try to force reveals visible, but
   both are outspecified by the .js waiting states at
   styles.css:2066-2067, so a reduced-motion user still gets
   content that is INVISIBLE until the observer notices it,
   flipping in 8% above the fold's edge. A2 fixed this for the
   shop grid only (A2-motion-cards.css:421 onward, its own
   comment records the same diagnosis). Restating the
   neutralisers here at winning specificity settles it
   sitewide: for reduced motion the content is simply present,
   observer or no observer. The delay reset is then strictly
   redundant, and kept anyway so no future change to the
   waiting states can quietly bring back a 350ms ripple of
   .01ms flickers.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal-group > * {
    animation: none;
  }
  .js .reveal:not(.in) {
    opacity: 1;
    transform: none;
  }
  .js .reveal-group:not(.in) > * {
    opacity: 1;
    transform: none;
  }
  .reveal-group.in > :nth-child(n) {
    transition-delay: 0s;
  }
  .site-header::after {
    animation: none;
  }
  .cart-count.has-items {
    animation: none;
  }
}
/* ============================================================
   v7 · A5 · Decorative graphics system
   ------------------------------------------------------------
   The pages are typographically strong but the panels between
   sections are bare flat colour, and nothing on the site says
   "lab" except the product photography. This file gives the
   brand a small visual vocabulary. Five marks, used quietly:

     1. CHROMATOGRAM TRACE. The HPLC output that backs the
        purity claim, drawn as a fine line with one dominant
        peak. A full-width strip-chart feed runs along the foot
        of the dark verification panel, and a 64px mini trace
        signs off every v6 section head, the way an instrument
        signs a report.
     2. PURITY ARC. A ring gauge closed to within a few degrees,
        so it reads as "all but complete" without printing a
        measured number the copy does not print.
     3. MOLECULAR LATTICE. A diamond mesh with hollow nodes,
        very fine stroke, used as a field behind hero and panel
        surfaces, never as an illustration.
     4. LOT GRID. Rows of small ledger marks, ticks, dots and
        the odd filled square, suggesting documented lots. Ambient
        texture for the flat tinted bands.
     5. GRAIN. feTurbulence film grain so large flat panels stop
        banding and stop reading as raw hex fill. Foil-toned dust
        on dark panels, ink-toned on light bands.

   Every mark is an inline data: URI, deliberately. build.sh has
   no sync step for images/deco/ (it copies only the wordmark,
   the icon set and the vial cutouts), and the concatenated parts
   are also served as css/v7.css on the static reference site,
   where a ../images/deco/ path would resolve to a directory that
   does not exist there. Inlining removes both failure modes. The
   editable sources live in woocommerce/halotir-theme/images/deco/
   and each URI below names the source it was encoded from.

   Recolouring: the line marks are applied as CSS masks (the same
   technique the wordmark already uses), so one URI serves every
   surface and the colour comes from background-color, which can
   be a token or a gradient fade. Only the two grain tiles carry
   baked colour, because noise needs its own alpha ramp. Literal
   colours used inside URIs and gradients, and the token each one
   is: #1c1917 is --ink, #a16207 is --accent, #e3c085 is the mid
   stop of --gold-foil.

   Contrast, computed (WCAG relative luminance, not eyeballed):
     white text over the trace at .22 on the light end of the
       verify gradient: 9.39:1
     white text over the arc at .18 on ink: 11.67:1
     footer #a8a29e over foil grain at .05 on ink: 6.32:1
     --ink-2 over the lot field at .05 on --tint: 6.63:1
     --muted over the lot field at .03 on white: 4.52:1
   The page-head field is capped at .03 for exactly that last
   pairing; at .04 it computes 4.44 and fails.

   What this file trades away: no glass-caustic wash (a sixth
   motif would dilute the five that earn their place), nothing on
   the home hero (it already carries halo, collage and vial
   layers from A1), and no decoration on cart, checkout or the
   auth head, which are working chrome above forms.

   Load order: A5 sorts after 99-mobile.css, so ties resolve here.
   Stacking pattern used throughout: the wrapper gets
   isolation: isolate, the pseudo gets z-index: -1, which places
   the mark above the wrapper's own background and below all of
   its in-flow content without touching global z-index. Where the
   base already lifts children (.verify-panel > *, .promo-panel
   > *), the pseudo stays at z-index auto and no isolation is
   needed. Every decorative layer is pointer-events: none.
   ============================================================ */

/* ---------- 1a. The section-head signature ----------
   A 64px chromatogram tick under every v6 section head. One
   dominant peak, two shoulders, drawn in accent gold. This is
   the systemic mark: it appears wherever a section introduces
   itself, so the lab motif travels with the type instead of
   living on one page. Decoration only; the head says what the
   section is, the trace just signs it.

   Attached to markup that exists:
     front-page.php:159, front-page.php:304
     inc/home-proof.php:86, inc/home-proof.php:186
     inc/partner.php:362, 401, 429, 458, 502
     inc/research.php:87, inc/research.php:114
     inc/build-a-box.php:293, inc/build-a-box.php:409 */
.section-head-v6::after {
  content: "";
  display: block;
  width: 64px;
  height: 14px;
  margin-top: 10px;
  background-color: var(--accent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2064%2014'%20width='64'%20height='14'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M1%2012%20L14%2012%20Q17%2012%2018.5%209.5%20Q20%207%2021.5%209.5%20Q23%2012%2026%2012%20L34%2012%20Q37%2012%2038.5%203.5%20Q40%201.4%2041.5%203.5%20Q43%2012%2046%2012%20L52%2012%20Q54%2012%2055.5%2010.5%20Q57%209%2058.5%2010.5%20Q60%2012%2063%2012'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2064%2014'%20width='64'%20height='14'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M1%2012%20L14%2012%20Q17%2012%2018.5%209.5%20Q20%207%2021.5%209.5%20Q23%2012%2026%2012%20L34%2012%20Q37%2012%2038.5%203.5%20Q40%201.4%2041.5%203.5%20Q43%2012%2046%2012%20L52%2012%20Q54%2012%2055.5%2010.5%20Q57%209%2058.5%2010.5%20Q60%2012%2063%2012'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 64px 14px;
  mask-size: 64px 14px;
  opacity: .9;
  pointer-events: none;
}

/* ---------- 1b. The strip-chart feed ----------
   The full trace, on the one panel that talks about testing:
   the dark "every lot is verified" panel on the research page
   (inc/research.php:54). It runs along the panel foot as a
   repeating 480px tile and creeps left like chart paper coming
   off an instrument, 480px every 64 seconds, slow enough to
   read as an idle recorder rather than as a marquee. The panel
   already has position: relative and overflow: hidden
   (styles.css:1849) and lifts its children to z-index 1
   (styles.css:1859), so the pseudo needs no z-index of its own.
   Transform only, so the loop composites; the tile starts and
   ends flat on the baseline, so the repeat has no seam. */
.verify-panel::after {
  content: "";
  position: absolute;
  left: -480px;
  bottom: 4px;
  width: calc(100% + 960px);
  height: 96px;
  background-color: #e3c085;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20480%2096'%20width='480'%20height='96'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M0%2084%20L34%2084%20Q40%2084%2043%2080%20Q46%2076%2049%2080%20Q52%2084%2058%2084%20L96%2084%20Q104%2084%20108%2072%20Q112%2060%20117%2060%20Q122%2060%20126%2072%20Q130%2084%20138%2084%20L166%2084%20Q172%2084%20175%2078%20Q178%2072%20181%2078%20Q184%2084%20190%2084%20L216%2084%20Q224%2084%20228%2046%20Q231%2012%20236%2010%20Q241%2012%20244%2046%20Q248%2084%20256%2084%20L292%2084%20Q300%2084%20304%2064%20Q308%2046%20313%2046%20Q318%2046%20322%2064%20Q326%2084%20334%2084%20L372%2084%20Q378%2084%20381%2079%20Q384%2074%20387%2079%20Q390%2084%20396%2084%20L432%2084%20Q438%2084%20441%2081%20Q444%2078%20447%2081%20Q450%2084%20456%2084%20L480%2084'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20480%2096'%20width='480'%20height='96'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M0%2084%20L34%2084%20Q40%2084%2043%2080%20Q46%2076%2049%2080%20Q52%2084%2058%2084%20L96%2084%20Q104%2084%20108%2072%20Q112%2060%20117%2060%20Q122%2060%20126%2072%20Q130%2084%20138%2084%20L166%2084%20Q172%2084%20175%2078%20Q178%2072%20181%2078%20Q184%2084%20190%2084%20L216%2084%20Q224%2084%20228%2046%20Q231%2012%20236%2010%20Q241%2012%20244%2046%20Q248%2084%20256%2084%20L292%2084%20Q300%2084%20304%2064%20Q308%2046%20313%2046%20Q318%2046%20322%2064%20Q326%2084%20334%2084%20L372%2084%20Q378%2084%20381%2079%20Q384%2074%20387%2079%20Q390%2084%20396%2084%20L432%2084%20Q438%2084%20441%2081%20Q444%2078%20447%2081%20Q450%2084%20456%2084%20L480%2084'/%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  -webkit-mask-size: 480px 96px;
  mask-size: 480px 96px;
  -webkit-mask-position: 0 bottom;
  mask-position: 0 bottom;
  opacity: .22;
  pointer-events: none;
  will-change: transform;
  animation: a5-chart-feed 64s linear infinite;
}
@keyframes a5-chart-feed {
  to { transform: translateX(-480px); }
}

/* ---------- 2. The purity arc ----------
   A ring gauge stroked through all but 18 degrees, with a dot
   terminator where the stroke stops. It reads as "nearly the
   whole circle" without printing a figure, which matters: the
   panel's own copy carries the real numbers, and this mark must
   never claim one. Placed once, on the partner page's dark
   proof card (inc/partner.php:367), the panel whose whole job
   is a single hero figure; the arc haloes that figure from the
   top-right corner. The card has position: relative and
   overflow: hidden but does not lift its children
   (15-signature.css), so it gets the isolation pattern. Its
   existing ::before is the foil top rule; the arc takes the
   free ::after. */
.proof-card {
  isolation: isolate;
}
.proof-card::after {
  content: "";
  position: absolute;
  top: -46px;
  right: -58px;
  width: 230px;
  height: 230px;
  z-index: -1;
  background-color: #e3c085;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20240%20240'%20width='240'%20height='240'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='3'%20stroke-linecap='round'%20d='M129%2016.4%20A104%20104%200%201%201%2096.6%2018.7'/%3E%3Ccircle%20cx='129'%20cy='16.4'%20r='4'%20fill='currentColor'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20240%20240'%20width='240'%20height='240'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='3'%20stroke-linecap='round'%20d='M129%2016.4%20A104%20104%200%201%201%2096.6%2018.7'/%3E%3Ccircle%20cx='129'%20cy='16.4'%20r='4'%20fill='currentColor'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  opacity: .18;
  transform: rotate(14deg);
  pointer-events: none;
}

/* ---------- 3. The molecular lattice ----------
   A 72px diamond mesh with hollow nodes at the intersections.
   One inscribed diamond per tile is all it takes: tiled, the
   edges join into a continuous diagonal lattice and the corner
   nodes complete each other across tile seams. It is a field,
   not a picture, so it always arrives through a gradient fade
   rather than at full strength, and the fade IS the colour
   layer: the pattern comes from the mask, the ink and its decay
   come from the background gradient underneath it. */

/* Partner hero (inc/partner.php:317, full bleed, bare gradient
   ground from styles.css:1100). Lattice falls from the top of
   the band and is gone before the stat strip. */
.partner-hero {
  position: relative;
  isolation: isolate;
}
.partner-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: linear-gradient(200deg, rgba(28, 25, 23, .06), rgba(28, 25, 23, 0) 62%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 72px 72px;
  mask-size: 72px 72px;
  pointer-events: none;
}

/* Light promo panels (front-page.php:452, inc/partner.php:569).
   The sage panel is the flattest surface on the front page; the
   dark twin beside it already carries the vial rain. Children
   are lifted to z-index 2 by styles.css:1631, so the pseudo
   stays at auto. */
.promo-panel.p-light::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(235deg, rgba(28, 25, 23, .07), rgba(28, 25, 23, 0) 58%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 72px 72px;
  mask-size: 72px 72px;
  pointer-events: none;
}

/* Footer (footer.php:30). The largest dark slab on every page.
   Foil-toned lattice falling from under the gold border-image,
   spent before the link columns, so link text never sits on it
   at more than a whisper. Computed pairing above: 6.32:1 even
   if it did. */
.site-footer {
  position: relative;
  isolation: isolate;
}
.site-footer::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 220px;
  z-index: -1;
  background-image: linear-gradient(180deg, rgba(227, 192, 133, .07), rgba(227, 192, 133, 0) 85%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2072%2072'%20width='72'%20height='72'%3E%3Cpath%20fill='none'%20stroke='currentColor'%20stroke-width='1'%20d='M36%200%20L72%2036%20L36%2072%20L0%2036%20Z'/%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Ccircle%20cx='36'%20cy='0'%20r='2.6'/%3E%3Ccircle%20cx='72'%20cy='36'%20r='2.6'/%3E%3Ccircle%20cx='36'%20cy='72'%20r='2.6'/%3E%3Ccircle%20cx='0'%20cy='36'%20r='2.6'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 72px 72px;
  mask-size: 72px 72px;
  pointer-events: none;
}

/* ---------- 4. The lot grid ----------
   A 96px ledger tile: short rules, dots, an outlined square, a
   filled one. Rows of documented lots, abstracted until it is
   texture. Nothing in it means anything, which is the point;
   the moment a mark like this looks like data, somebody reads
   it. */

/* Shortcode page heads: the research library head
   (inc/research.php:49) and the build-a-box head
   (inc/build-a-box.php:259). Both render inside page.php's
   .wrap, unlike the cart and checkout heads, which are sections
   CONTAINING a .wrap, so `.wrap .page-head` selects exactly the
   two decorated heads and leaves working chrome alone. The auth
   head (inc/phone-auth.php:466) also lands inside a wrap and is
   excluded by class. Capped at .03 ink for the --muted pairing
   computed in the header. */
.wrap .page-head:not(.auth-head) {
  position: relative;
  isolation: isolate;
}
.wrap .page-head:not(.auth-head)::after {
  content: "";
  position: absolute;
  top: 10px;
  right: 0;
  bottom: 10px;
  width: min(46%, 300px);
  z-index: -1;
  background-image: linear-gradient(250deg, rgba(28, 25, 23, .03), rgba(28, 25, 23, 0) 78%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2048'%20width='96'%20height='48'%3E%3Cg%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%3E%3Cpath%20d='M18%209.5%20H26'/%3E%3Cpath%20d='M30%209.5%20H34'/%3E%3Cpath%20d='M52%209.5%20H62'/%3E%3Cpath%20d='M84%209.5%20H90'/%3E%3Cpath%20d='M16%2033.5%20H28'/%3E%3Cpath%20d='M48%2033.5%20H54'/%3E%3Cpath%20d='M60%2033.5%20H70'/%3E%3Cpath%20d='M86%2033.5%20H91'/%3E%3C/g%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Crect%20x='4'%20y='7'%20width='5'%20height='5'/%3E%3Crect%20x='36'%20y='31'%20width='5'%20height='5'/%3E%3C/g%3E%3Crect%20x='70'%20y='7'%20width='5'%20height='5'%20fill='currentColor'/%3E%3Cg%20fill='currentColor'%3E%3Ccircle%20cx='44'%20cy='9.5'%20r='1.8'/%3E%3Ccircle%20cx='8'%20cy='33.5'%20r='1.8'/%3E%3Ccircle%20cx='80'%20cy='33.5'%20r='1.8'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2048'%20width='96'%20height='48'%3E%3Cg%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%3E%3Cpath%20d='M18%209.5%20H26'/%3E%3Cpath%20d='M30%209.5%20H34'/%3E%3Cpath%20d='M52%209.5%20H62'/%3E%3Cpath%20d='M84%209.5%20H90'/%3E%3Cpath%20d='M16%2033.5%20H28'/%3E%3Cpath%20d='M48%2033.5%20H54'/%3E%3Cpath%20d='M60%2033.5%20H70'/%3E%3Cpath%20d='M86%2033.5%20H91'/%3E%3C/g%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Crect%20x='4'%20y='7'%20width='5'%20height='5'/%3E%3Crect%20x='36'%20y='31'%20width='5'%20height='5'/%3E%3C/g%3E%3Crect%20x='70'%20y='7'%20width='5'%20height='5'%20fill='currentColor'/%3E%3Cg%20fill='currentColor'%3E%3Ccircle%20cx='44'%20cy='9.5'%20r='1.8'/%3E%3Ccircle%20cx='8'%20cy='33.5'%20r='1.8'/%3E%3Ccircle%20cx='80'%20cy='33.5'%20r='1.8'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 96px 48px;
  mask-size: 96px 48px;
  pointer-events: none;
}

/* Tinted v6 bands: front-page.php:156, front-page.php:359,
   inc/home-proof.php:83, inc/partner.php:399. These are the big
   flat panels the brief calls bare. The field hangs from the
   top of the band and is finished by half height, so the cards
   and rails lower down sit on clean tint. At .05 over --tint
   the --ink-2 pairing computes 6.63:1. */
.section-v6.ground-tint {
  position: relative;
  isolation: isolate;
}
.section-v6.ground-tint::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: linear-gradient(180deg, rgba(28, 25, 23, .05), rgba(28, 25, 23, 0) 52%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2048'%20width='96'%20height='48'%3E%3Cg%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%3E%3Cpath%20d='M18%209.5%20H26'/%3E%3Cpath%20d='M30%209.5%20H34'/%3E%3Cpath%20d='M52%209.5%20H62'/%3E%3Cpath%20d='M84%209.5%20H90'/%3E%3Cpath%20d='M16%2033.5%20H28'/%3E%3Cpath%20d='M48%2033.5%20H54'/%3E%3Cpath%20d='M60%2033.5%20H70'/%3E%3Cpath%20d='M86%2033.5%20H91'/%3E%3C/g%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Crect%20x='4'%20y='7'%20width='5'%20height='5'/%3E%3Crect%20x='36'%20y='31'%20width='5'%20height='5'/%3E%3C/g%3E%3Crect%20x='70'%20y='7'%20width='5'%20height='5'%20fill='currentColor'/%3E%3Cg%20fill='currentColor'%3E%3Ccircle%20cx='44'%20cy='9.5'%20r='1.8'/%3E%3Ccircle%20cx='8'%20cy='33.5'%20r='1.8'/%3E%3Ccircle%20cx='80'%20cy='33.5'%20r='1.8'/%3E%3C/g%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2048'%20width='96'%20height='48'%3E%3Cg%20stroke='currentColor'%20stroke-width='1.5'%20stroke-linecap='round'%3E%3Cpath%20d='M18%209.5%20H26'/%3E%3Cpath%20d='M30%209.5%20H34'/%3E%3Cpath%20d='M52%209.5%20H62'/%3E%3Cpath%20d='M84%209.5%20H90'/%3E%3Cpath%20d='M16%2033.5%20H28'/%3E%3Cpath%20d='M48%2033.5%20H54'/%3E%3Cpath%20d='M60%2033.5%20H70'/%3E%3Cpath%20d='M86%2033.5%20H91'/%3E%3C/g%3E%3Cg%20fill='none'%20stroke='currentColor'%20stroke-width='1'%3E%3Crect%20x='4'%20y='7'%20width='5'%20height='5'/%3E%3Crect%20x='36'%20y='31'%20width='5'%20height='5'/%3E%3C/g%3E%3Crect%20x='70'%20y='7'%20width='5'%20height='5'%20fill='currentColor'/%3E%3Cg%20fill='currentColor'%3E%3Ccircle%20cx='44'%20cy='9.5'%20r='1.8'/%3E%3Ccircle%20cx='8'%20cy='33.5'%20r='1.8'/%3E%3Ccircle%20cx='80'%20cy='33.5'%20r='1.8'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-size: 96px 48px;
  mask-size: 96px 48px;
  pointer-events: none;
}

/* ---------- 5. Grain ----------
   160px feTurbulence tiles. On the dark panels the dust is
   foil-toned at 8 percent peak alpha, which under the ink reads
   as material rather than colour; on the tinted bands it is
   ink-toned at 5 percent. Grain rides as an extra
   background-image layer on the element itself, under every
   masked pseudo. Where the base paints its ground with a
   `background` shorthand gradient, that gradient is restated
   here as the bottom layer, because the longhand replaces the
   whole image stack; the sources are cited so a base change has
   a trail to follow. Where the ground is a plain colour the
   longhand adds the tile without touching it. */

/* Ground was: styles.css:1851, linear-gradient(155deg, #1c1917
   0%, #2b2118 100%). */
.verify-panel {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='160'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20values='0%200%200%200%200.89%200%200%200%200%200.753%200%200%200%200%200.522%200%200%200%200.08%200'/%3E%3C/filter%3E%3Crect%20width='160'%20height='160'%20filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(155deg, #1c1917 0%, #2b2118 100%);
  background-repeat: repeat, no-repeat;
}

/* Ground was: styles.css:1627, linear-gradient(150deg, #241c14,
   #3b2a15). Grain sits under the vial rain, which styles.css
   keeps at z-index 1. */
.promo-panel.p-dark {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='160'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20values='0%200%200%200%200.89%200%200%200%200%200.753%200%200%200%200%200.522%200%200%200%200.08%200'/%3E%3C/filter%3E%3Crect%20width='160'%20height='160'%20filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(150deg, #241c14, #3b2a15);
  background-repeat: repeat, no-repeat;
}

/* Plain-colour grounds: var(--ink) on the proof card
   (15-signature.css) and the footer (styles.css:657), var(--tint)
   on the tinted bands (15-signature.css .ground-tint) and the
   prose page head (page.php:73). background-color survives the
   background-image longhand untouched. */
.proof-card,
.site-footer {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='160'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20values='0%200%200%200%200.89%200%200%200%200%200.753%200%200%200%200%200.522%200%200%200%200.08%200'/%3E%3C/filter%3E%3Crect%20width='160'%20height='160'%20filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}
.section-v6.ground-tint,
.page-head.ground-tint {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='160'%20height='160'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20values='0%200%200%200%200.11%200%200%200%200%200.098%200%200%200%200%200.09%200%200%200%200.05%200'/%3E%3C/filter%3E%3Crect%20width='160'%20height='160'%20filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

/* ---------- Motion off ----------
   The chart feed is the only animation this file owns. */
@media (prefers-reduced-motion: reduce) {
  .verify-panel::after {
    animation: none;
  }
}

/* ---------- Small screens carry less ----------
   At 375px the page-head field would sit under most of the
   heading measure and the proof-card arc crowds the figure, so
   both step back; the section-head trace and the fields already
   fade before they reach anything that matters. Nothing here
   can overflow horizontally: every layer is absolutely
   positioned inside a bound it cannot leave, or is a 64px
   in-flow block. */
@media (max-width: 479px) {
  .wrap .page-head:not(.auth-head)::after {
    width: 38%;
  }
  .proof-card::after {
    width: 190px;
    height: 190px;
    top: -50px;
    right: -64px;
  }
}
/* ============================================================
   A6 · PURCHASE PATH · motion and micro-interaction layer
   ------------------------------------------------------------
   PDP buy box, cart drawer, cart page, checkout, order
   confirmation. Loads last: build.sh concatenates the parts
   in codepoint order, digits sort before uppercase, so A6-
   lands after 99-mobile.css and wins every tie. That is a
   loaded gun, so this file adds STATES AND TIMING ONLY. It
   never restates a width, a height, a padding, a flex rule or
   a z-index that an earlier part owns; 99-mobile.css in
   particular (the buy-row growth rules, the bundle tier
   geometry, section 9.3's checkout clearance) is treated as
   law and never re-declared.

   Only transform, opacity, filter, backdrop-filter,
   background-position and clip-path are ever animated or
   transitioned here. Nothing in this file can move layout, so
   nothing in this file can cause CLS. Every moving thing is
   answered in the reduced-motion block at the end.

   No price, total, error message or submit control is ever
   parked at opacity 0 waiting for an animation: entrances run
   as keyframes whose absence leaves the element at its static,
   fully visible state, and every keyframe that touches content
   starts from a non-zero opacity so even a frozen first frame
   is readable. The only opacity-0 base states in this file are
   pure decoration (a focus ring, a celebration ring) whose
   correct resting state IS invisible.

   Every selector below was verified against the markup or the
   script that toggles it before it was styled:

     .single_add_to_cart_button
        markup woocommerce/single-product/add-to-cart/simple.php:73
        gated  assets/pdp.js:108  (addBtn.disabled = !ready)
        busy   assets/pdp.js:356  (disabled + label swap while the
               AJAX add is in flight; no class is added, see S1)
     .qty / .q-up / .q-dn      simple.php:48,56; driver main.js:310
     .price-row .price         functions.php:895 contract;
                               repainted assets/pdp.js:257,260
     .price-coupon.is-applied  main.js:690; markup functions.php:918
     .mass-opt.active          assets/pdp.js:132
     .bundle-box.revealed      assets/pdp.js:122; base 20-pdp.css:402
     .bundle-card.active       assets/pdp.js:272; markup
                               functions.php:2095 (first card ships
                               active); press kill under reduced
                               motion already at 99-mobile.css:349
     .pdp-bar.show             assets/pdp.js:190
     .pdp-stage / .pdp-glow    functions.php:844,845
     .cart-drawer.open         main.js:207 add, main.js:175 remove,
                               hidden re-applied 320ms later at
                               main.js:184-188, so every exit here
                               finishes inside 320ms
     .backdrop.show            main.js:209 / main.js:177
     .cd-item                  inc/cart-drawer.php:338; the whole
                               drawer body is re-rendered as HTML on
                               every mutation (main.js:467), so
                               entrance keyframes re-run per paint
     .cd-qty button:disabled   inc/cart-drawer.php:360 (minus at 1)
     .cd-x                     inc/cart-drawer.php:371 (no leaving
                               class exists, see S6d)
     .cd-bump-add busy         main.js:570-571 (disabled + "Adding")
     .cd-offer-apply busy      main.js:634
     .cd-promo-form.show       main.js:609; base styles.css:903
     .cart-count.has-items     main.js:295, main.js:476; display
                               flip at styles.css:139
     .toast.show / .error      main.js:433,435; base styles.css:641
     #cartBar.show             assets/cart.js:126; shell
                               10-tokens.css:157-168
     .cart-offer-apply busy    assets/cart.js:97-98
     .hl-step.open / .done     assets/checkout.js:85-86
     .hl-step-tick unhidden    assets/checkout.js:95
     .hl-step-recap unhidden   assets/checkout.js:74
     .hl-rail-dot.current/.done  assets/checkout.js:104-105
     #place_order gate         assets/checkout.js:168-169; markup
                               inc/checkout-steps.php:207
     form.checkout.processing  applied by WooCommerce core
                               assets/js/frontend/checkout.js on
                               submit. The plugin is not vendored in
                               this repo so no repo file:line exists;
                               the theme already banks on the same
                               script's classes at 50-cart.css:547
                               (.woocommerce-invalid), and
                               functions.php:480 empties Woo's
                               STYLES only, never its scripts. If
                               the class never lands, every rule on
                               it is inert and nothing is hidden.
     .form-row.woocommerce-invalid  same script; styled by the theme
                               at 50-cart.css:547-549
     .hl-bar-figs / .hl-pay-total   replaced wholesale as AJAX
                               fragments, inc/checkout-flow.php:227-232,
                               so keyframes on their children re-run
                               on every recalculation
     .hl-consent .hl-box       50-cart.css:1150-1151 flips the svg
                               opacity on :checked
     .ty-* confirmation        woocommerce/checkout/thankyou.php;
                               .ty-mark already pops (50-cart.css:1840
                               ty-pop) and keeps that job

   Verified, not fixed, because it was already right: every
   input on the path renders at 16px (10-tokens.css:81-84 sets
   input, select, textarea and the qty field globally; the
   floating checkout fields restate 16px at 50-cart.css:1487),
   so iOS has nothing to auto-zoom. No tap target is resized
   anywhere in this file.
   ============================================================ */

/* ------------------------------------------------------------
   S0 · Shared keyframes
   ------------------------------------------------------------
   One vocabulary for the whole path, so the drawer, the
   checkout and the confirmation read as the same hand. Content
   keyframes start from .25 to .35 opacity, never zero: a frame
   frozen at 0% is dim but legible, and an animation that never
   starts leaves the element at its static, fully painted state.

   a6-settle is the number move. The brief for tier and total
   changes is "the same number moving, not a different number
   appearing", and the whole path already re-renders its figures
   as fresh DOM (pdp.js:257-262, main.js:467, the checkout
   fragments). Fresh nodes restart animations, so a short rise
   from just below the baseline plays on exactly the figures
   that changed, on every change, with no script. Tabular
   numerals (10-tokens.css:141-146, 99-mobile.css:316) keep the
   glyph widths fixed while it happens.
   ------------------------------------------------------------ */

@keyframes a6-settle {
  from { opacity: .25; transform: translateY(.34em); }
  to   { opacity: 1;   transform: none; }
}

@keyframes a6-rise {
  from { opacity: .3; transform: translateY(8px); }
  to   { opacity: 1;  transform: none; }
}

@keyframes a6-rise-sm {
  from { opacity: .35; transform: translateY(5px); }
  to   { opacity: 1;   transform: none; }
}

@keyframes a6-pop {
  0%   { transform: scale(.55); opacity: .25; }
  62%  { transform: scale(1.09); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* Busy pulse for a control that is mid-request. Floor of .68 so
   the label stays readable through every phase of the loop. */
@keyframes a6-busy {
  0%, 100% { opacity: 1; }
  50%      { opacity: .68; }
}

/* The refusal. Three diminishing horizontal taps, the smallest
   gesture that reads as "no" without reading as an alarm. */
@keyframes a6-nudge {
  0%, 100% { transform: translateX(0); }
  22%      { transform: translateX(-4px); }
  48%      { transform: translateX(3px); }
  74%      { transform: translateX(-2px); }
}

/* One pass of light, long rest. Same period family as the home
   hero's sweep (A1-motion-hero.css a1-sweep) so the two glass
   objects on the site are lit by the same lamp. */
@keyframes a6-sweep {
  0%   { background-position: 130% 0; }
  40%  { background-position: -30% 0; }
  100% { background-position: -30% 0; }
}

/* ============================================================
   S1 · PDP · Add to cart
   ------------------------------------------------------------
   The button has four real states and three of them are
   CSS-reachable:

     idle      enabled, label from the product
     pressed   :active
     gated     disabled with the label "Select a mass"
               (pdp.js:108, simple.php:18-24)
     in-flight disabled with the label "Adding"
               (pdp.js:356-357), restored at pdp.js:394/400

   Gated and in-flight share :disabled and no class separates
   them, so this file styles :disabled once, quietly, and lets
   the label carry the distinction, which is exactly how the JS
   built it. No .loading or .added class exists on this button
   (grep of assets/pdp.js; the .added class at main.js:594
   belongs to the PLP quick-add pills, which are A2's surface
   and are not touched here). Success feedback is the drawer
   arriving with the item staggering in (S6) plus the badge pop
   (S6f); inventing a second success state on the button would
   fire it at the same moment the drawer covers it.

   The press: the sitewide pairing gives every button a 200ms
   ease into scale(.985) (10-tokens.css:128-131, 134-139). On
   the one button that spends money, contact should register
   inside a frame or two, so press-in gets a short duration and
   release falls back to the family 200ms. Same trick A2 plays
   on the cards, for the same reason.
   ------------------------------------------------------------ */

.buy-row .single_add_to_cart_button:active:not(:disabled),
.pdp-bar-add:active:not(:disabled) {
  transform: scale(.975);
  transition-duration: 70ms;
}

/* The disabled state reads as parked, not broken: desaturated
   and dimmed by filter, which repaints without touching the
   layout the buy-row rules in 99-mobile.css measured. Opacity
   floor keeps "Select a mass" and "Adding" fully legible,
   because a label that names the missing step is content. */
.buy-row .single_add_to_cart_button:disabled,
.pdp-bar-add:disabled {
  filter: grayscale(.35) opacity(.62);
  cursor: not-allowed;
}

/* ------------------------------------------------------------
   S2 · PDP · Quantity stepper
   ------------------------------------------------------------
   The plus and minus are 44px pills (simple.php:48,56) driven
   by main.js:310, which clamps at min and max without ever
   disabling, so on the PDP the bound state is "the number
   stopped moving". The press is the feedback channel that
   exists, so it gets the same fast-in physics as the buy
   button, scaled to the smaller control.

   The drawer stepper is different: its minus really is
   disabled at quantity 1 (inc/cart-drawer.php:360), so it gets
   a true disabled treatment in S6e.
   ------------------------------------------------------------ */

.qty .q-up:active,
.qty .q-dn:active {
  transform: scale(.88);
  transition-duration: 60ms;
}

/* ------------------------------------------------------------
   S3 · PDP · The price is one number moving
   ------------------------------------------------------------
   Choosing a tier makes pdp.js:260-263 replace the contents of
   .price-row .price with fresh .was / .bundle-flag / .now
   nodes, and choosing tier one restores the original Woo
   markup the same way (pdp.js:257). Fresh nodes restart
   animations, so a6-settle plays on precisely the figures that
   changed: the new price rises into the slot the old one
   occupied, in fixed-width digits, which the eye reads as the
   same number moving. The struck was-price and the flag follow
   a beat later so the sequence reads new price first,
   explanation second.

   The bare selector also catches the server-rendered price
   once on page load, which is kept deliberately: the price
   arriving with the same settle as every later change is what
   makes the motion a vocabulary instead of an effect.
   ------------------------------------------------------------ */

.price-row .price .now,
.price-row .price > .woocommerce-Price-amount {
  display: inline-block; /* transforms need a box; inline spans ignore them */
  animation: a6-settle 300ms var(--ease-out) both;
}

.price-row .price .was,
.price-row .price .bundle-flag {
  display: inline-block;
  animation: a6-settle 300ms var(--ease-out) 70ms both;
}

/* The coupon line's applied state (main.js:690 adds
   .is-applied and the tick is drawn by the existing ::after,
   20-pdp.css:386). The confirmation deserves a beat of arrival
   rather than an instant swap. */
.price-coupon.is-applied .coupon-tap::after {
  display: inline-block;
  animation: a6-pop 360ms var(--ease-out) both;
}

/* ------------------------------------------------------------
   S4 · PDP · Bundle tiers
   ------------------------------------------------------------
   The tier box is display:none until a mass is chosen, then
   pdp.js:122 adds .revealed and 20-pdp.css:407 flips it to
   block. A display flip restarts animations, so the reveal can
   ride a keyframe with no extra state: the box rises in as a
   consequence of the choice just made above it.

   Selection: pdp.js:272 moves .active between cards. The card
   chrome already transitions through the sitewide pairing
   (10-tokens.css:134) and 99-mobile.css owns the tier geometry,
   so the only thing added is the vials nodding when their card
   is chosen: the class landing on a new element restarts the
   pop. transform on .bundle-vials moves a 42px strip inside
   the card, never the card box, so the equal-height grid the
   mobile pass measured is untouched.

   Press: 20-pdp.css:460 already scales the card and
   99-mobile.css:349 already stills it under reduced motion.
   Nothing to add, nothing re-declared.
   ------------------------------------------------------------ */

.bundle-box.revealed {
  animation: a6-rise 340ms var(--ease-out) both;
}

.bundle-card.active .bundle-vials {
  animation: a6-vials 380ms var(--ease-out) both;
}

@keyframes a6-vials {
  0%   { transform: translateY(0) scale(1); }
  45%  { transform: translateY(-3px) scale(1.04); }
  100% { transform: translateY(0) scale(1); }
}

/* ------------------------------------------------------------
   S5 · PDP · Light on the glass, depth on the scroll
   ------------------------------------------------------------
   The stage is a photographic cutout on a warm tile with a
   breathing glow behind it (functions.php:844-846, float and
   glow at 20-pdp.css:95-174). What the home hero has and this
   bottle lacks is the specular pass: one light source crossing
   the glass. Same construction as A1 section 4, one lozenge
   pseudo, a band travelling by background-position (permitted
   list), an elliptical mask so the band dies before the
   lozenge's edge the way a reflection dies on curvature, and
   screen blending so it brightens the photograph instead of
   sitting on it. The PDP bottle stands upright so the lozenge
   is unrotated; insets hug the 47/96 bottle body inside the
   square stage with the letterboxing 20-pdp.css:153-172
   produces. The pseudo sits at z-index 2, over the img at
   z-index 1 and under nothing.

   The trade, stated: a background-position animation repaints
   the pseudo rather than riding the compositor. The pseudo is
   a fraction of one tile, the pass occupies 40% of a 15s
   cycle, and this page has no is-off observer, so the honest
   mitigation is the long rest plus the reduced-motion kill.
   ------------------------------------------------------------ */

.pdp-stage::after {
  content: "";
  position: absolute;
  inset: 7% 27% 17%;
  z-index: 2;
  border-radius: 30% / 9%;
  pointer-events: none;
  background-image: linear-gradient(105deg,
      transparent 40%,
      rgba(255, 255, 255, .20) 47%,
      rgba(255, 255, 255, .44) 50%,
      rgba(255, 255, 255, .20) 53%,
      transparent 60%);
  background-size: 340% 100%;
  background-repeat: no-repeat;
  background-position: 130% 0;
  mix-blend-mode: screen;
  -webkit-mask-image: radial-gradient(ellipse 60% 46% at 50% 46%, #000 50%, transparent 92%);
          mask-image: radial-gradient(ellipse 60% 46% at 50% 46%, #000 50%, transparent 92%);
  animation: a6-sweep 15s ease-in-out 2.6s infinite;
}

/* Parallax, gently and only where it is safe. The vial itself
   already owns its animation (the float), so depth is written
   on the stage box and composes with it. Gated twice: behind
   @supports because scroll-driven animation is the enhancement
   tier, and under 1024px because the desktop layout pins
   .pdp-media sticky (20-pdp.css:968) and a transform inside a
   sticky ancestor reads as slippage, not depth. */
@supports (animation-timeline: view()) {
  @media (max-width: 1023px) {
    .pdp-stage {
      animation: a6-plx linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
  }
}

@keyframes a6-plx {
  from { transform: translateY(12px); }
  to   { transform: translateY(-14px); }
}

/* ============================================================
   S6 · Cart drawer
   ------------------------------------------------------------
   The state machine, from main.js: open() unhides the panel,
   forces a reflow, then adds .open to the panel and .show to
   the shared backdrop (main.js:202-210); close() removes both
   and re-hides the DOM 320ms later (main.js:175-188). The base
   sheet slides the panel with one 300ms transition both ways
   (styles.css:803-813). Weight comes from asymmetry: arriving
   is the event, so it takes the long curve; dismissal is
   already understood, so it leaves at about 65% of the enter
   time and finishes with 60ms to spare inside the JS teardown
   window. The declaration order below makes the split: the
   base state's transition governs the exit, the .open state's
   transition governs the entrance.
   ============================================================ */

/* S6a · The panel and the scrim on their own curves. */
.cart-drawer {
  transition: transform 240ms var(--ease-soft);
}
.cart-drawer.open {
  transition: transform 380ms var(--ease);
}

/* The scrim is light, not furniture: it fades on plain curves,
   quicker than the panel on the way out so the page is back
   before the panel has fully left. The backdrop is shared with
   the nav drawer (header.php:183, one element for both), so
   the nav inherits this timing too; both drawers run the same
   320ms teardown in main.js, so the budget holds for both. */
.backdrop {
  transition: opacity 200ms ease-out;
}
.backdrop.show {
  transition: opacity 300ms ease-out;
}

/* S6b · Line items arrive as a cascade. Every mutation hands
   back the whole drawer as fresh HTML (main.js:467), so these
   keyframes re-run on every paint: on first open it reads as
   the cart assembling, and after a quantity change it reads as
   the lines re-settling, which is truthful, because the volume
   ladder really can reprice every line on one change
   (inc/cart-drawer.php header note). The soft .25 start is
   what keeps a repaint from reading as a blink. Delay is
   capped at the sixth row so a long cart never makes the foot
   of the list feel late. */
/* fill backwards, not both: a forwards fill would keep the
   final keyframe applied after the animation ends and would
   outrank the S6d press transition on the same properties. */
.cd-item {
  animation: a6-rise-sm 260ms var(--ease-out) backwards;
}
.cd-item:nth-child(2) { animation-delay: 40ms; }
.cd-item:nth-child(3) { animation-delay: 80ms; }
.cd-item:nth-child(4) { animation-delay: 120ms; }
.cd-item:nth-child(5) { animation-delay: 160ms; }
.cd-item:nth-child(n+6) { animation-delay: 190ms; }

/* The cards after the lines follow the cascade at its tail. */
.cd-assure,
.cd-bump,
.cd-offer {
  animation: a6-rise-sm 280ms var(--ease-out) 140ms both;
}

.cd-empty {
  animation: a6-rise 320ms var(--ease-out) both;
}

/* S6c · The totals settle as figures, not as a card. Rows are
   re-rendered per mutation like everything else, so the money
   lines get the number treatment from S0 and nothing else
   moves in the footer. The free-shipping fill keeps its
   existing width transition (styles.css:917, re-armed by
   main.js:483-489); width is not this file's to animate and
   the behaviour already ships. */
.cd-total-row,
.cd-urgency {
  animation: a6-settle 260ms var(--ease-out) both;
}
.cd-total-row.cd-total-due {
  animation-delay: 60ms;
}

/* S6d · Removal. The X posts, the server answers, the drawer
   repaints without the row (main.js:562-564); no class ever
   marks the leaving row, so a true exit animation is not
   honestly reachable from CSS and none is faked here. What CSS
   can reach is the press itself: while the finger is on the X,
   the whole row dims and shrinks under it, so the row is
   already visibly going before the repaint takes it. Scoped
   behind @supports because :has() is the only way to reach the
   row from its button. */
@supports selector(.a:has(.b)) {
  .cd-item:has(.cd-x:active) {
    opacity: .45;
    transform: scale(.975);
    transition: opacity 120ms ease-out, transform 120ms ease-out;
  }
}

/* S6e · Drawer stepper and the busy buttons. The minus is
   genuinely disabled at quantity 1 (inc/cart-drawer.php:360):
   at the bound the control goes flat and the press physics
   stop, which is the difference between "will not" and "did
   not register". */
.cd-qty button:not(:disabled):active {
  transform: scale(.86);
  transition-duration: 60ms;
}
.cd-qty button:disabled {
  opacity: .32;
  cursor: not-allowed;
  transform: none;
}

/* Buttons the JS parks in a disabled+relabelled busy state
   while a request runs: the bump add (main.js:570-571), the
   drawer offer (main.js:634), and the cart page's offer card
   (cart.js:97-98). One pulse says "working" on all three. */
.cd-bump-add:disabled,
.cd-offer-apply:disabled,
.cart-offer-apply:disabled {
  animation: a6-busy 1.1s ease-in-out infinite;
}

/* The promo field unfolds (display flip at styles.css:903 when
   main.js:609 adds .show), so its entrance can ride a
   keyframe. */
.cd-promo-form.show {
  animation: a6-rise-sm 240ms var(--ease-out) both;
}

/* S6f · The badge. main.js:295 and :476 toggle .has-items and
   styles.css:139 flips display, so the pop plays when the
   count first exists, which is the moment the user caused. A
   later 2 becoming 3 rewrites textContent with no class change
   and no display flip; CSS has no hook for that, it is not
   faked, and the drawer opening on every add is the feedback
   that moment already owns. */
.cart-count.has-items {
  animation: a6-pop 420ms var(--ease-out) both;
}

/* S6g · The toast, error flavour. The base slide-in
   (styles.css:641-648) stays; a failed action additionally
   refuses sideways. The keyframes restate the base centering
   translate because transform is atomic and a bare shake would
   throw the pill off its midline. */
.toast.error.show {
  animation: a6-toast-no 380ms var(--ease-out) 80ms;
}

@keyframes a6-toast-no {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  22%      { transform: translateX(calc(-50% - 5px)) translateY(0); }
  48%      { transform: translateX(calc(-50% + 4px)) translateY(0); }
  74%      { transform: translateX(calc(-50% - 2px)) translateY(0); }
}

/* ============================================================
   S7 · Cart page
   ------------------------------------------------------------
   The cart page is deliberately reload-driven: every drawer
   mutation on /cart/ reloads the page so PHP can restate every
   number (main.js:532), and quantity changes submit the form
   (cart.js:34-52). So "feedback" here is arrival feedback:
   the money figures settle on every load with the same S0
   vocabulary, which after a quantity change reads as the
   reprice landing. Kept to the figures alone; animating the
   rows on a page that reloads per edit would make editing feel
   like the page collapsing and rebuilding.

   The sticky bar's own slide already works: cart.js:122-127
   reveals it a frame after load and the shared shell animates
   it (10-tokens.css:157-169). Not re-declared.
   ------------------------------------------------------------ */

body.woocommerce-cart .cart-line-price,
body.woocommerce-cart .cart-summary .sum-row,
body.woocommerce-cart .cart-bar-sum b {
  animation: a6-settle 280ms var(--ease-out) both;
}

/* ============================================================
   S8 · Checkout
   ------------------------------------------------------------
   Money is on the table, so motion here is confined to three
   jobs: show which panel is alive, refuse bad input visibly,
   and make the one submit feel held while the order posts.
   Nothing decorative.
   ============================================================ */

/* S8a · Focus worth the money. The base already paints the
   focused field instantly (border to ink plus an 8% halo,
   50-cart.css:1492-1496) and the floating label already glides
   (50-cart.css:1464-1472). Added: the halo now arrives instead of
   appearing, drawn as a ring pseudo on the row that fades and
   tightens in. Ink, never gold: the tokens layer is explicit
   that the accent means "action", not "location"
   (10-tokens.css:44-47). The ring is decoration, so its
   resting opacity of 0 hides nothing that matters, and it is
   state-driven, so even with transitions unavailable the
   focused state still shows it. The :has() rule degrades to
   nothing in engines without it, leaving the base focus paint
   intact. */
.hl-checkout .form-row.hl-float::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 15px;
  border: 2px solid rgba(28, 25, 23, .14);
  opacity: 0;
  transform: scale(.99);
  pointer-events: none;
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
.hl-checkout .form-row.hl-float:has(input.input-text:focus)::after {
  opacity: 1;
  transform: scale(1);
}

/* S8b · Refusal, inline. WooCommerce core validation lands
   .woocommerce-invalid on the row (the theme already colours
   it, 50-cart.css:547-549); the class arriving triggers one
   nudge, so the red border is noticed the moment it is earned
   rather than on the next read-through. The error summary Woo
   prints on a failed submit rises in the same way; it is a
   keyframe, so the error is fully visible even if animation
   never runs. */
.hl-checkout .form-row.woocommerce-invalid {
  animation: a6-nudge 300ms var(--ease-out);
}

body.woocommerce-checkout .woocommerce-error {
  animation: a6-rise-sm 320ms var(--ease-out) both;
}

/* S8c · The panels. checkout.js:85-86 moves .open and .done;
   the body's hidden attribute flips with it (checkout.js:87),
   and the display flip restarts the keyframe, so opening a
   step plays its contents in. One caveat is handled by the JS
   itself: after an AJAX recalculation an already-open body is
   re-asserted visible without a display flip (checkout.js:238-242),
   so the animation does not replay under someone mid-typing. */
.hl-step.open .hl-step-body {
  animation: a6-rise-sm 280ms var(--ease-out) both;
}

/* The recap line and the tick appear by unhiding
   (checkout.js:74, :95): arrival, not toggling. */
.hl-step-recap {
  animation: a6-rise-sm 260ms var(--ease-out) both;
}
.hl-step-tick {
  animation: a6-pop 380ms var(--ease-out) both;
}

/* The rail: the dot that becomes current pops once
   (checkout.js:104 toggles the class as the buyer advances).
   The connecting fill is drawn by width at 50-cart.css:1319
   and width is not this file's to animate; the dot carries the
   moment instead. */
.hl-rail-dot.current {
  animation: a6-pop 380ms var(--ease-out) both;
}

/* S8d · Consents. The base flips the tick svg's opacity when
   checked (50-cart.css:1150-1151). Giving the svg a resting
   scale and a transition turns the flip into a small
   confirmation without touching the 44px labels or the box
   geometry. State-driven, so it works with or without the
   transition running. */
.hl-consent .hl-box svg {
  transform: scale(.55);
  transition: transform 200ms var(--ease-out), opacity 140ms linear;
}
.hl-consent input:checked + .hl-box svg {
  transform: scale(1);
}

/* S8e · Place order, held while it posts. WooCommerce core
   adds .processing to the form for the duration of the submit
   (provenance in the header map). The button cannot be
   double-tapped ambiguously: pointer-events stop instantly and
   the pulse says the first tap took. The label never dips
   below .68 opacity, because "what did I just press" must stay
   answerable for the whole wait. If the class never arrives,
   these rules never run and the button is its normal self. */
form.checkout.processing .hl-place-order {
  pointer-events: none;
  animation: a6-busy 1.2s ease-in-out infinite;
}
form.checkout.processing .hl-step-next,
form.checkout.processing .hl-step-back {
  pointer-events: none;
  opacity: .6;
}

/* S8f · The running total is a fragment WooCommerce replaces on
   every recalculation (inc/checkout-flow.php:227-232), so the
   figures are fresh nodes each time shipping or a coupon moves
   the number: the settle plays on exactly the totals that
   changed, in the bar and on the pay button both. */
.hl-bar-total,
.hl-pay-figure {
  display: inline-block;
  animation: a6-settle 280ms var(--ease-out) both;
}

/* ============================================================
   S9 · Order confirmation
   ------------------------------------------------------------
   The last screen a first-time buyer sees, and the only one
   allowed a moment of ceremony. The base already pops the tick
   (ty-pop, 50-cart.css:1830-1844) and that stays the anchor;
   this section sequences the rest of the page off that beat:
   greeting, lede, then the facts and next-steps in a cascade,
   so the eye is walked from "it worked" to "here is your
   number" to "here is what happens".

   The failed branch gets NOTHING. thankyou.php renders it as
   .ty-head-fail with its own copy ("Nothing celebratory here",
   thankyou.php:76-79), and every celebratory selector below is
   scoped out of it. A declined payment still deserves instant,
   motionless clarity, so the retry button never waits on a
   delay.
   ------------------------------------------------------------ */

.ty-head:not(.ty-head-fail) .eyebrow {
  animation: a6-rise 460ms var(--ease-out) 120ms both;
}
.ty-head:not(.ty-head-fail) h1 {
  animation: a6-rise 460ms var(--ease-out) 180ms both;
}
.ty-head:not(.ty-head-fail) .ty-lede,
.ty-head:not(.ty-head-fail) .ty-extra {
  animation: a6-rise 460ms var(--ease-out) 240ms both;
}

/* One ring leaves the tick as it lands: the celebration, spent
   in 700ms, then a still page. Pure decoration, invisible at
   rest by design, drawn in the success palette the mark
   already wears. position:relative is layout-neutral here; the
   mark is an inline-flex box whose children are unpositioned. */
.ty-head:not(.ty-head-fail) .ty-mark {
  position: relative;
}
.ty-head:not(.ty-head-fail) .ty-mark::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid var(--success, #15803d);
  opacity: 0;
  pointer-events: none;
  animation: a6-ty-ring 700ms var(--ease-out) 360ms;
}

@keyframes a6-ty-ring {
  0%   { opacity: .5; transform: scale(.72); }
  100% { opacity: 0;  transform: scale(1.5); }
}

/* The facts row: order number, date, total, one beat apart.
   The order number is the thing people screenshot, so the
   cascade leads with it. */
.ty-facts .ty-fact { animation: a6-rise 420ms var(--ease-out) both; }
.ty-facts .ty-fact:nth-child(1) { animation-delay: 320ms; }
.ty-facts .ty-fact:nth-child(2) { animation-delay: 380ms; }
.ty-facts .ty-fact:nth-child(3) { animation-delay: 440ms; }
.ty-facts .ty-fact:nth-child(n+4) { animation-delay: 500ms; }

/* What happens next, arriving in the order it will happen. */
.ty-steps .ty-step { animation: a6-rise 420ms var(--ease-out) both; }
.ty-steps .ty-step:nth-child(1) { animation-delay: 480ms; }
.ty-steps .ty-step:nth-child(2) { animation-delay: 560ms; }
.ty-steps .ty-step:nth-child(n+3) { animation-delay: 640ms; }

/* The aside follows the main column, quietly. */
.ty-aside .ty-card {
  animation: a6-rise 420ms var(--ease-out) 560ms both;
}

/* The bought vial floats over the aside on desktop
   (thankyou.php's .ty-deco, shown from 1000px at
   50-cart.css:2261-2263). It is the same object the PDP
   floats, so it gets the same idle from the shared keyframe
   set, slowed to match the calm of the page. */
.ty-deco .vial,
.ty-deco img.vial {
  animation: h-float-slow 7s ease-in-out 1.2s infinite;
}

/* ============================================================
   S10 · Motion off
   ------------------------------------------------------------
   Every animation and transition this file introduced, stilled.
   States remain: disabled still reads disabled, focus still
   rings, the tick still shows, errors still colour, the sweep
   pseudo rests parked outside its mask, and every entrance
   keyframe's absence leaves its element fully painted, because
   nothing here starts hidden. Base-layer motion (the drawer
   slide, the toast, the bottom bars, ty-pop) is already
   answered by its own layers; re-stilling the drawer and
   backdrop is repeated here anyway because this file re-timed
   them and the kill must outrank the re-timing.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .buy-row .single_add_to_cart_button:active:not(:disabled),
  .pdp-bar-add:active:not(:disabled),
  .qty .q-up:active,
  .qty .q-dn:active,
  .cd-qty button:not(:disabled):active {
    transform: none;
  }

  .price-row .price .now,
  .price-row .price > .woocommerce-Price-amount,
  .price-row .price .was,
  .price-row .price .bundle-flag,
  .price-coupon.is-applied .coupon-tap::after,
  .bundle-box.revealed,
  .bundle-card.active .bundle-vials,
  .pdp-stage,
  .pdp-stage::after,
  .cd-item,
  .cd-assure,
  .cd-bump,
  .cd-offer,
  .cd-empty,
  .cd-total-row,
  .cd-urgency,
  .cd-bump-add:disabled,
  .cd-offer-apply:disabled,
  .cart-offer-apply:disabled,
  .cd-promo-form.show,
  .cart-count.has-items,
  .toast.error.show,
  body.woocommerce-cart .cart-line-price,
  body.woocommerce-cart .cart-summary .sum-row,
  body.woocommerce-cart .cart-bar-sum b,
  .hl-checkout .form-row.woocommerce-invalid,
  body.woocommerce-checkout .woocommerce-error,
  .hl-step.open .hl-step-body,
  .hl-step-recap,
  .hl-step-tick,
  .hl-rail-dot.current,
  form.checkout.processing .hl-place-order,
  .hl-bar-total,
  .hl-pay-figure,
  .ty-head:not(.ty-head-fail) .eyebrow,
  .ty-head:not(.ty-head-fail) h1,
  .ty-head:not(.ty-head-fail) .ty-lede,
  .ty-head:not(.ty-head-fail) .ty-extra,
  .ty-head:not(.ty-head-fail) .ty-mark::after,
  .ty-facts .ty-fact,
  .ty-steps .ty-step,
  .ty-aside .ty-card,
  .ty-deco .vial,
  .ty-deco img.vial {
    animation: none;
  }

  .cart-drawer,
  .cart-drawer.open,
  .backdrop,
  .backdrop.show,
  .hl-checkout .form-row.hl-float::after,
  .hl-consent .hl-box svg {
    transition: none;
  }

  @supports selector(.a:has(.b)) {
    .cd-item:has(.cd-x:active) {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

  /* The consent tick and the focus ring keep their states with
     the travel removed. */
  .hl-consent .hl-box svg { transform: scale(1); }
  .hl-checkout .form-row.hl-float::after { transform: none; }
}
/* ============================================================
   A7 · Research library, certificates page, partner 375 sweep
   ------------------------------------------------------------
   Loads after 99-mobile.css: the build concatenates parts in
   codepoint order and digits sort before uppercase, so this file
   wins every equal-specificity tie. Nothing shipped in
   99-mobile.css is redone here; where a shipped rule exists (COA
   left alignment, .cat-pill 44px, .wrap.section-v6 gutter) this
   file builds on top of it.

   Scoping, so nothing leaks onto surfaces other agents own:

     Research library   .wrap:has(.verify-panel)
                        research.php:54 is the only .verify-panel
                        in the theme (checked with a full grep),
                        and page.php:97 wraps the shortcode in a
                        bare .wrap, so the hook matches exactly
                        one page.
     Certificates page  .prose:has(.coa-cert)
                        The PDP renders the same card inside
                        .coa-panel (functions.php:1240), never
                        inside .prose, and the PDP references
                        list sits inside .pdp-refs
                        (research.php:203). Only the certificates
                        page (provision.sh:601) puts either class
                        inside the .prose measure.
     Partner program    .partner-page (partner.php:315)

   Motion: none is added. No animation, no keyframes, no new
   transition. Sticky positioning is not motion. The hover lift
   and press states that fire on .spec-card and .coa-item come
   from 10-tokens.css and are already stilled under reduced
   motion there.
   ============================================================ */

/* ============================================================
   1 · Research library: the library goes first
   ------------------------------------------------------------
   Measured top to bottom at 375px, the page ran: title, a dark
   420px testing panel, a 760px stack of four COA explainer
   cards, and only then the compound browser. The browser is the
   page's one job and its best asset (the tinted tiles), and it
   started roughly 1,350px deep. Two blocks about certificates
   stood in front of the block the page is named after.

   page.php:97 wraps the shortcode output in one .wrap whose
   children are exactly: .page-head (research.php:49),
   .verify-panel (research.php:54), the spec section
   (research.php:86) and the browser section (research.php:113).
   Flex order re-sequences them CSS-only: title, browser, then
   the testing panel, then the COA explainer as supporting
   matter.

   Trade-off, stated honestly: keyboard tab order still follows
   the DOM, so the testing panel's CTA is reached before the
   filter pills even though it now sits below them. Judged
   acceptable against opening the page on its subject; whole
   sections move, nothing interleaves. Browsers without :has()
   keep the original order and lose nothing else.
   ============================================================ */
.wrap:has(.verify-panel) {
  display: flex;
  flex-direction: column;
}
.wrap:has(.verify-panel) > .verify-panel { order: 2; }
.wrap:has(.verify-panel) > .section-v6:has(.spec-cards) { order: 3; }

/* The browser section now follows the h1 directly, so its 44px
   section padding (styles.css:1646) doubled the page-head's own
   bottom padding into a 62px hole under the title. The heading
   hands straight into the eyebrow instead. */
.wrap:has(.verify-panel) > .section-v6:has(.cat-strip) { padding-block-start: 8px; }
@media (min-width: 768px) {
  /* Overrides the 68px desktop step-up at styles.css:1704. */
  .wrap:has(.verify-panel) > .section-v6:has(.cat-strip) { padding-block-start: 24px; }
}

/* After the move the verify panel's margin-top: 32px
   (styles.css:1852) stacked onto the browser section's 44px
   bottom padding for a 76px gap, while every other block break
   on the page is 44px. One rhythm, so the stray margin goes. */
@media (max-width: 767px) {
  .wrap:has(.verify-panel) > .verify-panel { margin-top: 0; }
}

/* ============================================================
   2 · Research library: type ramp at 375
   ------------------------------------------------------------
   Measured heading sizes at 375px before this file: h1 32px
   (99-mobile.css 5a), section h2 29.6px (styles.css:1652),
   verify h2 25.6px (styles.css:1861). Three sizes within 6px of
   each other is not a ramp, it is jitter: the section heads
   read as competing h1s. Normalised to one clear step per
   level: 32 / 24 / 15.2 (the card and spec h3s). Desktop keeps
   its own scale.
   ============================================================ */
@media (max-width: 767px) {
  .wrap:has(.verify-panel) .section-head-v6 h2 { font-size: 1.5rem; }
  .wrap:has(.verify-panel) .verify-panel h2 { font-size: 1.5rem; }
  /* The intro under the h1 is the page's lede, not a footnote.
     At 16px muted it spoke in the same voice as every caption
     below it. 17px at the 327px measure is about 40 characters
     a line, inside the 35 to 60 band. */
  .wrap:has(.verify-panel) .page-head p {
    font-size: 1.0625rem;
    line-height: 1.6;
  }
}

/* ============================================================
   3 · Research library: the filter travels with the grid
   ------------------------------------------------------------
   The category pills (research.php:120) filter a grid of every
   compound in the catalogue. Two screens into that grid the
   filter was gone. Sticky keeps it one thumb-reach away for the
   whole scroll, and it releases when its section ends, so it
   never haunts the panels that now follow the grid.

   The negative-margin trick on .cat-pills (styles.css:986) lets
   the row bleed to both screen edges, so the strip takes the
   same negative margin plus matching padding; its white ground
   then covers the full bleed while stuck and cards cannot show
   through beside the pills. z-index 5 sits above the raised
   .card-link (99-mobile.css 1a) and far below the header's 50.
   ============================================================ */
.wrap:has(.verify-panel) .cat-strip {
  position: sticky;
  top: var(--header-h);
  z-index: 5;
  background: var(--bg);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block: 8px 2px;
  margin-bottom: 12px;
  /* A hairline drawn as shadow, not border, so it adds no height
     and the pill row keeps its snap geometry. */
  box-shadow: 0 1px 0 var(--line);
}

/* ============================================================
   4 · Research library: the COA explainer stops wearing borders
   ------------------------------------------------------------
   The four .spec-card explainers (research.php:94) rendered as
   full-width grey boxes with a hairline border, stacked 760px
   tall at 375px: four identical weights directly beside the
   tinted compound tiles the owner singled out. The tiles are
   where this site uses colour with confidence, so the explainer
   borrows the same move: the four cards take the four warm
   tints in the site's fixed rotation, and the border stops
   pretending to separate what colour now separates.

   Contrast, checked before shipping: body copy is --ink-2
   #57534e, which measures 7.1:1 on --tint-gold #fdf6e9 and
   6.6:1 on the coolest ground --tint-steel #eaeff5, both over
   the 4.5:1 floor. --muted is not used on any tint here; it
   only clears 4.5:1 against --tint and lighter.
   ============================================================ */
.wrap:has(.verify-panel) .spec-card {
  border-color: transparent;
  background: var(--tint-gold);
  padding: 16px 18px;
}
.wrap:has(.verify-panel) .spec-cards > .spec-card:nth-child(4n + 2) { background: var(--tint-sage); }
.wrap:has(.verify-panel) .spec-cards > .spec-card:nth-child(4n + 3) { background: var(--tint-steel); }
.wrap:has(.verify-panel) .spec-cards > .spec-card:nth-child(4n + 4) { background: var(--tint-clay); }
.wrap:has(.verify-panel) .spec-card h3 { margin-bottom: 6px; }
.wrap:has(.verify-panel) .spec-card p {
  font-size: .9375rem;
  line-height: 1.55;
  color: var(--ink-2);
}
@media (max-width: 767px) {
  .wrap:has(.verify-panel) .spec-cards { gap: 10px; }
}
/* styles.css:1878 never gives .spec-cards columns, so on a
   1440px desktop these were four 1392px-wide ribbons. Two-up
   from tablet, four-up wide: the stat strip's own rhythm. */
@media (min-width: 640px) {
  .wrap:has(.verify-panel) .spec-cards { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .wrap:has(.verify-panel) .spec-cards { grid-template-columns: repeat(4, 1fr); }
}

/* ============================================================
   5 · Research library: the testing panel, demoted not deleted
   ------------------------------------------------------------
   The dark panel is good; it was doing a hero's job on a
   librarian's page. Following the grid it now closes the page
   as "how we test" plus the certificate CTA. Measured at 375 it
   ran about 420px on 28px padding with a 2rem display figure;
   tightened so it reads as a closing statement rather than a
   second hero. Its CTA goes full width: a clean 48px target,
   and the panel's one action reads as the panel's one action.
   Desktop (40px padding, 2.1rem h2 in the base layer) is
   untouched.
   ============================================================ */
@media (max-width: 767px) {
  .wrap:has(.verify-panel) > .verify-panel { padding: 22px 18px; }
  .wrap:has(.verify-panel) .verify-stats {
    margin-top: 16px;
    gap: 14px;
  }
  .wrap:has(.verify-panel) .verify-stat strong { font-size: 1.75rem; }
  .wrap:has(.verify-panel) .verify-pills { margin-top: 14px; }
  .wrap:has(.verify-panel) .verify-panel .btn {
    margin-top: 18px;
    width: 100%;
  }
}

/* ============================================================
   6 · Certificates page: read, then request
   ------------------------------------------------------------
   The page's real order at 375 (provision.sh:601 plus the
   shortcode at functions.php:1307): certificate card, then the
   full request-by-product directory, and only after that the
   lede, the "what the certificate reports" list, the release
   panel and the how-to steps. The directory of every product in
   the catalogue stood between the page's opening claim and the
   three sections that explain it.

   Flex order fixes the read: card, lede, what it reports, the
   release standard, how to request, older lots, and the
   directory last, where an index belongs, after the steps that
   tell you what to do with it. The directory trio is targeted
   precisely: the only h2 on the page carrying a style attribute
   (functions.php:1320), its caption paragraph, and .coa-list.
   Same tab-order trade as section 1, same verdict; without
   :has() the page keeps its original order.

   Flex stops sibling margins collapsing, which would have
   quietly changed every gap on the page, so the separators are
   re-normalised below. Measured separator heights at 375 before
   this file: paragraph to h2 44px, list to h2 60px
   (60-chrome.css:356), directory h2 56px (its inline 40px plus
   16px padding). All become 44px: 12px sibling margin, 16px
   margin, 16px padding. On desktop the same three measured 60,
   64 and 60, and all become 60.
   ============================================================ */
.prose:has(.coa-cert) {
  display: flex;
  flex-direction: column;
}
.prose:has(.coa-cert) > h2[style],
.prose:has(.coa-cert) > h2[style] + p,
.prose:has(.coa-cert) > .coa-list { order: 2; }

/* Separator normalisation under flex, desktop then mobile. */
.prose:has(.coa-cert) > h2 { margin-top: 28px; }
.prose:has(.coa-cert) > .proof-card,
.prose:has(.coa-cert) > .step-cards { margin-bottom: 12px; }
.prose:has(.coa-cert) > div:has(> .coa-cert) { margin-bottom: 24px; }
.prose:has(.coa-cert) > h2[style] {
  /* !important: overrides the element's own style attribute,
     margin-top:40px, written by halotir_certificates_shortcode()
     at functions.php:1320. An attribute style outranks every
     selector, so there is no other way in from CSS. */
  margin-top: 28px !important;
}
@media (max-width: 767px) {
  .prose:has(.coa-cert) > h2 { margin-top: 16px; }
  .prose:has(.coa-cert) > h2[style] {
    /* !important: same inline margin-top:40px from
       functions.php:1320, mobile step of the rule above. */
    margin-top: 16px !important;
  }
}

/* ============================================================
   7 · Certificates page: one spacing scale for the panels
   ------------------------------------------------------------
   "Not super tight" measured out as panels that almost agree:
   the certificate card padded 30/20/26 (styles.css:341) while
   .proof-card runs 24/20 (15-signature.css) and .step-card runs
   20 (styles.css:1119); inside the card, seal to h2 16, h2 to
   p 8 (styles.css:353), button 20 (styles.css:355), fine print
   14 (styles.css:363), list margin 20 (styles.css:367).
   Normalised onto the 4px scale the neighbouring panels already
   use: card padding 24/20/24, seal to h2 12, h2 to p 8, button
   24, fine print 12, list 16.

   Type on the same page, measured then fixed: the card h2 at
   22px sat between the 32px page h1 and the 20px prose h2s, so
   it takes the 24px step from the research ramp; card body copy
   was 14.7px and step-card copy 14.4px, both under the 15px
   floor, both to 15px (at the card's 287px inner measure that
   is about 40 characters a line); the release panel's note was
   12px and takes the 13px floor 10-tokens.css names for copy a
   buyer actually reads.
   ============================================================ */
.prose:has(.coa-cert) .coa-cert { padding: 24px 20px; }
.prose:has(.coa-cert) .coa-cert h2 {
  font-size: 1.5rem;
  margin: 12px 0 8px;
}
.prose:has(.coa-cert) .coa-cert p { font-size: .9375rem; }
.prose:has(.coa-cert) .coa-cert .btn { margin-top: 24px; }
.prose:has(.coa-cert) .coa-cert .coa-fine { margin-top: 12px; }
.prose:has(.coa-cert) .step-card p { font-size: .9375rem; }
.prose:has(.coa-cert) .proof-card .proof-note { font-size: .8125rem; }
@media (min-width: 768px) {
  .prose:has(.coa-cert) .coa-cert { padding: 32px 28px 28px; }
}

/* ============================================================
   8 · Certificates page: rows that read as requests
   ------------------------------------------------------------
   The catalog directory (functions.php:1322) was hairline
   rows, 6px of padding on the row with a 44px link floating
   inside it, and a bare gold "Request" that could as easily be
   a download. Certificates are on request by policy, so a row
   has to look like the start of a correspondence, not a file.

   Three moves:
     Rows become quiet --tint cards on an 8px grid; the tint
     does the separating the hairlines were doing badly, the
     same trick the compound tiles use.
     The action becomes a visible pill in the certificate
     card's cream-and-gold language.
     Every action whose href is a mailto: wears an envelope
     glyph (section 9). The card's alternate branch, a hosted
     report at functions.php:1291, is http and never matches,
     so the icon is the honest tell: envelope means request,
     no envelope means document.

   Contrast, checked: --accent-deep #854d0e on --bg white is
   6.9:1 and on --accent-tint #fdf8ee is 6.5:1; --muted catalog
   numbers on --tint measure 4.6:1. All over the 4.5:1 floor.
   ============================================================ */
.prose:has(.coa-cert) .coa-list {
  border-top: 0;
  display: grid;
  gap: 8px;
  margin-top: 16px;
}
.prose:has(.coa-cert) .coa-item {
  background: var(--tint);
  border-bottom: 0;
  border-radius: var(--radius-sm);
  padding: 10px 12px 10px 16px;
  gap: 12px;
}
.prose:has(.coa-cert) .coa-item > div { min-width: 0; }
.prose:has(.coa-cert) .coa-item-sub { font-size: .8125rem; }
.prose:has(.coa-cert) .coa-item a[href^="mailto:"] {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-pill);
  background: var(--bg);
  color: var(--accent-deep);
  font-size: .875rem;
  font-weight: 600;
  white-space: nowrap;
}
@media (hover: hover) {
  .prose:has(.coa-cert) .coa-item a[href^="mailto:"]:hover {
    border-color: var(--accent);
    background: var(--accent-tint);
    color: var(--accent-deep);
  }
}

/* The certificate card's own request button takes the same
   language: white pill, gold line, dark-gold label, envelope.
   Outline-on-cream was the lightest thing on the card; the
   page's single action should not be. */
.prose:has(.coa-cert) .coa-cert .btn[href^="mailto:"] {
  background: var(--bg);
  border-color: var(--accent-line);
  color: var(--accent-deep);
  font-weight: 600;
}
.prose:has(.coa-cert) .coa-cert .btn[href^="mailto:"]:hover {
  border-color: var(--accent);
  background: var(--accent-tint);
  color: var(--accent-deep);
}

/* ============================================================
   9 · The envelope, drawn inline
   ------------------------------------------------------------
   Mask plus currentColor, so the glyph inherits each control's
   text colour and its states. A data: URI is bytes in this
   file, not a request, so the no-new-HTTP-requests rule holds.
   ============================================================ */
.prose:has(.coa-cert) .coa-cert .btn[href^="mailto:"]::before,
.prose:has(.coa-cert) .coa-item a[href^="mailto:"]::before {
  content: "";
  flex: none;
  width: 15px;
  height: 15px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='4' width='20' height='16' rx='3'/%3E%3Cpath d='m2.5 6.5 9.5 7 9.5-7'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='4' width='20' height='16' rx='3'/%3E%3Cpath d='m2.5 6.5 9.5 7 9.5-7'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ============================================================
   10 · Partner program: 375 sweep only
   ------------------------------------------------------------
   The owner likes this page, so this section fixes measurements
   and nothing else. Swept at 375: no horizontal overflow found
   (the widest fixed content is the two stat cells at 158px each
   and the proof figure at about 160px in a 287px measure); tap
   targets already clear 44 (.faq-q 56px at styles.css:573, the
   CTA 52px at 60-chrome.css:224, and the missing side gutter
   shipped in 99-mobile.css 9.1). What failed was type:

     .bento-cell p    13.4px (styles.css:1596) in a 121px-wide
                      column: styles.css:1587 keeps two columns
                      at 375, so benefit copy ran about 18
                      characters a line, six or seven lines deep.
     .stat-cap span   12px (15-signature.css) under the hero
                      rates: a full sentence at caption size.
     .proof-note      12px (60-chrome.css:231) on the dark card.

   The two captions take the 13px floor 10-tokens.css names for
   copy a buyer actually reads; the bento copy is body copy and
   takes 15px, with the grid stacking one-up below 480 so 15px
   has a measure to live in (about 37 characters a line). All
   scoped to .partner-page (partner.php:315), all inside the
   mobile query; desktop keeps its shipped look everywhere.
   ============================================================ */
@media (max-width: 767px) {
  .partner-page .bento-cell p {
    font-size: .9375rem;
    line-height: 1.5;
  }
  .partner-page .stat-cap span { font-size: .8125rem; }
  .partner-page .proof-note { font-size: .8125rem; }
}
@media (max-width: 479px) {
  .partner-page .bento { grid-template-columns: 1fr; }
  .partner-page .bento-cell {
    min-height: 0;
    padding: 18px 20px;
  }
}
/* ============================================================
   A8 · Mobile space
   ============================================================

   Written off a measured audit rather than a scroll through
   screenshots. tools/mobile-audit.js walks every page at 375px in
   two-thirds-of-a-screen steps and measures the real ink extent of
   every run of text against its neighbours and against the box it
   sits in; tools/collide.js is the measuring part. Everything here
   is a number that came back from it.

   The headline number was on the shop. A phone opened /shop/ and
   saw no products at all: the first card began at 846px on an
   812px screen, behind 313px of chrome that was not merchandise.
   A shop whose first screen contains no shop is the whole problem
   the rest of this file is arranging around.

   ------------------------------------------------------------ */

@media (max-width: 767px) {

  /* ----------------------------------------------------------
     1 · The shop hero stops taking a whole screen
     ----------------------------------------------------------
     249px for a headline and a lede, of which the lede alone was
     102px because it ran to four lines. It is a shop page. The
     promise belongs on it, but not at the size of a poster: the
     first row of bottles is the argument, and it was below the
     fold entirely.

     Padding and leading only. The words are untouched and the
     type is not shrunk, because the lede is a claim about purity
     and diluent that has to stay legible.
     ---------------------------------------------------------- */
  .shop-hero { padding-block: 20px 16px; }
  .shop-hero .wrap > p {
    margin-top: 8px;
    line-height: 1.45;
  }

  /* ----------------------------------------------------------
     2 · Search and sort share a line
     ----------------------------------------------------------
     Two 44px controls stacked with a gap is 114px to say "find
     something" and "order it". Side by side they are 44px and
     lose nothing: the search field still takes every pixel it can
     get, and the sort collapses to the width of its longest
     option, which is the only width it ever needed.
     ---------------------------------------------------------- */
  .store-tools {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;
    padding-bottom: 12px;
  }
  .store-tools .search-field { flex: 1 1 auto; min-width: 0; }
  .store-tools .sort-field {
    flex: 0 0 auto;
    width: auto;
    min-width: 104px;
    max-width: 36%;
  }
  .store-tools .sort-field select { width: 100%; }

  /* ----------------------------------------------------------
     3 · Products before promotions
     ----------------------------------------------------------
     Two full-width banners sat between the filters and the grid
     and cost 199px, which is most of the reason the first bottle
     was off screen. They are not being deleted, they are being
     put after the merchandise, which is where an interstitial
     belongs on a listing page.

     order works because the wrap becomes a column flex container
     here. Flex also stops the vertical margins collapsing, so the
     gaps below are set explicitly rather than inherited from
     whichever neighbouring margin used to win.
     ---------------------------------------------------------- */
  body.woocommerce-shop main > .wrap {
    display: flex;
    flex-direction: column;
  }
  body.woocommerce-shop main > .wrap > * { order: 1; }
  body.woocommerce-shop main > .wrap > .shop-promos {
    order: 2;
    margin-block: 26px 0;
  }

  .cat-strip { margin-block: 12px 14px; }
  .result-count { padding-bottom: 8px; }

  /* ----------------------------------------------------------
     3b · The home hero admits there is a page under it
     ----------------------------------------------------------
     The hero measured 639px and started at 154px, so it finished
     at 793 on an 812px screen: nineteen pixels of the proof strip
     showed. Not a hero that fills the fold, a hero that lands on
     it exactly, which reads as the whole site. Nothing invited a
     scroll and the first trust claim was hidden by a hair.

     Taken out of the collage rather than the copy, because the
     copy is the promise and the collage is decoration. Scaling
     rather than resizing keeps every bottle's position, overlap
     and bleed exactly as composed; the negative margin below it
     gives back the space the scale no longer occupies, since a
     transform does not change layout on its own.

     639 becomes about 590, the hero ends near 744, and the whole
     proof strip clears the fold: purity target, certificate, the
     row that answers "why you". That row is worth more on the
     first screen than 49px of bottle.
     ---------------------------------------------------------- */
  .hero-v6 { padding-bottom: 14px; }
  .hero-collage {
    transform: scale(.88);
    transform-origin: 50% 0;
    /* 268px of box, 12% of it given back. */
    margin-bottom: -28px;
  }

  /* ----------------------------------------------------------
     4 · The card meta stops ending in an ellipsis
     ----------------------------------------------------------
     "10 mg · Metabolic Research" measures 145px of text in a
     134px column, so every card on the grid ended mid-word.
     Twelve pixels of type fits it with a pixel to spare, and a
     line that is complete reads as smaller than a line that is
     cut, whatever the point size says.
     ---------------------------------------------------------- */
  .product-card .card-meta {
    font-size: .75rem;
    letter-spacing: -.01em;
  }

  /* ----------------------------------------------------------
     5 · The discount badge stops crowding itself
     ----------------------------------------------------------
     line-height: 1 on "20%" gives the line box exactly the point
     size, but Poppins draws a percent sign taller than that, so
     the digits and the "off" underneath were left with three
     pixels between them inside a 54px circle. The badge is a
     sticker; it should look pressed on, not squeezed in.
     ---------------------------------------------------------- */
  .promo-disc {
    width: 58px;
    height: 58px;
    gap: 2px;
  }
  .promo-disc b { font-size: 1rem; line-height: 1.05; }
  .promo-disc small { font-size: .6875rem; line-height: 1; }

  /* ----------------------------------------------------------
     6 · Label stacks that were one pixel apart
     ----------------------------------------------------------
     .bundle-sub sat 2px under .bundle-qty and .bundle-each sat
     1px under .bundle-unit. Both are a value with its caption, and
     a caption that close stops reading as a separate thing and
     starts reading as a second line of the number.
     ---------------------------------------------------------- */
  .bundle-sub { margin-top: 5px; }
  .bundle-each { margin-top: 4px; }
}

/* ------------------------------------------------------------
   7 · The promo code sits in the middle of its pill
   ------------------------------------------------------------
   letter-spacing puts its space after the last character as well
   as between them, so "HALO20" carried 1.44px of tracking past
   the final zero and the whole word sat that far left of centre
   inside the gold pill. Adding the same amount back as a text
   indent moves the ink to where the padding says it should be.

   Not inside the media query: the pill is the same on every
   width and so is the arithmetic.
   ------------------------------------------------------------ */
.promo-code {
  justify-content: center;
  /* Half the tracking, not all of it. The trailing space sits on one
     side of the word, so the ink is off centre by half its width, and
     indenting by the full .16em simply moved it a pixel the other way.
     Measured after: 17.6px of pill to the left of the H, 17.6px to the
     right of the final zero. */
  text-indent: .08em;
}
/* ============================================================
   v8 · The shelf, and the band above it
   ------------------------------------------------------------
   Two changes, one cause.

   The sitewide code is applied to every cart automatically now
   (halotir_autoapply_promo), so the discounted figure is what
   the cart charges rather than what it would charge if somebody
   found a button and pressed it. Once that is true two things
   follow that were not available before:

   1. Prices can be struck. Every card, rail and buy box shows
      the list price crossed out beside the charged price. The
      old rule against this was right about the old mechanic and
      does not survive it.

   2. The offer bar has nothing to instruct. It states a fact, so
      it stops needing two rows, a four-unit countdown ticking
      seconds twenty-six days out, and a decorative halo to make
      an occasion of it.

   And the card was 443px tall on a 375px phone, which is 55% of
   the viewport for one tile of ten, because six pieces of
   information each had their own full-width row. Everything that
   is a marker rather than a sentence moves onto the picture,
   where there is empty tint on every SKU.

   Loads last, after A8, so equal-specificity rules in the older
   parts are simply overridden rather than fought with !important.
   ============================================================ */

/* ------------------------------------------------------------
   0 · <ins> is not underlined here
   ------------------------------------------------------------
   The charged price is marked up as <del>/<ins> rather than two
   spans, because that is the only thing in HTML that says which
   of two prices is the live one, and a screen reader reading
   "sixty four ninety nine, fifty one ninety nine" with no
   relationship between them is a coin toss. The UA sheet
   underlines <ins>, which on a price reads as a link.
   ------------------------------------------------------------ */
.was,
.now { text-decoration: none; }
del.was { text-decoration: line-through; }

/* ============================================================
   1 · The product card
   ============================================================ */

/* ------------------------------------------------------------
   1a · The tile
   ------------------------------------------------------------
   New box, and the thing that makes the overlay layout possible.
   The badges used to position against .product-card, which
   worked only because the media happened to be the first child;
   the proof pill has to sit on the bottom edge of the PICTURE,
   and the picture is not the bottom of the card.

   Square, not 4/5. On a 158px card that is 158px of art instead
   of 197px, and the vial is drawn rather than photographed so it
   loses nothing by being smaller: 39px of the 138px this rebuild
   saves comes from here alone. It also puts the proof pill and
   the price within one thumb-length of each other, which is the
   pair the shopper is actually comparing across tiles.
   ------------------------------------------------------------ */
.card-tile {
  position: relative;
  isolation: auto; /* see the z-index note below: no new stacking context. */
}

.product-card .product-media,
.rail-card .rail-media .product-media {
  aspect-ratio: 1 / 1;
}

/* The bottle keeps its presence at the smaller size, and rides a
   little high so the proof pill lands on plain glass rather than
   across the gold label band.

   margin:auto is doing the centring and is not optional.

   The vial is position:absolute with inset:0, which fills the tile
   only for as long as nothing constrains its size. Capping the
   height and the width here made the box smaller than its own
   inset rectangle, which over-constrains the equation, and CSS
   resolves that by discarding `right` and `bottom` in a
   left-to-right document. Every bottle on the site pinned itself
   to the top-left corner of its tile with 92px of dead space down
   the right-hand side and 72px underneath: on a 330px tile that is
   a quarter of the width, which is plainly visible and reads as
   the art having slipped rather than as a margin.

   With all four insets at 0, `margin: auto` centres an absolutely
   positioned box on both axes, which is the one line that was
   missing. translateY still does the optical lift on top of it. */
.product-card .product-media .vial,
.rail-card .rail-media .vial {
  height: 104%;
  max-width: 92%;
  margin: auto;
  transform: translateY(-5%);
}
@media (hover: hover) {
  .product-card:hover .product-media .vial,
  .rail-card:hover .rail-media .vial { transform: translateY(-5%) scale(1.05); }
}

/* ------------------------------------------------------------
   1b · The two markers, opposite corners
   ------------------------------------------------------------
   Deliberately unequal. They were two gold pills of the same
   size once, which read as one striped lump on a bottle whose
   own label is printed in gold.

   The discount is live, sitewide, and the reason the price below
   is struck, so it takes solid ink and rhymes with the Add
   button. The featured marker is an annotation, so it is a quiet
   warm pill. Different weight, different colour, different
   corner: there is no contest between them to resolve.

   Both are inert. .card-link covers the whole tile and these sit
   above it on the z axis, so without pointer-events:none they
   would eat a tap near the top of the card and do nothing with
   it. .promo-badge already has that from 99-mobile; .card-flag
   is new to this position and needs it too.
   ------------------------------------------------------------ */
.rail-badge,
.promo-badge {
  top: 8px;
  padding: 4px 9px;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .01em;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
}
.promo-badge {
  left: auto;
  right: 8px;
  background: var(--ink);
  color: #fff;
  box-shadow: 0 1px 2px rgba(28, 25, 23, .18);
}

.product-card .card-flag {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 4;
  pointer-events: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 0;
  padding: 4px 9px 4px 8px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .92);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--accent-deep);
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: .02em;
  white-space: nowrap;
}
/* Two absolutely positioned pills in the same 8px band cannot see
   each other, so the flag has to be told where the badge starts.
   The badge is 55px of pill plus its 8px inset; 70px leaves the
   gap between them visible rather than merely non-overlapping.
   Without this the flag simply ran underneath it and "Lab
   favorite" was served as "Lab favorit". */
.product-card .card-flag {
  max-width: calc(100% - 70px);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Below 480px it goes entirely, because a two-up phone grid gives
   the tile about 130px and the guard above would clip the label
   to "Lab f...", which is worse than absent. Of the two markers
   the discount is the one that changes the price, so it is the
   one that stays. The favourites are not lost with it: the shop
   sorts them to the top of the grid and the product page still
   says so on the page you land on. */
@media (max-width: 479px) {
  .product-card .card-flag { display: none; }
}

/* The dot survives the move. It is what stops a small warm pill
   from reading as a second discount flash. */
.product-card .card-flag::before {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

/* ------------------------------------------------------------
   1c · The proof pill, on the bottom edge of the picture
   ------------------------------------------------------------
   This was two chips in the card body taking 66px of height, and
   it was the single biggest reason the card ran to 443px: more
   vertical space than the price and the button combined. On the
   art it costs nothing, because the bottom of a vial cutout is
   empty tint on all ten SKUs.

   The pill is inert and the certificate link inside it is not.
   .card-link covers the tile, so anything meant to be separately
   clickable has to sit above it AND anything not meant to be
   clickable has to let taps through, or the purity figure
   becomes a 90px dead zone over the product link.

   Nothing here creates a stacking context on purpose: .card-tile
   and .card-proof both stay at z-index auto, so .cp-coa's 3 and
   .card-link's 2 still compare directly in the card's context.
   Giving either wrapper a z-index would trap the link's 3 inside
   it and hand the tap back to the card.
   ------------------------------------------------------------ */
.card-proof {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  z-index: 3;
  pointer-events: none;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 0;
  margin: 0;
  min-height: 26px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .94);
  box-shadow: 0 1px 3px rgba(28, 25, 23, .10);
}

/* Both halves lose the chip treatment they had as free-standing
   objects: inside one pill they are two fields, not two buttons,
   and two nested pills is the striped-lump problem again. */
.card-proof .cp-purity,
.card-proof .cp-coa {
  min-height: 0;
  padding: 3px 7px;
  border-radius: var(--radius-pill);
  background: none;
  box-shadow: none;
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1.2;
  white-space: nowrap;
}
.card-proof .cp-purity {
  color: var(--vivid-sage);
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
/* The tick is the whole reason this reads as verification rather
   than as a spec. It is decoration for a screen reader, which
   gets the words either way. */
.card-proof .cp-purity::before {
  content: "✓";
  font-size: .6875rem;
  line-height: 1;
}

/* The certificate is the one live control on the picture. */
.card-proof .cp-coa {
  pointer-events: auto;
  position: relative;
  z-index: 3;
  color: var(--ink-2);
  text-decoration: none;
}
/* Hairline between the two fields, and only when there are two:
   Halotir H2O carries no purity figure, so on that card the pill
   is the certificate alone and a leading divider would be a rule
   with nothing on its left. */
.card-proof .cp-purity + .cp-coa::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 1px;
  height: 11px;
  transform: translateY(-50%);
  background: var(--line-2);
}
@media (hover: hover) {
  .card-proof .cp-coa:hover { color: var(--accent-deep); box-shadow: none; }
}

/* "on request" is the wording and is not negotiable: certificates
   are held per lot and emailed, and no card may imply a library
   of published reports that does not exist. It is abbreviated
   below 768px purely because 142px of pill cannot hold it, and it
   is abbreviated rather than replaced. The visible word says less
   than the truth there, never more, and the full phrase is still
   in the accessibility tree and one tap away on the page it
   links to. */
.card-proof .cp-more {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* ------------------------------------------------------------
   1c-bis · The proof pill on a rail, which is a grid
   ------------------------------------------------------------
   styles.css:1559 carries

       .rail-media > div { width: 100%; height: 100%;
                           display: grid; place-items: center; }

   written when .rail-media had exactly one child, the wrapper
   around the vial. Putting the proof pill inside .rail-media so
   it could sit over the picture made it a second <div> child, so
   it took that rule too: 100% of the box, 94% white, and a
   9999px radius. The result was a white disc the size of the
   whole tile lying on top of the vial at 94% opacity, with the
   two chips flung to opposite ends of a 313px grid. Every card
   in "Most requested" and in the PDP's related rail rendered as
   a blank pale tile.

   The shop grid was untouched because those cards put the pill
   in .card-tile, which is a plain positioned box. This is the
   rail-only correction; .rail-media > .card-proof (0,2,0) beats
   .rail-media > div (0,1,1), and styles.css is not edited.

   top:auto matters as much as the rest. With height:100% and
   bottom:8px both set, the used top resolves to -8px, which is
   how the disc ended up hanging off the top edge as well.
   ------------------------------------------------------------ */
.rail-media > .card-proof {
  width: auto;
  height: auto;
  top: auto;
  display: inline-flex;
  place-items: center;
}

@media (min-width: 768px) {
  .card-proof .cp-more {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
  }

  /* The space in "COA on request" is in the markup and it still
     disappeared, because .cp-coa is a flex container: the bare
     word and the span are two flex items, and flex layout throws
     the whitespace between items away. It read "COAon request".
     Below 768 the span is out of flow and there is only one item,
     so the gap is inert there and this can stay unconditional if
     it ever needs to be. */
  .card-proof .cp-coa { gap: .3em; }

  .card-proof { min-height: 28px; }
  .card-proof .cp-purity,
  .card-proof .cp-coa { font-size: .6875rem; padding: 3px 9px; }
}

/* ------------------------------------------------------------
   1d · The body, down to four things
   ------------------------------------------------------------
   Name, what it is, what it costs, and the button. Everything
   else moved onto the tile. .card-row is gone from the markup
   entirely; the heading is a direct child now.
   ------------------------------------------------------------ */
.card-body {
  padding: 11px 11px 12px;
  gap: 0;
}
.card-body > h3 {
  margin: 0;
  font-size: .9375rem;
  font-weight: 650;
  line-height: 1.25;
  letter-spacing: -.005em;
  /* One line, with an ellipsis. Two-line clamping was what let a
     long name push the price and the button down on one card in a
     row and not the others; the full name is on the page the card
     links to. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.product-card .card-meta {
  margin: 2px 0 0;
  font-size: .75rem;
  line-height: 1.35;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ------------------------------------------------------------
   1e · The price pair
   ------------------------------------------------------------
   Struck list price, then the charged one. The order is the
   order the whole category uses and it is the right one: the eye
   lands on the struck figure, registers it as crossed out, and
   arrives at the live number already knowing it is lower.

   Deep gold on the live figure, matching .price-row .now in the
   buy box. Crimson is the category convention and does read as
   "reduced", but on a page whose only accent is gold it arrives
   as a second accent and an error colour at once. The ink badge
   over the picture is what carries the loudness.
   ------------------------------------------------------------ */
.card-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 3px 7px;
  margin: 8px 0 0;
  font-family: var(--font-num);
  line-height: 1.15;
  white-space: normal;
}
.card-price .was {
  font-size: .78rem;
  font-weight: 500;
  color: var(--muted);
  text-decoration-thickness: 1.5px;
}
.card-price .now,
.card-price > .amount {
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--accent-deep);
}
/* No promotion running: one figure, and it is not a sale price,
   so it takes ink rather than gold. */
.card-price > .amount { color: var(--ink); }

/* ------------------------------------------------------------
   1f · The button
   ------------------------------------------------------------
   Filled, not outlined. It is the only control in the body now
   and the card has room for exactly one piece of emphasis down
   there; an outlined pill under a gold price reads as the
   quieter of two options when it is the only option.
   ------------------------------------------------------------ */
.product-card .qa-btn,
.rail-card .qa-btn {
  margin-top: 10px;
  height: 40px;
  border-color: var(--ink);
  background: var(--ink);
  color: #fff;
  font-size: .8125rem;
  font-weight: 600;
}
.product-card .qa-btn:hover,
.rail-card .qa-btn:hover { background: #000; border-color: #000; }
.product-card .qa-btn.added,
.rail-card .qa-btn.added { background: var(--success); border-color: var(--success); }

/* 40px of pill with the hit area stretched back to 44px, rather
   than a 44px pill. The card is short now and the four extra
   pixels are worth more as air than as button; the overhang is
   safe because .card-price sits 10px above it and is not a
   control. Same pattern as .qa-btn::before was before 99-mobile
   retired it, restored here for the same reason it existed. */
.product-card .qa-btn::before,
.rail-card .qa-btn::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.product-card .qa-btn,
.rail-card .qa-btn { position: relative; }

.product-card .card-btn {
  margin-top: 10px;
  height: 40px;
}

@media (min-width: 1024px) {
  .card-body { padding: 14px 14px 15px; }
  .card-body > h3 { font-size: 1rem; }
  .product-card .card-meta { font-size: .8125rem; }
  .card-price .now { font-size: 1.15rem; }
  .product-card .qa-btn,
  .rail-card .qa-btn,
  .product-card .card-btn { height: 42px; }
}

/* ------------------------------------------------------------
   1g · The rails, kept in step
   ------------------------------------------------------------
   The home rail and the related rail run the same component with
   different wrappers, so the proof pill moved on all three
   surfaces at once. Only the body selectors differ.
   ------------------------------------------------------------ */
.rail-card .rail-media { position: relative; }
.rail-body h3 {
  font-size: .9375rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rail-cat {
  font-size: .75rem;
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rail-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 7px;
  margin-top: 8px;
  padding-bottom: 0;
  line-height: 1.15;
}
.rail-price .was {
  font-size: .78rem;
  font-weight: 500;
  color: var(--muted);
}
/* `>` and not a descendant selector, and that is the whole fix.
   wc_price() renders <span class="amount"> INSIDE the <del>, so
   `.rail-price .amount` reached the struck figure too and painted
   it gold at the live figure's size. The rail showed "$24.99
   $19.99" in two golds, with only the strikethrough telling them
   apart. The card grid was already written with the child
   combinator, which is why it was only ever wrong here. */
.rail-price .now,
.rail-price > .amount {
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--accent-deep);
}
/* Belt and braces: whatever the live figure is styled as, the
   struck one is muted and quiet. */
.rail-price .was,
.rail-price .was .amount,
.card-price .was,
.card-price .was .amount {
  color: var(--muted);
  font-weight: 500;
}
/* The caption belongs to the live figure, so it breaks to its own
   line rather than sitting between the two prices. */
.rail-price small {
  flex: 1 0 100%;
  margin-top: 1px;
  font-size: .7rem;
  color: var(--muted);
}
.rail-actions { margin-top: 10px; }

/* ============================================================
   2 · The offer bar
   ------------------------------------------------------------
   89px and two rows on a phone, for a message that no longer
   asks for anything. One row, one band height, and the
   decoration stands down where it has no gutter to sit in.

   --strip-h (58px) is kept rather than beaten. It is the shared
   height of every band under the header and the reason the promo
   bar, the free-shipping line and the research-use notice read
   as a stack rather than as three unrelated accidents. 89 to 58
   is 31px of a 812px viewport handed back, and the row fits
   inside 58 comfortably.
   ============================================================ */
@media (max-width: 639px) {
  .promo-bar {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding-block: 8px;
    padding-inline: 12px;
  }

  /* The campaign name goes. It is the least load-bearing thing in
     the band, it was the reason the bar needed a second row, and
     it survives on every wider screen. What a phone gets instead
     is the sentence that changed: the discount is already on. */
  .promo-eyebrow,
  .promo-rule { display: none; }

  .promo-line { flex: 0 1 auto; min-width: 0; }

  .promo-offer {
    font-size: 1.0625rem;
    line-height: 1.1;
    white-space: nowrap;
  }

  .promo-act {
    flex: 0 1 auto;
    min-width: 0;
    gap: 7px;
    /* nowrap, and this is the whole reason the bar was still 87px
       after the row was rebuilt: .promo-act inherits wrap from
       95-promo, so when its three children came to 217px in a
       351px row it broke its own line inside a bar that was
       already a single row, and the band grew to hold two. */
    flex-wrap: nowrap;
  }

  .promo-code {
    flex: none;
    /* 10-tokens gives every promo code a 44px min-height, which is
       the right instinct and the wrong shape here: 44px of solid
       gold inside a 58px band is three quarters of the band. The
       pill shrinks to 30 and the ::after below stretches the hit
       area back over the full height of the row, which is the same
       trade already made at .qa-btn and .cp-coa. */
    min-height: 30px;
    height: 30px;
    padding: 0 12px;
    font-size: .6875rem;
    letter-spacing: .08em;
  }
  .promo-code::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }
  .promo-code { position: relative; }

  .promo-auto {
    flex: none;
    font-size: .625rem;
    letter-spacing: .01em;
  }
  /* "Applied automatically" is 144px of a 351px row. On a phone the
     word that carries the meaning is the first one; the second is
     the kind of precision a desktop has room for. */
  .promo-auto-more { display: none; }

  /* The end date is a desktop luxury. On a phone the row has
     three objects already and a date twenty-six days out is the
     one a shopper is least deciding on. It comes back the moment
     it is actually deciding anything: main.js adds .is-urgent
     inside the last 48 hours and the countdown takes the slot
     the reassurance was in, because at that point the deadline
     IS the reassurance's replacement. */
  .promo-timer { display: none; }
  .promo-bar.is-urgent .promo-timer { display: inline-flex; }
  .promo-bar.is-urgent .promo-auto { display: none; }

  /* The halo and the horizon need a reserved gutter to sit in.
     On a phone there is no gutter, only the pills they were
     crossing, and a ring drawn across a code is not decoration.
     Both are pure ornament, so both simply stop below 640. */
  .promo-sun,
  .promo-bar::before { display: none; }
}

/* Under 360px the three parts still fit, but only just: measured on
   a 320px viewport the row comes to 246px of content in a 300px
   track. The offer gives back the two points it can spare so the
   margin is a margin rather than a rounding error, and nothing is
   dropped. (An earlier draft hid "Applied" here. It was both
   unnecessary and inert: the base rule below sets display on the
   same selector at the same specificity and later in the file, so
   it won regardless. Deleting it beats moving it.) */
@media (max-width: 359px) {
  .promo-bar { gap: 7px; padding-inline: 10px; }
  .promo-offer { font-size: .9375rem; }
}

/* ------------------------------------------------------------
   2b · "Applied", the line that replaced "Code"
   ------------------------------------------------------------
   The pill used to be labelled with the word "Code", which named
   the object beside it and told nobody anything. This says what
   the store has already done with it, which is the only new
   information the bar carries.
   ------------------------------------------------------------ */
.promo-auto {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-num);
  font-size: .6875rem;
  font-weight: 500;
  letter-spacing: .02em;
  color: rgba(232, 200, 126, .82);
  white-space: nowrap;
}
.promo-auto::before {
  content: "✓";
  font-size: .75rem;
  line-height: 1;
  color: rgba(232, 200, 126, .95);
}

/* Not a countdown any more, most of the time. A date is calm, it
   is identical for every visitor, and it therefore survives being
   cached publicly for a week, which a clock never could. */
.promo-timer {
  font-size: .6875rem;
  letter-spacing: .02em;
  padding: 4px 10px;
}

@media (min-width: 640px) {
  .promo-bar { gap: 10px 18px; }
  .promo-act { gap: 10px; }
}

/* ------------------------------------------------------------
   2c · The tablet band, where the bar quietly went back to two
        rows
   ------------------------------------------------------------
   95-promo puts the bar on one row from 640 up, and that was
   true until this rebuild changed what is on the row. "Applied
   automatically" is 144px where the label it replaced ("CODE")
   was about 40, and .promo-bar keeps flex-wrap:wrap, so the
   overflow does not clip or squeeze, it silently folds.

   Measured on an iPad at 768: the band is 753px with 24px of
   left padding and 132px reserved on the right for the halo, so
   597px of track for 625px of content. Twenty-eight pixels over,
   and the bar renders 97px tall, which is taller than the phone
   bar it was supposed to improve on and taller than the desktop
   bar it is supposed to match.

   Fixed by priority rather than by shrinking type. The bar has
   five things to say and they do not rank equally: the offer,
   the code and the fact that it is already applied are the
   message; the occasion and the end date are trim. So between
   640 and 899 the trim goes, exactly as it does on a phone, and
   the halo's gutter comes in by 24px to buy the margin back.
   That leaves 508px of track for 446px of content at the 640
   edge and more everywhere above it. Full sentence and end date
   return at 900, where 744px of track holds all 625px of it.
   ------------------------------------------------------------ */
@media (min-width: 640px) and (max-width: 899px) {
  .promo-bar { padding-right: 108px; }
  .promo-auto-more { display: none; }
  .promo-timer { display: none; }

  /* The gutter shrank, so the shape that lives in it has to as
     well, or the ring crosses the code pill. Same proportion,
     same amount outside the band, 24px less of it inside. */
  .promo-sun { width: 128px; height: 128px; right: -30px; bottom: -50px; }
  .promo-sun::before { width: 82px; height: 27px; }
}

/* ============================================================
   3 · The buy box, and the sticky bar
   ------------------------------------------------------------
   The offer line under the price used to be "Save 20% with code
   HALO20 · tap to apply", with a real button. There is nothing
   to tap now, so it is a sentence: no link colour, no underline,
   no 44px target reserved for a control that no longer exists.
   ============================================================ */
.price-coupon.is-auto {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 5px 12px 5px 10px;
  border-radius: var(--radius-pill);
  background: var(--tint-sage);
  box-shadow: inset 0 0 0 1px rgba(47, 122, 82, .22);
  color: var(--vivid-sage);
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1.35;
}
.price-coupon.is-auto::before {
  content: "✓";
  flex: none;
  font-size: .875rem;
  line-height: 1;
}
.price-coupon.is-auto b {
  color: inherit;
  font-weight: 800;
  letter-spacing: .04em;
}

/* Sticky-bar variant. .price-coupon-sm styled a .coupon-tap child
   that no longer exists, so the pill treatment moves onto the
   element itself. */
.price-coupon-sm.is-auto {
  flex: none;
  margin: 0;
  padding: 3px 9px 3px 8px;
  gap: 4px;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .03em;
  line-height: 1;
}
.price-coupon-sm.is-auto::before { font-size: .75rem; }

/* The bar quotes two figures now, on a strip that is one line of
   chrome over the page. The struck one shrinks rather than the
   live one. */
.pdp-bar-price .was { font-size: .8125rem; margin-right: 5px; }

/* ============================================================
   4 · The shop hero, trimmed
   ------------------------------------------------------------
   222px of headline and standfirst above a grid that the
   visitor came to scroll. The reference has no hero on its
   store page at all, which is one of the reasons theirs reads
   as shorter before a single card is measured.

   Trimmed rather than deleted. The H1 is the page's only
   heading and the paragraph is one of the four places the
   research-use condition is stated; losing either to save a
   scroll would be trading a structural thing for a cosmetic
   one. What goes is the slack: 37px of display type where 32
   reads the same at arm's length, a 16px standfirst that ran
   to four lines where 14.5px runs to three, and the padding
   around both.
   ============================================================ */
@media (max-width: 767px) {
  .shop-hero { padding-block: 16px 14px; }
  .shop-hero h1 {
    font-size: 2rem;
    line-height: 1.14;
  }
  .shop-hero p {
    margin-top: 7px;
    font-size: .9rem;
    line-height: 1.45;
  }
}


/* ------------------------------------------------------------
   3 · The coupon row that discounts nothing
   ------------------------------------------------------------
   Set in the muted voice rather than the savings green: the row
   is still a coupon row, but this half of it is an explanation,
   not an amount, and it should not compete with the figure it is
   explaining the absence of. See halotir_inert_coupon_amount().
   ------------------------------------------------------------ */
.coupon-inert {
  color: var(--muted);
  font-size: .8125rem;
  font-weight: 400;
  font-style: normal;
}
.sum-row.saving:has(.coupon-inert) { color: var(--muted); }


/* ------------------------------------------------------------
   4 · A lone promo tile
   ------------------------------------------------------------
   .shop-promos is a two-column grid from 1024 up and carried two
   tiles, Build a box and Partner program. The first went with
   that feature, which left the second sitting at half width with
   an empty column beside it. One tile takes the full row.
   ------------------------------------------------------------ */
.shop-promos > .shop-promo:only-child { grid-column: 1 / -1; }

/* ============================================================
   v8.1 — PDP tightening, the bump card, the offer pill
   ============================================================ */

/* ------------------------------------------------------------
   The bottle sits lower on the product page
   ------------------------------------------------------------
   The stage padding was 14px top and 40px bottom, which is a
   26px upward bias on a square tile. That was written when the
   chips overlapped further up the tile than they do now, and it
   left the bottle floating in the top two thirds with a band of
   empty tint under it. Evening the bias out drops it about 12px
   without taking the clearance the chip row overlaps into. */
.pdp-stage img.vial,
.pdp-stage .vial { padding: 26px 14px 30px; }

/* ------------------------------------------------------------
   The offer line stops being a pill when it stops being one line
   ------------------------------------------------------------
   .price-coupon.is-auto is an inline-flex pill with a fully
   round radius, which is correct for the sentence it was
   designed around and wrong for the sentence it got. On a 375px
   phone the copy wraps to three lines inside a shape whose ends
   are still drawn as half-circles of the full box height, so it
   renders as a green lozenge with enormous curved caps and text
   ragged inside it.

   Two changes, and the copy is the more important of them: the
   sentence lost "Nothing to enter", which said the same thing
   "applied automatically" had already said one clause earlier.
   That alone takes it to two lines on a phone. The shape then
   stops pretending: below 600px it is a left-aligned rounded
   rectangle that wraps like a paragraph, with the tick pinned to
   the first line rather than floating in the vertical middle of
   a three-line block. */
/* Flex is the wrong display type for this box and always was.
   The sentence contains a <b> around the coupon code, so its
   children are three flex items, not one paragraph: a text run,
   a bold run, and the full stop after it. `gap: 6px` then pushed
   them apart, which is why the desktop pill read "code HALO20 ."
   with a visible space before the period, and why wrapping it on
   a phone justified "HALO20" to the far right of its own line
   with a hole in the middle of the sentence.

   Block layout, with the tick lifted out of flow. Inline
   children behave like a sentence again, the letter-spacing on
   the code is cancelled on its right edge so it does not fake a
   space of its own, and the wrap is an ordinary text wrap. */
.price-coupon.is-auto {
  display: block;
  position: relative;
  gap: 0;
  padding: 6px 13px 6px 30px;
  line-height: 1.4;
}
.price-coupon.is-auto::before {
  position: absolute;
  left: 12px;
  top: 6px;
}
/* Tracking adds its gap AFTER the last letter as well as between
   letters, so a tracked run followed by a full stop always looks
   like it has a space in it. Taken back off the right edge. */
.price-coupon.is-auto b { margin-right: -.04em; }

@media (max-width: 599px) {
  .price-coupon.is-auto {
    width: 100%;
    padding: 9px 13px 9px 30px;
    border-radius: 12px;
  }
  .price-coupon.is-auto::before { top: 9px; }
}

/* The sticky bar's variant has no <b> and never wraps, so it
   keeps the centred pill. Restated because the rules above are
   written on the shared .is-auto class. */
.price-coupon-sm.is-auto {
  display: inline-flex;
  align-items: center;
  position: static;
  width: auto;
  padding: 3px 9px 3px 8px;
  gap: 4px;
  border-radius: var(--radius-pill);
}
.price-coupon-sm.is-auto::before { position: static; }

/* ------------------------------------------------------------
   The bump card's Add button and its dismiss X stop colliding
   ------------------------------------------------------------
   The X was absolutely positioned at top:6px right:6px, and the
   Add button is the last cell of a grid that ends at the same
   right edge. A 30px circle at the top of that corner and a 40px
   pill centred in it occupy the same 20px band, so on a phone
   the X sat on top of the button: two controls, one target, and
   whichever one the finger got was down to a few pixels.

   Taking the X out of absolute positioning is the whole fix. It
   becomes a fourth grid cell, aligned to the top of the row, and
   the grid guarantees it cannot overlap its sibling because that
   is what grid does. The invisible bleed target underneath it
   still gives it 44px to be pressed by; only the geometry that
   made it a floating sticker is gone.

   minmax(0, 1fr) on the text column and not 1fr: the h4 wraps,
   and a bare 1fr floors at the widest unbreakable word, which
   would push the button off the edge on a narrow drawer instead
   of wrapping. */
.cd-bump {
  grid-template-columns: 56px minmax(0, 1fr) auto auto;
  grid-template-rows: auto auto;
  /* Two rows, and the thumbnail spans both. The eyebrow gets a
     cell of its own rather than living inside the text column,
     which is what lets the phone layout below hand it the full
     width of the card without touching the markup again.

     The card does not get taller for it: rows 1 and 2 come to
     about 50px of content and the 56px thumbnail spanning them
     is what sets the height, exactly as it did when all three
     lines were stacked in one cell. */
  grid-template-areas:
    "media eyebrow add dismiss"
    "media body    add dismiss";
  column-gap: 10px;
  row-gap: 2px;
}
.cd-bump-media   { grid-area: media; }
.cd-bump-eyebrow { grid-area: eyebrow; align-self: end; }
.cd-bump-body    { grid-area: body; align-self: start; }
.cd-bump-add     { grid-area: add; }
/* relative, NOT static, and the difference is the whole card.
   50-cart.css gives this button a `::after { position: absolute;
   inset: -7px }` as an invisible 44px tap target. An absolutely
   positioned pseudo resolves against its nearest POSITIONED
   ancestor, and taking the button out of position entirely
   handed that job to .cd-bump, which is position:relative. The
   bleed stopped being a 44px pad around a 30px X and became a
   sheet 7px larger than the whole bump, laid over the heading,
   the price and the Add button. Every click on the upsell was
   landing on Dismiss: elementFromPoint at the dead centre of
   "Add" returned .cd-bump-dismiss on every viewport tested.

   position:relative keeps it in its grid cell exactly as static
   did, since relative does not remove it from flow, and gives
   the pseudo back the containing block it was written for. */
.cd-bump-dismiss {
  grid-area: dismiss;
  position: relative;
  align-self: start;
  justify-self: end;
  top: auto;
  right: auto;
  margin-top: 2px;
}

/* Three lines: label, name, reason. The price is not among them
   any more, it is the button, so nothing in this column is a
   figure and the whole column can be set tight.

   Everything here is a class and nothing is a bare element,
   which is not a style preference. The old price line was a <p>
   and this file styled it as `.cd-bump p`, which is 0,1,1. A
   `.cd-bump-eyebrow` is 0,1,0 and loses to it. Reintroducing the
   eyebrow as a <p> would have silently taken the leader's 12px
   floor back off it and set it at .875rem, from a rule written
   for a price that no longer exists. There is no <p> left in the
   card and no rule matching one.

   The 12px floor is why the eyebrow is not 9px like theirs. That
   floor is ours and it stays; the tracking and the caps do the
   same work at a size that can be read. */
.cd-bump-body { min-width: 0; }

.cd-bump h4 { line-height: 1.3; }

.cd-bump-eyebrow {
  display: block;
  line-height: 1.2;
}

.cd-bump-sub {
  display: block;
  margin-top: 1px;
  font-size: .75rem;
  line-height: 1.3;
  color: var(--muted);
  /* One line or an ellipsis, never two. The row's height is set
     by the 56px thumbnail beside it, and a descriptor that wraps
     is the one thing in this card that can push past it. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A glyph and a figure, so it has to be a flex box: a button
   with two children and no display of its own centres them as
   inline content and drops the icon a hair below the numeral's
   baseline. */
.cd-bump-add {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0 15px;
  white-space: nowrap;
}
.cd-bump-add svg {
  width: 13px;
  height: 13px;
  flex: 0 0 13px;
  stroke-width: 2.2;
}

/* ------------------------------------------------------------
   The bump on a phone
   ------------------------------------------------------------
   Three columns instead of four, and the eyebrow takes the whole
   top row.

   The numbers, at 375px: the card is 343px wide and 321px of
   that is usable. Nested in the text column the eyebrow had
   112px to render 159px of "COMPLETE YOUR ORDER", so it ran out
   from under its own cell and across the price button. Shaving
   the tracking, the thumbnail, the gaps and the button padding
   together recovered about 42px of the 47px needed, which is
   both a lot of fiddling and still short, and it would have
   failed outright at 320px.

   Giving it a row costs one 14px line and about 10px of card
   height, and every string renders whole at the size the type
   floor asks for. The dismiss rides the end of that row, so it
   costs nothing at all.
   ------------------------------------------------------------ */
@media (max-width: 560px) {
  .cd-bump {
    grid-template-columns: 48px minmax(0, 1fr) auto;
    grid-template-areas:
      "eyebrow eyebrow dismiss"
      "media   body    add";
    column-gap: 9px;
  }
  .cd-bump-media { width: 48px; height: 48px; }
}

/* ------------------------------------------------------------
   Three tiers, so three columns
   ------------------------------------------------------------
   The ladder lost a rung: 1 / 3+ / 10+ where it used to be
   1 / 2 / 3+ / 10+. The grid was written for four and neither
   of its two shapes survives an odd count. Two-up on a phone
   left the bulk tile alone on a second row at half width, and
   repeat(4, 1fr) on desktop left a quarter of the row empty
   with three tiles hugging the left edge.

   One column count at every width, because three is a number
   that works at both: three tiles across a 375px phone is about
   107px each, which is the same tile width the two-up grid was
   already giving the 320px case. */
.bundle-rows { grid-template-columns: repeat(3, 1fr); }

@media (min-width: 900px) {
  .bundle-rows { grid-template-columns: repeat(3, 1fr); }
}

/* The figures stack instead of running together. `each` is a
   unit, `save $194.90` is the argument, and at 107px they were
   never going to share a line. Kept at the same total height as
   the old two-line block by taking the leading down. */
.bundle-each,
.bundle-save { display: block; line-height: 1.3; }
.bundle-save { color: var(--accent-deep); font-weight: 650; }

@media (max-width: 479px) {
  /* Narrower tiles, so the two figures that set the tile's
     minimum width both come down a step. Everything else in the
     tile already wraps. */
  .bundle-unit { font-size: 1rem; }
  .bundle-each,
  .bundle-save { font-size: .625rem; }
  .bundle-vials { height: 38px; }
  .bundle-card { padding: 14px 6px 12px; min-height: 138px; }
}
/* ==========================================================================
   B2-pay-checkout.css — paying on the checkout page, before the order

   Rendered by Halotir_Gateway_Manual::payment_fields() inside WooCommerce's
   #payment .payment_box, for halotir_zelle and halotir_crypto only.

   This is the panel a buyer actually sends money from, so it is not the
   compact hint text .payment_box was built for. 50-cart.css sizes that box
   for one line of gateway description; everything here widens it back out
   and then lays out a two-step flow inside it.

   The sibling file is 80-pay.css, which styles the same information after
   the order exists. The two are deliberately separate: the post-order panel
   is a receipt and can be quiet, this one is a task and cannot be.
   ========================================================================== */

/* Give the box room. Scoped with :has() so every other gateway's payment
   box keeps the compact treatment it was designed for. */
body.woocommerce-checkout #payment div.payment_box:has(.pay-co) {
  padding: 16px;
  font-size: 1rem;
  color: var(--ink-2);
  background: var(--panel-2, var(--tint));
  border: 1px solid var(--line);
}

.pay-co-intro {
  margin: 0 0 14px;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-2);
}

.pay-co-intro p {
  margin: 0;
}

/* ---------- the two steps ---------- */

.pay-co-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

.pay-co-step {
  position: relative;
  display: grid;
  /* minmax(0, 1fr) rather than 1fr: the body holds a 42-character wallet
     address, and an implicit auto track floors at min-content and pushes
     the whole checkout wider than the phone. */
  grid-template-columns: 24px minmax(0, 1fr);
  gap: 0 12px;
  padding-bottom: 22px;
}

.pay-co-step:last-child {
  padding-bottom: 0;
}

/* The rail joining step 1 to step 2. Sits behind the numbers, which carry
   their own background so the line appears to stop at each of them. */
.pay-co-step:not(:last-child)::before {
  content: "";
  position: absolute;
  top: 26px;
  bottom: 6px;
  left: 11px;
  width: 2px;
  background: var(--line);
}

/* Outlined and muted, not a filled black disc. The checkout around this
   panel already numbers its own steps with filled black discs, and a
   matching marker two levels in reads as a fifth peer step rather than as
   a sub-step of Payment. Same ordering cue, visibly subordinate. */
.pay-co-num {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border: 1.5px solid var(--line-2, var(--line));
  border-radius: 50%;
  background: var(--bg);
  color: var(--muted);
  font-family: var(--font-num, inherit);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.pay-co-body {
  min-width: 0;
  padding-top: 3px;
}

.pay-co-lede {
  margin: 0 0 14px;
  font-size: 1rem;
  font-weight: 650;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* ---------- where the money goes ---------- */

.pay-co-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}

.pay-co-grid > * {
  min-width: 0;
}

.pay-co-rows {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pay-co-rows li {
  display: flex;
  gap: 14px;
  align-items: baseline;
  justify-content: space-between;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}

.pay-co-rows li:first-child {
  padding-top: 0;
}

.pay-co-rows li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.pay-co-k {
  flex: none;
  font-size: 0.8125rem;
  color: var(--muted);
}

.pay-co-v {
  min-width: 0;
  font-size: 0.9375rem;
  font-weight: 650;
  color: var(--ink);
  text-align: right;
  overflow-wrap: anywhere;
}

/* The amount and the reference: the two strings a buyer retypes into a
   banking app. Size rather than colour, because a tinted row reads as a
   promotion and this is an instruction. */
.pay-co-rows li.is-key .pay-co-v {
  font-family: var(--font-num, inherit);
  font-size: 1.0625rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.pay-co-block-k {
  margin: 14px 0 6px;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* Monospace, because a proportional face makes 0/O and 1/l the same shape
   and there is no undo on a mistyped wallet address. */
.pay-co-block-v {
  margin: 0;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--ink);
  overflow-wrap: anywhere;
  user-select: all;
}

.pay-co-qr {
  margin: 0;
  text-align: center;
}

.pay-co-qr img {
  display: block;
  width: 160px;
  height: 160px;
  max-width: 100%;
  margin: 0 auto;
  padding: 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  /* Always white, never a theme token. A QR needs light modules under the
     dark ones to scan, and a panel colour is no guarantee of that. */
  background: #fff;
}

.pay-co-qr figcaption {
  margin-top: 7px;
  font-size: 0.75rem;
  color: var(--muted);
}

.pay-co-note {
  margin: 14px 0 0;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-2);
}

.pay-co-note b {
  font-weight: 650;
  color: var(--ink);
}

.pay-co-note.is-quiet {
  margin-top: 8px;
  font-size: 0.8125rem;
  color: var(--muted);
}

/* ---------- confirming it ---------- */

.pay-co-field {
  margin: 0 0 14px;
}

.pay-co-field label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}

/* A chip, not a second label. At the label's own size this wraps onto a
   line of its own on a phone and reads as another field name. */
.pay-co-opt {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--tint);
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  vertical-align: 1px;
}

body.woocommerce-checkout #payment .pay-co-input {
  width: 100%;
  padding: 12px 13px;
  border: 1px solid var(--line-2, var(--line));
  border-radius: var(--radius-sm);
  background: var(--bg);
  /* 16px exactly. Anything smaller and iOS Safari zooms the page on focus,
     which on a checkout drops the buyer somewhere they did not ask to be. */
  font-size: 16px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ink);
}

.pay-co-hint {
  display: block;
  margin-top: 6px;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--muted);
}

/* The gate. A card rather than a bare checkbox: this is the control that
   places the order, and it has to look like a decision. */
.pay-co-tick {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin: 0;
  padding: 14px;
  border: 1px solid var(--line-2, var(--line));
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
  transition: border-color var(--motion, 200ms ease),
              background-color var(--motion, 200ms ease),
              box-shadow var(--motion, 200ms ease);
}

body.woocommerce-checkout #payment .pay-co-tick input[type="checkbox"] {
  flex: none;
  width: 22px;
  height: 22px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.pay-co-tick > span {
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--ink);
}

.pay-co-tick b {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.pay-co-tick:hover {
  border-color: var(--ink-2);
}

/* Confirmed. The colour change is the receipt for the tap, and it is the
   only thing on the page that says the order can now go through. */
.pay-co-tick:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-tint, var(--tint));
}

.pay-co-tick:focus-within {
  box-shadow: var(--ring);
}

/* ---------- wider screens ---------- */

@media (min-width: 560px) {
  body.woocommerce-checkout #payment div.payment_box:has(.pay-co) {
    padding: 20px 22px;
  }

  .pay-co-step {
    grid-template-columns: 28px minmax(0, 1fr);
    gap: 0 14px;
  }

  .pay-co-step:not(:last-child)::before {
    top: 30px;
    left: 13px;
  }

  .pay-co-num {
    width: 28px;
    height: 28px;
    font-size: 0.8125rem;
  }

  /* The QR moves alongside once the rows can spare the width. */
  .pay-co-grid {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 24px;
  }

  .pay-co-qr img {
    width: 172px;
    height: 172px;
  }
}
/* ============================================================
   Research-use-only gate
   ------------------------------------------------------------
   The confirmation that stands between the water SKU and the
   cart. Structure and copy are in inc/ruo-gate.php, behaviour is
   in assets/ruo.js, and the reasoning for all three is in the
   header of the PHP file.

   Two things about this file are load-bearing rather than
   cosmetic.

   The z index is 110: above --z-drawer (100), because the cart
   drawer's own upsell is one of the paths that opens this and
   the dialog has to land on top of it, and below --z-toast
   (120), because an add that fails still has to be able to say
   so. That is the same slot the category leader's sits in, for
   the same reason.

   And the scroll lock is a SECOND class, not a shared one. When
   this opens over the drawer the page is already held still by
   body.drawer-open; closing the dialog must not hand the scroll
   back while the drawer is still up. Two independent rules, each
   removed by its own owner, and the page stays locked until the
   last one lets go.
   ============================================================ */

body.ruo-open { overflow: hidden; }

.ruo-gate {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  /* The panel is short and the viewport rarely is, but a 380px
     card in a 420px landscape phone still has to be reachable. */
  overflow-y: auto;
}
.ruo-gate[hidden] { display: none; }

.ruo-scrim {
  position: fixed;
  inset: 0;
  background: rgba(28, 25, 23, .34);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 220ms var(--ease-soft);
}
.ruo-gate.open .ruo-scrim { opacity: 1; }

.ruo-panel {
  position: relative;
  width: 100%;
  max-width: 380px;
  margin: auto;
  padding: 26px 24px 22px;
  background: var(--bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  text-align: left;
  opacity: 0;
  transform: translateY(10px) scale(.97);
  transition:
    opacity 240ms var(--ease-out),
    transform 240ms var(--ease-out);
}
.ruo-gate.open .ruo-panel {
  opacity: 1;
  transform: none;
}

/* ---------- close ---------- */

.ruo-x {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: background var(--motion), color var(--motion);
}
.ruo-x:hover { background: rgba(28, 25, 23, .05); color: var(--ink); }
.ruo-x svg { width: 15px; height: 15px; }

/* ---------- copy ---------- */

.ruo-eyebrow {
  /* Tracking this wide puts its own gap after the last letter as
     well as between them, which is why nothing is allowed to sit
     on the same line as this. It is a lone word on a row of its
     own and stays that way. */
  font-family: var(--font-num);
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  /* Room for the close button, which overlaps this row. */
  padding-right: 34px;
}

.ruo-title {
  margin-top: 6px;
  font-size: 1.3125rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
}
.ruo-title em {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  /* Deliberately not the accent. A gold flourish on a legal
     attestation reads as decoration, and decoration is the cue
     that tells people a box is safe to dismiss unread. */
  color: inherit;
}

.ruo-lede {
  margin-top: 9px;
  font-size: .875rem;
  line-height: 1.6;
  color: var(--ink-2);
}

/* ---------- the three lines ---------- */

.ruo-terms {
  margin-top: 16px;
  list-style: none;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.ruo-terms li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  font-size: .8125rem;
  line-height: 1.4;
  color: var(--ink-2);
}
.ruo-terms li + li { border-top: 1px solid var(--line); }
.ruo-terms svg {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  /* Muted, not accent-gold and not green. Every line here is a
     prohibition, and a bright approving tick beside "Not for
     human use" says the opposite of the sentence next to it. */
  color: var(--muted);
}

.ruo-fine {
  margin-top: 12px;
  font-size: .6875rem;
  line-height: 1.5;
  color: var(--muted);
}

/* ---------- actions ---------- */

/* Nothing here but width and the gap above it. .btn already sets
   the 48px min-height, the pill radius and the type; restating any
   of that would be a second copy of the button system that drifts
   the first time the real one is touched. */
.ruo-ok {
  width: 100%;
  margin-top: 16px;
}

.ruo-cancel {
  width: 100%;
  height: 36px;
  margin-top: 6px;
  border: 0;
  background: none;
  font-family: var(--font);
  font-size: .875rem;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--motion);
}
.ruo-cancel:hover { color: var(--ink); }

/* ---------- small screens ---------- */

@media (max-width: 419px) {
  .ruo-panel { padding: 24px 18px 20px; }
  .ruo-title { font-size: 1.1875rem; }
}

@media (prefers-reduced-motion: reduce) {
  .ruo-scrim,
  .ruo-panel { transition: none; }
  .ruo-panel { transform: none; }
}

/* ============================================================
   Checkout · one promo row, not two
   ------------------------------------------------------------
   WooCommerce's coupon field now renders inside .co-promo rather
   than as its own band underneath it (see the comment above
   halotir_checkout_promo_row()). That means three things have to
   be re-laid-out here: the row becomes a wrapping flex line so
   the link can sit beside the applied sentence, the notice
   WooCommerce prints has to stop looking like a notice, and the
   form has to drop onto a full-width line of its own when it
   opens.
   ============================================================ */

.co-promo { flex-wrap: wrap; }

/* Both of these are full-width rows under the sentence. order
   puts the form last no matter which way WooCommerce emitted
   them, so the note never lands between the link and the field
   it opens. */
.co-promo .woocommerce-form-coupon-toggle,
.co-promo form.checkout_coupon { flex: 1 0 100%; }
.co-promo form.checkout_coupon { order: 3; }
.co-promo-note { order: 4; }

/* The toggle is the exception: it is three words and it belongs
   ON the sentence's line, pushed to the far end. */
.co-promo .woocommerce-form-coupon-toggle {
  flex: 0 0 auto;
  margin-left: auto;
  order: 2;
}

/* The toggle is a plain link now (see the template override at
   woocommerce/checkout/form-coupon.php). Colour comes from the
   row so it turns green with it once a code is on. */
.co-promo .showcoupon {
  color: inherit;
  font-size: .875rem;
  font-weight: 600;
  line-height: 1.4;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  cursor: pointer;
}
.co-promo .showcoupon:hover { color: var(--ink); }

/* Applying a second code drops WooCommerce's success notice in
   through $form.before(), which lands it inside this row. Same
   treatment: a line of text, not a band inside a band. */
.co-promo .woocommerce-message,
.co-promo .woocommerce-info {
  flex: 1 0 100%;
  order: 3;
  margin: 8px 0 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-size: .875rem;
  line-height: 1.4;
}
.co-promo .woocommerce-message::before,
.co-promo .woocommerce-info::before { display: none; }

.co-promo-note {
  flex: 1 0 100%;
  margin-top: 6px;
  font-size: .8125rem;
  line-height: 1.45;
  color: var(--ink-2);
}
.co-promo.is-on .co-promo-note { color: var(--ink-2); }

/* The field itself, on its own full-width line under the sentence.
   WooCommerce hides it until .showcoupon is clicked and then
   slideToggles it, so display is not ours to set; everything else
   is. Capped at 420px because a promo code is six characters and a
   field that runs the whole width of a desktop row reads as the
   main event, which it is not. */
body.woocommerce-checkout .co-promo form.checkout_coupon {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  max-width: 420px;
  margin: 10px 0 0;
  padding: 0;
  border: 0;
  background: none;
}
body.woocommerce-checkout .co-promo form.checkout_coupon p { margin: 0; }
.co-promo .co-code-field { flex: 1 1 auto; min-width: 0; }
.co-promo .co-code-go { flex: 0 0 auto; }

/* Not .form-row, so none of checkout's 52px field styling reaches
   it: this one is sized to sit level with .co-promo-tap beside it.
   16px font is the iOS zoom floor and is not negotiable. */
body.woocommerce-checkout .co-promo #coupon_code {
  width: 100%;
  min-height: 40px;
  padding: 9px 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-pill);
  background: var(--bg);
  color: var(--ink);
  font-size: 16px;
  line-height: 1.25;
  transition: border-color var(--motion);
}
body.woocommerce-checkout .co-promo #coupon_code:focus {
  outline: none;
  border-color: var(--ink);
}
body.woocommerce-checkout .co-promo #coupon_code::placeholder { color: var(--muted); }
.co-promo .co-code .co-promo-tap { white-space: nowrap; }

/* checkout.js appends this to the field's wrapper on a bad code. */
.co-promo .coupon-error-notice {
  display: block;
  margin: 6px 0 0;
  padding: 0 16px;
  font-size: .8125rem;
  line-height: 1.4;
  color: #dc2626;
}
body.woocommerce-checkout .co-promo #coupon_code.has-error { border-color: #dc2626; }
/* ============================================================
   B4 · Touch targets and the type floor
   ============================================================
   Everything here came out of one measurement pass: a real
   mobile Chrome at 390 and 375 CSS pixels, walking fourteen
   pages plus the open cart drawer and the checkout, recording
   the used box of every control and the computed size of every
   text run. Two lists came back.

   The first is controls whose smallest side measured under 44
   pixels. Forty four is not a style preference, it is the
   figure both platform guidelines land on and the one WCAG
   2.5.8 writes down, and the reason it exists is that a thumb
   is not a cursor. Inline links inside a sentence are excluded
   by that success criterion and are excluded here too: giving
   a link in a paragraph a 44px hit area means overlapping the
   lines above and below it, which trades one miss for two.

   The second is text under 12 pixels. 10-tokens.css already
   sets the house floor, 11px for a tracked uppercase badge and
   13px for anything a buyer has to read, with the reasoning
   written out there. The runs corrected below are ones that
   were set before that floor existed or by a later part that
   did not know about it. Nothing here lowers a size, and
   nothing here changes a size that was already at the floor on
   purpose: the promo bar's 11px code pill, the nav drawer's
   group headings and the dose labels are all deliberate and
   all left alone.

   This part loads last, so equal specificity resolves here.
   Where a rule below has to out-rank a descendant selector in
   the base sheet the extra qualifier is noted on the rule.
   ============================================================ */


/* ------------------------------------------------------------
   1 · The upsell card in the cart drawer
   ------------------------------------------------------------
   The card offers reconstitution solution beside the vial that
   needs it, and it was carrying two circular objects on its
   right side: the black add button, and a dismiss drawn as a
   white disc with a hairline ring. The disc is the louder of
   the two at a glance, because it is the lightest shape on a
   cream card, and it is attached to the one action on the card
   nobody is being asked to take.

   The same drawer already has the answer four pixels above it.
   Every line item carries .cd-x, which is a bare grey glyph on
   no background, and it reads as exactly what it is. The
   dismiss now matches it. This is a subtraction, not a new
   treatment: no disc, no ring, same muted ink, same glyph size
   as the sibling it sits under.

   The geometry is untouched on purpose. B1-shop-v8.css:1035
   documents what happens when this button stops being
   positioned: its ::after bleed hands its containing block to
   .cd-bump, which is position:relative, and the 7px halo
   becomes a sheet over the entire card that swallows every tap
   aimed at the add button. It stays positioned. Only paint
   changes here.
   ------------------------------------------------------------ */
.cd-bump-dismiss {
  border: 0;
  background: none;
  color: var(--muted);
  /* .cd-x on the line items above renders at 17px. Matching it is
     the point of the rule; 13px on no background reads as a stray
     character rather than a control. */
  font-size: 17px;
}
.cd-bump-dismiss:active { color: var(--ink); }
@media (hover: hover) {
  .cd-bump-dismiss:hover { background: none; color: var(--ink); }
}

/* Measured 91 by 40. The button is the whole reason the card is
   in the drawer and it was the shortest control on it. */
.cd-bump-add {
  height: 44px;
  min-height: 44px;
}


/* ------------------------------------------------------------
   2 · Controls that measured short
   ------------------------------------------------------------ */

/* 40 by 44. 10-tokens.css raised the height of every drawer close
   and left the width at the 40 the base sheet set, so the button
   came out four pixels narrow on one axis only. width:40px in the
   base stands; a larger min-width simply wins the used value. */
.cd-close { min-width: 44px; }

/* 134 by 34. 8px of padding on an 18px line. */
.cd-promo-toggle { min-height: 44px; }

/* 40 by 40, absolutely placed in the corner of the checkout's
   summary sheet. Growing it four pixels on each axis would push
   the glyph off the optical centre of the corner, so the offsets
   come in by two to hold it where it already reads. */
.hl-sheet-x {
  width: 44px;
  height: 44px;
  top: 12px;
  right: 8px;
}

/* The coupon toggle at the top of checkout measured 98 by 40, and
   the coupon remove link in the totals 41.6. Both are corrected at
   the rule that owns them, 50-cart.css:2419, rather than here: that
   rule already pairs a floor with a negative margin so the row does
   not move, and a second floor in a later part would have been an
   override arguing with a decision instead of correcting it. */

/* 94 by 40, inside WooCommerce's own cart notice. Stock button,
   stock markup, so this is the one place the fix is a floor
   rather than a redesign. */
body.woocommerce-checkout .woocommerce-message .button,
body.woocommerce-checkout .woocommerce-info .button {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* The "ship to a different address" row. The input is 20 by 20 and
   the label that wraps it, which is what actually receives the tap,
   measured 22 pixels tall. The checkbox keeps its size; the row it
   sits in becomes a row. */
body.woocommerce-checkout .woocommerce-form__label-for-checkbox.checkbox {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin: 0;
}
body.woocommerce-checkout .woocommerce-form__label-for-checkbox.checkbox
.woocommerce-form__input-checkbox {
  flex: none;
  width: 20px;
  height: 20px;
  margin: 0;
}

/* The certificate link inside the proof pill, 31 by 18, and the
   smallest real target measured anywhere on the site. It sits on
   the picture with .card-link underneath it, which is why it was
   never given a bigger box: every pixel it grows is a pixel the
   card link stops receiving.

   So it does not grow. The visible pill is unchanged and an
   invisible bleed carries the hit area to 49 by 44 around it,
   which is the same trade 10-tokens.css already made at .box-coa
   and 99-mobile.css made at the promo code pill. ::after, not
   ::before: the hairline divider between the purity figure and
   the certificate is a ::before and is still there.

   pointer-events is inherited from .cp-coa, which sets auto
   against the pill's none, so the bleed is live without a second
   declaration. z-index is not restated for the same reason it is
   not set on the wrappers: giving this element a stacking context
   would trap its own 3 and hand the tap back to the card. */
.card-proof .cp-coa::after {
  content: "";
  position: absolute;
  inset: -13px -9px;
}


/* ------------------------------------------------------------
   3 · The type floor, where later parts fell through it
   ------------------------------------------------------------ */

/* 10px. Set by the proof-pill rebuild in B1-shop-v8.css, which
   shrank both fields to fit a 26px pill on the bottom edge of the
   picture. The pill has the room for the badge floor: at 11px the
   two fields measure 120.6px inside a pill that is 141.5px wide on
   a 375px viewport and 149px on a 390px one, and neither wraps. */
.card-proof .cp-purity,
.card-proof .cp-coa { font-size: .6875rem; }
.card-proof .cp-purity::before { font-size: .75rem; }

/* 11.2px. "per vial" is the caption that tells a buyer the price
   is not for the box, which makes it the readable tier, not the
   badge tier. 40-plp-home.css had it right at 12px and the rail
   rebuild took it back down. */
.rail-price small { font-size: .75rem; }

/* 11.52px, and it is a heading on a compliance block. The sitewide
   FDA notice had the identical problem and 10-tokens.css settled
   it at 13px with the reasoning written out: a disclaimer nobody
   can read is a disclaimer that is not really there. Same tier,
   same figure. */
.research-notice h3 { font-size: .8125rem; }

/* 9.92px, and the only text on the site under ten pixels. It is a
   numeral in an 18px disc, so there is room; the size was set
   before the floor existed. */
.cart-count { font-size: .6875rem; }

/* 11px, in the sticky bar under the PDP. Sentence case, so it is
   read rather than scanned, which puts it above the badge tier.
   12 rather than 13 because the bar is one line of chrome and the
   figure beside it is the thing being read first. */
.price-coupon-sm.is-auto { font-size: .75rem; }

/* The three sub-11px runs on a product page, found on the second
   pass once the audit was pointed at the real product permalinks
   rather than at a slug that had been 404ing since the catalogue
   was renamed. */

/* 10px, under a 33.6px figure, in the "most researchers save"
   panel. Tracked uppercase, so it is the badge tier and 11 is its
   floor. The figure above it is doing all the shouting either way,
   which is the argument for raising the caption rather than for
   leaving it as the smallest thing in the block. */
.mws-fig small { font-size: .6875rem; }

/* 10px each, in the bundle tiles. B1-shop-v8.css:1167 takes both
   figures down a step under 480px so three tiles fit across, and
   the arithmetic there was right about the width and wrong about
   which line to spend it on: "save $194.90" is a dollar amount a
   buyer is deciding on, not a unit label. Measured at 390px each
   tile has 95px of track inside its padding and the longest string
   in the set, "save $135.00", renders 73.4px at 12px on one line,
   so the step down was buying room that was never needed. */
@media (max-width: 479px) {
  .bundle-each,
  .bundle-save { font-size: .75rem; }
}

/* 9.1px, and the smallest text anywhere on the site: a formula
   subscript at .7em of a 13px monospace run. Subscripts are meant
   to be smaller than their line, so the fix is not to flatten them
   but to give the line they sit on something to be smaller than.
   The sequence beside it gets the same rise and needs it more: a
   forty character amino-acid string at 13px monospace is the
   hardest thing to read on the page, and it already breaks on
   words, so a larger size costs lines rather than overflow. */
.spec-formula,
.spec-seq { font-size: .875rem; }
.spec-formula sub { font-size: .72em; }
/* ============================================================
   Bottles: rasterise once, then only move
   ------------------------------------------------------------
   Reported as the bottle images "shaking and glitching", which is
   a different fault from the gate field sliding on a URL-bar
   transition (96-gate.css) and needed a different fix. That one
   was geometry. This one is rasterisation.

   Every animated bottle on this site is the same shape:

       a wrapper that animates `transform`, forever
         -> an <img> carrying `filter: drop-shadow(...)`

   .hero-vial, .gs and .rain-tile are all built that way, and on
   the rails and the product page the animation and the filter sit
   on the same element. A filtered element inside a continuously
   transformed ancestor is not something a browser can composite
   by moving a finished layer: unless the subtree is promoted, it
   re-runs the filter and re-samples the source bitmap every
   frame, at a slightly different subpixel offset each time. On a
   phone that lands as edge crawl along the glass and the cap,
   which is exactly what "glitching" describes.

   Measured on the live home page, worst first: the hero bottles
   draw a 1100px-tall cutout into boxes 88 to 212px tall, which is
   2.6x to 6.3x more source pixels than a 2x screen can use, and
   every one of those resamples was being redone per frame. The
   rain tiles are 3x over on the small cutout. Oversampling did
   not cause the shimmer, but it is what made each redundant
   resample expensive enough to see.

   `will-change: transform` is the whole fix: it promotes the
   subtree, so the bottle and its shadow are rastered once and
   the compositor thereafter only translates a finished texture.
   `backface-visibility: hidden` is carried alongside it because
   several mobile engines still take that as the stronger hint
   and will otherwise re-raster a promoted layer anyway.

   THE .is-lit GATE IS NOT OPTIONAL, and it is the reason this
   file cannot simply say `.gs { will-change: transform }`.
   90-home-hero.css:247 and 96-gate.css:106 already hold the
   animations back until every cutout has decoded, because a
   promoted layer with nothing rastered into it paints as the page
   background: twelve pale rectangles across the first screen a
   visitor ever sees. `will-change` promotes on its own, animation
   or not, so it has to wait behind the same gate or it
   reintroduces that bug without the animation to blame.
   ============================================================ */

/* Home hero collage. Gated on .is-lit exactly like the floats it
   is promoting; the four slots are named individually rather than
   through .hero-vial so this cannot outlive the composition. */
.hero-v6 .hero-collage.is-lit .hero-vial.v1,
.hero-v6 .hero-collage.is-lit .hero-vial.v2,
.hero-v6 .hero-collage.is-lit .hero-vial.v3,
.hero-v6 .hero-collage.is-lit .hero-vial.v4 {
  will-change: transform;
  backface-visibility: hidden;
}

/* The gate's twelve. Same gate, same reason, twelve times over:
   this is the one screen where an unrastered promoted layer would
   be the entire first impression. */
.gate-scatter.is-lit .gs {
  will-change: transform;
  backface-visibility: hidden;
}

/* The falling tiles behind the dark promo panel. No decode gate
   here because these carry a background and a border of their
   own, so a layer that promotes a frame early paints a chip
   rather than a hole. */
.rain-tile {
  will-change: transform;
  backface-visibility: hidden;
}

/* Where the float and the filter share one element: the rail
   media bottle and the product-page bottle. Promoting the image
   itself is the same trade, one layer instead of one resample per
   frame. Scoped to the two surfaces that actually animate rather
   than to .vial, which matches thirty static thumbnails on the
   home page alone and would promote every one of them. */
.rail-media-inner .vial,
.pdp-media .vial,
.pdp-stage img.vial {
  will-change: transform;
  backface-visibility: hidden;
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------
   With the animations gone there is nothing to composite, and a
   promoted layer that never moves is memory spent on a still
   image. Every selector above is restated here rather than
   filtered, because `will-change: auto` has to beat them on the
   cascade and they are not all the same specificity.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .hero-v6 .hero-collage.is-lit .hero-vial.v1,
  .hero-v6 .hero-collage.is-lit .hero-vial.v2,
  .hero-v6 .hero-collage.is-lit .hero-vial.v3,
  .hero-v6 .hero-collage.is-lit .hero-vial.v4,
  .gate-scatter.is-lit .gs,
  .rain-tile,
  .rail-media-inner .vial,
  .pdp-media .vial,
  .pdp-stage img.vial {
    will-change: auto;
  }
}
/* ============================================================
   The bottles hold still
   ------------------------------------------------------------
   Sebastian reported the bottles glitching up and down three
   times, across two attempted fixes. Before this file was
   written the motion was instrumented in both engines, at iPhone
   size, at rest and under a frame-by-frame scroll:

     Chromium   max frame-to-frame step 0.30px, travel 10-14px
     WebKit     max frame-to-frame step 0.57px, travel 10-14px

   Both traces are clean monotonic ease-in-out curves with no
   discontinuity anywhere, and both engines report full support
   for animation-timeline, animation-composition, svh and
   will-change. There is no rendering fault in the float. What is
   on screen IS the float: a dozen bottles each drifting 10 to
   14px on periods between 4 and 7 seconds, under drop shadows,
   and on the gate that is the entire first screen.

   So this is not another guess at a glitch. The motion itself is
   what was being objected to, and it stops here.

   `animation-play-state: paused` rather than `animation: none`,
   and the distinction matters. `none` snaps every element to its
   base transform, which for the promo rain means eleven tiles
   that were spread down a 920px fall all stacking at the top of
   the panel in a row. Paused holds each one exactly where it
   already is: the hero and the gate at their slot positions
   (their delays are positive, so they are still at the from
   frame), the rain scattered down its fall by the negative
   delays it was built with. Every composition on the site keeps
   the shape it has right now and only loses the drift. It is
   also the idiom already in use here, at 40-plp-home.css:648.

   TO BRING THE MOTION BACK: delete this file and rebuild. That
   is the whole revert. Nothing else was changed to achieve it,
   and B5-vial-raster.css is left intact underneath so the
   compositor promotion is still correct the moment anything
   moves again.
   ============================================================ */

.hero-v6 .hero-vial,
.hero-v6 .hero-collage.is-lit .hero-vial.v1,
.hero-v6 .hero-collage.is-lit .hero-vial.v2,
.hero-v6 .hero-collage.is-lit .hero-vial.v3,
.hero-v6 .hero-collage.is-lit .hero-vial.v4,
.hero-vials .hv,
.gate-scatter .gs,
.gate-scatter.is-lit .gs,
.rain-tile,
.rail-media-inner .vial,
.pdp-media .vial,
.pdp-stage img.vial,
img.vial {
  animation-play-state: paused;
}

/* A paused layer never moves, so the promotion B5 asks for is
   memory spent holding a texture that nothing will translate.
   Released here rather than by editing B5, so that deleting this
   one file restores both the motion and the compositing it
   needs, together, with nothing left half-applied. */
.hero-v6 .hero-collage.is-lit .hero-vial.v1,
.hero-v6 .hero-collage.is-lit .hero-vial.v2,
.hero-v6 .hero-collage.is-lit .hero-vial.v3,
.hero-v6 .hero-collage.is-lit .hero-vial.v4,
.gate-scatter.is-lit .gs,
.rain-tile,
.rail-media-inner .vial,
.pdp-media .vial,
.pdp-stage img.vial {
  will-change: auto;
}
