/* ============================================================
   NUXI — clinically dosed
   Display: Figtree (heavy) · Body: Inter
   Paper + cream base, near-black bands,
   punchy violet / orchid accents with gold as the warm pole,
   monochrome primary actions (see the no-fourth-hue note below)
   ============================================================ */

:root{
  --paper:     #FFFFFF;
  --cream:     #FBF6EE;
  --cream-2:   #F3EADC;
  --cream-3:   #EADCC8;
  /* The dark, and the single biggest reason this page used to read purple.
     It was #17092E — hue 263 at 80% saturation, which is not a near-black at
     all, it is a violet that happens to be dark. Measured over every painted
     element on the home page, the violet family covered 12.7% of the page
     against gold's 6.8%, and 91% of that violet was this one token: the signup
     band, the footer, the ticker, the announce bar, every solid CTA. The
     accents were never the problem. The ground was.
     #1A1720 holds the same hue (260) and drops saturation to 0.28, so nothing
     new is introduced — this is the same violet, taken down to where it reads
     as black with a cast rather than as purple. Contrast barely moves and stays
     nowhere near the floor: 16.4:1 on cream and 17.7:1 on paper, against 17.5
     and 18.8 before, so every ink-on-cream and paper-on-ink pairing on the site
     is still far past AA without re-checking a single one.
     What this buys is distribution. With the ground neutral, violet drops to
     about 1% of the page and goes back to being an accent you notice — the
     headline, the eyebrows, the progress rail — instead of the colour of
     everything dark. Gold becomes the dominant hue, which is what a cream page
     wants. Keep this desaturated; if it drifts back up the whole page tints. */
  --ink:       #1A1720;
  --black:     #0A0A0A;
  --black-46:  rgba(10,10,10,.46);

  --violet:    #6D28D9;
  --violet-2:  #8B5CF6;
  --violet-3:  #A78BFA;
  --violet-lt: #F2EBFF;
  /* The pressed step under the primary button, and the only fill that survives
     the bundle band. Not a new hue — same 263 hue as --violet, taken down in
     lightness — so it adds a value step to the violet ramp rather than a fourth
     colour to the page. Measured against the band's three stops it holds
     7.15 / 5.05 / 3.49:1, clearing the 3:1 floor at the flame end where plain
     --violet drops to 2.26. */
  --violet-dk: #4C1D95;

  --gold:      #E8A317;
  --gold-2:    #FFC94D;
  --gold-lt:   #FFF4DC;

  /* Orange is back, but as the far end of the existing warm pole rather than a
     new hue of its own — which is the whole difference between this and the
     #FF5A1F that got pulled. The rule it obeys: --flame only ever appears
     touching gold, in a gradient or immediately beside it, so the eye reads one
     warm ramp running yellow -> amber -> orange instead of two warm colours
     arguing. It is never a flat fill on its own and never a primary action.
     The two-value split mirrors gold/gold-2 exactly, and it is a contrast
     split, not a taste one:
        --flame   #F2661A   ink on it 6.00:1  ✓   it on paper 3.14:1  ✗
        --flame-2 #FF8A3D   on ink    8.03:1  ✓   on paper    2.35:1  ✗
     So --flame is a FILL that carries ink text, and --flame-2 is text and small
     accents on the ink bands. Neither is ever used for text on cream or paper —
     that is the failing 3.12:1 that sank the old orange, and nothing here
     repeats it. */
  --flame:     #F2661A;
  --flame-2:   #FF8A3D;
  /* The claim button, and only the claim button. It was asked to be lighter and
     punchier than --flame, and the honest way to do that without opening a
     fourth warm hue was to stay on the ramp and move along it:
        --flame     hue 21.1  L 52.5%  S  89.3%   ink on it 5.64:1
        --flame-hot hue 22.7  L 58.0%  S 100.0%   ink on it 6.79:1  <- here
        --flame-2   hue 23.8  L 62.0%  S 100.0%   ink on it 7.54:1
     Lighter (+5.5 points of L), punchier (saturation pinned at 100%, which is
     where the "electric" reading comes from — not from a hue shift), and it
     lands between the two tokens that already exist, so the yellow -> amber ->
     orange ramp is unbroken. Contrast goes UP, not down: a lighter fill under
     the same --ink label is a bigger step, 6.79:1 against the old 5.64:1.

     The "Save ₹51" pill inside the button quietly came along for the ride, and
     it was the one that actually needed it. .po-save has no colour of its own —
     it is rgba(26,23,32,.15) over whatever the fill is — so lightening the fill
     lightens the pill with it: composite rgb(210,90,27) -> rgb(221,107,40), and
     --ink on it 4.40:1 -> 5.23:1. The old value was under AA for normal text and
     was only passing on the large-bold exemption. This one passes outright.
     Anything that changes this fill changes that pill; re-measure both.

     Same law as the rest of the pole, and it matters more here than anywhere
     because this is the most saturated thing on the page: it is a FILL. On
     paper it is 2.60:1 and must never be text. The ticking figure beside the
     button is type on paper and stays on --flame-dk. */
  --flame-hot: #FF7A29;
  /* the text-safe end of the ramp, for warm type on the cream/paper grounds.
     Needed because the whole warm pole is unreadable up there — gold is 1.99:1
     on its own tint and --flame only reaches 2.87:1, both under the 4.5 floor.
     This clears it at 5.00:1 on #FFF4DC and 5.46:1 on paper while still reading
     as orange rather than brown. */
  --flame-dk:  #B4470D;

  /* There is no cool pole. Two tokens stood here, --sky and --sky-dk, lifted
     off the brand film's closing sky so the landing hero could stand in the
     colour the curtain had just left. The continuity argument was sound and the
     result was not: a blue wash behind white glass reads as a tint on the
     product, not as the room around it. The hero is a warm neutral seamless now
     and the blue has no second home on the site, so the tokens go with it
     rather than sitting here waiting to be reached for. film.js still owns its
     own sky, which is where that blue belongs — in the film. */

  --orchid:    #C026D3;

  /* There is still deliberately no fourth brand hue in this slot.
     It held #FF5A1F and then #D81B60, and both failed for the same structural
     reason: violet and orchid are already two loud saturated hues, and a third
     one competing with them made the page read as a paint chart rather than a
     product. The fix was not a better hue, it was one hue fewer.
     --flame above is not a reopening of this slot, and the distinction is the
     load-bearing one: a brand hue is a colour that can appear anywhere on its
     own, and --flame cannot — it is bound to gold and only extends a ramp that
     already existed. Orange as a free agent is what failed; orange as the hot
     end of the gold is what works.
     Primary actions therefore stay monochrome — ink on the light grounds, paper
     on the ink bands — which lets violet and the warm ramp go back to being
     accents instead of contestants. Black on white is also the strongest
     contrast available (16.4:1 against the old orange's failing 3.12:1). */

  /* Strictly functional. Not a brand colour and never used as one: this is
     destructive-and-warning only, at small sizes, in the cart. It is kept far
     from the accents on purpose so that red always means red. */
  --warn:      #C0281C;

  --ink-90: rgba(26,23,32,.90);
  --ink-64: rgba(26,23,32,.64);
  --ink-46: rgba(26,23,32,.46);
  --ink-30: rgba(26,23,32,.28);
  --ink-14: rgba(26,23,32,.13);
  --ink-08: rgba(26,23,32,.07);

  --paper-64: rgba(255,255,255,.64);
  --paper-40: rgba(255,255,255,.40);
  --paper-16: rgba(255,255,255,.16);

  --ease:      cubic-bezier(.16,1,.3,1);
  --ease-soft: cubic-bezier(.22,.61,.36,1);
  --ease-back: cubic-bezier(.34,1.56,.64,1);

  --s-1:8px; --s-2:16px; --s-3:24px; --s-4:40px; --s-5:64px; --s-6:96px; --s-7:140px;
  --vh:100dvh;
  --maxw:1240px;
  --pad:clamp(16px,4vw,44px);

  --font-display:'Figtree';
  --font-body:'Inter';
}

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

/* scroll-padding keeps in-page anchor jumps clear of the sticky nav — without
   it the target section lands flush at y=0 and its heading sits underneath.
   Nav is 69px here, 104px once the links wrap at 720px; both get a little
   breathing room on top. */
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth; overflow-x:clip; scroll-padding-top:92px }

body{
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font-family:var(--font-body),'Helvetica Neue',Arial,sans-serif;
  font-size:16px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:clip;
}
body.lock{overflow:hidden}

img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
h1,h2,h3,h4{
  margin:0;
  font-family:var(--font-display),sans-serif;
  font-weight:900;
  letter-spacing:-.03em;
}
p{margin:0}
ul,ol{margin:0;padding:0;list-style:none}
button{font:inherit;color:inherit}
/* the UA's [hidden]{display:none} loses to any author display rule, so the
   cart's grid/flex containers would stay visible while "hidden" */
[hidden]{display:none !important}

/* ============ intro curtain ============
   The curtain is the brand film (film.js, and see film.html for the same film
   with a player around it) run full-bleed for five seconds on the first visit
   of a session. There is almost nothing to style: the whole sequence is drawn
   into one canvas, so what used to be eight hundred lines of keyframes down
   here is now a fixed panel, a way out of it, and the dissolve at the end.

   The panel is the film's opening sky rather than black. It is what the canvas
   paints on its first frame, so the moment the bitmaps land there is no step in
   colour — the picture simply arrives in a field that was already the right
   blue. */
.intro{
  position:fixed;inset:0;z-index:200;
  background:#0C2A5E;
  overflow:hidden;
  /* Opacity, and nothing else. See .intro.gone. */
  transition:opacity .3s ease;
}

/* .film-stage is shared with film.html, where it is one screen inside a scrolling
   page and caps its own height so an ultrawide does not get a 400px letterbox.
   In here it IS the screen and those limits are wrong — a 1040px cap on a tall
   phone would letterbox the curtain against its own background. */
.intro .film-stage{height:100%;max-height:none;min-height:0}

/* Only script.js ever takes the curtain down, so without it the page would sit
   behind a fixed opaque panel forever. The .js class is set by an inline script
   in <head>, so this costs no flash for anyone who does have it. */
html:not(.js) .intro{display:none}
/* Second visit this session: never paint it at all. The same inline script sets
   this, which is the entire point of it being inline — script.js is deferred,
   and by the time a deferred script could add a class the sky has already had a
   frame or two on screen. The skip button goes with it; there is nothing left to
   skip. script.js reads this class rather than re-reading storage, so the
   decision is made exactly once. */
html.intro-seen .intro,
html.intro-seen .intro-skip{display:none}

/* ---- skip ----
   Fixed and a sibling of the curtain, not a child of it: #intro is aria-hidden,
   and a tab stop inside an aria-hidden subtree is a control that exists for a
   mouse and not for a screen reader. z-index clears the curtain's 200.

   White-on-sky rather than white-on-black now, so the border and fill carry more
   weight than they did over pitch — a 6% white wash is invisible against a mid
   blue. It fades in late on purpose: offering an exit in the first second is an
   apology for the thing you just started. */
.intro-skip{
  position:fixed;z-index:201;
  right:clamp(14px,3vw,30px);bottom:clamp(14px,3vw,30px);
  padding:9px 17px;border-radius:999px;
  border:1px solid rgba(255,255,255,.34);
  background:rgba(9,32,72,.30);
  -webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
  color:rgba(255,255,255,.86);
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;
  cursor:pointer;opacity:0;
  /* 1.1s of delay was a third of a 3.6s film. Against 2.56s it would leave the
     way out on screen for less than a second, which is an exit nobody has time
     to find. Same reasoning as before, rescaled: late enough not to be an
     apology for the thing you just started, early enough to be usable. */
  animation:fade .45s var(--ease) .8s forwards;
  transition:color .25s var(--ease),border-color .25s var(--ease),background .25s var(--ease);
}
.intro-skip:hover{color:#0C2A5E;border-color:transparent;background:rgba(255,255,255,.92)}
.intro-skip:focus-visible{outline:2px solid #fff;outline-offset:3px}
html:not(.js) .intro-skip{display:none}

/* Phone: the thumb is at the bottom of the screen and the film's last act is a
   wordmark centred in it, so the button moves out of the middle and gets a
   proper tap target rather than a 32px pill. */
@media (max-width:560px){
  .intro-skip{
    right:14px;bottom:calc(14px + env(safe-area-inset-bottom,0px));
    padding:11px 19px;font-size:11.5px;
  }
}

/* OPACITY ONLY, AND THAT IS LOAD-BEARING.

   This rule has been three things. It slid the panel off the top, which read as
   a cut — a panel travelling 101% of the viewport is a scene change. Then it
   pushed in and defocused, scale(1.09) plus blur(7px), on the argument that the
   film's last move was the camera coming out of a cloud and carrying straight
   on through the wordmark kept one continuous shot.

   Both are wrong now, and the second one is actively destructive. The film no
   longer ends on a wordmark centred in a blue sky. It ends on WHITE PAPER with
   the wordmark standing on the exact rect of the nav logo below — same place,
   same size, same #111 — which film.js spends the last second and a half of the
   cut arriving at. Scaling the curtain 9% about its centre would take that mark
   and shove it several pixels up and left of the logo it just landed on, and a
   7px blur would smear it, so the final frames would be a sharp logo emerging
   from a soft doubled ghost of itself sliding off it. Every pixel of precision
   film.js earned would be thrown away here, in the last 200ms.

   So: nothing moves across the cut. Two identical marks, one on the curtain and
   one on the page, and the only thing that changes is which one you are looking
   at.

   .3s, down from .55s, for the same reason the film's tail was cut: a dissolve
   between two frames that already match is not a transition anybody watches, it
   is a delay in front of the page. The only thing it still has to do is cover
   the sub-pixel difference between a mark drawn into a canvas and the same mark
   as an <img>, and three tenths is generous for that. What actually changes
   across it now is the hero coming into view — and the hero is finished before
   the sheet starts to thin, because script.js composes it under the curtain
   rather than after it. See compose() there. */
.intro.gone{
  opacity:0;
  pointer-events:none;
}

/* ============ scroll progress ============ */
/* violet -> orchid -> gold had to cross from magenta straight to yellow, and
   the midpoint of that interpolation is a dull brown. --flame is very close to
   where that blend already wanted to go, so dropping it in as a real stop is
   less an addition than a correction — the sweep now reads as one continuous
   spectrum. Same fix wherever this ramp repeats (.rail, ship bar). */
.progress{
  position:fixed;top:0;left:0;right:0;height:2px;z-index:120;
  background:linear-gradient(90deg,var(--violet),var(--orchid),var(--flame),var(--gold));
  transform:scaleX(var(--p,0));transform-origin:0 50%;
  will-change:transform;pointer-events:none;
}

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

/* ============ nav ============ */
#nav{
  position:sticky;top:0;z-index:80;
  display:flex;align-items:center;gap:var(--s-3);
  padding:16px var(--pad);
  background:rgba(255,255,255,0);
  transition:background .5s var(--ease), backdrop-filter .5s var(--ease), border-color .5s var(--ease), padding .4s var(--ease);
  border-bottom:1px solid transparent;
}
#nav.stuck{
  background:rgba(255,255,255,.86);
  backdrop-filter:saturate(150%) blur(16px);
  -webkit-backdrop-filter:saturate(150%) blur(16px);
  border-bottom-color:var(--ink-08);
  padding-top:11px;padding-bottom:11px;
}
.logo{flex:0 0 auto;position:relative}
.logo img{height:19px;width:auto;transition:transform .5s var(--ease)}
.logo:hover img{transform:scale(1.04)}
/* The logo anchor shrink-wraps its image, so it is only as tall as the image:
   19px here, 17px on phones (the max-width:720px override below). Both are
   under the 24x24 minimum in WCAG 2.5.8 (AA), and this is the "go home" link,
   so it is worth hitting. Expanded with a pseudo-element rather than padding
   because .logo is a flex item in a row whose height the nav's padding is
   tuned against — padding would grow the nav; an absolutely positioned
   ::after is out of flow and cannot. Measured room at 360-390 wide, where the
   nav wraps to three rows: 28.6px of clear nav above the logo, and 22.6px
   below it before .nav-links starts. -7px vertical takes the target to 31px
   tall and still leaves 15.6px clear of the links row. -6px horizontal is
   comfort, not compliance (73px already passes); it reaches 6px into the
   nav's 16px side padding and the nearest neighbour, .nav-cta, is 145px away
   at the narrowest size. Do not add a z-index here: the ::after is
   transparent and must stay in the same stacking context as the image. */
.logo::after{content:'';position:absolute;inset:-7px -6px}

.nav-links{margin-left:auto;display:flex;gap:var(--s-4)}
.nav-links a{
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:700;letter-spacing:-.02em;
  color:var(--ink-64);position:relative;padding:4px 0;
  transition:color .35s var(--ease);
}
.nav-links a::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:2px;border-radius:2px;
  background:var(--violet);transform:scaleX(0);transform-origin:100% 50%;
  transition:transform .5s var(--ease);
}
.nav-links a:hover{color:var(--ink)}
.nav-links a:hover::after{transform:scaleX(1);transform-origin:0 50%}
.nav-links a[aria-current]{color:var(--ink)}
.nav-links a[aria-current]::after{transform:scaleX(1);transform-origin:0 50%}

.nav-cta{margin-left:var(--s-3);display:flex;align-items:center;gap:10px}
.navbtn{
  position:relative;overflow:hidden;flex:0 0 auto;
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:-.02em;
  padding:11px 20px;min-height:40px;
  display:inline-flex;align-items:center;justify-content:center;
  border:1.5px solid var(--ink);border-radius:999px;
  background:var(--ink);color:var(--paper);
  white-space:nowrap;
  transition:background .4s var(--ease), color .4s var(--ease), border-color .4s var(--ease);
}
.navbtn span{position:relative;z-index:1}
.navbtn::after{
  content:"";position:absolute;inset:0;z-index:0;
  background:var(--violet);
  transform:translateY(101%);border-radius:50% 50% 0 0;
  transition:transform .55s var(--ease), border-radius .55s var(--ease);
}
.navbtn:hover::after{transform:translateY(0);border-radius:0}
.navbtn:hover{border-color:var(--violet)}
.navbtn-short{display:none}

.bagbtn{
  position:relative;flex:0 0 auto;cursor:pointer;
  width:40px;height:40px;border-radius:999px;
  border:1.5px solid var(--ink-14);background:transparent;
  display:grid;place-items:center;
  transition:border-color .35s var(--ease), transform .35s var(--ease-back);
}
.bagbtn:hover{border-color:var(--ink)}
.bagbtn.pop{transform:scale(1.16)}
.bagbtn b{
  position:absolute;top:-5px;right:-5px;
  min-width:19px;height:19px;padding:0 5px;border-radius:999px;
  background:var(--ink);color:#fff;
  font-family:var(--font-display),sans-serif;font-size:11px;font-weight:800;
  display:grid;place-items:center;
  transform:scale(0);transition:transform .4s var(--ease-back);
}
.bagbtn.has b{transform:scale(1)}

/* ============ hero ============ */
.hero{
  position:relative;
  /* the nav occupies ~79px above this; subtracting it is what keeps the CTA
     inside the first screen instead of just below the fold. It was 114 while
     the announce bar sat on top of the nav — the bar was a measured 35.6px on
     every screen, and it is gone. */
  min-height:calc(var(--vh) - 79px);
  display:flex;align-items:center;justify-content:center;
  /* This used to be the scroll hint's lane, sized so the centred stage could not
     grow into the centred hint. The hint is in the right margin now (see
     .scroll-hint for why), so this is just breathing room under the buttons and
     it is sized for that: clamp(56,8vh,86) -> clamp(20,2.6vh,34) is 32px at 900
     tall handed straight to --gh. Do not put the hint back in the centre without
     putting this back too. */
  padding:clamp(20px,3.4vh,44px) var(--pad) clamp(20px,2.6vh,34px);
  overflow:hidden;
  /* A SEAMLESS, and nothing else.
     This was a blue stage with a warm bounce inside it, and the objection to it
     was the correct one: a tint behind white glass is a colour the product does
     not have, so it reads as a filter laid over the shot rather than as the room
     the shot was taken in. Tinted grounds are also the single most common tell
     of a cheap product page, because they are what you reach for when the
     photography is not carrying the frame on its own.
     What a studio actually gives you is one tone and a light — paper curving
     away from the key, deeper where it turns, clean at the feet. So: a single
     warm neutral with a luminance ramp on it and no second hue anywhere. The
     glass now separates from the ground by VALUE, which is what makes an object
     look lit, instead of by hue, which is what made it look tinted.
     Two layers. The ramp lands on pure white at the bottom edge so the buttons
     sit on paper and the products section below joins with no seam to hide; the
     ellipse over it is the paper turning away at the outside corners, which is
     the only thing stopping a vertical ramp from reading as a flat backdrop.
     It is full bleed and fades to transparent well inside its own box, so there
     is no rectangle edge anywhere in it. */
  background:
    radial-gradient(ellipse 80% 74% at 50% 44%, transparent 50%, rgba(190,180,164,.26) 100%),
    linear-gradient(180deg,#EAE5DD 0%,#F1EDE6 28%,#F8F6F2 62%,#FFF 100%);
}
.hero::before{
  content:"";position:absolute;z-index:0;
  left:50%;top:50%;transform:translate(-50%,-50%) rotate(var(--spin,0deg));
  width:min(1150px,124vw);height:min(820px,92vh);
  /* The key light, and that is the whole of it. One soft white pool centred a
     little above the feet, so the brightest part of the room is exactly where
     the glass is standing. Nothing here has a hue: this is the same paper as
     the band behind it with more light falling on it.

     Four layers used to live on this element — flame bounce, sky core, sky
     field, white base — and three of them were colour. They are gone rather
     than dimmed, because the problem was never the strength of the tint. Two
     hues on a ground behind a white product is a decision the product did not
     ask for, and at any alpha it is still visible as paint.

     The stops carry a long way out and reach transparent at 84%, well inside
     this element's box, so the fixed 1150x820 frame never shows an edge. The
     ellipse is deliberately not a circle, which is what gives --spin (set from
     the pointer in script.js) something to actually move. */
  background:
    radial-gradient(ellipse 47% 45% at 50% 46%, rgba(255,255,255,.97) 0, rgba(255,255,255,.78) 36%, rgba(255,255,255,.34) 62%, transparent 84%);
  pointer-events:none;
}
/* Grain, and it matters more here than it did. On a tinted ground the hue was
   doing the work of saying "this is a surface"; on a single neutral tone the
   tooth of the paper is the only thing left that does, and without it the ramp
   would band visibly across a large light area. .38 rather than .5 because it
   multiplies against a lighter ground now and the old value read as dirt. */
.hero::after{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;opacity:.38;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.34'/%3E%3C/svg%3E");
  mix-blend-mode:multiply;
}
/* The product is the subject: the headline splits around it, one line above and
   one below, and the whole column shares a single centre axis. */
.hero-stage{
  position:relative;z-index:2;
  width:100%;max-width:var(--maxw);
  display:flex;flex-direction:column;align-items:center;text-align:center;
}
/* ---- hero banner strip ----
   Sits in the band between the nav and the eyebrow, on every width. It is meant
   to be compact: it fills that band rather than becoming a feature of its own,
   so --hb-h is the smallest height the copy will sit in, not the largest the
   space allows. It was briefly run at 172px on a phone to "fill the gap" and
   that was the wrong lever — the gap is held open by .hero's padding now
   (see align-items:flex-start below), so height buys nothing back and only
   pushes the buttons toward the fold. Keep it small.

   72 here (the wide bar), 104 on a phone — down from 88 and 124, and with the
   ~16px dot row gone the two strips are 31% and 26% shorter than they were.
   None of that is handed back to anything: the height simply leaves the stage,
   which on desktop lands in the auto-margin gap under the banner and on a phone
   lands as fold clearance. Blank, as asked. */
.hero-banner{display:block;width:100%;margin-bottom:14px;--hb-h:72px}
.hb-track{
  position:relative;width:100%;height:var(--hb-h,132px);
  border-radius:20px;overflow:hidden;
  box-shadow:0 14px 30px rgba(26,23,32,.14);
}
.hb-slide{
  position:absolute;inset:0;
  display:grid;grid-template-columns:minmax(0,1fr) 40%;
  align-items:center;
  text-align:left;
  opacity:0;visibility:hidden;
  transition:opacity .6s var(--ease), visibility 0s linear .6s;
}
/* visibility is what actually takes the off slides out of hit-testing — opacity
   alone leaves three stacked links all catching the same tap */
.hb-slide.is-on{opacity:1;visibility:visible;transition:opacity .6s var(--ease), visibility 0s}
/* --hb-sc is the scrim laid over the tile. The tiles are only saturated in
   their lower third — the top half of every one of them is the same neutral
   grey, which met the pastel as a dead grey rectangle. The scrim carries the
   tone up through the whole media block so the two halves read as one card. */
.hb-slide[data-tone="violet"]{--hb-bg:linear-gradient(118deg,#F1E8FF 0%,#DCCBFF 100%);--hb-ac:#6D28D9;--hb-sc:rgba(109,40,217,.34)}
/* the warm slide's accent was #9A5B00, which is where "dark gold" lands once it
   is pushed far enough to be legible — a brown. --flame-dk gets to the same
   legibility (5.00:1 on the pastel and 5.46:1 under the white pill label,
   against 4.97 and 5.43 before) while actually reading as orange, so this is a
   free swap. It also matches the face serum's showcase panel, which is the same
   product wearing the same warm identity on the wide layout. */
.hb-slide[data-tone="gold"]  {--hb-bg:linear-gradient(118deg,#FFF4DC 0%,#FFE0A6 100%);--hb-ac:var(--flame-dk);--hb-sc:rgba(214,90,18,.34)}
/* The mask slide follows its tile. That tile is a charcoal stage now, not the
   magenta one this pastel was picked against, and a pink copy panel butted onto
   a near-black photo reads as two unrelated cards rather than one. Warm stone
   from the page's own cream, an ink accent, and a neutral scrim instead —
   which also stops the mask being the third loud hue on a phone screen that
   already carries a violet slide and a gold one.
   Legibility is unchanged or better: ink on #EFEBE4 is 15.0:1, and the white
   pill label on the ink accent is 17.6:1, against 4.9 and 4.6 for the magenta
   it replaces. */
.hb-slide[data-tone="orchid"]{--hb-bg:linear-gradient(118deg,#F6F3EE 0%,#E4DED4 100%);--hb-ac:var(--ink);--hb-sc:rgba(26,23,32,.34)}
.hb-slide{background:var(--hb-bg)}
.hb-copy{
  display:flex;flex-direction:column;gap:5px;
  padding:0 4px 0 16px;min-width:0;
}
.hb-kicker{
  font-family:var(--font-display),sans-serif;
  font-size:9.5px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;
  color:var(--hb-ac);
}
.hb-line{
  font-family:var(--font-display),sans-serif;
  font-size:17.5px;font-weight:900;line-height:1.06;letter-spacing:-.035em;
  color:var(--ink);
}
.hb-cta{
  display:inline-flex;align-items:center;gap:9px;margin-top:2px;
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;color:var(--ink);
}
.hb-cta i{
  font-style:normal;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;
  padding:5px 11px;border-radius:999px;
  background:var(--hb-ac);color:#fff;
}
/* the tile carries its own saturated backdrop, so it reads as a colour block
   butted against the pastel rather than as a cut-out floating on it */
.hb-media{position:relative;align-self:stretch;overflow:hidden}
.hb-media img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 42%}
.hb-media::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,var(--hb-sc) 0%,rgba(0,0,0,0) 62%);
}

/* ---- wide layout: the same card laid down into one row ----
   A phone gets a stacked card because it has width to spare and no height. A
   desktop has the opposite problem: the band under the nav is 62px, so the
   strip has to be a bar or it stops being a strip. Same DOM either way — the
   copy column turns into a row, the two-line headline is unwrapped by hiding
   its <br />, and the price/pill pair is pushed to the far end with margin.
   The media panel stays on the right but goes to a fixed 210px: at 40% of a
   1240px stage it was a 496px-wide crop of a tile whose subject is a vertical
   bottle, which is mostly empty backdrop. */
@media (min-width:721px){
  /* ---- where the slack sits ----
     The band under the nav was 37.3px on a 1440x900 laptop while the banner sat
     14px off the eyebrow below it — backwards, and the thing that got circled in
     a photo of the actual screen. Both numbers come from .hero: clamp(20,3.4vh,44)
     of top padding, plus align-items:center splitting the leftover height evenly
     above and below the stage. The even split is the same trap the phone layout
     already documents — spare height silently reopens the gap at the top, and it
     grows with the screen (6.8px at 900 tall, 28 at 1080, 52.7 at 768x1024), so
     no fixed padding can hold the top gap constant on its own.

     So the stage is stretched to the full content box and the leftover is handed
     to auto margins *inside* it instead: margin-top:auto on the eyebrow collects
     it below the banner, margin-bottom:auto on the foot keeps the rest of the
     hero optically centred rather than pinned to the top — which is what
     align-items:flex-start would have done, and it leaves ~270px dead under the
     buttons on a 1080 monitor. Top gap is now a flat 14px at every desktop size.
     If free space ever goes negative the autos resolve to 0 and this degrades to
     ordinary top packing, which is the right failure. */
  .hero{align-items:stretch;padding-top:14px}
  .hero-eyebrow{margin-top:auto}
  .hero-foot{margin-bottom:auto}
  /* 24, against 14 above. The banner is a separate object from the hero pitch
     and needs to read that way; at 14 top and 14 bottom it looked welded to the
     eyebrow. It was 38, and the ratio against the 14 above it is what does the
     separating, not the absolute number — so the 14px this gives back goes to
     the lineup, which is the thing the hero is actually read for. */
  .hero-banner{margin-bottom:24px}

  /* 600px, not the stage's 1240. Stretched to full width the bar became a
     billboard with a lane of dead pastel between the headline and the price,
     which is the opposite of compact — and it out-weighed the headline it sits
     above. Still narrower than "Twelve Weeks." renders, so it frames the hero
     instead of competing with it. Was 640; the headline dropping to 18px left
     enough slack to take the width down with the height. */
  .hero-banner{max-width:600px}
  /* 96, tracking --hb-h's 72. object-fit:contain renders a square tile at the
     panel's height, so the panel's width is really just how much pastel sits
     either side of it — 12px here, as it was at 116/88. Leave them in step. */
  .hb-slide{grid-template-columns:minmax(0,1fr) 96px}
  /* kicker over line on the left, price pinned right. Three flex siblings can't
     do that, so the copy block becomes a 2x2 grid and the CTA spans both rows. */
  .hb-copy{
    display:grid;grid-template-columns:minmax(0,1fr) auto;
    grid-template-areas:"kicker cta" "line cta";
    align-items:center;column-gap:18px;row-gap:3px;
    padding:0 18px 0 22px;
  }
  .hb-kicker{grid-area:kicker;align-self:end;font-size:9.5px}
  .hb-line{grid-area:line;align-self:start;font-size:18px}
  /* unwrapped here — a two-line headline plus a kicker will not sit in 72px */
  .hb-line br{display:none}
  .hb-cta{grid-area:cta;align-self:center;margin-top:0}
  /* contain, not cover. Cover on an 88px-tall panel takes a 42% horizontal band
     out of a 1200px square whose subject is a standing bottle, so it served a
     decapitated middle. Contain drops the whole tile in as an 88px square that
     reads as a colour block butted against the pastel — which is what the tile
     backdrop was built to do. The scrim goes with it: it existed to carry tone
     across a bleeding crop, and there is no crop left to carry. */
  .hb-media img{object-fit:contain;object-position:50% 50%}
  .hb-media::after{display:none}
}

/* .hb-dots / .hb-dot / @keyframes hbFill lived here. The pip row under the
   track was ~16px of vertical space spent on an indicator for three slides,
   and it was the single biggest contributor to the phone card reading as
   crowded. Removed, along with the progress sweep that rode on it; banners.js
   no longer builds the markup, so there is nothing left for these to style.
   The space it freed is left blank on purpose — see the --hb-h notes. */

.hero-eyebrow{
  font-family:var(--font-display),sans-serif;
  font-size:clamp(10.5px,1.05vw,12.5px);font-weight:800;
  letter-spacing:.2em;text-transform:uppercase;
  color:var(--ink);
  margin-bottom:clamp(5px,.6vw,8px);
  display:flex;align-items:center;justify-content:center;gap:10px;
}
.hero-eyebrow::before{content:"";width:26px;height:2px;border-radius:2px;background:var(--ink)}

.hero-h{
  /* the vh term is what keeps the two lines, the glass and the buttons inside
     one screen on a short laptop — vw alone overflows below ~800px tall.
     Both terms have now come down twice for the same reason: this line is the
     measure the product group is read against, and the group cannot grow to
     meet it because the hero's height is spoken for. At 8.8vh "Twelve Weeks."
     ran 502px against a 259px cluster — the products covered 52% of the words
     above them, which is what made them look dropped in rather than presented.
     At 7.4vh the headline is 422px and the group 374px, so the lineup sits at
     ~89% of the headline: clearly narrower, unmistakably the same object.
     The 2vh this frees is what pays for --gh going 32vh -> 34vh.

     Now 6.5vh, and the 89% rule is deliberately broken the other way: the group
     was asked to fill the page, so it is allowed to outgrow the words. At 900
     tall this line renders 58.5px against a lineup that is 324 tall and ~400
     wide, so the products are now the larger object and the headline reads as
     the caption around them — which is the composition the brief asks for.
     Both terms moved together (6.0vw -> 5.6vw) so the wide-and-short case does
     not quietly keep the old size and undo it. The 30px floor does not move:
     that is the 390px phone, where the vw term binds and the line is already as
     small as "Twelve Weeks." can be set and still hit. */
  font-size:clamp(30px, min(5.6vw, 6.5vh), 78px);
  line-height:.94;
  letter-spacing:-.045em;
  /* true black above the fold — the violet-black --ink reads soft against the
     cream wash, and the first screen is meant to hit hard */
  color:var(--black);
}
.hero-h2{color:var(--violet)}

/* The group sits in the gap between the two headline lines and must NOT overlap
   them: it paints above the text, so any overlap reads as a bottle slicing
   through a glyph rather than as depth.
   --gh is the whole group's height budget. Every overlap and offset below is
   derived from it, so the cluster holds its proportions as the viewport scales
   and only one number has to change on mobile. */
.bottle{
  /* --gh is the group's height budget and --gap the air between products; every
     other number in the group is derived from one of the two, so the whole
     lineup rescales from these and nothing has to be re-tuned by hand.
     34vh, not the old 32vh: the ceiling exists because the scroll hint is
     pinned to the bottom of the hero rather than sitting in the flow, so a
     taller group runs the buttons into it. The extra 2vh is paid for by the
     headline coming down (see .hero-h) — measured clearance at 1280x860 goes
     up from 24.8px to ~30px, so this is not spending slack that wasn't there.
     --gap replaces a negative margin. The three used to be pulled together by
     -.02gh and stacked, which is what made the two flanks read as tucked in
     behind the serum. They are a lineup on one surface, so they get real air —
     but air between them, not distance — this is a family shot, so they stand
     shoulder to shoulder.
     Two things make .02 safe where the old negative margin glitched. First, the
     box gap is not the visual gap: each PNG carries its own transparent margin,
     3.7% of file width on the hair serum against ~1% on the other two, so the
     glass-to-glass distance lands about 3.8px wider than the box gap at desktop
     — and, usefully, 3.76px on the left joint against 3.86px on the right, so a
     single uniform gap still reads even. Second, nothing here is negative, so
     the silhouettes cannot cross no matter how the flex line resolves; the old
     -.02gh could and did, which is what made the flanks look tucked behind the
     serum. Rendered at .08 / .035 / .02 / .012 and looked: .012 puts the jar's
     rim into the serum's glass, .02 is the tightest spacing where all three
     still own their edge. The soft white halos (.hero-prod::before, 158% wide)
     overlap freely at this distance and that is what knits the row together. */

  /* ---- what changed when the lineup was asked to fill the page ----
     --gh is the GLASS now, not the glass plus the caption under it. It used to
     be the whole column's height with .bottle-inner on flex:1, so the caption
     and its gap came off the products — about 29px on desktop, which is a tenth
     of the lineup spent on a line naming three products the banner directly
     above is already cycling by name and price. Moving the height onto
     .bottle-inner and letting .bottle size to content hands that back to the
     glass at no cost to the layout, and it makes every tier below mean what it
     says: 36vh of products, not 36vh minus a caption.
     It also fixes a trap in the phone rules. Those hide .bottle-tag and the old
     comment there explained that the glass grows to absorb it — true under
     flex:1, and it meant the phone and desktop numbers were not comparable.
     They are now.

     38vh / 560px, from 34vh / 330px. The cap is the bigger half of that: it
     bound from 971px tall, so every monitor taller than that had a lineup that
     had simply stopped growing while the dead air above and below it kept
     going. On a 1080 screen this is 410px of glass against 301 before.
     The ceiling still exists, it is just no longer the scroll hint: that has
     moved to the right margin, and what binds now is .hero-foot's
     margin-bottom:auto collapsing to 0 once --gh has taken all the free space,
     after which the buttons head for the fold. 38vh is set by measuring 961
     tall, which is the worst case on the whole page and not an obvious one:
     it is the first pixel this tier owns, so it runs the *loose* margins (38px
     under the banner, 11 on the eyebrow, 17 on the foot) on the *shortest*
     screen that ever sees them — about 21px more chrome than a 900-tall laptop
     carries, on 61px more screen. 41vh was set from arithmetic and measured
     6.2px off the fold there. Above 961 the chrome grows slower than the
     viewport so clearance only improves, until the 560px cap takes over around
     1474 tall. Anything re-tuned here must be checked at 961, not at 1080.

     --gap .055, from .02. The old value was the tightest spacing where all
     three still own their edge, which was right when the group was 330px wide
     on a 1240px stage and needed to read as one object. At this size the row
     can afford to span: .055 adds about 2x18px at 900 tall, so the lineup
     covers more of the stage without the three drifting apart into separate
     cutouts. The floor from the old tuning still holds — nothing here is
     negative and .012 is still where the jar's rim reaches the serum's glass. */

  /* ---- the lineup takes the free height instead of the margins ----
     Everything above is the history of guessing a vh per height tier. That is
     what this replaces, and the reason is not tidiness — it is that a flat vh
     is the wrong shape for the problem. The chrome around the lineup (banner,
     eyebrow, two headline lines, subline, buttons, padding) is almost entirely
     FIXED px, so free height does not scale with the viewport, it scales with
     the viewport MINUS a constant. A flat 38vh therefore overshoots on a short
     screen and undershoots badly on a tall one, and no single number fixes
     both — which is exactly why there were three tiers, and why they still
     disagreed with each other at the seams.

     Measured, on this page, at 1440 wide with the tier trims below in place:
         790 tall -> 276px of room     961 tall -> 439px
         831 tall -> 314px            1400 tall -> 813px
     which is a straight line: room(h) = 0.911h - 436. Slope 0.911 and not 1.0
     because two things in the chrome DO scale (the headline's 6.5vh and the
     hero's vh padding); the -436 is everything that does not.
     --gh is that line held slightly flatter — 89vh - 430 rather than 91.1 - 436
     — because a line fitted to the room runs out of clearance at the top of its
     range first, and a fit that is 3px generous at 790 and 3px mean at 1080
     leaves the same ~8px in hand at every height instead of 12 at one end and 5
     at the other. Re-measured after the change: 790/900/961/1080 clear the
     stage by 9, 11, 8 and 8px, and the hero never exceeds its min-height.

     What this is worth: the leftover used to be handed to margin-top:auto on
     the eyebrow and margin-bottom:auto on the foot, i.e. spent on blank bands
     above and below the hero. At 1400 tall that was 244px of dead air sitting
     directly above a lineup that had stopped growing at 532px. It now goes to
     the glass, and the three tier overrides below can drop their --gh entirely
     because one line covers the whole range.

     It also fixes a real break. At exactly 831 tall the max-height:960 tier
     took over with 38vh = 316px, when the room there is 314 BEFORE the trims —
     the buttons rendered 42px under the fold, and the 30vh tier a pixel lower
     existed to catch it, overshooting the other way. The line has no seams, so
     there is nothing left to fall through.

     592px, not 560. That is the ceiling the ARTWORK sets, not the layout: at
     592 the hair mask renders 622 device px wide on a 2x screen against 625 in
     the file, so every product is still a downsample and nothing is ever
     resampled up. Past it the jar would start inventing pixels. It binds from
     about 1144 tall.
     78vw is the width guard. The cluster measures ~1.19x --gh across, so at the
     721px tier edge an unguarded 592 would run 30px wider than the stage. */
  --gh:max(150px, min(calc(89vh - 430px), 592px, 78vw));
  --gap:calc(var(--gh) * .055);
  position:relative;z-index:3;
  /* this margin is the only thing keeping the glass off the two headline lines,
     so the floor is 10px rather than 6px — at 390px wide the old floor left a
     6px gap, close enough that a font swap could close it */
  margin:clamp(8px,.6vw,10px) 0;
  display:flex;flex-direction:column;align-items:center;gap:clamp(6px,.5vw,8px);
  opacity:0;
  animation:bottleIn 1.5s var(--ease) .1s forwards;
  animation-play-state:paused;
}
.ready .bottle{animation-play-state:running}
.bottle-inner{
  /* the glass owns --gh outright; .bottle is auto and just stacks this and the
     caption. flex:0 0 auto so the caption can never bargain height off it */
  position:relative;flex:0 0 auto;height:var(--gh);
  /* flex-end is the shared baseline — the one thing that makes three objects of
     different shapes read as a range standing on a surface rather than as three
     cutouts pasted at different depths */
  display:flex;align-items:flex-end;justify-content:center;gap:var(--gap);
  /* Scroll parallax only. The pointer rotation used to live here, which meant
     the three turned as one flat plane — the group tilted but no individual
     product moved against its neighbours, so the back one just looked stuck.
     Every pointer response is per-product now, on .hp-in. */
  transform:
    translate3d(0, calc(var(--scroll,0) * -130px), 0)
    scale(calc(1 - var(--scroll,0) * .08));
  will-change:transform;
}

/* ---- one product in the group ----
   --sw  a fixed inward turn, mirrored across the centre, that bends the row into
         a shallow arc instead of a flat wall of three cutouts
   --fd  float duration
   --fp  float phase, negative so the cycle starts already in progress
   Only --sw, --fd and --fp differ between the three, and all of them are
   symmetric or phase-only. Everything that carries visual weight — size against
   its own vessel, light, contact shadow, stacking, entrance timing, and how far
   and how hard it swerves against the pointer — is now identical by
   construction. The old ramp (--d .88/1/.92, --ryk 1.3/1/.72) scaled the flanks
   off the serum, which is exactly how you say "this one is the real product and
   these two are its supporting cast". */
.hero-prod{
  position:relative;flex:0 0 auto;height:100%;
  margin:0;
  /* one z-index for all three. With a real --gap there is nothing to resolve,
     and that is the point: no product is in front of another. */
  z-index:2;
  opacity:0;
  /* no per-product delay — the three arrive together. The stagger used to make
     the flanks "step out from behind the serum", which is a hierarchy told as
     choreography. Desynchronisation now lives only in the float loop, which has
     no first and no last. */
  animation:prodIn 1.15s var(--ease) .35s forwards;
  animation-play-state:paused;
}
.ready .hero-prod{animation-play-state:running}
/* Handed off from the film rather than arrived at cold. Both entrances above
   are written for someone who typed the URL and saw no curtain, where 1.5s of
   glass rising into place is the first thing that happens. After a film that
   has just spent two and a half seconds on these same three products, a second
   arrival is not an entrance — it is the same shot played twice, and the second
   time is slower. So on this path the lineup is simply already there, and the
   dissolve uncovers it. The float loop is untouched: the scene stays alive, it
   just stops introducing itself. See .intro.gone and compose() in script.js. */
.film-hand .bottle,
.film-hand .hero-prod{animation-name:none;opacity:1;transform:none}
/* the entrance owns .hero-prod's transform (fill-mode forwards pins it), so the
   pointer parallax has to live on its own element or the two fight */
.hp-in{
  display:block;height:100%;
  /* perspective() as a transform function applies to this element only, which
     is exactly what is wanted: each product is its own little 3D scene, so no
     preserve-3d chain is needed and z-index / filter keep working normally.
     The origin sits low so the neck swings further than the base — a bottle
     pivoting on the surface it stands on, not a card spinning about its middle. */
  /* --sw is added inside the rotateY so the pointer swerve happens about the
     product's own splayed rest angle rather than snapping it back to flat */
  transform:
    perspective(620px)
    translate3d(var(--px,0px), var(--py,0px), 0)
    rotateY(calc(var(--bry,0deg) + var(--sw,0deg)))
    rotateX(var(--brx,0deg));
  transform-origin:50% 74%;
  will-change:transform;
}
.hero-prod img{
  display:block;height:100%;width:auto;
  position:relative;z-index:2;
  filter:
    drop-shadow(0 40px 54px rgba(62,40,20,.26))
    drop-shadow(0 12px 20px rgba(62,40,20,.16))
    drop-shadow(0 0 90px rgba(255,255,255,.65));
  animation:float var(--fd,7s) ease-in-out infinite;
  /* negative, so each product is already mid-cycle the moment it appears
     instead of waiting its turn — desync without a running order */
  animation-delay:var(--fp,0s);
}
/* A halo each. Sized in percentages of the product's own box, not off --gh: a
   fixed .66gh x .82gh box was a generous glow around a 76px-wide bottle and a
   tight collar on the 135px-wide jar, so the "equal light" it was meant to give
   was not equal at all. Percentages make the light scale with whatever it is
   lighting, which is the only version that holds when the three have different
   silhouettes. */
.hero-prod::before{
  content:"";position:absolute;z-index:0;
  left:50%;top:50%;
  width:158%;height:118%;
  transform:translate(-50%,-50%);
  background:radial-gradient(ellipse at center, rgba(255,255,255,.88) 0, rgba(255,255,255,.5) 42%, transparent 74%);
  pointer-events:none;
}
/* a contact shadow each — the one thing that sells three separate objects
   standing on the same surface rather than a row of floating cutouts.
   Width tracks the product (a wider object has a wider footprint) but the
   thickness is a constant off --gh: height:5% of the element gave the jar a 6px
   smear and the bottles a 15px one, which reads as two different floor heights. */
.hero-prod::after{
  content:"";position:absolute;z-index:1;
  left:50%;bottom:calc(var(--gh) * -.012);
  width:84%;height:calc(var(--gh) * .05);
  transform:translateX(-50%);
  background:radial-gradient(ellipse at center, rgba(76,50,26,.46) 0, rgba(76,50,26,.22) 40%, transparent 76%);
  filter:blur(12px);
  animation:contact var(--fd,7s) ease-in-out infinite;
  animation-delay:var(--fp,0s);
}

/* Heights are derived from volume, not set by eye. This is what kills the
   "morphed" look, and it is a layout bug rather than an artwork one: the face
   serum is a genuinely slimmer bottle than the hair serum, so forcing the two
   to the same height rendered its glass at 83% of the hair serum's diameter and
   it read as the same bottle squeezed narrow. Measured glass (alpha>200):
       face  354 wide x 1061 tall  ->  h/d = 3.00
       hair  456 wide x  955 tall  ->  h/d = 2.09
   Both hold 30 ml. For a cylinder V is proportional to k*d^3 with k = h/d, so
   equal volume forces d_face = d_hair * (2.09/3.00)^(1/3) = 0.887, and since
   the file heights differ that lands the face serum at 1.072x the hair serum's
   render height. Expressed against the container with the tallest at 100%:
       face  100%      slim barrel, so it stands tallest — correct for 30 ml
       hair   93.3%    = 100 / 1.072
       mask   52.2%    = 93.3% * .56, i.e. still .56 of the hair serum
   Rendered opaque mass then comes out 1.00 / 1.03 / 1.22. The two bottles are
   within 3% of each other, and the jar keeps the deliberate ~20% surplus so a
   200 ml jar does not read as smaller than a 30 ml bottle. The .56 itself is
   the earlier judgement call: equal mass wanted 50.7%, equal bounding-box area
   54.3%, physical truth for 200 ml beside 30 ml about 64%.
   Everything else is symmetric. --sw mirrors +/-4deg so the outer two turn
   inward and the row bends into a shallow arc; --fd and --fp are phase only, so
   the three never bob in lockstep without any of them leading. */
.hp-face { --sw: 4deg; --fd:7.8s; --fp:-2.6s; height:100%}
.hp-serum{ --sw: 0deg; --fd:7s;   --fp:0s;    height:93.3%}
.hp-mask { --sw:-4deg; --fd:6.4s; --fp:-1.3s; height:52.2%}
/* .bottle-tag lived here — the "Hair Serum · Face Serum · Hair Mask · From
   ₹699" line under the group. It is gone from the markup, not hidden: the three
   product names are printed on the three bottles, and the whole point of the
   --gh line above is that they are now large enough to read, so the caption was
   restating its own subject. That is also why removing it is not a loss of
   information but a transfer of it — 19px of type plus a 7px gap went to the
   glass, which is the only thing here that gets clearer for being bigger.
   The price is on .hero-foot's line now. .bottle is left as a one-child column
   so the group can still grow a caption back without re-plumbing anything. */
/* soft elliptical stage behind the group — gives the three something to stand
   on and lifts them off the cream wash. It spans the cluster now rather than a
   single bottle, so the old 230% would have run off the stage entirely.
   Turned down hard once each product got its own halo: at the old alphas the
   two stacked to near-white in the middle, which put the light back on the
   centre bottle and undid the point of the per-product halos. */
.plinth{
  position:absolute;z-index:0;
  left:50%;top:50%;width:126%;height:88%;
  transform:translate(-50%,-50%);
  background:radial-gradient(ellipse at center, rgba(255,255,255,.6) 0, rgba(255,255,255,.42) 34%, rgba(255,252,246,.2) 56%, transparent 74%);
  pointer-events:none;
}

/* No scroll parallax here, and the removal is the fix for a real collision.
   This used to carry translate3d(0, --scroll * -46px), matching the product
   group's -130px and the hint's -30px. The difference is what sits immediately
   above each of them: the group and the hint have a screen's worth of air to
   drift through, while .hero-foot is 10px under the "Real Change." headline and
   .hero-h has no transform at all. So the whole 46px was spent shearing the foot
   into a fixed headline, and with only 10px of clearance it began overlapping at
   about a third of the way down the hero.
   Measured at 1280x800: by scrollY 480 the .tag was 22px into the headline's
   line box and rendering straight through the glyphs — "Copper peptides,
   Redensyl and PDRN..." printed across "Real Change." in the middle of the
   viewport, not off the edge of it. On phones the .tag is display:none, so the
   same 46px landed on the buttons instead and drove the gap to -20px.
   There is no version of this worth keeping. A headline, its subtitle and its
   CTAs are one lockup; they have no depth relationship to express, and any
   nonzero rise just moves the collision to a different scroll offset rather
   than removing it — 10px of rest clearance means even a 12px drift touches.
   The parallax the hero is actually read for is .bottle-inner's, which is
   untouched and has the room to do it. */
.hero-foot{
  position:relative;z-index:2;
  margin-top:clamp(8px,.9vw,12px);
  display:flex;flex-direction:column;align-items:center;gap:clamp(11px,1.3vw,15px);
}
.tag{
  max-width:64ch;text-wrap:balance;
  font-size:clamp(14px,1.1vw,16px);line-height:1.6;
  color:var(--black-46);
}
.hero-btns{display:flex;flex-wrap:wrap;justify-content:center;gap:12px}
/* ---- .hero-assure — two facts under the CTAs --------------------------
   Same reason as .buyassure on shop.html: the announce bar carried cash on
   delivery and the free-shipping threshold on every page and it is gone, so
   the home page stated neither. Two, not three — the seven-day replacement is
   already in the FAQ further down this page, and a third clause is what turns
   a quiet line into a strip.

   NO MARGIN HERE ON PURPOSE. .hero-foot is a flex column with its own gap,
   so this inherits the same clamp(11px,1.3vw,15px) that separates the tag
   from the buttons. Setting a margin as well would double the gap on one side
   only and break the lockup's rhythm.

   THE FOLD. This costs the hero about 27px on a phone, where .tag is hidden
   and the foot is buttons plus this line. Measured after: 375x667 keeps ~25px
   of clearance under it and 390x844 keeps ~75px, so the CTAs and this line are
   both above the fold on the short phone the budget is tuned for. If anything
   else joins .hero-foot, re-measure at 375x667 first — that is the size that
   binds, not 390x844.

   --ink-64 rather than --black-46: this is a claim, not a caption, and 46%
   sets it lighter than the .tag above it, which would read as a footnote to
   a footnote. 12.5px is the site's small-print size (see .retail-note) and
   --ink-64 measures 5.29:1, so it clears AA there. */
.hero-assure{
  font-size:12.5px;line-height:1.5;letter-spacing:.005em;
  color:var(--ink-64);text-wrap:balance;
}
/* One button, four grounds, five tokens. The variants used to each restate
   background/color/border by hand, which is how .cta ended up with no border
   while .cta-ghost carried 1.6px — the two hero buttons sat 3.2px different in
   height. Every variant now sets the same five tokens and nothing else, so the
   box model is identical by construction and only the colours move. */
.cta{
  --cta-bg:var(--ink);          /* resting fill */
  --cta-fg:var(--paper);        /* resting label */
  --cta-bd:var(--ink);          /* border, always present, so all variants match */
  --cta-fill:var(--paper);      /* the sweep that rises on hover */
  --cta-fg-on:var(--ink);       /* label once the sweep has covered the button */
  --cta-shadow:0 14px 30px rgba(26,23,32,.26);
  --cta-shadow-on:0 18px 40px rgba(26,23,32,.34);

  position:relative;overflow:hidden;
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;
  padding:15px 30px;border-radius:999px;
  background:var(--cta-bg);color:var(--cta-fg);
  border:1.6px solid var(--cta-bd);
  display:inline-flex;align-items:center;
  box-shadow:var(--cta-shadow);
  transition:box-shadow .45s var(--ease), color .35s var(--ease);
  opacity:0;animation:fade .8s var(--ease-soft) 1.5s forwards;
}
.cta-ghost{
  --cta-bg:transparent;--cta-fg:var(--black);--cta-bd:rgba(10,10,10,.16);
  --cta-fill:var(--black);--cta-fg-on:#fff;
  --cta-shadow:none;--cta-shadow-on:0 14px 30px rgba(10,10,10,.22);
  animation-delay:1.65s;
}
.cta-ghost:hover{border-color:var(--black)}
/* On the ink bands the solid primary inverts rather than picking up a hue —
   paper fill on ink is the same idea as ink fill on cream, and it is the only
   pairing that keeps the primary the highest-contrast thing in the band. */
.science .cta:not(.cta-ghost):not(.cta-light),
.scan .cta:not(.cta-ghost):not(.cta-light){
  --cta-bg:var(--paper);--cta-fg:var(--ink);--cta-bd:var(--paper);
  --cta-fill:var(--ink);--cta-fg-on:var(--paper);
  --cta-shadow:0 14px 30px rgba(0,0,0,.34);--cta-shadow-on:0 18px 40px rgba(0,0,0,.42);
}
.cta span{position:relative;z-index:1}
.cta::after{
  content:"";position:absolute;inset:0;background:var(--cta-fill);
  transform:translateY(101%);border-radius:50% 50% 0 0;
  transition:transform .55s var(--ease), border-radius .55s var(--ease);
}
.cta:hover::after{transform:translateY(0);border-radius:0}
.cta:hover{color:var(--cta-fg-on);box-shadow:var(--cta-shadow-on)}

/* pinned to the bottom of the hero — .hero is a centring flex row now, so left
   in flow this became a third column and parked itself beside the bottle */
/* Out of the centre and into the right margin, which is the single change that
   let the lineup grow.
   Centred, this was in the buttons' own column, and that made the hint and the
   product group fight over the same pixels in a way no amount of padding could
   settle. The hint is positioned off .hero's bottom edge while .hero-foot's
   margin-bottom:auto collapses to 0 the moment --gh takes the free space, so
   the buttons end up flush on the bottom padding — and then trimming that
   padding moves the hero's bottom edge, which moves the hint by exactly as
   much. Both slide up together and the 13px overlap between them never closes.
   Measured at 1440x900: buttons ended at 846.3, hint began at 833.1. To open
   that gap by shrinking the group would have cost ~54px of --gh — taking the
   lineup below where it started, to protect a decorative cue.
   In the right margin it shares a column with nothing. .hero-stage is capped at
   1240 so there is a 100px gutter at 1440 wide, the lineup is ~385 and centred,
   and .tag is capped at 64ch — none of them reach here. That frees the bottom
   padding to be sized for the buttons alone, which is where ~32px of the
   group's new height came from.
   Phones never saw any of this: the hint is display:none from 720px down. */
.scroll-hint{
  position:absolute;z-index:2;
  right:var(--pad);bottom:clamp(10px,2vh,24px);
  text-align:center;
  opacity:0;animation:fade 1s var(--ease-soft) 1.9s forwards;
  transform:translate3d(0,calc(var(--scroll,0) * -30px),0);
}
.scroll-hint .line{
  width:1.5px;height:22px;margin:0 auto 7px;border-radius:2px;
  background:linear-gradient(180deg,var(--ink-30),transparent);
  animation:drip 2.4s ease-in-out infinite;
}
.scroll-hint span{
  font-family:var(--font-display),sans-serif;
  font-size:10px;font-weight:700;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-30);
}

/* The scrolling marquee that used to sit here — a band of "Advanced Peptide
   Technology · PDRN Regenerative Science · Clinically Dosed Actives" drifting
   between the hero and the range — is gone, along with its rAF loop in
   script.js §5 and its markup on index.html.
   It was aria-hidden, so it said nothing to a screen reader; it repeated claims
   the headline and the product cards already make; and it was 50px of moving
   furniture standing between "Shop The Range" and the first product on the one
   device where that distance is most expensive. Motion draws the eye, and here
   it drew the eye at something unclickable. */

/* ============ section furniture ============ */
.eyebrow{
  font-family:var(--font-display),sans-serif;
  font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;
  color:var(--violet);
  margin-bottom:var(--s-2);
}
.sec-head{max-width:var(--maxw);margin:0 auto var(--s-5);padding:0 var(--pad)}
.sec-head h2,.science h2,.results h2{
  font-size:clamp(36px,6.4vw,84px);line-height:.94;
  color:var(--ink);
}
.sec-sub{
  margin-top:var(--s-3);max-width:52ch;
  font-size:16px;line-height:1.7;color:var(--ink-64);
}

/* ============ tabs ============ */
.tabs{
  position:relative;display:inline-flex;gap:4px;margin-top:var(--s-4);
  padding:5px;border-radius:999px;background:var(--cream);border:1px solid var(--ink-08);
}
.tab{
  position:relative;z-index:1;cursor:pointer;border:0;background:transparent;
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;
  padding:9px 22px;border-radius:999px;color:var(--ink-64);
  transition:color .4s var(--ease);
}
.tab.is-on{color:var(--paper)}
.tab-pill{
  position:absolute;z-index:0;top:5px;left:5px;height:calc(100% - 10px);
  border-radius:999px;background:var(--violet);
  transform:translateX(var(--x,0));width:var(--w,90px);
  transition:transform .55s var(--ease), width .55s var(--ease);
}

/* The "Or shop by concern" chip row that used to sit here is gone, with its
   handlers in script.js §9 and the data-concern attributes it filtered on.

   It was built on a sound instinct — people arrive with a problem, not a
   category — but with three SKUs it was solving a problem this catalogue does
   not have. Four chips, plus the three tabs above them, made seven controls
   guarding a grid of three products, every one of which was already visible by
   scrolling. Filters earn their keep when the list is long enough that reading
   it costs more than narrowing it; at three, narrowing IS the expensive part.
   On a phone the row also cost about 90px directly above the first card, on the
   screen with the least of it to give.

   If the range grows past roughly a dozen SKUs this is worth rebuilding from
   scratch — there is no repository here, so nothing of it survives but this
   note. The tabs stay: two categories over three products is one glance rather
   than a decision. */

/* ============ product grid ============ */
/* Top padding is --s-6 - 40. The section's own rhythm is right for the run of
   sections below it, where each one is a fresh subject and the space is what
   separates them. This one sits directly under the hero CTA, so the space is
   not separating two subjects — it is standing between "Shop The Range" and
   the range. Only the top is cut; the bottom keeps --s-6 because below the grid
   the argument reverses and it is dividing sections again. */
.products{padding:56px 0 var(--s-6)}
/* The header block over the grid, tightened on the same grounds and scoped to
   .products so Results and Science keep the wider setting — there the header
   introduces a long read and the air is doing real work.
   Desktop is cut about half as hard as the phone (see the @720 block): the run
   here was 481px against 900 of screen, which is worth closing, but a 1440px
   layout can afford rhythm that a 390px one cannot, and stripping it to phone
   numbers would read as cramped rather than direct. */
.products .sec-head{margin-bottom:32px}
.products .tabs{margin-top:24px}
/* Shop is a sub-page, so its listing is the first section on it and picks up
   .page-sub main > section:first-child — a (0,2,2) selector that outranks the
   .products padding above and put 96/72px over the header. That rule is right
   for the pages it was written for: Science and Results open with a heading
   that has a sticky nav directly above it and needs the air to read as a page
   rather than a continuation. A product grid does not — the customer arrived
   already wanting the list.
   Note this is decoration, not clearance: #nav is sticky, so it holds its own
   space in flow and nothing tucks under it when this shrinks. Scoped with
   .products at (0,3,2) so Science, Results and the quiz keep the original. */
.page-sub main > section.products:first-child{padding-top:40px}
.grid{
  /* The column gutter is named because the lone-card rule further down has to
     do arithmetic with it: a card that spans the whole grid needs to know the
     gutter to work back to what one column was worth. Kept as one value for
     both axes here, and split at 720 where the rows need more air than the
     columns do. */
  --col-gap:clamp(18px,2.2vw,30px);
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:repeat(3,1fr);gap:var(--col-gap);
}
.card{
  display:flex;flex-direction:column;
  background:var(--paper);
  border:1px solid var(--ink-08);
  border-radius:22px;
  overflow:hidden;
  transform:perspective(900px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateY(var(--ty,0));
  transition:box-shadow .6s var(--ease), border-color .6s var(--ease), transform .5s var(--ease);
  will-change:transform;
}
.card:hover{
  border-color:rgba(109,40,217,.34);
  box-shadow:0 30px 60px rgba(26,23,32,.16);
}
.card.hide{display:none}

/* ---- the card left alone in the last row ---------------------------------
   Three products in a two-up grid leaves the third one hard against the left
   edge with 185px of nothing beside it, which reads as a layout fault rather
   than the end of a list. It is centred below, and sized up while it is there,
   so the row looks intended instead of unfinished.

   The counting is the whole trick, and the obvious version of it is wrong.
   `:last-child:nth-child(odd)` looks right and breaks the moment a tab is
   used, because the tabs hide cards with display:none and :nth-child still
   counts hidden elements. Filtering to Hair leaves a full row of two and the
   naive rule would centre the second one anyway; filtering to Skin leaves one
   card genuinely alone and the naive rule would not touch it. `of .card:not(
   .hide)` counts only what is on screen, so the rule tracks the filter instead
   of the markup. Verified against all four states — All, Hair, Skin, and the
   empty set.

   Two rules rather than one because "alone in the last row" is a different
   sum per column count: at three columns it means the visible index is 3n+1,
   at two it means the index is odd. Same selector shape, different modulus.

   If a browser does not understand `of S` the whole rule is dropped and the
   card sits where it does today — the degradation is the current design, not a
   broken one, so no fallback is needed. */
.grid > .card:nth-child(3n+1 of .card:not(.hide)):nth-last-child(1 of .card:not(.hide)){
  /* Three columns: no span and no arithmetic needed. Dropping it into column 2
     centres it exactly, at exactly a column's width, whatever the container is
     doing. This state only appears under a filter — the default three products
     fill the row — so it is deliberately not sized up the way the two-up case
     is: a card that grew when you clicked a tab would read as a glitch. */
  grid-column:2;
}
.card.flip-in{animation:flipIn .55s var(--ease) both}

/* the tiles are pre-composited (gradient + product + shadow), so the media box
   just needs to hold them full-bleed rather than centre a transparent cutout */
.card-media{
  position:relative;aspect-ratio:1/1;overflow:hidden;
  background:var(--cream);
}
.card-media::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,transparent 55%,rgba(26,23,32,.24));
  opacity:0;transition:opacity .6s var(--ease);
}
.card:hover .card-media::after{opacity:1}
.card-media img{
  position:absolute;inset:0;
  width:100%;height:100%;object-fit:cover;
  transition:transform .9s var(--ease);
  transform:scale(1.005);
}
.card:hover .card-media img{transform:scale(1.07)}

/* These sit behind the tile image and are only ever seen while it loads. The
   first two are the brand token their stage was built from, which reads close
   enough because each of those tiles is one hue all the way through. The mask's
   cannot work that way any more: its stage now runs warm cream at the top to
   burnt orange at the floor, so every single token flashes hard against it.
   tone-ember is the measured average of the finished tile instead, and it has to
   be re-measured if tools/retone-mask-tile.py is re-run with a different ramp —
   the script prints it. The class read tone-orchid while the stage was charcoal;
   --orchid is still the mask's accent hue wherever it is type or a line. */
.tone-violet{background:var(--violet)}
.tone-gold{background:var(--gold)}
.tone-ember{background:#C29774}

.badge{
  position:absolute;top:14px;left:14px;z-index:2;
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;letter-spacing:.04em;
  padding:6px 11px;border-radius:999px;
  background:var(--ink);color:var(--paper);
  /* The z-index above puts this over the card-wide link overlay (see the note
     at .card-link::after), which turned the badge into a small patch of image
     that did not open the product. It is a label, not a control, so it stops
     intercepting taps and keeps its place in the paint order. Worth most on the
     phone grid, where the card is 173px wide and the badge is a real share of
     the only thing on it big enough to aim at. */
  pointer-events:none;
}
/* gold-on-ink reads as a highlight against the gold tile; a warm accent fought it */
.badge-new{background:var(--gold-2);color:var(--ink)}

.quick{
  position:absolute;left:50%;bottom:14px;z-index:2;cursor:pointer;
  transform:translate(-50%,140%);
  font-family:var(--font-display),sans-serif;
  font-size:12.5px;font-weight:800;letter-spacing:-.01em;
  padding:11px 22px;border-radius:999px;border:0;
  background:rgba(255,255,255,.92);backdrop-filter:blur(8px);color:var(--ink);
  box-shadow:0 10px 24px rgba(80,55,32,.18);
  transition:transform .5s var(--ease), background .3s;
  white-space:nowrap;
}
.card:hover .quick{transform:translate(-50%,0)}
.quick:hover{background:var(--violet);color:#fff}

.card-body{padding:clamp(18px,1.9vw,26px);display:flex;flex-direction:column;flex:1}

.rating{display:flex;align-items:center;gap:7px;margin-bottom:10px}
.stars{position:relative;display:inline-block;font-size:13px;line-height:1;color:var(--ink-14)}
.stars::before{content:"\2605\2605\2605\2605\2605";letter-spacing:1.5px}
.stars i{
  position:absolute;left:0;top:0;overflow:hidden;white-space:nowrap;
  width:var(--r,100%);color:var(--gold);
}
.stars i::before{content:"\2605\2605\2605\2605\2605";letter-spacing:1.5px}
.rating b{font-family:var(--font-display),sans-serif;font-size:13px;font-weight:800}
.rating .rc{font-size:12.5px;color:var(--ink-46)}

.card h3{
  font-size:clamp(18px,1.55vw,22px);font-weight:900;line-height:1.12;
  margin-bottom:10px;
}

.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:14px;flex:1;align-content:flex-start}
.chips li{
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:700;
  padding:5px 10px;border-radius:999px;
  background:var(--cream);color:var(--ink-64);
}

/* single 30ml variant per formula — the volume is a spec, not a chooser */
.chip-vol{
  background:var(--violet-lt) !important;
  color:var(--violet) !important;
  font-weight:800 !important;
}

.card-foot{
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-2);
  margin-top:var(--s-3);padding-top:16px;border-top:1px solid var(--ink-08);
}
.pricing{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.price{font-family:var(--font-display),sans-serif;font-size:20px;font-weight:900;letter-spacing:-.03em}
.mrp{font-size:13px;color:var(--ink-46);text-decoration:line-through}
.off{
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;color:var(--ink);
}
/* ---- the button, and the one rule that governs its colour ----------------
   Same five-token shape as .cta, for the same reason: variants set colours and
   nothing else, so the box model cannot drift between them.

   THE RULE: a button is a shape before it is a label, and a shape has to clear
   3:1 against the surface it is lying on (WCAG 1.4.11). Text contrast INSIDE
   the button does not buy you this and is the trap the old styling fell into —
   ink on --gold-2 is a comfortable 11.55:1, which is why nothing looked wrong
   in isolation, while the button itself sat at 1.42:1 against cream and 1.00:1
   against the bundle band. One-to-one. On the offer band the primary call to
   action was the same colour as the thing behind it.

   Violet replaces it because it is the only hue already in this palette that
   clears the floor on every ground the button lands on, and because it is used
   nowhere else at this weight — which is the part that actually moves the
   number. The isolation effect is what makes a call to action findable: it
   wins by being the only element wearing the colour, not by being any
   particular colour. So violet is now spent on exactly one thing, spending
   money, and gold keeps the flags and badges. Two jobs, two colours, no
   overlap. */
.btn{
  --btn-bg:transparent;        /* resting fill */
  --btn-fg:var(--ink);         /* resting label */
  --btn-bd:var(--ink);         /* border, always present so variants match */
  --btn-fill:var(--ink);       /* the sweep that rises on hover */
  --btn-fg-on:var(--paper);    /* label once the sweep has covered the button */

  cursor:pointer;position:relative;z-index:0;overflow:hidden;
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:-.02em;
  padding:11px 18px;border-radius:999px;
  border:1.5px solid var(--btn-bd);background:var(--btn-bg);color:var(--btn-fg);
  white-space:nowrap;
  transition:color .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease);
}
/* label text is a bare text node, so the fill has to sit behind it */
.btn::after{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--btn-fill);
  transform:translateY(101%);border-radius:50% 50% 0 0;
  transition:transform .5s var(--ease), border-radius .5s var(--ease);
}
.btn:hover::after{transform:translateY(0);border-radius:0}
.btn:hover{color:var(--btn-fg-on)}
.btn:focus-visible{outline:2px solid var(--violet);outline-offset:3px}
/* The .added confirmation state lives further down, immediately after the
   ground-specific variants. It has to: the variants are (0,2,0) selectors like
   .bundle .btn-solid, .added is (0,2,0) too, and at equal specificity the
   later rule wins — declared up here it would be overwritten by the very
   variant it is supposed to replace. */

/* ---- the closer under the grid: quiz first, science second ----
   On both index and shop. It is deliberately NOT a band: a full-width coloured
   strip here would read as a fourth product and compete with the three cards it
   is supposed to be closing.

   This was a small grey "Still not convinced?" over a ghost link to the science
   page. The line answered an objection the reader had not raised, and someone
   who has scrolled three cards without adding one is rarely short of
   reassurance — they are short of a decision, and there are three products to
   decide between. The quiz makes that decision, so it takes the primary slot
   and the science page steps back to the secondary it always was.

   .cta is built for the hero, where it fades in on a load-timed delay. Down
   here that delay is meaningless — the section is nowhere near the fold — so
   the animation is dropped and .reveal does the entrance instead. Without this
   the button sits at opacity 0 until 1.5s after load. */

/* ---- .buyassure — the three promises, under the grid on shop.html --------
   WHY IT EXISTS AT ALL. The announce bar carried cash on delivery, free
   shipping over ₹999 and the seven-day replacement across the top of every
   page, and it was removed. That left shop.html — the one page whose entire
   job is the purchase decision — stating none of the three anywhere on it.
   This is not the bar returning in a new costume: it is stated once, at the
   point of decision, instead of scrolling past on every page including the
   ones where nobody is buying anything.

   ONLY THESE THREE, AND ONLY EVER THESE THREE — the same rule .pdp-promise
   is held to. These are the promises somebody has actually agreed to keep.
   No money-back guarantee: that one is specifically not offered. No rating,
   no review count, no dispatch time. A fourth item goes in only when the desk
   that has to honour it says so.

   ICONS ARE INK, NOT FLAME. Copied from .pp-ico deliberately — three orange
   glyphs in the middle of a cream page would be the largest block of accent
   colour on the site, and the accent is meant to be a small tint.

   A ROW, NOT A THREE-ACROSS STRIP. .pdp-promise is the strip shape and it is
   right in a 334px rail, where three cells of equal width read as peers. Out
   here the container is 1240px and the same construction gives three cells
   410px wide with a 19px glyph adrift in each — the hollow-cell failure the
   .pdp-promise note warns about, at triple the width. Inline, icon beside
   label, centred under the grid: the row is as wide as its words. */
.buyassure{
  list-style:none;
  max-width:var(--maxw);margin:var(--s-4) auto 0;padding:0 var(--pad);
  display:flex;align-items:center;justify-content:center;
  flex-wrap:wrap;gap:12px clamp(20px,3vw,46px);
}
.buyassure li{
  display:inline-flex;align-items:center;gap:9px;
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(13px,1.05vw,15px);letter-spacing:-.015em;
  line-height:1.25;color:var(--ink);
}
/* Sized off the label rather than set in px so the glyph tracks the type when
   the clamp above moves. .pp-ico's disc is dropped: a 44px filled circle per
   item works in a vertical strip and reads as three buttons in a horizontal
   row, which is the one thing this must not look like. */
.buyassure svg{
  width:1.28em;height:1.28em;flex:0 0 auto;display:block;color:var(--ink);
}
/* Phone: two per row at best, so let them stack cleanly and pull the gap in.
   Left-aligned, not centred — centred wrapped rows leave a ragged last line
   under a grid whose cards are flush to both margins. */
@media (max-width:560px){
  .buyassure{justify-content:flex-start;gap:11px 22px;margin-top:var(--s-3)}
  .buyassure li{font-size:13px}
}

.convince{
  max-width:var(--maxw);margin:var(--s-4) auto 0;padding:0 var(--pad);
  display:flex;align-items:center;justify-content:center;
  gap:clamp(12px,1.6vw,20px);flex-wrap:wrap;
}
.convince .cta{animation:none;opacity:1}
/* The label is a full instruction, not a two-word verb, so it needs more room
   than the hero's buttons — and it has to out-weigh the ghost next to it
   without a second colour, since violet on this site is reserved for spending
   money and this button does not spend any. Size is the lever left. */
.convince .cta-lead{font-size:15.5px;padding:18px 34px}

/* Phone: stack it. Side by side the two labels each get about half of a 358px
   stage, and the lead one is a whole sentence — it wrapped to three lines at
   360 wide. Stacked, both are full width like the hero's. */
@media (max-width:520px){
  .convince{flex-direction:column;gap:12px}
  .convince .cta{width:100%;justify-content:center;text-align:center}
}

/* the dot that flies to the bag */
.fly{
  position:fixed;z-index:140;width:12px;height:12px;border-radius:50%;
  background:var(--violet);pointer-events:none;
  box-shadow:0 4px 14px rgba(109,40,217,.55);
}

/* ============ ingredient showcase ============ */
/* Markup is injected by showcase.js, so everything here hangs off .sc-*.
   --tone is set per product from the [data-tone] attribute below. */
.showcase{
  padding:var(--s-6) 0 var(--s-7);
  background:linear-gradient(180deg,var(--paper) 0,var(--cream) 34%,var(--cream) 100%);
}
.sc-inner{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}

.sc-stage{
  --tone:var(--violet);
  --tone-tx:var(--tone);
  --tone-lt:var(--violet-lt);
  position:relative;
  display:grid;grid-template-columns:minmax(0,.86fr) minmax(0,1.14fr);
  gap:clamp(20px,3.4vw,56px);align-items:center;
  padding:clamp(18px,2.4vw,32px);
  border-radius:32px;
  background:var(--paper);
  border:1px solid var(--ink-08);
  box-shadow:0 30px 70px rgba(26,23,32,.07);
  transition:box-shadow .6s var(--ease);
}
/* --tone drives both fills and type, and those two jobs want different
   luminance, so the warm panel splits them. Its fill goes to --flame (the
   2px playhead bar, the button wash) where saturation is the point, and its
   type goes to --flame-dk where legibility is. This also repairs a real bug
   rather than just recolouring: the warm panel's kicker and lit ingredient
   name were --gold on #FFF4DC, which is 1.99:1 — effectively unreadable. They
   are now 5.00:1. Violet and orchid already clear the floor as text (6.50 and
   4.31 on their own tints) so they just point --tone-tx back at --tone. */
.sc-stage[data-tone="gold"]  {--tone:var(--flame); --tone-tx:var(--flame-dk); --tone-lt:var(--gold-lt)}
.sc-stage[data-tone="orchid"]{--tone:var(--orchid);--tone-lt:#FCEBFF}
.sc-stage:hover{box-shadow:0 36px 90px rgba(26,23,32,.11)}

.sc-media{
  position:relative;border-radius:24px;overflow:hidden;
  background:var(--tone-lt);
  aspect-ratio:1;
}
.sc-media img{
  position:relative;z-index:1;width:100%;height:100%;object-fit:cover;
}
/* the tile fades up on every change, which is what sells it as a clip
   advancing rather than a static image being swapped */
.sc-play .sc-media img{animation:scMedia 1.1s var(--ease) backwards}
.sc-glow{
  position:absolute;z-index:2;inset:0;pointer-events:none;
  background:linear-gradient(115deg,transparent 34%,rgba(255,255,255,.42) 50%,transparent 66%);
  transform:translateX(-120%);
}
.sc-play .sc-glow{animation:scSheen 1.5s var(--ease-soft) .35s backwards}

.sc-panel{min-width:0}
.sc-kicker{
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;
  color:var(--tone-tx);
}
.sc-panel h3{
  margin-top:10px;
  font-size:clamp(24px,2.7vw,36px);line-height:1.06;color:var(--ink);
}
.sc-play .sc-kicker,.sc-play .sc-panel h3{animation:scUp .7s var(--ease) backwards}
.sc-play .sc-panel h3{animation-delay:.06s}

.sc-list{margin-top:clamp(18px,2vw,26px);display:flex;flex-direction:column;gap:2px}
.sc-list li{
  position:relative;
  padding:13px 16px;border-radius:14px;
  border-top:1px solid var(--ink-08);
  transition:background .5s var(--ease), padding-left .5s var(--ease);
}
.sc-list li:first-child{border-top:0}
.sc-list li::before{
  content:"";position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;
  background:var(--tone);
  transform:scaleY(0);transform-origin:50% 0;
  transition:transform .5s var(--ease);
}
.sc-list li b{
  display:block;
  font-family:var(--font-display),sans-serif;
  font-size:15.5px;font-weight:800;letter-spacing:-.02em;color:var(--ink);
  transition:color .5s var(--ease);
}
.sc-list li p{
  margin-top:3px;font-size:14px;line-height:1.6;color:var(--ink-64);
}
/* the row that is currently "playing" — padding rather than transform, because
   the staggered entry animation owns transform and would clobber it on release */
.sc-list li.lit{background:var(--tone-lt);padding-left:22px}
.sc-list li.lit::before{transform:scaleY(1)}
.sc-list li.lit b{color:var(--tone-tx)}
.sc-play .sc-list li{animation:scUp .6s var(--ease) backwards;animation-delay:calc(.1s + var(--i) * .07s)}

.sc-foot{
  margin-top:clamp(18px,2vw,26px);padding-top:18px;
  border-top:1px solid var(--ink-08);
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-2);
}
.sc-foot .btn{--btn-bd:var(--tone);--btn-fg:var(--tone-tx);--btn-fill:var(--tone)}

/* tabs double as the playback indicator: --p is the 0→1 slide progress */
.sc-nav{--p:0;margin-top:var(--s-3);display:flex;flex-wrap:wrap;gap:10px}
.sc-tab{
  position:relative;overflow:hidden;cursor:pointer;
  padding:10px 18px;border-radius:999px;
  border:1.5px solid var(--ink-14);background:transparent;
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:-.02em;color:var(--ink-64);
  transition:color .4s var(--ease), border-color .4s var(--ease);
}
.sc-tab span{position:relative;z-index:1}
.sc-tab i{
  position:absolute;z-index:0;inset:0;
  background:var(--ink-08);
  transform:scaleX(0);transform-origin:0 50%;
}
.sc-tab:hover{color:var(--ink);border-color:var(--ink-30)}
.sc-tab.is-on{color:var(--ink);border-color:var(--ink)}
.sc-tab.is-on i{transform:scaleX(var(--p))}

@keyframes scMedia{from{opacity:0;transform:scale(1.06)}to{opacity:1;transform:none}}
@keyframes scSheen{from{transform:translateX(-120%)}to{transform:translateX(120%)}}
@keyframes scUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}

/* ============ science — ink band ============ */
.science{
  position:relative;
  /* Was a flat fill of --ink, and on a band this tall that read as a void with
     one violet smudge floating in it. Three layers now: a vertical ramp so the
     band has a top and a bottom instead of being one value everywhere, a warm
     pole low-right, and a fainter one top-left.
     The warm pole is the actual fix. The :root note sets gold as the dominant
     hue and violet as an accent, and this band had that exactly backwards —
     the only colour in it was the violet glow. Nothing is taken away here; the
     violet in ::before is untouched and at the same strength. What was missing
     was the other side of the ramp, so that is what got added. Both warm layers
     are gold and flame at low alpha over ink, which is --flame used the one way
     the token is allowed to be used: touching gold, never on its own. */
  background:
    radial-gradient(78% 54% at 86% 88%, rgba(232,163,23,.155), transparent 64%),
    radial-gradient(64% 46% at 4% 2%,  rgba(255,138,61,.075), transparent 62%),
    linear-gradient(178deg, #211C29 0%, var(--ink) 38%, #141119 100%);
  color:var(--paper);
  padding:var(--s-7) 0;
  border-radius:36px 36px 0 0;
  margin-top:var(--s-5);
  overflow:hidden;
}
.science::before{
  content:"";position:absolute;left:50%;top:-10%;width:min(900px,110vw);height:60%;
  transform:translateX(-50%);pointer-events:none;
  background:radial-gradient(ellipse 50% 50% at 50% 50%, rgba(139,92,246,.30), transparent 70%);
}
.science-inner{position:relative;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.science h2{color:var(--paper);max-width:14ch}
.science .eyebrow{color:var(--gold-2)}

.lede{
  margin-top:var(--s-4);max-width:44ch;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(19px,2.2vw,30px);font-weight:700;line-height:1.34;letter-spacing:-.02em;
}
/* .30, not .18. At .18 an unlit lede does not read as "waiting to light up",
   it reads as broken text — and it is the first thing on the page. */
.lede .lw{transition:color .25s linear;color:rgba(255,255,255,.30)}

.stats{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:var(--s-3);
  margin-top:var(--s-6);padding-bottom:var(--s-5);
  border-bottom:1px solid var(--paper-16);
}
.stat b{
  display:block;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(34px,4.4vw,60px);font-weight:900;letter-spacing:-.05em;line-height:1;
  /* Flat gold-2 declared first and kept as the fallback, then ramped into
     flame-2 where background-clip:text is supported. Four big warm numerals
     are the most colour this band can carry without becoming a paint chart,
     and they are the numbers the page is actually about. */
  color:var(--gold-2);
  background-image:linear-gradient(96deg,var(--gold-2) 16%,var(--flame-2));
}
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .stat b{-webkit-background-clip:text;background-clip:text;color:transparent}
}
.stat span{display:block;margin-top:10px;font-size:14px;line-height:1.55;color:rgba(255,255,255,.74);max-width:22ch}

.facts{margin-top:var(--s-5);display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s-4)}
.facts li{display:flex;gap:var(--s-2)}
.fact-n{
  font-family:var(--font-display),sans-serif;
  font-size:12px;font-weight:800;color:var(--gold-2);padding-top:5px;flex:0 0 auto;
}
.facts h4{font-size:17px;font-weight:800;margin-bottom:8px;color:var(--paper)}
.facts p{font-size:14.5px;line-height:1.68;color:rgba(255,255,255,.70)}

/* A warm tick over each fact, on the ink band only — .facts-light is the same
   list on paper further down results.html and wants none of this. Three short
   gold rules across the row give the block a skeleton; without them the three
   columns just float in the dark with nothing holding them to the page. */
.science .facts li{position:relative;padding-top:22px}
.science .facts li::before{
  content:"";position:absolute;top:0;left:0;width:34px;height:2px;border-radius:2px;
  background:linear-gradient(90deg,var(--gold-2),var(--flame-2));
}

/* same list on a paper background */
.facts-light .fact-n{color:var(--ink)}
.facts-light h4{color:var(--ink)}
.facts-light p{color:var(--ink-64)}

/* ===== the dose bench (how-we-formulate) ================================ */
.bench{
  position:relative;overflow:hidden;
  margin-top:var(--s-6);padding:clamp(24px,3.4vw,44px);
  border:1px solid rgba(255,255,255,.14);border-radius:22px;
  background:
    radial-gradient(110% 90% at 2% 0%, rgba(232,163,23,.075), transparent 56%),
    linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.018));
  box-shadow:0 30px 70px rgba(0,0,0,.30);
}
/* A lit top edge, so the card reads as a lab bench under a lamp rather than a
   rectangle of slightly-less-black. Fading to transparent at both ends keeps it
   from looking like a border someone forgot to finish. */
.bench::before{
  content:"";position:absolute;top:0;left:0;right:0;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--gold-2) 28%,var(--flame-2) 62%,transparent);
  opacity:.62;
}
.bench-head{max-width:64ch}
.bench h3{font-size:clamp(26px,3.4vw,40px);font-weight:900;letter-spacing:-.02em;line-height:1.06;color:var(--paper);margin:6px 0 12px}
.bench-sub{font-size:15px;line-height:1.7;color:rgba(255,255,255,.72)}

.bench-tabs{margin-top:var(--s-3);display:flex;flex-wrap:wrap;gap:8px}
.bench-tab{
  font:600 13.5px/1 var(--font-body),system-ui,sans-serif;color:rgba(255,255,255,.74);
  padding:10px 16px;border-radius:999px;border:1px solid var(--paper-16);
  background:transparent;cursor:pointer;transition:color .2s linear,background .2s linear,border-color .2s linear;
}
.bench-tab:hover{color:var(--paper);border-color:var(--paper-40)}
.bench-tab.is-on{background:var(--paper);border-color:var(--paper);color:var(--ink)}

.bench-grid{margin-top:var(--s-4);display:grid;grid-template-columns:1fr .74fr;gap:clamp(24px,3.6vw,52px);align-items:start}
.bench-rows{display:grid;gap:var(--s-3)}

.brow{padding-bottom:var(--s-3);border-bottom:1px solid rgba(255,255,255,.09)}
.brow:last-child{border-bottom:0;padding-bottom:0}
.brow-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}
.brow-top label{font:700 15.5px/1.2 var(--font-display),system-ui,sans-serif;color:var(--paper);cursor:pointer}
.brow-val{
  font:800 15.5px/1 var(--font-display),system-ui,sans-serif;font-variant-numeric:tabular-nums;
  color:var(--gold-2);transition:color .2s linear;
}
.brow.is-low .brow-val{color:#FF8A80}
.brow.is-high .brow-val{color:var(--flame-2)}

/* The rail. --th is the thumb width and it is load-bearing: a range thumb's
   centre travels from th/2 to (100% - th/2), so the band and the dose marker
   are laid out in that same inset track rather than in raw percentages of the
   rail — otherwise both sit a thumb-radius off at the ends, which is exactly
   where the reader is looking when they push a slider to zero. */
.brow-rail{position:relative;height:26px;display:flex;align-items:center;--th:18px}
.brow-rail::before{content:'';position:absolute;left:0;right:0;height:3px;border-radius:3px;background:var(--paper-16)}
.brow-band{
  position:absolute;height:8px;border-radius:5px;pointer-events:none;
  background:linear-gradient(90deg,var(--gold),var(--gold-2));opacity:.34;
  left:calc(var(--th)/2 + (100% - var(--th)) * var(--a));
  width:calc((100% - var(--th)) * (var(--b) - var(--a)));
}
.brow-mark{
  position:absolute;top:3px;bottom:3px;width:2px;border-radius:2px;pointer-events:none;
  background:var(--gold-2);opacity:.85;
  left:calc(var(--th)/2 + (100% - var(--th)) * var(--d) - 1px);
}
.brow-rail input{
  position:relative;z-index:1;width:100%;height:26px;margin:0;padding:0;
  background:none;border:0;-webkit-appearance:none;appearance:none;cursor:grab;
}
.brow-rail input:active{cursor:grabbing}
.brow-rail input::-webkit-slider-runnable-track{height:26px;background:none;border:0}
.brow-rail input::-moz-range-track{height:26px;background:none;border:0}
.brow-rail input::-webkit-slider-thumb{
  -webkit-appearance:none;width:var(--th);height:var(--th);margin-top:calc((26px - var(--th))/2);
  border:0;border-radius:50%;background:var(--paper);box-shadow:0 3px 12px rgba(0,0,0,.5);
  transition:transform .16s var(--ease-back),box-shadow .2s linear;
}
.brow-rail input::-moz-range-thumb{
  width:var(--th);height:var(--th);border:0;border-radius:50%;background:var(--paper);
  box-shadow:0 3px 12px rgba(0,0,0,.5);transition:transform .16s var(--ease-back),box-shadow .2s linear;
}
.brow-rail input:hover::-webkit-slider-thumb{transform:scale(1.14)}
.brow-rail input:hover::-moz-range-thumb{transform:scale(1.14)}
.brow-rail input:focus{outline:none}
.brow-rail input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--gold),0 3px 12px rgba(0,0,0,.5)}
.brow-rail input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--gold),0 3px 12px rgba(0,0,0,.5)}

.brow-scale{
  margin-top:7px;display:flex;justify-content:space-between;gap:10px;
  font:600 11px/1 var(--font-body),system-ui,sans-serif;letter-spacing:.06em;text-transform:uppercase;
  color:rgba(255,255,255,.44);font-variant-numeric:tabular-nums;
}
.brow-band-txt{color:var(--gold-2);opacity:.72}
.brow-say{margin-top:10px;font-size:13.5px;line-height:1.62;color:rgba(255,255,255,.68);transition:color .2s linear}
.brow.is-ok .brow-say{color:rgba(255,255,255,.84)}

.bench-read{
  position:sticky;top:88px;padding:clamp(20px,2.4vw,28px);border-radius:18px;
  border:1px solid var(--paper-16);background:rgba(0,0,0,.24);
}
.bench-lbl{font:700 11px/1 var(--font-body),system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.52)}
.bench-weeks{margin-top:10px;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.bench-weeks b{
  font:900 clamp(44px,5.6vw,68px)/.9 var(--font-display),system-ui,sans-serif;letter-spacing:-.03em;
  color:var(--gold-2);font-variant-numeric:tabular-nums;transition:color .25s linear;
}
.bench-weeks span{font-size:13.5px;font-weight:600;color:rgba(255,255,255,.64)}
.bench-verdict{margin-top:12px;font:800 17px/1.2 var(--font-display),system-ui,sans-serif;color:var(--paper)}
.bench-copy{margin-top:8px;font-size:14px;line-height:1.68;color:rgba(255,255,255,.72)}
.bench-read.is-low .bench-weeks b{
  color:rgba(255,255,255,.32);
  text-decoration:line-through;text-decoration-color:#FF8A80;text-decoration-thickness:5px;
}
.bench-read.is-low .bench-verdict{color:#FF8A80}
.bench-read.is-high .bench-weeks b{color:var(--flame-2)}
.bench-read.is-high .bench-verdict{color:var(--flame-2)}

.bench-label{margin-top:var(--s-3);padding:14px 16px;border-radius:12px;border:1px dashed var(--paper-16);background:rgba(255,255,255,.03)}
.bench-label-txt{margin-top:7px;font:700 14.5px/1.45 var(--font-display),system-ui,sans-serif;color:var(--paper)}
.bench-label-fine{margin-top:7px;font-size:12.5px;line-height:1.55;color:rgba(255,255,255,.54)}
.bench-read .btn{margin-top:var(--s-3);width:100%}
.bench-fine{margin-top:14px;font-size:12px;line-height:1.6;color:rgba(255,255,255,.46)}

@media (max-width:900px){
  .bench-grid{grid-template-columns:1fr;gap:var(--s-4)}
  .bench-read{position:static}
}

/* No animation to strip here — the bench has no entrance of its own and the
   thumb's scale is the only motion. Kill that and leave the widget working. */
@media (prefers-reduced-motion:reduce){
  .brow-rail input:hover::-webkit-slider-thumb{transform:none}
  .brow-rail input:hover::-moz-range-thumb{transform:none}
}

.sci-cta{margin-top:var(--s-5);display:flex;flex-wrap:wrap;gap:12px}
/* the ghost CTA's black outline disappears on the ink band */
.cta-light{
  --cta-bg:transparent;--cta-fg:var(--paper);--cta-bd:var(--paper-40);
  --cta-fill:var(--paper);--cta-fg-on:var(--ink);
  --cta-shadow:none;--cta-shadow-on:0 14px 30px rgba(0,0,0,.3);
  animation-delay:1.65s;
}
.cta-light:hover{border-color:var(--paper)}

/* ============ results ============ */
.results{padding:var(--s-7) 0;background:var(--cream)}
.results-inner{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.timeline{
  position:relative;margin-top:var(--s-6);
  display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(18px,2.4vw,32px);
}
.timeline .rail{position:absolute;top:6px;left:0;right:0;height:2px;background:var(--ink-14);border-radius:2px}
.timeline .rail i{
  display:block;height:100%;background:linear-gradient(90deg,var(--violet),var(--orchid),var(--flame),var(--gold));border-radius:2px;
  transform:scaleX(var(--f,0));transform-origin:0 50%;
  transition:transform .1s linear;
}
.timeline li{position:relative;padding-top:var(--s-4)}
.timeline .dot{
  position:absolute;top:0;left:0;width:14px;height:14px;border-radius:50%;
  background:var(--cream);border:2.5px solid var(--ink-14);
  transition:border-color .4s var(--ease), transform .4s var(--ease-back);
}
.timeline li.lit .dot{border-color:var(--violet);transform:scale(1.18)}
.timeline b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:19px;font-weight:900;letter-spacing:-.03em;margin-bottom:8px;
}
.timeline p{font-size:14px;line-height:1.62;color:var(--ink-64)}
/* Used by the vertical phone timeline further down. It opens outward and
   fades rather than pulsing on a loop: the dot lighting is an event — the rail
   reached this week — and an event should happen once, not keep announcing
   itself while you read the entry next to it. */
@keyframes dot-ring{
  from{opacity:.9;transform:scale(.6)}
  to{opacity:0;transform:scale(2.1)}
}
@media (prefers-reduced-motion:reduce){
  .timeline li.lit .dot::after{animation:none}
  .timeline .rail i{transition:none}
}

/* ---- hair / face subsections ---- */
.res-group{margin-top:var(--s-6)}
/* the second group needs a clear break, not just more of the same rhythm —
   otherwise the face timeline reads as four more weeks of the hair one */
.res-group + .res-group{margin-top:var(--s-7);padding-top:var(--s-6);border-top:1px solid var(--ink-14)}
.res-head{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.res-head h3{
  font-family:var(--font-display),sans-serif;
  font-size:clamp(21px,2.6vw,30px);font-weight:900;letter-spacing:-.04em;line-height:1.05;
}
/* forced onto its own row so the tag and product name stay on one line together */
.res-meta{flex:1 0 100%;margin:0;font-size:13.5px;line-height:1.6;color:var(--ink-64)}
.res-tag{
  display:inline-flex;align-items:center;flex:0 0 auto;
  font-family:var(--font-display),sans-serif;
  font-size:10.5px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  padding:6px 13px;border-radius:999px;color:#fff;
}
.res-tag-hair{background:var(--violet)}
.res-tag-face{background:var(--orchid)}
/* the group carries its own heading, so the rail starts tighter than it would
   under a full section head */
.res-group .timeline{margin-top:var(--s-5)}
/* face runs on its own colour so the two tracks are not mistaken for one */
.res-group[data-tone="face"] .rail i{background:linear-gradient(90deg,var(--orchid),var(--flame),var(--gold))}
.res-group[data-tone="face"] .timeline li.lit .dot{border-color:var(--orchid)}

/* ===== before/after wipe ================================================ */
.ba{margin-top:var(--s-5)}
.ba-stage{
  position:relative;isolation:isolate;overflow:hidden;
  aspect-ratio:4/3;max-height:min(56vh,520px);
  border-radius:18px;background:var(--ink-08);
  box-shadow:0 18px 44px rgba(10,10,10,.14);
}
.ba-img{position:absolute;inset:0;z-index:1;display:block;width:100%;height:100%;object-fit:cover}
/* The wipe itself. clip-path on the top image rather than a wrapping div with
   overflow:hidden — a wrapper narrower than the stage squashes the image it
   contains unless the image is re-sized to the stage in a second place, and
   two sources of truth for one width is how these things drift. */
.ba-before{clip-path:inset(0 calc(100% - var(--p,50) * 1%) 0 0)}

/* The real control, laid over the whole stage and invisible. Full-bleed so a
   click anywhere jumps the wipe to that point, which is what people try first. */
.ba-range{
  position:absolute;inset:0;z-index:3;width:100%;height:100%;
  margin:0;padding:0;border:0;background:none;opacity:0;
  -webkit-appearance:none;appearance:none;cursor:ew-resize;
}
.ba-range::-webkit-slider-thumb{-webkit-appearance:none;width:48px;height:100%;border:0;background:none}
.ba-range::-moz-range-thumb{width:48px;height:100%;border:0;background:none;opacity:0}
.ba-range::-moz-range-track{background:none;border:0}
.ba-range:focus{outline:none}

.ba-line{
  position:absolute;top:0;bottom:0;z-index:2;pointer-events:none;
  left:calc(var(--p,50) * 1%);width:2px;margin-left:-1px;
  background:var(--paper);box-shadow:0 0 0 1px rgba(10,10,10,.2);
}
.ba-line i{
  position:absolute;top:50%;left:50%;width:46px;height:46px;margin:-23px 0 0 -23px;
  border-radius:50%;background:var(--paper);box-shadow:0 6px 22px rgba(10,10,10,.34);
  transition:box-shadow .2s linear,transform .18s var(--ease-back);
}
/* Two chevrons, pointing out. Borders rather than glyphs so they inherit
   nothing from the font stack and cannot be shifted by a fallback face. */
.ba-line i::before,.ba-line i::after{
  content:'';position:absolute;top:50%;width:7px;height:7px;margin-top:-4px;
  border:2px solid var(--ink);border-width:2px 2px 0 0;
}
.ba-line i::before{left:13px;transform:rotate(-135deg)}
.ba-line i::after{right:13px;transform:rotate(45deg)}
.ba-stage:hover .ba-line i{transform:scale(1.08)}
.ba-range:focus-visible ~ .ba-line i{box-shadow:0 0 0 3px var(--gold),0 6px 22px rgba(10,10,10,.34)}

.ba-tag{
  position:absolute;bottom:14px;z-index:2;pointer-events:none;
  font:700 10.5px/1 var(--font-body),system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;
  color:#fff;padding:8px 12px;border-radius:999px;
  background:rgba(10,10,10,.52);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
}
.ba-tag-l{left:14px}
.ba-tag-r{right:14px}

.ba-cap{margin-top:14px;max-width:72ch;font-size:13px;line-height:1.66;color:var(--ink-46)}
.ba-cap b{font-weight:700;color:var(--ink-90)}

@media (prefers-reduced-motion:reduce){
  .ba-line i{transition:none}
  .ba-stage:hover .ba-line i{transform:none}
}

.pairing{
  margin-top:var(--s-5);padding:var(--s-4);
  background:var(--paper);border:1px solid var(--ink-14);border-radius:22px;
}
.pairing h4{
  font-family:var(--font-display),sans-serif;
  font-size:18px;font-weight:800;letter-spacing:-.02em;margin-bottom:10px;
}
.pairing p{font-size:14.5px;line-height:1.7;color:var(--ink-64)}
.pair-list{margin-top:var(--s-3);display:grid;grid-template-columns:1fr 1fr;gap:var(--s-3)}
.pair-list b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;margin-bottom:6px;color:var(--orchid);
}
.pair-list span{display:block;font-size:14px;line-height:1.65;color:var(--ink-64)}
.pair-note{margin-top:var(--s-3)}

/* ============ ai analysis — ink band ============ */
.scan{position:relative;padding:var(--s-7) 0;background:var(--ink);color:var(--paper);overflow:hidden}
.scan::before{
  content:"";position:absolute;left:50%;top:-14%;width:min(900px,110vw);height:62%;
  transform:translateX(-50%);pointer-events:none;
  background:radial-gradient(ellipse 50% 50% at 50% 50%, rgba(192,38,211,.26), transparent 70%);
}
.scan-inner{position:relative;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.scan-inner .sec-head{margin:0;padding:0;max-width:none}
.scan h2{color:var(--paper)}
.scan .eyebrow{color:var(--gold-2)}
.scan .sec-sub{color:rgba(255,255,255,.62)}
/* ---- the analysis preview graphic ----------------------------------------
   Sits on the ink band, so it is drawn as a lit instrument on a dark deck
   rather than as a picture pasted onto one.

   The frame is capped at the graphic's own 720-unit width so the SVG renders
   at roughly 1:1 on a large screen. That is not a cosmetic choice: everything
   inside an SVG is in user units, so a viewBox stretched to 1152px scales the
   14px labels to 22px and the hairlines to 1.6px, and the whole thing stops
   reading as an instrument and starts reading as a poster. Capping it keeps
   one scale factor near 1 on desktop; the phone block near the end of this
   file re-sizes the type in user units for the same reason in reverse. */
.scanviz{margin:var(--s-6) 0 0}
.sv-frame{
  max-width:720px;margin-inline:auto;
  border-radius:18px;overflow:hidden;
  border:1px solid rgba(255,255,255,.10);
  box-shadow:0 30px 70px rgba(0,0,0,.45);
}
.sv-svg{display:block;width:100%;height:auto;color:rgba(255,255,255,.55)}
.sv-bg{fill:rgba(255,255,255,.035)}
.sv-grid{color:rgba(255,255,255,.5)}
.sv-hair{color:rgba(255,255,255,.5)}
/* the two edges of the part, and the callout measuring across it */
.sv-part path{fill:none;stroke:var(--orchid);stroke-width:1.6;opacity:.75}
.sv-measure path{fill:none;stroke:var(--gold);stroke-width:1.6;opacity:.9}
.sv-brackets path{fill:none;stroke:var(--gold);stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.sv-nodes circle{fill:none;stroke:var(--gold);stroke-width:2.2}
.sv-nodes text{
  fill:var(--paper);font-size:14px;font-weight:700;
  letter-spacing:.01em;dominant-baseline:middle;
}
.sv-status{
  fill:rgba(255,255,255,.42);font-size:11px;font-weight:800;
  letter-spacing:.16em;text-transform:uppercase;
}

/* Motion is parked until the section is actually on screen. .reveal/.in is
   already doing the visibility half of this, and without the pause the sweep
   would run its loop for the whole length of the page above it for nobody. */
.sv-sweep{opacity:0}
.sv-node{opacity:0;transform-box:fill-box;transform-origin:center}
.scanviz .sv-sweep,.scanviz .sv-node{animation-play-state:paused}
.scanviz.in .sv-sweep{animation:sv-sweep 4.4s cubic-bezier(.55,0,.45,1) infinite;animation-play-state:running}
.scanviz.in .sv-node{
  animation:sv-pop 4.4s var(--ease) infinite;
  /* This delay has to live here, after the shorthand, and not on .sv-node.
     `animation` is a shorthand, so it resets animation-delay to 0s — and it
     does that at (0,3,0), which outranks a delay set on .sv-node at (0,1,0)
     no matter where that rule sits in the file. Written the other way it
     measured as four delays of 0s and all four labels landed on the same
     frame, which is the one thing the stagger exists to avoid.

     The step is .72s rather than a round .5s so the labels track the sweep
     instead of drifting ahead of it. The nodes sit at y=52/168/214/262 and
     the sweep crosses those at roughly 1.28/2.48/2.95/3.45s into the loop;
     a node becomes visible at its delay plus the 24% mark, i.e. delay+1.06s.
     .22s + i*.72s puts the first and last on their crossings almost exactly
     and the middle two within a few tenths. It cannot be exact for all four
     — the nodes are not evenly spaced down the frame and one linear stagger
     cannot match four uneven gaps — but it reads as the sweep finding them
     rather than as four things blinking on a timer. */
  animation-delay:calc(.22s + var(--i) * .72s);
  animation-play-state:running;
}
/* the sweep starts above the frame and leaves below it, so the pass reads as
   something travelling through rather than appearing and vanishing */
@keyframes sv-sweep{
  0%{opacity:0;transform:translateY(-72px)}
  8%,84%{opacity:1}
  92%,100%{opacity:0;transform:translateY(320px)}
}
@keyframes sv-pop{
  0%,16%{opacity:0;transform:translateY(6px)}
  24%,86%{opacity:1;transform:translateY(0)}
  96%,100%{opacity:0;transform:translateY(0)}
}
@media (prefers-reduced-motion:reduce){
  /* the graphic still has to say what it says, so the nodes stay — they just
     stop arriving. The sweep is the only purely decorative part, so it goes. */
  .scanviz.in .sv-sweep{animation:none;opacity:0}
  .scanviz.in .sv-node{animation:none;opacity:1}
}

.scan-grid{margin-top:var(--s-6);display:grid;grid-template-columns:1fr 1fr;gap:var(--s-4)}
.scan-card{
  padding:var(--s-4);border-radius:24px;
  background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);
}
.scan-card h3{
  font-family:var(--font-display),sans-serif;
  font-size:21px;font-weight:900;letter-spacing:-.03em;margin:14px 0 10px;
}
.scan-card > p{font-size:14.5px;line-height:1.68;color:rgba(255,255,255,.58)}
.scan-metrics{margin-top:var(--s-4);display:grid;gap:15px}
.scan-metrics li{position:relative;padding-left:17px}
.scan-metrics li::before{
  content:"";position:absolute;left:0;top:7px;width:6px;height:6px;border-radius:50%;background:var(--gold-2);
}
.scan-metrics b{display:block;font-size:14.5px;font-weight:800;margin-bottom:4px}
.scan-metrics span{display:block;font-size:14px;line-height:1.62;color:rgba(255,255,255,.55)}
/* the instruction rows sit in the same list as the measurements above them,
   because they only mean anything as a consequence of those measurements —
   the bullet is what separates read-off-you from do-this */
.scan-metrics li.is-do::before{background:var(--paper)}
.scan-metrics li.is-do b{color:var(--paper)}

.scan-real{
  margin-top:var(--s-4);padding:var(--s-4);border-radius:24px;
  background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.26);
}
.scan-real h4{
  font-family:var(--font-display),sans-serif;
  font-size:19px;font-weight:800;letter-spacing:-.025em;
  margin-bottom:10px;color:var(--paper);
}
.scan-real > p{font-size:14.5px;line-height:1.7;color:rgba(255,255,255,.62)}
.real-list{margin-top:var(--s-3);display:grid;grid-template-columns:1fr 1fr;gap:var(--s-3)}
.real-list b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;margin-bottom:6px;color:var(--paper);
}
.real-list span{display:block;font-size:14px;line-height:1.65;color:rgba(255,255,255,.58)}
.real-note{margin-top:var(--s-3)}

/* ============ how to track ============ */
.track{padding:var(--s-7) 0;background:var(--paper)}
.track-inner{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.track-inner .sec-head{margin-left:0;margin-right:0;padding:0}
.track-cta{margin-top:var(--s-5);display:flex;flex-wrap:wrap;gap:12px}

/* ============ authorised retailers ============ */
/* A quiet white strip between the kit band and the science orb, hairlined top
   and bottom. Deliberately not a hero: it sits between a gold band and a cream
   one, and a third loud field there would turn the middle of the page into a
   stack of competing posters. The rules do the separating so the padding does
   not have to.

   Marks, not names — see the comment in index.html for where the files go and
   what happens before they arrive. */
.retail{
  background:var(--paper);
  padding:var(--s-6) 0;
  border-top:1px solid var(--ink-08);
  border-bottom:1px solid var(--ink-08);
}
.retail-inner{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);text-align:center}
.retail h2{font-size:clamp(24px,3.2vw,38px);line-height:1.06;color:var(--ink);margin:0 0 6px}
.retail-sub{font-size:14.5px;color:var(--ink-64);margin:0 0 var(--s-5)}

/* One row wherever one fits, wrapping rather than shrinking.

   The column gap came down from --s-6 to --s-2 when the marks grew, and that
   is not the same as tightening the row. Each PNG is a 4:1 canvas with the
   mark centred inside it, and the marks only fill 47–70% of that width — so
   between any two of them there is already 60–90px of transparent canvas
   doing the job a gap would do. The old 96px was being ADDED to that, which
   is why four marks at any readable size used to run off the end of the row.
   16px of declared gap plus the canvases' own margins is what reads, on the
   page, as about 100px of air. */
.retail-list{
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:var(--s-4) var(--s-2);
}
.retail-list li{display:flex;align-items:center;justify-content:center}

/* Height-constrained, not width-constrained, because retailer marks arrive at
   wildly different aspect ratios — a wordmark like Flipkart is four times the
   width of a square-ish one at the same optical size. Matching the heights is
   what makes a mixed row read as one row. max-width catches the pathological
   case of a very wide lockup without distorting it, since object-fit keeps the
   ratio either way.

   Full colour, at full strength. This was greyscale-until-hover, on the
   argument that four sets of someone else's brand colours would shout over
   ours — but the row is only worth its space if a shopper recognises the
   shops, and people recognise these four by colour before they read the
   words. Muting them protected the page's palette at the expense of the one
   job the section has.

   Nothing replaces the hover state, because these are not links yet and a
   hover response on something that cannot be clicked is a promise the page
   does not keep. When the storefront URLs arrive, that is where it belongs. */
/* SIZED BY CANVAS, READ BY INK, AND THOSE ARE NOT THE SAME NUMBER. Every mark
   sits on the same 4:1 canvas so the four boxes match, but the ink inside
   fills only 50–80% of that canvas height — Blinkit's wordmark has the least
   vertical range of the four and covers half of it. So the declared height is
   NOT what a shopper perceives: at the old 40px the marks were reading at
   20–32px, which is smaller than the body copy two lines above them.

   68px here puts the ink at roughly 34–54px, which is the size the row was
   always supposed to be. It is also the ceiling: the canvas is 4:1, so the
   boxes are 4 x 68 = 272px, and four of those plus gaps come to 1136px inside
   the 1152px that --maxw leaves at full width. Raising the cap past 68 does
   not make the marks bigger, it makes the row wrap to two lines.

   max-width had to move with it. At 168px it was below the 272px the box now
   needs, so it would have clamped the width and let object-fit letterbox the
   height straight back down to 42px — the cap silently undoing the size. */
.retail-list img{
  display:block;height:clamp(38px,5.2vw,68px);width:auto;max-width:280px;
  object-fit:contain;
}

/* The fallback. Hidden until the inline onerror in index.html says the artwork
   is not there, at which point the retailer's name stands in for its mark. The
   point is that a missing file costs a logo, never a broken-image icon and
   never a silently empty row that makes the section look unfinished. */
.retail-list li span{display:none}
.retail-list li.no-art img{display:none}
.retail-list li.no-art span{
  display:block;
  font-family:var(--font-display),sans-serif;
  /* tracks the marks: a text stand-in that stayed at the old size would make
     one missing file look like a deliberately quieter retailer */
  font-size:clamp(22px,3.4vw,44px);font-weight:800;letter-spacing:-.03em;
  color:var(--ink-64);
}

/* Dormant until the names become links. Styled now so that adding an href is
   the whole change rather than the start of one. */
.retail-list a{display:block;color:inherit}

/* The reason the section earns its space. A list of shops is a convenience;
   naming what falls away outside that list is the part a shopper can act on,
   and it is the same 7-day replacement the bag promises, so it states a limit
   that exists rather than inventing one. (It used to cite the announce bar as
   a second source for that promise; the bar is gone, and the bag is now the
   only other place on this page the seven days are stated.)
   --ink-64 measures 5.29:1 on paper, so it clears AA at this size. */
.retail-note{
  margin:var(--s-4) auto 0;max-width:54ch;
  font-size:12.5px;line-height:1.65;color:var(--ink-64);
}

/* ============ footer — paper ============ */
/* Flipped off ink, and not only for the colour. The brand mark is near-black
   artwork; on an ink footer the only way to show it was the knocked-out light
   version at 20px tall, which reads as a caption. On paper at full width it
   reads as a signature, which is what a mark at the end of a page is for.

   Everything else moved from white-on-ink to ink-on-paper at matching relative
   weights, so the hierarchy is unchanged: strapline and legal line sit back,
   nav and email address come forward. The one substitution is the accent —
   gold carries an ink band and goes illegible on white, so the links take
   --flame-dk, which is the text-safe end of the warm ramp (6.31:1 here). */
footer{
  background:var(--paper);color:var(--ink);
  padding:var(--s-5) var(--s-3) var(--s-4);
  border-top:1px solid var(--ink-14);
  overflow:hidden;
}
.foot-inner{max-width:var(--maxw);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center}
.foot-line{font-family:var(--font-display),sans-serif;font-size:13px;font-weight:700;color:var(--ink-46)}
.foot-nav{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--s-3);margin-top:4px}
.foot-nav a{
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:700;letter-spacing:-.02em;
  color:var(--ink-64);
  transition:color .35s var(--ease);
}
.foot-nav a:hover{color:var(--flame-dk)}
.foot-contact{font-size:13px;color:var(--ink-64)}
.foot-contact a{color:var(--flame-dk);border-bottom:1px solid rgba(180,71,13,.34);transition:border-color .35s var(--ease)}
.foot-contact a:hover{border-bottom-color:var(--flame-dk)}

/* Tap targets, and nothing else — this block moves no pixels.
   These are 13px links that render 16-17px tall. That is under the 24x24
   minimum in WCAG 2.5.8 (AA) and, more to the point, genuinely awkward to hit
   with a thumb: five of them sit in the footer of every page and one is the
   only contact address on the site.
   The hit area is grown with a pseudo-element instead of padding on purpose.
   Padding would grow the inline boxes and push the whole centred footer
   column down, which is a visual change to fix an invisible problem; an
   absolutely-positioned ::after inside the anchor is still part of the
   anchor for hit-testing and occupies no layout space at all.
   The numbers are bounded by the neighbours, not chosen for looks: 7px
   vertical takes 16px to 30px, and the two .foot-nav rows sit 49px apart, so
   33px of clear space absorbs 14px without the rows touching. 6px horizontal
   eats 12px of .foot-nav's 24px (--s-3) gap and leaves 12px between adjacent
   targets. Widen either number and neighbouring links start stealing each
   other's taps, which is worse than the small target was. */
.foot-nav a,.foot-contact a{position:relative}
.foot-nav a::after,.foot-contact a::after{content:'';position:absolute;inset:-7px -6px}
.foot-legal{font-size:12px;color:var(--ink-46)}

/* The signature — a mark, not a mural.

   It used to be min(1080px, 88vw), which on a desktop monitor put a 1080x251
   slab of NUXI across the bottom of every page. The intent had been a brand
   sign-off; what it actually did was end all eleven pages on the largest
   single element most of them contain, with the shop, the science and the
   bag all closing on a wordmark several times the size of anything they were
   selling. A footer signature is the quietest thing on the page by function:
   it says who this was, to someone who has already finished reading.

   So it is sized like a logo now. 200px wide on the artwork's own 4.3:1
   ratio comes to about 47px tall — the same order as the nav lockup, which
   is the right comparison, because this is the same mark doing the same job
   at the other end of the page. The vw term keeps it proportional on a phone
   rather than letting a fixed 200px creep back toward edge-to-edge at 375px.

   alt="" and aria-hidden, because the word it spells is already in the legal
   line one row above it — a screen reader saying "NUXI" twice in two lines is
   noise. It is decoration here in the precise sense: removing it loses nothing
   a non-sighted visitor was not already told.

   margin-top carries the whole gap so the flex column above keeps its own
   12px rhythm. It came down with the mark: 64px of clearance was runway for
   something large arriving, and at this size it just left a hole. */
.foot-mark{
  display:block;width:min(200px,46vw);height:auto;
  margin:var(--s-4) auto 0;
}

/* Clearance for the PDP buy bar, and it only became necessary when the mark
   got small. The bar is position:fixed at bottom:0, so it lies on top of
   whatever the bottom of the page happens to be — and the bottom of the page
   is this. At 251px tall the bar hid the mark's lower fifth and the wordmark
   still read; at 47px it would cover almost the whole thing, which is a
   signature you can only see by scrolling up.

   Measured, not guessed: 74px at desktop, 68px on a phone where the
   thumbnail and bag link are hidden and the padding tightens. One number
   rather than a breakpoint, because over-reserving the phone by 6px is
   invisible and a second rule is another thing to keep in sync.

   Scoped to .nx-barred, which convert.js puts on the body only while the bar
   is actually shown and takes off again when the real Add To Bag scrolls back
   into view. So every page without a buy bar keeps its plain 40px, and the
   PDPs only pay for the space at the moment the bar is there to need it. */
body.nx-barred footer{padding-bottom:calc(74px + var(--s-2))}

/* ============ quiz — violet band ============ */
/* Deliberately still violet, and it is meant to stay that way.
   tools/hue_audit.py will flag this every time it is run: on quiz.html this
   band is the entire <main>, so that page measures 99.9% violet at roughly six
   times the colour load of any other, and it single-handedly takes the site
   total from 14% violet to 58%. That is a real reading, not a bug, and it was
   put to the brief and kept on purpose — the quiz is the one branded moment
   that is allowed to be loud. Charcoal, a violet card on cream, and charcoal
   on the standalone page only were all rendered and all turned down.
   So: do not desaturate this to make the audit look better. The other four
   pages are where the purple was actually cut, and they read 80% gold. */
.quiz{
  position:relative;overflow:hidden;
  background:linear-gradient(158deg,var(--violet-2) 0%,var(--violet) 42%,#3F1178 100%);
  color:var(--paper);
  padding:var(--s-7) 0;
  border-radius:36px 36px 0 0;
  margin-top:var(--s-6);
}
.quiz::before{
  content:"";position:absolute;left:50%;top:-16%;width:min(900px,110vw);height:70%;
  transform:translateX(-50%);pointer-events:none;
  background:radial-gradient(ellipse 50% 50% at 50% 50%, rgba(255,201,77,.26), transparent 70%);
}
.quiz-inner{position:relative;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.quiz-head{text-align:center;max-width:46ch;margin:0 auto var(--s-5)}
.quiz h2{color:var(--paper);font-size:clamp(32px,5.4vw,68px);line-height:.96}
.quiz .eyebrow{color:var(--gold-2)}
.quiz .sec-sub{color:rgba(255,255,255,.74);margin-left:auto;margin-right:auto}

.quiz-card{
  max-width:840px;margin:0 auto;
  background:rgba(255,255,255,.08);
  border:1px solid rgba(255,255,255,.18);
  border-radius:26px;
  padding:clamp(20px,3vw,40px);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
}
.quiz-top{display:flex;align-items:center;gap:16px;margin-bottom:var(--s-4)}
.quiz-count{
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  color:rgba(255,255,255,.68);white-space:nowrap;
}
.quiz-bar{flex:1;height:3px;border-radius:3px;background:rgba(255,255,255,.18);overflow:hidden}
.quiz-bar i{
  display:block;height:100%;width:100%;background:var(--gold-2);
  transform:scaleX(var(--qp,.33));transform-origin:0 50%;
  transition:transform .6s var(--ease);
}

/* the card animates between question heights, so it needs to be the thing
   whose height is being set — see swap() in quiz.js */
.quiz-body{overflow:hidden}
.quiz-back{
  flex:0 0 auto;background:none;border:0;cursor:pointer;
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;
  color:rgba(255,255,255,.62);padding:4px 2px;
  transition:color .3s var(--ease);
}
.quiz-back:hover{color:var(--gold-2)}
.quiz-back[hidden]{display:none}

.qstep{display:none}
.qstep.is-on{display:block;animation:qIn .5s var(--ease) both}
.qstep h3{font-size:clamp(20px,2.6vw,30px);color:var(--paper);margin-bottom:var(--s-2);letter-spacing:-.03em}
.qnote{font-size:13.5px;line-height:1.6;color:rgba(255,255,255,.58);margin:0 0 var(--s-3);max-width:56ch}
.opts{display:grid;gap:10px}
.qopt{
  cursor:pointer;text-align:left;
  display:grid;grid-template-columns:26px 1fr;align-items:center;gap:14px;
  font-family:var(--font-display),sans-serif;
  font-size:15px;font-weight:700;letter-spacing:-.02em;
  padding:14px 20px;border-radius:14px;
  border:1.5px solid rgba(255,255,255,.20);
  background:rgba(255,255,255,.05);color:var(--paper);
  transition:border-color .35s var(--ease), background .35s var(--ease), transform .35s var(--ease);
}
.qopt:hover{border-color:var(--gold-2);background:rgba(255,255,255,.13);transform:translateX(5px)}
.qopt:focus-visible{outline:2px solid var(--gold-2);outline-offset:3px}
/* Printed because they work — quiz.js binds 1-9 to the options in order, and a
   shortcut nobody can see is a shortcut nobody uses. */
.qopt kbd{
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;line-height:24px;
  width:24px;height:24px;border-radius:7px;text-align:center;
  background:rgba(255,255,255,.10);color:rgba(255,255,255,.62);
  transition:background .3s var(--ease), color .3s var(--ease);
}
.qopt:hover kbd{background:var(--gold-2);color:var(--ink)}
.qopt-t b{display:block;font-weight:700}
.qopt-t i{display:block;font-style:normal;font-family:var(--font-body),sans-serif;
  font-size:12.5px;font-weight:400;letter-spacing:0;color:rgba(255,255,255,.52);margin-top:2px}
/* multi-select: the picked state has to survive hover leaving, so it is a
   border and a fill rather than the hover treatment */
.qopt.is-picked{border-color:var(--gold-2);background:rgba(255,201,77,.16)}
.qopt.is-picked kbd{background:var(--gold-2);color:var(--ink)}

/* ============ the steps that take more than one answer ============
   Three of the eight questions accept several answers, and before this they
   looked exactly like the five that do not. The result was the single worst
   moment in the form: people tapped the row that described them, the step did
   not advance the way every previous step had, and with six options pushing the
   Continue button below the fold on a phone there was nothing on screen to
   explain why. A question that appears to have stopped responding is a question
   people leave.

   Nothing here is decoration. The cue says it before the tap, the tick box says
   it at a glance, and the button says what it will do — and quiz.js §6c gives
   the one-answer reader a way out that is not the button at all. */

/* Above the options, not inside the note. Shaped so it cannot be mistaken for
   body copy: it is the instruction, and it has to be read as one. */
.qmulti{
  display:flex;align-items:center;gap:9px;
  margin:0 0 12px;
  font-family:var(--font-display),sans-serif;
  font-size:12px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
  color:var(--gold-2);
}
/* Two stacked ticked squares — the same shape as the boxes below it, said
   twice, which is the entire message. Drawn in CSS because it is two rectangles
   and an inline SVG for that is a request the page does not need to make. */
.qmulti-i{
  position:relative;flex:0 0 auto;width:17px;height:15px;
}
.qmulti-i::before,.qmulti-i::after{
  content:'';position:absolute;width:11px;height:11px;border-radius:3.5px;
  border:1.6px solid currentColor;
}
.qmulti-i::before{left:0;top:0;opacity:.45}
.qmulti-i::after{right:0;bottom:0;background:currentColor}

/* Room on the trailing edge for the box. The single-answer steps keep their
   two-column grid untouched — the difference between the two shapes is the
   point, and it disappears if every row gets the extra track. */
/* The long steps — five options or more, which in practice is the two "what
   have you already tried" questions and the six-way "what are you seeing".
   Those are the only steps that outgrow the screen, so they are the only ones
   that pay for the density. Everything here is air, not content: the row
   padding, the gaps between rows, and the margins under the heading, the note
   and the cue. Nothing is hidden and nothing is dropped — in particular the
   grey note stays, because on these three questions it is not a restatement of
   "pick as many as apply", it is the reason to answer honestly, and that is the
   sentence doing the most work on the step.

   Set on .qstep rather than on .qopt so the four-option steps are untouched:
   quiz.js adds .is-dense only when the filtered list actually runs long. */
.qstep.is-dense h3{margin-bottom:12px}
.qstep.is-dense .qnote{margin-bottom:13px}
.qstep.is-dense .qmulti{margin-bottom:8px}
.qstep.is-dense .opts{gap:7px}
.qstep.is-dense .qopt{padding:9px 20px}
.qstep.is-dense .qgo{margin-top:16px}

.qopt.qmany{grid-template-columns:26px 1fr 22px}
.qtick{
  width:22px;height:22px;border-radius:7px;
  border:1.6px solid rgba(255,255,255,.34);
  display:grid;place-items:center;
  color:transparent;background:transparent;
  transition:background .25s var(--ease), border-color .25s var(--ease), color .25s var(--ease);
}
.qtick svg{width:13px;height:13px;display:block}
.qopt.qmany:hover .qtick{border-color:var(--gold-2)}
.qopt.qmany.is-picked .qtick{background:var(--gold-2);border-color:var(--gold-2);color:var(--ink)}

/* ---- the button, in its two jobs ----
   With nothing picked it is an answer ("none of these") and stays quiet. With
   something picked it is the exit, and it is allowed to look like one.

   scroll-margin-bottom is read by showGo(), which scrolls with block:'end' and
   would otherwise park the button flush against the bottom edge of the screen —
   on a phone that is exactly where the browser chrome starts, so the button
   reads as cut off rather than as the end of the question. */
.qgo{margin-top:var(--s-3);position:relative;overflow:hidden;scroll-margin-bottom:18px}
.qgo:not(.is-ready){opacity:.82}
.qgo-n{
  display:inline-grid;place-items:center;min-width:21px;height:21px;
  margin-left:10px;padding:0 6px;border-radius:999px;
  font-style:normal;font-size:11.5px;font-weight:800;line-height:1;
  background:rgba(26,23,32,.16);
}

/* THE FILL IS THE NOTICE. quiz.js arms a two-second advance on the first pick
   and this is what makes that honest rather than a trapdoor: the button is
   visibly performing the press the reader would otherwise have to make, and any
   touch, key or scroll stops it for good. Without something on screen counting
   down, an auto-advance is a page that moved on its own.

   ::before, and that is not arbitrary: .btn::after is already the hover sweep,
   and it carries border-radius:50% 50% 0 0 so the sweep rises as a dome. Hang
   the arming fill on that same pseudo-element and it inherits the dome — the
   countdown grows a curved crown and reads as a bubble rather than a bar. It
   also inherits the sweep's .5s transform transition, which then argues with
   the animation every time the class comes off. ::before is free on .btn
   (content:none), so the two effects get one element each.

   z-index:-1 is the same trick .btn::after uses to sit behind a label that is a
   bare text node with no box of its own. Both pseudos are at -1, so they stack
   in document order and the hover sweep correctly paints over the countdown.
   Clipped to the pill by .btn's own overflow:hidden. */
.qgo.is-arming::before{
  content:'';position:absolute;inset:0;z-index:-1;
  border-radius:0;
  background:rgba(26,23,32,.16);
  transform-origin:0 50%;transform:scaleX(0);
  animation:qArm 2s linear forwards;
  pointer-events:none;
}
@keyframes qArm{from{transform:scaleX(0)}to{transform:scaleX(1)}}
/* No fill means no warning, so quiz.js does not arm at all here. The rule is
   restated rather than left to the script so the two cannot drift apart. */
@media (prefers-reduced-motion:reduce){
  .qgo.is-arming::before{display:none}
}

/* ---- the hand, on the seventh question only ----
   Drawn by quiz.js §5b, which decides WHICH step gets one. This decides WHEN,
   and the answer is: not while the button is arming. Those two seconds are the
   button pressing itself, and a hand tapping a control that is already being
   pressed is animation arguing with animation. The moment the arm is cancelled
   — a scroll, a key, a second pick — .is-arming comes off, the hand fades up
   and starts tapping, which is the first moment it is actually true that this
   button is the only way forward.

   FOUR TAPS, THEN IT STOPS, and that is the whole difference between a cue and
   a nag. This step is six rows long and somebody may sit on it for half a
   minute; a glyph still wiggling in the corner of their eye at second thirty is
   the page fidgeting at them. `both` holds the last frame, so what is left
   afterwards is a plain static hand — still a signpost, just no longer waving.
   Every pick rebuilds the button, so the taps replay on each one, which is
   exactly when something has changed and re-announcing is worth doing.

   currentColor throughout, so it rides the .btn hover sweep: when the fill
   rises and the label flips to --btn-fg-on, the hand flips with it. A hardcoded
   ink here would vanish into the violet at the halfway point of the sweep. */
.qgo-hand{
  display:inline-block;vertical-align:-3px;
  width:16px;height:16px;margin-left:9px;
  /* bottom centre: the squash on the impact frame has to compress the hand
     onto the button, not shrink it towards its own middle */
  transform-origin:50% 100%;
  opacity:1;transition:opacity .28s var(--ease);
  animation:qgoTap 1.9s var(--ease) 4 both;
}
.qgo-hand svg{width:100%;height:100%;display:block;fill:currentColor}
/* animation:none as well as opacity:0 — restoring the property when the class
   comes off restarts the sequence from frame one, so the taps begin on the
   cancel rather than halfway through a cycle that ran while it was invisible */
.qgo.is-arming .qgo-hand{opacity:0;animation:none}

/* Lift, drop, squash, settle. The pause at the top of each cycle is most of the
   duration on purpose — a hand tapping continuously is a hand having a seizure;
   a hand that taps, waits, taps again is a hand demonstrating. */
@keyframes qgoTap{
  0%,44%  {transform:translateY(0) rotate(-7deg) scaleY(1)}
  57%     {transform:translateY(-3.5px) rotate(-7deg) scaleY(1)}
  69%     {transform:translateY(2px) rotate(-7deg) scaleY(.9)}
  78%     {transform:translateY(0) rotate(-7deg) scaleY(1)}
  100%    {transform:translateY(0) rotate(-7deg) scaleY(1)}
}
/* The hand stays — it is a signpost and it still points at the right control
   with no motion at all. Only the tapping goes, and the cant goes with it,
   because a permanently tilted hand with no movement to explain the tilt just
   reads as a glyph someone set crooked. */
@media (prefers-reduced-motion:reduce){
  .qgo-hand{animation:none;transform:none}
}

/* The opening question is a fork rather than a list — two sides, nothing
   shared between them. Two tabs say that at a glance, where the same options
   stacked as rows read as one long menu the reader has to get through. */
.opts.is-tabs{grid-template-columns:1fr 1fr;gap:14px}
.qtab{
  position:relative;
  grid-template-columns:1fr;justify-items:start;
  align-content:space-between;gap:16px;
  min-height:170px;padding:22px 20px 20px;border-radius:18px;
  background:rgba(255,255,255,.06);
}
.qtab svg{width:34px;height:34px;color:var(--gold-2)}
.qtab kbd{position:absolute;top:14px;right:14px}
.qtab .qopt-t b{font-size:19px;letter-spacing:-.03em;line-height:1.15}
.qtab .qopt-t i{margin-top:6px;line-height:1.5}
/* a card this size sliding sideways looks like a mistake; lift it instead */
.qtab:hover{transform:translateY(-4px)}
.qtab.is-picked svg{color:var(--gold-2)}
@media (max-width:560px){
  .opts.is-tabs{grid-template-columns:1fr;gap:12px}
  .qtab{min-height:0;padding:18px;gap:12px}
  .qtab svg{width:28px;height:28px}
}

.qr-eyebrow{
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--gold-2);margin-bottom:var(--s-3);
}
.qr-body{display:grid;grid-template-columns:190px 1fr;gap:var(--s-4);align-items:center}
.qr-media{border-radius:18px;overflow:hidden;box-shadow:0 20px 40px rgba(26,23,32,.34)}
.qr-media img{width:100%;height:auto}
.qr-copy h3{font-size:clamp(19px,2.2vw,26px);margin-bottom:10px}
.qr-copy p{font-size:14.5px;line-height:1.65;color:rgba(255,255,255,.74);margin-bottom:var(--s-3)}
.qr-foot{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.qr-foot .price{color:var(--paper);font-size:24px}
.qr-restart{
  background:none;border:0;cursor:pointer;
  font-size:13px;color:rgba(255,255,255,.62);text-decoration:underline;
  transition:color .3s var(--ease);
}
.qr-restart:hover{color:var(--gold-2)}

/* ---- the plan ----
   The two panels are the reason the finder is worth seven questions instead of
   three: a product name answers "which one", and these answer "and then what",
   which is the question the reader actually walked in with. */
.qr-plan{
  margin-top:var(--s-5);display:grid;grid-template-columns:1fr 1fr;gap:var(--s-4);
}
.qr-panel{
  background:rgba(0,0,0,.16);border:1px solid rgba(255,255,255,.14);
  border-radius:18px;padding:var(--s-3) var(--s-4) var(--s-4);
}
.qr-panel h4{
  font-family:var(--font-display),sans-serif;
  font-size:11.5px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--gold-2);margin:0 0 var(--s-3);
}
.qr-routine,.qr-time{list-style:none;margin:0;padding:0;display:grid;gap:14px}
.qr-routine li,.qr-time li{display:grid;grid-template-columns:96px 1fr;gap:14px;align-items:baseline}
.qr-routine b,.qr-time b{
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  color:rgba(255,255,255,.86);
}
.qr-routine span,.qr-time span{font-size:13.5px;line-height:1.6;color:rgba(255,255,255,.66)}
/* the timeline is ordered and reads as a sequence, so it gets a spine */
.qr-time li{position:relative;padding-left:18px;grid-template-columns:80px 1fr}
.qr-time li::before{
  content:"";position:absolute;left:3px;top:7px;width:7px;height:7px;border-radius:50%;
  background:var(--gold-2);
}
.qr-time li:not(:last-child)::after{
  content:"";position:absolute;left:6px;top:16px;bottom:-14px;width:1px;
  background:rgba(255,255,255,.20);
}

/* ---- the second bottle ----
   Only rendered when the runner-up actually scored close. Deliberately quieter
   than the primary: it is an observation about the answers, not an upsell. */
.qr-second{margin-top:var(--s-4)}
.qr-second-in{
  display:grid;grid-template-columns:62px 1fr auto;gap:var(--s-3);align-items:center;
  background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);
  border-radius:18px;padding:14px 18px;
}
.qr-second-in img{width:62px;height:62px;border-radius:12px;object-fit:cover}
.qr-sec-lbl{
  font-family:var(--font-display),sans-serif;
  font-size:10.5px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:rgba(255,255,255,.52);margin:0 0 3px;
}
.qr-second h4{font-family:var(--font-display),sans-serif;font-size:16px;font-weight:800;letter-spacing:-.02em;margin:0 0 4px;color:var(--paper)}
.qr-second p{font-size:13px;line-height:1.55;color:rgba(255,255,255,.62);margin:0}
/* The neutral button for dark grounds — secondary actions and controls that
   are not purchases. Tokenised like the rest, which also fixes a dead hover:
   it inherited --btn-fill:var(--ink) and so swept near-black up a near-black
   band, i.e. nothing visibly happened. Paper rising with an ink label is the
   same inversion the dark-band primary uses. */
.btn-ghost-light{
  --btn-bg:transparent;--btn-fg:var(--paper);--btn-bd:rgba(255,255,255,.42);
  --btn-fill:var(--paper);--btn-fg-on:var(--ink);
  white-space:nowrap;
}
.btn-ghost-light:hover{border-color:var(--paper)}

/* ---- the answers, given back ----
   A reader who disagrees with the result disagrees with one specific answer.
   Chips are the shortest path from "that is wrong" to "that is fixed" — one
   click back into the exact question, with the rest of the run intact. */
.qr-answers{margin-top:var(--s-4);padding-top:var(--s-4);border-top:1px solid rgba(255,255,255,.14)}
.qr-chips{display:flex;flex-wrap:wrap;gap:8px;margin:var(--s-2) 0 var(--s-3)}
.qchip{
  cursor:pointer;
  font-family:var(--font-body),sans-serif;font-size:12.5px;
  padding:7px 13px;border-radius:999px;
  border:1px solid rgba(255,255,255,.22);background:rgba(255,255,255,.05);
  color:rgba(255,255,255,.76);
  transition:border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease);
}
.qchip:hover{border-color:var(--gold-2);background:rgba(255,201,77,.14);color:var(--paper)}
.qr-fine{margin:var(--s-4) 0 0;font-size:12px;line-height:1.6;color:rgba(255,255,255,.46);max-width:62ch}

@media (max-width:760px){
  .qr-plan{grid-template-columns:1fr}
  .qr-routine li,.qr-time li{grid-template-columns:1fr;gap:3px}
  .qr-time li{grid-template-columns:1fr}
  .qr-second-in{grid-template-columns:52px 1fr;row-gap:12px}
  .qr-second-in img{width:52px;height:52px}
  .qr-second-in .btn{grid-column:1/-1;justify-self:start}
}

/* ============ the primary — the one button that spends money ============
   .btn-solid is now reserved for commerce: add to bag, add the kit, checkout.
   Navigation that happens to be important ("See the range", "See the science")
   stays on the neutral .btn, because if everything is primary nothing is.

   One hue across three grounds, and the ground decides the value, not the
   taste. Each fill below was picked as the lightest/darkest member of the
   violet ramp that still clears 3:1 against what it sits on:

     ground              fill          shape   label
     cream / paper       --violet       6.60    7.10  paper
     bundle band         --violet-dk    3.49    10.95 paper   (worst stop)
     ink bands           --violet-3     6.50    6.50  ink

   The band cases are not decoration. On the gold band --violet falls to
   2.26:1 at the flame stop and on the ink bands to 2.49:1 — both would repeat
   the exact failure this change exists to fix, in a different hue. */
.btn-solid{
  --btn-bg:var(--violet);--btn-fg:var(--paper);--btn-bd:var(--violet);
  --btn-fill:var(--violet-dk);--btn-fg-on:var(--paper);
  box-shadow:0 12px 26px -10px rgba(109,40,217,.5);
}
.btn-solid:hover{border-color:var(--violet-dk);box-shadow:0 16px 34px -12px rgba(109,40,217,.6)}

/* The offer band sweeps gold-2 -> gold -> flame under this button, so the fill
   has to clear the floor at every stop, not at the average. --violet-dk holds
   7.15 / 5.05 / 3.49. The sweep goes to ink rather than deeper violet — there
   is nothing below --violet-dk that stays recognisably violet. */
.bundle .btn-solid{
  --btn-bg:var(--violet-dk);--btn-bd:var(--violet-dk);--btn-fill:var(--ink);
  box-shadow:0 14px 30px -12px rgba(26,23,32,.46);
}
.bundle .btn-solid:hover{border-color:var(--ink);box-shadow:0 18px 38px -14px rgba(26,23,32,.56)}

/* Dark bands invert up the ramp instead of down: --violet-3 carries an ink
   label at 6.50:1 and reads as the same buy colour one lightness step over. */
.quiz .btn-solid,.science .btn-solid,.scan .btn-solid{
  --btn-bg:var(--violet-3);--btn-fg:var(--ink);--btn-bd:var(--violet-3);
  --btn-fill:var(--paper);--btn-fg-on:var(--ink);
  box-shadow:0 12px 28px -12px rgba(0,0,0,.55);
}
.quiz .btn-solid:hover,.science .btn-solid:hover,.scan .btn-solid:hover{border-color:var(--paper)}

/* ---- added ----
   Confirmation has to read as a CHANGE, not merely as a colour. Violet is the
   resting state now, so the old violet "added" style would have been a no-op —
   the button flashing to the colour it already was. Ink is 2.49:1 away from
   resting violet and 16.4:1 on cream, so the swap is unmistakable while the
   button stays the same object. On the dark bands ink would vanish, so those
   inverted to paper. */
.btn.added{
  --btn-bg:var(--ink);--btn-fg:var(--paper);--btn-bd:var(--ink);
  --btn-fg-on:var(--paper);
  box-shadow:none;
}
.quiz .btn.added,.science .btn.added,.scan .btn.added{
  --btn-bg:var(--paper);--btn-fg:var(--ink);--btn-bd:var(--paper);--btn-fg-on:var(--ink);
}
.btn.added::after{display:none}

/* ---- order on WhatsApp ----
   The kit is the one thing on this site that is not checked out, so its second
   CTA is an <a href="wa.me/..."> rather than a <button>. Two consequences.

   Layout: an anchor is display:inline, so .btn's vertical padding would not
   have opened it to the height of the violet button beside it. Both of its
   homes (.kitband-row, .bundle-foot) are flex containers, which blockifies it
   for free — but the glyph and the label still need a box of their own to
   share a centre line, so it gets one here. inline-flex rather than flex
   because that computes to flex as a flex item and still behaves if this
   button is ever dropped somewhere that is not one.

   Colour: it inherits the plain .btn ink outline and deliberately does NOT
   take #25D366. That reasoning originally leaned on .wa-float already owning
   the green; the float is gone, but the rule stands on its own and the ink
   outline stays — a second saturated fill standing beside the violet Add The
   Kit would leave the block with two primaries and no hierarchy, and the
   band's whole job is to sell the bag first and offer the WhatsApp route
   second. The glyph carries the recognition without help from the fill. */
.kitband-wa,.bundle-wa{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
}
/* the glyph must not absorb the row's leftover width, and it has to paint
   above .btn::after, which sits at z-index:-1 inside the button's own stack */
.kitband-wa svg,.bundle-wa svg{flex:none;position:relative}

/* ============ bundle — gold band ============ */
/* The kit band is the one big warm field on the page, and flat amber into a
   darker amber (#C4820C) made it read as mustard — the weakest colour moment
   here. Running the ramp out to --flame instead turns the same band into a
   sunset, and it earns its warmth: this is the offer block, so heat is the
   right register. All three stops carry the ink text safely — 12.30:1 on
   gold-2, 8.68:1 on gold, 6.00:1 on flame — so the copy never rides a stop
   that fails as the gradient sweeps under it. */
.bundle{
  background:linear-gradient(150deg,var(--gold-2) 0%,var(--gold) 46%,var(--flame) 104%);
  color:var(--ink);padding:var(--s-7) 0;
}
.bundle-inner{
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:1fr 1fr;gap:clamp(28px,4vw,64px);align-items:center;
}
.bundle .eyebrow{color:var(--violet)}
/* the kit name holds one line. At the old 62px cap it measured 601px into a
   550px column and broke at the hyphen, so the cap comes down to 54 and the
   vw slope to 4.3 — the tightest two-column case is 1025px, just above the
   breakpoint where .bundle-inner is still 1fr 1fr. nowrap keeps the name on
   one line even when a font swaps in wider than Figtree; measured at the
   narrowest case, 375px, it sets to 343px against the 343px it has. */
.bundle h2{font-size:clamp(28px,4.3vw,54px);line-height:.98;white-space:nowrap}
.bundle .sec-sub{color:rgba(26,23,32,.74)}
.bundle-foot{margin-top:var(--s-4);display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap}
.bundle .price{font-size:28px}
.bundle .mrp{color:rgba(26,23,32,.52)}
.bundle .off{
  background:var(--ink);color:var(--gold-2);
  padding:5px 11px;border-radius:999px;font-size:11.5px;
}
/* the three tiles butt together into one panel: the radius and the drop
   shadow move to the container so the seams stay flat and only the outer
   corners round. display:block on the images kills the inline baseline gap,
   which would otherwise leak a line of gold band inside the panel. */
.bundle-media{
  border-radius:18px;overflow:hidden;
  box-shadow:0 22px 44px rgba(26,23,32,.26);
}
.kitpick{display:grid;grid-template-columns:repeat(3,1fr);gap:0}
.kitpick li{position:relative;min-width:0}
/* The whole cell is the target, not the picture and not the name. A tile that
   only responds on its image leaves a dead strip under it that looks clickable
   and is not, and the caption is the part carrying the name — which is the
   word a reader is aiming at when they wonder what the middle bottle is. */
.kitpick a{
  display:flex;flex-direction:column;height:100%;
  text-decoration:none;color:inherit;
  background:var(--paper);
}
/* Clipped by the cell, not by the image, so the scale below has somewhere to
   go: the picture grows past its own box and the box crops it, which reads as
   a push-in. Scaling the box instead would shove the neighbours. */
.kitpick a > img{
  display:block;width:100%;height:auto;
  transition:transform .55s var(--ease);
}
.kitpick li{overflow:hidden}

/* The caption band. The three strips butt into one continuous bar across the
   foot of the panel, which is what keeps this reading as a single object with
   three compartments rather than three cards that happen to touch. Ink rather
   than a gradient scrim: a scrim is for text sitting ON an image, and this
   sits BELOW one, where a hard edge is honest about where the photo stops. */
.kitpick-cap{
  display:block;margin-top:auto;
  padding:11px 12px 12px;
  background:var(--ink);color:var(--paper);
  transition:background .35s var(--ease);
}
.kitpick-cap b{
  display:block;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(11.5px,1.05vw,13.5px);font-weight:800;
  letter-spacing:-.015em;line-height:1.2;
}
.kitpick-cap i{
  display:block;margin-top:3px;font-style:normal;
  font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--paper-40);
}
.kitpick-cap em{
  display:block;margin-top:7px;font-style:normal;
  font-size:clamp(13px,1.25vw,16px);font-weight:900;
  color:var(--gold-2);
  transition:color .35s var(--ease);
}

/* Dim the set, undim the one. Written as "list hovered" + "item hovered"
   rather than a sibling combinator because `~` only reaches forwards, so the
   first tile could never dim the two after it. :focus-within carries the same
   treatment to the keyboard, where these are three ordinary tab stops. */
.kitpick:hover li,
.kitpick:focus-within li{opacity:.62;transition:opacity .35s var(--ease)}
.kitpick li:hover,
.kitpick li:focus-within{opacity:1}
.kitpick li:hover a > img,
.kitpick li:focus-within a > img{transform:scale(1.07)}
.kitpick li:hover .kitpick-cap,
.kitpick li:focus-within .kitpick-cap{background:var(--flame-dk)}
.kitpick li:hover .kitpick-cap em,
.kitpick li:focus-within .kitpick-cap em{color:var(--paper)}
.kitpick a:focus-visible{outline:3px solid var(--ink);outline-offset:-3px}

/* The arithmetic, stated rather than asserted. "Save ₹498" on the left is a
   claim; this is the working behind it, and it is here rather than up there
   because it belongs next to the three prices it adds up. Sits inside the
   clipped panel as its own band so the panel still ends on one clean edge. */
.kitsum{
  margin:0;padding:11px 14px;
  background:var(--ink);color:var(--paper-64);
  border-top:1px solid rgba(255,255,255,.14);
  font-size:12.5px;line-height:1.5;text-align:center;
}
.kitsum b{color:var(--paper);font-weight:800}
.kitsum [data-kit-sum]{white-space:nowrap}

/* ============ the science orb ============ */
/* A doorway to science.html, sat between the gold bundle band and the paper
   FAQ. Cream, because those two neighbours are the loudest and the quietest
   ground on the page and something has to sit between them.

   The whole thing is one <a>. The brief was that interacting with it — or even
   reaching it — should take you to the science page, and a band that only
   navigates from a small button in its corner does not honour that. What it is
   NOT is a scroll-triggered redirect: navigating a reader somewhere they did
   not click steals the back button and traps them, because going back just
   re-enters the trigger. So the whole surface is the target, the cursor says so
   on every pixel of it, and the choice stays the reader's.

   The circle itself is shared with the science page's hero prop — every rule
   below is written .orb-x,.dish-x and the two are the same object. They used to
   be separate "so each could be tuned without breaking the other", and what
   actually happened is that they drifted: this one got taken back to clear
   glass while the science page was left a violet-and-gold ball with fizz rising
   through it. Two copies of a thing nobody remembers to update twice is not
   flexibility. The only differences kept are the two that are real — size, and
   the fact that only this one tracks a pointer. */
.orb{background:var(--cream);padding:var(--s-7) 0}
.orb-link{
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:1fr auto;align-items:center;
  gap:clamp(28px,5vw,80px);
  color:inherit;text-decoration:none;cursor:pointer;
}
.orb-copy h2{margin:0}
.orb-copy .sec-sub{margin-top:var(--s-2)}

/* The cue is not a .cta. A real button inside a fully clickable band invites
   the reader to hunt for "the" hit area, which is the opposite of the point —
   this reads as a link and underlines on hover, and the band carries the click.
   The arrow is its own span so only it moves; animating the whole line makes
   the text jitter against the paragraph above it. */
.orb-cue{
  margin-top:var(--s-4);
  display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;color:var(--ink);
}
.orb-cue::after{
  content:"";width:22px;height:1.6px;background:var(--ink);
  transition:width .45s var(--ease);
}
.orb-link:hover .orb-cue::after,
.orb-link:focus-visible .orb-cue::after{width:38px}
.orb-link:focus-visible{outline:2px solid var(--violet);outline-offset:10px;border-radius:24px}

.orb-stage{
  /* the pointer wiring writes --ox/--oy here; both default to 0 so the band is
     correct with the script absent or on a coarse pointer */
  --ox:0px;--oy:0px;
  position:relative;display:grid;place-items:center;
  perspective:900px;
}
.orb-dish,.dish{
  --dish:clamp(230px,30vw,420px);
  position:relative;width:var(--dish);height:var(--dish);
  border-radius:50%;overflow:hidden;
  /* --ox/--oy are written by the pointer wiring on index and never exist on the
     science page, hence the 0px fallbacks — without them the whole declaration
     would be invalid at computed-value time on science.html and drop out. It
     resolves to a no-op there anyway, and .reveal.in{transform:none} overrides
     it outright once the hero has settled, which is fine: nothing on that page
     needs the circle to move. Compositing is not this rule's job either — each
     bubble carries its own will-change. */
  transform:translate3d(var(--ox,0px),var(--oy,0px),0);
  /* transform is deliberately NOT in this transition: --ox/--oy are already
     eased frame by frame by the lerp in script.js, and a .5s transition on top
     of that smooths an already-smooth value — the circle then trails the cursor
     by half a second and feels broken rather than soft. box-shadow gets it
     because nothing animates box-shadow per frame. */
  transition:box-shadow .5s var(--ease);
  /* Neutral on purpose. Both of these used to carry a violet wash and a gold
     one, and two saturated lights inside a single sphere is what made them read
     as pearlescent toys rather than instruments -- the hue did the talking and
     the structure underneath was invisible. Glass in a lab is colourless; the
     only light in here now is white, so the graduations, the reticle and the
     bubbles are what the eye lands on. The brand colour arrives on hover, as
     one hairline, and that is the whole of it. */
  background:
    radial-gradient(circle at 33% 22%, rgba(255,255,255,.98), transparent 42%),
    radial-gradient(circle at 72% 82%, rgba(26,23,32,.10), transparent 62%),
    linear-gradient(158deg,#FFFFFF,var(--cream-2));
  box-shadow:
    inset 0 0 0 1.5px var(--ink-08),
    inset 0 -30px 60px rgba(26,23,32,.09),
    0 40px 90px rgba(26,23,32,.14);
}
.orb-link:hover .orb-dish{box-shadow:
  inset 0 0 0 1.5px var(--ink-14),
  inset 0 -30px 60px rgba(26,23,32,.06),
  0 52px 110px rgba(26,23,32,.20);
}
/* The rim. Violet only lands here, only on hover, and only as a hairline —
   it is the one place the science page's colour is allowed to show through,
   because this ring is the thing the reader is being invited to click. */
.orb-ring,.dish-ring{
  position:absolute;inset:5%;border-radius:50%;
  border:1px solid var(--ink-08);pointer-events:none;
  transition:border-color .5s var(--ease);
}
.orb-link:hover .orb-ring{border-color:rgba(109,40,217,.34)}

/* Barrel graduations. One element and one gradient rather than 72 ticks: the
   conic repeats every 5deg and the mask keeps only a thin annulus of it, so
   the whole ring costs a single composited layer. It turns once every 90s —
   slow enough that it never reads as a spinner, present enough that the glass
   is obviously live when you stop and look at it. */
.orb-ticks,.dish-ticks{
  position:absolute;inset:3%;border-radius:50%;pointer-events:none;
  background:repeating-conic-gradient(from 0deg,
    var(--ink-46) 0 .5deg, transparent .5deg 5deg);
  /* closest-side, not the default farthest-corner: on a square box the corner
     radius is 0.707 of the side, so a 91% stop lands well outside the circle
     the border-radius is clipping to and the annulus disappears entirely.
     Against the side, 91% sits just inside the rim where graduations belong. */
  -webkit-mask:radial-gradient(circle closest-side at 50% 50%, transparent 0 90%, #000 90.5% 100%);
          mask:radial-gradient(circle closest-side at 50% 50%, transparent 0 90%, #000 90.5% 100%);
  opacity:.62;
  animation:orbTicks 90s linear infinite;
}
/* The orb's ring waits: its band sits deep in the page and there is no reason
   to turn a graduation nobody has scrolled to yet. The science hero's is on
   screen at load, so it just runs. */
.orb-ticks{animation-play-state:paused}
.orb.is-awake .orb-ticks{animation-play-state:running}
@keyframes orbTicks{to{transform:rotate(360deg)}}

/* The reticle does NOT turn. On a real instrument the barrel is what moves and
   the measuring axis is what stays put, and rotating the crosshair too would
   turn the whole thing into a loading spinner. */
.orb-grat,.dish-grat{
  position:absolute;inset:14%;border-radius:50%;
  border:1px dashed var(--ink-14);pointer-events:none;
}
.orb-grat::before,.orb-grat::after,
.dish-grat::before,.dish-grat::after{content:"";position:absolute;background:var(--ink-08)}
.orb-grat::before,.dish-grat::before{left:50%;top:-7%;bottom:-7%;width:1px}
.orb-grat::after,.dish-grat::after{top:50%;left:-7%;right:-7%;height:1px}

/* the pointer's own light. --mx/--my are 0..100% within the dish; at rest the
   script parks them at 50/50 so this is just a soft centre bloom. */
.orb-glow{
  position:absolute;inset:0;border-radius:50%;pointer-events:none;
  background:radial-gradient(circle at var(--mx,50%) var(--my,50%),
    rgba(255,255,255,.92), rgba(255,255,255,.12) 17%, transparent 38%);
  opacity:0;transition:opacity .5s var(--ease);
}
.orb-link:hover .orb-glow{opacity:1}

/* ---- the bubbles ----
   Suspended, not rising. Fizz — round things racing to the top and popping out
   of frame — is the carbonated-drink cue, and it is the single thing that made
   the old science-page circle read as bubble tea rather than as a sample. Air
   trapped in something as viscous as a serum barely rises at all: it hangs, it
   drifts a couple of percent, and it turns over on a timescale you notice only
   if you stop and watch it. So nothing here is ever born and nothing pops. The
   field you see at 0s is the field you see at 30s, slightly rearranged.

   Each bubble is drawn the way air in liquid actually reads on a pale ground:
   the thing that defines it is a fine DARK refraction rim, not a light one. The
   old ones were filled white blobs with a violet and a gold inner shadow, which
   on a near-white sphere left them both invisible and, where they did show,
   plastic. Here the body is nearly clear, one specular dot sits where the
   sphere's own light is (upper left, matching the 33%/22% highlight above), a
   weaker caustic sits opposite it, and a hairline of ink closes the edge. */
.bub{position:absolute;inset:0;pointer-events:none}
.bub i{
  position:absolute;left:var(--x);top:var(--y);
  /* Sized as a FRACTION of the circle, never in px. The old field was authored
     in px, so on a phone the circle halved and the bubbles did not — a 78px
     bubble in a 230px dish. Everything below scales with --dish, so the same
     ten bubbles compose identically at 420 and at 200. */
  width:calc(var(--dish) * var(--s));height:calc(var(--dish) * var(--s));
  margin:calc(var(--dish) * var(--s) * -.5) 0 0 calc(var(--dish) * var(--s) * -.5);
  border-radius:50%;
  /* The body is kept nearly clear and the specular kept tight. Widen either and
     the largest bubbles fill in and start reading as pearls — which is the same
     failure as the old white blobs, just better lit. What should carry a bubble
     is its edge, so the ink hairline is the strongest thing here. */
  background:
    radial-gradient(circle at 33% 29%, rgba(255,255,255,.92), rgba(255,255,255,0) 30%),
    radial-gradient(circle at 71% 75%, rgba(255,255,255,.55), rgba(255,255,255,0) 26%),
    radial-gradient(circle closest-side, rgba(255,255,255,.22), rgba(255,255,255,.04));
  box-shadow:
    inset 0 0 0 1px rgba(26,23,32,.13),
    inset 0 -3px 6px -2px rgba(26,23,32,.07),
    0 2px 6px -3px rgba(26,23,32,.13);
  opacity:var(--o,.8);
  animation:bubDrift var(--t,22s) ease-in-out var(--d,0s) infinite;
  will-change:transform;
}
/* One authored keyframe, not three. With only 50% written the browser fills 0%
   and 100% from the element's own transform, so each bubble eases out to its
   own --bx/--by and back again without a from/to pair repeated per bubble — and
   because the two ends are the same value the loop has no seam to catch. */
@keyframes bubDrift{
  50%{transform:translate3d(
        calc(var(--dish) * var(--bx,.012)),
        calc(var(--dish) * var(--by,-.02)),0)}
}

/* The sweeping white band that used to cross both circles is gone: a highlight
   travelling over a sphere is the one gesture that says "shiny ball". Glass
   does not do that. It holds a fixed crescent where the light is, which is what
   this is, and the only thing that moves across it is the reader's pointer.

   The crescent is two inset shadows rather than a gradient because what makes
   glass look like glass is the rim light on the FAR side -- light that went
   through the body and caught the inside of the opposite edge. A radial blob in
   the middle cannot say that; an inset shadow hugging the circumference can.
   The dark one on the near rim is the same effect inverted, and together they
   give the circle its roundness without a drop of colour. */
.orb-edge,.dish-edge{
  position:absolute;inset:0;border-radius:50%;pointer-events:none;
  box-shadow:
    inset -6px -9px 20px -9px rgba(255,255,255,.95),
    inset 6px 8px 22px -12px rgba(26,23,32,.20);
}

/* Below 900 the two columns stop fitting: the dish's 30vw floor is 230px and
   the copy needs about 420 to hold "See What Is Working." on two lines, which
   is 650 plus the gap before the padding is counted. Stacked, the dish leads —
   it is the thing that says "there is more here", and burying it under a
   paragraph on the screen where scrolling costs most gets that backwards. */
@media (max-width:900px){
  .orb{padding:var(--s-6) 0}
  .orb-link{grid-template-columns:1fr;justify-items:center;text-align:center;gap:var(--s-4)}
  .orb-stage{order:-1}
  .orb-dish{--dish:clamp(200px,58vw,300px)}
  .orb-cue{margin-top:var(--s-3)}
}

/* ============ the teardown ============ */
/* THE HAIR SERUM PDP ONLY. It was built for index.html and carried a second,
   stale copy at the foot of shop.html; both are gone. The markup lives in
   product-hair-serum.html and nothing else loads serum.js, but every selector
   below is still written page-agnostically — it is scoped by where the markup
   is, not by a body class, so dropping the section onto another product page
   is a copy-paste and a script tag rather than a stylesheet edit.

   It is dark because the product is a frosted bottle photographed on black —
   putting it on cream would mean either a halo or a recut, and the artwork is
   not to be recut. On a PDP that also does the job the section needs doing:
   every other block on that page is white, so the one ink band is the page
   telling the reader, before they read a word of it, that this bit is
   different.

   Geometry. .td-rig is an aspect-ratio box the exact shape of the source file
   (492x1542). Every part is absolutely inset to the FULL rig and carries its
   slice as an absolutely positioned <img> at the slice's original offset. That
   indirection is the whole trick: because each .td-part is rig-sized, a
   translateY on it is a percentage of the RIG, not of the part, so one number
   means the same distance whichever part it is written on. Stacked at rest the
   three slices are the source photograph, unmodified. */
/* `clip`, not `hidden`. They look identical here and they are not: `hidden`
   makes the section a scroll container, and a scroll container is exactly what
   position:sticky measures itself against — so the pin below would silently
   stop pinning and the whole section would just scroll past assembled. `clip`
   crops without creating that container, which is the reason it exists. */
.teardown{background:var(--ink);color:var(--paper);overflow:clip}
/* Scroll budget, and nothing else. Three screens is what it takes for the parts
   to separate at a rate the eye can follow without the section feeling like it
   has taken the page hostage — roughly a second and a half of ordinary wheel
   scrolling from closed to open. */
.td-track{position:relative;height:300vh}
.td-inner{
  position:sticky;top:0;min-height:100svh;box-sizing:border-box;
  max-width:var(--maxw);margin:0 auto;
  /* Top clears #nav, which is sticky at top:0 and 68px tall once stuck, so the
     content is centred in what is left rather than pushed below it. Bottom
     clears .nx-buybar, which is position:fixed and roughly 68px tall — it is
     not in the flow, so nothing else on the page has to think about it, but
     this is the one block that pins a full viewport against the bottom edge
     and would otherwise centre its lowest row underneath the bar. */
  padding:calc(68px + var(--s-4)) var(--pad) calc(68px + var(--s-2));
  display:grid;grid-template-columns:1fr .82fr;align-items:center;
  align-content:center;
  gap:clamp(28px,5vw,72px);
}
.td-copy h2{margin:0;color:var(--paper)}
.td-copy .eyebrow{color:var(--gold)}
.td-copy .sec-sub{margin-top:var(--s-2);color:var(--paper-64)}

.td-spec{margin:var(--s-4) 0 0;display:grid;gap:1px;background:var(--paper-16);border:1px solid var(--paper-16)}
.td-spec > div{display:grid;grid-template-columns:104px 1fr;gap:var(--s-2);background:var(--ink);padding:12px 14px}
.td-spec dt{
  font-family:var(--font-display),sans-serif;
  font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--paper-40);align-self:center;
}
.td-spec dd{margin:0;font-size:14px;color:var(--paper);align-self:center}

.td-controls{margin-top:var(--s-4);display:flex;flex-wrap:wrap;align-items:center;gap:12px 18px}

/* The readout. A scroll-driven section has a real problem a button does not:
   nothing tells the reader they are the one doing it. This is the smallest
   honest answer — a rail that fills exactly as far as the bottle has come
   apart, so the connection between the wheel and the glass is stated rather
   than hoped for. */
.td-scrub{display:flex;align-items:center;gap:10px;min-width:150px}
.td-scrub-rail{
  position:relative;flex:1 1 auto;height:2px;border-radius:2px;
  background:var(--paper-16);overflow:hidden;
}
.td-scrub-rail i{
  position:absolute;inset:0 auto 0 0;display:block;
  width:calc(var(--pa,0) * 100%);
  background:var(--gold);
}
.td-scrub-lbl{
  flex:0 0 auto;
  font-family:var(--font-display),sans-serif;
  font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--paper-40);
  /* three words of different lengths swapping in place would nudge the rail
     around; reserving the width of the longest keeps it still */
  min-width:9ch;
}
.td-btn{
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.01em;
  padding:13px 26px;border-radius:999px;border:1.5px solid transparent;
  background:var(--paper);color:var(--ink);cursor:pointer;
  transition:transform .3s var(--ease), background .3s var(--ease), border-color .3s var(--ease), opacity .3s var(--ease);
}
.td-btn:hover{transform:translateY(-2px)}
/* .td-ghost lived here and is deleted. It was the outline treatment for the
   second button in this row — "Take It Apart" — and that button stopped
   existing when the dismantle became scroll-driven. The only markup still
   carrying it was shop.html's stale copy of this section, which is also gone.
   One button left, and it is the solid one. */
.td-btn:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
/* Spraying an already-dismantled bottle is not a thing, so the control says so
   rather than quietly doing nothing. */
.td-btn[disabled]{opacity:.34;cursor:not-allowed;transform:none}
.td-note{
  margin:var(--s-2) 0 0;font-size:12px;color:var(--paper-40);
}
@media (hover:none),(pointer:coarse){.td-note{display:none}}

.td-stage{position:relative}
/* Tall rather than square: the actuator travels a quarter of the rig upward and
   the labels need somewhere to be. 1:1.16 leaves headroom above the pump that
   is empty at rest and full the moment it comes apart. */
.td-scene{
  position:relative;width:100%;aspect-ratio:1/1.16;
  perspective:1400px;
  background:
    radial-gradient(58% 42% at 50% 88%, rgba(255,255,255,.10), transparent 70%),
    radial-gradient(46% 34% at 42% 26%, rgba(255,255,255,.07), transparent 72%);
}
.td-rig{
  position:absolute;left:50%;bottom:8%;height:70%;aspect-ratio:492/1542;
  transform:translateX(-50%) rotateY(var(--ry,0deg));
  transform-style:preserve-3d;
  will-change:transform;
}
@media (hover:hover) and (pointer:fine){.td-rig{cursor:grab}}
.td-rig.is-drag{cursor:grabbing}

/* No transition on the parts any more, and that is the point. A transition is
   the right tool for "go from A to B in your own time"; this is "be exactly
   where the reader has put you", and a duration on top of that is just lag
   between the wheel and the glass. serum.js already eases the progress value
   itself, so the smoothing happens once, upstream, instead of twice. */
.td-part{
  position:absolute;inset:0;transform-style:preserve-3d;
  transform:translate3d(0,0,0);
}
.td-part img{position:absolute;left:0;width:100%;display:block;user-select:none}
/* the three slice offsets, straight out of the crop: 266, 263 and 1013 rows of
   a 1542-row original */
.td-p-act img{top:0;height:17.2503%}
.td-p-col img{top:17.2503%;height:17.0558%}
.td-p-bot img{top:34.3061%;height:65.6939%}

/* Staggered so it reads as a sequence rather than a shatter: the pump lifts
   clear first, the collar follows it off the neck a beat later. That used to be
   two transition-delays; on a scrub there is no delay to set, so the stagger is
   carried by the numbers instead — serum.js hands each part its own slice of
   the scroll (--pa opens over the first two thirds, --pc starts a fifth of the
   way in), and reversing the scroll reverses the order for free because the
   windows are just read backwards.

   The Y offsets are percentages of .td-part, which is rig-sized via inset:0, so
   -24% is 24% of the whole bottle however wide the stage happens to be. Same
   trick as the slice offsets above; the Z is px because a percentage is
   meaningless there. */
.td-p-act{transform:translate3d(0, calc(var(--pa,0) * -24%), calc(var(--pa,0) * 58px))}
.td-p-col{transform:translate3d(0, calc(var(--pc,0) * -10%), calc(var(--pc,0) * 26px))}

/* The dose. Overrides the delay above so the head answers the click on the same
   frame — a pump that pauses before it moves feels broken, and this is the one
   gesture in the section the reader performs rather than watches. */
.td-rig.is-press .td-p-act{
  transform:translate3d(0,1.5%,0);
  transition:transform .11s ease-out;transition-delay:0s;
}

/* Not photographed, and it cannot be: a dip tube is invisible through frosted
   glass. It is the only invented element in the section and it is labelled as a
   dip tube, so nothing here pretends the photo shows something it does not.

   It hangs inside .td-p-col, which is rig-sized like every part, so its top is
   given in rig percent: 34.31% is exactly where the collar's crop ends and the
   glass shoulder begins. 22% of the rig is long enough to read as a tube and
   short enough that, once the collar has lifted, it stops above the wordmark
   rather than crossing it. */
.td-tube{
  position:absolute;left:50%;top:34.3061%;width:2.6%;height:22%;
  transform:translateX(-50%);
  border-radius:0 0 40% 40%/0 0 4% 4%;
  background:linear-gradient(90deg,
    rgba(255,255,255,.06), rgba(255,255,255,.30) 34%, rgba(255,255,255,.10) 62%, rgba(255,255,255,.04));
  /* fades in on its own slice of the scroll, after the collar has actually
     cleared the neck — a tube visible through a seated pump would be a lie */
  opacity:var(--pt,0);
}

/* the pump head is the hit area for a spray; the buttons carry the accessible
   path, so this is a pointer affordance only */
.td-press{position:absolute;left:18%;right:18%;top:0;height:17.2503%;cursor:pointer}
@media (hover:none),(pointer:coarse){.td-press{display:none}}

.td-mist{position:absolute;inset:0;pointer-events:none;overflow:hidden}
/* Droplets, not blobs. A hair serum leaves the nozzle as a fine mist and the
   whole point of the brief was that it is not a cream and does not drip, so
   these are small, cool, translucent and short-lived — they disperse upward and
   outward and never fall. */
.td-drop{
  position:absolute;top:0;left:0;border-radius:50%;
  background:radial-gradient(circle at 36% 32%, rgba(255,255,255,.92), rgba(255,255,255,.34) 52%, rgba(255,255,255,0) 74%);
  will-change:transform,opacity;
}

.td-labels{list-style:none;margin:0;padding:0;position:absolute;inset:0;pointer-events:none}
.td-lbl{
  position:absolute;left:64%;
  opacity:0;transform:translateX(-10px);
  transition:opacity .5s var(--ease), transform .5s var(--ease);
}
.td-lbl b{
  display:block;
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:-.01em;color:var(--paper);
}
.td-lbl span{display:block;font-size:11.5px;color:var(--paper-40);margin-top:1px}
/* a hairline back to the part it names */
.td-lbl::before{
  content:"";position:absolute;right:100%;top:9px;width:clamp(18px,4vw,42px);height:1px;
  background:var(--paper-16);
}
.td-scene.is-open .td-lbl{opacity:1;transform:translateX(0)}
.td-scene.is-open .td-l-act{transition-delay:.34s}
.td-scene.is-open .td-l-col{transition-delay:.44s}
.td-scene.is-open .td-l-tub{transition-delay:.54s}
.td-scene.is-open .td-l-bot{transition-delay:.64s}
/* stage-space centres of each part once open — see the rig maths above */
.td-l-act{top:9%}
.td-l-col{top:31%}
.td-l-tub{top:47%}
.td-l-bot{top:70%}

/* Below 900 the pin comes off entirely. Stacked, this section is taller than a
   phone screen on its own, so pinning it would mean either cropping the copy or
   shrinking the bottle to a thumbnail — and a pin that has to hide half its
   content to hold still is not worth holding still for. Unpinned, the section
   simply scrolls and serum.js measures the stage's travel across the viewport
   instead of the track's; the bottle still comes apart on scroll, it just does
   it while moving. The script picks the branch by asking whether .td-inner
   computes to sticky, so this one declaration is what switches it. */
@media (max-width:900px){
  .td-track{height:auto}
  .td-inner{
    position:static;min-height:0;
    padding:var(--s-6) var(--pad);
    grid-template-columns:1fr;gap:var(--s-4);
  }
  /* the stage leads on a phone for the same reason the orb's dish does: it is
     the thing that says there is something to do here */
  .td-stage{order:-1;max-width:340px;margin:0 auto;width:100%}
  .td-copy{text-align:center}
  .td-spec > div{grid-template-columns:88px 1fr;text-align:left}
  .td-controls{justify-content:center}
  .td-scrub{flex:0 0 auto;width:min(240px,70vw)}
  /* Hidden from the picture, not from the page. There is no room beside a
     340px stage for four callouts, but `display:none` would take these four
     facts out of the accessibility tree as well — and on a phone the diagram is
     the one thing a screen reader user cannot fall back on. Clipped to a pixel
     keeps them readable and keeps them out of the way. */
  .td-labels{
    inset:auto;width:1px;height:1px;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap;
  }
}

/* ============ faq ============ */
.faq{padding:var(--s-7) 0;background:var(--paper)}
.faq-inner{max-width:920px;margin:0 auto;padding:0 var(--pad)}
.faq-head{margin:0 0 var(--s-5);padding:0;max-width:none}
.faq-list{display:grid;gap:10px}
.faq-item{
  border:1px solid var(--ink-08);border-radius:18px;
  background:var(--cream);overflow:hidden;
  transition:border-color .4s var(--ease), background .4s var(--ease);
}
.faq-item.open{border-color:rgba(109,40,217,.32);background:var(--violet-lt)}
.faq-q{
  width:100%;cursor:pointer;border:0;background:transparent;text-align:left;
  display:flex;align-items:center;justify-content:space-between;gap:18px;
  padding:20px 22px;
  font-family:var(--font-display),sans-serif;
  font-size:16.5px;font-weight:800;letter-spacing:-.025em;
}
.faq-q i{position:relative;width:16px;height:16px;flex:0 0 auto}
.faq-q i::before,.faq-q i::after{
  content:"";position:absolute;background:var(--violet);border-radius:2px;
  transition:transform .45s var(--ease);
}
.faq-q i::before{left:0;top:7px;width:16px;height:2px}
.faq-q i::after{left:7px;top:0;width:2px;height:16px}
.faq-item.open .faq-q i::after{transform:scaleY(0)}
/* 0fr→1fr animates the row box; the child clips its own padding while closed.
   minmax(0,…) is load-bearing: a bare `0fr` track keeps an automatic minimum of
   min-content, and the child's 22px bottom padding counts toward that — so the
   closed row measured 22px and every item leaked a band of dead space. */
.faq-a{display:grid;grid-template-rows:minmax(0,0fr);transition:grid-template-rows .5s var(--ease)}
.faq-item.open .faq-a{grid-template-rows:minmax(0,1fr)}
.faq-a > p{
  overflow:hidden;min-height:0;
  padding:0 22px 22px;
  font-size:15px;line-height:1.72;color:var(--ink-64);
}

/* ============ split text ============ */
.sp-l{display:block}
/* the mask needs room for descenders or lowercase p/q/y get sliced */
.sp-w{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.22em;margin-bottom:-.22em}
.sp-c{
  display:inline-block;
  transform:translateY(110%) rotate(4deg);
  opacity:0;
}
.js .sp-c{transition:transform 1s var(--ease), opacity .6s linear;transition-delay:calc(var(--i,0) * 22ms)}
.in .sp-c,.sp-go .sp-c{transform:none;opacity:1}

/* ============ reveal ============ */
.js .reveal{opacity:0;transform:translateY(34px);transition:opacity 1.1s var(--ease), transform 1.1s var(--ease)}
.js .reveal.in{opacity:1;transform:none}
.js .reveal[data-d="1"]{transition-delay:.12s}
.js .reveal[data-d="2"]{transition-delay:.24s}

/* ============ keyframes ============ */
@keyframes bottleIn{
  0%  {opacity:0;transform:translateY(60px) scale(.92)}
  100%{opacity:1;transform:none}
}
/* The group rises as one via bottleIn; this settles each product on top of that.
   Purely a scale-and-fade, identical for all three — it used to slide the
   flanks in from +/-.2gh so they "stepped out from behind" the serum, which
   staged the serum as the subject and the other two as its entourage. */
@keyframes prodIn{
  0%  {opacity:0;transform:scale(.94)}
  100%{opacity:1;transform:none}
}
/* proportional to the group, not a flat 14px: at a fixed distance the 125px jar
   bobbed through 11% of its own height while the 300px bottles moved 4.6%, so
   the jar looked like the only restless one in the row */
@keyframes float{
  0%,100%{transform:translateY(0)}
  50%    {transform:translateY(calc(var(--gh) * -.045))}
}
@keyframes contact{
  0%,100%{transform:translateX(-50%) scale(1);opacity:.9}
  50%    {transform:translateX(-50%) scale(.88);opacity:.6}
}
@keyframes fade{to{opacity:1}}
@keyframes drip{
  0%,100%{transform:scaleY(.4);transform-origin:50% 0;opacity:.35}
  50%    {transform:scaleY(1);  transform-origin:50% 0;opacity:1}
}
@keyframes qIn{
  0%  {opacity:0;transform:translateY(14px)}
  100%{opacity:1;transform:none}
}
@keyframes flipIn{
  0%  {opacity:0;transform:translateY(26px) scale(.96)}
  100%{opacity:1;transform:none}
}

/* ============ interior pages ============ */
/* These pages open straight onto a coloured band, so the nav never gets a
   stretch of paper to be transparent over — it rests solid instead of .stuck. */
.page-cart #nav,
.page-sub #nav{
  background:rgba(255,255,255,.88);
  backdrop-filter:saturate(150%) blur(16px);
  -webkit-backdrop-filter:saturate(150%) blur(16px);
  border-bottom-color:var(--ink-08);
}
/* the rounded top edge and the gap above it exist to lift the band off the
   section before it; as the first thing on a page they just leave a white strip */
.page-sub .science,
.page-sub .quiz{margin-top:0;border-radius:0}
.page-sub main > section:first-child{padding-top:var(--s-6)}

.cart-page{
  max-width:var(--maxw);margin:0 auto;
  padding:var(--s-5) var(--pad) var(--s-6);
  min-height:calc(var(--vh) - 300px);
}
.cart-head{margin-bottom:var(--s-4)}
.cart-head h1{font-size:clamp(38px,6vw,68px);line-height:1;color:var(--black)}
.cart-sub{margin-top:10px;color:var(--ink-64);font-size:15px}

/* free-shipping meter */
.ship-bar{
  margin-bottom:var(--s-3);padding:14px 18px;border-radius:16px;
  background:var(--cream);border:1px solid var(--ink-08);
}
.ship-bar p{font-size:13.5px;color:var(--ink-64)}
.ship-bar b{color:var(--ink);font-family:var(--font-display),sans-serif;font-weight:800}
.ship-track{margin-top:9px;height:5px;border-radius:999px;background:var(--ink-08);overflow:hidden}
.ship-track i{
  display:block;height:100%;border-radius:999px;
  background:linear-gradient(90deg,var(--violet),var(--orchid));
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .6s var(--ease);
}
.ship-bar.is-done .ship-track i{background:linear-gradient(90deg,var(--flame),var(--gold-2))}

.cart-wrap{display:grid;grid-template-columns:minmax(0,1fr) 372px;gap:var(--s-4);align-items:start}

.cart-lines ul{display:flex;flex-direction:column;gap:12px}
.cart-line{
  display:grid;grid-template-columns:104px minmax(0,1fr) auto;gap:var(--s-3);
  align-items:center;
  padding:16px;border-radius:20px;
  background:var(--paper);border:1px solid var(--ink-08);
  transition:border-color .4s var(--ease), box-shadow .4s var(--ease);
}
.cart-line:hover{border-color:var(--ink-14);box-shadow:0 12px 30px rgba(26,23,32,.06)}
.line-media{
  width:104px;aspect-ratio:1;border-radius:14px;overflow:hidden;
  background:var(--cream-2);
}
.line-media img{width:100%;height:100%;object-fit:cover}
.line-body h3{font-size:17px;letter-spacing:-.025em;line-height:1.25}
.line-sub{margin-top:4px;font-size:13px;color:var(--ink-46)}
.line-pricing{margin-top:9px;display:flex;align-items:baseline;gap:9px}
.line-pricing .price{font-size:17px}
.line-ctrl{margin-top:13px;display:flex;align-items:center;gap:16px}

.qty{
  display:inline-flex;align-items:center;
  border:1.5px solid var(--ink-14);border-radius:999px;overflow:hidden;
}
.qty-b{
  width:34px;height:34px;border:0;background:transparent;cursor:pointer;
  font-size:17px;line-height:1;color:var(--ink-64);
  display:grid;place-items:center;
  transition:background .3s var(--ease), color .3s var(--ease);
}
.qty-b:hover{background:var(--violet-lt);color:var(--violet)}
.qty-n{
  min-width:30px;text-align:center;
  font-family:var(--font-display),sans-serif;font-size:14px;font-weight:800;
}
.line-rm{
  border:0;background:transparent;cursor:pointer;padding:0;
  font-size:13px;color:var(--ink-46);
  border-bottom:1px solid transparent;
  transition:color .3s var(--ease), border-color .3s var(--ease);
}
.line-rm:hover{color:var(--warn);border-bottom-color:var(--warn)}
.line-amount{
  align-self:center;
  font-family:var(--font-display),sans-serif;
  font-size:20px;font-weight:900;letter-spacing:-.03em;white-space:nowrap;
}

/* summary rail */
.summary{
  position:sticky;top:88px;
  padding:var(--s-3);border-radius:22px;
  background:var(--cream);border:1px solid var(--ink-08);
}
.summary h2{font-size:19px;letter-spacing:-.03em}
.sum-rows{margin:18px 0 0;display:flex;flex-direction:column;gap:11px}
.sum-rows div{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.sum-rows dt{font-size:14px;color:var(--ink-64)}
.sum-rows dd{margin:0;font-size:14.5px;font-weight:600}
.sum-save dd{color:var(--violet)}
.sum-total{
  margin-top:16px;padding-top:16px;border-top:1px solid var(--ink-14);
  display:flex;justify-content:space-between;align-items:baseline;gap:12px;
}
.sum-total span{font-family:var(--font-display),sans-serif;font-size:15px;font-weight:800}
.sum-total b{font-family:var(--font-display),sans-serif;font-size:26px;font-weight:900;letter-spacing:-.035em}
.checkout{width:100%;margin-top:18px;justify-content:center}
.sum-note{margin-top:11px;font-size:12.5px;color:var(--ink-46);text-align:center}
.sum-note.is-warn{color:var(--warn);font-weight:500}
.sum-trust{
  margin-top:16px;padding-top:15px;border-top:1px solid var(--ink-08);
  display:flex;flex-direction:column;gap:7px;
}
.sum-trust li{
  font-size:12.5px;color:var(--ink-64);
  display:flex;align-items:center;gap:8px;
}
.sum-trust li::before{
  content:"";width:5px;height:5px;border-radius:50%;background:var(--violet-3);flex:0 0 auto;
}

/* ---- complete the routine ----
   Deliberately quieter than a bag line: a hairline card, no shadow, no photo
   stage. It has to be findable without ever competing with the items the
   shopper has already chosen, because the moment a suggestion reads as loudly
   as a decision the page starts feeling like it is arguing with them. */
.cart-cross{
  margin-top:var(--s-4);padding-top:var(--s-4);
  border-top:1px solid var(--ink-08);
}
.xs-h{
  font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-46);margin-bottom:14px;
}
.xs-list{display:flex;flex-direction:column;gap:10px}
.xs-item{
  display:grid;grid-template-columns:54px minmax(0,1fr) auto auto;
  align-items:center;gap:14px;
  padding:12px 14px;border-radius:16px;
  border:1px solid var(--ink-08);background:var(--paper);
}
.xs-item img{width:54px;height:54px;border-radius:12px;object-fit:cover;background:var(--cream-2)}
.xs-txt{min-width:0}
.xs-txt b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:14.5px;font-weight:800;letter-spacing:-.02em;line-height:1.25;
}
.xs-txt span{display:block;margin-top:3px;font-size:12.5px;color:var(--ink-64);line-height:1.45}
.xs-p{font-family:var(--font-display),sans-serif;font-size:15px;font-weight:900;white-space:nowrap}
.xs-add{padding:9px 18px;font-size:12.5px}

@media (max-width:560px){
  /* the price moves under the name so the button keeps a full-width tap
     target rather than being squeezed to an ellipsis beside it */
  .xs-item{grid-template-columns:48px minmax(0,1fr);row-gap:10px}
  .xs-item img{width:48px;height:48px}
  .xs-p{grid-column:1/2;align-self:center}
  .xs-add{grid-column:2/3;justify-self:end}
}

/* empty state */
.cart-empty{
  padding:var(--s-6) var(--s-3);text-align:center;
  display:flex;flex-direction:column;align-items:center;gap:14px;
}
.empty-mark{width:52px;height:52px;color:var(--ink-30)}
.empty-mark svg{width:100%;height:100%;display:block}
.cart-empty h2{font-size:clamp(26px,3.4vw,36px);letter-spacing:-.035em}
.cart-empty p{max-width:44ch;color:var(--ink-64);font-size:15px}
.empty-btns{margin-top:10px;display:flex;flex-wrap:wrap;gap:12px;justify-content:center}

/* ============ product page ============ */
/* One SKU per page. The brief was "one creative, huge, and its own price", and
   the layout is built around taking that literally: a single 1200×1560 studio
   frame in a column wider than anything else on the site, and a buy rail that
   rides alongside it instead of sitting under it.

   The container is 1420px rather than the site's 1240px --maxw. That is the one
   place the grid is allowed to break, and it is deliberate — at --maxw the image
   column lands at about 600px and the frame reads as a large card. At 1420 it is
   ~700 wide and ~910 tall, which is taller than most laptop viewports, and the
   thing stops being a card and starts being a poster. Every section BELOW the
   fold goes back to --maxw so the page still lines up with the rest of the site;
   only the hero is oversized, which is what makes it read as the hero. */
.pdp{padding-bottom:var(--s-6)}
.pdp-inner{
  max-width:1420px;margin:0 auto;padding:0 var(--pad);
  display:grid;
  grid-template-columns:minmax(0,1.14fr) minmax(0,.86fr);
  gap:clamp(28px,4.2vw,74px);
  align-items:start;
}

/* ---- the creative ---- */
/* The frames are 1200x1200 RGBA with a transparent field — the product floats
   on a bloom of its own colour that falls to zero alpha before the edge, and
   the page's own paper is the background. So there is deliberately no card
   here: no fill, no radius, no shadow. Every one of those was drawn to contain
   a dark rectangle, and a rectangle is exactly what the creative no longer is.
   A box-shadow in particular would trace a hard edge around thin air.

   object-fit is contain, not cover. cover existed to crop a 1200x1560 frame to
   a square and was aimed at 50% 38% to keep the product inside the window;
   the frame is already square and already composed, and cropping it would eat
   the bloom's falloff and put the cliff back that the artwork exists to avoid. */
.pdp-stage{
  position:relative;margin:0;
  aspect-ratio:1/1;
  max-height:min(70vh,680px);
}
.pdp-stage img{width:100%;height:100%;object-fit:contain;object-position:50% 50%}
/* Both of these used to be paper-white laid on a dark corner. There is no dark
   corner any more — white on white is nothing — so they invert to ink on a
   translucent paper pill, which reads against both the white field and the
   coloured bloom it may overlap. */
.pdp-flag{
  position:absolute;top:18px;left:18px;z-index:2;
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;
  padding:7px 13px;border-radius:999px;
  background:rgba(255,255,255,.72);border:1px solid rgba(26,23,32,.10);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  color:var(--ink);
}
/* .pdp-cap is gone, along with the caption it styled. It was 12.5px of grey
   laid over the bottom of the shot repeating the fill and the pump — exactly
   the register this page has been stripped of. Those facts are in the bold
   blocks below now, at a size that does not require leaning in. */

/* ---- the buy rail ---- */
/* Sticky only where there is room for it. The rail runs about 700px tall and
   sticky elements taller than the viewport strand their own bottom edge, so the
   height query is a correctness guard rather than polish — below it the rail
   scrolls normally and nothing is unreachable. */
.pdp-buy{padding-top:6px}
/* THE RAIL NO LONGER SLIDES, BECAUSE IT NO LONGER CAN — and that is the
   deliberate trade, not an oversight.

   It used to be `position:sticky;top:92px`, and the reason was a measured
   hole: the creative column is 630px tall at 1280px and the rail was 334px,
   so 296px of blank paper hung below Buy Now. Sticky spent that hole as
   travel — the rail rode down the shot instead of sitting in a gap.

   The promise list now fills the same 296px with content (see .pdp-promise),
   which means the rail is exactly as tall as the creative beside it. An
   element the full height of its containing block has nowhere to stick to:
   travel is row height minus element height, and that is now zero. Keeping
   the declaration would have been dead CSS pretending to do something.

   The CTA is still reachable the whole way down the page. That job belongs to
   the fixed buy bar in convert.js, which is the mechanism anyone actually
   sees working — it takes over the moment Add To Bag leaves the viewport and
   carries the price and the button to the bottom of the window. Sticky was
   only ever covering the first screen, and only while the hole existed. */

/* The breadcrumb is gone: three links, two of which are in the nav and the
   third of which was the page you were already standing on. The phone
   breakpoint had been hiding it for that reason already; this finishes the
   thought on every width. */

/* Bigger than it was, because it is now the first thing in the rail rather
   than the third. */
.pdp-buy h1{
  font-size:clamp(33px,3.8vw,56px);line-height:1;
  letter-spacing:-.04em;color:var(--ink);
  margin-bottom:16px;
}
.pdp-buy .rating{margin-bottom:20px}

/* padding-bottom was 20px and is 13px, and .pdp-clock's top margin came down
   with it. There are four rows under the price now rather than three — the
   dispatch clock, the claim window, the buttons, the promises — so the gaps
   between them are the only place the room could come from. Tightening the
   price/clock seam is the right one to give up: those two belong together
   anyway (what it costs, and by when), and the seam that still has to read as
   a real break is the one above the buttons. */
.pdp-price{
  display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
  padding-bottom:13px;border-bottom:1px solid var(--ink-08);
}
.pdp-price .price{font-size:clamp(32px,3.4vw,42px);line-height:1}
.pdp-price .mrp{font-size:15px}
.pdp-price .off{
  font-size:11.5px;letter-spacing:.04em;text-transform:uppercase;
  padding:5px 10px;border-radius:999px;
  background:var(--violet-lt);color:var(--violet);
}
/* .pdp-unit and .pdp-lede are both gone. The unit line was a 12.5px tax
   disclaimer and a per-night cost — the two smallest, greyest things on the
   page, sitting directly under the largest. The lede was a paragraph of
   .64-alpha prose between the price and the button, which is the one place on
   a product page where nothing is allowed to stand.
   The lede's one load-bearing sentence — the limitation, "it will not thicken
   the hair you already have" — was not dropped with it. It is the closing line
   of the What to expect block now, set at 32px instead of 15.5. A limitation
   printed larger than the sales copy is the only honest way to print one. */

.pdp-cta{margin-top:16px;display:flex;flex-wrap:wrap;gap:12px;align-items:stretch}
/* Two buttons on one row, and the row is what buys the vertical space that
   puts them beside the shot instead of below the fold. Add To Bag takes the
   larger basis because it is the primary; both are the same height, because a
   Buy Now that looks like a footnote is not a Buy Now. Under ~420px of rail
   the bases can no longer both be met and they stack, which is correct — two
   140px buttons side by side are two buttons nobody can hit. */
/* .pdp-now, not .pdp-buy — .pdp-buy is already the rail this sits inside, and
   a second rule on that name would hand the whole column a button's padding. */
/* Up from 15px/18px. These are the last thing in the rail now — the trust
   strip, the assurance list and the spec table that used to follow them are
   all gone — so there is nothing below to balance against and no reason for
   the one control the page exists for to be the size of body copy. */
/* EQUAL SLACK, NOT EQUAL BOXES — flex-basis is auto on purpose.

   Two earlier versions were both wrong, in opposite directions. First was a
   fixed ratio, 3:2 (flex:3 1 230px against flex:2 1 150px): the widths were
   decided by a number while the labels were decided by their text, so the two
   never agreed. Then equal halves, flex:1 1 190px, which is worse in the way
   that matters — measured at 1440px it gave both buttons a 267px box holding
   labels of 146px and 67px. "Buy Now" sat in 148px of empty space while
   "Add To Bag — ₹899" had 69px. Two boxes the same size with one of them
   half empty does not read as a pair; it reads as a mistake.

   basis:auto makes each basis its own label, and grow:1 then splits what is
   left over EQUALLY — so both buttons end up with the same number of pixels
   of air around their text, which is the thing the eye is actually judging.
   The boxes come out unequal because the labels are unequal, and that is the
   point. It also self-corrects per page: ₹799, ₹899, ₹1,199 and ₹2,399 are
   four different label widths and none of them need a number in here. That
   matters more since the claim window went in — offer.js rewrites these
   labels to the lapsed price mid-session, so the widths change while the page
   is open and there is nothing here to go back and re-measure. */
.pdp-add,
.pdp-now{
  flex:1 1 auto;justify-content:center;
  font-size:16.5px;padding:21px 26px;
}
/* The stock line is a third flex item in this row, so without a full basis it
   sits BESIDE the buttons and squeezes them the moment it unhides — the row
   silently reflows from two halves to three uneven thirds. It belongs on its
   own line under them. */
.pdp-cta .pdp-stock{flex:1 0 100%;margin:2px 0 0}

/* ---- the three promises -------------------------------------------------
   THIS IS NOT .pdp-trust COMING BACK. What used to sit below the buttons was
   .pdp-trust, .pdp-assure and .pdp-spec — roughly 420px of 12.5–14px grey —
   and deleting all three was the right call. Those were copy. This is three
   facts, set at 19px bold ink, and the size is the entire difference between
   a promise and a footnote.

   IT IS HERE TO FILL A MEASURED HOLE. At 1280px the creative column is 630px
   and the rail was 334px, so 296px of blank paper hung below Buy Now down to
   the bottom of the shot — and align-items:start put all of it in one lump at
   the bottom, which is the worst possible place for it. The last thing under
   the primary call to action was nothing at all.

   IT IS NOW THE ONLY PLACE THESE THREE ARE STATED. This used to read "yes,
   all three also run in the announce bar" and argue that saying them again at
   the point of decision was not repetition. The announce bar has since been
   removed from every page, so the argument is moot and the stakes went up:
   delete this block and cash on delivery, the seven-day replacement and
   WhatsApp support are stated nowhere on a product page at all.

   ONLY THESE THREE, AND ONLY EVER THESE THREE. Cash on delivery, seven-day
   replacement, WhatsApp support — the promises somebody has actually agreed
   to keep. No money-back guarantee: that one is specifically not offered, and
   the FAQ on index.html still contradicts this block. No free-shipping
   threshold — the ship bar does that arithmetic live off the real bag, and a
   static line here could be wrong. No rating. A fourth row goes in only if
   the dispatch desk keeps it. */
/* THREE ACROSS, ICON ABOVE LABEL. This is the reference page's trust strip and
   it is the right shape for three peer facts: stacked rows imply a reading
   order and a first among equals, and none of these three outranks the others.

   It was three rows with a rotated-border tick until now. Same three strings,
   same weight, different axis.

   THE STRIP KEEPS ITS NATURAL HEIGHT — it does not stretch, and it is not
   pinned anywhere. It is simply the last child of a column that shares its
   leftover height out across every gap (see the min-width:1081px block below
   for what that cost to work out). Because it is last, it still closes the
   rail flush with the bottom of the shot, and the rail is still exactly the
   creative's height — the invariant that matters here. See the .pdp-buy note
   about why sticky was dropped.

   DO NOT GIVE THESE CELLS flex:1 or a min-height. That was tried: at 1214px
   it made three 154x254 cells with a 41px icon floating in the middle of each
   and 254px vertical hairlines around them, and it read as an empty table.
   Three stacked rows can be stretched — they share the slack and each one
   still looks like a row. One row of three cannot: all the slack lands inside
   single cells and they go hollow. */
.pdp-promise{
  list-style:none;margin:clamp(22px,2.4vw,32px) 0 0;padding:0;
  border-top:2px solid var(--ink);
  display:grid;grid-template-columns:repeat(3,1fr);
}
.pdp-promise li{
  display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  gap:clamp(11px,1.1vw,15px);text-align:center;
  padding:clamp(20px,2.2vw,30px) clamp(6px,0.8vw,12px);
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(13px,1.1vw,16px);line-height:1.22;letter-spacing:-.018em;
  color:var(--ink);text-wrap:balance;
}
/* Hairlines between, nothing around. A box per promise would be three cards
   in a rail that already has a price card and a countdown card above it. */
.pdp-promise li + li{border-left:1px solid var(--ink-14)}

/* The disc is --ink-08 and the glyph is full ink. NOT --flame: warm is spoken
   for on this page, it marks the cells the comparison table concedes, and the
   same colour forty pixels above saying "reassurance" would be one voice
   making two opposite arguments.

   Stroked SVG rather than a font glyph — three different marks are needed
   here and a webfont that fails delivers three tofu boxes in the one strip
   whose entire job is looking trustworthy. */
.pp-ico{
  flex:none;display:grid;place-items:center;
  width:clamp(44px,4vw,54px);aspect-ratio:1;
  border-radius:50%;background:var(--ink-08);color:var(--ink);
}
.pp-ico svg{width:54%;height:54%;display:block}
/* Two columns only. The rail stretches to the creative's height and shares the
   difference out across its own gaps — so the column ends exactly level with
   the bottom of the shot at every width, rather than landing near it and
   leaving a remainder to explain.

   1081px, and it has to be exactly that. The obvious guess is 861, because
   there is a `.pdp-inner{grid-template-columns:1fr}` in a max-width:860 block
   — but that is not the rule that governs this layout. A SECOND one at
   max-width:1080 sets the same property, sits later in the file, and wins.
   Measured: the rail is still stacked under the shot at 900, 960, 1000 and
   1080, and only goes side-by-side at 1081.

   Getting this wrong is not cosmetic. Between 861 and 1080 the gate would
   have switched .pdp-buy to display:flex on a rail that is stacked, which
   changes margin collapsing for the ship bar underneath it — a spacing bug
   200px wide that never shows up in a two-column screenshot.

   At or below 1081 the rail is as tall as its own content and there is no
   leftover to distribute, so everything falls back to its natural spacing.
   That is what a phone wants anyway.

   THE SLACK IS SHARED OUT, NOT PARKED IN ONE GAP. The rail's natural content
   is about 443px against a 613px creative, so ~170px has to go somewhere, and
   there are exactly three places it can go. All three were built and measured:

     into the strip     flex:1 1 auto on the cells → three 154x254 cells with
                        a 41px icon adrift in the middle of each. An empty
                        table. (The .pdp-promise note has the long version.)
     above the strip    margin-top:auto → the strip itself looks right, and a
                        170px void opens directly under Buy Now. That is the
                        exact fault this whole block was built to close.
     across every gap   justify-content:space-between → ~42px of extra air
                        shared between the name, the price, the countdown, the
                        buttons and the strip. Nothing is hollow, nothing is
                        empty, and no single gap is carrying the remainder.

   The third one ships. .pdp-promise is the last child, so it still lands flush
   with the bottom of the shot; it just is not the thing being stretched. */
@media (min-width:1081px){
  .pdp-buy{
    align-self:stretch;
    display:flex;flex-direction:column;justify-content:space-between;
  }
}

/* ============ below the buttons =========================================
   FIVE SECTIONS, FIVE SHAPES, and the shape follows the content every time:

     1 .pform   what's inside   the actives, one open at a time
     2 .phow    how to use      four numbered steps
     3 .ptime   what to expect  a spine, because it is a sequence in time
     4 .pvs     the comparison  a real <table>, because it is a real comparison
     5 .pvoice  the reviews     other people's words, and only ever theirs

   THE BLOCKS ARE IN PAGE ORDER ABOVE AND NOT IN THE FILE. Read the numbers,
   not the scroll position.

   THE PAGE NOW OPENS ON THE INGREDIENTS. It used to open on .pwhy — four
   skimmable claims carrying no ingredient names — with .pideal (type, made
   for, NOT for, spec) sitting between the steps and the timeline. Both were
   removed on request. Two consequences worth knowing before anything is added
   back here:

     .pform is now the first thing under the buy rail, so the accordion is
     doing the work .pwhy used to do — convincing somebody who has not agreed
     to read yet. That is exactly why its four names stay at headline size and
     the panels ship open to anything without JavaScript.

     Nothing on the page tells a reader NOT to buy any more. That was
     .pideal's "Not for" row. The non-substitute clauses under .pvs survive, so
     the regulatory line holds, but the tone is now uniformly for the sale.

   Nothing sits on an ink band. A full-bleed dark panel with gold keys was a
   lot of paint spent on a list of four weeks, and it made the timeline look
   like the loudest claim on the page when it is the most qualified one. */

/* ---- 1. what's inside --------------------------------------------------
   The card is the ingredient showcase's — .sc-stage, .sc-media, .sc-glow and
   .sc-panel all come from the block near the top of this file and are shared
   with shop.html and science.html. Only the shell is shared. Everything from
   .pform-list down is this page's, because .sc-list sets its copy at 14px
   --ink-64 and this page spent two rounds deleting 14px grey.

   THE PANELS ARE OPEN IN THE STYLESHEET AND CLOSED BY .is-live. That direction
   matters: the markup ships expanded, which is the truth of the page before
   any script runs, and formula.js adds .is-live to take over. A stylesheet
   that collapsed them unconditionally would hide four paragraphs from every
   reader whose JS failed, and every reader that has none. */
.pform{padding:clamp(54px,7vw,104px) 0 0}
.pform-in{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.pform-h{
  font-size:clamp(32px,4.4vw,56px);line-height:1;letter-spacing:-.045em;
  color:var(--ink);text-wrap:balance;
}
.pform-lede{
  margin:16px 0 0;max-width:56ch;
  font-size:clamp(16px,1.25vw,18px);line-height:1.5;color:var(--ink-64);
}
.pform-stage{margin-top:clamp(28px,3.4vw,48px)}
/* The shot is the hero's own file — same URL, already in cache, no second
   download. It reads as a different picture because the PNG's field is
   genuinely transparent and .sc-media puts --tone-lt behind it: white float at
   the top of the page, tinted square here. contain rather than the inherited
   cover, because the product is centred in a square frame with its own margin
   and cover would crop that margin away at any non-square box. */
.pform-media img{object-fit:contain}

.pform-list{margin:0;padding:0;list-style:none}
.pform-item{position:relative;border-top:1px solid var(--ink-14)}
.pform-item:first-child{border-top:0}
/* The playhead. It draws over the open row's top rule while the panel is
   cycling on its own, and it is the only moving thing in the section. --p is
   written by formula.js each frame; it is 0 here so that a row with no script
   behind it draws nothing at all. */
.pform-item{--p:0}
.pform-item::after{
  content:"";position:absolute;left:0;top:-1px;height:2px;width:100%;
  background:var(--tone);border-radius:2px;
  transform:scaleX(0);transform-origin:0 50%;opacity:0;
}
.pform-stage.is-live .pform-item.is-open::after{opacity:1;transform:scaleX(var(--p))}

.pform-name{margin:0}
/* A grid rather than flex so the chevron can span both rows of the label and
   sit centred against the pair, instead of aligning to the name and floating
   above the role. */
.pform-tog{
  display:grid;width:100%;
  grid-template-columns:minmax(0,1fr) auto;
  align-items:center;gap:0 16px;
  padding:clamp(13px,1.4vw,18px) 0;
  margin:0;border:0;background:none;
  font:inherit;color:inherit;text-align:left;cursor:pointer;
}
.pform-nm{
  grid-column:1;grid-row:1;
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(18px,1.7vw,24px);line-height:1.08;letter-spacing:-.03em;
  color:var(--ink);transition:color .35s var(--ease);
}
/* The role label, and this is the one place in the section where small type is
   the right answer. It is a filing label, not a sentence. */
.pform-role{
  grid-column:1;grid-row:2;
  margin:6px 0 0;
  font-size:12px;font-weight:700;
  letter-spacing:.11em;text-transform:uppercase;color:var(--ink-64);
}
.pform-chev{
  grid-column:2;grid-row:1 / span 2;
  width:11px;height:11px;flex:none;
  border:solid var(--ink-46);border-width:0 2.5px 2.5px 0;
  transform:translateY(-3px) rotate(45deg);
  transition:transform .4s var(--ease), border-color .35s var(--ease);
}
.pform-tog:hover .pform-nm{color:var(--tone-tx)}
.pform-tog:hover .pform-chev{border-color:var(--ink)}
.pform-item.is-open .pform-nm{color:var(--tone-tx)}
.pform-item.is-open .pform-chev{
  transform:translateY(2px) rotate(-135deg);border-color:var(--tone-tx);
}

/* 0fr→1fr is what animates a disclosure to its real height without anybody
   measuring anything. The child needs overflow:hidden for the row to be
   allowed to collapse under its content. */
.pform-body{display:grid;grid-template-rows:1fr}
.pform-body > *{overflow:hidden;min-height:0}
.pform-stage.is-live .pform-body{
  grid-template-rows:0fr;transition:grid-template-rows .45s var(--ease);
}
.pform-stage.is-live .pform-item.is-open .pform-body{grid-template-rows:1fr}
/* THE SPACING GOES ON .pform-say, INSIDE THE CLIP — never on .pform-clip, and
   never as a margin. Both of the obvious placements leave a dead strip under
   every closed panel, for two different reasons:

     margin on either      a margin sits outside the element's own box, so the
                           row collapses around it and the gap survives;
     padding on the clip    padding is not compressible. min-height:0 takes the
                           CONTENT to zero, but a 0fr row still cannot shrink
                           past the grid item's own padding, so the row floors
                           at the padding value.

   The second one was the live bug: with the <p> as the grid item, its 21px
   padding-bottom made every closed row compute to 21px instead of 0. The clip
   is now a bare box, and the padding sits inside it where overflow:hidden can
   hide it on the way down. */
.pform-clip{padding:0;margin:0}
.pform-say{
  margin:0;padding:0 0 clamp(15px,1.6vw,21px);
  font-size:clamp(15.5px,1.15vw,17px);line-height:1.56;color:var(--ink-90);
}
.pform-say b{font-weight:700;color:var(--ink)}

/* The blanket reduced-motion rule further down collapses every transition to
   .001ms, which already makes the disclosure snap. What it does NOT stop is the
   playhead: --p is written by script on every frame, so the bar would still
   crawl across the rule under a setting that asked for nothing to move. It is
   switched off here rather than left to formula.js, which separately declines
   to auto-advance at all under the same query — the rule is stated in both
   places on purpose, so that neither one silently becomes the only guard.

   What is NOT restated here is "open every panel". That is the script's job at
   init, and forcing it in CSS would mean a reduced-motion reader could never
   close a panel again — their chevrons would point one way and their panels
   the other. Reduced motion is a request about movement, not about control. */
@media (prefers-reduced-motion:reduce){
  .pform-item::after{display:none}
  .pform-stage.is-live .pform-body{transition:none}
}

/* ---- 2. how to use -----------------------------------------------------
   Four steps across, because they are four short steps and stacking them puts
   a four-line instruction on three screens. The number is a CSS counter: the
   <ol> already knows its own order, and printing the digit into the markup
   would mean two sources for one fact. */
.phow{padding:clamp(54px,7vw,104px) 0 0}
.phow-in{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.phow-h{
  font-size:clamp(32px,4.4vw,56px);line-height:1;letter-spacing:-.045em;
  color:var(--ink);text-wrap:balance;
}
.phow-list{
  counter-reset:phow;
  margin:clamp(28px,3.4vw,48px) 0 0;padding:0;list-style:none;
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  gap:clamp(20px,2.6vw,38px);
}
.phow-list > li{
  counter-increment:phow;
  padding-top:clamp(13px,1.4vw,19px);border-top:2px solid var(--ink);
}
.phow-list > li::before{
  content:counter(phow,decimal-leading-zero);
  display:block;
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(24px,2.4vw,34px);line-height:1;letter-spacing:-.04em;
  color:var(--ink-30);
}
.phow-list > li > b{
  display:block;margin-top:9px;
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(17px,1.5vw,21px);line-height:1.1;letter-spacing:-.03em;
  color:var(--ink);text-wrap:balance;
}
.phow-list > li > p{
  margin:9px 0 0;
  font-size:clamp(14.5px,1.05vw,16px);line-height:1.5;color:var(--ink-90);
}
/* Scoped to the paragraph, because .phow-list > li > b is the step title and
   this is the emphasis inside a step's sentence. Same element, two jobs, and
   without the > the title would inherit inline sizing. */
.phow-list > li > p b{font-weight:800;color:var(--ink)}
.phow-note{
  margin:clamp(26px,3vw,40px) 0 0;padding-top:clamp(16px,1.6vw,22px);
  border-top:2px solid var(--ink-14);max-width:64ch;
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(17px,1.5vw,21px);line-height:1.35;letter-spacing:-.025em;
  color:var(--ink);
}
.phow-note em{font-style:normal;color:var(--flame-dk)}

/* ---- 4. the comparison ------------------------------------------------ */
/* Available to the screen reader, absent from the page. Used for the table's
   <caption> and for the empty corner cell of the header row: a sighted reader
   gets the comparison from the column headings, but a screen reader announces
   each cell as "row-label, column-heading", and without these two it reads the
   most important block on the page as a list of unlabelled fragments.
   clip-path rather than display:none — hidden content is not announced. */
.sr-only{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;
}
.pvs{padding:clamp(54px,7vw,104px) 0 0}
.pvs-in{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.pvs-h{
  font-size:clamp(32px,4.4vw,56px);line-height:1;letter-spacing:-.045em;
  color:var(--ink);text-wrap:balance;
}
.pvs-lede{
  margin:16px 0 0;max-width:60ch;
  font-size:clamp(16px,1.25vw,18px);line-height:1.5;color:var(--ink-64);
}
/* Sideways scroll is the DESKTOP safety net only — at 760px and up the 700px
   table already fits, so this never fires there. The phone gets a different
   shape entirely; see the max-width:759px block below. */
.pvs-wrap{
  margin-top:clamp(28px,3.4vw,48px);
  overflow-x:auto;-webkit-overflow-scrolling:touch;
}
/* 680, not 700. The narrowest this table is ever asked to render is 760px,
   where --pad is 30.4px and the content box is 699.2px — so a 700px minimum
   overflowed by 0.8px and switched the sideways scroll on for one pixel at
   exactly the width the phone layout hands over. 680 clears it by 19px, and
   every width above 760 has more room than 760 does. */
.pvs table{width:100%;min-width:680px;border-collapse:collapse;text-align:left}
.pvs th,.pvs td{
  padding:clamp(15px,1.6vw,22px) clamp(13px,1.4vw,22px);
  vertical-align:top;border-bottom:1px solid var(--ink-14);
}
.pvs thead th{
  border-bottom:2px solid var(--ink);
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(18px,1.7vw,24px);line-height:1.08;letter-spacing:-.035em;
  color:var(--ink);
}
.pvs thead th span{
  display:block;margin-top:6px;font-size:13px;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-64);
}
/* The row label is the question and the cells are the answers, so the label
   steps back: same face, grey, one size down. Without that the table is a
   wall of solid black and nothing in it is findable. Still set at reading
   size — this is a label, not fine print, and nothing here is fine print. */
.pvs tbody th{
  position:sticky;left:0;background:var(--paper);
  width:24%;min-width:150px;
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(15px,1.3vw,18px);line-height:1.2;letter-spacing:-.02em;
  color:var(--ink-64);
}
/* THE ANSWERS ARE THE LOUDEST TYPE IN THE TABLE. They used to be 14.5px grey
   sentences — a paragraph wearing a table's clothes, which is the worst of
   both: too long to scan, too cramped to read. The cells are now phrases, set
   in the display face at 23px, so a whole column can be taken in at a glance
   and the comparison happens in the eye rather than in the reading. If a cell
   will not fit in four or five words it is the wrong fact for this table —
   put it in the notes underneath, which is what they are for. */
.pvs td{
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(17px,1.65vw,23px);line-height:1.18;letter-spacing:-.03em;
  color:var(--ink);text-wrap:balance;
}
/* The one warm mark in the table, and it is spent on the row we lose. */
.pvs td em{font-style:normal;color:var(--flame-dk)}
/* Our column, marked with a wash rather than a colour. A saturated brand
   column would read as a scorecard we wrote for ourselves — which is the
   exact impression the note under this table exists to avoid. */
.pvs .is-us{background:var(--ink-08)}
.pvs-note{
  margin:clamp(24px,2.8vw,38px) 0 0;max-width:62ch;
  font-size:clamp(16px,1.3vw,19px);line-height:1.52;color:var(--ink-90);
}
.pvs-note b{font-weight:800;color:var(--ink)}
.pvs-note + .pvs-note{margin-top:16px}

/* ---- the comparison on a phone --------------------------------------------
   THE TABLE STOPS BEING A TABLE HERE, AND THAT IS THE WHOLE POINT.

   Measured at 390px: the wrap is 358px wide, the table is 700px, and the row
   label is pinned at 168px of it. So you got a ~190px window onto one column
   at a time, and the second answer started 31px past the right edge — the two
   halves of a comparison could not be on screen together at any scroll
   position. You slid right, read one, slid back, read the other, and compared
   them from memory. That is not a comparison. It was also the widest-travel
   thing on the page: 342px of horizontal scroll, most of a screen.

   The fix is not to shrink the type until it fits. Three columns will not fit
   in 358px at a size anyone reads, so the row stops being a row: the question
   goes across the full width and the two answers sit side by side underneath
   it, each labelled with its column name (data-col, written by vrows in
   tools/pdp_body.py). Half of 358px is 179px per answer, which is narrow but
   it is a four-word phrase — that is exactly why the cells were cut to
   phrases in the first place.

   Not stacked lists. Stacking our column above theirs would be two lists one
   after the other, which loses the same thing the sideways scroll lost. Side
   by side in a 2-up grid is the minimum shape that still compares. */
@media (max-width:759px){
  /* The wrap must stop being a scroll container or the sticky row label keeps
     resolving against it and the block layout inherits a phantom 700px. */
  .pvs-wrap{overflow:visible}
  /* Dropping table display is what frees tr to be a grid. min-width goes with
     it — it is the 700px that caused the overflow. */
  .pvs table{display:block;width:100%;min-width:0}
  .pvs tbody{display:block}
  /* The column names now live in every cell, so the header row would just be
     saying it a seventh time. caption stays: it is .sr-only either way. */
  .pvs thead{display:none}

  .pvs tbody tr{
    display:grid;grid-template-columns:1fr 1fr;
    margin-top:clamp(20px,5vw,26px);
    border-top:2px solid var(--ink);
  }
  .pvs tbody tr:first-child{margin-top:0}

  /* The question, full width across the top of its block. Flush to the left
     margin so it lines up with the heading and lede above — the answers are
     indented under it, which is the hierarchy doing the work that a table
     column used to do. */
  .pvs tbody th{
    display:block;grid-column:1 / -1;
    position:static;background:none;width:auto;min-width:0;
    padding:13px 0 3px;border-bottom:0;
    font-size:14px;letter-spacing:.08em;text-transform:uppercase;
    color:var(--ink-64);
  }
  /* Both answers stretch to the taller of the two, so the wash on our side is
     a full-height block rather than a ragged one. */
  .pvs tbody td{
    display:block;
    padding:11px 13px 15px;border-bottom:0;
    font-size:16.5px;line-height:1.22;
    text-wrap:pretty;
  }
  /* The key. Small on purpose — it is a filing label like .pform-role, and at
     reading size it would compete with the four bold words underneath it that
     are the actual content of the cell. */
  .pvs tbody td::before{
    content:attr(data-col);
    display:block;margin:0 0 7px;
    font-family:var(--font-body),sans-serif;
    font-size:11px;font-weight:700;letter-spacing:.11em;
    text-transform:uppercase;color:var(--ink-46);
  }
  /* Our key gets full ink against their grey — the one place the two sides
     are marked differently in the type rather than the background. */
  .pvs tbody td.is-us::before{color:var(--ink)}
  /* A hairline between the two answers. The wash already implies the split,
     but it stops at our cell's edge and the eye wants the seam. */
  .pvs tbody td + td{border-left:1px solid var(--ink-14)}
}

/* ---- 3. the timeline --------------------------------------------------
   A spine with markers. The rule is the passage of time and the dots are the
   checkpoints on it, which is the same information the ink band carried and
   about a hundredth of the ink. The last marker is filled because the last
   row is the limit rather than a milestone — it is where the line stops. */
.ptime{padding:clamp(54px,7vw,104px) 0 0}
.ptime-in{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.ptime-h{
  font-size:clamp(32px,4.4vw,56px);line-height:1;letter-spacing:-.045em;
  color:var(--ink);text-wrap:balance;
}
.ptime-list{
  margin:clamp(28px,3.4vw,48px) 0 0;
  padding:0 0 0 clamp(22px,2.4vw,34px);
  list-style:none;position:relative;
}
.ptime-list::before{
  content:"";position:absolute;left:0;top:7px;bottom:7px;width:2px;
  background:var(--ink-14);
}
.ptime-list > li{position:relative;padding:0 0 clamp(26px,2.8vw,38px)}
.ptime-list > li:last-child{padding-bottom:0}
/* -6px puts the 14px marker (10px + 2px border each side) centred on the 2px
   spine: the li content edge is one padding-left in from the spine, so the
   offset is that padding back out, then half the marker less half the rule. */
.ptime-list > li::before{
  content:"";position:absolute;
  left:calc(-1 * clamp(22px,2.4vw,34px) - 6px);top:6px;
  width:10px;height:10px;border-radius:50%;
  background:var(--paper);border:2px solid var(--ink-30);
}
.ptime-list > li:last-child::before{background:var(--ink);border-color:var(--ink)}
.ptime-k{
  display:block;font-size:13px;font-weight:700;
  letter-spacing:.11em;text-transform:uppercase;color:var(--ink-64);
}
.ptime-t{
  display:block;margin-top:6px;
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(20px,2vw,28px);line-height:1.04;letter-spacing:-.035em;
  color:var(--ink);
}
.ptime-list > li p{
  margin:9px 0 0;max-width:50ch;
  font-size:clamp(15.5px,1.15vw,17px);line-height:1.52;color:var(--ink-90);
}
.ptime-note{
  margin:clamp(28px,3vw,42px) 0 0;padding-top:clamp(16px,1.6vw,22px);
  border-top:2px solid var(--ink-14);max-width:46ch;
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(18px,1.7vw,24px);line-height:1.3;letter-spacing:-.03em;
  color:var(--ink);
}
.ptime-note em{font-style:normal;color:var(--flame-dk)}

/* ---- 5. the reviews ----------------------------------------------------
   Everything here is painted by reviews.js from a list of actual reviews.
   There is no markup for it in the PDP — the section ships as an empty
   <section data-reviews="sku"> and REMOVES ITSELF if that SKU has none. So
   every rule below is styling something that only exists when somebody
   really wrote it, and none of it can be filled in by hand.

   THE AVERAGE AND THE COUNT ARE NOT AUTHORED ANYWHERE. .pvoice-avg and
   .pvoice-count are computed from the list, which is why there is no "4.8"
   or "1,204" in this stylesheet, in the markup, or in pdp_body.py. This page
   shipped all three of those as hard-coded star rows once. Inflating the
   number now costs you a whole fabricated review to go with it.

   The section sits between the comparison table and the kit band. That is
   the last thing on the page that argues, and it should be the only part
   where the argument is not in our voice. */
.pvoice{padding:clamp(54px,7vw,104px) 0 0}
.pvoice-in{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}
.pvoice-h{
  font-size:clamp(32px,4.4vw,56px);line-height:1;letter-spacing:-.045em;
  color:var(--ink);text-wrap:balance;
}

/* ---- the sample-content banner ----
   Deliberately ugly. It only renders at ?reviews=demo, and the entire job of
   this rule is to make a screenshot taken in demo mode obviously a demo, so
   that nobody pastes one into a deck as if it were the site. If it ever
   looks at home on the page, it has failed. */
.pvoice-demo{
  margin:18px 0 0;padding:13px 16px;
  border:2px dashed var(--flame);border-radius:12px;
  background:var(--gold-lt);
  font-size:14.5px;line-height:1.5;color:var(--ink-90);max-width:64ch;
}
.pvoice-demo b{font-weight:800;color:var(--flame-dk)}
.pvoice-demo code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;
  background:rgba(255,255,255,.75);padding:1px 6px;border-radius:5px;
}

/* ---- the summary ----
   Score on the left, distribution on the right. The distribution is not
   decoration: an average on its own hides whether 4.4 means "everyone said
   4" or "half said 5 and half said 3", and those are different products. */
.pvoice-sum{
  margin:clamp(26px,3.2vw,44px) 0 0;
  padding-bottom:clamp(24px,2.8vw,36px);
  border-bottom:2px solid var(--ink-14);
  display:grid;grid-template-columns:auto 1fr;
  gap:clamp(22px,4vw,62px);align-items:center;
}
.pvoice-score{display:flex;flex-direction:column;align-items:flex-start;gap:9px}
.pvoice-avg{
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(44px,5.6vw,72px);line-height:.86;letter-spacing:-.05em;
  color:var(--ink);
}
/* .stars is the shared component from the product cards, set at 13px for a
   tile. Here it is carrying the headline number, so it is scaled up rather
   than redrawn — same glyphs, same --r fill, one size larger. */
.pvoice-score .stars{font-size:19px}
.pvoice-count{font-size:13.5px;color:var(--ink-64);font-variant-numeric:tabular-nums}

.pvoice-dist{margin:0;padding:0;list-style:none;width:100%;max-width:400px}
.pvoice-dist li{
  display:grid;grid-template-columns:auto 1fr auto;
  align-items:center;gap:11px;padding:3.5px 0;
}
.pvoice-dk{
  display:inline-flex;align-items:center;gap:2px;
  font-size:12.5px;font-weight:700;color:var(--ink-64);
  font-variant-numeric:tabular-nums;
}
.pvoice-dk i{font-style:normal;color:var(--gold)}
.pvoice-bar{
  display:block;height:8px;border-radius:999px;
  background:var(--ink-08);overflow:hidden;
}
/* Width, not transform. The bar is a proportion of a real count and it does
   not animate — a rating that slides into place invites the eye to watch the
   motion instead of reading the shape, and there is nothing to reveal. */
.pvoice-bar i{display:block;height:100%;background:var(--gold);border-radius:999px}
.pvoice-dn{
  font-size:12.5px;color:var(--ink-46);text-align:right;min-width:2.5ch;
  font-variant-numeric:tabular-nums;
}

/* ---- the reviews themselves ----
   Rules between them, not cards on a wash. A boxed card per review makes
   three reviews look like a designed grid of testimonials; a list separated
   by hairlines looks like a list of things people wrote, which is what it
   is. */
.pvoice-list{margin:0;padding:0;list-style:none}
.pvoice-card{padding:clamp(22px,2.4vw,30px) 0;border-bottom:1px solid var(--ink-14)}
/* Everything past the third is hidden until the button is pressed. display
   rather than max-height: a clipped fourth review peeking out from under a
   fade is a dark pattern in miniature — it implies there is more of the same
   below when what is below might be the one-star. Either it is on the page
   or it is behind an honest button that says how many. */
.pvoice-card[data-extra]{display:none}
.pvoice.is-all .pvoice-card[data-extra]{display:block}

.pvoice-top{display:flex;align-items:center;flex-wrap:wrap;gap:9px}
.pvoice-top .stars{font-size:14px}
.pvoice-who{
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:15px;letter-spacing:-.01em;color:var(--ink);
}
/* Quiet, and quiet on purpose. "Verified purchase" is a fact about the order
   and not a badge we award — at chip weight with a tick it starts reading
   like an endorsement. */
.pvoice-ok{
  display:inline-flex;align-items:center;gap:5px;
  font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-46);
}
.pvoice-ok::before{
  content:"";width:5px;height:5px;border-radius:50%;background:var(--ink-30);
}
.pvoice-t{
  margin:11px 0 0;max-width:46ch;
  font-family:var(--font-display),sans-serif;font-weight:900;
  font-size:clamp(17px,1.5vw,21px);line-height:1.16;letter-spacing:-.03em;
  color:var(--ink);text-wrap:balance;
}
.pvoice-b{
  margin:8px 0 0;max-width:66ch;
  font-size:clamp(15.5px,1.15vw,17px);line-height:1.55;color:var(--ink-90);
  text-wrap:pretty;
}
/* How long they had been using it, and when they said so. Both matter more
   here than on most sites, because this page spends a whole section telling
   you it takes twelve to sixteen weeks — a five-star review at week two
   would be evidence against us, not for us. */
.pvoice-meta{
  margin:12px 0 0;display:flex;flex-wrap:wrap;gap:6px 16px;
  font-size:12.5px;color:var(--ink-46);
}

.pvoice-more{
  margin:clamp(22px,2.4vw,30px) 0 0;
  padding:13px 26px;border-radius:999px;
  border:2px solid var(--ink);background:none;color:var(--ink);
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:14.5px;letter-spacing:-.01em;cursor:pointer;
  transition:background .25s var(--ease), color .25s var(--ease);
}
.pvoice-more:hover{background:var(--ink);color:var(--paper)}
.pvoice-more:focus-visible{outline:2px solid var(--violet);outline-offset:3px}

@media (max-width:640px){
  /* The score stops sitting beside the bars and sits above them. At 390px the
     two-column grid leaves the bars about 150px wide, at which point a 1-star
     row with a single review is a dot. */
  .pvoice-sum{grid-template-columns:1fr;gap:20px;align-items:start}
  .pvoice-score{flex-direction:row;align-items:baseline;gap:12px;flex-wrap:wrap}
  .pvoice-dist{max-width:none}
}

/* ---- the shop tiles become links ----
   A stretched ::after rather than wrapping the card in an <a>: the card already
   holds two buttons, and a nested interactive element inside a link is both
   invalid and unusable with a keyboard.

   The stacking is the fiddly part and is worth spelling out. .card carries a
   transform, so it is the containing block for the absolute ::after and inset:0
   covers the whole tile, media included — which is what we want, the image
   should open the page too. .card-media must therefore NOT get a z-index of its
   own: leaving it at auto keeps it in .card's stacking context, where painting
   falls back to document order and .card-body — the link's parent — comes
   second and wins the image's pixels.

   The two buttons then climb back out on their own z-indexes: .quick and .badge
   already sit at 2, and .card-foot .btn is given the same. Both clear the
   link's 0. It looked for a while as though .quick was losing that comparison,
   but the hit test was being run at its rest position — .quick is translated
   140% down and clipped out of the tile until :hover, so it was being probed
   somewhere it is never clickable in the first place. */
.card{position:relative}
.card-link{color:inherit}
.card-link::after{content:"";position:absolute;inset:0;z-index:0}
.card:hover .card-link{color:var(--violet)}
.card-foot .btn{position:relative;z-index:2}

@media (max-width:1080px){
  .pdp-inner{grid-template-columns:1fr;gap:var(--s-4);max-width:var(--maxw)}
  /* Below the two-column break the stage is the full width of the page and
     square, so on a 900px column it would be 900px of picture before a word of
     copy. The cap keeps the product large without pushing the price under two
     screens of scroll. */
  .pdp-stage{max-width:620px;margin:0 auto}
  /* Two by two. A quarter of a 1000px page is 235px, which sets the longer
     steps to eight and nine ragged lines under a two-line title — a column of
     confetti. Half the page is 470px and the same sentence is three lines. */
  .phow-list{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:560px){
  /* One step at a time. Two columns on a 358px content box is 170px a step,
     which is about 22 characters — the titles alone go to three lines. */
  .phow-list{grid-template-columns:1fr;gap:clamp(18px,5vw,26px)}
  /* The number stops being a block above the title and sits beside it, because
     stacked in one column each step was four elements tall for one sentence of
     instruction and the list ran a screen and a half. */
  .phow-list > li{
    display:grid;grid-template-columns:auto minmax(0,1fr);
    column-gap:14px;align-items:baseline;
  }
  .phow-list > li::before{grid-column:1;grid-row:1}
  .phow-list > li > b{grid-column:2;grid-row:1;margin-top:0}
  .phow-list > li > p{grid-column:2;grid-row:2}

  /* ============ the phone PDP: product and button on one screen ============
     Measured on a 390x844 phone before any of this existed: the bottom edge
     of Add To Bag sat at y=1208, which is 1.43 screens. The picture and the
     one control the whole page exists for were never visible at the same
     time. Everything in this block is that 1208 taken apart, and the numbers
     in the comments are measured, not estimated.

     What did NOT survive the first attempt: this rule used to read
     `.pdp-stage{aspect-ratio:4/3}`, which bought its height by letting
     object-fit:cover throw away the top and bottom of the frame. On a
     1200x1560 shot whose product runs y=40..1290 that window covers
     y=251..1151 — it cut the pump off the serum and the base off the bottle.
     Height is now taken by making the stage smaller, not by cropping it. */

  /* ---- the frame, whole ----
     The stage carries the shot's own ratio and is sized by height with
     width:auto, so it is a scaled-down copy of the frame rather than a window
     onto part of it. justify-self:center is load-bearing: a grid item with
     width:auto stretches to its column by default, which would put the ratio
     back to work computing a 465px height.

     The ratio is 1/1 because the frames are 1200x1200 now. It read 1200/1560
     while they were; left stale it would have letterboxed a square image into
     a portrait box and shrunk the product by a further 23% on the one screen
     size where the product's size was already the binding constraint. */
  .pdp-stage{
    aspect-ratio:1/1;
    /* The one number that decides whether the button makes the screen. The
       rail below the stage is fixed at 586px on a 375px phone, measured, so
       the stage gets whatever the viewport has left: 27vh is 228px on a
       390x844 and 219 on a 375x812, which lands the button at 798 and 805
       against 844 and 812. Nothing above 27 fits both.
       The bottle's rendered size is a pure function of this height — the
       frame is height-limited in every phone-width box — so this is also the
       only dial that trades picture size against the button, and cropping
       cannot buy anything back. */
    height:clamp(170px,27vh,270px);
    width:auto;max-width:100%;
    justify-self:center;
  }
  /* Identical output to cover while the box matches the frame. Stated anyway
     so that if the ratio above is ever changed the failure is letterboxing,
     which is visible, rather than a silent crop, which is what was here. */
  .pdp-stage img{object-fit:contain;object-position:50% 50%}
  /* Bottom-left, not top-left, and only at this width. The stage is 219px on a
     375px phone while the pill is 87px — 40% of the frame — and all three
     products are centred with their tallest part at the very top of the shot,
     so the top-left corner is not a free corner here the way it is at 620px.
     Measured against each frame's opaque bbox it was landing on the serum's
     pump. The bottom-left is genuinely empty on all three: the pill's top edge
     sits at 186px against product bottoms of 178 (serum), 176 (face) and 157
     (mask), so it clears the shortest clearance by 8px. */
  .pdp-flag{top:auto;bottom:10px;left:10px;font-size:10px;padding:5px 10px}

  /* ---- everything above the button ----
     72px of section padding and a 40px column gap were 112px of nothing
     between the nav and the shot.

     The long selector is not decoration. `.pdp{padding-top:14px}` was written
     here first and measured no change at all: the 72 comes from
     `.page-sub main > section:first-child`, which weighs (0,2,2) against
     .pdp's (0,1,0), and a media query adds nothing to specificity. Naming the
     class instead of the tag takes it to (0,3,1), which is the smallest edit
     that actually wins. */
  .page-sub main > .pdp:first-child{padding-top:14px}
  .pdp-inner{gap:16px}

  /* ---- the buy rail ----
     The reorder that used to live here is gone, and so is the reason for it.
     It was a twelve-line flex `order` list moving the lede, the sub-line, the
     trust strip, the assurance list and the spec table below the buttons,
     because source order put them between the picture and the one control the
     page exists for. Every one of those five elements has since been deleted.
     What is left in the rail is the name, the price, the buttons and the ship
     bar, in that order, in the markup — so the markup is the order now and
     there is nothing to override.

     display:flex stays. The rail is still a column and the ship bar still
     needs its margin to collapse the same way it does at every other width. */
  .pdp-buy{display:flex;flex-direction:column;padding-top:0}

  .pdp-buy h1{font-size:25px;line-height:1.08;margin-bottom:10px}
  /* The pill's own padding was setting the height of the whole price row,
     because the row is align-items:baseline and the pill is the deepest box
     in it. Nine pixels off the pill is nine pixels off the row. */
  .pdp-price{padding-bottom:10px}
  .pdp-price .price{font-size:30px}
  .pdp-price .off{padding:4px 8px}

  .pdp-cta{margin-top:16px;gap:10px}
  /* Scoped to .pdp-cta, and both buttons named together, for a reason worth
     recording: the ≤720px block further down shrinks every .btn-solid to
     12.5px/10px for the product cards, and at equal specificity it is later in
     the file, so it was winning here. That left Add To Bag 43px tall beside a
     63px Buy Now — the primary literally smaller than the secondary, on the
     width where it matters most. Two classes, one declaration, so the pair can
     never drift apart again. */
  /* flex-basis back to 100% so they STACK. Above this width the basis is auto
     and the two share a row; at 360–560px auto would just fit — roughly 200px
     of Add To Bag beside 120px of Buy Now — and they would quietly un-stack
     into two cramped targets. Full basis forces one per line, which is what
     the width wants and what a thumb wants. */
  .pdp-cta .pdp-add,
  .pdp-cta .pdp-now{font-size:15.5px;padding:16px 22px;flex-basis:100%}
}

/* ============ responsive ============ */
/* Short screens, any width. The scroll hint is absolutely positioned in the
   hero's bottom padding lane, and that lane is 8vh while the hint needs its own
   64.6px plus a 2.2vh offset. Above ~880px tall the hero sits at its min-height
   with slack and the two never meet; below it the content block grows into the
   lane and the buttons run straight through the word SCROLL — measured at
   -22.8px of clearance on a 1280x720 laptop, which is a common size and was
   broken before this hero was touched.
   Hiding it is the right trade rather than shrinking the products to make room:
   the hint is decorative and aria-hidden, on a screen this short the fold is
   self-evident, and the phone breakpoint below already makes the same call.
   Reclaiming the lane also gives the content back ~4vh, which is what actually
   lets the hero fit inside the first screen at 720px tall. */
@media (max-height:880px){
  .hero{padding-bottom:clamp(28px,4vh,44px)}
  .scroll-hint{display:none}
}
@media (max-width:1000px){
  .grid{grid-template-columns:1fr 1fr}
  /* Two columns, so "alone in the last row" becomes an odd visible index — see
     the long note by the three-column rule for why the count has to exclude
     .hide. This lands later in the file than that rule and both weigh (0,4,0),
     so on a one-card filter this correctly overrides the grid-column:2 above
     rather than fighting it. */
  .grid > .card:nth-child(odd of .card:not(.hide)):nth-last-child(1 of .card:not(.hide)){
    grid-column:1 / -1;
    /* A third wider than a normal column — 173px becomes 230px on a 390px
       phone. Written as a ratio off the real gutter rather than as 230px flat,
       because a column is 338px at the top of this breakpoint and a fixed 230
       would make the featured card the smallest thing in the grid. */
    width:calc((100% - var(--col-gap)) / 2 * 1.33);
    margin-inline:auto;
  }
  .facts{grid-template-columns:1fr;gap:var(--s-3)}
  .stats{grid-template-columns:1fr 1fr;gap:var(--s-4)}
  .sc-stage{grid-template-columns:minmax(0,.7fr) minmax(0,1.3fr);gap:var(--s-3)}
  /* ---- the timeline turns, it does not switch off ------------------------
     This used to be a 2x2 grid with the rail set to display:none, which left
     a section headed "Proof, On A Timeline" with four loose boxes and four
     dots connected to nothing. The dots still lit as you scrolled — the JS
     never stopped — but with the line gone there was no line being drawn, so
     the one graphic on the page was doing its work invisibly.

     Rotating it is nearly free because the scroll loop already publishes the
     one number that matters. script.js measures each .timeline against the
     viewport and writes a 0-1 fill to --f on .rail i; horizontally that drives
     scaleX. Nothing about that value is horizontal, so the same --f drives
     scaleY here and the rail draws downward instead of rightward, through the
     dots, in the reading direction the phone already scrolls. No new JS, no
     second measurement, and the two tracks still light independently. */
  .timeline{grid-template-columns:1fr;gap:var(--s-3)}
  .timeline .rail{
    /* through the dot centres: the dot is 14px wide at left:0, so its axis is
       at 7px, and a 2px rail at left:6px sits on it. */
    top:7px;bottom:0;left:6px;right:auto;width:2px;height:auto;
  }
  .timeline .rail i{
    width:100%;
    transform:scaleY(var(--f,0));transform-origin:50% 0;
    /* the gradient was written to run along the rail, so it has to turn with
       it or all four entries share the one colour at the top of the ramp */
    background:linear-gradient(180deg,var(--violet),var(--orchid),var(--flame),var(--gold));
  }
  .timeline li{padding-top:0;padding-left:34px;min-height:22px}
  .timeline .dot{top:1px}
  /* A ring that opens once as the rail arrives. .lit is toggled by the same
     scroll loop and CSS only restarts an animation when the class lands, so
     this fires on the frame the dot lights rather than looping — and fires
     again if you scroll back up and come down a second time, which is the
     behaviour you want from something announcing "the rail got here". */
  .timeline .dot::after{
    content:"";position:absolute;inset:-3px;border-radius:50%;
    border:2px solid var(--violet);opacity:0;
  }
  .timeline li.lit .dot::after{animation:dot-ring .62s var(--ease) 1}
  .res-group[data-tone="face"] .timeline .dot::after{border-color:var(--orchid)}
  .res-group[data-tone="face"] .rail i{
    background:linear-gradient(180deg,var(--orchid),var(--flame),var(--gold));
  }
  .scan-grid{grid-template-columns:1fr;gap:var(--s-3)}
  .real-list{grid-template-columns:1fr;gap:var(--s-2)}
  .pair-list{grid-template-columns:1fr;gap:var(--s-2)}
  .bundle-inner{grid-template-columns:1fr;gap:var(--s-4)}
  /* the summary drops under the lines, so it must stop sticking */
  .cart-wrap{grid-template-columns:1fr}
  .summary{position:static}
}
@media (max-width:720px){
  :root{--s-7:88px;--s-6:72px}

  /* ---- the kit: picture before the price, not after the buttons ----
     .bundle-inner is two cells, copy and media, and on a phone it collapses to
     one column in source order. The buttons live inside .bundle-copy, so the
     picture of the thing being bought landed UNDER "Order on WhatsApp" — the
     reader was asked to commit to a kit and only then shown it. On desktop the
     two cells are side by side so the question never came up.
     display:contents dissolves the copy wrapper on this width only, promoting
     its four children to grid items alongside the media, which is what makes
     them orderable against it. The wrapper carries no box of its own — no
     background, padding, border or radius anywhere in this file — so there is
     nothing to lose by removing it from the box tree.
     row-gap:0 is what keeps the spacing identical rather than merely close.
     The ≤960 rule sets gap:var(--s-4), which as a row gap would add 40px
     between every promoted child. With it at 0 the stack falls back to the
     children's own margins, and because h1-h4 and p are both reset to margin:0
     in this file, every adjacent pair has exactly one non-zero margin — 16 from
     .eyebrow, 24 from .sec-sub, 40 from .bundle-foot. Those are the same values
     margin collapsing was already producing inside the block, so the promotion
     is invisible. column-gap is untouched; there is only one column here. */
  .bundle-copy{display:contents}
  .bundle-inner{row-gap:0}
  .bundle-media{order:1;margin-top:var(--s-3)}
  .bundle-foot{order:2}

  /* At 119px per tile "Hair Growth Serum" and "Premium Hair Mask" wrap to two
     lines and "PDRN Face Serum" does not, so the middle caption was one line
     shorter. margin-top:auto holds the three bars to a common BOTTOM, which
     pushed that difference to the top as a 14px strip of .kitpick a's white
     showing under the middle picture — the stepped edge the continuous-bar
     note above is written to prevent. Reserving two lines on the name levels
     the tops instead. 2.4em is exactly two lines at the 1.2 line-height. */
  .kitpick-cap b{min-height:2.4em}
  /* The break between the hair and face tracks is 88+72=160px on the phone —
     a hairline with eighty pixels of nothing on each side, which reads as the
     end of the page rather than as a divider. Same failure as the old hero-to-
     ticker run: the gap is real content-free scroll on a screen that has none
     to spare. Half it, and let the border do the separating. */
  .res-group{margin-top:var(--s-5)}
  .res-group + .res-group{margin-top:var(--s-4);padding-top:var(--s-4)}
  .res-group .timeline{margin-top:var(--s-4)}
  /* the nav grows to 104px once the links wrap onto their own row */
  html{scroll-padding-top:124px}
  /* the links are the only route to the other pages now, so they wrap onto a
     scrollable second row instead of being hidden */
  #nav{flex-wrap:wrap;row-gap:10px}
  .nav-links{
    order:3;width:100%;margin-left:0;
    gap:var(--s-3);overflow-x:auto;scrollbar-width:none;
    -webkit-overflow-scrolling:touch;
  }
  .nav-links::-webkit-scrollbar{display:none}
  .nav-links a{font-size:13px;white-space:nowrap}
  .nav-cta{margin-left:auto}
  .navbtn-full{display:none}
  .navbtn-short{display:inline}
  .navbtn{font-size:12px;padding:10px 16px;min-height:38px}
  .logo img{height:17px}

  /* The phone hero is a height budget, not a set of independent numbers. Chrome
     above it is 114px — the nav alone, once its links wrap to a second row —
     against the 79px the desktop rule subtracts, and the stack below has to fit
     in what is left or the buttons fall under the fold. It was 150 and 114 while
     the announce bar stood above the nav; that bar was a measured 35.6px and it
     is gone, so both came down by it. Re-measured at 390x844 after the captions
     came off and the group grew to 30vh:
       banner 148 + gap 16            = 164
       eyebrow 17 + 10                =  27
       headline                       =  42
       products 253 + margins 20      = 273   (was 194+20 at 23vh with a caption)
       headline                       =  42
       foot 55 + margin 14            =  69   (was 130 with the .tag)
                                        ---
                                        617  against 844-114-48 = 682
     Two lines of copy paid for sixty pixels of glass. Change any one of these
     and re-check that total, in both directions: too tall and the buttons fall
     under the fold, too short and the column centres inside its min-height and
     opens a dead band at each end, which is exactly what made the scroll to
     "Three Formulas" feel endless.

     THE min-height BELOW IS OVERRIDDEN AND HAS BEEN FOR A WHILE. `.hero{
     min-height:auto}` appears later in this same max-width:720px block — see
     the long note there about releasing the pin on phones — so it wins, and
     the computed value at 390x844 is 0px. The budget above is still the live
     arithmetic: it is what --gh is derived from, and the hero has to fit the
     screen whether or not anything pins it. The declaration is kept in step
     with the budget rather than deleted so the two cannot silently disagree
     if the pin is ever restored. Do not tune this number expecting it to move
     anything on a phone; it will not. */
  .hero{
    min-height:calc(var(--vh) - 114px);
    /* 12px top, not 24, and top-aligned rather than centred.
       The gap under the nav was 38.5px: 24px of padding plus half of whatever
       slack was left over, because a centred column splits its spare height
       evenly between top and bottom. That split is the real problem — it means
       the gap changes with the screen, and any time the content shrinks the gap
       silently grows back. Trimming the pad alone would have fixed 390x844 and
       left 375x812 at 27px.
       flex-start pins the column under the nav so the gap is exactly the 12px
       of padding on every phone, and sends all the spare height to the bottom
       instead, where it is useful — it becomes clearance under the buttons and
       the room the scroll gesture starts in. */
    align-items:flex-start;
    padding:12px var(--s-2) var(--s-3);
  }
  /* 124px. This was 172 for one pass, on the theory that the banner should grow
     into the empty band under the nav. Wrong lever, twice over. The gap is held
     by .hero's 12px of padding now, not by how tall this card is, so growing it
     closes nothing — and every pixel added here comes straight off the buttons'
     fold clearance, which is the tightest budget on the page.
     104, down from 124: the headline came off 19px to 17.5 and the copy gap
     from 6 to 5, which took the stacked content box to ~83px and left ~10px
     clear top and bottom. That is the floor — below it the kicker's top-left
     starts crossing the 20px corner radius. Compact, not big.
     22 below against 12 above, for the same reason the desktop bar runs 38/14:
     at 12 and 14 the two gaps read as equal and the banner looked welded to the
     eyebrow under it. Only phones with the height to spare get the full 22 — the
     max-height:700px tier below trims it to 18, which is still clearly the wider
     of the two gaps and so still reads as a separate object.

     62px now, and 104 was only ever "the floor" for the wrong shape. That number
     was measured against a STACKED card — kicker, two headline lines, then the
     price and its pill on a row of their own — and the row of its own is what
     cost 30px. It was never load-bearing copy, it was a layout choice, and the
     desktop bar has been doing without it since it was built: .hb-copy becomes a
     2x2 grid there, kicker over line on the left, the CTA spanning both rows on
     the right. Same DOM, no content dropped.
     Those rules live in @media (min-width:721px), so a phone never sees them and
     they have to be restated here rather than shared — the two bars want the
     same structure at different sizes, not the same numbers.
     Measured content box: kicker 9.6 + 2px row gap + two 13px lines at 1.06 =
     39.4px, which fits 62 with 11.3px clear top and bottom. 72 is that same
     content with ~16px of air instead — the bar was cut to 62 to buy height for
     the lineup, and once the lineup stopped taking its width from the viewport
     (see --gh below) there was height to give back. The corner-radius warning
     above is answered by dropping the radius with the height: at r=14 the curve
     has met the top edge well before x=14, where the copy starts.
     14 below against 12 above. The 22 was buying separation from the eyebrow for
     an object that was a card; a bar this size does not need as loud a gap to
     read as its own thing, and the ratio is still the right way round. */
  .hero-banner{--hb-h:72px;margin-bottom:14px}
  .hb-track{border-radius:14px;box-shadow:0 8px 18px rgba(26,23,32,.12)}
  /* square, so the tile reads as a colour block butted against the pastel —
     track this to --hb-h. The base rule's 40% would hand it 143px of a 358px
     strip. */
  .hb-slide{grid-template-columns:minmax(0,1fr) 72px}
  .hb-copy{
    display:grid;grid-template-columns:minmax(0,1fr) auto;
    grid-template-areas:"kicker cta" "line cta";
    align-items:center;column-gap:10px;row-gap:2px;
    padding:0 10px 0 14px;
  }
  /* 8px and .09em, not the base 9.5/.13. The kicker is the widest line in the
     left column, not the headline — "Copper Peptide + Redensyl" is 25 characters
     against "Tone correction" at 15 — so it is the one that decides whether this
     column fits. At 8/.09 it measures ~136px against the 148px a 360px-wide
     phone leaves, which is the narrowest screen this has to hold. */
  .hb-kicker{grid-area:kicker;align-self:end;font-size:8px;letter-spacing:.09em}
  .hb-line{grid-area:line;align-self:start;font-size:13px}
  /* the br stays on a phone. Desktop unwraps it because a 72px bar cannot hold
     two lines beside a kicker; 62px can, because the CTA row is gone. Keeping it
     is also what keeps the left column narrow enough to fit at 360. */
  .hb-cta{grid-area:cta;align-self:center;margin-top:0;font-size:12.5px;gap:6px}
  .hb-cta i{font-size:9px;padding:4px 8px}
  /* contain for the same reason desktop uses it: cover on a 62px panel takes a
     horizontal band out of a square tile whose subject is a standing bottle. The
     scrim goes with it — it existed to carry tone across a bleeding crop. */
  .hb-media img{object-fit:contain;object-position:50% 50%}
  .hb-media::after{display:none}
  /* Only the group's height budget changes — the gap and every offset are
     derived from it. 23vh rather than 22vh, and the extra is paid for by the
     headline dropping to 11.4vw just below, so the total above is unchanged:
     the phone has ~11px of slack and this does not spend it.
     Why bother: at 22vh against a 12.4vw headline the lineup covered 76.5% of
     the words above it, where desktop sits at ~95%. Trading a little type for a
     little glass puts the phone at ~87% and makes the two read as the same
     composition instead of two different ones. */
  /* 30vh, up from 23vh, and the seven points come straight out of the two
     caption lines below. Dropping them freed 136px on a 390x844 screen, and
     because .hero centres its column inside a min-height that did not change,
     all 136px landed as dead band — 68px of nothing above the banner and 68px
     below the buttons, which is the exact failure this file already warns about
     twice. Air is not the same as a void. About 60px goes back into the glass
     and the rest stays as margin, which leaves ~38px top and bottom, the same
     clearance the budget below was written against.
     The size is not arbitrary: at 23vh the lineup spanned 217px against a 295px
     headline, 74%, where desktop sits near 95% and the two read as different
     compositions. At 30vh it spans ~283px, ~96%, so the phone finally matches
     the rule the desktop hero was built on.
     34vh now, and the 96% rule is deliberately overshot for the same reason
     desktop overshoots it: the lineup was asked to fill the page, so it is
     allowed to be the larger object. The headline dropping to 9.2vh below pays
     for most of it and the trimmed eyebrow/foot margins pay the rest, so this
     is not spending the ~11px of slack the phone hero has.
     37vh, not 34, and the extra three points are measured rather than argued:
     at 34 a 390x844 phone came out 36px *under* .hero's min-height, which on a
     phone is not shared out the way it is on desktop — the phone rule sets
     align-items:flex-start, so every pixel of it lands as dead band under the
     buttons instead of being split above and below. That is the same "air is
     not a void" failure this file has already been caught by twice. 37vh spends
     25 of the 36 and still leaves the buttons ~53px clear.
     The 340px cap binds from 919px tall, which is a 6.7"-and-up phone held
     portrait; past it the group stops growing while the screen does not, which
     is the safe direction to fail in.

     The third term is a width guard, and it is the one that actually binds on
     most phones. Everywhere else on this page --gh is a height budget, but the
     products are sized by height with width:auto, so the row's *width* is a
     consequence of it: measured, the cluster comes out about 1.19x --gh once
     the two --gap joints are counted. At 37vh a 390x844 phone rendered a 371px
     row inside a 358px stage — 103.6%, clipped at both ends by .hero's
     overflow:hidden, and invisible in a height-only test because it costs no
     scrollbar. 75vw keeps the row at roughly 97% of the stage on any phone
     width, which is as close to edge-to-edge as it can go and still be whole.
     If --gap or the three heights ever change, that 1.19 changes with them and
     this number has to be re-measured, not re-derived.

     ---- rebuilt as one line, the way desktop already is ----

     The banner note above frees 50px, but handing it to 37vh would have bought
     nothing at all, and this is the part that was not obvious: on a phone the
     height terms were not binding. Measured at 390x844, min(37vh, 340, 75vw)
     resolved 312.3 / 340 / 292.5 — the WIDTH guard won, by 20px. The lineup was
     small because it had run out of room sideways, so every pixel of banner
     trimmed would have landed as dead band under the buttons, which is the "air
     is not a void" failure this file has been caught by three times now.
     So both axes move together, and the two of them are what make the group
     bigger rather than either one alone.

     Width. --gap drops from .055 to .03, which is the cheapest width there is:
     the joints were 16.1px between an 84px bottle and a 99px one, and at 10.2px
     they are still unmistakably joints. The cluster factor goes 1.0798 + 2(.055)
     = 1.1898 to 1.0798 + 2(.03) = 1.1398, so the same screen width carries a
     4.4% taller group before the first change is counted.

     The guard is the STAGE width, not the viewport. An earlier pass let .bottle
     break out to 100vw on the theory that the products have no reason to respect
     a text margin. Measured, that was wrong in a way that is obvious on the
     screen and invisible in the numbers: the cluster ran 4->386 while the banner
     and the buttons run 16->374, so the one object with no straight edges of its
     own was the only thing in the hero overhanging the column — 12px proud on
     each side, which reads as a mistake rather than as a break-out. Sharing the
     stage edge is what makes the lineup and the bar above it look like one
     column. Hence (100vw - 32px) for .hero's two 16px pads, over 1.1398 to turn
     a cluster width back into a height budget.
     Box edges, not glass edges — but the two outer PNGs carry only ~1% of
     transparent margin (the 3.7% one is the hair serum, which sits in the
     middle and cannot touch an outer edge), so the glass lands within 2px of
     the stage and the alignment survives the artwork.

     Height. Same shape as desktop's calc(89vh - 430px): the phone hero's fixed
     costs are 409.2px — 114 of nav, 32 of .hero padding, 86 of banner and its
     margin, 21.8 of eyebrow, two 41.8px headline lines (.hero-h is 11.4vw on a
     phone, so it is fixed against height), 16 of group margin and 55.4 of foot
     — plus .hero-foot's own clamp(16, 5.9vh-22, 30), which is the one elastic
     item. Subtract those from the screen, leave 20px of clearance so the scroll
     gesture has somewhere to start, and the whole phone range is
     94.1vh - 407px: 387 at 844 tall, 327 at 780, 221 at 667, 195 at 640.

     It was 444.8 and 443px while the announce bar stood above the nav. That bar
     measured 35.6px on every screen — 9px of padding either side of an 11px
     display line — and it is gone, so both figures came down by it. The 390x844
     case does not move: the width guard is smaller than either number there and
     still binds. What the 35.6px actually buys is the SHORT phones, where the
     height term is what binds — 667 tall goes 185 -> 221.

     Above about 830 tall the width guard is the smaller of the two and binds
     instead, which is the point — on the phones most people hold, the lineup is
     sized by the column it has to line up with, and height only takes over on
     screens short enough that fitting is the harder problem than matching.
     That is why the two max-height tiers below no longer set --gh. A line has no
     seams, and the seams were real — the 830 boundary stepped the group from
     240px to 234px across one pixel of screen.

     380px, not 340. This is the artwork ceiling, the same rule desktop's 592
     follows: the jar renders .528 x --gh wide, so 380 puts it at 200.6 CSS px,
     which is 602 device px on a 3x phone against 625 in the file. Still a
     downsample. It binds above 441px of width, so no phone in use reaches it.

     Net at 390x844: 292.5 -> 314.1, the cluster spanning 16->374 to the pixel
     the banner does. At 375x667 the height term binds instead and it goes
     160 -> 220.6. */
  .bottle{
    --gh:max(140px, min(calc(94.1vh - 407px), 380px, calc((100vw - 32px) / 1.1398)));
    --gap:calc(var(--gh) * .03);
  }
  /* Phone only: the two support lines come off. Desktop keeps both — there the
     hero is one wide column with room to breathe and the copy reads as part of
     the composition; stacked on a phone the same words turn the fold into a
     list. The captions are the ones to lose because neither carries anything
     the page doesn't say again further down: the product names and the ₹699 are
     the three cards a screen below, and the molecule line is the first thing
     science.html opens with.
     Both are display:none rather than visibility:hidden so their boxes leave the
     flow, and both are now pure subtraction: ~25px for the caption plus its gap,
     and ~60px for the .tag (two wrapped lines at 14.5/1.6 plus the foot's gap),
     straight off the height budget rather than off the glass.
     That last part changed. --gh used to be the whole column with .bottle-inner
     on flex:1, so hiding the caption here silently handed its height to the
     products and the phone's --gh was not comparable to desktop's. --gh is the
     glass on every width now, so these two rules do exactly what they look like
     they do and the vh figures across all the tiers can be read against each
     other. */
  /* The .bottle-tag half of this pair is gone: the caption no longer exists on
     any width, so there is nothing left here to hide. Only the foot's .tag is
     still a phone-only subtraction. */
  .hero-foot .tag{display:none}
  /* Hiding the tag above did not just remove a paragraph, it removed a gap. On
     desktop the headline is separated from the CTAs by three things — the foot's
     margin-top, two lines of .tag, and the foot's own row gap, about 90px of
     stack. On a phone the tag's display:none takes the paragraph *and* the gap
     it was on one side of, so the entire separation collapses to the foot's
     margin-top, which the tiers below drive to 10px.
     10px is worse than it sounds. .hero-h runs line-height:.94, so the line box
     is ~3px shorter than the glyphs it holds and the descender of the g in
     "Change" hangs below it. The measured 10px box gap is closer to 5px of
     actual white, and the buttons read as welded to the headline.
     Height-relative rather than a flat number, because this is the one gap on
     the phone hero with nowhere to be pushed to. It is charged to the dead space
     under the buttons, not to the product glass — .scroll-hint is hidden here so
     nothing else wants that room — but the shortest phone only has ~25px of that
     space, so the slope matters more than the endpoints.
     Hence the offset rather than a plain 3.3vh. A pure vh through 28px at 844
     tall passes through 21 at 640, which left 360x640 clearing the fold by 14 —
     inside the "above it only on paper" band this file has been caught by twice.
     Steepening the line and pulling it down 22px hits the same 28 on the phones
     that reported the problem while easing to the 16px floor on the one screen
     that cannot pay: 640 -> 16, 740 -> 22, 844 -> 28, 915 and up -> the 30 cap.
     28px on a 44px headline is a bit over half its size, the normal rhythm for
     heading-to-CTA; 16px on a 32px one is the same ratio, which is why the short
     phone reads as proportioned rather than as the squeeze it also is. */
  .hero-foot{margin-top:clamp(16px, calc(5.9vh - 22px), 30px)}
  /* The margin above only fixes the gap at rest. The other half of this bug was
     .hero-foot's -46px scroll parallax closing that gap as you scrolled — at
     375x812 it measured -0.8px mid-scroll, the buttons touching the g of
     "Change". That is fixed at the base rule rather than here, because the same
     shear was overprinting the .tag across the headline on desktop; see the note
     on .hero-foot above. Nothing needed on the phone beyond the margin. */
  .hero-btns{width:100%}
  /* 22px, not the 30px desktop uses, and it is worth knowing why such a small
     number matters. At 375px the row has 343px and the pair intrinsically needs
     101.1 + 112.6 of label, 4x30 of padding and a 12px gap = 345.7. It wrapped
     by 2.7px — and a wrap costs a whole second row, 67px, which was enough to
     push both CTAs under the fold on every 375-wide phone. Trimming 8px a side
     frees 32px and takes the requirement to 313.7, so the row holds with real
     margin instead of sitting 3px from a cliff.
     This does not make the buttons narrower. They are flex:1 1 auto inside a
     width:100% row, so once they fit on one line they grow back to fill all
     343px at ~165px each; the padding value only decides whether they wrap, and
     the space around the labels ends up wider than 22px, not tighter. */
  .hero-btns .cta{flex:1 1 auto;justify-content:center;padding-left:22px;padding-right:22px}
  .tag{font-size:14.5px}
  /* 9.2vh, from 9.8. Same trade the desktop headline just made and for the same
     reason — the four points --gh gained above have to come from somewhere, and
     on a 390x844 phone this is 5px a line, 10px total, against ~34px of glass.
     The vw term is untouched: it binds below ~355px wide, where the line is
     already at its floor and there is nothing to give. */
  .hero-h{font-size:clamp(32px, min(11.4vw, 9.2vh), 56px)}
  /* the stacked hero fills the screen; the hint just collided with the buttons */
  .scroll-hint{display:none}
  /* The last stretch under the buttons was not padding and no amount of
     trimming padding could reach it. The hero is pinned to the first screen by
     min-height; its content came in under that, and the difference sat as dead
     slack between the CTA and the section below. Cutting padding-bottom only
     converted padding into more slack and moved nothing on screen.

     The pin is released on phones instead. Two fixed subtractions were tried
     first and both failed the same way: the stage inside the hero is itself
     sized off viewport height and shrinks *faster* than the viewport does, so a
     number tuned to sit flush at 390x844 left 26px of slack again at 375x812.
     There is no constant that works, because the thing being subtracted from
     and the thing being subtracted both move.

     So: no pin. The hero is exactly as tall as what is in it, on every phone,
     and the stage still scales with the screen so it stays proportionate — it
     lands at 74-78% of the viewport across the sizes measured rather than a
     forced 100%. Nothing moves the CTA down; everything below moves up to meet
     it, and the run to the first card comes out at 217-218px on all three sizes
     tested — the device-independence is the whole reason for doing it this way
     rather than with a tuned constant.
     The side effect is worth having on its own: "The Range" now breaks the fold
     instead of the hero ending exactly on it, so the first screen shows there
     is a section under it. The cards themselves are still below — reaching them
     would mean cutting type, not space. Desktop keeps the pin. */
  .hero{min-height:auto;padding-bottom:20px}

  /* ---- the run from "Shop The Range" to the first product -----------------
     Measured at 390x844 with everything above already trimmed, this run was
     still 352px — most of a phone screen between the button that says buy and
     the first thing that can be bought. It broke down as 24 hero tail + 40
     section top + 29 eyebrow + 68 heading + 92 tabs + 64 header margin, and
     only 97 of that is type. The rest was rhythm inherited from a desktop
     layout that has 900px to spend and no fold to lose.

     Every number below is cut against the element it sits next to rather than
     to a flat value, because the gaps still have to rank: heading-to-tabs must
     read as tighter than tabs-to-products, or the tabs float between the two
     and belong to neither.

     These are scoped to .products on purpose. .sec-head is shared with Results
     and Science, where the same header introduces a long read rather than a
     grid and the wider spacing is doing real work. Nothing here should reach
     them. */
  .products{padding-top:18px}
  /* Shop's copy of the same cut — see the .page-sub note by the base rule. 72px
     of nav-to-eyebrow on a phone is a quarter of the screen spent introducing a
     list the visitor tapped "Shop" to reach. */
  .page-sub main > section.products:first-child{padding-top:24px}
  .sec-head .eyebrow{margin-bottom:6px}
  /* 40 -> 14. The tabs are a control strip for the grid, not a second heading,
     so they belong to the row below them. At 40 they sat exactly between the
     h2 and the cards and looked like neither. */
  .products .tabs{margin-top:14px}
  /* 64 -> 20, the single biggest line item on this list. --s-5 is the site's
     section-header gap and it is correct on a page you read; under a grid it
     is a held breath before the only content that sells anything. 20 still
     separates the control strip from the first card by more than the 12px
     gutter between the cards themselves, which is the relationship that has
     to hold. */
  .products .sec-head{margin-bottom:20px}
  /* Two up, not one. Measured at 390x844: the one-per-row listing ran 2089px
     top to bottom, or 2.5 phone screens for three products, which meant no two
     of them were ever on the glass at the same time. A listing page exists to
     let people compare; one card per screen is a slideshow. Two up brings the
     same three products to 803px — 62% less, and the whole range now fits
     inside a single screen.
     The card rules that make a 173px card readable are grouped under "the 2-up
     card" further down rather than scattered through this block. */
  .grid{grid-template-columns:1fr 1fr;--col-gap:12px;gap:22px var(--col-gap)}
  .stats{grid-template-columns:1fr}
  .timeline{grid-template-columns:1fr;gap:var(--s-3)}

  /* ---- the scan graphic, re-typed for the phone -------------------------
     This is the block the note up at .scanviz promises. The frame is capped
     at 720px on desktop so the SVG draws near 1:1; on a 390px phone the same
     viewBox is squeezed into 356px, a scale of 0.494, and everything inside
     is in user units, so the 14px labels land at 6.9px and the status line
     at 5.4px. Measured, not estimated. The fix is to size the type back up
     in user units so it comes out the far side at a readable pixel size:
     26 x 0.494 is 12.8px and 21 x 0.494 is 10.4px.

     Strokes get the same treatment for the same reason — a 1.6 hairline
     renders at 0.79px and disappears into the background on a dark panel. */
  .sv-nodes text{font-size:26px}
  .sv-nodes circle{stroke-width:3.4}
  .sv-part path,.sv-measure path{stroke-width:2.6}
  .sv-brackets path{stroke-width:4}
  /* Enlarging the type costs horizontal room, and two things stop fitting.
     "Scalp condition" is the longest label and at 26 units it runs to x=770
     inside a 720-unit viewBox — it would simply be cut off at the frame
     edge. Its dot is at cx=556, far enough right that there is no room to
     its right at this size, so the label moves to the other side of its own
     dot: anchored at its end and pulled 40 units back, it occupies 334-532
     and clears the dot's left edge at 549 by 17 units. */
  .sv-node:last-child text{text-anchor:end;transform:translateX(-40px)}
  /* The status line is the other casualty, and it took a screenshot to see
     the real problem: the arithmetic said 688 < 720 and called it fitting,
     but the bottom corner brackets sit at x=18-40 and x=680-702 on the same
     y as this text. The line was not running off the frame, it was running
     through both brackets. Measuring against the frame was the wrong test;
     the space it actually has is the band between the brackets, 44 to 676.

     So it is set to the largest size that fits that band — 21 units at .05em
     tracking measures 615 and, nudged 24 to the right to clear the left
     bracket's rounded cap, occupies 48-663. The size is kept and the
     tracking spent instead: this is the honesty disclaimer and legibility
     matters more than letter-spacing does. It still renders at 10.4px, and
     the figcaption below repeats the same statement at full size anyway. */
  .sv-status{font-size:21px;letter-spacing:.05em;transform:translateX(24px)}

  .science{border-radius:26px 26px 0 0}
  .sc-stage{grid-template-columns:1fr;gap:var(--s-3);padding:14px;border-radius:24px}
  .sc-media{max-width:280px;margin:0 auto;width:100%}
  .sc-list li{padding:11px 13px}
  .sc-list li.lit{padding-left:18px}
  .sc-foot{flex-direction:column;align-items:flex-start;gap:12px}
  .sc-tab{flex:1 1 auto;padding:9px 12px;font-size:12px}
  /* Two up rather than four across. The clamp alone would fit four on one line
     at 24px a name, which is small enough to read as body copy rather than as
     a shopfront — and the whole point of the row is that it looks like one. */
  /* Two up, and the height is capped by the COLUMN, not by taste. Each cell is
     half the row, so a 4:1 mark can only be as tall as a quarter of that —
     at 375px the cell is 155px and the mark tops out near 38px. 9vw tracks
     that ceiling down to 320px instead of stating one number that overflows
     on the narrow end; the gap dropped to --s-2 to buy the cells the width
     that makes 40px reachable at all. */
  .retail-list{gap:var(--s-3) var(--s-2)}
  .retail-list li{flex:0 0 calc(50% - var(--s-2))}
  .retail-list img{height:clamp(30px,9vw,40px)}
  .btn{width:100%}

  /* ---- the 2-up card ----------------------------------------------------
     At 390px wide the grid is 358px, a 12px gutter leaves 173px a card, and
     12px of body padding leaves 149px of actual content. Every number below
     is measured against that 149, because the desktop card was written for
     roughly three times it and nothing survives the move unchanged.

     The card's blurb and its marketing chips used to be hidden here and shown
     on desktop. They are gone from the markup entirely now: a grid is for
     comparing and choosing, and nobody reads a paragraph inside a tile at any
     width. Only the volume chip survived, because 30 ml against 200 ml is the
     one spec that changes what the price means. The prose lives on the product
     page, which is where a reader who wants it is headed. */
  .card{border-radius:16px}
  .card-body{padding:12px}
  .card h3{
    font-size:14px;line-height:1.25;margin-bottom:8px;
    /* Three lines, then an ellipsis. Two would have clipped "PDRN Face Serum
       — Niacinamide + Alpha Arbutin" mid-ingredient, and the grid rows stretch
       to the tallest card anyway, so reserving the third line is free: a short
       title just hands the space to the chip row below it. */
    display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;
    -webkit-box-orient:vertical;overflow:hidden;
  }
  .chips{margin-top:0;gap:5px}
  .chips li{font-size:10.5px;padding:4px 8px}
  .badge{top:8px;left:8px;font-size:9.5px;padding:4px 8px}

  .card-foot{
    flex-direction:column;align-items:stretch;gap:10px;
    margin-top:12px;padding-top:10px;
  }
  /* The discount is put on its own line deliberately rather than left to wrap.
     Inline, "₹1,199  ₹1,400  14% off" needs about 153px against the 149 there
     is, so it wraps on the face serum and does not on the mask — three cards
     with the price block breaking differently in each. Forcing the break makes
     all three agree, and it costs one line of height to keep the percentage,
     which in this market is the number that moves a first order. */
  .pricing{gap:6px}
  .price{font-size:17px}
  .mrp{font-size:12px}
  /* Scoped to .pricing. Unscoped it also matched .pdp-price .off — the
     discount pill on the product page, which is a 68px badge sitting in a
     358px flex row with the price and the strikethrough and had no reason to
     break. flex-basis:100% gave it a row of its own anyway, and that row cost
     40px directly above Add To Bag on every phone. */
  .pricing .off{flex:0 0 100%;font-size:11px;margin-top:-1px}
  /* The WhatsApp buttons ride along with .btn-solid rather than keeping the
     base 13px/11px 18px, because at this width .btn is already width:100% and
     the two of them stack — Add The Kit directly above Order On WhatsApp, both
     343px wide on a 375px phone. Left out of this rule they measured 43px and
     46px, and a 3px step between two full-width buttons in a stack is read as
     a mistake rather than as a hierarchy. The glyph stays 17px: it is not what
     was setting the height (the anchor simply was not matching this selector),
     and it is the thing that says WhatsApp before the label is read. */
  .btn-solid,.kitband-wa,.bundle-wa{font-size:12.5px;padding:10px 12px}

  /* Nothing here re-declares the whole-card tap target. It already exists — see
     the .card-link::after block above — and it works at this width, verified
     with elementFromPoint on both pages: the image, title, chip and every part
     of the price row resolve to the product page, and Add To Bag still wins its
     own pixels. Adding a second overlay here was tried and did active harm: it
     needed .card-foot lifted to clear it, which took .pricing up as well and
     left ~50px of card that answered to nothing at all. */

  /* amount moves under the controls — a third column here squeezes the title
     to a couple of characters per line */
  .cart-line{grid-template-columns:76px minmax(0,1fr);gap:14px;padding:13px}
  .line-media{width:76px}
  .line-body h3{font-size:15.5px}
  .line-amount{grid-column:2;justify-self:start;margin-top:-4px;font-size:18px}
  .line-ctrl{gap:12px}
  .empty-btns .cta{width:100%;justify-content:center}

  .quiz{border-radius:26px 26px 0 0}
  .quiz-card{padding:18px}
  .quiz-top{gap:12px}
  .opts button{font-size:14px;padding:14px 16px}
  /* stacked, image first — a 190px rail leaves the copy unreadably narrow */
  .qr-body{grid-template-columns:1fr;gap:var(--s-3)}
  .qr-media{max-width:190px}
  .qr-foot .btn{width:auto}
  .faq-q{font-size:15px;padding:17px 18px}
  .faq-a > p{padding:0 18px 18px;font-size:14.5px}
}

/* ---- short phones ----
   The hero's fixed costs do not scale with the screen. The nav takes 114px,
   the two headlines 84, the foot 55, the eyebrow and every margin about 77 —
   roughly 330px that an iPhone SE or an 812-tall phone owes exactly as much as
   a 932-tall one does. (It was 150 and 366 while the announce bar stood above
   the nav; the bar was a measured 35.6px on every screen and it is gone.)
   Only the banner and the product group are free
   to move, so on a short screen they are the only things that can pay.
   The banner no longer pays anything here: at a compact 124px it is already at
   the floor of what the stacked card can be, and shrinking it further to buy
   ~20px would cost more legibility than the 20px is worth. The product group
   has 60px of give and takes the whole trim instead.
   Height, not width, is the right query: a 390x780 phone has the same problem
   and a 375x900 one does not. */
/* This tier is empty on purpose, and is left standing as the record of why.
   It held --gh:min(30vh,240px), the middle step of a three-tier staircase, and
   the staircase is gone: .bottle now states the same relationship as one line,
   calc(94.1vh - 433px), derived from the phone's fixed chrome the way desktop
   derives calc(89vh - 430px) from its own.
   The seam this removes was not theoretical. At 831 tall the base tier gave
   37vh = 307px and at 830 this one gave 30vh = 249 — a 58px step across one
   pixel of screen, in the middle of the range real phones sit in. The line
   passes through 305 and 348 at those two heights instead.
   Nothing else belonged here, so the query is gone with it rather than being
   left standing empty. */

/* ---- 667-tall phones (iPhone SE / 8, 360x640 Android) ----
   These were 69.9px under the fold back when the banner was 172px. Dropping it
   to a compact 124 gave 48 of that back for free, so what is left here is small:
   the margins, which cost no content at all — 6px off the eyebrow, 8 off the
   group, 4 off the banner and 4 off the foot — plus the last of the group's
   give. The banner itself is left alone; it is already at its floor.

   The tier is written against 667, and 640 does not just follow it down: --gh's
   160px cap binds at 667 but 24vh binds at 640, so the group only gives back
   6.4px across a 27px drop in screen and the shortest phone landed 2.5px under.
   Hence margin:2px, not 6. Measured stage at 360x640: banner 150, eyebrow 22.8,
   headlines 38.6 each, group 165.6, foot 65.4 = 481.0 in a 478.4 budget. The
   group's own margins were the only line left that costs no content to spend.

   Both of those were squeezes taken under protest, and dropping the banner to
   104px with the dot row gone handed 36px straight back — 360x640 went to
   +41.5 without touching anything here. So the squeezes are unwound: the group
   gets its 6px margin back and the banner sits 18px off the eyebrow instead of
   10, which is the same "don't weld it to the eyebrow" the taller phones get at
   22. That spends 16 of the 36 and leaves 360x640 at about +25, which is real
   clearance rather than the 5.5px it was scraping by on.
   Below 640 tall the fixed costs alone exceed the screen and no amount of
   shrinking the elastic items wins; that is a scroll, not a layout bug. */
@media (max-width:720px) and (max-height:700px){
  /* 10 and 4, against the 14 and 5 the taller phones now get. Both were 18 and
     6, which were trims off a 22px banner margin — against 14 they would have
     been increases, i.e. this tier would have been spending height on the
     shortest screens on the page. The ~5px they save is not claimed back by
     --gh, which is a fixed line now; it lands as fold clearance, which is the
     safe direction to be wrong in down here. */
  .hero-banner{margin-bottom:10px}
  .hero-eyebrow{margin-bottom:4px}
  /* This is the one tier that does not move at all, and it was tried. 25vh
     measured 19.1px of fold clearance at 360x640 against the 24vh's ~25 — the
     headline trim did not cover the 6.4px the extra point of vh costs here,
     because at 640 tall there is no vh-scaled chrome left to trim. The note
     above spent real effort getting this screen off 5.5px and onto 25; that is
     not worth 6px of glass. The cap is left at 160 rather than raised with the
     other tiers because it never binds below 667 tall anyway — 24vh of this
     tier's 700px ceiling is 168 — and a cap that cannot bind is a decoration
     that will be read as a measurement later. */
  /* --gh is gone from here too, for the reason given where the 830 tier used to
     be. The line in .bottle resolves 194.7 at 667 tall and 169.2 at 640, against
     the 160 and 153.6 this hard-coded — so the shortest phones gain as well,
     they just gain less, which is the correct shape for a screen whose fixed
     chrome is a bigger share of it. The 6px margin stays: it is a real trim and
     the line's 20px clearance is budgeted against 16. */
  .bottle{margin:6px 0}
  /* The margin-top this tier used to pin at 10px is gone. The rule above it is
     3.3vh now, which on this tier's 640-700px of screen resolves to 18-23px on
     its own — the same "shorter screen, tighter rhythm" this override existed to
     express, arrived at by the slope instead of by a second number. Keeping the
     10 would have re-broken the gap on exactly the phones with the least room to
     spare, which is where a welded headline looks worst.
     It costs this tier ~11px of the ~25 it clears the fold by at 360x640, and
     that is the right way round: the clearance is empty space no element wants,
     while the gap it buys is a defect you can see from across the room. */
}

/* ---- laptops (1366x768, 1280x800, 1440x900) ----
   The banner costs the desktop hero 118px it did not used to spend, and a
   1440x900 screen had 159px of fold clearance to absorb that. A 768-tall laptop
   had nowhere near it and went 45.2px under. Measured stage at 1366x768: banner
   118, eyebrow 36, headlines 53.4 each, group 288.4, foot 123.4 — so the group
   is the only line with real give, and 34vh of a short screen is a big number
   for a lineup that is already the widest thing on the page.
   28vh gives 46px back, the eyebrow and foot margins another 14, which turns
   -45.2 into about +15. The banner is left at 88px: it is at its floor on the
   phone for the same reason, and shrinking it here would buy 12px for a strip
   that stops holding its copy.
   960, not the 820 this was first written at. 820 let 1440x900 through on the
   full 34vh and it cleared the fold by 17px — technically above it, but thinner
   than the 768 screen this rule was added for, which is the wrong way round.
   The 330px cap in --gh binds from 971px tall anyway, so past this boundary the
   group has stopped growing with the viewport and the clearance only improves.

   Moving the nav-side gap below the banner cost this tier another 11.9px net
   (+24 on the banner's bottom margin, -12.1 off .hero's top padding) and 768
   tall came down to 3px of clearance, which is above the fold only on paper.
   26px of the 38 is enough to keep the banner reading as its own object here —
   the ratio against the 14px above it is what does that work, not the absolute
   number — and on a screen this short the alternative is trimming the product
   lineup again, which is the one thing on the page that should not keep giving.
   That plus 27vh puts 1366x768 back to about +22.

   Re-based for the bigger lineup. 27vh was the number that made a 1440x900
   laptop — the commonest desktop there is — render 243px of column, of which
   the caption took 29, so the actual glass was 218px on a 1240px stage. That is
   the "suddenly looks small" this tier was quietly responsible for.
   36vh with --gh now meaning glass puts it at 324, a third taller and about
   80% more product by area, and it is affordable because the trims above give
   this tier back ~50px (16 off the hint's lane, 15 off the two headline lines,
   4 off the eyebrow, 4 off the foot, and the caption's 29 stops being charged
   to the glass at all).
   One vh cannot serve this whole tier, which is why there is a second one below
   it: the chrome around the lineup is mostly fixed, so as the screen shortens
   the free space falls away much faster than vh does. At 900 the budget is
   ~335px, at 768 it is ~226 — 37vh against 29vh for the same rule. */
@media (min-width:721px) and (max-height:960px){
  /* --gh is gone from here and from the tier below it. Everything the two long
     notes above worked out — that the free height falls away faster than vh
     does, so one vh cannot serve the whole range — is now stated directly as
     a line in .bottle, and a line has no seams to re-tune. What is left in
     these tiers is only the chrome trim, which is what they were really for. */
  .hero-banner{margin-bottom:20px}
  .hero-eyebrow{margin-bottom:7px}
  .hero-foot{margin-top:12px}
}

/* The short-laptop tier: 1366x768 and 1280x800 both land here. Everything above
   is written against 900 tall, where the fixed chrome is a smaller share of the
   screen; at 768 that same chrome leaves only ~250px, so the 38vh above (292)
   would put the buttons under the fold. 31vh is 238 — the same "as much as
   fits" answer the tier above gives, arrived at against this screen's own
   budget. 30vh, not the 31 the arithmetic suggested: measured at 1366x768, 31
   ran the hero 11px past its min-height and left the buttons 19px off the fold,
   which is the same "above it only on paper" this file has already been caught
   by twice. 30 is 230 and measures ~27 clear. */
@media (min-width:721px) and (max-height:830px){
  .hero-eyebrow{margin-bottom:6px}
  .hero-foot{margin-top:10px}
}

/* ============ reduced motion ============ */
/* ============================================================
   THE SCIENCE — how PDRN and peptides work
   ============================================================
   Colour is split down the middle on purpose and it is not decoration:
   warm (gold -> flame) is the face/PDRN side, violet is the hair/peptide
   side, matching the tone each product already carries in the banners, the
   showcase and the tiles. So a reader who has seen the gold bar on the home
   page arrives here and finds gold still means "face". That split is also
   what keeps this page off the purple complaint — violet gets one of the two
   mechanism bands rather than the whole page, and the hero is neutral cream.

   Everything here is CSS and inline SVG. No new raster assets: a lab diagram
   drawn as vector stays sharp, restyles with a token change, and costs no
   download. The bottle artwork is not involved on this page at all. */

/* the first band already sits under the nav; .page-sub adds top padding for
   sections that open on a colour, and the hero opens on paper instead */
.page-science main > section:first-child{padding-top:0}

/* ---- breadcrumb back to the parent page (used on how-we-formulate) ---- */
.crumb{
  display:inline-flex;align-items:center;gap:8px;
  margin-bottom:var(--s-3);
  font-family:var(--font-display),sans-serif;
  font-size:12.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;
  color:rgba(255,255,255,.56);
  transition:color .3s var(--ease);
}
.crumb span{transition:transform .3s var(--ease)}
.crumb:hover{color:var(--paper)}
.crumb:hover span{transform:translateX(-4px)}

/* ============ hero — the microscope field ============ */
.sci-hero{
  position:relative;
  background:linear-gradient(180deg,var(--paper) 0%,var(--cream) 62%,var(--cream-2) 100%);
  padding:clamp(28px,5vh,64px) 0 clamp(56px,8vh,104px);
  overflow:hidden;
}
.sci-hero-in{
  position:relative;
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:1.02fr .98fr;
  gap:clamp(24px,4vw,64px);align-items:center;
}
.sci-h{
  font-size:clamp(38px,6.2vw,84px);line-height:.94;letter-spacing:-.038em;
  color:var(--ink);max-width:11ch;
}
.sci-lede{
  margin-top:var(--s-3);max-width:36ch;
  font-size:clamp(15px,1.2vw,17.5px);line-height:1.66;color:var(--ink-64);
}
.sci-hero-cta{margin-top:var(--s-4);display:flex;flex-wrap:wrap;gap:12px}
/* these two are in-page jumps, not the page's primary action, so they do not
   inherit the hero CTA's entrance delay — they are already on screen */
.sci-hero-cta .cta{animation-delay:.2s}
.sci-hero-cta .cta-ghost{animation-delay:.3s}

/* ---- the dish ----
   The circle, its ring, its graduations, its reticle, its edge and its bubbles
   are all defined once, up in the science-orb section, and this page uses the
   same object under .dish-* names. Only the two things that genuinely differ
   are set here.

   Size is one of them, and it is smaller than it was. This used to be a
   38vw/500px sphere, which at any normal desktop width put a half-metre of pale
   gradient beside a headline and pushed itself past the edge of the hero. The
   shared 30vw/420px is the size the same object already is on the index page,
   so the two now read as one thing seen twice rather than as a hero prop and a
   trinket. */
.dish{margin-left:auto}

/* ============ the two molecules ============ */
.mol{padding:var(--s-7) 0 var(--s-6);background:var(--paper)}

/* On index the molecules band follows the product grid directly, and both are
   --paper, so the boundary between them is not a colour change -- it is 236px
   of plain white (the grid's 96px bottom padding plus this band's 140px top).
   Two stacked section paddings with nothing to separate reads as a hole in the
   page rather than as a break, and the reader loses the thread between "here
   are the three products" and "here is what is inside them".

   The negative margin eats 40px of the grid's bottom padding and the padding
   puts 40px back inside the band, so the white measures 96px -- one standard
   band gap instead of two. It is written as an adjacent-sibling rule rather
   than on .mol itself because the science page's copy of this band follows
   .sci-hero, where the full 140px is correct. */
.products + .mol{margin-top:calc(var(--s-4) * -1);padding-top:var(--s-4)}

.mol-grid{
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:1fr 1fr;gap:var(--s-3);
}
.mol-card{
  position:relative;overflow:hidden;
  padding:var(--s-4) var(--s-4) var(--s-3);
  border-radius:28px;border:1.5px solid var(--ink-08);
  transition:transform .5s var(--ease), box-shadow .5s var(--ease),
             border-color .5s var(--ease);
}
.mol-pdrn{background:linear-gradient(168deg,#FFFBF0,var(--gold-lt) 60%,#FFE9BE)}
.mol-pep {background:linear-gradient(168deg,#FBF8FF,var(--violet-lt) 60%,#E7DBFF)}
.mol-card:hover{transform:translateY(-4px);box-shadow:0 26px 54px rgba(26,23,32,.13)}
.mol-pdrn:hover{border-color:rgba(180,71,13,.30)}
.mol-pep:hover {border-color:rgba(109,40,217,.30)}

/* ---------------- the artwork ----------------
   Both illustrations are real 3-D models projected to 2-D by script.js, not
   drawn curves: the helix is sin/cos around a horizontal axis and the peptide
   is six atoms rotated by a yaw/pitch matrix and divided by depth. That is why
   almost nothing here sets a coordinate -- every cx/cy/r/d in the HTML is one
   authored frame that the loop then overwrites. It is authored rather than
   left empty so the card is a finished drawing before script.js parses, and
   still is one if JS never runs.

   Depth is carried by per-element opacity and radius, both written by the
   loop, so the CSS only owns colour and line weight. The one thing CSS must
   not own is any transition on those properties: the loop already eases the
   rotation, and a transition on top of it would smooth an already-smooth
   value and make the model lag the cursor. The only transitions below are on
   states the loop does not touch per frame. */
.mol-art{position:relative;height:clamp(132px,15.5vw,186px);margin-bottom:var(--s-3)}
.mol-art svg{width:100%;height:100%;display:block;overflow:visible}
@media (hover:hover) and (pointer:fine){
  /* the model answers the pointer, so say so before it is touched */
  .mol-art{cursor:grab}
}

/* the "move to explore" whisper: it exists to reveal that the drawing is not a
   drawing. Once the reader has moved over it the hint has done its job and
   script.js drops it for good, so it never becomes furniture. */
.mol-hint{
  position:absolute;right:2px;bottom:-2px;z-index:2;pointer-events:none;
  font-family:var(--font-display),sans-serif;
  font-size:9.5px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;
  opacity:.38;transition:opacity .5s var(--ease);
}
.mol-pdrn .mol-hint{color:var(--flame-dk)}
.mol-pep  .mol-hint{color:var(--violet)}
.mol-art.is-used .mol-hint{opacity:0}
@media (hover:none),(pointer:coarse){.mol-hint{display:none}}

/* ---- PDRN: a fragment of double helix ----
   The two backbones cross, so neither can be a single path: each frame the
   loop splits both strands at every front/back crossing and pours the pieces
   into these two elements. Everything in mol-bb-b is behind the axis and
   everything in mol-bb-f is in front, which is what makes the strands weave
   through each other instead of merely overlapping.

   The mask is the science. PDRN is not a strand, it is strand *offcuts* --
   short fragments cut out of salmon DNA -- so the drawing has to end mid-turn
   at both edges rather than tuck itself away neatly. Fading the whole group to
   nothing across the last 13% of the box reads as a shear rather than as an
   end, and it costs one gradient instead of thirty per-element opacities. */
.mol-bb{fill:none;stroke:var(--flame-dk);stroke-linecap:round;stroke-linejoin:round}
.mol-bb-b{stroke-width:2.4;opacity:.38}
.mol-bb-f{stroke-width:4.2}
.mol-rungs{
  fill:none;stroke:var(--flame);stroke-width:2.3;stroke-linecap:round;opacity:.5;
}
/* the base pair under the cursor. Its d is rewritten per frame; only the fade
   in and out is CSS, and that property is never touched by the loop. */
.mol-rung-hot{
  fill:none;stroke:var(--flame);stroke-width:5.4;stroke-linecap:round;
  opacity:0;transition:opacity .3s var(--ease);
}
.mol-art.is-focus .mol-rung-hot{opacity:.92}
/* a hairline rim is what stops the spheres reading as flat discs once the
   radial gradient is dimmed by depth */
.mol-node{stroke:rgba(126,47,5,.30);stroke-width:.6}

/* ---- Copper tripeptide: GHK + Cu(II), ball and stick ----
   Bonds are drawn under every atom on purpose. A bond always runs between two
   atom centres, so both of its ends belong under its own spheres; painting the
   whole stick layer first gives that for free and saves re-sorting seven more
   nodes every frame. Only the six atoms are depth-sorted.

   The dashes are coordination, not covalence -- the copper is held by three
   donor atoms, it is not bonded into the chain -- and drawing that difference
   is the whole point of the picture. They march toward the ion so the chelation
   reads as something happening rather than something drawn.

   The ion is the one warm thing on a violet card, and deliberately: it is a
   metal, not a peptide, and the picture stops making sense if it is violet
   like everything it is being held by. It is the gold, heated -- the same ramp
   the PDRN card runs on -- not a new hue, and at 27px across it is jewellery
   on the card rather than a second brand colour competing with the first. */
.mol-bond{fill:none;stroke:var(--violet-3);stroke-width:5.4;stroke-linecap:round;opacity:.9}
.mol-coord{
  fill:none;stroke:var(--flame-dk);stroke-width:2;stroke-linecap:round;
  stroke-dasharray:1.6 7;opacity:.6;
  animation:molCoord 1.7s linear infinite;
}
@keyframes molCoord{to{stroke-dashoffset:-8.6}}
.mol-atom{stroke:rgba(29,10,60,.22);stroke-width:.7}
.mol-halo{animation:molHalo 3.8s ease-in-out infinite}
@keyframes molHalo{0%,100%{opacity:.55}50%{opacity:1}}
.mol-art.is-focus .mol-halo{animation-duration:1.9s}
/* G, H and K sit on their own residues, so the molecule spells its own name.
   They stay upright while the model turns -- a billboard, like every molecular
   viewer -- because a letter that rotates away is a letter nobody reads. */
.mol-lbl{
  fill:#fff;font-family:var(--font-display),sans-serif;font-weight:900;
  text-anchor:middle;paint-order:stroke;
}
.mol-cu-t{
  fill:var(--flame-dk);font-family:var(--font-display),sans-serif;
  font-size:10.5px;font-weight:900;letter-spacing:.08em;text-anchor:middle;
}

.mol-tag{
  font-family:var(--font-display),sans-serif;
  font-size:11px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
}
.mol-pdrn .mol-tag{color:var(--flame-dk)}
.mol-pep  .mol-tag{color:var(--violet)}
.mol-name{
  margin-top:6px;
  font-size:clamp(38px,4.6vw,60px);line-height:.96;letter-spacing:-.04em;
  color:var(--ink);
}
.mol-claim{
  margin-top:10px;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(18px,1.7vw,23px);font-weight:800;letter-spacing:-.025em;
  line-height:1.2;color:var(--ink);
}
.mol-body{
  margin-top:10px;max-width:32ch;
  font-size:14.5px;line-height:1.66;color:var(--ink-64);
}
.mol-where{
  margin-top:var(--s-3);display:inline-flex;align-items:center;gap:7px;
  padding:7px 14px;border-radius:999px;
  background:rgba(255,255,255,.72);border:1px solid var(--ink-08);
  font-size:12.5px;font-weight:600;color:var(--ink);
}
.mol-where span{
  font-family:var(--font-display),sans-serif;
  font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-46);
}

/* ============ the two mechanism bands ============ */
.mech{padding:var(--s-7) 0}
.mech-face{background:linear-gradient(180deg,#FFFDF7,var(--gold-lt) 55%,#FFEFCE)}
.mech-hair{background:linear-gradient(180deg,#FCFAFF,var(--violet-lt) 55%,#EAE1FF)}
.mech-in{
  max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:1.04fr .96fr;
  column-gap:clamp(24px,4vw,64px);row-gap:var(--s-4);
  grid-template-areas:
    "head  head"
    "art   steps"
    "out   out";
  align-items:center;
}
.mech-head{grid-area:head}
.mech-head h2{font-size:clamp(34px,5.4vw,72px);line-height:.95;color:var(--ink);max-width:12ch}
.mech-face .eyebrow{color:var(--flame-dk)}
.mech-hair .eyebrow{color:var(--violet)}

.mech-art{
  grid-area:art;border-radius:24px;overflow:hidden;
  background:rgba(255,255,255,.6);border:1.5px solid var(--ink-08);
  box-shadow:0 24px 60px rgba(26,23,32,.10);
}
.mech-art svg{width:100%;height:auto;display:block}

/* ---- skin cross-section ---- */
.sk-derm{fill:#F4DCAE}
.sk-epi{fill:#FBEBCB}
.sk-corn{fill:#FFF7E6}
.sk-fib path{fill:none;stroke:#D89B36;stroke-width:2.6;stroke-linecap:round;opacity:.62}
.sk-vessel{fill:none;stroke:var(--flame);stroke-width:3.4;stroke-linecap:round;opacity:.75}
.sk-path{
  fill:none;stroke:var(--flame-dk);stroke-width:2;stroke-dasharray:3 8;
  stroke-linecap:round;opacity:.5;
  animation:mechFlow 2.4s linear infinite;
}
.sk-frag{fill:var(--flame-dk)}
.sk-frag.f1{animation:mechPulse 2.4s ease-in-out infinite}
.sk-frag.f2{animation:mechPulse 2.4s ease-in-out .35s infinite}
.sk-frag.f3{animation:mechPulse 2.4s ease-in-out .7s infinite}
.sk-cell circle{fill:#fff;stroke:var(--flame-dk);stroke-width:3}
.sk-cell-arm line{stroke:var(--flame-dk);stroke-width:3;stroke-linecap:round}
.sk-cell{animation:mechBreathe 3.6s ease-in-out infinite;transform-origin:196px 258px}

/* ---- scalp / follicle cross-section ---- */
.sc-deep{fill:#DCCEF7}
.sc-skin{fill:#F1EAFE}
.sc-canal{fill:#FFFFFF;stroke:var(--violet-3);stroke-width:2.4}
.sc-bulb{fill:#FFFFFF;stroke:var(--violet);stroke-width:3.4}
.sc-pap{fill:var(--violet-3)}
.sc-vessel{fill:none;stroke:var(--orchid);stroke-width:3.2;stroke-linecap:round;opacity:.55}
.sc-shaft{fill:none;stroke:var(--ink);stroke-width:6;stroke-linecap:round}
.sc-bead{fill:var(--violet)}
.sc-bead.b1{animation:mechPulse 2.4s ease-in-out infinite}
.sc-bead.b2{animation:mechPulse 2.4s ease-in-out .35s infinite}
.sc-bead.b3{animation:mechPulse 2.4s ease-in-out .7s infinite}
.sc-dorm path{fill:none;stroke:var(--violet-2);stroke-width:4;stroke-linecap:round;opacity:.5}
.sc-dorm ellipse{fill:none;stroke:var(--violet-2);stroke-width:3;stroke-dasharray:6 7;opacity:.75;
                 animation:mechBreathe 3.6s ease-in-out .8s infinite;transform-origin:452px 312px}

.sk-lbl text{
  fill:var(--ink-46);font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;text-anchor:end;
}
.sc-lbl text{
  fill:var(--ink-46);font-family:var(--font-display),sans-serif;
  font-size:13px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;text-anchor:middle;
}
.sk-pin circle,.sc-pin circle{fill:var(--ink)}
.sk-pin text,.sc-pin text{
  fill:var(--paper);font-family:var(--font-display),sans-serif;
  font-size:15px;font-weight:900;text-anchor:middle;
}

@keyframes mechFlow{to{stroke-dashoffset:-22}}
@keyframes mechPulse{0%,100%{opacity:.35;r:6}50%{opacity:1;r:9}}
@keyframes mechBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}}

/* ---- the three steps ---- */
.steps{grid-area:steps;display:grid;gap:var(--s-3)}
.steps li{display:grid;grid-template-columns:auto 1fr;column-gap:var(--s-2);align-items:start}
.steps b{
  font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:900;letter-spacing:.04em;
  padding-top:3px;
}
.mech-face .steps b{color:var(--flame-dk)}
.mech-hair .steps b{color:var(--violet)}
.steps h4{font-size:clamp(17px,1.6vw,21px);font-weight:900;letter-spacing:-.025em;color:var(--ink)}
.steps p{margin-top:5px;font-size:14.5px;line-height:1.62;color:var(--ink-64);max-width:34ch}

/* ---- the outcome line: the only number on the band, and a published one ---- */
.mech-out{
  grid-area:out;
  display:flex;align-items:baseline;justify-content:center;gap:14px;flex-wrap:wrap;
  padding-top:var(--s-4);border-top:1.5px solid var(--ink-14);
  text-align:center;
}
.mech-out b{
  font-family:var(--font-display),sans-serif;
  font-size:clamp(34px,4.4vw,56px);font-weight:900;letter-spacing:-.04em;line-height:1;
}
.mech-face .mech-out b{color:var(--flame-dk)}
.mech-hair .mech-out b{color:var(--violet)}
.mech-out span{font-size:15px;line-height:1.5;color:var(--ink-64)}

/* ---- the closing ink band gets its lift back ----
   .page-sub flattens .science because it is normally the first thing on the
   page; here it is the last, so the rounded top edge is wanted again. */
.page-science .science{margin-top:0;border-radius:36px 36px 0 0}
.uniq::before{background:radial-gradient(ellipse 50% 50% at 50% 50%,
  rgba(255,201,77,.22),transparent 70%)}

/* ============ narrow ============ */
@media (max-width:960px){
  .sci-hero-in{grid-template-columns:1fr;gap:var(--s-4)}
  .sci-h{max-width:none}
  /* same numbers the orb drops to at 900, so the shared circle stays shared */
  .dish{--dish:clamp(200px,58vw,300px);margin:0 auto}
  .mol-grid{grid-template-columns:1fr}
  .mech-in{
    grid-template-columns:1fr;
    grid-template-areas:"head" "art" "steps" "out";
  }
  .steps p{max-width:46ch}
}
@media (max-width:720px){
  .sci-hero{padding:var(--s-3) 0 var(--s-5)}
  .mol,.mech{padding:var(--s-6) 0 var(--s-5)}
  .mol-card{padding:var(--s-3);border-radius:22px}
  /* the models are a 360x130 viewBox, so on a 360px phone the art is drawn at
     about 1:1 and 132 is just enough height to hold it without letterboxing. */
  .mol-art{height:132px}
  .mol-card:hover{transform:none;box-shadow:none}
  .mech-out{flex-direction:column;align-items:center;gap:4px}
  .steps li{column-gap:12px}
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;
  }
  .sp-c{transform:none!important;opacity:1!important}
  .bottle,.cta,.scroll-hint,.hero-prod{opacity:1!important}
  .bottle-inner,.hp-in{transform:none!important}
  .reveal{opacity:1!important;transform:none!important}
  .lede .lw{color:#fff!important}
  .sc-glow{display:none}
  /* The science page is built almost entirely out of loops — a travelling
     dashed path, pulsing fragments. The blanket rule above collapses their
     duration but an infinite count would still repaint forever, so they are
     stopped outright and parked at a legible frame. The diagrams read fine
     static: the numbered pins, not the motion, carry the sequence.

     The bubbles are stopped, not hidden. They used to be deleted here because
     they were an entrance animation — a bubble that has not risen yet is not on
     screen at all — but they are suspended now, so a stopped one is simply a
     bubble sitting where it was authored. Taking them away would leave a reader
     on reduced motion looking at a different picture from everyone else, and
     the picture is the point; only the drift was ever the motion. */
  .bub i{animation:none!important}
  /* Both graduated rings are one infinite rotation; stopped outright rather
     than collapsed, since a 0.001ms duration on an infinite count still
     repaints. Parked at 0deg each is simply a graduated ring, which is the
     picture anyway — nothing depends on it having turned. */
  .orb-ticks,.dish-ticks{animation:none!important}
  /* The teardown keeps working — it is a control, not a decoration, and taking
     it away would remove information. The blanket rule above collapses the
     travel to nothing so the parts jump between assembled and exploded instead
     of sliding, which is the right trade. serum.js checks the same query and
     skips the mist, so no particle is ever created. */
  .td-rig{cursor:default}
  /* The pin comes off too. A pin's entire job is to buy time for travel, and
     there is no travel left to buy time for — three screens of scroll would
     purchase nothing but three screens of scroll, which is the sort of thing
     that makes a reduced-motion reader give up on a page. The section drops to
     its natural height and serum.js parks the parts open on sight, so this
     reader arrives at exactly the diagram everyone else scrolls to; they just
     do not have to travel to it. */
  .td-track{height:auto!important}
  .td-inner{position:static!important;min-height:0!important;padding-block:var(--s-6)!important}
  .td-scrub{display:none}
  /* The molecules are the one pair here that is driven from JS, so stopping
     their CSS is only half the job -- script.js checks the same query and never
     starts its loop, which leaves the authored frame on screen. That frame is a
     complete illustration, so the cards lose the motion and nothing else. */
  .mol-coord,.mol-halo,
  .sk-path,.sk-frag,.sk-cell,.sc-bead,.sc-dorm ellipse{animation:none!important}
  .mol-halo{opacity:.8}
  .mol-art{cursor:default}
  .sk-frag,.sc-bead{opacity:1}
  /* The curtain is deleted outright, not stopped. Everything else in this block
     is a picture that happens to move, so parking it at a frame keeps the reader
     looking at what everyone else looks at. This is the opposite: it is a
     performance, and it is a full-bleed panel standing between the reader and
     the page. Parked at any frame it is still a screen of sky they have to get
     past to reach the content they came for.

     script.js already removes it on this query, so in practice this rule is the
     belt to that braces: it covers the window between first paint and script
     execution, and the case where the script never runs at all. The skip button
     goes with it — with no sequence to skip it is an orphaned control. */
  .intro,.intro-skip{display:none!important}
}

/* ============ conversion layer ============
   The sweep, the confirmation panel and the sticky buy bar. All three are
   injected by convert.js, so none of this is reachable from the markup —
   deliberately: a page that drops the script tag loses the feature and keeps
   its layout, rather than rendering an empty shell of it. */

/* ---- the cart that crosses the screen ----
   A white sheet over the entire viewport for a beat over a second. z-index
   180 clears the buy bar (90), the toast (130) and the old fly-to-bag dot
   (140), but stays under the opening curtain at 200 — an add cannot happen
   while the intro is up, and if the ordering ever changed the curtain is
   the one thing that should still win.

   pointer-events is deliberately NOT none. The page underneath is invisible
   for the duration, and a click that lands on a control the shopper cannot
   see is worse than a click that lands on nothing; swallowing them also
   makes a panicked double-tap on Add impossible. */
.nx-sweep{
  position:fixed;inset:0;z-index:180;
  background:var(--paper);
  overflow:hidden;
  will-change:transform,opacity,clip-path;
}
/* The printed side of the page: everything with ink on it, tilting away as
   one sheet while the white stock underneath (the parent's own background)
   stays flat and whole. The creases are deliberately NOT in here — a fold
   shades the paper itself, so it cannot ride the same tilt as the printing.

   will-change is load-bearing rather than decorative. The parent's clip-path
   is animating, and a clip-path change repaints whatever it clips; promoting
   this layer means the compositor re-clips a ready-made texture instead of
   re-rasterising the wordmark and the cart sixty times a second. */
.nx-sweep-ink{
  position:absolute;inset:0;
  will-change:transform,opacity;
}
/* The supplied wordmark, unaltered — it ships pure black on transparency,
   so the white field alone is enough to render it black. No filter, no
   recolour, no type substituted for the artwork. */
.nx-sweep-logo{
  position:absolute;left:50%;top:30%;
  transform:translate(-50%,-50%);
  width:clamp(240px,34vw,460px);height:auto;
  will-change:transform,opacity;
}
/* Sized against the SHORT axis as well as the wide one. On a laptop the
   viewport is wider than it is tall, so a cart measured in vw alone grows
   until it is half the height of the screen and climbs into the wordmark.
   min() lets whichever axis is tighter set the size, which is the only way
   this stays a stacked composition on a 16:10 panel and on a phone. */
.nx-sweep-cart{
  --nx-cart:min(clamp(220px,38vw,360px),40vh);
  position:absolute;left:50%;top:62%;
  /* Also stated here, not only in the keyframes. A WAAPI transform replaces
     this one outright while it runs, so the animation still has to carry its
     own translate — but the moment it ends it stops applying, and without a
     resting value the cart would jump to a top-left anchor. It happens to be
     invisible today because the cart is faded out by then; that is luck, not
     design, and it would surface the instant the timing changed. */
  transform:translate(-50%,-50%);
  width:var(--nx-cart);height:var(--nx-cart);
  color:var(--ink);
  will-change:transform,opacity;
}
.nx-sweep-cart svg{width:100%;height:100%;display:block}
/* Stroke width is in viewBox units, so the 1.8 that reads as a hairline at
   22px would come out a 25px slab at 340. Pulled back so the outline stays
   confident instead of turning into a blob, and the wheels are filled —
   at this scale a 1.4-unit ring reads as two empty circles, not wheels. */
.nx-sweep-cart svg{stroke-width:1.35}
.nx-sweep-cart circle{fill:currentColor;stroke:none}
.nx-sweep-say{
  position:absolute;left:50%;top:88%;transform:translate(-50%,-50%);
  margin:0;white-space:nowrap;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(11px,1.5vw,13px);font-weight:800;
  letter-spacing:.24em;text-transform:uppercase;
  color:var(--ink-46);
}
/* The folds — and the only thing selling the crumple now that no pixels are
   actually displaced. Geometry alone never read as paper anyway: a shape
   that deforms while staying perfectly flat-lit looks like melting, because
   a crease is a lighting event before it is a shape one. So the whole job is
   done in shading, which costs one fade instead of a per-frame filter pass.

   Three stacked gradients over a full viewport is a genuinely expensive
   paint, and it is the reason the will-change below is not optional: painted
   once and promoted, it costs nothing to fade; left unpromoted, the parent's
   animating clip-path would re-rasterise all three on every single frame. */
.nx-sweep-crease{
  position:absolute;inset:0;opacity:0;pointer-events:none;
  will-change:opacity;
  background:
    /* Two fans of hard-edged wedges thrown from two different crush points.
       Parallel bands were the first attempt and they read as corrugation —
       evenly spaced stripes are a woven or folded-once surface, not a
       crushed one. Paper crumples around a few points and the facets
       RADIATE from them, so where these two fans cross they cut each other
       into irregular quadrilaterals, which is the actual geometry of a wad.
       Angles are deliberately uneven so no wedge repeats. */
    conic-gradient(from 12deg at 34% 28%,
      rgba(26,23,32,.14) 0 17deg, rgba(255,255,255,.50) 17deg 46deg,
      rgba(26,23,32,.10) 46deg 89deg, rgba(255,255,255,.34) 89deg 128deg,
      rgba(26,23,32,.16) 128deg 167deg, rgba(255,255,255,.46) 167deg 214deg,
      rgba(26,23,32,.07) 214deg 258deg, rgba(255,255,255,.40) 258deg 303deg,
      rgba(26,23,32,.13) 303deg 342deg, rgba(255,255,255,.30) 342deg 360deg),
    conic-gradient(from 197deg at 71% 73%,
      rgba(26,23,32,.12) 0 24deg, rgba(255,255,255,.42) 24deg 58deg,
      rgba(26,23,32,.16) 58deg 92deg, rgba(255,255,255,.28) 92deg 141deg,
      rgba(26,23,32,.08) 141deg 179deg, rgba(255,255,255,.44) 179deg 221deg,
      rgba(26,23,32,.15) 221deg 267deg, rgba(255,255,255,.32) 267deg 311deg,
      rgba(26,23,32,.09) 311deg 349deg, rgba(255,255,255,.38) 349deg 360deg),
    /* The volume, so the finished wad has a lit side and a shaded one. */
    radial-gradient(66% 66% at 38% 32%,rgba(255,255,255,.92),rgba(26,23,32,.20));
}
/* A phone is tall, so the same three percentage anchors that sit close
   together on a laptop drift a hundred-odd pixels apart here and stop
   reading as one lockup. Pulled in so the wordmark, the cart and the line
   stay a single object on the narrow screen. */
@media (max-width:640px){
  .nx-sweep-logo{top:34%}
  .nx-sweep-cart{top:60%}
  .nx-sweep-say{top:82%}
}

/* ---- confirmation panel ---- */
.nx-toast{
  position:fixed;z-index:130;
  right:clamp(14px,2.4vw,28px);bottom:clamp(14px,2.4vw,28px);
  width:min(362px,calc(100vw - 28px));
  padding:16px 16px 15px;
  background:var(--paper);color:var(--ink);
  border:1px solid var(--ink-08);border-radius:20px;
  box-shadow:0 32px 74px -30px rgba(26,23,32,.52);
  opacity:0;transform:translateY(18px) scale(.98);pointer-events:none;
  transition:opacity .38s var(--ease),transform .5s var(--ease);
}
.nx-toast.in{opacity:1;transform:none;pointer-events:auto}
.nx-toast-x{
  position:absolute;top:8px;right:10px;
  width:26px;height:26px;line-height:1;
  border:0;background:transparent;cursor:pointer;
  font-size:19px;color:var(--ink-46);border-radius:50%;
}
.nx-toast-x:hover{color:var(--ink);background:var(--ink-08)}
.nx-toast-head{display:flex;align-items:center;gap:12px;padding-right:22px}
.nx-toast-head img{
  width:52px;height:52px;flex:0 0 auto;
  border-radius:12px;object-fit:cover;background:var(--cream-2);
}
.nx-toast-what b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;
}
.nx-toast-what span{display:block;margin-top:2px;font-size:12.5px;line-height:1.35;color:var(--ink-64)}

/* ---- free-shipping gap ----
   A bar plus a number. The number is the part that works: "add ₹100 more"
   is an instruction and "free shipping over ₹999" is a policy, and only one
   of those has ever moved a basket. */
.nx-ship{margin-top:14px}
.nx-ship-bar{height:5px;border-radius:999px;background:var(--ink-08);overflow:hidden}
.nx-ship-bar i{
  display:block;height:100%;border-radius:999px;width:0;
  background:linear-gradient(90deg,var(--gold),var(--flame));
  transition:width .65s var(--ease);
}
.nx-ship p{margin:7px 0 0;font-size:12px;line-height:1.4;color:var(--ink-64)}
.nx-ship p b{color:var(--ink);font-weight:800}
.nx-ship.done .nx-ship-bar i{background:linear-gradient(90deg,var(--gold),var(--gold-2))}
.nx-ship.done p{color:var(--flame-dk);font-weight:600}

.nx-toast-cta{display:flex;gap:8px;margin-top:15px}
.nx-toast-cta .nx-go{
  flex:1 1 auto;display:inline-flex;align-items:center;justify-content:center;
  text-decoration:none;padding:12px 16px;
}
.nx-toast-cta .nx-keep{flex:0 0 auto}

/* ---- sticky buy bar ---- */
.nx-buybar{
  position:fixed;left:0;right:0;bottom:0;z-index:90;
  display:flex;align-items:center;gap:14px;
  padding:11px clamp(14px,3vw,28px);
  background:rgba(255,255,255,.93);
  -webkit-backdrop-filter:saturate(150%) blur(14px);
  backdrop-filter:saturate(150%) blur(14px);
  border-top:1px solid var(--ink-08);
  box-shadow:0 -16px 44px -30px rgba(26,23,32,.55);
  transform:translateY(102%);
  transition:transform .45s var(--ease);
}
.nx-buybar.in{transform:none}
.nx-buybar img{
  width:46px;height:46px;flex:0 0 auto;
  border-radius:10px;object-fit:cover;background:var(--cream-2);
}
.nx-buybar-txt{flex:1 1 auto;min-width:0}
.nx-buybar-txt b{
  display:block;font-family:var(--font-display),sans-serif;
  font-size:14px;font-weight:800;letter-spacing:-.02em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.nx-buybar-txt span{font-size:13px;font-weight:700}
.nx-buybar-txt s{margin-left:6px;font-weight:500;color:var(--ink-46)}
.nx-buybar-bag{
  flex:0 0 auto;text-decoration:none;color:var(--ink-64);
  font-size:13px;font-weight:600;padding:0 4px;
}
.nx-buybar-bag b{color:var(--ink);font-weight:800}
/* width:auto is load-bearing, not tidying. `.btn{width:100%}` in the
   max-width:720px block applies to every button on a phone, which is right for
   one standing in normal flow and wrong for this one: it is a flex item in a
   horizontal bar, so 100% resolved to the bar's whole 351px content box. With
   flex:0 0 auto it then refused to shrink, squeezed its `.nx-buybar-txt`
   sibling (flex:1 1 auto; min-width:0) down to literally 0px wide, and — since
   that sibling is overflow:visible — painted straight over the price still
   spilling out of it. The result on a 375px phone was 28px of "₹899" hidden
   behind an opaque violet button, on the one control that has to state what is
   being bought. The button's real content is 111px.
   This wins over the blanket rule on source order at equal specificity, so it
   must stay BELOW line ~5354. Do not "simplify" it by moving it up. */
.nx-buybar-add{flex:0 0 auto;width:auto;padding:13px 28px;font-size:14px}

/* Both are pinned to the bottom edge, so on a wide screen the panel would
   land on top of the bar. Above the phone breakpoint there is room for both,
   so the panel simply steps up over it; below it, the bar stands down instead
   (see the rule in the phone block). Scoped to min-width so that this more
   specific selector cannot leak past the breakpoint and undo that. */
@media (min-width:641px){
  body.nx-barred .nx-toast{bottom:98px}
}

@media (max-width:640px){
  .nx-toast{left:12px;right:12px;bottom:12px;width:auto}
  /* Both are anchored to the bottom edge, so on a phone they would sit on top
     of one another. The panel is the one carrying the checkout button, so the
     bar stands down while it is up. */
  body.nx-toasting .nx-buybar{transform:translateY(102%)}
  .nx-buybar{gap:10px;padding:9px 12px}
  .nx-buybar img,.nx-buybar-bag{display:none}
  .nx-buybar-add{padding:12px 20px;font-size:13.5px}
}

/* ---- the Full Glow Up Kit band ----------------------------
   This replaced a 300px .pdp-kit aside that lived inside the buy rail. The
   aside was not underperforming because of its copy; it was underperforming
   because a ₹2,399 offer cannot be sold at the width of a sidebar note. So it
   is a full-bleed band now, sitting directly under the buy rail where the
   decision is still open.

   No card, no border, no shadow, and that is deliberate rather than minimal:
   assets/pdp-kit.png is RGBA with a genuinely transparent surround, the same
   as every SKU frame on the site, and a panel drawn behind it would trace a
   hard rectangle around the blend the artwork exists to avoid. The section
   wash is the artwork's own three hues at a tenth of its alpha, positioned to
   sit under the same three products, so the band reads as the field the
   creative is standing in rather than a coloured stripe behind it. */
.kitband{
  margin-top:var(--s-5);
  padding:clamp(26px,3.6vw,52px) 0 clamp(30px,4vw,58px);
  /* One flat sheet of warm paper, and that is all it is.
     Three radial washes stood here — violet at 16%, amber at 36%, ember at 53%,
     each at .10 — placed to sit under the three products in the creative and
     echo the coloured bloom that used to be baked into pdp-kit.png. The bloom
     is gone (see tools/pdp_float.py: a hue behind white glass reads as a filter
     laid over the shot, not as the room it was shot in) and the echo has to go
     with it, or the section is still wearing the colour the art just took off.
     Flat rather than another gradient, because the creative now carries its own
     seamless falloff: two soft fields, one inside the other, is the thing that
     reads as a smudge. This is the same warm neutral that art falls off into,
     so the band is the paper the shot was taken on, continuing past its edge. */
  background:#F7F5F2;
}
.kitband-in{
  max-width:1420px;margin:0 auto;padding:0 var(--pad);
  display:grid;grid-template-columns:minmax(0,1.22fr) minmax(0,.78fr);
  gap:clamp(18px,2.6vw,52px);align-items:center;
}
.kitband-art{margin:0}
.kitband-art img{display:block;width:100%;height:auto}

/* .kitband-tag and .kitband-fine are gone, and they went for the same reason
   the rest of the page's small type went: a 10.5px "BEST VALUE" pill above a
   60px headline that already says The Full Glow Up Kit, and a 12.5px line
   below the buttons repeating free shipping, cash on delivery and the
   seven-day replacement. The ₹498 saving is the only claim either of them made
   that is not printed elsewhere, and that is in .kitband-price at 11.5px
   already. It is bigger now; see below.
   CAVEAT, AND IT IS A REAL ONE: the justification for deleting .kitband-fine
   was that all three of its claims also ran across the announce bar at the top
   of this page. The announce bar has since been removed sitewide, so that
   justification no longer holds — free shipping and cash on delivery are now
   stated nowhere on this page. Raised with the user; not re-added unilaterally. */
.kitband-h{
  margin:0;
  font-size:clamp(34px,4.4vw,60px);line-height:.96;letter-spacing:-.045em;
  /* At 1280 this sets to "The Full Glow Up / Kit" and leaves one three-letter
     word alone on the second line of a 56px display headline. Balance moves
     the break to "The Full Glow / Up Kit"; where it is unsupported the wrap is
     simply the one above, so nothing depends on it. */
  text-wrap:balance;
}
/* Figtree 700 at 21px, up from Inter at 15 in .64 grey. One sentence, and it
   is the only sentence in the band — it has to carry the whole argument for
   ₹2,399, which a caption cannot do. */
.kitband-lede{
  margin:16px 0 0;
  font-family:var(--font-display),sans-serif;font-weight:700;
  font-size:clamp(18px,1.62vw,23px);line-height:1.36;letter-spacing:-.022em;
  color:var(--ink-90);
}
.kitband-lede b{color:var(--ink);font-weight:900}

.kitband-price{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin:22px 0 0}
.kitband-price b{
  font-family:var(--font-display),sans-serif;
  font-size:clamp(32px,3.4vw,46px);font-weight:900;letter-spacing:-.045em;line-height:1;
}
.kitband-price s{font-size:19px;font-weight:600;color:var(--ink-64)}
/* Was 11.5px uppercase in a violet pill. Violet is spent on exactly one thing
   on this site — the button that takes money — and a saving is not that
   thing, so the pill goes and the number is carried by size and weight
   instead. 21px of Figtree 900 in --flame-dk, which is the one warm token
   that clears 4.5:1 as text on paper. */
.kitband-price em{
  font-style:normal;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(17px,1.5vw,21px);font-weight:900;letter-spacing:-.03em;
  color:var(--flame-dk);
}

/* Both buttons run at 17px in a 22px-tall pill — roughly double the rail's
   Add To Bag — because "huge and bold" is the whole brief for this block and a
   default-sized button under a 60px headline reads as an afterthought. */
.kitband-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px}
.kitband-add,.kitband-buy{
  justify-content:center;
  font-size:17px;padding:22px 30px;
}
/* Both full width, stacked, at every size. Side by side they need 280+170+12
   = 462px and the say column is 452px at 1280 and ~510px at 1440 — so the row
   wrapped on one common desktop size and did not on the next, which is the
   kind of layout that looks broken on whichever one the shopper is using.
   Stacked is also simply the right answer for this block: the brief was huge
   and bold, and the solid violet against the ink outline already carries the
   hierarchy without needing a width difference to say it again. */
.kitband-add,.kitband-buy{flex:1 1 100%}

@media (max-width:900px){
  .kitband{padding:clamp(18px,2.6vw,52px) 0 clamp(20px,3vw,58px)}
  .kitband-in{grid-template-columns:1fr;gap:4px}
  /* The art leads on a phone: it is the reason this block converts, and a
     headline above it would push it under the fold on a 667px screen. */
  .kitband-art{order:1;margin:0 -8px}
  /* ...and it now leads in less height than it did, without losing a pixel of
     subject. assets/pdp-kit.png is 1600x1000 with a transparent surround, and
     the three products only occupy y=64..884 — 64 rows of empty alpha above
     them and 116 below. 18% of the frame was paying rent as blank screen.
     aspect-ratio crops the box down to the subject band and cover scales into
     it; the 36% is not a taste call, it is where the split falls, because 64
     of the 180 cropped rows (35.6%) come off the top. Measured at 375 wide:
     224px -> 184px. Nothing is cut but air — do not push this past 1600/820
     or it starts eating product. Done in CSS rather than by re-exporting the
     PNG because the artwork is supplied brand material: the standing rule is
     to re-derive from it, never to overwrite it. */
  .kitband-art img{aspect-ratio:1600/820;object-fit:cover;object-position:center 36%}
  .kitband-say{order:2;text-align:center}
  .kitband-h{font-size:clamp(30px,8.6vw,40px)}

  /* The vertical rhythm here was inherited from the desktop column and never
     re-thought for a phone: 16px under the headline, 22px under the lede,
     18px above the buttons and 12px between them — 68px of gap separating
     four things that are all one offer. It costs more than it looks, because
     a phone already spends ~203px on fixed chrome (the sticky nav is ~110px
     and the buy bar 93px), so on a 667px screen the band was asking for more
     room than the screen has. Tightened rather than shrunk: every element
     keeps its own size, they simply stop standing so far apart. */
  .kitband-lede{margin-top:10px}
  .kitband-price{justify-content:center;margin-top:13px}
  .kitband-row{margin-top:13px;gap:10px}
  /* 19px of padding down to 15px. Still a 58px pill — well clear of the 44px
     tap minimum and still the largest control on the page, which was the
     brief. Below about 13px it stops reading as the headline act of the band
     and starts reading as an ordinary button, so that is the floor. */
  .kitband-add,.kitband-buy{font-size:15.5px;padding:15px 24px}
}

/* Short phones get one more pass, and only they do. A 667px or 640px screen
   carries exactly the same ~203px of fixed chrome as an 844px one but has
   ~200px less to pay it from, so the block above — which leaves 129px spare
   at 375x812 — was still 16px over at 375x667 and 33px over at 360x640.
   Keyed on height, not width, because that is the axis that actually runs
   out: a 375-wide phone is fine at 812 and is not at 667, and nothing here
   should touch the taller phone that already had the room.
   These are the last honest millimetres. Everything is still full size and
   legible; the buttons measure 51px, above the 44px tap minimum. If this block
   ever needs to give up more, take it from the lede — do not shrink the
   price or crop further into the art. */
@media (max-width:900px) and (max-height:730px){
  .kitband{padding:14px 0 16px}
  .kitband-lede{margin-top:6px;font-size:16.5px;line-height:1.3}
  .kitband-price{margin-top:8px}
  .kitband-row{margin-top:8px;gap:8px}
  .kitband-add,.kitband-buy{padding:12px 22px;font-size:15px}
}

@media (prefers-reduced-motion:reduce){
  .nx-toast,.nx-buybar,.nx-ship-bar i{transition:none}
}

/* ============================================================
   CONVERSION FURNITURE
   Supply tiers, the offer clock, the free-shipping rail, the
   trust strip and the WhatsApp float. Rendered or wired by
   conversion.js; anything it has no real data for stays hidden,
   so every rule below has to look right as an absence too.
   ============================================================ */

/* ---- the offer clock --------------------------------------
   Gold rather than red. A countdown is already the loudest thing
   on a page; putting it in an alarm colour on a cream site makes
   it read as a third-party widget bolted on, which is exactly the
   tell that makes shoppers discount the whole page. */
.pdp-clock{
  margin:10px 0 0;display:inline-flex;align-items:center;gap:9px;
  padding:8px 13px;border-radius:999px;
  background:var(--gold-lt);border:1px solid rgba(232,163,23,.34);
  font-size:12.5px;color:var(--ink);
}
.pdp-clock .cd-lab{color:var(--ink-64)}
.pdp-clock .cd-t{
  font-family:var(--font-display),sans-serif;font-weight:800;
  letter-spacing:.02em;
  /* Tabular figures are the whole reason this does not twitch. With
     proportional numerals a 1 is narrower than a 0, so the pill would
     resize on most ticks and drag the paragraph under it around. */
  font-variant-numeric:tabular-nums;
}
/* "and it ships today" — the half of the sentence that says why the number
   matters. Same weight as the label, so the time stays the only loud part. */
.pdp-clock .cd-tail,.sum-clock .cd-tail{color:var(--ink-64)}

/* ---- the claim window -------------------------------------------------
   Rendered by offer.js into an empty [data-offer] div, in one of three
   states, and the three look nothing like each other on purpose.

   THREE STACKED ROWS, THREE COLOURS, THREE JOBS. Gold above (when it
   ships), orange here (what it costs, for the next two minutes), violet
   below (buy it). Nothing in this rail is the same colour as the thing
   above it, because three warm pills in a column read as one decorative
   stack that the eye skips whole.

   Orange is --flame as a FILL carrying --ink text. That is the only legal
   way to use it — see the token block at the top of this file: --flame on
   paper is 3.14:1 and fails, --ink on --flame is 6.00:1 and passes. The
   ticking figure beside the button IS type on paper, so it takes
   --flame-dk, which is the text-safe end of the same ramp at 5.46:1. */
.pdp-offer{
  margin:12px 0 0;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
}

/* This is an <a> to wa.me, not a <button> — the code is issued by a person
   on WhatsApp, so the control's job is to open that chat. It keeps the
   button shape because it is still the primary action in this slot, and it
   keeps --flame because the brief for it was orange. The glyph is what tells
   you it leaves the site before you press it. */
.pdp-offer-btn{
  /* Grows to the rail but not past it, and drops the timer onto its own
     line under ~380px rather than squeezing both into one unreadable row. */
  flex:1 1 15rem;
  display:flex;align-items:center;justify-content:center;gap:10px;
  padding:13px 20px;border:none;border-radius:999px;cursor:pointer;
  background:var(--flame-hot);color:var(--ink);
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:clamp(14px,1.2vw,16.5px);letter-spacing:-.015em;
  transition:background .2s var(--ease), transform .2s var(--ease),
             box-shadow .25s var(--ease);
}
/* Hover keeps travelling up the same ramp to --flame-2. It is only four points
   of lightness from the rest state, which on its own would be too quiet to
   register — the lift and the glow are carrying most of the feedback, and the
   colour step is there so the button does not look inert on a machine where
   transforms are off. The glow is tinted to the new fill rather than left on
   the old #F2661A, or it reads as a shadow belonging to a different button. */
.pdp-offer-btn:hover{
  background:var(--flame-2);transform:translateY(-1px);
  box-shadow:0 8px 22px rgba(255,122,41,.38);
}
.pdp-offer-btn:active{transform:translateY(0)}
.pdp-offer-btn:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
/* the gap already spaces it; flex:none stops it being squeezed when the
   label wraps at the narrow end of the clamp */
.po-wa{flex:none}

/* The rupee figure, in a knocked-back well inside the button. It is the
   part people actually compare, and "Save ₹100" set at the same weight as
   the rest of the label disappears into it. */
.po-save{
  padding:3px 10px;border-radius:999px;
  background:rgba(26,23,32,.15);
  font-size:.86em;letter-spacing:-.01em;white-space:nowrap;
}

.pdp-offer-t{
  margin:0;display:flex;align-items:baseline;gap:6px;
  font-size:12.5px;color:var(--ink-64);white-space:nowrap;
}
.pdp-offer-t b{
  font-family:var(--font-display),sans-serif;font-weight:800;
  font-size:16.5px;color:var(--flame-dk);
  /* Same reason as the dispatch clock: without tabular figures the pill
     resizes on most ticks and drags the buttons under it around. */
  font-variant-numeric:tabular-nums;
}

/* Claimed. Deliberately NOT a second orange button — the chat is open and a
   button that cannot be pressed again is a button that looks broken. Flat,
   quiet, and it states the rupee figure a second time because that is the
   thing worth confirming.

   align-items goes to flex-start rather than center: this block wraps to two
   lines now that it carries the reopen link, and centring a 20px tick against
   two lines of 13.5px floats it into the gutter between them. */
.pdp-offer-done{
  margin:0;width:100%;display:flex;align-items:flex-start;gap:9px;flex-wrap:wrap;
  padding:11px 15px;border-radius:12px;
  /* tinted off --flame-hot, not --flame: this box replaces the button in the
     same slot, and a tint mixed from the previous orange puts two warm greys
     next to each other across a state change nobody asked to notice */
  background:rgba(255,122,41,.10);border:1px solid rgba(255,122,41,.32);
  font-size:13.5px;line-height:1.45;color:var(--ink-64);
}
.pdp-offer-done b{color:var(--ink);font-weight:800}
/* underlined because it sits mid-sentence inside a paragraph, where colour
   alone is not an affordance — and --flame-dk is the text-safe end of the
   warm ramp, 5.46:1 here. --flame itself would be 3.14:1 and unreadable. */
.po-relink{
  color:var(--flame-dk);font-weight:700;
  text-decoration:underline;text-underline-offset:2px;
}
.po-relink:hover{text-decoration-thickness:2px}
.po-tick{
  flex:none;display:grid;place-items:center;width:20px;height:20px;
  border-radius:50%;background:var(--flame);color:var(--ink);
  font-size:12px;line-height:1;
}

@media (prefers-reduced-motion:reduce){
  .pdp-offer-btn{transition:background .2s var(--ease)}
  .pdp-offer-btn:hover{transform:none}
}

/* The same clock in the bag. Full width and squared off rather than a pill,
   because in the summary column it is a row in a stack, not a badge floating
   in body copy. */
.sum-clock{
  margin:14px 0 0;display:flex;flex-wrap:wrap;align-items:center;gap:6px;
  padding:10px 13px;border-radius:12px;
  background:var(--gold-lt);border:1px solid rgba(232,163,23,.34);
  font-size:12.5px;line-height:1.45;color:var(--ink);
}
.sum-clock .cd-lab{color:var(--ink-64)}
.sum-clock .cd-t{
  font-family:var(--font-display),sans-serif;font-weight:800;
  letter-spacing:.02em;font-variant-numeric:tabular-nums;
}

/* ---- stock ------------------------------------------------ */
.pdp-stock{
  flex:1 1 100%;margin:0;text-align:center;
  font-size:12.5px;font-weight:700;color:var(--flame-dk);
}
.pdp-stock.is-out{color:var(--ink-46);font-weight:600}

/* ---- the free-shipping rail ------------------------------- */
.shipbar{margin-top:18px}
.shipbar-say{margin:0 0 8px;font-size:13px;color:var(--ink-64)}
.shipbar-say b{color:var(--ink);font-weight:700}
.shipbar-rail{
  display:block;height:6px;border-radius:999px;
  background:var(--ink-08);overflow:hidden;
}
.shipbar-rail i{
  display:block;height:100%;border-radius:999px;
  background:linear-gradient(90deg,var(--gold),var(--flame));
  transition:width .5s var(--ease);
}

/* .pdp-trust is gone. Four 12.5px ticked claims — cash on delivery, easy
   replacement, free shipping over ₹999, dermatologist formulated — sat in a
   bordered strip under the buy buttons on all three product pages. The reason
   for deleting it was that all four were already in the announce bar at the
   top of every page, so the strip was a second printing of the same four
   promises set smaller than the first: the fix for saying something twice is
   not to say it twice more loudly.

   THAT REASON NO LONGER HOLDS. The announce bar has been removed sitewide.
   Two of the four survive in .pdp-promise below the buttons (cash on delivery,
   seven-day replacement); the other two — free shipping over ₹999 and
   dermatologist formulated — are now stated nowhere on a product page.
   Flagged to the user rather than silently restored, because .pdp-promise is
   deliberately capped at three rows and what fills the gap is a copy
   decision, not a CSS one. */

/* The floating WhatsApp pill that used to live here is gone, at the user's
   request — the green .wa-float fixed to the bottom-right of all eleven
   pages, its :hover lift, its .is-yield collide-and-fade state, and the
   @media (max-width:860px) block that lifted it over the sticky buy bar and
   dropped its label. That media block held nothing else, so it went whole.
   conversion.js lost the matching whatsapp()/waYield()/YIELD_TO apparatus.
   CONFIG.whatsapp survives there: the kit's "Order on WhatsApp" anchors are
   a separate, real sale route and are untouched. #25D366 is now unused. */

/* The phone half of the first-screen work in the .pdp block near the top of
   this file. It is down here rather than up there because the base .pdp-clock
   rule further up this file sets margin:10px 0 0, and at equal specificity
   the later declaration wins — a max-width:560px rule written up in the .pdp
   block would have been silently overridden by that base rule, media query or
   not. It has to stay below .pdp-clock. */
@media (max-width:560px){
  /* The clock is display:inline-flex and shrink-wrapped to its own text. As a
     child of the block .pdp-buy that was automatic; as a flex item of the
     column .pdp-buy is now, align-items:stretch would blow the pill out to
     the full width of the rail. */
  .pdp-clock{margin:16px 0 0;align-self:flex-start}
}

@media (prefers-reduced-motion:reduce){
  .shipbar-rail i{transition:none}
}

/* ============================================================================
   THE BRAND FILM  (film.html)
   ==========================================================================*/
/* film.js measures .film-stage's bounding box and renders to it, so the stage
   must have a real height before the script runs — a stage that is auto-height
   around an unsized canvas measures zero and the film boots into a 1x1 buffer.
   Everything here is layout for that box plus the chrome floating over it. */

.page-film main > section:first-child{padding-top:0}

/* ---- the nav floats over the sky ----------------------------------------- */
/* The first version subtracted the nav's height from the stage. That was wrong
   twice over: the measured height is 78.8px, not the 61 I assumed, and it drops
   to 68.8 the moment .stuck trims the padding — so the stage would resize, and
   film.js re-bakes its buffers on every resize. Taking the nav out of flow makes
   the film exactly one screen tall at any nav height, and it is the better shot
   anyway: a solid white bar slicing the top off a sky is not a film.
   script.js flips .stuck at 24px of scroll, so this transparent state is only
   ever seen while the film is the thing on screen. */
.page-film #nav{position:fixed;top:0;left:0;right:0}

/* Scoped to :not(.stuck) rather than written twice — the instant the nav goes
   solid these simply stop matching and the site's own nav styling returns, with
   nothing to undo. */
.page-film #nav:not(.stuck){
  background:transparent;
  backdrop-filter:none;-webkit-backdrop-filter:none;
  border-bottom-color:transparent;
}
/* The wordmark ships dark, for cream pages. brightness(0) crushes it to a solid
   silhouette and invert(1) lifts that to white, which keeps the supplied
   geometry exactly and only changes its colour — no second logo file, and
   nothing redrawn. */
.page-film #nav:not(.stuck) .logo img{filter:brightness(0) invert(1)}
/* ...and it stands down for the last beat. The film ends by flying its own
   wordmark into this exact rect and etching it dark, and on this page the nav is
   fixed OVER the canvas — so the white one above would sit on the etched one
   below, white on white paper, and erase the payoff. film.js adds .film-washed
   when the frame has gone to paper and takes it off again on replay. Opacity,
   not display or visibility: film.js measures this img to find the slot, and a
   box with no geometry would send the mark to a fallback rect instead. */
.page-film.film-washed #nav:not(.stuck) .logo img{opacity:0}
.page-film #nav:not(.stuck) .nav-links a{
  color:#fff;text-shadow:0 1px 10px rgba(6,20,48,.45);
}
.page-film #nav:not(.stuck) .navbtn{
  background:rgba(255,255,255,.16);
  border-color:rgba(255,255,255,.6);
  color:#fff;
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
}
.page-film #nav:not(.stuck) .bagbtn{
  border-color:rgba(255,255,255,.5);color:#fff;
}

.film{
  position:relative;
  /* The under-colour is the film's own opening sky top stop. While the four
     bitmaps load the page shows the first frame's colour rather than white,
     so the fade-in reads as a lift in brightness, not a flash. */
  background:#0C2A5E;
}

.film-stage{
  position:relative;
  width:100%;
  /* svh, not vh. On iOS vh is the *large* viewport, so with the URL bar showing
     the stage runs ~90px past the fold and the scroll cue is never on screen. */
  height:100svh;
  min-height:440px;
  /* A 32:9 ultrawide would otherwise hand the film a 400px-tall letterbox with
     nowhere to put the clouds. Capping the height and centring keeps the
     composition upright on every desk. */
  max-height:1040px;
  overflow:hidden;
  isolation:isolate;
  /* The opening frame, standing in until the canvas has something to show.
     Same four stops as SKY[0] in film.js — keep them in step. */
  background:
    linear-gradient(180deg,#0C2A5E 0%,#2A63AE 42%,#93C0E4 78%,#E3D4BC 100%);
}

/* .is-ready lands in the same tick the canvas gets its context now — film.js
   boots the sky synchronously and waits for the photographs at frame zero, so
   there is no longer a gap to cover. The transition used to be 0.8s, which was
   sized for fading in over a stand-in gradient; against a canvas that is already
   painting the correct sky it is 0.8s of dimming the thing you came to see. What
   is left is just enough to stop the first frame arriving as a hard switch. */
.film-stage canvas{
  display:block;
  width:100%;height:100%;
  opacity:0;
  transition:opacity .22s var(--ease);
}
.film-stage.is-ready canvas{opacity:1}

/* ---- the chrome ---------------------------------------------------------- */
/* pointer-events go off on the layer and back on for the controls, so the film
   underneath still takes a click anywhere the chrome merely floats. */
/* justify-content is flex-end, not space-between: the chrome is one row now and
   it lives at the bottom. The top of this stage belongs to the fixed nav, and
   anything put up there lands underneath the logo and the bag button. */
.film-chrome{
  position:absolute;inset:0;z-index:2;
  display:flex;flex-direction:column;justify-content:flex-end;
  padding:18px clamp(16px,3.2vw,34px) 20px;
  pointer-events:none;
  opacity:0;transition:opacity .8s var(--ease) .25s;
}
.film-stage.is-ready .film-chrome{opacity:1}
.film-stage.is-failed .film-chrome{display:none}

.film-skip{
  pointer-events:auto;flex:0 0 auto;
  font-family:var(--font-display),sans-serif;
  font-size:12px;font-weight:700;letter-spacing:.01em;
  color:rgba(255,255,255,.86);
  background:rgba(10,26,58,.24);
  border:1px solid rgba(255,255,255,.30);
  border-radius:999px;padding:8px 15px;
  backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);
  cursor:pointer;
  transition:background .25s var(--ease),border-color .25s var(--ease),color .25s var(--ease);
}
.film-skip:hover{background:rgba(255,255,255,.92);border-color:transparent;color:#0C2A5E}
.film-skip:focus-visible{outline:2px solid #fff;outline-offset:3px}

.film-bottom{display:flex;align-items:center;gap:14px}

.film-play{
  pointer-events:auto;flex:0 0 auto;
  width:46px;height:46px;border-radius:50%;
  display:grid;place-items:center;
  color:#fff;
  background:rgba(10,26,58,.26);
  border:1px solid rgba(255,255,255,.34);
  backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px);
  cursor:pointer;
  transition:background .25s var(--ease),transform .25s var(--ease),color .25s var(--ease);
}
.film-play:hover{background:rgba(255,255,255,.94);color:#0C2A5E;transform:scale(1.06)}
.film-play:active{transform:scale(.96)}
.film-play:focus-visible{outline:2px solid #fff;outline-offset:3px}
.film-play svg{width:19px;height:19px;fill:currentColor;grid-area:1/1;display:none}
/* film.js writes data-state; the button carries all three glyphs and shows one.
   Swapping visibility beats swapping innerHTML — no layout, no re-parse. */
.film-play[data-state="play"]   .ico-play{display:block;margin-left:2px}
.film-play[data-state="pause"]  .ico-pause{display:block}
.film-play[data-state="replay"] .ico-replay{display:block}

.film-track{
  flex:1 1 auto;height:3px;border-radius:2px;
  background:rgba(255,255,255,.26);
  overflow:hidden;
}
/* scaleX on a full-width bar, not a width animation: this runs every single
   frame of a twenty-second film, and width would relayout on all of them. */
.film-bar{
  display:block;height:100%;width:100%;
  background:rgba(255,255,255,.95);
  border-radius:2px;
  transform:scaleX(0);transform-origin:0 50%;
}

.film-hint{
  position:absolute;left:50%;bottom:64px;transform:translateX(-50%);
  margin:0;white-space:nowrap;
  font-size:12px;font-weight:600;letter-spacing:.02em;
  color:rgba(255,255,255,.80);
  text-shadow:0 1px 3px rgba(6,20,48,.55);
}
.film-hint[hidden]{display:none}

/* ---- scroll cue ---------------------------------------------------------- */
.film-scroll{
  position:absolute;left:50%;bottom:-21px;z-index:3;
  transform:translateX(-50%);
  width:42px;height:42px;border-radius:50%;
  display:grid;place-items:center;
  color:var(--ink);
  background:var(--paper);
  box-shadow:0 10px 26px -10px rgba(10,26,58,.5);
  animation:film-bob 2.6s var(--ease) infinite;
}
.film-scroll svg{width:17px;height:17px}
.film-scroll:hover{color:var(--violet)}
@keyframes film-bob{
  0%,100%{transform:translate(-50%,0)}
  50%    {transform:translate(-50%,5px)}
}

/* ---- the no-JS / broken-asset floor -------------------------------------- */
/* Four PNGs are the film's entire dependency. If any of them 404s the canvas
   stays a flat gradient, which on a page whose only job is the product would be
   a dead end — so the stage falls back to the bottle, the tagline and a way to
   buy. */
.film-fallback{
  position:absolute;inset:0;z-index:4;
  display:none;
  flex-direction:column;align-items:center;justify-content:center;gap:18px;
  padding:36px 20px;text-align:center;
}
.film-stage.is-failed .film-fallback{display:flex}
.film-stage.is-failed canvas{display:none}
.film-fallback img{height:min(46vh,340px);width:auto;filter:drop-shadow(0 24px 40px rgba(6,20,48,.35))}
.film-fallback p{
  margin:0;font-family:var(--font-display),sans-serif;
  font-size:clamp(22px,4vw,34px);font-weight:800;letter-spacing:-.02em;
  color:#fff;text-shadow:0 2px 14px rgba(6,20,48,.5);
}
/* Without JS the canvas never gets .is-ready, so it never fades in and the
   stage would be an empty gradient. Show the floor instead. */
html:not(.js) .film-fallback{display:flex}
html:not(.js) .film-chrome{display:none}

/* ---- the band under the film --------------------------------------------- */
.film-after{
  /* The fixed nav would otherwise land on top of the eyebrow when the scroll cue
     jumps here. */
  scroll-margin-top:70px;
  background:var(--cream);
  padding:clamp(56px,8vw,104px) var(--pad) clamp(52px,7vw,92px);
  text-align:center;
}
.film-wrap{max-width:660px;margin:0 auto}
.film-h{
  margin:10px 0 0;
  font-family:var(--font-display),sans-serif;
  font-size:clamp(28px,5vw,46px);font-weight:900;letter-spacing:-.035em;line-height:1.04;
  color:var(--ink);
}
.film-p{
  margin:16px auto 0;max-width:52ch;
  font-size:clamp(14.5px,1.6vw,16.5px);line-height:1.66;color:var(--ink-64);
}
.film-cta{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:28px}
.film-note{
  margin:20px 0 0;font-size:12.5px;font-weight:600;letter-spacing:.01em;
  color:var(--ink-46);
}

@media (max-width:760px){
  .film-stage{min-height:520px}
  .film-chrome{padding:14px 14px 18px}
  .film-play{width:42px;height:42px}
  .film-play svg{width:17px;height:17px}
  .film-hint{bottom:58px;font-size:11.5px}
  .film-skip{font-size:11.5px;padding:7px 12px}
  .film-cta .cta{width:100%;max-width:340px}
}

@media (prefers-reduced-motion:reduce){
  .film-scroll{animation:none}
  .film-stage canvas,.film-chrome{transition:none}
}
