  /* ============================================================
     DESIGN TOKENS
     A small, systematic scale for color and spacing — same idea
     as Tailwind's config: name the values once, reference them
     everywhere, never hardcode a raw hex or rem in a component.
     ============================================================ */
  /* A Eurostile headline experiment lived here and has been removed. The
     @font-face pointed at a font file that is no longer in the repo, so it
     silently fell back to Space Grotesk while firing a 404 on every page
     load of every page.

     Keeping the findings, since the idea may come back:
       - Eurostile Extended Black measures ~1.5x the width of Space Grotesk
         at the same size. On nav links, buttons and the proof strip that is
         what blew the layout apart; on headlines it is the point.
       - Applying it to headlines only works WITHOUT touching any markup:
         redefine the token on the elements rather than setting font-family,
         i.e. `h1, h2, .promo-title{--font-display:var(--font-headline);}`.
         Four h2s on index.html carry an inline style="font-family:..." that
         beats any normal rule, but cannot beat a redefined custom property.
       - Both font files supplied so far were unlicensed rips with real
         defects (missing glyphs in one, a corrupt cmap in the other). A
         licensed copy from Monotype/Fonts.com/Adobe Fonts is the prerequisite,
         not an optional nicety. */
  :root{
    /* ---- Brand base colors ---- */
    --color-ember-500:#F3591D;
    --color-ember-400: color-mix(in srgb, var(--color-ember-500) 85%, white);
    --color-ember-600: color-mix(in srgb, var(--color-ember-500) 85%, black);
    --color-charcoal-500:#292929;
    --color-charcoal-600:#3a3937;   /* warm charcoal — the atmosphere/haze tone */
    --color-air-white-500:#F5F3EE;
    --color-near-black-500:#151515;
    --color-black-900:#0B0B0B;      /* deeper than the page bg — vignette/depth only */
    --color-white:#ffffff;          /* pure white — reserved for the placeholder texture only */
    /* Gold. --color-gold-500 is the long-standing rating-star value and must
       not move — every star on the site reads from it. The 200 and 700 stops
       exist so the Best Value badge can be a metallic gradient rather than a
       flat yellow: a single hue reads as "yellow label", while a pale-to-deep
       ramp across the pill is what the eye accepts as metal. */
    --color-gold-200:#FFE9A8;
    --color-gold-500:#FFC53D;
    --color-gold-700:#B8821A;

    /* ---- Semantic aliases: what a component actually asks for ---- */
    --color-bg: var(--color-near-black-500);
    --color-bg-elevated: var(--color-charcoal-500);
    /* Footer base — a hair of charcoal so it reads as a plinth under the CTA. */
    --color-bg-plinth: color-mix(in srgb, var(--color-charcoal-500) 40%, var(--color-near-black-500));
    /* Full-bleed section band — breaks the page out of one flat near-black column. */
    --color-bg-band: color-mix(in srgb, var(--color-charcoal-500) 55%, var(--color-near-black-500));
    --color-bg-surface: var(--color-air-white-500);
    --color-bg-card: var(--color-white);
    --color-text: var(--color-air-white-500);        /* default text on dark */
    --color-text-inverse: var(--color-near-black-500); /* default text on light */
    --color-accent: var(--color-ember-500);
    --color-accent-hover: var(--color-ember-400);
    /* Rating stars, and the one badge that has to outrank an ember pill —
       both deliberately distinct from the ember accent. */
    --color-star: var(--color-gold-500);
    /* The badge takes the full ramp, not the flat mid-tone. */
    --gradient-gold: linear-gradient(145deg,
      var(--color-gold-200) 0%,
      var(--color-gold-500) 46%,
      var(--color-gold-700) 100%);
    --gold-alpha-45: color-mix(in srgb, var(--color-gold-500) 45%, transparent);
    /* Deliberate addition (2026-10-22): the faint fill inside the citation
       badge's gold ring, on the same 12% step as --accent-alpha-12. */
    --gold-alpha-12: color-mix(in srgb, var(--color-gold-500) 12%, transparent);
    --gold-sheen: color-mix(in srgb, var(--color-white) 55%, transparent);

    /* ---- Alpha ramps, generated from the two text colors ----
       Every "text at X% opacity" value in the design reduces to
       one of these — same pattern as Tailwind's text-white/60 etc. */
    --text-on-dark-85: color-mix(in srgb, var(--color-air-white-500) 85%, transparent);
    --text-on-dark-78: color-mix(in srgb, var(--color-air-white-500) 78%, transparent);
    --text-on-dark-75: color-mix(in srgb, var(--color-air-white-500) 75%, transparent);
    --text-on-dark-70: color-mix(in srgb, var(--color-air-white-500) 70%, transparent);
    --text-on-dark-60: color-mix(in srgb, var(--color-air-white-500) 60%, transparent);
    --text-on-dark-55: color-mix(in srgb, var(--color-air-white-500) 55%, transparent);
    --text-on-dark-50: color-mix(in srgb, var(--color-air-white-500) 50%, transparent);
    --text-on-dark-40: color-mix(in srgb, var(--color-air-white-500) 40%, transparent);
    --text-on-dark-30: color-mix(in srgb, var(--color-air-white-500) 30%, transparent);

    --text-on-light-75: color-mix(in srgb, var(--color-near-black-500) 75%, transparent);
    --text-on-light-65: color-mix(in srgb, var(--color-near-black-500) 65%, transparent);
    --text-on-light-55: color-mix(in srgb, var(--color-near-black-500) 55%, transparent);
    --text-on-light-50: color-mix(in srgb, var(--color-near-black-500) 50%, transparent);
    --text-on-light-20: color-mix(in srgb, var(--color-near-black-500) 20%, transparent);
    --text-on-light-10: color-mix(in srgb, var(--color-near-black-500) 10%, transparent);
    --text-on-light-08: color-mix(in srgb, var(--color-near-black-500) 8%, transparent);

    --border-on-dark-08: color-mix(in srgb, var(--color-white) 8%, transparent);
    --border-on-dark-25: color-mix(in srgb, var(--color-white) 25%, transparent);
    /* Ramp extension — the promo pill's ring. Solid white was heavier than
       the type it encloses; 25% vanished against a bright sky. */
    --border-on-dark-65: color-mix(in srgb, var(--color-white) 65%, transparent);
    --border-on-light-08: color-mix(in srgb, var(--color-near-black-500) 8%, transparent);
    --border-on-light-10: color-mix(in srgb, var(--color-near-black-500) 10%, transparent);
    --border-on-light-20: color-mix(in srgb, var(--color-near-black-500) 20%, transparent);

    --accent-alpha-12: color-mix(in srgb, var(--color-accent) 12%, transparent);
    --accent-alpha-15: color-mix(in srgb, var(--color-accent) 15%, transparent);
    --accent-alpha-22: color-mix(in srgb, var(--color-accent) 22%, transparent);
    --accent-alpha-40: color-mix(in srgb, var(--color-accent) 40%, transparent);
    --accent-alpha-50: color-mix(in srgb, var(--color-accent) 50%, transparent);
    --accent-alpha-60: color-mix(in srgb, var(--color-accent) 60%, transparent);
    --accent-alpha-70: color-mix(in srgb, var(--color-accent) 70%, transparent);

    /* Placeholder texture only — the one remaining white-based tint. */
    --overlay-smoke-1: color-mix(in srgb, var(--color-white) 5%, transparent);

    /* Atmosphere: warm charcoal haze. Replaces the old white-based smoke
       so the hero lifts toward charcoal instead of going cool-grey, which
       is what split it into a grey half and an orange half. */
    --overlay-charcoal-1: color-mix(in srgb, var(--color-charcoal-600) 70%, transparent);
    --overlay-charcoal-2: color-mix(in srgb, var(--color-charcoal-600) 45%, transparent);
    --overlay-vignette:   color-mix(in srgb, var(--color-black-900) 75%, transparent);

    /* ---- Spacing scale (4px base, same increments as Tailwind) ---- */
    --space-0: 0;
    --space-1: 0.25rem;
    --space-1_5: 0.375rem;
    --space-2: 0.5rem;
    --space-2_5: 0.625rem;
    --space-3: 0.75rem;
    --space-3_5: 0.875rem;
    --space-4: 1rem;
    --space-5: 1.25rem;
    --space-6: 1.5rem;
    --space-7: 1.75rem;
    --space-8: 2rem;
    --space-9: 2.25rem;
    --space-10: 2.5rem;
    --space-12: 3rem;
    --space-14: 3.5rem;
    --space-16: 4rem;
    --space-18: 4.5rem;   /* project extension — headline drop in .photo-hero */
    --space-20: 5rem;
    --space-22: 5.5rem;   /* project extension — headline-block breathing room */
    --space-24: 6rem;
    --space-28: 7rem;
    --space-32: 8rem;

    /* Fluid horizontal page gutter — used on every full-width section */
    --space-container: clamp(1.25rem, 4vw, 3.5rem);

    /* ---- Radius scale ---- */
    --radius-none: 0;
    --radius-sm: 2px;
    --radius-md: 3px;
    --radius-lg: 4px;
    --radius-xl: 6px;
    --radius-2xl: 14px;
    --radius-full: 9999px;

    /* ---- Type (unchanged from v1 — not in scope of this pass) ---- */
    --font-display:'Space Grotesk', sans-serif;
    --font-body:'Inter', sans-serif;
    /* Scoped extension — advertorial.html only. A long-form reading face
       distinct from the brand's own two, so the article body reads as the
       outlet's editorial voice and --font-display/--font-body read as the
       advertiser's chrome (CTA, byline meta, disclosure), the same split a
       real sponsored-content page makes between its own voice and the
       brand it's carrying. */
    --font-editorial:'Newsreader', Georgia, 'Times New Roman', serif;

    /* ---- Motion ----
       One custom curve. The house `ease` is symmetric, which is right for
       hover state changes but wrong for entrances — a decelerating curve is
       what makes a reveal read as "settling" rather than "starting slow". */
    --ease-out-soft: cubic-bezier(.22,.61,.36,1);
    --dur-micro: .2s;      /* nav icons, link colors */
    --dur-btn: .25s;       /* buttons */
    --dur-surface: .3s;    /* cards, header */
    --dur-reveal: .55s;    /* scroll entrances */
    --flip-cycle: 9s;                   /* lifestyle cards + heading swap */
    --reveal-shift: var(--space-3_5);   /* 14px rise */
    --reveal-step: 60ms;                /* stagger between grid children */
    --focus-ring-on-accent: var(--color-near-black-500);
    --scroll-offset: var(--space-24);   /* announce bar + header height */
  }

  *{box-sizing:border-box; margin:0; padding:0;}
  /* Horizontal clipping has to be on BOTH html and body, and it has to be
     `clip`, not `hidden`.
       - On iOS Safari the scroll container is <html>, so overflow-x:hidden
         set only on <body> does not stop the layout viewport widening —
         the page still zooms out on a phone while every desktop
         measurement reads clean, because body clips it from scrollWidth.
       - `clip` does not create a scroll container, so .site-top's
         position:sticky keeps working. `hidden` would break it.
     100% max-width is the belt to that braces. */
  /* Any open dialog freezes the page behind it. <dialog> blocks clicks when
     opened with showModal(), but a touch scroll still reaches the document
     underneath on iOS, so the storefront slides around behind the cart.
     :has() does this without JS, and covers every dialog on the site — cart,
     promo, nutrition label and the review lightbox — rather than just the
     one that happened to surface the problem. */
  html:has(dialog[open]){
    overflow:hidden;
    /* Reserves the scrollbar's width so the page does not jump sideways the
       moment the scrollbar is removed on desktop. */
    scrollbar-gutter:stable;
  }
  html{
    scroll-behavior:smooth;
    /* `clip` only — never `hidden` here. hidden on <html> makes it a scroll
       container and breaks .site-top's position:sticky. Browsers without
       overflow:clip (Safari < 16) simply drop this line and fall back to
       body's hidden below, which is the behaviour this file always had. */
    overflow-x:clip;
  }
  body{
    font-family:var(--font-body);
    background:var(--color-bg);
    color:var(--color-text);
    max-width:100%;
    /* hidden first as the old-browser fallback, clip second so modern
       browsers use it and keep sticky working. */
    overflow-x:hidden;
    overflow-x:clip;
    -webkit-font-smoothing:antialiased;
  }
  a{color:inherit; text-decoration:none;}
  img{max-width:100%; display:block;}
  /* Anchor targets clear the sticky header. Without this, every nav link
     scrolls its section to a position hidden behind .site-top. */
  #product, #shop, #real-people, #ingredients, #ritual, #reviews, #customer-reviews{scroll-margin-top:var(--scroll-offset);}
  .eyebrow{
    font-family:var(--font-display);
    font-size:0.72rem;
    letter-spacing:0.22em;
    text-transform:uppercase;
    color:var(--color-accent);
    font-weight:600;
  }
  .btn{
    display:inline-flex;
    align-items:center;
    gap:var(--space-2);
    font-family:var(--font-display);
    font-weight:600;
    font-size:0.85rem;
    letter-spacing:0.06em;
    text-transform:uppercase;
    padding:var(--space-4) var(--space-8);
    border-radius:var(--radius-full);
    cursor:pointer;
    border:1px solid transparent;
    transition:transform var(--dur-btn) var(--ease-out-soft),
               background var(--dur-btn) ease,
               border-color var(--dur-btn) ease;
  }
  /* Guarded: on touch, a tap leaves the lift stuck until you tap elsewhere,
     which reads as a rendering bug. */
  @media (hover:hover){ .btn:hover{transform:translateY(-2px);} }
  /* Must follow :hover to win the cascade while a pressed button is hovered. */
  .btn:active{transform:translateY(0) scale(.985); transition-duration:.08s;}
  .btn-primary{background:var(--color-accent); color:var(--color-near-black-500);}
  .btn-primary:hover{background:var(--color-accent-hover);}
  .btn-ghost{border-color:currentColor; color:inherit; background:transparent;}
  .btn-ghost:hover{border-color:var(--color-accent); color:var(--color-accent);}

  /* ---------- Focus ----------
     Nothing authored a focus style before this, so keyboard users got the
     UA default only. :where() keeps specificity at 0 so components can
     override the ring colour without an escalation war. */
  :where(a, button, input, [tabindex]):focus-visible{
    outline:2px solid var(--color-accent);
    outline-offset:3px;
    border-radius:var(--radius-sm);
  }
  /* An ember ring on the ember-filled button would be invisible. */
  .btn-primary:focus-visible{outline-color:var(--focus-ring-on-accent);}

  /* ---------- Brand mark (logo) ---------- */
  /* Width-based sizing everywhere: height:auto means the aspect
     ratio can never break, even if a parent column gets squeezed
     on mobile — this is what caused the earlier footer stretch bug. */
  /* Scales rather than changing height, so only padding-block drives layout
     and backdrop-filter isn't asked to re-blur a resizing box every frame. */
  .brand-mark{
    display:block; width:auto; height:1.875rem;
    transform-origin:left center;
    transition:transform var(--dur-surface) var(--ease-out-soft);
  }
  header.condensed .brand-mark{transform:scale(.84);}
  .brand-mark--footer{display:block; width:min(220px, 100%); height:auto; margin:0 auto var(--space-1);}

  /* ---------- Photo placeholder system ---------- */
  .photo-slot{
    position:relative;
    aspect-ratio: var(--slot-ratio, 4/3);
    display:flex;
    align-items:center;
    justify-content:center;
    background:
      repeating-linear-gradient(135deg, var(--overlay-smoke-1) 0 2px, transparent 2px 14px);
    border:1px dashed var(--border-on-dark-25);
    color:var(--text-on-dark-55);
    text-align:center;
    overflow:hidden;
    border-radius:var(--radius-lg);
  }
  /* Any slot holding a real photo fills edge-to-edge — no border,
     no dashed placeholder texture, and the image is pinned to all
     four sides so there's no room for a stray gap underneath it. */
  .photo-slot:has(img){border:none; background:none;}
  .photo-slot > img{
    position:absolute; inset:0;
    width:100%; height:100%;
    object-fit:cover;
    display:block;
  }
  .photo-slot .slot-label{
    font-family:var(--font-display);
    font-size:0.68rem;
    letter-spacing:0.14em;
    text-transform:uppercase;
    padding:0 var(--space-6);
    line-height:1.6;
  }
  .photo-slot .slot-tag{
    position:absolute;
    top:var(--space-3); left:var(--space-3);
    font-family:var(--font-display);
    font-size:0.6rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    background:var(--color-accent);
    color:var(--color-near-black-500);
    padding:var(--space-1) var(--space-2);
    border-radius:var(--radius-sm);
    font-weight:700;
  }
  /* Source photo is landscape (1536x1024). At a strict 4:5, cover can only
     ever show ~53% of the width — no object-position fixes that, it just
     trades left crop for right crop. Easing to 9:10 shows meaningfully
     more of both sides (~60% of width) while still reading as portrait. */
  .hero-image .photo-slot{ --slot-ratio: 9/10; border-radius:var(--radius-xl); }
  .hero-image .photo-slot > img{object-position:32% center;}
  /* Matches ingredients-card (1856x2304, ~4/5). object-fit:cover only
     crops when the slot and the source disagree — at parity nothing is
     trimmed, so the wordmark at the top of the graphic stays intact. */
  .proof-grid .photo-slot{ --slot-ratio: 4/5; border-radius:var(--radius-2xl); }
  /* Ember hairline, same token as .purchase-box — the one other accent-bordered
     surface on the page, so the two read as the same treatment. */
  .proof-grid .proof-photo{ border:2px solid var(--accent-alpha-50); }
  /* .lifestyle-grid no longer uses .photo-slot — its cards are .flip-card,
     which sets its own 1/1 ratio and puts the radius on .flip-face. */

  /* ---------- Announcement bar ---------- */
  .announce-bar{
    /* Solid ember on near-black text — the one bright band on the page, so
       it reads as a banner instead of dissolving into the dark background. */
    background:var(--color-accent);
    color:var(--color-near-black-500);
    overflow:hidden;
    padding:var(--space-2_5) 0;
    font-family:var(--font-display);
    font-size:0.72rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    font-weight:700;
    position:relative;
    z-index:101;
  }
  /* Same seamless-loop technique as the two photo carousels: the message
     set is emitted exactly twice, animated 0 -> -50%, so the loop point is
     invisible. Slow and continuous, not a scroll-triggered marquee — this
     is a thin persistent strip, not content someone reads start-to-finish. */
  .announce-track{
    display:flex;
    align-items:center;
    gap:var(--space-8);
    width:max-content;
    padding-right:var(--space-8);
    animation:announce-scroll 26s linear infinite;
  }
  .announce-bar:hover .announce-track{animation-play-state:paused;}
  @keyframes announce-scroll{
    from{ transform:translateX(0); }
    to{ transform:translateX(-50%); }
  }
  .announce-track span[aria-hidden]{opacity:0.6;}

  .site-top{
    position:sticky;
    top:0;
    z-index:100;
  }
  /* ---------- Nav ---------- */
  header{
    position:relative;
    display:flex; align-items:center; justify-content:space-between;
    /* Split from the `padding` shorthand so padding-block alone can animate. */
    padding-block:var(--space-4);
    padding-inline:var(--space-container);
    background:linear-gradient(to bottom, color-mix(in srgb, var(--color-near-black-500) 95%, transparent), color-mix(in srgb, var(--color-near-black-500) 75%, transparent));
    transition:background var(--dur-surface) ease,
               padding-block var(--dur-surface) var(--ease-out-soft);
  }
  header.scrolled{background:color-mix(in srgb, var(--color-near-black-500) 92%, transparent); backdrop-filter:blur(8px);}
  header.condensed{padding-block:var(--space-2_5);}
  nav ul{list-style:none; display:none; gap:var(--space-10);}
  nav a{
    font-family:var(--font-display);
    font-size:0.78rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    opacity:0.85;
    transition:opacity var(--dur-micro) ease, color var(--dur-micro) ease;
  }
  nav a:hover{opacity:1; color:var(--color-accent);}
  .nav-icons{display:flex; align-items:center; gap:var(--space-6);}
  .nav-icon-btn{
    display:flex; align-items:center; justify-content:center;
    background:none; border:none; cursor:pointer;
    color:var(--color-text);
    opacity:0.9;
    transition:opacity var(--dur-micro) ease, color var(--dur-micro) ease;
  }
  .nav-icon-btn:hover{opacity:1; color:var(--color-accent);}
  .nav-icon-btn svg{width:20px; height:20px;}
  .cart-badge{position:relative;}
  .cart-count{
    position:absolute; top:calc(-1 * var(--space-1_5)); right:calc(-1 * var(--space-2));
    background:var(--color-accent); color:var(--color-near-black-500);
    font-family:var(--font-display);
    font-size:0.6rem; font-weight:700;
    width:16px; height:16px; border-radius:var(--radius-full);
    display:flex; align-items:center; justify-content:center;
    line-height:1;
  }
  @media(min-width:821px){ nav ul{display:flex;} }

  /* ---------- Hero ---------- */
  .hero{
    min-height:100vh;
    display:grid;
    /* minmax(0,1fr), never a bare 1fr. A `1fr` track carries an implicit
       min-width:auto, so it CANNOT shrink below its content's min-content
       width — and .hero-proof is white-space:nowrap by design, giving it a
       ~307px min-content. At 320px that forced this track to 307px, and
       .hero-image (negative margins, to bleed past the gutter) rode it out
       to 347px: a 27px-wide overflow that body's overflow-x:clip hides from
       scrollWidth while a phone still responds by zooming the page out.
       minmax(0,...) lets the track shrink and makes the nowrap row clip
       itself instead of dragging the whole layout with it. */
    grid-template-columns:minmax(0, 1fr);
    align-items:center;
    /* Mobile: zero top padding — the photo leads now, so it should sit
       flush against the sticky header with nothing between them. Desktop
       is untouched below (still text-first, needs the clearance). */
    padding:0 var(--space-container) var(--space-12);
    /* Mobile stacks photo-over-copy, so this is the gap between them —
       48px was the biggest space in the section. Desktop restores it,
       where the same value is the horizontal column gutter instead. */
    gap:var(--space-6);
    position:relative;
    overflow:hidden;
    isolation:isolate;
  }
  /* Mobile: photo before copy — the product is the first thing you see,
     text follows. Desktop reverts to source order (copy | image) once
     it's a genuine side-by-side two-column layout. */
  .hero-image{
    order:1;
    /* Bleeds past .hero's own horizontal padding to the screen edges, so
       the photo shows more of its actual frame instead of being cropped
       narrower by the section gutter. */
    margin-inline:calc(-1 * var(--space-container));
  }
  .hero-copy{order:2; text-align:center;}
  .hero p.sub{margin-inline:auto;}
  .hero-ctas{justify-content:center;}
  .hero-proof{justify-content:center;}
  @media(min-width:901px){
    .hero{grid-template-columns:1.1fr 0.9fr; gap:var(--space-12); padding-top:var(--space-32);}
    .hero-image, .hero-copy{order:initial;}
    .hero-image{margin-inline:0;}
    .hero-image .photo-slot{border-radius:var(--radius-2xl);}
    .hero-copy{text-align:left;}
    .hero p.sub{margin-inline:0;}
    .hero-ctas{justify-content:flex-start;}
    .hero-proof{justify-content:flex-start;}
  }
  .hero::before{
    /* Charcoal atmosphere across the WHOLE hero, plus a vignette that
       drops the edges below the page bg. Full-bleed on purpose: the old
       version only covered the left 70%, which is what made the hero read
       as two stitched-together halves. */
    content:'';
    position:absolute;
    inset:-10% -10% 0 -10%;
    background:
      radial-gradient(ellipse 70% 60% at 28% 22%, var(--overlay-charcoal-2), transparent 65%),
      radial-gradient(ellipse 60% 55% at 65% 75%, var(--overlay-charcoal-1), transparent 60%),
      linear-gradient(180deg, transparent 55%, var(--overlay-vignette) 100%);
    /* Forces every atmosphere layer to zero at the bottom edge, so the hero
       ends on the exact page background and meets the next section with no
       seam. The vignette still reads as depth just above the fade. */
    -webkit-mask-image:linear-gradient(180deg, black 86%, transparent 100%);
    mask-image:linear-gradient(180deg, black 86%, transparent 100%);
    z-index:0;
    pointer-events:none;
  }
  .hero::after{
    /* One ember light source, anchored on the product itself. Two stops
       share a center so it reads as a single glow rather than two blooms.
       Mobile: product sits below the copy, so the glow sits low. */
    content:'';
    position:absolute;
    inset:0;
    background:
      radial-gradient(circle at 50% 78%, var(--accent-alpha-22), transparent 55%),
      radial-gradient(circle at 50% 65%, var(--accent-alpha-12), transparent 45%);
    z-index:0;
    pointer-events:none;
  }
  @media(min-width:901px){
    /* Two-column layout — the glow follows the product to the right. */
    .hero::after{
      background:
        radial-gradient(circle at 76% 46%, var(--accent-alpha-22), transparent 55%),
        radial-gradient(circle at 70% 62%, var(--accent-alpha-12), transparent 45%);
    }
  }
  .hero > *{position:relative; z-index:1;}
  .ember-specks{
    /* Specks drift through the whole scene and fade radially around the
       same light source as .hero::after — no hard vertical cutoff. */
    position:absolute;
    inset:0;
    z-index:0;
    pointer-events:none;
    opacity:0.7;
    background-image:
      radial-gradient(1.5px 1.5px at 42% 34%, var(--color-accent), transparent),
      radial-gradient(1px 1px at 55% 62%, var(--color-accent), transparent),
      radial-gradient(2px 2px at 72% 24%, var(--accent-alpha-70), transparent),
      radial-gradient(1px 1px at 88% 72%, var(--color-accent), transparent),
      radial-gradient(1.5px 1.5px at 64% 84%, var(--accent-alpha-60), transparent),
      radial-gradient(1px 1px at 34% 78%, var(--color-accent), transparent),
      radial-gradient(1.5px 1.5px at 92% 44%, var(--accent-alpha-50), transparent),
      radial-gradient(1px 1px at 24% 56%, var(--accent-alpha-40), transparent);
    background-repeat:no-repeat;
    -webkit-mask-image:radial-gradient(ellipse 55% 60% at 50% 72%, black 0%, transparent 78%);
    mask-image:radial-gradient(ellipse 55% 60% at 50% 72%, black 0%, transparent 78%);
  }
  @media(min-width:901px){
    .ember-specks{
      -webkit-mask-image:radial-gradient(ellipse 48% 58% at 76% 48%, black 0%, transparent 78%);
      mask-image:radial-gradient(ellipse 48% 58% at 76% 48%, black 0%, transparent 78%);
    }
  }
  .hero-copy .eyebrow{margin-bottom:var(--space-4); display:block;}
  .hero h1{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(2.9rem, 7.2vw, 5.6rem);
    line-height:0.98;
    letter-spacing:-0.015em;
    margin-bottom:var(--space-5);
    text-transform:uppercase;
  }
  .hero h1 .accent{color:var(--color-accent);}
  .hero p.sub{
    font-family:var(--font-body);
    font-size:1.15rem;
    line-height:1.55;
    color:var(--text-on-dark-78);
    max-width:34ch;
    margin-bottom:var(--space-7);
  }
  .hero p.sub .sub-accent{color:var(--color-text); font-weight:600;}
  /* Last element in .hero-copy now that the proof strip moved above the h1 —
     a trailing margin here would just pad the bottom of the hero. */
  .hero-ctas{display:flex; gap:var(--space-4); flex-wrap:wrap;}

  /* Shared micro credibility strip. Separators are hairline rules, not
     middots — item text like "Non-GMO · Vegan Friendly" already owns the dot.
     Two users with different wrapping needs, so sizing and wrap live on the
     context class rather than here. */
  .proof-strip{
    display:flex;
    align-items:center;
    gap:var(--space-2);
    font-family:var(--font-display);
    letter-spacing:0.04em;
    color:var(--text-on-dark-60);
  }
  .proof-strip span{display:inline-flex; align-items:center; gap:var(--space-2); white-space:nowrap;}
  .proof-strip span + span::before{
    content:'';
    flex:0 0 auto;
    width:1px; height:0.95em;
    background:var(--border-on-dark-25);
  }
  .proof-strip strong{color:var(--color-star); font-weight:600; letter-spacing:0;}

  /* Hero: sits above the h1 where the eyebrow used to. Strictly one line at
     every width — nowrap plus a font-size that scales with the viewport,
     floored low enough to survive a 320px screen. */
  /* Three items totalling ~60 characters. One line at every width would
     need ~7px type on a phone, so instead the items stay unbreakable
     (white-space on .proof-strip span) while the GROUP is allowed to wrap
     to a second centred row on mobile. Desktop has the room for one line. */
  .hero-proof{
    flex-wrap:nowrap;
    /* One line at every width. Getting there costs characters, not just
       CSS: gaps are down to --space-1 and the copy was cut ~20 chars,
       because the previous wording needed ~7px type to fit a 375px phone. */
    gap:var(--space-1);
    margin-bottom:var(--space-4);
    font-size:clamp(0.56rem, 2.6vw, 0.72rem);
    white-space:nowrap;
  }

  /* Same wrapping contract as .hero-proof: items never break internally,
     but the group wraps to a second centred row on mobile rather than
     shrinking the type past legibility. One line from 901px up. */
  .lifestyle-proof{
    flex-wrap:wrap;
    justify-content:center;
    gap:var(--space-1_5) var(--space-2);
    margin-top:var(--space-4);
    font-size:clamp(0.62rem, 2.6vw, 0.72rem);
  }
  @media(min-width:901px){
    .lifestyle-proof{flex-wrap:nowrap;}
  }

  .stat-bar{
    display:grid;
    grid-template-columns:1fr 1fr;
    /* Sits mid-page now, so it's a self-contained rule-off band rather than
       the gradient bridge it used to be directly under the hero. */
    border-block:1px solid var(--border-on-dark-08);
    background:var(--color-bg);
    position:relative;
    z-index:2;
  }
  .stat-item{
    display:flex;
    align-items:flex-start;
    gap:var(--space-3_5);
    padding:var(--space-7) clamp(var(--space-4), 2.5vw, var(--space-8));
    border-right:1px solid var(--border-on-dark-08);
    border-bottom:1px solid var(--border-on-dark-08);
  }
  .stat-item:last-child{border-right:none;}
  @media(min-width:821px){
    .stat-bar{grid-template-columns:repeat(4,1fr);}
    .stat-item{border-bottom:none;}
  }
  .stat-item svg{width:26px; height:26px; stroke:var(--color-accent); flex-shrink:0; margin-top:0.15rem;}
  .stat-item .stat-title{
    font-family:var(--font-display);
    font-size:0.82rem;
    font-weight:600;
    letter-spacing:0.02em;
    text-transform:uppercase;
    line-height:1.3;
    margin-bottom:var(--space-1);
  }
  .stat-item .stat-sub{
    font-size:0.82rem;
    color:var(--text-on-dark-55);
    line-height:1.4;
  }

  /* ---------- Tension / insecurity+persuasion section ---------- */
  /* Full-bleed section — the copy holds its own 900px measure inside, so a
     background (band, or the hero bridge below) can paint edge to edge. */
  .tension{
    padding:var(--space-16) var(--space-container);
    text-align:center;
  }
  .tension > *{max-width:900px; margin-inline:auto;}
  /* Tightens the gap when the stat bar follows — a full space-24 below the
     copy left the two reading as unrelated blocks. */
  .tension:has(+ .stat-bar){padding-bottom:var(--space-8);}
  /* Pulled up toward the customer carousel above it. The marquee already
     carries its own space-10 bottom margin, so a full space-16 on top of
     that left the question floating too far below the proof it follows.
     Scoped away from .tension--band, which sits between two full sections
     and still needs the room. */
  .tension:not(.tension--band){padding-top:var(--space-10);}
  /* Same pull, from the other side: the customer marquee hands straight to
     the question, so it doesn't need a full space-10 under it. Scoped by id
     so the lifestyle marquee lower down keeps its own spacing. */
  .carousel-wrap#reviews{margin-bottom:var(--space-4);}
  .tension--band{
    background:var(--color-bg-band);
    border-block:1px solid var(--border-on-dark-08);
  }
  .tension .eyebrow{justify-content:center; display:block; margin-bottom:var(--space-5);}
  .tension h2{
    font-family:var(--font-body);
    font-weight:500;
    font-size:clamp(1.6rem, 3.4vw, 2.5rem);
    line-height:1.35;
    color:var(--color-text);
  }
  .tension h2 .hot{color:var(--color-accent); font-style:normal; font-family:var(--font-display); font-weight:700;}
  /* Body copy inside a .tension block. Distinct from .fine, which is an
     uppercase display tagline and unreadable at paragraph length. Narrower
     than the 900px .tension > * cap so a full paragraph keeps a sane
     measure instead of running the width of the band. */
  .tension .detail{
    margin-top:var(--space-6);
    max-width:640px;
    font-size:1rem;
    line-height:1.6;
    color:var(--text-on-dark-70);
  }
  /* Uppercase micro-type is fine for a few words and punishing at sentence
     length, so once this runs past one line it needs a measure and looser
     leading — 900px of 0.75rem all-caps is unreadable. */
  .tension .fine{
    margin-top:var(--space-7);
    max-width:620px;
    font-family:var(--font-display);
    font-size:0.75rem;
    line-height:1.8;
    letter-spacing:0.06em;
    color:var(--text-on-dark-55);
    text-transform:uppercase;
    text-wrap:balance;
  }
  /* Citation, not a certification stamp — deliberately an icon rather than
     an agency logo. Naming EMA/NCCIH is a fair citation of published
     recognition for the herb; their actual insignia here would read as an
     endorsement of AfterSmoke itself, which neither body has given.
     The document icon sits in a gold ring (the review stars' gold), so it
     reads as trust without imitating any agency's seal. */
  .citation-badge{
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:var(--space-3);
    max-width:480px;
    margin:var(--space-6) auto 0;
    font-size:0.75rem;
    line-height:1.5;
    color:var(--text-on-dark-60);
    text-align:center;
    text-transform:none;
    letter-spacing:normal;
  }
  /* A minted-seal ring, kept flat and crisp: a metallic edge (the gold
     badge gradient, painted on the border box under a transparent border),
     a hairline inner ring set 3px in (two inset shadows: a band of the
     section colour, then gold), and a faint top-down sheen on the face.
     The flat fill is wrapped in a gradient because only the last background
     layer may be a plain colour. --citation-ring is reused by the desktop
     padding below so the text stays centred. */
  .citation-badge{--citation-ring:50px;}
  .citation-badge-icon{
    display:flex;
    align-items:center;
    justify-content:center;
    flex:0 0 auto;
    width:var(--citation-ring);
    height:var(--citation-ring);
    border:1px solid transparent;
    border-radius:var(--radius-full);
    background:
      linear-gradient(180deg, var(--gold-alpha-12), transparent 60%) padding-box,
      linear-gradient(var(--color-bg-band), var(--color-bg-band)) padding-box,
      var(--gradient-gold) border-box;
    box-shadow:
      inset 0 0 0 3px var(--color-bg-band),
      inset 0 0 0 4px var(--gold-alpha-45);
  }
  .citation-badge svg{width:22px; height:22px; stroke:var(--color-star);}
  /* pretty, not balance: balance shortens every line and leaves a gap
     between the ring and the centred text; pretty only stops a lone last
     word ("comfort.") at desktop width. */
  .citation-badge > span:last-child{text-wrap:pretty;}
  /* Phones: ring stacked above the text, both on the centre line. Beside
     the text, the ring pushed it right and it read off-centre.
     From 561px the ring sits beside the text again, and the other side is
     padded by ring plus gap, so the text still sits on the page's centre
     line under the headline with the ring hanging to its left. */
  @media (min-width:561px){
    .citation-badge{
      flex-direction:row;
      max-width:560px;
      padding-inline-end:calc(var(--citation-ring) + var(--space-3));
    }
  }

  /* ---------- Content wrapper (formerly a scroll-color transition —
     removed in favor of one consistent brand theme site-wide) ---------- */
  #transition-wrap{
    position:relative;
    background:var(--color-bg);
  }

  .habit-heading{
    text-align:center;
    margin-top:var(--space-12);
    padding:0 var(--space-container);
  }
  .habit-heading .eyebrow{display:block;}
  .habit-row{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:1px;
    background:var(--border-on-dark-08);
    /* Bottom margin matters as much as the top: with 0 the last row of
       tiles butted straight into the next section and the block read as
       part of it. */
    margin:var(--space-6) var(--space-container) var(--space-12);
    /* The fill matches the page too closely to describe the block's own
       shape, so the rounded edge gets a hairline — same device the
       ingredients band uses top and bottom. */
    border:1px solid var(--border-on-dark-08);
    /* radius-lg is 4px — barely a corner on a block this size, so the
       "rounded" edge read as an almost-square one. radius-2xl matches the
       other large surfaces on the page (photo frames, cards). */
    border-radius:var(--radius-2xl);
    overflow:hidden;
  }
  @media(min-width:701px){ .habit-row{grid-template-columns:repeat(4,1fr);} }
  .habit-card{
    /* Exactly the ingredients band colour, so the row reads as part of the
       same surface. At 11 levels above the page the fill alone can't define
       the block's edge, so .habit-row carries a hairline instead — the same
       way .section--band does. */
    background:var(--color-bg-band);
    /* Phones show these 2-up, so the tile is short and space-8 top/bottom
       was 43% of the card height — the icon and label floated in it. The
       4-up desktop tile is wider and carries the taller padding fine. */
    padding:var(--space-7) var(--space-5);
    text-align:center;
    transition:background .3s ease;
  }
  /* Must sit after the base rule, not in the earlier media block — same
     specificity, so source order decides. */
  @media(min-width:701px){ .habit-card{padding:var(--space-8) var(--space-5);} }
  .habit-card:hover{
    /* Lifts off the flat band colour rather than off the old gradient. */
    background:var(--color-charcoal-500);
  }
  /* The icon sits in an ember well so each tile has a focal anchor
     instead of a small stroke floating in dead space. */
  .habit-card svg{
    width:54px; height:54px;
    padding:var(--space-3_5);
    margin:0 auto var(--space-3);
    stroke:var(--color-accent);
    background:var(--accent-alpha-12);
    border:1px solid var(--accent-alpha-22);
    border-radius:var(--radius-full);
    display:block;
    transition:background .3s ease, border-color .3s ease, transform .3s ease;
  }
  @media (hover:hover){
    .habit-card:hover svg{
      background:var(--accent-alpha-22);
      border-color:var(--accent-alpha-40);
      transform:translateY(-2px);
    }
  }
  .habit-card .label{
    font-family:var(--font-display);
    font-size:0.72rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color:var(--text-on-dark-85);
    transition:color .3s ease;
  }
  .habit-card:hover .label{color:var(--color-text);}

  .headline-block{
    text-align:center;
    /* A card mid-rotation projects wider than its own box. Clipping here
       guarantees it can never paint over the section edges. */
    overflow:hidden;
    padding:var(--space-14) var(--space-container) var(--space-8);
  }
  /* Every neighbouring block announces itself with an eyebrow before it
     speaks. Without one this block started mid-thought and read as an
     interstitial rather than a section of its own. */
  .headline-block .eyebrow{display:block; margin-bottom:var(--space-4);}
  .headline-block h2{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.8rem, 4vw, 3rem);
    line-height:1.15;
    max-width:23ch;
    margin:0 auto;
    /* Evens out the line lengths instead of filling greedily — without it
       the second sentence strands "be too." alone on a fourth line. */
    text-wrap:balance;
  }
  /* Concession, then pivot. These are two different jobs, so they get two
     different weights: the setup recedes, the turn carries the ember. Each
     is display:block so the pair breaks cleanly and balances independently
     — a hard <br> would fix the break point at one viewport width only. */
  /* Full white, deliberately. Dimming this line was tried and reverted: a
     display heading rendered half-white/half-grey reads as two elements, or
     as something disabled, rather than as one sentence with a turn in it.
     The ember phrase below carries the emphasis on its own. */
  .headline-block h2 .headline-setup{display:block;}
  .headline-block h2 .headline-turn{
    display:block;
    margin-top:var(--space-5);
  }
  .headline-block h2 .ember-word{color:var(--color-accent);}
  /* Fixed box, every width: 560px max, 4:3 -> 420px tall on desktop, and
     the same ratio scaled down on mobile via width:100%. .headline-photo
     also carries .flip-card, whose shared rule supplies overflow:hidden —
     a hard clip at THIS exact box, at every rotation angle, regardless of
     perspective distance, guaranteeing the photo can't escape on desktop.
     No border here deliberately: it lives on .flip-face below instead, so
     the frame rotates WITH the card. A border on this static outer box
     would sit still while only the photo underneath turned — a static
     frame over swapping content, not one card flipping, which is exactly
     what looked wrong on desktop's flatter, more visible rotation. */
  /* Reuses .proof-strip (the same star/pipe component from the hero and
     lifestyle sections) rather than a one-off — centered here since the
     whole block is centered, unlike the hero's left-aligned desktop use. */
  .headline-proof{
    justify-content:center;
    margin-top:var(--space-3);
  }
  .headline-block .headline-photo{
    width:min(560px, 100%);
    aspect-ratio:4/3;
    margin:var(--space-10) auto 0;
  }
  /* A single still in the headline slot — no flip, no frame. The ember
     hairline below belongs to the rotating faces: on a static photo it
     reads as a border rather than as part of a card that turns. */
  /* ---- Headline tumble card ----
     Six photos through a TWO-faced card. CLAUDE.md is explicit that
     .flip-card is two faces only — a third was tried and broke twice — so
     this doesn't add faces. It swaps the hidden face's image to the next
     slide just before each turn, which gives any number of slides from a
     card that is never more than two-sided. Deliberately its own component
     rather than a .flip-card variant: that one rotates on Y, this on X.

     FAILSAFE: with JS off the card never turns and slide 1 simply shows,
     which is why the front face carries the real alt text and the back is
     aria-hidden. */
  .headline-block .headline-tumble{
    width:min(1120px, 100%);
    margin:var(--space-10) auto 0;
    aspect-ratio:4/3;
    perspective:2400px;
  }
  .headline-tumble .tumble-inner{
    position:relative;
    width:100%; height:100%;
    transform-style:preserve-3d;
    transition:transform var(--dur-surface) var(--ease-out-soft);
  }
  .js-motion .headline-tumble .tumble-inner{transition-duration:.5s;}
  .headline-tumble .tumble-face{
    position:absolute;
    inset:0;
    overflow:hidden;
    backface-visibility:hidden;
    -webkit-backface-visibility:hidden;
    border:2px solid var(--accent-alpha-50);
    border-radius:var(--radius-2xl);
  }
  /* Over the top, not side to side: the back face starts a half-turn up
     the X axis so the card tumbles forward. */
  .headline-tumble .tumble-b{transform:rotateX(180deg);}
  .headline-tumble .tumble-face img{
    display:block;
    width:100%; height:100%;
    object-fit:cover;
  }

  /* Ember hairline on each rotating face — matches .proof-grid .proof-photo
     — so the whole card, frame included, appears to turn as one piece,
     the same read the lifestyle grid cards already have (they carry no
     competing outer border to begin with). */
  .headline-flip .flip-face{border:2px solid var(--accent-alpha-50);}
  /* Perspective distortion scales with how large the element is relative to
     the vanishing distance: fine on a phone (~350px wide, ratio ~2.9) but
     this card reaches its full 560px on desktop, dropping the ratio to 1.8
     and swinging the rotating edge out further than at small sizes. A
     longer vanishing distance keeps the turn looking gentle rather than a
     lurch — and the .flip-card overflow:hidden clip means even if this
     number is ever wrong again, nothing can render outside the box.
     Specificity note: (0,2,0) here deliberately beats the unconditional
     .flip-card{perspective:1000px} rule regardless of where either sits in
     the file — a bare .headline-flip{} tied with .flip-card on specificity
     and silently lost once before. */
  @media(min-width:901px){
    .headline-block .headline-flip{perspective:2400px;}
  }
  /* The page's emotional turn dead-ended into an ingredient list. This is the
     highest-intent moment outside the hero, so it gets a real exit. */
  .headline-block .headline-cta{margin-top:var(--space-10);}

  /* ---------- Ingredient / proof section ---------- */
  .section{
    padding:var(--space-14) var(--space-container);
  }
  /* Single-photo testimonial slot — same sizing/border language as the
     other one-photo moments on the page (.headline-photo, .proof-photo):
     4/5 matches the source art exactly (1120x1400), so nothing is cropped. */
  /* "not ours.": an ember glow that slowly breathes, with a very gentle float. It is the
     only .hl-mark on the page. Static by default (the glow is already there with no JS and
     under reduced motion, where .js-motion is never set); the motion only starts under
     .js-motion once the heading scrolls into view. inline-block so it can float as one
     piece, and so "not ours." wraps to the next line as a unit instead of splitting. */
  .hl-mark{
    display:inline-block;
    color:var(--color-accent);
    text-shadow:0 0 0.3em var(--accent-alpha-40), 0 0 0.8em var(--accent-alpha-22);
  }
  .js-motion [data-reveal="highlight"].hl-mark{opacity:1;}
  .js-motion [data-reveal="highlight"].hl-mark.is-visible{
    animation:hl-float 6s ease-in-out infinite, hl-glow 3.6s ease-in-out infinite;
  }
  @keyframes hl-float{
    0%,100%{transform:translateY(0);}
    50%    {transform:translateY(-0.1em);}
  }
  @keyframes hl-glow{
    0%,100%{text-shadow:0 0 0.3em var(--accent-alpha-40), 0 0 0.8em var(--accent-alpha-22);}
    50%    {text-shadow:0 0 0.4em var(--accent-alpha-70), 0 0 1.1em var(--accent-alpha-50), 0 0 1.8em var(--accent-alpha-22);}
  }
  /* Three reviews on one graphic. Its background is the page colour (#151515), so there
     is no frame: it sits straight on the page. The art has wide empty margins, so on a
     phone the review text would be tiny; it is shown 25% larger, centred on the three
     reviews (a display crop only; the file itself is untouched). */
  .home-testimonial-photo{
    --slot-ratio:3/4;
    /* Full screen width on a phone (the text is small art, every pixel counts), capped at
       480px; centred on the viewport even though the section has side padding. */
    width:min(480px, 100vw);
    position:relative; left:50%; translate:-50% 0;
    margin:var(--space-8) 0 0;
    border:0;
    border-radius:0;
    overflow:hidden;
  }
  .home-testimonial-photo img{transform:scale(1.25); transform-origin:50% 54.4%;}
  /* Trust ticker between the testimonial and product sections. Same
     seamless-loop technique as .announce-track / .carousel-track (message
     set emitted exactly twice, animated 0 -> -50%), but quieter than the
     bright ember top banner — this sits mid-page on the dark background,
     so it's text-on-dark rather than a solid ember band. The hairline
     borders do double duty as the section divider. */
  .trust-ticker{
    overflow:hidden;
    padding:var(--space-6) 0;
    border-block:1px solid var(--border-on-dark-08);
  }
  /* Lead-in between the habits row and the label section. No rules top or
     bottom, and the line dissolves at both ends instead of being clipped,
     so it reads as part of the page rather than a band across it. */
  .trust-ticker--seam{
    border-block:none;
    /* Centred in the gap between the habit cards and the headline below.
       Measured rather than guessed: at the original --space-8 of negative
       margin the text had 48px of air above it and 96px below, so it read
       as stuck to the cards.

       Both sides had to move: shifting the ticker alone pushes everything
       below it down too, so the air underneath never changes — that gap is
       the mascot band's own padding. This sets the margin above and trims
       that padding to meet at 80px each side. */
    margin-top:var(--space-4);
    /* Top padding, not more margin: the margin above collapses against the
       habit row's own, so past a point raising it changed nothing — 48px,
       then 64px, then 64px again. Padding does not collapse, so this is
       what actually moves the text. */
    padding:var(--space-8) 0 var(--space-8);
    -webkit-mask-image:linear-gradient(to right, transparent 0, black var(--space-20), black calc(100% - var(--space-20)), transparent 100%);
    mask-image:linear-gradient(to right, transparent 0, black var(--space-20), black calc(100% - var(--space-20)), transparent 100%);
  }
  .trust-ticker-track{
    display:flex;
    align-items:center;
    gap:var(--space-8);
    width:max-content;
    padding-right:var(--space-8);
    font-family:var(--font-display);
    font-size:0.78rem;
    letter-spacing:0.08em;
    text-transform:uppercase;
    color:var(--text-on-dark-60);
    /* Slower than the top banner's 26s — this one is ambient background
       texture, not a lead promo, so it shouldn't compete for attention. */
    /* The track holds six messages per set now (the offer twice). At 52s it
       scrolls ~55px/s, about 15% quicker than the original 42s over four
       messages (~47px/s). If messages are added or removed, rescale: duration
       = half the track's width / 55. */
    animation:trust-ticker-scroll 52s linear infinite;
  }
  .trust-ticker:hover .trust-ticker-track{animation-play-state:paused;}
  /* The offer is the one message that stands out in the ticker. */
  .trust-ticker-track .trust-ticker-offer{color:var(--color-accent); font-weight:700;}
  .trust-ticker-track span[aria-hidden]{color:var(--color-accent); opacity:0.7;}
  @keyframes trust-ticker-scroll{
    from{ transform:translateX(0); }
    to{ transform:translateX(-50%); }
  }
  .section--band{
    background:var(--color-bg-band);
    border-block:1px solid var(--border-on-dark-08);
  }
  /* The band's own borders already separate this from the sections either
     side, so the full .section padding left the label photo floating below
     the top edge and a wide dead space under the last ingredient. */
  #ingredients{padding-block:var(--space-8);}
  .proof-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:var(--space-14);
    align-items:center;
    max-width:1200px;
    margin:0 auto;
  }
  /* Mobile: heading reads first, photo sits below it — so this section
     doesn't stack directly on top of the headline-block photo above it
     with nothing but a photo-to-photo seam between them. Desktop reverts
     to source order (photo | text) once it's a genuine side-by-side. */
  /* Mobile: the photo sits *inside* the reading order — after the heading,
     before the ingredient list — so the section opens with words, not a
     second photo. `display:contents` dissolves .proof-text's box so its
     children become grid items alongside .proof-photo and can be ordered
     against it; `order` alone can't reorder across nesting levels. Row gap
     is zeroed here because each element already carries its own top margin.
     Desktop restores the real two-column box. */
  .proof-grid{row-gap:0;}
  .proof-grid .proof-text{display:contents;}
  .proof-grid .proof-text > .eyebrow{order:1; text-align:center;}
  .proof-grid .proof-text > h2{order:2; text-align:center; text-wrap:balance;}
  .proof-grid .proof-text > .label-caption{order:3; text-align:center;}
  .proof-grid .proof-photo{order:4; margin-top:var(--space-10);}
  .proof-grid .proof-text > .ingredient-list{order:5;}
  @media(min-width:901px){
    .proof-grid{grid-template-columns:0.9fr 1.1fr; row-gap:var(--space-14);}
    .proof-grid .proof-text{display:block;}
    .proof-grid .proof-photo{order:initial; margin-top:0;}
    .proof-grid .proof-text > .eyebrow,
    .proof-grid .proof-text > h2,
    .proof-grid .proof-text > .label-caption,
    .proof-grid .proof-text > .ingredient-list{order:initial;}
    .proof-grid .proof-text > .eyebrow,
    .proof-grid .proof-text > h2,
    .proof-grid .proof-text > .label-caption{text-align:left;}
  }
  .label-caption{
    margin-top:var(--space-3);
    font-family:var(--font-display);
    font-size:0.78rem;
    letter-spacing:0.08em;
    text-transform:uppercase;
    color:var(--text-on-dark-55);
  }
  .ingredient-list{display:flex; flex-direction:column; gap:var(--space-6); margin-top:var(--space-8);}
  .ingredient-item{
    display:grid;
    grid-template-columns:auto 1fr;
    gap:var(--space-4);
    padding-bottom:var(--space-6);
    border-bottom:1px solid var(--border-on-dark-08);
  }
  /* A divider separates items; under the last one it separates nothing and
     just reads as a stray line above the band edge. */
  .ingredient-item:last-child{border-bottom:none; padding-bottom:0;}
  .ingredient-item .num{font-family:var(--font-display); font-weight:700; color:var(--color-accent); font-size:0.85rem;}
  .ingredient-item h4{font-family:var(--font-display); font-weight:600; font-size:0.95rem; margin-bottom:var(--space-1);}
  .ingredient-item p{font-size:0.92rem; line-height:1.5; color:var(--text-on-dark-60);}

  /* ---------- How to use ---------- */
  .ritual{
    max-width:1100px; margin:0 auto;
    text-align:center;
  }
  .ritual .ritual-aside{
    margin:var(--space-5) auto 0;
    max-width:54ch;
    font-size:1rem;
    line-height:1.55;
    color:var(--text-on-dark-60);
  }
  .ritual .ritual-aside .hot{color:var(--color-accent); font-weight:600;}
  .ritual-steps{
    display:grid;
    grid-template-columns:1fr;
    gap:var(--space-10);
    margin-top:var(--space-12);
    text-align:center;
  }
  .ritual-step{text-align:center;}
  /* Four steps: 2x2 on tablet, one row of four on desktop. A repeat(3) tier
     would strand step 04 alone on its own line. */
  @media(min-width:561px){ .ritual-steps{grid-template-columns:repeat(2,1fr);} }
  @media(min-width:901px){ .ritual-steps{grid-template-columns:repeat(4,1fr);} }
  @media(min-width:901px){ .ritual-step{text-align:left;} }
  .ritual-step .step-num{
    /* Outlined ember numerals. Previously stroked in near-black on a
       near-black background with a transparent fill — i.e. invisible. */
    font-family:var(--font-display);
    font-size:2.4rem;
    font-weight:700;
    -webkit-text-stroke:1.5px var(--color-accent);
    -webkit-text-fill-color:transparent;
    color:var(--color-accent);
    margin-bottom:var(--space-3);
    display:block;
  }
  .ritual-step h4{font-family:var(--font-display); font-weight:600; font-size:1.05rem; margin-bottom:var(--space-2);}
  .ritual-step p{font-size:0.95rem; line-height:1.55; color:var(--text-on-dark-60);}
  /* Tutorial clip under the four steps. The block is wide enough for a real
     section heading (eyebrow + headline, same pattern as the other sections);
     only the clip itself is held to phone-screenshot width, since the source
     is portrait 9:16. It's an animated GIF, not a <video>: this host doesn't
     answer HTTP Range requests, and Safari (every iPhone) refuses to play
     video without them; an animated image needs none, loops with no gap,
     and always autoplays. Size limit is 25 MiB per asset (a Workers cap);
     earlier deploy failures that looked like a ~3.7 MB ceiling were really
     .git being uploaded as an asset -- see .assetsignore. This one is
     7.2 MB, 540x960, so it's sharp at ~1.8x in the 300px slot; that's a
     heavy mobile download, so weigh it against a smaller export or the
     MP4 route before growing it further. When it's replaced under
     the same name, bump the ?v= on its link in index.html (/assets/* is
     cached for a year, immutable). */
  .ritual-gif{
    margin:var(--space-14) auto 0;
    max-width:min(520px, 100%);
    text-align:center;
  }
  .ritual-gif .eyebrow{display:block; margin-bottom:var(--space-3);}
  .ritual-gif-title{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.5rem, 2.8vw, 2rem);
    line-height:1.2;
    text-wrap:balance;
    margin-bottom:var(--space-6);
  }
  .ritual-gif-title .ember-word{color:var(--color-accent);}
  /* No frame: the clip sits on the page on its own, rounded corners only. */
  .ritual-gif img{
    display:block;
    width:min(300px, 100%);
    height:auto;
    margin:0 auto;
    border-radius:var(--radius-2xl);
    background:var(--color-bg-elevated);
  }

  /* CTA under the tutorial clip: tucked closer to the video than the default
     .lifestyle-cta spacing, and the next section starts a little sooner
     (was the full 56px section padding under it). */
  .ritual .lifestyle-cta{margin-top:var(--space-8);}
  #ritual{padding-bottom:clamp(var(--space-3), 2vh, var(--space-4));}
  /* ---------- Lifestyle grid ---------- */
  .lifestyle-grid{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:var(--space-4);
    max-width:1300px;
    margin:var(--space-12) auto 0;
  }

  /* ---------- Lifestyle flip cards ----------
     Front = the product where it lives. Back = the person who put it there.
     One shared 9s cycle drives the cards and the heading, so the copy swaps
     at the exact moment the cards are edge-on and showing nothing.

     Timing (see @keyframes below):
       0–42%   front, heading A
       42–50%  flip out          <- 46% is edge-on, the text swap point
       50–92%  back, heading B
       92–100% flip back         <- 96% is edge-on, text returns
     ------------------------------------------------------------------ */
  /* overflow:hidden here is spec-safe: it forces transform-style:flat only
     on the element that itself declares preserve-3d, which is .flip-inner,
     not .flip-card. Clipping at this level — the card's own box — means a
     rotating face can never visually bulge past its true bounds, at any
     angle, regardless of perspective distance. */
  /* No overflow:hidden here. It clips the card at its exact box, which
     slices the projecting edge mid-rotation into a hard straight cut
     instead of a smooth turn. Containment comes from the perspective
     distance instead (see .headline-flip), plus the section-level clip
     on .headline-block. */
  .flip-card{
    position:relative;
    perspective:1000px;
  }
  .lifestyle-grid .flip-card{aspect-ratio:1/1;}
  .flip-inner{
    position:absolute;
    inset:0;
    transform-style:preserve-3d;
    animation:card-flip var(--flip-cycle) ease-in-out infinite;
  }
  /* Small ripple across the row so it reads as depth rather than four
     shutters snapping at once — under a tenth of a second apart, so the
     group still lands as one event against the heading. */
  /* The stagger lives on the CARD as a custom property, not on .flip-inner
     directly, so every animation inside that card reads the same value.
     Setting it per-element is what broke it: the rotation was staggered
     but the front-face crossfade was not, so on cards 2-4 the image swap
     fired while that card's rotation sat .09-.27s behind. The swap window
     is only ~4% of the cycle, so a .27s offset pushes the tail of the fade
     into the visible period — the image visibly changes on a face that is
     still pointed at you. Custom properties inherit; animation-delay does
     not. This makes drift structurally impossible. */
  .lifestyle-grid .flip-card{--flip-delay:0s;}
  .lifestyle-grid .flip-card:nth-child(2){--flip-delay:.09s;}
  .lifestyle-grid .flip-card:nth-child(3){--flip-delay:.18s;}
  .lifestyle-grid .flip-card:nth-child(4){--flip-delay:.27s;}
  .flip-face{
    position:absolute;
    inset:0;
    backface-visibility:hidden;
    -webkit-backface-visibility:hidden;
    border-radius:var(--radius-2xl);
    overflow:hidden;
  }
  .flip-face img{width:100%; height:100%; object-fit:cover; display:block;}
  .flip-back{transform:rotateY(180deg);}
  @keyframes card-flip{
    0%, 42%   {transform:rotateY(0deg);}
    50%, 92%  {transform:rotateY(180deg);}
    100%      {transform:rotateY(360deg);}
  }

  /* All three headings occupy the same grid cell so the block is sized by
     the tallest one and nothing reflows when they trade places. */
  .heading-swap{display:grid;}
  .heading-swap > span{grid-area:1 / 1;}
  .lifestyle-h2{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.6rem, 3vw, 2.2rem);
    margin-top:0.8rem;
    text-wrap:balance;
  }
  /* Delay matches the middle of the card ripple so the swap reads as one
     synchronised beat with the row rather than leading it. */
  .heading-a, .heading-b{animation:var(--flip-cycle) ease-in-out infinite; animation-delay:.13s;}
  .heading-a{animation-name:heading-out;}
  .heading-b{opacity:0; animation-name:heading-in;}
  @keyframes heading-out{
    0%, 40%    {opacity:1; transform:translateY(0);}
    46%, 90%   {opacity:0; transform:translateY(calc(-1 * var(--space-2)));}
    96%, 100%  {opacity:1; transform:translateY(0);}
  }
  @keyframes heading-in{
    0%, 40%    {opacity:0; transform:translateY(var(--space-2));}
    46%, 90%   {opacity:1; transform:translateY(0);}
    96%, 100%  {opacity:0; transform:translateY(var(--space-2));}
  }
  @media(min-width:901px){ .lifestyle-grid{grid-template-columns:repeat(4,1fr);} }
  .lifestyle-cta{
    text-align:center;
    margin-top:var(--space-12);
  }
  .lifestyle-cta-note{
    margin-top:var(--space-4);
    font-family:var(--font-display);
    font-size:0.72rem;
    letter-spacing:0.08em;
    text-transform:uppercase;
    color:var(--text-on-dark-50);
  }
  /* ---------- Cost comparison chart ---------- */
  .cost-compare-inner{max-width:640px; margin:0 auto;}
  .cost-sub{
    font-family:var(--font-body);
    font-size:0.92rem;
    line-height:1.5;
    color:var(--text-on-dark-60);
    text-align:center;
    max-width:460px;
    margin:var(--space-3) auto 0;
  }
  /* Lifts to full white rather than ember — the accent is already carrying
     the price in .cost-callout, and a second orange phrase this close would
     split the emphasis instead of concentrating it. */
  .cost-sub strong{color:var(--color-text); font-weight:600;}

  .cost-chart{
    list-style:none;
    margin:var(--space-10) 0 0;
    padding:0;
    display:flex;
    flex-direction:column;
    gap:var(--space-6);
  }
  .cost-row-head{
    display:flex;
    align-items:baseline;
    justify-content:space-between;
    gap:var(--space-3);
    margin-bottom:var(--space-2);
    flex-wrap:wrap;
  }
  .cost-name{font-family:var(--font-display); font-weight:600; font-size:0.92rem; color:var(--color-text);}
  .cost-note{font-size:0.76rem; color:var(--text-on-dark-50);}
  .cost-row-body{display:flex; align-items:center; gap:var(--space-4);}
  .cost-track{
    position:relative;
    flex:1;
    height:20px;
    background:var(--border-on-dark-08);
    border-radius:var(--radius-lg);
    overflow:hidden;
  }
  .cost-fill{
    position:absolute;
    left:0; top:0;
    height:100%;
    background:var(--text-on-dark-30);
    border-radius:0 var(--radius-lg) var(--radius-lg) 0;
  }
  .cost-row--accent .cost-fill{background:var(--color-accent);}
  .cost-value{
    flex:0 0 auto;
    min-width:64px;
    text-align:right;
    font-variant-numeric:tabular-nums;
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.92rem;
    color:var(--color-text);
  }
  .cost-value .cost-unit{font-weight:500; font-size:0.68rem; color:var(--text-on-dark-50); margin-left:1px;}
  .cost-callout{
    margin-top:var(--space-8);
    text-align:center;
    font-family:var(--font-display);
    font-weight:600;
    font-size:1rem;
    line-height:1.5;
    color:var(--color-text);
  }
  /* The button's label is a full sentence, so it is set in sentence case (not
     the all-caps .btn style, which would run to three heavy lines), centred,
     and the line break is balanced so it splits into two even lines instead
     of one long line and one orphan word. Phones also get a real gap before
     the footnote; from 561px up the footnote's own spacing applies. */
  .cost-cta{display:flex; justify-content:center; margin-top:var(--space-5); margin-bottom:var(--space-8);}
  .cost-cta .btn{
    justify-content:center;
    text-align:center;
    text-transform:none;
    letter-spacing:0.02em;
    line-height:1.4;
    text-wrap:balance;
    font-size:clamp(0.78rem, 3.4vw, 0.85rem);
    padding:var(--space-3) clamp(var(--space-3), 4vw, var(--space-6));
    /* Phones: hug the two-line label instead of stretching edge to edge. */
    max-width:min(100%, clamp(15.5rem, 62vw, 19rem));
  }
  @media (min-width:561px){
    .cost-cta{margin-bottom:0;}
    .cost-cta .btn{font-size:0.95rem; padding:var(--space-4) var(--space-8); max-width:100%;}
  }
  .cost-callout strong{color:var(--color-accent);}
  .cost-footnote{
    margin-top:var(--space-3);
    text-align:center;
    font-size:0.7rem;
    line-height:1.5;
    color:var(--text-on-dark-40);
  }

  /* ---------- Real-customer photo carousel (auto-scrolling, infinite loop) ---------- */
  .carousel-heading{
    text-align:center;
    max-width:620px;
    margin:var(--space-6) auto 0;
    padding:0 var(--space-container);
  }
  .carousel-heading .eyebrow{display:block; margin-bottom:var(--space-3);}
  .carousel-heading h2{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.5rem, 3.2vw, 2.1rem);
    line-height:1.3;
  }
  .carousel-heading h2 .ember-word{color:var(--color-accent);}
  .carousel-heading .carousel-subtext{
    margin-top:var(--space-3);
    font-family:var(--font-display);
    font-size:0.78rem;
    letter-spacing:0.04em;
    color:var(--text-on-dark-55);
  }
  .carousel-wrap{
    max-width:100%;
    /* Bottom was 0 — the only gap under a carousel came from whatever block
       happened to follow it, so the strip sat tight against the next
       section. space-10 gives it room of its own. */
    margin:var(--space-6) auto var(--space-10);
    position:relative;
    overflow:hidden;
    -webkit-mask-image:linear-gradient(to right, transparent 0, black var(--space-8), black calc(100% - var(--space-8)), transparent 100%);
    mask-image:linear-gradient(to right, transparent 0, black var(--space-8), black calc(100% - var(--space-8)), transparent 100%);
  }
  .carousel-track{
    display:flex;
    gap:var(--space-3);
    width:max-content;
    animation:carousel-scroll 34s linear infinite;
  }
  /* Customer-photo strip only. The loop time is fixed, so every card added
     speeds the strip up — 34s was right for 16 cards. Kept at ~2.2s per
     card (35 cards = 78s), a hair slower than the original 2.1s/card.
     Retune this if the card count changes again. */
  #ugcTrack{animation-duration:78s;}
  .carousel-wrap:hover .carousel-track{animation-play-state:paused;}
  /* Second marquee runs the other way so the two don't read as one long
     conveyor. Starting at -50% and ending at 0 is the same distance, just
     reversed — the halves still line up, so the loop stays seamless. */
  .carousel-heading--lifestyle{margin-top:var(--space-14);}

  /* "Why it's worth keeping close" — reuses .icon-list, and follows the
     same centred-on-mobile / left-on-desktop pattern as .ritual-step so the
     page has one behaviour for a row of short benefits, not two. Sits right
     under a full-bleed marquee, so it needs real space above it or the two
     read as one block. */
  .why-block{
    max-width:1100px;
    /* Space above separates it from the full-bleed marquee; space below
       keeps the last item off the next section's band edge, which read as
       the two running together. */
    margin:var(--space-16) auto var(--space-16);
    padding-inline:var(--space-container);
    text-align:center;
  }
  /* Same size as the page's other section headings, not a subhead — this is
     a section in its own right. */
  .why-title{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.6rem, 3vw, 2.2rem);
    margin-bottom:var(--space-10);
    text-wrap:balance;
  }
  /* (0,2,0) so it beats the unscoped .icon-list{display:flex} regardless of
     which rule comes later in the file. */
  .icon-list.why-list{
    display:grid;
    grid-template-columns:1fr;
    gap:var(--space-10);
  }
  /* Badge over centred text on phones — matches the heading above it. */
  .icon-list.why-list li{
    grid-template-columns:1fr;
    justify-items:center;
    gap:var(--space-3);
    text-align:center;
  }
  /* .icon-list's own type is tuned for the narrow accordion column — 0.9rem
     and letter-spaced, sized to sit beside panel copy. In a full-width 3-up
     row that reads small and off-register against the rest of the page, so
     the title and body take .ritual-step's scale instead, which is the
     component this row actually resembles. */
  .icon-list.why-list .icon-list-title{
    font-size:1.05rem;
    font-weight:600;
    letter-spacing:0;
    margin-bottom:var(--space-2);
  }
  .icon-list.why-list li > span:last-child{
    font-size:0.95rem;
    line-height:1.55;
    color:var(--text-on-dark-60);
  }
  /* Solid ember discs rather than the hairline outline the accordion uses.
     Scoped to .why-list so the accordion panels keep their lighter
     treatment — filled badges there would shout over the body copy.
     The glyph flips to near-black for contrast on the fill, and the stroke
     thickens slightly because thin strokes disappear on a solid ground. */
  .icon-list.why-list .icon-list-badge{
    width:var(--space-12); height:var(--space-12);
    background:var(--color-accent);
    border-color:transparent;
  }
  .icon-list.why-list .icon-list-badge svg{
    width:var(--space-6); height:var(--space-6);
    stroke:var(--color-near-black-500);
    stroke-width:2;
  }
  @media(min-width:901px){
    .icon-list.why-list{grid-template-columns:repeat(3,1fr); gap:var(--space-8);}
  }

  .carousel-wrap--reverse .carousel-track{animation-name:carousel-scroll-rev;}
  /* "Where It Ends Up" strip. Same reasoning as #ugcTrack: the loop time is
     fixed, so more cards means faster. Running at ~2.9s/card since this strip
     was deliberately sped up a tad; 17 cards = 49s. Retune if the count
     changes. */
  .carousel-wrap--reverse .carousel-track{animation-duration:49s;}
  @keyframes carousel-scroll-rev{
    from{ transform:translateX(-50%); }
    to{ transform:translateX(0); }
  }
  @keyframes carousel-scroll{
    from{ transform:translateX(0); }
    to{ transform:translateX(-50%); }
  }
  .carousel-card{
    flex:0 0 auto;
    width:min(160px, 42vw);
    border-radius:var(--radius-lg);
    overflow:hidden;
    aspect-ratio:3/4;
    position:relative;
    background:var(--color-bg-elevated);
  }
  .carousel-card img{width:100%; height:100%; object-fit:cover; display:block;}
  /* Both photo strips ("Real People, Real Relief" and "One bottle, and everywhere the day takes
     it"): a small, clear white Instagram glyph in each photo's top-right corner. Decorative (a
     pseudo-element: not clickable, not read out). The soft shadow keeps it legible on bright shots. */
  .carousel-card::after{
    content:'';
    position:absolute;
    top:var(--space-3);
    right:var(--space-3);
    width:var(--space-5);
    height:var(--space-5);
    background:url("../assets/icons/instagram-white.svg") center/contain no-repeat;
    filter:drop-shadow(0 1px 3px var(--overlay-vignette));
    pointer-events:none;
  }
  /* Per-photo crop nudge, asked for on the grey bathroom-counter shot only.
     1.12 is a considered one-off, not a scale token: it's a crop, not
     spacing. Origin sits high so the dropper tip isn't clipped. */
  .carousel-card--zoom img{transform:scale(1.12); transform-origin:45% 10%;}

  /* ---------- Text review fader ----------
     No card chrome — the quote sits straight on the page like the photos.
     All items are stacked in one grid cell so the block sizes itself to the
     tallest quote, then each crossfades in turn. 10 reviews x 4s = 40s cycle. */
  .review-heading{text-align:center; margin-top:var(--space-12); padding:0 var(--space-container);}

  /* ---------- Customer reviews (vertical list, ecom-style) ----------
     Replaces the old single-quote fading carousel. Cards are rendered by
     JS from window.AFTERSMOKE_REVIEWS (js/reviews-data.js) — 8 at a time,
     "Show More" appends the next batch. Static base markup (heading,
     summary, empty list/photo containers) always renders; only the cards
     themselves depend on JS, same no-JS-safety posture as the rest of the
     page's dynamic bits (cart, gallery). */
  .reviews-section{
    max-width:720px;
    margin:var(--space-8) auto 0;
    /* This is the last thing in #transition-wrap — footer starts right
       after it with only its own top padding for breathing room. Without
       a bottom pad here, "Show More Reviews" sits almost flush against
       the footer logo, reading as one run-on block instead of two
       sections. Matches the space-16 close other full sections (e.g.
       .final-cta) use before handing off to the footer. */
    padding:0 var(--space-container) var(--space-16);
  }
  .reviews-summary{
    display:flex;
    flex-wrap:wrap;
    align-items:baseline;
    justify-content:center;
    gap:var(--space-2) var(--space-3);
    text-align:center;
  }
  .reviews-summary-stars{color:var(--color-star); font-size:1.1rem; letter-spacing:0.14em;}
  .reviews-summary-score{font-family:var(--font-display); font-weight:700; font-size:1rem; color:var(--color-text);}
  .reviews-summary-count{font-size:0.85rem; color:var(--text-on-dark-50);}

  /* "What customers say most" — the digest above the filter pills. Sits on
     the band tone rather than a card so it reads as part of the section
     header rather than as another review. Prose, deliberately: it is a
     summary of what the reviews below actually say, so it should read like
     one rather than like a feature list. */
  .review-digest{
    margin-top:var(--space-8);
    padding:var(--space-6);
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-2xl);
    background:var(--color-bg-band);
  }
  .review-digest-head{
    display:flex;
    flex-wrap:wrap;          /* label and meta sit side by side when there's
                                room and stack when there isn't */
    align-items:baseline;
    justify-content:space-between;
    gap:var(--space-2) var(--space-4);
    padding-bottom:var(--space-4);
    border-bottom:1px solid var(--border-on-dark-08);
  }
  .review-digest-label{
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.95rem;
    letter-spacing:0.02em;
    color:var(--color-text);
  }
  .review-digest-meta{
    display:flex;
    align-items:center;
    gap:var(--space-2);
    font-size:0.8rem;
    color:var(--text-on-dark-50);
  }

  /* Sparkle glyph — the mark an AI-written summary is conventionally given,
     so it reads as "summarised" at a glance rather than needing a caption. */
  .review-digest-pulse{
    display:flex;
    align-items:center;
    flex:0 0 auto;
    color:var(--color-accent);
  }
  .review-digest-pulse svg{
    width:var(--space-4); height:var(--space-4);
    display:block;
    overflow:visible;        /* the large spark scales past its own box */
  }
  .digest-spark-sm{opacity:.6;}


  .review-digest-p{
    margin:var(--space-4) 0 0;
    font-size:0.9rem;
    line-height:1.75;
    color:var(--text-on-dark-70);
  }
  /* The opening paragraph carries the headline finding, so it gets the
     brighter of the two body ramps and the rest recede under it. */
  .review-digest-p:first-of-type{
    font-size:0.95rem;
    color:var(--text-on-dark-78);
  }
  /* A measure cap rather than a breakpoint — long prose at 1440 would run
     past a comfortable line length, and ch tracks the type rather than the
     viewport. */
  @media (min-width:701px){
    .review-digest{padding:var(--space-8);}
    .review-digest-p{max-width:78ch;}
  }

  /* Star-rating filter pills — "All" plus one per rating present in the
     data, built by JS so the counts can never drift out of sync. */
  .reviews-filter{
    display:flex;
    justify-content:center;
    flex-wrap:nowrap;
    gap:clamp(var(--space-1_5), 2vw, var(--space-2));
    margin-top:var(--space-5);
  }
  .reviews-filter-btn{
    flex:0 1 auto;
    min-width:0;
    white-space:nowrap;
    padding:var(--space-1_5) clamp(var(--space-1_5), 2.2vw, var(--space-3));
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-full);
    background:none;
    font-family:var(--font-body);
    /* Scales with the screen so all four pills share one line down to 320px;
       tops out at the original 0.8rem. */
    font-size:clamp(0.66rem, 2.9vw, 0.8rem);
    color:var(--text-on-dark-60);
    cursor:pointer;
    transition:border-color var(--dur-micro) ease, color var(--dur-micro) ease, background var(--dur-micro) ease;
  }
  .reviews-filter-btn:hover{border-color:var(--border-on-dark-25); color:var(--text-on-dark-75);}
  .reviews-filter-btn.is-active{
    background:var(--accent-alpha-12);
    border-color:var(--accent-alpha-40);
    color:var(--color-accent);
  }

  /* Photo strip — Amazon-style "photos from reviews" preview row. Each
     thumbnail opens the lightbox dialog with that review's full text. */
  .reviews-photos{
    display:flex;
    justify-content:center;
    flex-wrap:wrap;
    gap:var(--space-3);
    margin-top:var(--space-6);
  }
  .reviews-photo-thumb{
    aspect-ratio:1/1;
    width:64px;
    padding:0;
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-md);
    overflow:hidden;
    cursor:pointer;
    background:none;
    transition:border-color var(--dur-micro) ease;
  }
  .reviews-photo-thumb:hover{border-color:var(--accent-alpha-50);}
  .reviews-photo-thumb img{display:block; width:100%; height:100%; object-fit:cover;}

  .reviews-list{
    display:flex;
    flex-direction:column;
    margin-top:var(--space-10);
  }
  .review-card{padding:var(--space-6) 0; border-bottom:1px solid var(--border-on-dark-08);}
  .review-card:first-child{padding-top:0;}
  .review-card-stars{color:var(--color-star); font-size:0.95rem; letter-spacing:0.14em;}
  .review-card-stars .is-empty{color:var(--border-on-dark-25);}
  .review-card-title{
    margin-top:var(--space-2);
    font-family:var(--font-display);
    font-weight:700;
    font-size:1rem;
    color:var(--color-text);
  }
  .review-card-body{
    margin-top:var(--space-2);
    font-size:0.92rem;
    line-height:1.65;
    color:var(--text-on-dark-70);
  }
  /* Inline photo on the ~10% of reviews that have one — same lightbox as
     the strip at the top of the section, opened from either place. */
  .review-card-photo{
    display:block;
    width:96px;
    aspect-ratio:1/1;
    margin-top:var(--space-3);
    padding:0;
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-md);
    overflow:hidden;
    cursor:pointer;
    background:none;
    transition:border-color var(--dur-micro) ease;
  }
  .review-card-photo:hover{border-color:var(--accent-alpha-50);}
  .review-card-photo img{display:block; width:100%; height:100%; object-fit:cover;}
  /* Reviewer line. Markup (js/main.js renderCard):
       .review-card-who > .review-card-line (name, Verified Buyer)
                        > .review-card-line (United States, date)
       then the helpful button.
     Phones: the two lines stack as written, so Verified Buyer sits right
     beside the name and every card shares one left edge. (Laid out in a
     row, Verified Buyer started wherever the name ended and drifted from
     card to card.) The helpful button goes under them on the same left
     edge: beside them it only fit on the widest phones, and wrapped to a
     lone right-aligned line everywhere else. */
  .review-card-foot{
    display:flex;
    flex-direction:column;
    align-items:flex-start;
    gap:var(--space-3);
    margin-top:var(--space-4);
    font-size:0.8rem;
    color:var(--text-on-dark-50);
  }
  .review-card-who{display:flex; flex-direction:column; gap:var(--space-1);}
  .review-card-line{display:flex; align-items:center; flex-wrap:wrap; gap:0 var(--space-2);}
  .review-card-name{font-family:var(--font-display); font-weight:600; color:var(--text-on-dark-75);}
  .review-card-verified{display:inline-flex; align-items:center; gap:var(--space-1); color:var(--color-accent);}
  .review-card-verified svg{width:14px; height:14px; flex:none;}
  .review-card-loc{display:inline-flex; align-items:center; gap:var(--space-1_5); font-size:0.72rem; font-weight:400; color:var(--text-on-dark-50);}
  .review-card-loc svg{width:15px; height:8px; flex:none; border-radius:1px;}
  .review-card-date{font-size:0.72rem;}
  .review-card-date::before{content:'·'; margin-inline-end:var(--space-2);}
  .review-card-foot .review-helpful{margin-left:0;}

  /* From 561px: the original one-row arrangement, unchanged. Name with its
     location under it, then Verified Buyer, the date, and the helpful button
     at the far right, all on the name's baseline. The two line wrappers
     drop out (display:contents) so the grid can place each piece. 2px is a
     sub-scale nudge between the name and its location. */
  @media (min-width:561px){
    .review-card-foot{
      display:grid;
      grid-template-columns:auto auto auto 1fr;
      align-items:baseline;
      gap:2px var(--space-3);
    }
    .review-card-who,
    .review-card-line{display:contents;}
    .review-card-name{grid-area:1 / 1;}
    .review-card-loc{grid-area:2 / 1;}
    .review-card-verified{grid-area:1 / 2; display:inline;}
    .review-card-verified svg{display:none;}
    .review-card-date{grid-area:1 / 3; font-size:inherit;}
    .review-card-date::before{content:none;}
    .review-card-foot .review-helpful{grid-area:1 / 4 / 3; justify-self:end; align-self:baseline;}
  }
  .review-helpful{
    display:inline-flex;
    align-items:center;
    gap:var(--space-1_5);
    margin-left:auto;
    padding:var(--space-1) var(--space-2_5);
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-full);
    background:none;
    font-family:var(--font-body);
    font-size:0.78rem;
    color:var(--text-on-dark-50);
    cursor:pointer;
    transition:border-color var(--dur-micro) ease, color var(--dur-micro) ease;
  }
  .review-helpful svg{width:14px; height:14px; stroke:currentColor; fill:none;}
  .review-helpful:hover{color:var(--text-on-dark-75); border-color:var(--border-on-dark-25);}
  .review-helpful.is-active{color:var(--color-accent); border-color:var(--accent-alpha-40);}
  .review-helpful.is-active svg{fill:var(--color-accent);}

  .reviews-showmore{display:flex; margin:var(--space-8) auto 0; justify-content:center;}
  .reviews-showmore[hidden]{display:none;}

  /* Photo review lightbox — same centering technique as the other two
     dialogs (explicit inset+margin rather than the UA default). */
  dialog.review-photo-dialog{
    inset:0;
    margin:auto;
    border:none;
    padding:0;
    border-radius:var(--radius-2xl);
    background:transparent;
    width:calc(100vw - var(--space-8));
    max-width:420px;
    max-height:94vh;
  }
  dialog.review-photo-dialog::backdrop{
    background:color-mix(in srgb, var(--color-near-black-500) 70%, transparent);
    backdrop-filter:blur(4px);
  }
  .review-photo-card{
    position:relative;
    overflow:hidden;
    max-height:94vh;
    overflow-y:auto;
    border-radius:var(--radius-2xl);
    border:1px solid var(--border-on-dark-08);
    background:var(--color-bg-elevated);
  }
  .review-photo-card img{display:block; width:100%; aspect-ratio:1/1; object-fit:cover;}
  .review-photo-close{
    position:absolute;
    top:var(--space-3); right:var(--space-3);
    width:28px; height:28px;
    display:flex; align-items:center; justify-content:center;
    background:color-mix(in srgb, var(--color-near-black-500) 55%, transparent);
    border:none; border-radius:var(--radius-full);
    font-size:1.3rem; line-height:1;
    color:var(--color-text);
    cursor:pointer;
  }
  .review-photo-body{padding:var(--space-5) var(--space-6) var(--space-6);}
  .review-photo-body h4{
    margin-top:var(--space-2);
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.05rem;
    color:var(--color-text);
  }
  .review-photo-body p{margin-top:var(--space-2); font-size:0.9rem; line-height:1.6; color:var(--text-on-dark-70);}
  .review-photo-meta{
    display:flex;
    gap:var(--space-2);
    margin-top:var(--space-4);
    font-size:0.8rem;
    color:var(--text-on-dark-50);
  }
  .review-photo-meta span:first-child{font-weight:600; color:var(--text-on-dark-75);}
  /* Reduced-motion handling for the whole page now lives in one block at the
     bottom of this file — see "Reduced motion". */

  /* ---------- Final CTA ---------- */
  .final-cta{
    background:var(--color-near-black-500);
    color:var(--color-text);
    text-align:center;
    padding:var(--space-16) var(--space-container);
    position:relative;
    overflow:hidden;
    isolation:isolate;
  }
  .final-cta::before{
    /* Bookends the hero: same single ember source, rising behind the CTA. */
    content:'';
    position:absolute;
    inset:0;
    background:
      radial-gradient(ellipse 70% 60% at 50% 92%, var(--accent-alpha-22), transparent 62%),
      radial-gradient(ellipse 55% 48% at 50% 74%, var(--accent-alpha-12), transparent 55%);
    z-index:0;
    pointer-events:none;
  }
  .final-cta > *{position:relative; z-index:1;}
  .final-cta h2{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.9rem, 4vw, 3.2rem);
    margin-bottom:var(--space-5);
  }
  /* Scoped to the intro line by class, NOT `.final-cta p`. That descendant
     selector is (0,1,1) and was reaching every paragraph nested inside the
     section — seven in the accordion, plus .cost-footnote, .directions-note
     and .product-tagline — overriding each one's font-size, max-width and
     margins with the intro's. */
  .final-cta-intro{
    font-family:var(--font-body);
    font-size:1.05rem;
    color:var(--text-on-dark-70);
    max-width:40ch;
    margin:0 auto var(--space-8);
  }
  .final-cta-intro .accent-word{color:var(--color-accent); font-weight:600;}

  /* ---------- Product photo gallery ----------
     One large main photo with a clickable thumbnail strip beneath — swaps
     via JS (main.js), degrades to just showing the first photo without it. */
  .product-gallery{margin:var(--space-8) 0;}
  .product-gallery-main{
    max-width:420px;
    margin:0 auto;
    position:relative;
  }
  .product-gallery-mainslot{--slot-ratio:4/5; border-radius:var(--radius-2xl);}
  /* Sits on .product-gallery-main, NOT inside #galleryMain — the thumbnail
     handler wipes that element's innerHTML on every swap, which would take
     the badge with it. Same pill treatment as .purchase-box-badge.
     In normal flow directly above the photo (it used to be overlaid on it
     at top:14%, tuned by hand to clear the wordmark baked into the photo;
     out of the photo, it can't collide with whatever the photo contains). */
  .product-badge{
    display:inline-block;
    margin-bottom:var(--space-2);
    background:var(--color-accent);
    color:var(--color-near-black-500);
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.66rem;
    letter-spacing:0.06em;
    text-transform:uppercase;
    padding:3px var(--space-3);
    border-radius:var(--radius-full);
  }
  .product-gallery-thumbs{
    display:flex;
    justify-content:center;
    flex-wrap:wrap;
    gap:var(--space-2);
    max-width:420px;
    margin:var(--space-3) auto 0;
  }
  .product-gallery-thumb{
    display:block;
    flex:0 0 auto;
    width:56px;
    padding:0;
    background:none;
    border:2px solid transparent;
    border-radius:var(--radius-md);
    overflow:hidden;
    cursor:pointer;
    opacity:0.55;
    transition:opacity var(--dur-micro) ease, border-color var(--dur-micro) ease;
  }
  .product-gallery-thumb-slot{--slot-ratio:4/5; pointer-events:none;}
  .product-gallery-thumb:hover{opacity:0.85;}
  .product-gallery-thumb.is-active{opacity:1; border-color:var(--color-accent);}

  /* ---------- Product title block ---------- */
  .product-rating{
    display:flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);
    margin:var(--space-8) 0 var(--space-3);
  }
  .product-rating .stars{color:var(--color-star); letter-spacing:0.15em; font-size:1rem;}
  .product-rating .rating-count{font-size:0.85rem; color:var(--text-on-dark-60);}
  /* Now an <a> to the written reviews. Resting state is deliberately
     identical to the old <div> — this sits directly above the product title
     and buy box, so it shouldn't start competing for attention. The count is
     the part that reads as the link, underlined on hover/focus only; the
     stars stay their own colour. */
  /* Only what the <a> needs. No padding to enlarge the hit area: the
     obvious padding-plus-negative-margin trick shifted the whole row 8px
     down here (the gallery's margin-bottom collapses against this one, so
     the subtraction doesn't come back out the way it looks like it should),
     and this row sits directly above the product title. Matching the old
     spacing exactly beats a bigger tap target it was never asked for. */
  a.product-rating{text-decoration:none; width:fit-content; margin-inline:auto; cursor:pointer;}
  a.product-rating .rating-count{
    text-underline-offset:3px;
    transition:color var(--dur-micro) ease;
  }
  /* The stars are inside the anchor, so they already navigate. This just
     makes them LOOK live on hover: they lift toward full-strength star
     yellow instead of sitting there like static decoration while the text
     next to them underlines. */
  a.product-rating .stars{
    transition:filter var(--dur-micro) ease, transform var(--dur-micro) var(--ease-out-soft);
  }
  @media (hover:hover){
    a.product-rating:hover .rating-count{color:var(--color-text); text-decoration:underline;}
    a.product-rating:hover .stars{filter:brightness(1.18); transform:scale(1.04);}
  }
  a.product-rating:focus-visible .rating-count{text-decoration:underline;}
  .product-title{
    font-family:var(--font-display);
    font-weight:800;
    font-size:clamp(1.5rem, 3vw, 2.1rem);
    color:var(--color-accent);
    margin-bottom:var(--space-2);
  }
  /* Held to one line at every width. nowrap alone would overflow a narrow
     phone, so the size scales with the viewport and only caps at 0.95rem
     once there's room for the full string. */
  .product-tagline{
    font-size:clamp(0.7rem, 3.2vw, 0.95rem);
    font-style:italic;
    color:var(--text-on-dark-70);
    margin-bottom:var(--space-8);
    white-space:nowrap;
  }

  /* ---------- Product benefits checklist ---------- */
  .product-benefits{
    list-style:none;
    display:flex;
    flex-direction:column;
    gap:var(--space-3);
    text-align:left;
    max-width:420px;
    margin:0 auto var(--space-8);
  }
  .product-benefits li{
    display:flex;
    align-items:flex-start;
    gap:var(--space-3);
    font-size:0.95rem;
    color:var(--text-on-dark-85);
  }
  .product-benefits li::before{
    content:'✓';
    flex:0 0 auto;
    color:var(--color-accent);
    font-weight:700;
  }

  /* ---------- Purchase box ---------- */
  /* Two selectable plans. The radio is a real <input> with appearance:none
     rather than a decorative span, so selection state, keyboard arrow-key
     navigation and screen-reader announcement all come from the platform —
     only the CTA label needs JS. */
  .purchase-options{
    display:flex;
    flex-direction:column;
    /* space-5, not -4: .purchase-box-badge is translateY(-50%) off the top
       edge, so the gap has to clear the badge overhang of the card below. */
    gap:var(--space-5);
    max-width:420px;
    margin:var(--space-6) auto 0;
  }
  .purchase-box{
    display:flex;
    align-items:center;
    justify-content:space-between;
    /* wrap so .purchase-box-perks can claim a full-width row of its own
       beneath the name/price row. */
    flex-wrap:wrap;
    gap:var(--space-4);
    padding:var(--space-5);
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-lg);
    background:transparent;
    text-align:left;
    position:relative;
    cursor:pointer;
    transition:border-color var(--dur-surface) ease,
               background var(--dur-surface) ease;
  }
  .purchase-box:has(.purchase-box-thumb){gap:var(--space-4) clamp(var(--space-3), 3vw, var(--space-4));}
  .purchase-box:has(input:checked){
    border-color:var(--accent-alpha-40);
    background:var(--accent-alpha-12);
  }
  /* A little more room all round once the perks are showing — the same
     padding that suits a single name/price row reads cramped around a
     five-line list. */
  .purchase-box:has(.purchase-box-perks):has(input:checked){
    padding:var(--space-6) var(--space-5) var(--space-5);
  }
  .purchase-box input[type="radio"]{
    appearance:none;
    -webkit-appearance:none;
    flex:0 0 auto;
    width:20px; height:20px;
    margin:0;
    border-radius:var(--radius-full);
    border:2px solid var(--color-accent);
    background:none;
    position:relative;
    cursor:pointer;
  }
  .purchase-box input[type="radio"]::after{
    content:'';
    position:absolute; inset:3px;
    border-radius:var(--radius-full);
    background:var(--color-accent);
    opacity:0;
    transition:opacity var(--dur-micro) ease;
  }
  .purchase-box input[type="radio"]:checked::after{opacity:1;}
  .purchase-box-badge{
    position:absolute;
    top:0;
    left:var(--space-5);
    transform:translateY(-50%);
    background:var(--color-accent);
    color:var(--color-near-black-500);
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.66rem;
    letter-spacing:0.06em;
    text-transform:uppercase;
    padding:3px var(--space-3);
    border-radius:var(--radius-full);
  }
  /* The one badge that has to win against the other two. Ember is the page's
     accent, so an ember pill reads as the same rank as its neighbours no
     matter what it says; gold is the only colour here that outranks it
     without introducing a new hue. Text stays near-black — gold is light
     enough that the pair clears contrast comfortably.

     Must stay AFTER .purchase-box-badge: same specificity, so source order
     is what decides. It was first written further up the file, next to a
     different component's pill, and the base rule below simply overrode
     it — the badge stayed ember with no sign anything was wrong. */
  .purchase-box-badge--best{
    background:var(--gradient-gold);
    /* Size is inherited from .purchase-box-badge — this pill matches its
       neighbours exactly and stands out on the gold alone. */
    /* The two things that separate metal from a yellow rectangle: a lit top
       edge, and the colour throwing light onto what is behind it. */
    box-shadow:inset 0 1px 0 var(--gold-sheen),
               0 var(--space-1) var(--space-2_5) var(--gold-alpha-45);
  }
  /* Two pills sharing the card's top edge. The wrapper takes over the
     badge's absolute position; the pills inside just sit in a row. */
  .purchase-box-badges{
    position:absolute;
    top:0;
    left:var(--space-5);
    transform:translateY(-50%);
    display:flex;
    gap:var(--space-1_5);
  }
  .purchase-box-badges .purchase-box-badge{position:static; transform:none; white-space:nowrap;}
  /* Product thumb inside an option row. Reuses .photo-slot, so while it's
     empty it renders the same dashed placeholder frame as every other
     unfilled slot on the page, and once an <img> is dropped in the
     :has(img) rule strips the border automatically — no extra CSS. */
  .purchase-box .purchase-box-thumb{
    --slot-ratio:1/1;
    flex:0 0 auto;
    width:clamp(2.5rem, 11vw, var(--space-12));
    border-radius:var(--radius-md);
  }

  /* These are <span>s inside a <label> — label only accepts phrasing
     content, so they can't be divs and need display:block to stack. */
  /* Perks list, revealed only while its option is selected. CSS-only via
     :has(), so there is no JS to keep in sync with the radio state.

     .purchase-box is a flex ROW, so this needs flex-basis:100% to drop onto
     its own line, and the box itself needs to be allowed to wrap. Height is
     not animated: a transition from 0 needs a known end height, and a
     max-height guess either clips the copy or leaves dead space under it.
     Appearing cleanly beats animating badly. */
  .purchase-box-perks{
    flex-basis:100%;
    list-style:none;
    margin:0;
    padding:0;
    display:none;
  }
  /* Heading over the perks. Reuses .eyebrow for the type rather than
     restating it, and carries the divider and top spacing that used to sit
     on the list, since it is now the first thing in the revealed block. */
  .purchase-box-perks-title{
    flex-basis:100%;
    display:none;
    /* .eyebrow sets no line-height, so this inherited the card's — 39px of
       box around 11.5px of text, and 53px at 320px. All that half-leading
       read as a gap between the heading and the first check. */
    line-height:1.4;
  }
  .purchase-box:has(input:checked) .purchase-box-perks-title{
    display:block;
    margin-top:var(--space-5);
    padding-top:var(--space-5);
    border-top:1px solid var(--accent-alpha-22);
    /* Centred on a phone, where the card is narrow enough that a flush-left
       heading over centred-feeling content looks stranded. Left-aligned
       again from 701px, where it lines up with the checks beneath it.

       text-indent cancels the trailing letter-space: .eyebrow tracks at
       0.22em, which centring counts as part of the line, so without this the
       heading sits about half a character left of true centre. */
    text-align:center;
    text-indent:0.22em;
    animation:purchase-perks-in var(--dur-surface) var(--ease-out-soft) both;
  }
  @media(min-width:701px){
    .purchase-box:has(input:checked) .purchase-box-perks-title{
      text-align:left;
      text-indent:0;
    }
  }
  .purchase-box:has(input:checked) .purchase-box-perks{
    display:block;
    /* Zero, because each li already carries --space-2_5 of its own block
       padding — the two stack, and anything here reads as a hole between
       the heading and the first check. */
    margin-top:0;
    padding-bottom:var(--space-2);
    animation:purchase-perks-in var(--dur-surface) var(--ease-out-soft) both;
  }
  @keyframes purchase-perks-in{
    from{opacity:0; transform:translateY(-4px);}
    to{opacity:1; transform:none;}
  }
  @media(prefers-reduced-motion:reduce){
    .purchase-box:has(input:checked) .purchase-box-perks,
    .purchase-box:has(input:checked) .purchase-box-perks-title{animation:none;}
  }
  .purchase-box-perks li{
    display:flex;
    align-items:flex-start;
    gap:var(--space-3_5);
    font-size:0.9rem;
    line-height:1.5;
    color:var(--text-on-dark-85);
    padding-block:var(--space-2_5);
  }
  /* Ember check.

     Height is exactly 1.5em — one line box, matching the li's line-height —
     with the mark centred inside it. That makes it align to the first line
     of text by construction, including when the copy wraps to two lines,
     rather than by a hand-tuned margin.

     The earlier version was a rotated border box. Two problems: rotation
     moves the visual mark away from the element's box centre, so it can
     never be aligned by offsetting the box, and the rotated square's
     bounding box is taller than the mark, which pushed it high. An inline
     SVG has no such gap between the drawn shape and its box.

     The ember is a literal hex here because a data URI cannot read a CSS
     custom property. It is #F3591D, --color-ember-500; change both together. */
  .purchase-box-perks li::before{
    content:'';
    flex:0 0 auto;
    width:0.95em;
    height:1.5em;
    background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 11'%3E%3Cpath d='M1.4 5.8 5.2 9.4 12.6 1.6' fill='none' stroke='%23F3591D' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
    background-size:0.95em auto;
  }
  .purchase-box-info{flex:1;}
  .purchase-box-name{display:block; font-family:var(--font-display); font-weight:700; font-size:0.95rem; color:var(--color-text);}
  .purchase-box-sub{display:block; font-size:0.8rem; color:var(--text-on-dark-55); margin-top:2px;}
  /* The 3-Pack's offer, the loudest line on its card: own line, display
     face, accent colour, a size up from the option name. */
  .purchase-box-offer{
    display:block;
    font-family:var(--font-display);
    font-weight:700;
    /* Scales down with the screen: the column beside the thumbnail and the
       price is narrow on a phone, and "get 1 free" must never break apart. */
    font-size:clamp(0.8rem, 3.2vw, 1.05rem);
    line-height:1.25;
    white-space:nowrap;
    color:var(--color-accent);
    margin-bottom:2px;
  }
  .purchase-box-perks li strong{color:var(--color-text);}
  .purchase-box-price{flex:0 0 auto; text-align:right;}
  .purchase-box-amount{display:block; font-family:var(--font-display); font-weight:700; font-size:1.15rem; color:var(--color-text);}
  .purchase-box-perday{display:block; font-size:0.75rem; color:var(--text-on-dark-50);}

  /* Quiet captions, not boxed callouts — a pill here competed with the buy
     button right above it for attention it doesn't need. The block is
     centred but each line is left-aligned, so the icons form one clean
     column instead of drifting with each line's length. */
  .final-cta .trust-lines{
    list-style:none;
    display:flex;
    flex-direction:column;
    gap:var(--space-3);
    width:max-content;
    max-width:100%;
    margin:var(--space-8) auto 0;
    text-align:left;
  }
  .final-cta .trust-lines li{
    display:flex;
    align-items:flex-start;
    gap:var(--space-2);
    font-size:0.75rem;
    line-height:1.5;
    color:var(--text-on-dark-40);
  }
  /* 18px icon against an 18px line box (0.75rem x 1.5) — they align exactly
     at flex-start, so any nudge here is what knocks the icon off. */
  .final-cta .trust-lines svg{width:18px; height:18px; flex:0 0 auto; stroke:var(--text-on-dark-40);}
  .final-cta .trust-lines strong{color:var(--text-on-dark-60); font-weight:600;}

  /* Accepted-card row under the buy button. flex-wrap:nowrap per the
     flex-row contract (see CLAUDE.md) — six icons at 38px with an 8px gap
     is 268px, inside the 280px available at the 320px floor
     (320 - 2*--space-container's 20px floor), so nowrap never overflows. */
  .final-cta .payment-icons{
    list-style:none;
    display:flex;
    flex-wrap:nowrap;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);
    margin:var(--space-5) auto 0;
    padding:0;
  }
  .final-cta .payment-icons img{display:block; width:38px; height:24px;}

  /* Estimated delivery, between the plan cards and the buy button: one
     quiet line with the earliest date as the only bright text. About 270px
     at its longest, inside the 280px at the 320px floor; flex-wrap is the
     safety net if a font renders wider. */
  .final-cta .delivery-estimate{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:center;
    gap:var(--space-1) var(--space-2);
    max-width:420px;
    margin:var(--space-6) auto var(--space-5);
    font-size:0.82rem;
    line-height:1.4;
    color:var(--text-on-dark-60);
  }
  .final-cta .delivery-estimate[hidden]{display:none;}
  .final-cta .delivery-estimate svg{width:18px; height:18px; flex:0 0 auto; stroke:var(--color-accent);}
  .final-cta .delivery-estimate strong{color:var(--color-text); font-weight:600;}

  /* Guarantee line between the card row and the trust lines. Gold seal
     with a check (Lucide's badge-check icon, ISC), in the review
     stars' gold, so it reads as a promise rather than another ember pill.
     One short line, nowrap: about 250px, inside the 280px available at the
     320px floor. */
  .final-cta .guarantee-seal{
    display:flex;
    flex-wrap:nowrap;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);
    margin:var(--space-6) auto 0;
    font-family:var(--font-display);
    font-weight:600;
    font-size:0.85rem;
    letter-spacing:0.02em;
    color:var(--text-on-dark-85);
    white-space:nowrap;
  }
  .final-cta .guarantee-seal svg{width:24px; height:24px; flex:0 0 auto; stroke:var(--color-star);}

  /* ---------- Product accordion ----------
     Native <details>/<summary> — no JS required to expand/collapse. */
  .product-accordion{
    max-width:560px;
    margin:var(--space-10) auto 0;
    text-align:left;
    border-top:1px solid var(--border-on-dark-08);
  }
  .product-accordion details{border-bottom:1px solid var(--border-on-dark-08);}
  .product-accordion summary{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-4);
    padding:var(--space-5) var(--space-1);
    font-family:var(--font-display);
    font-weight:600;
    font-size:0.95rem;
    color:var(--color-text);
    cursor:pointer;
    list-style:none;
  }
  .product-accordion summary::-webkit-details-marker{display:none;}
  .product-accordion summary::after{
    content:'+';
    flex:0 0 auto;
    font-size:1.2rem;
    color:var(--color-accent);
    transition:transform var(--dur-surface) var(--ease-out-soft);
  }
  /* Rotating the same glyph 45° turns + into × — no character swap needed,
     so it animates smoothly instead of popping between two glyphs. */
  .product-accordion details[open] summary::after{transform:rotate(45deg);}
  .product-accordion .acc-body{
    padding:0 var(--space-1) var(--space-6);
    font-size:0.92rem;
    line-height:1.6;
    color:var(--text-on-dark-60);
  }
  /* The global reset zeroes every margin, so stacked paragraphs inside a
     panel would run together into one block without this. */
  .product-accordion .acc-body p + p{margin-top:var(--space-4);}
  .product-accordion .acc-body ul{margin-top:var(--space-2); padding-left:var(--space-5);}
  .product-accordion .acc-body li{margin-bottom:var(--space-1);}
  /* The cost chart is reused verbatim inside an accordion panel. The two
     rules above are (0,2,1)/(0,2,1) and would out-specify .cost-chart's own
     padding/margin resets, indenting the bars and double-spacing the rows
     on top of the flex gap. These put it back. */
  .product-accordion .acc-body .cost-chart{margin-top:var(--space-6); padding-left:0;}
  .product-accordion .acc-body .cost-chart li{margin-bottom:0;}
  .product-accordion .acc-body strong{color:var(--color-text); font-weight:600;}

  /* ---------- Icon list (accordion) ----------
     Ember-ringed icon badges beside a title and a line of detail. Shared by
     the Directions steps and the Priced For You options — <ol> where order
     matters, <ul> where it doesn't. Scoped past .acc-body's list rules,
     which are (0,2,1) and would otherwise indent it and stack extra margin
     on top of the flex gap. */
  /* Structural rules are unscoped so .icon-list is usable anywhere, not only
     inside an accordion panel — without these a list outside .acc-body falls
     back to raw <ul> bullets with the badge stacked above the text. The
     accordion keeps only its own top spacing below. */
  .icon-list{
    list-style:none;
    display:flex;
    flex-direction:column;
    gap:var(--space-5);
    padding-left:0;
  }
  .icon-list li{
    display:grid;
    grid-template-columns:auto 1fr;
    gap:var(--space-4);
    align-items:start;
    margin-bottom:0;
  }
  .product-accordion .acc-body .icon-list{margin-top:var(--space-4);}
  .icon-list-badge{
    display:grid;
    place-items:center;
    width:var(--space-10); height:var(--space-10);
    border:1px solid var(--accent-alpha-40);
    border-radius:var(--radius-full);
  }
  .icon-list-badge svg{width:var(--space-5); height:var(--space-5); stroke:var(--color-accent);}
  .icon-list-title{
    display:block;
    margin-bottom:var(--space-1);
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.9rem;
    letter-spacing:0.02em;
    color:var(--color-text);
  }
  .directions-note{
    margin-top:var(--space-8);
    padding-top:var(--space-5);
    border-top:1px solid var(--border-on-dark-08);
    font-size:0.85rem;
    line-height:1.7;
    color:var(--text-on-dark-50);
  }

  /* ---------- Static content pages (FAQ / Shipping / Returns) ----------
     Same 560px column .product-accordion already uses, so an FAQ page
     built from that component and a prose page like Shipping sit at
     identical width — one shared "reading column" rather than a second
     magic number. */
  .static-page{
    max-width:560px;
    margin:0 auto;
    padding:var(--space-20) var(--space-container) var(--space-24);
  }
  /* Mascot band — the parent line over the illustration, directly under the
     trust ticker. Centred and large, because this is the brand's position
     stated plainly rather than a caption on a picture. */
  .mascot-band{
    /* --space-12, not -16: with the ticker's own --space-8 bottom padding
       this lands the ticker text 80px clear of the headline, matching the
       80px above it. */
    padding:var(--space-12) var(--space-container) var(--space-10);
    text-align:center;
  }
  /* Sends the reader on to the ingredients rather than ending the band on a
     picture. Ghost, not primary: this is navigation, and an ember button
     here would compete with the buy CTAs either side of it. */
  .mascot-band-cta{margin-top:var(--space-6);}
  .mascot-band-title{
    font-family:var(--font-display);
    font-weight:700;
    /* Set to fill its column, the way the line does in the original
       creative. The vw middle term carries the scaling, so it grows and
       shrinks continuously instead of snapping at a breakpoint. */
    text-transform:uppercase;
    font-size:clamp(2.4rem, 12.5vw, 5.4rem);
    line-height:0.95;
    letter-spacing:-0.02em;
    color:var(--color-text);
    /* No measure cap: the point is that it fills its column, the way the
       line does in the original creative. The band's own gutter is what
       sets the width. */
  }
  /* Left-aligned variant. Alignment only — a band standing between two
     full-width sections still needs the page gutter. */
  .mascot-band--left{text-align:left;}
  /* Drops the gutter too, for a band inside a column that already has a
     measure: there the lockup should line up with the paragraphs rather than
     inset further. */
  .mascot-band--flush{padding-inline:0;}
  /* Two classes, because .static-page h2 is (0,1,1) and sits later in the
     file — it was overriding the lockup's size and margins and rendering the
     line at 1.15rem, the page's ordinary heading size. */
  .mascot-band .mascot-band-title{
    font-size:clamp(2.4rem, 12.5vw, 5.4rem);
    margin-top:0;
    margin-bottom:0;
  }
  .mascot-band--left .mascot-band-title{margin-inline:0;}
  .mascot-band--left .mascot-band-art{margin-inline:0;}
  /* Radius only, no outline. A hairline was tried to even up the frame —
     the picture's top edge averages #171717 against a #151515 page and so
     dissolves, while the sidewalk along the bottom sits at #574C47 and
     holds — but drawing a box around it read worse than the soft top edge
     it was meant to fix. The artwork is left to blend. */
  .mascot-band-art--framed img{border-radius:var(--radius-2xl);}
  /* .ember-word carries no colour of its own — every use in this file is
     scoped to its parent, so this one needs its own rule. */
  .mascot-band-title .ember-word{color:var(--color-accent);}
  /* Two classes, so it beats .brand-figure--band's own margin-block — that
     rule sits later in the file and at equal specificity it was winning,
     leaving 64px between the line and the art. They are one statement, so
     the art sits right under the type. */
  .mascot-band .mascot-band-art{
    margin-block:var(--space-2) 0;
    padding-inline:0;
    max-width:min(520px, 100%);
  }

  /* Brand illustration set into a column of copy. Only the About page uses
     it at the moment, but it is named for what it is rather than for where
     it sits, so dropping it into another column needs no rename.

     No radius and no frame: the artwork's ground is exactly
     --color-near-black-500, so it has no visible edge against the page. If
     it is ever swapped for a file whose ground is not that value, a radius
     has to come back to hide the seam.

     The width cap does nothing on the About column, which is narrower than
     it — it is there so a wider column cannot turn this into a banner. */
  .brand-figure{
    margin:var(--space-10) auto;
    max-width:440px;
  }
  .brand-figure img{
    display:block;
    width:100%;
    height:auto;
  }
  /* Standing on its own between two sections rather than inside a column of
     copy, so it gets the page gutter and a wider cap — at the in-column
     440px it read as a thumbnail dropped between two full-width bands. */
  .brand-figure--band{
    max-width:min(560px, 100%);
    margin-block:var(--space-16);
    padding-inline:var(--space-container);
  }

  .static-page > .eyebrow{display:block; text-align:center; margin-bottom:var(--space-4);}
  .static-page > h1{
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(1.8rem, 4vw, 2.6rem);
    line-height:1.15;
    text-align:center;
  }
  .static-page > .static-intro{
    margin-top:var(--space-5);
    font-size:1rem;
    line-height:1.7;
    color:var(--text-on-dark-75);
    text-align:center;
  }
  .static-page h2{
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.15rem;
    color:var(--color-text);
    margin-top:var(--space-14);
    margin-bottom:var(--space-3);
  }
  .static-page p{
    font-size:0.92rem;
    line-height:1.75;
    color:var(--text-on-dark-60);
  }
  .static-page p + p{margin-top:var(--space-4);}
  .static-page ul{margin-top:var(--space-3); padding-left:var(--space-5); font-size:0.92rem; line-height:1.75; color:var(--text-on-dark-60);}
  .static-page li{margin-bottom:var(--space-2);}
  .static-page strong{color:var(--color-text); font-weight:600;}
  /* Same treatment as the other scoped accent spans on the page (.hero
     h1 .accent, .ritual-aside .hot, .promo-title .accent) — ember colour
     for the one key phrase per paragraph worth the emphasis, not the
     whole sentence. */
  .static-page .accent{color:var(--color-accent); font-weight:600;}
  /* :not(.btn) — otherwise this wins over .btn-primary's own
     color:near-black (higher specificity: class+tag beats a single
     class, and this rule also sits later in the file), turning any
     button placed inside .static-page into ember text on an ember
     background. Real bug, not hypothetical: it's exactly what happened
     to .about-cta. */
  .static-page a:not(.btn){color:var(--color-accent);}
  .static-page a:not(.btn):hover{color:var(--color-accent-hover);}
  .static-page .product-accordion{margin-top:var(--space-10);}
  /* .btn is inline-flex (inline-level) by default, so auto margins alone
     wouldn't center it here — flex (block-level) plus a fit-content
     width is what lets margin-inline:auto actually apply. */
  .about-cta{display:flex; width:fit-content; margin:var(--space-8) auto 0; justify-content:center;}

  .supplement-facts-trigger{
    display:inline-flex;
    align-items:center;
    margin-top:var(--space-3);
    font-family:var(--font-display);
    font-size:0.78rem;
    font-weight:600;
    letter-spacing:0.04em;
    text-transform:uppercase;
    color:var(--color-accent);
    background:none;
    border:none;
    border-bottom:1px solid currentColor;
    padding:0 0 2px;
    cursor:pointer;
    transition:color var(--dur-micro) ease;
  }
  .supplement-facts-trigger:hover{color:var(--color-accent-hover);}
  /* The product-page trigger sits right after .product-tagline, which
     already carries its own margin-bottom:space-8 for the gap down to
     the benefits list below. Margins here don't collapse (this is
     inline-flex, not a block box), so the base margin-top:space-3 above
     was stacking on top of that space-8, pushing the button far lower
     than intended. Zero the redundant top gap and move the real spacing
     to margin-bottom instead, so the button sits close under the
     tagline and the benefits list still gets proper breathing room
     below it. */
  .product-tagline:has(+ .supplement-facts-trigger){margin-bottom:var(--space-2);}
  .product-tagline + .supplement-facts-trigger{margin-top:0; margin-bottom:var(--space-6);}

  /* ---------- Supplement Facts dialog ----------
     One continuous on-brand panel — tag, title, headline stat, ingredient
     pills, then the facts table, all on the same charcoal surface. Real
     data (serving size, blend, amounts) comes straight off the physical
     label; the presentation is the site's own card treatment, not a
     photocopy of a printed panel. */
  dialog.nutrition-dialog{
    border:none;
    padding:0;
    border-radius:var(--radius-lg);
    background:transparent;
    max-width:min(420px, 92vw);
    width:100%;
  }
  dialog.nutrition-dialog::backdrop{
    background:color-mix(in srgb, var(--color-near-black-500) 70%, transparent);
    backdrop-filter:blur(4px);
  }

  /* ---------- First-order promo popup ---------- */
  dialog.promo-dialog{
    /* A floating card, not a screen takeover — this is a first-order
       discount popup, and every DTC site running one (Klaviyo/Privy-style)
       keeps a visible dimmed strip of the page around it on every side.
       That margin is what reads as "this is an overlay", not a new page.
       inset:0 + margin:auto explicitly, rather than trusting the browser's
       default modal-centering margin, which was once seen leaving a gap on
       only ONE side (anchored to an edge, not centered). width is a direct
       viewport-minus-gutter calc rather than a vw percentage, so the
       gutter is an exact, predictable 16px each side at any width. */
    inset:0;
    margin:auto;
    border:none;
    padding:0;
    border-radius:var(--radius-2xl);
    background:transparent;
    width:calc(100vw - var(--space-8));
    max-width:430px;   /* sized to the artwork, not to a text card */
    /* Ceiling, not a target — the card below is sized by its own content
       now, so this only ever engages on a short/landscape screen where
       the content genuinely needs to scroll inside the card. */
    max-height:92dvh;
  }
  dialog.promo-dialog::backdrop{
    background:color-mix(in srgb, var(--color-near-black-500) 70%, transparent);
    backdrop-filter:blur(4px);
  }
  /* Takes the dialog's initial focus so showModal() does not land on the
     close button, which then drew a focus ring the moment the popup opened.
     Not a control, so no ring of its own; the real controls still show
     theirs when tabbed to. */
  .promo-card:focus{outline:none;}
  .promo-card{
    position:relative;
    display:flex;
    flex-direction:column;
    overflow:hidden;
    border-radius:var(--radius-2xl);
    /* No border. It inset the photo by 1px inside the radius, and around the
       curve that strip of card background read as a gap between the artwork
       and the card edge. The photo and the action panel are the card's
       surface now, so they define its shape themselves. */
    border:none;
    background:var(--color-bg-elevated);
    max-height:92dvh;
  }

  /* Visually hidden, but read. The dialog's aria-labelledby needs a real
     element now that the headline lives inside the artwork. */
  .promo-sr{
    position:absolute;
    width:1px; height:1px;
    margin:-1px; padding:0;
    overflow:hidden;
    clip-path:inset(50%);
    white-space:nowrap;
  }

  .promo-photo-wrap{
    position:relative;
    min-height:0;      /* lets the photo shrink inside the flex column */
    display:flex;
    /* The image is a flex item sized height:100% against a box whose own
       height comes from content, and that resolves a fraction of a pixel
       long — at 375px it ran 3.6px past the bottom. The caption is pinned to
       this element, so those pixels showed as a strip of photo beneath the
       scrim, which read as the scrim being cut off. */
    overflow:hidden;
  }
  .promo-photo{
    display:block;
    width:100%;
    height:100%;
    min-height:0;
    /* cover, so the artwork always reaches all four edges. contain was
       letterboxing by about a pixel a side whenever the box ratio drifted
       from the file's 1611:2000, and that sliver of card showing through at
       the rounded corners is what read as a gap.

       Safe to crop here because the box only ever drifts a pixel or two from
       the file's own ratio — the headline at the top and the bottle through
       the middle are never close to being cut. */
    object-fit:cover;
    object-position:center;
  }

  /* Sits on the artwork's top-right, which is empty sky — the only area of
     the frame clear of both the headline and the bottle. Dark circle rather
     than a bare glyph, because the sky behind it is light. */
  /* The actions had no rule of their own — they inherited the old text
     card's gap and padding, which the photo card does not have. */
  .promo-actions{
    flex:0 0 auto;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-3);
    padding:var(--space-5) var(--space-5) var(--space-4);
    border-top:1px solid var(--border-on-dark-08);
    background:var(--color-bg-elevated);
  }

  /* Caption across the foot of the artwork. The scrim is a gradient rather
     than a flat band so it has no visible top edge, and it is fully
     transparent well before the bottle's base at ~85% of the frame. */
  .promo-photo-caption{
    position:absolute;
    left:0; right:0; bottom:0;
    z-index:1;
    margin:0;
    padding:var(--space-10) var(--space-4) var(--space-3);
    background:linear-gradient(to top,
      var(--color-black-900) 0%,
      var(--overlay-vignette) 45%,
      transparent 100%);
    font-family:var(--font-display);
    font-weight:700;
    font-size:clamp(0.85rem, 3.6vw, 1rem);
    line-height:1.35;
    text-align:center;
    text-wrap:balance;
    color:var(--color-text);
  }

  /* The offer, typeset over the artwork's dark upper area. */
  .promo-overlay{
    position:absolute;
    top:0; left:0; right:0;
    z-index:1;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-1);
    /* Tight, because the lockup has to finish above the dropper cap, which
       starts at 34% of the frame. At the old spacing the pill bottom landed
       at 34.0-34.6% and sat on the bottle. */
    padding:var(--space-4) var(--space-6) var(--space-3);
    text-align:center;
    pointer-events:none;   /* never steals the close button's hit area */
  }
  .promo-overlay-pct{
    font-family:var(--font-display);
    font-weight:700;
    text-transform:uppercase;
    font-size:clamp(2.6rem, 13vw, 3.6rem);
    line-height:0.95;
    letter-spacing:-0.01em;
    color:var(--color-accent);
  }
  .promo-overlay-sub{
    font-family:var(--font-display);
    font-weight:700;
    text-transform:uppercase;
    font-size:clamp(1.15rem, 5.6vw, 1.6rem);
    line-height:1.1;
    letter-spacing:0.02em;
    color:var(--color-text);
  }
  /* The lockup is white on every frame. On a bright sky that is a weak
     contrast on its own, so the type carries a soft dark shadow — it does
     not read as a shadow at this size, it just keeps the edges from
     dissolving into the blue. */
  .promo-overlay-pct,
  .promo-overlay-sub,
  .promo-overlay-pill{
    text-shadow:0 1px 10px var(--overlay-vignette);
  }
  .promo-overlay-pill{
    margin-top:var(--space-1);
    padding:var(--space-1_5) var(--space-5);
    /* 65% white at 2px. 25% disappeared against a bright sky and solid read
       heavier than the type it encloses; this keeps the ring clearly drawn
       while still letting the photo through it. */
    border:2px solid var(--border-on-dark-65);
    border-radius:var(--radius-full);
    font-family:var(--font-display);
    font-weight:600;
    text-transform:uppercase;
    font-size:clamp(0.68rem, 3vw, 0.8rem);
    letter-spacing:0.14em;
    color:var(--color-text);
  }

  .promo-close{
    position:absolute;
    top:var(--space-3); right:var(--space-3);
    width:34px; height:34px;
    display:flex; align-items:center; justify-content:center;
    /* Solid disc, no border. A ring around a dark circle on light sky read
       as two edges competing — one outline on the disc, another a hair
       outside it. The disc alone is the edge. */
    background:var(--color-black-900);
    border:none;
    border-radius:var(--radius-full);
    color:var(--color-text);
    cursor:pointer;
    z-index:2;
    transition:background var(--dur-micro) ease;
  }
  .promo-close svg{width:15px; height:15px; display:block;}
  .promo-close:hover{background:var(--color-near-black-500);}
  /* Dashed border echoes the site's own placeholder-slot styling, so the
     code reads as "fill this in at checkout" rather than a random pill. */
  .promo-code{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);
    max-width:420px;
    margin:0 auto;
    padding:var(--space-4) var(--space-6);
    border:1px dashed var(--border-on-dark-25);
    border-radius:var(--radius-lg);
  }
  .promo-code-label{
    font-family:var(--font-display);
    font-size:0.68rem;
    letter-spacing:0.06em;
    text-transform:uppercase;
    color:var(--text-on-dark-50);
  }
  .promo-code-value{
    font-family:var(--font-display);
    font-weight:700;
    font-size:1rem;
    letter-spacing:0.04em;
    color:var(--color-accent);
  }
  /* Both capped to match .promo-code's 420px column — on the card's full
     760px width, a truly full-width button/link stretched edge to edge
     rather than reading as a deliberate column. */
  .promo-cta{max-width:420px; width:100%; margin-inline:auto; justify-content:center;}
  .promo-decline{
    display:block;
    max-width:420px;
    width:100%;
    /* No top margin of its own — .promo-actions is a flex column with its
       own gap now, and the old --space-6 stacked on top of that and left a
       hole between the button and this link. */
    margin:0 auto;
    padding:var(--space-1);
    background:none;
    border:none;
    font-family:var(--font-body);
    font-size:0.82rem;
    color:var(--text-on-dark-50);
    cursor:pointer;
    text-decoration:underline;
    text-underline-offset:3px;
  }
  .promo-decline:hover{color:var(--text-on-dark-70);}

  /* ---- Popup rule, desktop only (901px+). Mobile is the reference and is
     deliberately untouched by this block. ----
     1. The artwork is never cropped. A desktop screen is short and wide, so
        at a fixed 430px the card hit its 92dvh ceiling and the flex column
        squeezed the photo; cover then cut the top and bottom, the lockup
        landed on the dropper cap and the caption scrim on the bottle's base.
        Instead the card's WIDTH is derived from the screen's height: the
        photo's natural height (width x 1398/1125, the file's ratio) plus the
        action panel must fit inside 92dvh. Tall screens still get 430px.
     2. Everything laid over the artwork scales with the CARD, not the
        viewport. cqi rather than vw is a considered exception to the
        "clamp middle term is vw" rule: on desktop the viewport is far wider
        than the card, so vw sizes would stay large while the card shrank
        and the lockup would cover the bottle again. The middle values are
        the mobile maxima divided by 430px, so a 430px desktop card renders
        exactly what a 430px phone card does. */
  @media(min-width:901px){
    dialog.promo-dialog{
      /* Measured: the code box, button and decline link come to ~195px;
         13rem (208px) leaves slack so a rounding pixel never crops. */
      --promo-actions-h:13rem;
      width:min(430px, calc((92dvh - var(--promo-actions-h)) * 1125 / 1398));
    }
    .promo-photo-wrap{container-type:inline-size;}
    .promo-overlay-pct{font-size:clamp(1.6rem, 13.4cqi, 3.6rem);}
    .promo-overlay-sub{font-size:clamp(0.85rem, 5.95cqi, 1.6rem);}
    .promo-overlay-pill{font-size:clamp(0.55rem, 2.98cqi, 0.8rem);}
  }

  /* ---------- Cart drawer ----------
     Right-anchored, full-height panel that opens the instant "Add to
     Cart" is clicked, pre-filled with the selected plan. A native
     <dialog> gets us focus trap / ESC / top-layer for free, same as the
     other two dialogs — the only new thing here is the slide transform.
     The UA stylesheet gives an open dialog `position:fixed; inset:0;
     margin:auto`, which is what centers the other two; overriding inset
     to `0 0 0 auto` (top/right/bottom pinned, left auto) anchors it to
     the right edge full-height instead, same trick as the promo dialog's
     centering override just aimed at an edge instead of the middle. */
  dialog.cart-drawer{
    inset:0 0 0 auto;
    margin:0;
    border:none;
    padding:0;
    width:min(420px, 100vw);
    /* dvh, not vh. On iOS Safari 100vh is the LARGE viewport height — it
       ignores the URL bar — so the drawer was taller than the area actually
       on screen, pushing its own content below the fold and making the body
       scroll. dvh tracks the visible area as the bar shows and hides.
       vh first as the fallback for browsers without dvh; they get the old
       behaviour rather than nothing. */
    /* dvh, which tracks the visible area, so the drawer always reaches the
       bottom of the screen.

       This was briefly svh to stop the message panel resizing mid-scroll:
       svh is the URL-bar-VISIBLE measurement, so when the bar retracted the
       drawer stayed short and left a strip of the page showing beneath it.
       The resizing had a better fix anyway — html:has(dialog[open]) now
       freezes the page behind the drawer, so the bar cannot collapse while
       the cart is open and dvh does not move. Fill the screen, and remove
       the thing that made it change. */
    height:100vh;
    height:100dvh;
    max-height:100vh;
    max-height:100dvh;
    background:transparent;
    /* Starts off-screen right; JS adds .is-open one frame after showModal()
       so the transition has a starting value to animate from — a dialog
       has no equivalent to @starting-style support old enough to rely on
       here, so this is the same double-rAF pattern used for other
       JS-driven reveals on the page. */
    transform:translateX(100%);
    transition:transform var(--dur-surface) var(--ease-out-soft);
  }
  dialog.cart-drawer.is-open{transform:translateX(0);}
  dialog.cart-drawer::backdrop{
    background:color-mix(in srgb, var(--color-near-black-500) 70%, transparent);
    backdrop-filter:blur(4px);
    opacity:0;
    transition:opacity var(--dur-surface) ease;
  }
  dialog.cart-drawer.is-open::backdrop{opacity:1;}
  .cart-drawer-card{
    display:flex;
    flex-direction:column;
    height:100%;
    background:var(--color-bg-elevated);
    border-left:1px solid var(--border-on-dark-08);
  }
  .cart-drawer-header{
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding:var(--space-5) var(--space-6);
    border-bottom:1px solid var(--border-on-dark-08);
  }
  .cart-drawer-header h3{
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.1rem;
    color:var(--color-text);
  }
  .cart-drawer-close{
    width:28px; height:28px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none;
    font-size:1.3rem; line-height:1;
    color:var(--text-on-dark-60);
    cursor:pointer;
  }
  .cart-drawer-close:hover{color:var(--color-text);}
  .cart-drawer-body{
    /* Takes only the height its line items need, and scrolls once there are
       more than fit. It no longer holds the message panel, so scrolling the
       items cannot move the message. */
    flex:0 1 auto;
    min-height:0;
    overflow-y:auto;
    /* Stops scroll chaining: without this, once this scroller hits its top
       or bottom the gesture carries on to the page behind the drawer, so
       flicking inside the cart scrolls the storefront underneath it. */
    overscroll-behavior:contain;
    /* Momentum scrolling on iOS, which also keeps the gesture inside. */
    -webkit-overflow-scrolling:touch;
    padding:var(--space-5) var(--space-6);
    /* Column, so .cart-drawer-note can take the leftover height instead of
       the drawer holding a tall empty gap between one line item and the
       totals. */
    display:flex;
    flex-direction:column;
    /* No visible scrollbar. It still scrolls when a cart is long enough to
       need it — this only hides the chrome, which otherwise draws a track
       down the right edge straight across the divider rules and breaks them
       into two short lines. */
    scrollbar-width:none;
    -ms-overflow-style:none;
    /* Sideways scroll, shut off explicitly.
       Setting only overflow-y:auto makes the computed overflow-x `auto` too,
       and .cart-drawer-note deliberately uses negative inline margins to run
       edge to edge — so it sits exactly flush with the body's content box.
       Any fractional rounding, or iOS rendering the copy a hair wider, tips
       that over and the message panel can be dragged sideways.
       clip rather than hidden: clip does not create a scroll container, so
       this cannot interfere with the vertical scrolling above it. */
    overflow-x:hidden;
    overflow-x:clip;
  }
  .cart-drawer-body::-webkit-scrollbar{width:0; height:0; display:none;}
  /* Name/sub/price layout matches .purchase-box-name/-sub/-amount exactly
     — this is the same line item the customer just picked, just rendered
     in the drawer instead of the purchase card. */
  /* padding-bottom, not a margin on the panel below: this keeps the gap
     attached to the line item, so it holds whether or not the message panel
     is present and however many lines there are. */
  /* The opacity transition is what the pending line settles on: the node is
     now filled in place rather than replaced, so dropping .is-pending is a
     0.72 -> 1 step on the very element the customer is already looking at.
     Eased, that reads as the price arriving; uneased, it is a flash. */
  .cart-line{
    display:flex; align-items:flex-start; gap:var(--space-4);
    padding-bottom:clamp(var(--space-3), 2vh, var(--space-4));
    transition:opacity var(--dur-surface) var(--ease-out-soft);
  }
  /* Same treatment as .purchase-box-thumb — the one other small square
     product thumbnail on the page — so the two read as the same object. */
  .cart-line-thumb{--slot-ratio:1/1; flex:0 0 auto; width:var(--space-14); border-radius:var(--radius-md);}
  .cart-line-info{flex:1; display:flex; flex-direction:column;}
  .cart-line-product{font-size:0.72rem; letter-spacing:0.02em; color:var(--text-on-dark-50);}
  .cart-line-name{font-family:var(--font-display); font-weight:700; font-size:0.95rem; color:var(--color-text); margin-top:2px;}
  /* min-height reserves two lines of this element's own type, so the line
     item is the same height whether the sub copy wraps or not. Subscribe's
     "Delivers every 30 days" fits one line while the other two wrap to two,
     which left Subscribe's line item 15px shorter and its message panel
     correspondingly taller.
     Expressed in em, not px: it is two lines of whatever this element's
     font-size resolves to, so it stays correct if the type scale moves.
     The 2px margin is a sub-scale nudge the spacing tokens deliberately do
     not cover, per CLAUDE.md. */
  .cart-line-sub{
    font-size:0.8rem;
    color:var(--text-on-dark-55);
    margin-top:2px;
    line-height:1.4;
    min-height:2.8em;
  }
  /* Savings math for the two discounted plans (Subscribe & Save's 20%,
     the 3-Pack's flat vs.-three-singles amount). Hidden entirely for the
     One Bottle plan, which has nothing to discount against. */
  .cart-line-savings{display:block; margin-top:var(--space-1_5); font-size:0.75rem; font-weight:600; color:var(--color-accent);}
  .cart-line-savings[hidden]{display:none;}
  /* ---- Cart line controls (quantity stepper + remove) ----
     Added for the Shopify cart, built from the drawer's existing language
     rather than a new visual treatment: same hairline border, same radius
     scale, same muted-to-ember hover as every other control in here. */
  .cart-line-controls{
    display:flex;
    align-items:center;
    gap:var(--space-2);
    margin-top:var(--space-2_5);
  }
  .cart-qty-btn{
    width:26px; height:26px;
    display:flex; align-items:center; justify-content:center;
    border:1px solid var(--border-on-dark-25);
    border-radius:var(--radius-sm);
    background:none;
    color:var(--text-on-dark-75);
    font-size:0.95rem; line-height:1;
    cursor:pointer;
    transition:border-color var(--dur-micro) ease, color var(--dur-micro) ease;
  }
  .cart-qty-btn:hover{border-color:var(--color-accent); color:var(--color-accent);}
  .cart-qty-value{
    min-width:1.5ch;
    text-align:center;
    font-family:var(--font-display);
    font-weight:600;
    font-size:0.85rem;
    color:var(--color-text);
  }
  .cart-line-remove{
    margin-left:var(--space-2);
    background:none; border:none;
    padding:0;
    font-family:var(--font-body);
    font-size:0.75rem;
    color:var(--text-on-dark-50);
    text-decoration:underline;
    text-underline-offset:3px;
    cursor:pointer;
    transition:color var(--dur-micro) ease;
  }
  .cart-line-remove:hover{color:var(--color-text);}
  /* Optimistic line, on screen while Shopify is still answering. Everything
     shown is already known for certain (name, option, artwork); only the
     price is pending, so only the price shimmers. */
  .cart-line.is-pending{opacity:0.72;}
  .cart-line.is-pending [data-cart-line-price]{
    display:inline-block;
    min-width:4.5ch;
    min-height:1em;
    border-radius:var(--radius-sm);
    background:linear-gradient(90deg,
      var(--border-on-dark-08) 25%,
      var(--border-on-dark-25) 50%,
      var(--border-on-dark-08) 75%);
    background-size:200% 100%;
    animation:cart-price-pending 1.1s ease-in-out infinite;
  }
  @keyframes cart-price-pending{
    from{background-position:200% 0;}
    to{background-position:-200% 0;}
  }
  @media(prefers-reduced-motion:reduce){
    .cart-line.is-pending [data-cart-line-price]{animation:none;}
  }
  /* Loading / empty / error line inside the drawer body. */
  /* Brand note under the line items — a statement, not a footnote.

     The drawer has a tall dead stretch between the single line item and the
     totals, and small muted text in the middle of it just looked like
     something left over. This fills that space on purpose: display face,
     headline scale, and the opening clause carrying the ember so it reads
     as the brand speaking at the last moment before checkout. */
  .cart-drawer-note{
    /* Compact panel, floated to the middle of the drawer.

       Two things that look alike but are not: stretching the panel to fill
       the space (flex:1) made it a huge near-empty block on a tall screen,
       while `margin-block:auto` in this flex column gives it only the height
       its content needs and distributes the leftover space evenly above and
       below. Same centred position, none of the dead area inside it. */
    /* Fills the leftover space, content centred inside it.

       margin-block:auto was tried instead and looks wrong here: it sizes the
       panel to its text and then splits the spare height evenly above and
       below, so the two rules float with ~110px of dead gap on each side.
       Taking the space with flex:1 puts those rules hard against the line
       item above and the totals below, and the message still sits in the
       middle — which is the part worth keeping. */
    /* Takes the space the line items do not, so it still fills the drawer on
       a one-item cart. flex-shrink lets it give way first when items pile
       up, and min-height:0 permits that; the items are the thing a customer
       came to see.

       flex:0 1 auto was tried, to stop the panel inflating when Safari's URL
       bar retracts and the drawer gains ~90px. It does stop that, but the
       spare height has to land somewhere, and handing it to the body opened
       a far worse dead gap between the line item and the message. Letting
       the panel absorb it and centre the text is the better of the two. */
    flex:1 1 auto;
    min-height:0;
    display:flex;
    flex-direction:column;
    justify-content:center;
    /* Negative bottom margin eats the body's own bottom padding so the panel
       runs right down to the footer. Without it the panel stops 20px short,
       and that strip plus the footer's border reads as a second line. */
    /* Vertical spacing scales with viewport HEIGHT, not a breakpoint. On a
       667px phone a 3-Pack line (which carries an extra "You save" row) plus
       this panel overflowed the body by 11px, and an overflow here is what
       puts a scrollbar across the rating line. vh keeps it continuous, so
       there is no height at which it suddenly snaps.
       Horizontal padding stays narrow: the panel is already inset by the
       drawer's own gutter, and doubling up squeezed the headline to ~300px
       and forced a third line. */
    /* No negative margins any more: outside .cart-drawer-body it is not
       fighting that element's padding, so it already spans the full width. */
    margin:0;
    /* Symmetric, so the message stays optically centred in the panel.
       An earlier version weighted the bottom padding to close the gap above
       the text, which worked but pushed everything off centre. With the
       panel pinned between the divider rule and the footer, and the content
       centred, that gap is just (panel height - content height) / 2 — so the
       way to close it without moving the text off centre is to let the
       content occupy more of the panel, which the type size below does. */
    padding:clamp(var(--space-3), 2.5vh, var(--space-5)) var(--space-4);
    border-top:1px solid var(--border-on-dark-08);
    /* No border-bottom: .cart-drawer-footer already draws a border-top
       directly beneath this, and the two together read as a double rule. */
    background:var(--accent-alpha-06);
    font-family:var(--font-display);
    font-weight:700;
    /* Backed off from 6vw: at 390px that was 23px, which pushed the message
       to three lines and, with a 3-Pack line item above it, overflowed the
       drawer body — producing the scrollbar that then drew across the rating
       line underneath. */
    /* min(vw, vh) inside the clamp: the message has to answer to BOTH axes.
       Width alone sized it fine on a 390px phone and still overflowed a 600px
       viewport, because a short screen has no room for three big lines no
       matter how wide it is. Continuous in both directions, so there is no
       size at which it jumps. */
    font-size:clamp(1rem, min(5.5vw, 3.2vh), 1.6rem);
    line-height:1.28;
    letter-spacing:-0.015em;
    text-align:center;
    text-wrap:balance;
    color:var(--color-text);
  }
  .cart-drawer-note-copy{margin:0;}
  .cart-drawer-note strong{
    display:block;
    color:var(--color-accent);
    font-weight:700;
    font-size:1.12em;
    margin-bottom:var(--space-4);
    /* min-height reserves one line so the block cannot jolt when a shorter
       phrase rotates in. The fade is opacity + a small lift, matching the
       photo-hero caption swap elsewhere on the site. */
    min-height:1.2em;
    /* Opened up from space-2. With the panel centred, its gap above and
       below is just (panel height - content height) / 2, so the only way to
       trim that gap without shrinking the panel, moving the divider or
       growing the type is to let the content itself take up more room. */
    /* Every rotating phrase is kept short enough to hold one line down to
       320px — verified, not assumed. balance stops the browser breaking a
       short line awkwardly if a longer phrase is ever added. */
    text-wrap:balance;
    transition:opacity var(--dur-surface) var(--ease-out-soft),
               transform var(--dur-surface) var(--ease-out-soft);
  }
  .cart-drawer-note strong.is-fading{opacity:0; transform:translateY(-6px);}
  @media(prefers-reduced-motion:reduce){
    .cart-drawer-note strong{transition:none;}
  }
  /* Rating under the message. Reuses .stars / .rating-count so it inherits
     the star colour and muted count treatment used in the product section,
     rather than restating them. Reset to body scale because it sits inside
     a display-face block that would otherwise inflate it. */
  .cart-drawer-rating{
    display:flex;
    align-items:center;
    justify-content:center;
    flex-wrap:wrap;
    gap:var(--space-1) var(--space-2);
    margin-top:clamp(var(--space-4), 3vh, var(--space-8));
    font-family:var(--font-body);
    font-weight:400;
    letter-spacing:normal;
  }
  .cart-drawer-rating .stars{
    color:var(--color-star);
    letter-spacing:0.12em;
    font-size:0.82rem;
  }
  .cart-drawer-rating .rating-count{
    font-size:0.76rem;
    color:var(--text-on-dark-60);
  }
  .cart-drawer-note[hidden]{display:none;}
  /* Short viewports: a 600-667px visible area (a small phone, or a tall one
     with the URL bar and tab strip both showing) cannot hold a tall line
     item, a full-size message and the whole totals block at once — the
     3-Pack row overflowed by up to 96px. Rather than let the drawer scroll,
     the message gives way, since it is the supporting element and the cart
     itself has to stay usable.
     This is a HEIGHT query, not one of the width breakpoints CLAUDE.md
     closes off; the constraint here really is vertical. */
  @media (max-height: 700px){
    .cart-drawer-note{
      padding-block:var(--space-3);
      font-size:clamp(0.95rem, 4vw, 1.1rem);
      line-height:1.35;
    }
    .cart-drawer-note strong{margin-bottom:var(--space-1);}
    /* The rating repeats what the product section already said; it is the
       first thing worth losing when the choice is that or a scrollbar. */
    .cart-drawer-rating{display:none;}
    .cart-line{padding-bottom:var(--space-5);}
    .cart-drawer-footer{padding:var(--space-4) var(--space-6) var(--space-5);}
  }
  /* Tightest case: a 3-Pack line (tallest, it carries the "You save" row) in
     a ~600px visible area still ran 25px over after the trim above. */
  @media (max-height: 620px){
    .cart-drawer-note{padding-block:var(--space-2); margin-top:0;}
    .cart-line{padding-bottom:var(--space-3);}
    .cart-drawer-trust{margin:var(--space-2) 0; gap:var(--space-1_5);}
    .cart-drawer-continue{padding-top:var(--space-2);}
    .cart-drawer-header{padding-block:var(--space-3);}
  }
  .cart-drawer-status{
    padding:var(--space-6) var(--space-2);
    text-align:center;
    font-size:0.85rem;
    color:var(--text-on-dark-55);
  }
  .cart-drawer-status.is-error{color:var(--color-accent);}
  .cart-drawer-status[hidden]{display:none;}
  /* Checkout with nothing to buy: still visible, visibly not actionable.
     pointer-events:none is what makes the missing href safe on an <a>. */
  .cart-drawer-checkout.is-disabled{
    opacity:0.45;
    pointer-events:none;
  }
  .cart-line-price{
    /* Fixed width, wide enough for "$129.99". Sized to content, this column
       was narrow on Subscribe ($39.99) and wide on the 3-Pack ($129.99),
       which changed how much room the info column had and therefore how the
       product name wrapped — so the line item came out 118px, 133px or
       147px depending purely on the price string. The message panel takes
       whatever height that leaves, so each option's cart looked spaced
       differently. One width means one wrap means one height. */
    flex:0 0 auto;
    /* ch, not px: one character width of the current font, so the column
       reserves room for "$129.99" and keeps doing so if the type changes. */
    min-width:7ch;
    display:flex;
    flex-direction:column;
    align-items:flex-end;
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.05rem;
    color:var(--color-text);
  }
  .cart-line-price-strike{
    font-family:var(--font-body);
    font-weight:400;
    font-size:0.78rem;
    color:var(--text-on-dark-40);
    text-decoration:line-through;
  }
  .cart-drawer-footer{
    padding:var(--space-5) var(--space-6) var(--space-6);
    border-top:1px solid var(--border-on-dark-08);
  }
  .cart-drawer-discount{
    display:flex;
    justify-content:space-between;
    font-size:0.85rem;
    font-weight:600;
    color:var(--color-accent);
    margin-bottom:var(--space-2);
  }
  .cart-drawer-discount[hidden]{display:none;}
  /* Same row shape as the discount above it, but muted rather than ember:
     this is a statement of what WILL happen at checkout, not money already
     coming off the subtotal, and colouring it like the live discount would
     imply the total below it already reflects the 25%. */
  .cart-drawer-promo{
    display:flex;
    justify-content:space-between;
    gap:var(--space-3);
    font-size:0.78rem;
    color:var(--text-on-dark-60);
    margin-bottom:var(--space-2);
  }
  /* Order-cutoff countdown: one line, accent only on the time. */
  .cart-drawer-cutoff{
    display:flex;
    align-items:center;
    gap:var(--space-2);
    font-size:0.78rem;
    color:var(--text-on-dark-75);
    margin-bottom:var(--space-3);
  }
  .cart-drawer-cutoff[hidden]{display:none;}
  .cart-drawer-cutoff svg{flex:none; width:14px; height:14px; color:var(--color-accent);}
  .cart-drawer-cutoff strong{color:var(--color-accent); font-variant-numeric:tabular-nums;}
  .cart-drawer-promo strong{color:var(--color-accent); letter-spacing:0.02em;}
  /* "Want more? Buy 2, get 1 free." under the promo row: plain text-sized
     link, accent only on the offer. */
  .cart-drawer-nudge{
    display:block;
    width:100%;
    margin:0 0 var(--space-2);
    padding:0;
    border:0;
    background:none;
    font:inherit;
    font-size:0.78rem;
    text-align:left;
    color:var(--text-on-dark-75);
    cursor:pointer;
  }
  .cart-drawer-nudge strong{color:var(--color-accent);}
  .cart-drawer-nudge:hover strong{text-decoration:underline;}
  .cart-drawer-nudge[hidden]{display:none;}
  .cart-drawer-promo[hidden]{display:none;}
  /* Explanatory variant: a sentence, not a label/value pair, so it wraps
     across the full width instead of being squeezed into the right-hand
     column that holds "Applied". */
  .cart-drawer-promo.is-note{
    display:block;
    line-height:1.5;
    color:var(--text-on-dark-55);
  }

  /* ---------- 404 page ---------- */
  .notfound{text-align:center;}
  .notfound-art{display:block; width:min(20rem, 80%); height:auto; margin:var(--space-8) auto 0; border-radius:var(--radius-2xl);}
  .notfound-actions{display:flex; flex-wrap:wrap; justify-content:center; gap:var(--space-3); margin-top:var(--space-8);}

  /* ---------- Cookie notice (js/consent.js) ----------
     Small and quiet: a compact card that fades in once. On a phone it sits centred above the chat
     launcher (same clearance as the promo toast); from 901px it tucks into the bottom-left corner,
     clear of the chat button on the right. */
  .cookie-banner{
    position:fixed;
    left:var(--space-container); right:var(--space-container);
    bottom:calc(var(--space-container) + 56px + var(--space-3));
    z-index:205;
    max-width:21rem;
    margin:0 auto;
    display:flex; flex-direction:column; gap:var(--space-2_5);
    padding:var(--space-3) var(--space-4) var(--space-3);
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-2xl);
    background:color-mix(in srgb, var(--color-bg-elevated) 88%, transparent);
    backdrop-filter:blur(10px);
    -webkit-backdrop-filter:blur(10px);
    box-shadow:0 8px 28px var(--overlay-vignette);
    font-family:var(--font-body);
    font-size:0.75rem;
    line-height:1.5;
    color:var(--text-on-dark-60);
    animation:cookie-in var(--dur-surface) var(--ease-out-soft) both;
  }
  @media(min-width:901px){
    .cookie-banner{right:auto; margin:0; bottom:var(--space-container);}
  }
  .cookie-banner[hidden]{display:none;}
  .cookie-banner-text{margin:0; padding-right:var(--space-6);}
  .cookie-banner-text a{color:var(--text-on-dark-75); text-decoration:underline; text-underline-offset:0.2em;}
  .cookie-close{
    position:absolute; top:var(--space-1); right:var(--space-1);
    width:2.25rem; height:2.25rem;
    display:flex; align-items:center; justify-content:center;
    background:none; border:0; border-radius:var(--radius-full);
    font-size:1.1rem; line-height:1; color:var(--text-on-dark-50); cursor:pointer;
  }
  .cookie-close:hover{color:var(--color-text);}
  .cookie-banner-actions{display:flex; gap:var(--space-2); justify-content:flex-end;}
  .cookie-btn{
    min-height:2.5rem;
    padding:0 var(--space-4);
    border-radius:var(--radius-full);
    font-family:var(--font-display);
    font-weight:600;
    font-size:0.68rem;
    letter-spacing:0.06em;
    text-transform:uppercase;
    cursor:pointer;
  }
  .cookie-btn--accept{background:var(--color-accent); color:var(--color-near-black-500); border:1px solid transparent;}
  .cookie-btn--decline{background:transparent; color:var(--text-on-dark-75); border:1px solid var(--border-on-dark-25);}
  .cookie-settings-row{display:block;}
  .cookie-settings-link{background:none; border:0; padding:var(--space-2) 0; font:inherit; color:inherit; text-decoration:underline; text-underline-offset:0.2em; cursor:pointer;}
  @keyframes cookie-in{from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:none;}}
  @media(prefers-reduced-motion:reduce){ .cookie-banner{animation:none;} }

  /* ---------- Promo claim toast ---------- */
  .promo-toast{
    position:fixed;
    left:var(--space-container);
    /* Clears the 56px chat launcher and its own bottom offset, so the two
       never collide on a phone. */
    bottom:calc(var(--space-container) + 56px + var(--space-3));
    z-index:210;
    display:flex;
    align-items:center;
    gap:var(--space-3);
    max-width:min(380px, calc(100vw - 2 * var(--space-container)));
    padding:var(--space-3) var(--space-4);
    border-radius:var(--radius-2xl);
    border:1px solid var(--accent-alpha-40);
    background:var(--color-bg-elevated);
    box-shadow:0 12px 40px var(--overlay-vignette);
    animation:promo-toast-in var(--dur-surface) var(--ease-out-soft) both;
  }
  .promo-toast[hidden]{display:none;}
  .promo-toast-icon{
    display:grid; place-items:center;
    flex:0 0 auto;
    width:26px; height:26px;
    border-radius:var(--radius-full);
    background:var(--color-accent);
    color:var(--color-near-black-500);
  }
  .promo-toast-icon svg{width:15px; height:15px;}
  .promo-toast-text{font-size:0.82rem; line-height:1.45; color:var(--text-on-dark-75);}
  .promo-toast-text strong{color:var(--color-text);}
  .promo-toast-close{
    flex:0 0 auto;
    margin-left:auto;
    width:26px; height:26px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none;
    font-size:1.1rem; line-height:1;
    color:var(--text-on-dark-50);
    cursor:pointer;
  }
  .promo-toast-close:hover{color:var(--color-text);}
  @keyframes promo-toast-in{
    from{opacity:0; transform:translateY(12px);}
    to{opacity:1; transform:none;}
  }
  @media(prefers-reduced-motion:reduce){
    .promo-toast{animation:none;}
  }

  /* ---------- Second-chance promo: left-edge tab + slide-out card ----------
     Shown only after the popup was closed without claiming (js/main.js).
     Fixed to the viewport and narrower than any screen, so it can never
     widen the page. Sits above the sticky header (100/101) and below the
     cookie notice (205) and the claim toast (210). */
  .promo-tab{
    position:fixed;
    left:0;
    top:50%;
    z-index:190;
    /* Vertical label reading bottom to top: vertical-rl runs top to bottom,
       so the tab is turned 180deg. The rounded corners are written on the
       left and land on the right, away from the screen edge. */
    writing-mode:vertical-rl;
    transform:translateY(-50%) rotate(180deg);
    padding:var(--space-4) var(--space-2);
    border:none;
    border-radius:var(--radius-xl) 0 0 var(--radius-xl);
    background:var(--color-accent);
    color:var(--color-near-black-500);
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.72rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    box-shadow:0 var(--space-1) var(--space-4) var(--overlay-vignette);
    cursor:pointer;
    animation:promo-tab-in var(--dur-surface) var(--ease-out-soft) both;
  }
  .promo-tab:hover{background:var(--color-accent-hover);}
  .promo-tab[hidden], .promo-peek[hidden]{display:none;}

  .promo-peek{
    position:fixed;
    left:var(--space-container);
    top:50%;
    z-index:195;
    width:min(20rem, calc(100vw - 2 * var(--space-container)));
    padding:var(--space-5);
    border:1px solid var(--accent-alpha-40);
    border-radius:var(--radius-2xl);
    background:var(--color-bg-elevated);
    box-shadow:0 12px 40px var(--overlay-vignette);
    text-align:center;
    transform:translateY(-50%);
    animation:promo-peek-in var(--dur-surface) var(--ease-out-soft) both;
  }
  .promo-peek:focus{outline:none;}
  .promo-peek-close{
    position:absolute;
    top:var(--space-2);
    right:var(--space-2);
    width:32px; height:32px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none;
    color:var(--text-on-dark-50);
    cursor:pointer;
  }
  .promo-peek-close:hover{color:var(--color-text);}
  .promo-peek-close svg{width:16px; height:16px;}
  .promo-peek-eyebrow{
    display:block;
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.68rem;
    letter-spacing:0.14em;
    text-transform:uppercase;
    color:var(--color-accent);
  }
  .promo-peek-title{
    margin-top:var(--space-2);
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.2rem;
    line-height:1.25;
    color:var(--color-text);
    text-wrap:balance;
  }
  .promo-peek-title span{display:block; color:var(--color-accent);}
  .promo-peek-text{
    margin-top:var(--space-2_5);
    font-size:0.82rem;
    line-height:1.5;
    color:var(--text-on-dark-70);
    text-wrap:pretty;
  }
  .promo-peek-code{margin-top:var(--space-4); padding:var(--space-3) var(--space-4);}
  .promo-peek-cta{width:100%; justify-content:center; margin-top:var(--space-3);}
  @keyframes promo-tab-in{
    from{opacity:0; transform:translate(-100%, -50%) rotate(180deg);}
    to{opacity:1; transform:translateY(-50%) rotate(180deg);}
  }
  @keyframes promo-peek-in{
    from{opacity:0; transform:translate(calc(-1 * var(--space-4)), -50%);}
    to{opacity:1; transform:translateY(-50%);}
  }
  @media(prefers-reduced-motion:reduce){
    .promo-tab, .promo-peek{animation:none;}
  }
  .cart-drawer-subtotal{
    display:flex;
    justify-content:space-between;
    font-family:var(--font-display);
    font-weight:700;
    font-size:1rem;
    color:var(--color-text);
    margin-bottom:var(--space-4);
  }
  .cart-drawer-checkout{display:flex; width:100%; justify-content:center;}
  .cart-drawer-continue{
    display:block;
    width:100%;
    margin-top:var(--space-3);
    padding:var(--space-2);
    background:none;
    border:none;
    font-family:var(--font-body);
    font-size:0.82rem;
    color:var(--text-on-dark-50);
    cursor:pointer;
    text-decoration:underline;
    text-underline-offset:3px;
  }
  .cart-drawer-continue:hover{color:var(--text-on-dark-70);}
  /* Sits between the total and the checkout button now, not under it, so
     the spacing is symmetrical rather than hanging off the bottom. */
  .cart-drawer-trust li{
    display:flex;
    align-items:center;
    gap:var(--space-2_5);
  }
  /* Ember glyphs, sized to the line rather than a fixed px so they stay in
     proportion if the trust copy is ever resized. */
  .cart-drawer-trust svg{
    flex:0 0 auto;
    width:1.15rem; height:1.15rem;
    color:var(--color-accent);
  }
  .cart-drawer-trust{
    list-style:none;
    margin:var(--space-4) 0;
    display:flex;
    flex-direction:column;
    gap:var(--space-2);
    font-size:0.75rem;
    color:var(--text-on-dark-50);
  }

  /* ---------- Support chat widget ----------
     Floating launcher + a non-modal panel. Plain positioned elements, not
     a <dialog> — this should stay open alongside normal page scrolling
     and interaction, unlike the site's three modal dialogs, so none of
     that top-layer/backdrop/focus-trap machinery applies here. z-index
     is set above the sticky header's (101) so it can never end up
     underneath it on a short viewport. */
  .chat-widget{
    position:fixed;
    right:var(--space-container);
    bottom:var(--space-container);
    z-index:200;
  }
  .chat-launcher{
    width:56px; height:56px;
    display:flex; align-items:center; justify-content:center;
    border-radius:var(--radius-full);
    border:none;
    background:var(--color-accent);
    color:var(--color-near-black-500);
    cursor:pointer;
    box-shadow:0 4px 16px var(--overlay-vignette);
    transition:background var(--dur-btn) ease;
  }
  .chat-launcher:hover{background:var(--color-accent-hover);}
  .chat-launcher svg{width:24px; height:24px;}
  .chat-launcher-icon-close{display:none;}
  .chat-widget.is-open .chat-launcher-icon-open{display:none;}
  .chat-widget.is-open .chat-launcher-icon-close{display:block;}

  /* Anchored above the launcher, not centered on the viewport — a plain
     display:none/flex toggle (no [hidden] attribute involved) sidesteps
     the earlier [hidden]-vs-authored-display bug documented on the cart
     drawer discount row: nothing here ever sets `display` outside this
     one is-open toggle, so there's no authored rule left to conflict
     with a UA default. Height uses dvh, not vh, so a mobile on-screen
     keyboard resizing the visual viewport doesn't leave the input row
     pinned off-screen the way a fixed vh figure would. */
  /* Mobile first: a full screen of its own. A floating panel on a phone
     is the wrong shape — it leaves a few inches of chat above the
     keyboard, and because the panel is sized from dvh it SHRINKS as the
     keyboard opens, so typing hides the conversation you are having. Full
     screen means the keyboard takes space from the page, not from the
     chat. The floating panel returns at tablet width and up. */
  .chat-panel{
    display:none;
    flex-direction:column;
    position:fixed;
    inset:0;
    width:100%;
    height:100dvh;
    background:var(--color-bg-elevated);
    border:none;
    border-radius:0;
    overflow:hidden;
    z-index:60;
  }
  @media(min-width:701px){
    .chat-panel{
      inset:auto;
      right:var(--space-container);
      bottom:calc(56px + var(--space-container) + var(--space-3));
      width:min(380px, calc(100vw - 2 * var(--space-container)));
      height:min(560px, calc(100dvh - 56px - 3 * var(--space-container)));
      border:1px solid var(--border-on-dark-08);
      border-radius:var(--radius-2xl);
      box-shadow:0 12px 40px var(--overlay-vignette);
    }
  }
  .chat-panel.is-open{display:flex;}
  .chat-panel-header{
    display:flex;
    align-items:center;
    gap:var(--space-3);
    padding:var(--space-4) var(--space-5);
    border-bottom:1px solid var(--border-on-dark-08);
    background:linear-gradient(180deg, var(--color-charcoal-500), var(--color-charcoal-600));
  }
  /* The mark ships with its own dark plate, which against the header's
     gradient reads as a square sticker pasted on. Rounding it turns that
     plate into a deliberate tile — the same move the favicon makes on a
     home screen. object-fit:cover rather than contain: the source is
     already square, and contain would letterbox it inside the radius. */
  .chat-panel-logo{
    width:40px; height:40px;
    object-fit:cover;
    border-radius:var(--radius-xl);
    flex:0 0 auto;
  }
  .chat-panel-heading{display:flex; flex-direction:column; flex:1; min-width:0;}
  .chat-panel-name{font-family:var(--font-display); font-weight:700; font-size:0.9rem; color:var(--color-text);}
  .chat-panel-status{display:flex; align-items:center; gap:var(--space-1_5); font-size:0.72rem; color:var(--text-on-dark-70);}
  /* Ember, not green — the palette is deliberately one accent colour
     throughout (see README "Current theme"); a status dot is not worth
     breaking that for. */
  .chat-status-dot{width:6px; height:6px; border-radius:var(--radius-full); background:var(--color-accent);}
  .chat-panel-close{
    width:28px; height:28px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none;
    font-size:1.3rem; line-height:1;
    color:var(--text-on-dark-60);
    cursor:pointer;
  }
  .chat-panel-close:hover{color:var(--color-text);}
  .chat-messages{
    flex:1;
    overflow-y:auto;
    /* Tighter than the panel's other sections on purpose. This is the only
       area that has to hold an growing number of items, so every bit of
       vertical rhythm spent here costs a message on a phone. The desktop
       panel has the height to afford the looser spacing and takes it back
       at 701px. */
    padding:var(--space-3) var(--space-4);
    display:flex;
    flex-direction:column;
    gap:var(--space-3);
  }
  /* Bubbles can also run wider here, since the avatar column is gone. */
  .chat-bubble{max-width:92%;}
  .chat-bubble p{padding:var(--space-2_5) var(--space-3_5);}
  .chat-bubble{display:flex; gap:var(--space-2); max-width:88%;}
  .chat-bubble p{
    padding:var(--space-3) var(--space-4);
    border-radius:var(--radius-xl);
    font-size:0.85rem;
    line-height:1.55;
  }
  /* The bubble used to sit on --border-on-dark-08, a border tint, which
     over the panel's charcoal is nearly the panel itself — the message
     read as loose text rather than as a bubble. A real surface tone and
     the full text colour give it an edge to sit on. */
  .chat-bubble-bot p{
    background:var(--color-bg-band);
    border:1px solid var(--border-on-dark-08);
    color:var(--color-text);
  }
  /* Bot bubbles are a column (name over text); user bubbles stay a bare
     paragraph, since labelling someone's own message is just noise.
     min-width:0 so a long unbroken string wraps instead of widening the
     flex row past the panel. */
  .chat-bubble-content{display:flex; flex-direction:column; gap:var(--space-1); min-width:0;}
  .chat-bubble-name{
    font-family:var(--font-display);
    font-size:0.68rem;
    font-weight:600;
    letter-spacing:0.04em;
    color:var(--text-on-dark-50);
    padding-left:var(--space-1);
  }
  .chat-bubble-user{align-self:flex-end; flex-direction:row-reverse;}
  .chat-bubble-user p{background:var(--color-accent); color:var(--color-near-black-500);}
  /* "Typing" indicator — same bubble treatment as a real bot reply, just
     three dots instead of text, shown while the reply is pending. */
  .chat-typing{
    display:flex;
    align-items:center;
    gap:4px;
    padding:var(--space-3) var(--space-4);
    background:var(--border-on-dark-08);
    border-radius:var(--radius-xl);
  }
  .chat-typing span{
    width:6px; height:6px;
    border-radius:var(--radius-full);
    background:var(--text-on-dark-50);
    animation:chat-typing-bounce 1.2s ease-in-out infinite;
  }
  .chat-typing span:nth-child(2){animation-delay:0.15s;}
  .chat-typing span:nth-child(3){animation-delay:0.3s;}
  @keyframes chat-typing-bounce{
    0%, 60%, 100%{transform:translateY(0); opacity:0.5;}
    30%{transform:translateY(-4px); opacity:1;}
  }
  /* The support email in bot replies (agent handoff, guarantee, fallback)
     is a real mailto link, not just text — built via safe DOM node
     insertion in JS, not innerHTML, since only fixed bot copy goes
     through that path, never what a visitor typed. */
  .chat-bubble-bot p a{color:var(--color-accent); text-decoration:underline; text-underline-offset:2px;}
  .chat-bubble-bot p a:hover{color:var(--color-accent-hover);}

  /* Preset quick-reply chips — tappable shortcuts into the same
     keyword-matched reply logic as typing, pinned above the input so
     they're always available, not just on first open. */
  .chat-quick-replies{
    display:flex;
    flex-wrap:wrap;
    gap:var(--space-2);
    /* Top padding separates the row from the last message; it used to be 0,
       so a bubble ending near the bottom sat right on the chips. */
    padding:var(--space-4) var(--space-5) var(--space-3);
  }
  .chat-quick-btn{
    padding:var(--space-2) var(--space-3_5);
    /* Filled, bordered and at full text colour. Unfilled chips with a
       border at 8% over a charcoal panel were close to invisible — the
       same tint the panel's own dividers use. */
    border:1px solid var(--border-on-dark-25);
    border-radius:var(--radius-full);
    background:var(--color-bg-band);
    font-family:var(--font-body);
    font-size:0.78rem;
    color:var(--color-text);
    cursor:pointer;
    transition:border-color var(--dur-micro) ease, color var(--dur-micro) ease,
               background var(--dur-micro) ease;
  }
  .chat-quick-btn:hover{
    border-color:var(--accent-alpha-40);
    background:var(--accent-alpha-12);
    color:var(--color-accent);
  }
  .chat-input-row{
    display:flex;
    align-items:center;
    gap:var(--space-3);
    /* Right padding matches the left so the send button is inset the same
       amount the input's text is, rather than sitting hard against the
       panel edge. */
    padding:var(--space-3) var(--space-5);
    border-top:1px solid var(--border-on-dark-08);
  }
  .chat-input{
    flex:1;
    background:none;
    border:none;
    font-family:var(--font-body);
    /* 16px, not the usual 0.88rem body-text size — iOS Safari auto-zooms
       the whole page on focus for any text input under 16px, which is
       exactly the "zooms in, everything's misaligned" glitch. Nothing
       else on the site has a real text <input>, so this is the only
       place that rule bites. */
    font-size:1rem;
    color:var(--color-text);
    padding:var(--space-2) 0;
  }
  .chat-input::placeholder{color:var(--text-on-dark-55);}
  .chat-send{
    width:36px; height:36px;
    display:flex; align-items:center; justify-content:center;
    border-radius:var(--radius-full);
    border:none;
    background:var(--color-accent);
    color:var(--color-near-black-500);
    cursor:pointer;
    flex:0 0 auto;
    transition:background var(--dur-btn) ease;
  }
  .chat-send:hover{background:var(--color-accent-hover);}
  .chat-send svg{width:16px; height:16px;}
  /* Phone-sized touch targets, and the input row clears the home indicator
     on a notched device. Reverts to the compact desktop sizing at 701px. */
  .chat-panel-header{padding-top:max(var(--space-4), env(safe-area-inset-top));}
  .chat-input-row{padding-bottom:max(var(--space-3), env(safe-area-inset-bottom));}
  .chat-send{width:44px; height:44px;}
  .chat-send svg{width:18px; height:18px;}
  .chat-panel-close{width:44px; height:44px; font-size:1.5rem;}
  @media(min-width:701px){
    .chat-messages{padding:var(--space-4) var(--space-5); gap:var(--space-4);}
    .chat-bubble{max-width:88%;}
    .chat-bubble p{padding:var(--space-3) var(--space-4);}
    .chat-panel-header{padding-top:var(--space-4);}
    .chat-input-row{padding-bottom:var(--space-3);}
    .chat-send{width:36px; height:36px;}
    .chat-send svg{width:16px; height:16px;}
    .chat-panel-close{width:auto; height:auto; font-size:1.25rem;}
  }
  /* Below ~440px wide, a fixed 380px-minus-gutter panel plus the 56px
     launcher can crowd the corner — drop the right gutter to the same
     inline padding the rest of the page uses at that width instead of
     the larger --space-container value. */
  @media(max-width:440px){
    .chat-widget{right:var(--space-4); bottom:var(--space-4);}
    .chat-panel{right:var(--space-4);}
  }

  .nutrition-label{
    background:linear-gradient(160deg, var(--color-charcoal-600), var(--color-charcoal-500));
    color:var(--color-text);
    font-family:var(--font-body);
    /* --nl-boost: a little extra vertical air on tall phones. 0 on short screens (the card
       already scrolls there) and from 561px up; reaches 4px per gap on a ~850px-tall phone,
       roughly +60px in total, spread through every gap so the card is longer but still evenly
       filled rather than padded at the bottom. */
    --nl-boost:clamp(0px, calc((100dvh - 640px) * 0.02), 0.25rem);
    padding:calc(var(--space-6) + var(--nl-boost)) var(--space-5) calc(var(--space-5) + var(--nl-boost));
    border:1px solid var(--border-on-dark-08);
    border-radius:var(--radius-lg);
    position:relative;
  }
  @media(min-width:561px){ .nutrition-label{--nl-boost:0px;} }
  .nutrition-label-close{
    position:absolute;
    top:var(--space-3); right:var(--space-3);
    width:28px; height:28px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none;
    font-size:1.3rem; line-height:1;
    color:var(--text-on-dark-60);
    cursor:pointer;
  }
  .nutrition-label-close:hover{color:var(--color-text);}

  .nutrition-label .nl-tag{
    display:block;
    font-family:var(--font-display);
    font-weight:700;
    font-size:0.72rem;
    letter-spacing:0.12em;
    text-transform:uppercase;
    color:var(--color-accent);
    margin-bottom:var(--space-2);
  }
  .nutrition-label .nl-brand{
    font-family:var(--font-display);
    font-weight:800;
    font-size:2rem;
    color:var(--color-text);
    margin-bottom:calc(var(--space-4) + var(--nl-boost));
  }
  .nutrition-label .nl-serving{
    font-size:0.85rem;
    line-height:1.7;
    color:var(--text-on-dark-85);
    padding-bottom:calc(var(--space-4) + var(--nl-boost));
    border-bottom:1px solid var(--border-on-dark-08);
  }
  .nutrition-label .nl-serving strong{color:var(--color-text);}

  /* The equivalent of "9g of Net Carbs" — one honest, on-brand stat rather
     than a fabricated nutrition breakdown that doesn't apply to a tincture. */
  .nutrition-label .nl-callout{
    display:flex;
    align-items:baseline;
    gap:var(--space-2);
    margin-top:calc(var(--space-4) + var(--nl-boost));
  }
  .nutrition-label .nl-callout-num{
    font-family:var(--font-display);
    font-weight:800;
    font-size:2rem;
    color:var(--color-accent);
  }
  .nutrition-label .nl-callout-label{
    font-family:var(--font-display);
    font-size:0.85rem;
    color:var(--text-on-dark-70);
  }

  .nutrition-label .nl-pill-row{
    display:flex;
    flex-wrap:wrap;
    gap:var(--space-2);
    margin-top:calc(var(--space-4) + var(--nl-boost));
  }
  .nutrition-label .nl-pill{
    font-family:var(--font-display);
    font-size:0.7rem;
    font-weight:600;
    letter-spacing:0.02em;
    color:var(--color-text);
    background:var(--accent-alpha-12);
    border:1px solid var(--accent-alpha-40);
    border-radius:var(--radius-full);
    padding:var(--space-1_5) var(--space-3);
  }

  .nutrition-label .nl-divider{
    height:1px;
    background:var(--border-on-dark-08);
    margin:calc(var(--space-5) + var(--nl-boost)) 0;
  }
  .nutrition-label .nl-facts-title{
    font-family:var(--font-display);
    font-weight:700;
    font-size:1.1rem;
    color:var(--color-text);
    margin-bottom:calc(var(--space-3) + var(--nl-boost));
  }
  .nutrition-label .nl-amount-head{
    display:flex; justify-content:space-between; align-items:baseline;
    font-size:0.75rem; font-weight:700;
    color:var(--text-on-dark-55);
    padding-bottom:var(--space-2);
    border-bottom:1px solid var(--border-on-dark-08);
  }
  .nutrition-label .nl-row{
    display:flex; justify-content:space-between; align-items:baseline;
    padding:calc(var(--space-3) + var(--nl-boost)) 0;
    border-bottom:1px solid var(--border-on-dark-08);
    font-weight:700;
    font-size:0.9rem;
    color:var(--color-text);
  }
  .nutrition-label .nl-blend-list{
    font-size:0.78rem;
    font-weight:400;
    color:var(--text-on-dark-60);
    padding:calc(var(--space-3) + var(--nl-boost)) 0;
    border-bottom:1px solid var(--border-on-dark-08);
    line-height:1.6;
  }
  .nutrition-label .nl-footnote{
    font-size:0.72rem;
    color:var(--text-on-dark-50);
    margin-top:calc(var(--space-3) + var(--nl-boost));
    line-height:1.5;
  }
  .nutrition-label .nl-other{
    font-size:0.78rem;
    color:var(--text-on-dark-85);
    margin-top:var(--space-2);
    font-weight:600;
  }

  /* ---------- Photo hero (full-bleed image, copy laid over it) ----------
     Same treatment as the top hero — display type, ember accent word, one
     primary CTA — but the photo is the whole surface rather than sitting
     beside the copy. Source is 9:16, so the frame is tall on a phone and
     flattens as the screen widens; without that it would be a 2500px slab
     on desktop. */
  .photo-hero{
    position:relative;
    isolation:isolate;
    display:grid;
    place-items:stretch;
    /* Shorter than the 9:16 source on purpose: at a true 9:16 nothing is
       cropped, the frame is enormously tall, and the headline is forced
       down across the bottle label. 4:5 trims the top of the room and
       leaves the bottle in clear space under the type. */
    aspect-ratio:3/4;
    max-height:88vh;
    overflow:hidden;
  }
  @media(min-width:701px){ .photo-hero{aspect-ratio:16/10;} }
  @media(min-width:1200px){ .photo-hero{aspect-ratio:21/9;} }
  /* Crossfading stack. The sources are pre-cropped to 3:4 with the bottle
     centred, so object-position has nothing left to correct here.
     FAILSAFE: the first image is visible from CSS alone, so with JS off or
     reduced motion on the section shows one photo rather than nothing. */
  .photo-hero-fade{
    position:absolute;
    inset:0;
    z-index:-2;
  }
  .photo-hero-fade img{
    position:absolute;
    inset:0;
    width:100%; height:100%;
    object-fit:cover;
    object-position:center center;
    opacity:0;
    transition:opacity 1.1s var(--ease-out-soft);
  }
  .photo-hero-fade img:first-child{opacity:1;}
  /* The convertible photo ("Made for the after."). No zoom: the thin bright band that
     used to run along its top edge is cropped out of the file itself. On wide, short
     frames (701px and up) it is positioned to show more of the top, so the headline
     sits above his glasses instead of over them. */
  @media(min-width:701px){
    .photo-hero-fade img[src*="fade-made-for"]{object-position:50% 20%;}
  }
  /* Scrim: the copy sits over a photographed room, so it needs its own
     ground or the type competes with whatever is behind it. */
  .photo-hero::after{
    content:'';
    position:absolute;
    inset:0;
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--color-near-black-500) 12%, transparent) 0%,
      color-mix(in srgb, var(--color-near-black-500) 24%, transparent) 45%,
      color-mix(in srgb, var(--color-near-black-500) 62%, transparent) 100%);
    z-index:-1;
  }
  /* Headline at the top, CTA at the bottom, photo breathing between them.
     Centring the whole block put the type straight over the bottle, which
     is the one thing in frame that shouldn't be covered. */
  .photo-hero-copy{
    /* A soft shadow under the type, not a hard drop: the scrim is light
       now, so the copy sits on a photographed room that shifts with every
       slide. This keeps it legible over the bright frames without reading
       as an effect. Inherited by the eyebrow, headline and rating. */
    text-shadow:0 1px 3px color-mix(in srgb, var(--color-near-black-500) 55%, transparent),
                0 2px 12px color-mix(in srgb, var(--color-near-black-500) 35%, transparent);
    width:min(1180px, 100%);
    /* Text fades up as the section arrives, a beat behind the photo. */
    animation:photo-hero-copy-in .9s var(--ease-out-soft) .15s both;
    justify-self:center;
    display:flex;
    flex-direction:column;
    justify-content:flex-start;
    /* Tight against the top and bottom edges on purpose: it pushes the
       headline and the CTA apart so the bottle sits in clear space between
       them rather than behind type. */
    /* Enough top padding for the eyebrow to clear the frame edge — at
       space-5 it was sitting under the sticky header and reading as
       missing. */
    padding:var(--space-6) var(--space-container) var(--space-7);
    text-align:center;
  }
  .photo-hero-copy .eyebrow{display:block; margin-bottom:var(--space-3);}
  /* Eyebrow and headline travel together as ONE flex child, otherwise
     space-between spreads all three items and drops the headline into the
     middle of the frame — straight over the bottle. */
  /* Three flex children again, deliberately this time: the eyebrow holds
     the very top, the headline sits lower into the frame, the CTA holds the
     bottom. margin-top on the headline does the dropping, not the flex
     distribution — space-between alone would centre it on the bottle. */
  .photo-hero-head{margin:var(--space-8) 0 0;}
  @media(min-width:701px){ .photo-hero-head{margin-top:var(--space-8);} }
  /* Rating + CTA travel together as the bottom flex child, so margin-top
     auto still pins the pair to the foot of the frame. */
  /* The CTA sits on a solid ember fill, so it has all the contrast it
     needs — the inherited shadow just muddied the label. */
  .photo-hero-copy .btn{text-shadow:none;}
  .photo-hero-foot{
    margin-top:auto;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-4);
  }
  /* Wraps to two centred rows on a phone, one line from 901px — the same
     contract .lifestyle-proof uses. */
  .photo-hero-proof{
    /* White here, not the muted grey .proof-strip uses elsewhere — this one
       sits over a photograph that changes every few seconds, so it needs
       more weight than it does on a flat background. */
    color:var(--color-text);
    flex-wrap:wrap;
    justify-content:center;
    gap:var(--space-1_5) var(--space-3);
    font-size:clamp(0.68rem, 2.8vw, 0.78rem);
  }
  @media(min-width:901px){ .photo-hero-proof{flex-wrap:nowrap;} }
  .photo-hero-copy h2{
    font-family:var(--font-display);
    font-weight:700;
    /* Type carries the frame: at full-bleed the headline runs nearly the
       width of the photo, so it reads as a poster rather than a caption
       floating over someone's living room. */
    /* 12vw, not 6.4 — on a 390px phone 6.4vw is only 25px, so the 2rem
       floor won and the headline never scaled with the photo. At 12vw it
       fills the frame on a phone and still caps at the same 4.6rem on
       desktop, so the wide layout is unchanged. */
    /* Sized for the longest caption in the rotation, not the shortest —
       "Smoking may already be part of your routine." sets the ceiling. */
    font-size:clamp(1.38rem, 6.5vw, 3.6rem);
    /* 1.02 was tight enough to look cramped once a caption ran three or
       four lines; 1.14 lets the lines breathe without adding a line. */
    line-height:1.14;
    letter-spacing:-0.015em;
    text-wrap:balance;
    margin-bottom:0;
  }
  .photo-hero-copy .ember-word{color:var(--color-accent);}
  /* The caption fades with its photo. Transition sits on the h2 so the
     swap itself is invisible; JS holds .is-swapping, changes the text,
     then releases it.
     Opacity alone made it pop into place — the line arrived at full
     position the instant it became visible. Pairing the fade with a small
     rise gives the eye something continuous to follow, and a longer
     duration keeps it from reading as a flicker. Both properties
     composite on the GPU. */
  .photo-hero-copy h2{
    transition:opacity .5s var(--ease-out-soft),
               transform .5s var(--ease-out-soft);
    will-change:opacity, transform;
  }
  /* Long captions only — keeps them to three lines so the bottle stays in
     clear space. Short captions keep the full display size above. */
  .photo-hero-copy h2.is-long{font-size:clamp(1.38rem, 6.5vw, 3rem);}
  /* One slide (the convertible) has his glasses right where a three-line headline
     lands. Slightly smaller type, nudged up a little on phones, keeps the text
     above them. Same size cap as normal, so wide screens are unchanged. */
  .photo-hero-copy h2.is-compact{margin-top:calc(-1 * var(--space-4));}
  @media(min-width:701px){ .photo-hero-copy h2.is-compact{margin-top:0;} }
  .photo-hero-copy h2.is-swapping{opacity:0; transform:translateY(10px);}
  @keyframes photo-hero-copy-in{from{opacity:0; transform:translateY(12px);} to{opacity:1; transform:none;}}
  @media(prefers-reduced-motion:reduce){
    .photo-hero-copy{animation:none;}
    .photo-hero-fade img{transition:none;}
    .photo-hero-copy h2{transition:none; transform:none;}
  }

  /* ---- One moment, one treatment ----
     The photo hero (slideshow, copy over the photo) runs at every width.
     The headline block ("The Other Ritual" + flip card) used to replace it
     from 901px; it was retired from desktop on 2026-10-23 and is now hidden
     everywhere. Its markup stays in index.html, so restoring it is putting
     back `@media(min-width:901px){ .headline-block{display:block;}
     .photo-hero{display:none;} }`. */
  .headline-block{display:none;}

  /* Desktop: the phone's 3:4 frame as a centred card, never a crop. A wide
     banner cut a portrait photo to a strip (the dropper went, and the
     headline landed on the bottle). There is no content outside the photo,
     so the card's height is the screen less the sticky announce bar +
     header (--scroll-offset, 96px; measured 84px at 1440) and a space-4
     gap, and its width follows at 3:4 (CLAUDE.md, "Never let the artwork
     crop"). Sized off 88% of the screen instead, the header covered the
     card's top edge and its eyebrow. The headline is
     sized in cqi against the card, 6.5 to match the phone's 6.5vw, so it
     covers the same share of the photo as on a 390px phone at any desktop
     size. The convertible slide keeps its phone nudge for the same reason.
     Mobile and the 701-900px tablet banner are untouched. */
  @media(min-width:901px){
    .photo-hero{
      /* vh, not dvh: desktop has no collapsing toolbar, and a custom
         property can't fall back if a browser lacks dvh. */
      --photo-hero-card-h:calc(100vh - var(--scroll-offset) - var(--space-4));
      container-type:inline-size;
      aspect-ratio:3/4;
      max-height:none;
      width:min(calc(100% - 2 * var(--space-container)), calc(var(--photo-hero-card-h) * 3 / 4));
      margin:var(--space-12) auto;
      border-radius:var(--radius-2xl);
    }
    .photo-hero-copy h2{font-size:clamp(1.38rem, 6.5cqi, 3.6rem);}
    .photo-hero-copy h2.is-long{font-size:clamp(1.38rem, 6.5cqi, 3rem);}
    .photo-hero-copy h2.is-compact{margin-top:calc(-1 * var(--space-4));}
  }

  /* ---------- Story CTA (last band before the footer) ---------- */
  /* Type hierarchy carries this on its own: the headline runs roughly five
     times the size of the line beneath it, which is what makes a plain band
     read as designed rather than decorated. Deliberately no card, no glass,
     no gradient text — an ember hairline, one accent word, and a lot of air,
     which is the same restraint the rest of the page uses. */
  .story-cta{
    padding:var(--space-24) var(--space-container);
    border-top:1px solid var(--accent-alpha-15);
    background:var(--color-bg-band);
    text-align:center;
  }
  .story-cta-inner{max-width:900px; margin:0 auto;}
  .story-cta .eyebrow{display:block; margin-bottom:var(--space-5);}
  .story-cta-title{
    font-family:var(--font-display);
    font-weight:700;
    /* Tight leading and a slight negative track are what keep type this
       large from looking like a template. Both are set at display sizes
       only; the body copy below keeps the house defaults. */
    font-size:clamp(2.5rem, 7vw, 5rem);
    line-height:0.95;
    letter-spacing:-0.02em;
    text-wrap:balance;
    margin-bottom:var(--space-6);
  }
  .story-cta-title .ember-word{color:var(--color-accent);}
  .story-cta-sub{
    max-width:52ch;
    margin:0 auto var(--space-8);
    font-size:0.98rem;
    line-height:1.65;
    color:var(--text-on-dark-60);
  }

  /* Second half of the card: a hairline, then the community / affiliate pitch. */
  .story-cta-community{
    margin-top:var(--space-12);
    padding-top:var(--space-12);
    border-top:1px solid var(--border-on-dark-08);
  }
  .story-cta-community .eyebrow{margin-bottom:var(--space-4);}
  .story-cta-community .story-cta-sub{margin-bottom:var(--space-6);}

  /* ---------- Footer ---------- */
  footer{
    background:linear-gradient(180deg, var(--color-near-black-500), var(--color-bg-plinth) 100%);
    padding:var(--space-14) var(--space-container) var(--space-8);
    border-top:1px solid var(--border-on-dark-08);
  }
  .footer-grid{
    display:grid;
    grid-template-columns:1fr;
    gap:var(--space-8);
    max-width:1300px;
    margin:0 auto var(--space-12);
    text-align:center;
  }
  .footer-grid p{margin:0 auto;}
  .footer-grid h5{
    font-family:var(--font-display);
    font-size:0.72rem;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color:var(--text-on-dark-50);
    margin-bottom:var(--space-4);
  }
  .footer-grid ul{list-style:none; display:flex; flex-direction:column; gap:var(--space-2_5);}
  .footer-grid a{font-size:0.88rem; color:var(--text-on-dark-75); transition:color var(--dur-micro) ease;}
  .footer-grid a:hover{color:var(--color-accent);}
  .footer-bottom a{transition:color var(--dur-micro) ease;}
  .footer-bottom a:hover{color:var(--color-accent);}
  .footer-bottom{
    max-width:1300px; margin:0 auto;
    display:flex; justify-content:space-between;
    flex-direction:column; align-items:center;
    text-align:center;
    padding-top:var(--space-8); border-top:1px solid var(--border-on-dark-08);
    font-size:0.75rem; line-height:1.6; color:var(--text-on-dark-40);
    flex-wrap:wrap; gap:var(--space-3);
  }
  .social-links{
    display:flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-5);
    width:100%;
  }
  .social-icon{color:var(--text-on-dark-75); display:flex; transition:color var(--dur-micro) ease;}
  .social-icon:hover{color:var(--color-accent);}
  .social-icon svg{width:18px; height:18px;}
  .footer-legal-links{
    display:flex;
    justify-content:center;
    gap:var(--space-2);
    margin-top:var(--space-6);
    font-size:0.75rem;
    color:var(--text-on-dark-40);
  }
  .footer-legal-links a{color:inherit; transition:color var(--dur-micro) ease;}
  .footer-legal-links a:hover{color:var(--color-accent);}
  .legal-note{
    max-width:1300px; margin:var(--space-4) auto 0;
    font-size:0.7rem; color:var(--text-on-dark-30); line-height:1.6;
  }

  @media(min-width:561px){
    .footer-grid{grid-template-columns:1fr 1fr; gap:var(--space-8) var(--space-6); text-align:left;}
    .footer-grid p{margin:0;}
    .brand-mark--footer{margin:0 0 var(--space-1);}
    .footer-bottom{flex-direction:row; align-items:center;}
    .social-links{width:auto; justify-content:flex-start;}
  }
  @media(min-width:821px){
    /* Five tracks: logo + Shop + Support + Company + Partner. Was four,
       which left the fifth column wrapping onto its own row and the
       footer bottom ragged. */
    .footer-grid{grid-template-columns:1.4fr 1fr 1fr 1fr 1fr; gap:var(--space-10);}
  }

  /* ---------- Scroll reveal ----------
     The hidden state is gated behind .js-motion on <html>, which only the
     inline head script can add. No JS (or a thrown error, or a 404 on
     main.js) means the class is never set / gets removed, and every element
     renders normally. Content is never left hidden by a script failure.

     These use `animation`, not `transition`, on purpose: the stagger rule
     `.js-motion [data-reveal-stagger] > *` has specificity (0,2,0) and a
     `transition` shorthand there would out-specify `.habit-card` (0,1,0),
     silently wiping its hover transition. Animations can't collide that way. */
  .js-motion [data-reveal],
  .js-motion [data-reveal-stagger] > *{opacity:0;}

  .js-motion [data-reveal].is-visible{
    animation:reveal-rise var(--dur-reveal) var(--ease-out-soft) both;
  }
  .js-motion [data-reveal-stagger].is-visible > *{
    animation:reveal-rise var(--dur-reveal) var(--ease-out-soft)
              calc(var(--i,0) * var(--reveal-step)) both;
  }
  /* Fade-only. Used where children share hairline borders or a 1px-gap
     divider background (.stat-bar, .habit-row) — translating those tears
     the grid lines mid-animation — and on .carousel-wrap, which is masked
     and hosts the marquee, so it must not take a transform of its own. */
  .js-motion [data-reveal="fade"].is-visible,
  .js-motion [data-reveal-stagger="fade"].is-visible > *{
    animation-name:reveal-fade;
  }
  @keyframes reveal-rise{
    from{opacity:0; transform:translateY(var(--reveal-shift));}
    to{opacity:1; transform:none;}
  }
  @keyframes reveal-fade{
    from{opacity:0;}
    to{opacity:1;}
  }

  /* ---------- Cart badge (presentational) ---------- */
  .cart-count.is-bumped{animation:cart-bump .34s var(--ease-out-soft);}
  @keyframes cart-bump{
    0%{transform:scale(1);}
    40%{transform:scale(1.28);}
    100%{transform:scale(1);}
  }

  /* ---------- Reduced motion ----------
     One block for the whole page. Previously this covered only the review
     fader, leaving the 34s photo marquee — the most motion-heavy element
     here — plus smooth scrolling and the hover transforms running at full
     motion for someone who explicitly asked the OS to cut it. */
  @media(prefers-reduced-motion:reduce){
    html{scroll-behavior:auto;}

    /* The photo marquee. */
    .carousel-track{animation:none;}
    .announce-track{animation:none;}
    .trust-ticker-track{animation:none;}

    /* Flip cards settle on the product side and the heading holds its
       original line — the UGC and the golf/third state stay reachable via
       the review carousel. */
    .flip-inner{animation:none;}
        .heading-a{animation:none; opacity:1;}
    .heading-b{animation:none; opacity:0;}

    /* Reveals: show everything immediately, no rise, no stagger. */
    .js-motion [data-reveal],
    .js-motion [data-reveal-stagger] > *{opacity:1; animation:none;}

    /* Movement-based feedback drops out; colour feedback stays. */
    .btn:hover, .btn:active, .habit-card:hover svg{transform:none;}
    .cart-count.is-bumped{animation:none;}
    .cart-drawer{transition:none;}
    .cart-drawer::backdrop{transition:none;}
    .chat-typing span{animation:none; opacity:1;}
  }
