/* =========================================================================
   Purify Beauty — landing page
   Monochrome editorial. Polish from typography, whitespace, tonal contrast.
   ========================================================================= */

/* ---------- Design tokens ---------- */
:root {
  /* color */
  --c-bg:        #FFFFFF;
  --c-ink:       #3A3A3A;   /* headlines — graphite */
  --c-ink-2:     #494949;   /* strong body — graphite */
  --c-grey:      #6B6B6B;   /* secondary copy */
  --c-grey-soft: #9A9A9A;
  --c-line:      #E6E6E6;   /* hairline dividers */
  --c-silver:    #C8C8CC;   /* the "premium" accent — used sparingly */
  --c-wash:      #F7F7F5;   /* faint off-white section */

  /* depth — all neutral; no hue. Two-layer shadows = premium lift, not a blurry drop. */
  --c-surface:   #FBFBFA;   /* elevated white, a hair off pure #FFF */
  --shadow-sm: 0 1px 2px rgba(17,17,17,.05), 0 1px 1px rgba(17,17,17,.04);
  --shadow-md: 0 2px 4px rgba(17,17,17,.05), 0 6px 14px rgba(17,17,17,.07);
  --shadow-lg: 0 4px 10px rgba(17,17,17,.06), 0 14px 34px rgba(17,17,17,.10);

  /* type — Oswald: bold condensed grotesque, matching the box ("PURIFY YOUR HAIR") */
  --font:       "Oswald", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --font-serif: var(--font);
  --font-sans:  var(--font);
  --font-body:  "Cormorant Garamond", Georgia, "Times New Roman", serif;  /* elegant editorial subtext */

  /* spacing / layout */
  --maxw:        1180px;
  --measure:     680px;
  /* horizontal gutter grows to clear the notch in landscape (safe-area insets are 0 in
     portrait, so the page is unchanged there). Threads through every .wrap + the nav. */
  --gutter:      max(clamp(1.25rem, 5vw, 2.5rem), env(safe-area-inset-left), env(safe-area-inset-right));
  --section-y:   clamp(4.5rem, 9.5vw, 7rem);   /* generous but tight rhythm */

  /* motion */
  --t:           200ms ease;
  --t-slow:      700ms cubic-bezier(0.22, 0.61, 0.36, 1);

  /* the brand mark's exact hexagon silhouette (measured from pb-mark.png: flat top/bottom,
     pointed left/right). Pair with aspect-ratio: 481/421 so the hexagon meets every edge. */
  --hex:         polygon(24% 0%, 76% 0%, 100% 49%, 76% 100%, 24% 100%, 0% 49%);
}

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
  -webkit-tap-highlight-color: transparent;   /* no grey flash on tap — we add our own feedback */
}
body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-ink-2);
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.7;
  font-weight: 500;            /* a hair bolder, to match the box */
  text-transform: uppercase;   /* entire site in all caps */
  letter-spacing: .03em;       /* narrower caps want a touch more air */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Letterpress engrave for the all-caps Oswald: a faint shadow above each glyph + a
     light lower lip, so the type reads as pressed into the surface. The Cormorant serif
     subtext opts out (below), and dark-surface type (orbs) keeps its own deeper engrave. */
  text-shadow: 0 -1px 1px rgba(0, 0, 0, .10), 0 1px 0 rgba(255, 255, 255, .7);
}
/* serif editorial subtext stays flat — the engrave is only the all-caps Oswald */
.lede, .body, .hero__sub, .card__desc, .pullquote, .founder__sign, .site-footer__tag { text-shadow: none; }
img, svg { display: block; max-width: 100%; }
img { height: auto; }

/* Tactile feedback on touch (we removed the default tap highlight): a quick, quiet
   press response on the interactive elements, in keeping with the calm motion. */
@media (hover: none) {
  .btn:active, .nav__cta:active, .nav__menu a:active,
  .nav__toggle:active, .flip-step__inner:active, .approach__zoom-close:active,
  .site-footer__links a:active {
    opacity: .6;
    transition: opacity .06s ease;
  }
}

/* Film grain — one fixed overlay gives the whole page a paper-tooth surface.
   z-index 70: above content, below header(80)/menu(85)/intro(120)/progress(130).
   Static (no animation), so no reduced-motion gating needed. */
body::after {
  content: ""; position: fixed; inset: 0;
  z-index: 70; pointer-events: none;
  opacity: .035; mix-blend-mode: multiply;     /* invisible on near-black → dark .shows stays clean */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size: 180px 180px;
}
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }
h1, h2, h3 { margin: 0; font-weight: 600; }
p { margin: 0; }
:focus-visible { outline: 2px solid var(--c-ink); outline-offset: 3px; border-radius: 2px; }

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--c-ink); color: #fff; padding: .6rem 1rem; z-index: 200;
  font-size: .85rem; letter-spacing: .04em;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* ---------- Layout helpers ---------- */
.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.measure { max-width: var(--measure); }
.center { margin-inline: auto; text-align: center; }
/* centered sections: the title is capped at 18ch, so center the block itself (it would
   otherwise hug the left and read as off-center, e.g. the Contact heading) */
.center .section-title,
.center .lede { margin-inline: auto; }
.section { padding-block: var(--section-y); }

/* offset anchor targets for the fixed header */
:where(section[id], #top) { scroll-margin-top: 84px; }
body.menu-open { overflow: hidden; }

/* Menu open → full-screen overlay. The header's backdrop-filter would otherwise be the
   containing block for the fixed full-screen menu and collapse it to the header bar, so
   neutralize the frost while the menu is open. The reveal uses opacity (not a transform)
   precisely so it can't reintroduce that containing block. main.js holds .menu-open until
   the overlay finishes closing, so the frost isn't restored mid-close. */
body.menu-open .site-header {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-color: transparent;
  box-shadow: none;
  opacity: 1 !important;
  visibility: visible !important;
  pointer-events: auto !important;
}
body.menu-open .site-header::before { opacity: 0; }
body.menu-open .scroll-progress { opacity: 0; }   /* hide the hairline over the overlay */

/* ---------- Shared type ---------- */
.section-title {
  font-family: var(--font-serif);
  color: var(--c-ink);
  font-size: clamp(1.9rem, 4.6vw, 3rem);
  line-height: 1.1;
  letter-spacing: .01em;
  max-width: 18ch;
}
.lede {
  font-family: var(--font-body);
  text-transform: none;          /* let Cormorant's lowercase carry the editorial feel */
  font-size: clamp(1.3rem, 2.4vw, 1.7rem);
  line-height: 1.45;
  font-weight: 500;
  color: var(--c-ink-2);
  margin-top: 1.5rem;
}
.body {
  font-family: var(--font-body);
  text-transform: none;
  font-size: clamp(1.25rem, 2.2vw, 1.55rem);
  line-height: 1.5;
  font-weight: 500;
  color: var(--c-ink-2);
  margin-top: 1.25rem;
}
.measure .body + .body,
.measure .lede + .body { margin-top: 1.25rem; }

/* =========================================================================
   Intro veil (the signature moment)
   ========================================================================= */
#intro {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: grid;
  place-items: center;
  background: var(--c-bg);
  color: var(--c-ink);
  /* No-JS / fallback safety: always fade away even if the script never runs. */
  animation: introFallback .8s ease 4.4s forwards;
}
.intro__mark {
  width: clamp(104px, 19vw, 168px);
  aspect-ratio: 560 / 499;          /* the real logo's proportions */
  position: relative;
  overflow: hidden;
  background: #dcdcdc;              /* the "empty" monogram before it inks in */
  /* once the fill completes (~3.25s) the mark stamps down — punch, thud, settle */
  animation: markStamp .55s cubic-bezier(.2, .7, .2, 1) 3.2s;
  -webkit-mask: url(../img/pb-mark.png) center / contain no-repeat;
          mask: url(../img/pb-mark.png) center / contain no-repeat;
}
/* PB "loading fill" — an ink fill rises through the whole monogram, bottom to top. */
.intro__fill {
  position: absolute; inset: 0;
  background: var(--c-ink);
  transform: translateY(100%);
  animation: pbFill 3s cubic-bezier(.42,.04,.24,1) .25s forwards;
}

/* Impact — the filled mark stamps down (sharp punch, brief undershoot, settle)
   while a burst of fine silver dust explodes outward from behind the logo.
   The dust motes are spans injected by main.js (behind the mark, so they emerge
   from under its edges); each carries its own trajectory/size/timing in CSS
   vars. transform + opacity only — fully compositor-driven, no paint/layout per
   frame, so the burst holds 60fps. */
.intro__dust { position: absolute; inset: 0; pointer-events: none; }
.intro__dust span {
  position: absolute; left: 50%; top: 50%;
  width: var(--ds, 4px); height: var(--ds, 4px);
  border-radius: 50%;
  /* a soft-edged silver mote with a faint top-light, like metal dust */
  background: radial-gradient(circle at 38% 32%,
    #EFEFF1 0%, var(--dc, #C2C2C8) 55%, rgba(173, 173, 180, 0) 100%);
  opacity: 0;
  transform: translate(-50%, -50%) scale(.5);
  will-change: transform, opacity;
  animation: dustBurst var(--ddur, .8s) cubic-bezier(.12, .75, .25, 1) var(--ddelay, 3.2s) forwards;
}
/* explode fast from the mark's edge, decelerate, dissipate — motes shrink as
   they fade. --dx0/--dy0 = launch point just outside the logo's silhouette. */
@keyframes dustBurst {
  0% {
    opacity: 0;
    transform: translate(calc(-50% + var(--dx0, 0px)), calc(-50% + var(--dy0, 0px))) scale(1);
  }
  8%   { opacity: 1; }
  70%  { opacity: .9; }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(.3);
  }
}

/* the stamp: sharp punch up + flash, undershoot (the "thud"), tiny rebound, settle */
@keyframes markStamp {
  0%   { transform: scale(1);     filter: brightness(1); }
  16%  { transform: scale(1.08);  filter: brightness(1.65); }
  46%  { transform: scale(.982);  filter: brightness(1.04); }
  70%  { transform: scale(1.008); filter: brightness(1); }
  100% { transform: scale(1);     filter: brightness(1); }
}
/* JS-driven dismissal (precise, releases scroll). Overrides the fallback. */
#intro.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .8s ease, visibility 0s linear .8s;
  animation: none;
}
body.intro-active { overflow: hidden; }

@keyframes pbFill        { to { transform: translateY(0); } }
@keyframes introFallback { to { opacity: 0; visibility: hidden; pointer-events: none; } }

/* =========================================================================
   Header / Nav
   ========================================================================= */
.site-header {
  position: fixed;
  /* Floating liquid-glass capsule. Float below the notch/top edge; inset symmetrically
     so margin-auto centers it WITHOUT a transform — a transform here would become the
     containing block for the fixed full-screen mobile menu and collapse it (see §6.3). */
  top: max(0.7rem, calc(env(safe-area-inset-top, 0px) + 0.4rem));
  left: clamp(.6rem, 2vw, 1rem);
  right: clamp(.6rem, 2vw, 1rem);
  margin-inline: auto;
  max-width: var(--maxw);
  z-index: 80;
  border-radius: 999px;                 /* capsule — radius caps at half-height */
  /* gunmetal liquid glass — dark near-neutral tint, kept VERY translucent so the backdrop
     smokes through (far more see-through than a solid bar). Within strict-monochrome: only
     the faintest cool lean. The tint is CONSTANT (it does NOT shift on scroll). Because the
     glass is this thin, the light lockup carries its OWN contrast over the white/silver
     sections via text-shadow (wordmark + links) and drop-shadow (mark + toggle) below —
     those shadows are invisible over the dark sections where the bar first reveals. */
  background: linear-gradient(180deg, rgba(62, 65, 70, .32), rgba(46, 48, 52, .22));
  -webkit-backdrop-filter: blur(20px) saturate(165%);
          backdrop-filter: blur(20px) saturate(165%);
  border: 1px solid rgba(255, 255, 255, .20);
  /* bright specular rim on the dark body = polished-metal read; depth stays neutral */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .46),
    inset 0 -1px 1px rgba(255, 255, 255, .12),
    0 3px 10px rgba(17, 17, 17, .14),
    0 18px 42px rgba(17, 17, 17, .24);
  transition: opacity .5s ease, visibility 0s, background var(--t), box-shadow var(--t), border-color var(--t);
}
/* glossy sheen catch across the top of the pill (paints behind the nav content) */
.site-header::before {
  content: ""; position: absolute; inset: 0;
  border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, .26) 0%, rgba(255, 255, 255, 0) 50%);
}
/* The header is fully consistent on scroll — no colour, shadow, or size change. The JS
   still toggles `.is-scrolled`, but nothing keys off it visually any more (kept harmless
   in case a future scroll affordance wants it). */

/* ----- Header entrance — the same soft "bounce" the intro logo does (see markStamp),
   just subtler and without the dust burst: the capsule fades in and pops — a small punch
   up, a soft undershoot (the "thud"), a tiny rebound, settle. Same curve + duration as
   the logo stamp for a consistent feel. One-time, fired by main.js on `intro-done`.
   transform/filter are transient and only while the menu is closed (right after the
   intro); `backwards` fill clears them at rest, so they never leave the value that would
   create the fixed-menu containing-block trap. ----- */
.site-header.is-entering { animation: headerPop .55s cubic-bezier(.2, .7, .2, 1) .15s backwards; }
@keyframes headerPop {
  0%   { opacity: 0; transform: scale(1);     filter: brightness(1); }
  16%  { opacity: 1; transform: scale(1.03);  filter: brightness(1.1); }   /* punch up — the bounce */
  46%  {             transform: scale(.992);  filter: brightness(1.02); }  /* soft undershoot — the thud */
  72%  {             transform: scale(1.004); }                            /* tiny rebound */
  100% { opacity: 1; transform: scale(1);     filter: brightness(1); }
}
/* JS hides the nav over the hero and FADES it in once you scroll past it — the big
   "Pūrify Beauty" hero title hands the name off to the bar. A fade (NOT a transform) is
   used deliberately: a transform here becomes the containing block for the fixed
   full-screen mobile menu and collapses it. Gated on .nav-managed so the header stays
   visible if the script never runs. */
.nav-managed .site-header {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .4s ease, visibility 0s linear .4s, background var(--t), border-color var(--t);
}
.nav-managed .site-header.is-revealed {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity .5s ease, visibility 0s, background var(--t), border-color var(--t);
}
/* graceful fallback where backdrop-filter is unsupported — keep the bar readable */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  /* no blur to lean on here, so keep a bit more body than the glass version — still well
     down from the old near-solid bar. The lockup's text/drop shadows carry the rest. */
  .site-header { background: rgba(50, 53, 58, .70); }
}
.nav {
  max-width: 100%;
  /* inner padding for the pill — the capsule itself now owns width + notch clearance.
     Slim vertical padding keeps the pill compressed. */
  padding: .28rem clamp(.9rem, 2.4vw, 1.2rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  position: relative;   /* positioning context for the absolutely-placed mobile toggle */
}

/* brand lockup */
.brand { display: inline-flex; align-items: center; gap: .6rem; color: var(--c-ink); }
.brand__mark {
  width: 30px; height: 27px;        /* real logo aspect ≈ 1.12 : 1 */
  flex: none;
  background: var(--c-ink);
  -webkit-mask: url(../img/pb-mark.png) center / contain no-repeat;
          mask: url(../img/pb-mark.png) center / contain no-repeat;
}
.brand__word {
  font-family: var(--font);
  text-transform: uppercase;
  letter-spacing: .22em;
  font-weight: 700;
  font-size: .98rem;
  line-height: 1;
  padding-left: .1em;
}
/* Gunmetal glass → the header lockup reads light. Scoped to .site-header so the footer
   brand (which sits on the white page) keeps its dark ink. */
.site-header .brand { color: #F2F2F4; }
/* slightly smaller mark in the header so the pill sits slimmer (footer mark keeps its size).
   drop-shadow (NOT box-shadow — the mark is mask-clipped, see §hard-rule-12) keeps the light
   hexagon from vanishing where the translucent glass sits over a white section. */
.site-header .brand__mark {
  width: 23px; height: 21px; background: #E9E9EC;
  filter: drop-shadow(0 1px 2px rgba(12, 14, 16, .5));
}

/* The header wordmark hands off from the hero — hidden while the hero's big PŪRIFY is on
   screen, then RESOLVES in once it's gone (main.js toggles .show-word on the header). A soft
   focus-pull (blur→sharp) + fade on a slow ease — echoing the hero brand's own reveal — so it
   settles in elegantly instead of snapping. The filter is on the wordmark span only (it has no
   fixed descendants), so it never becomes the containing block for the fixed mobile menu
   (§hard-rule-12 intact). Gated on .js so it's visible without JS; scoped to .site-header. */
.js .site-header .brand__word {
  opacity: 0;
  filter: blur(5px);
  transition: opacity .8s cubic-bezier(.2, .7, .2, 1), filter .8s cubic-bezier(.2, .7, .2, 1);
}
.site-header.show-word .brand__word { opacity: 1; filter: blur(0); }
/* contrast insurance for the light wordmark over white sections (the glass is very thin now);
   invisible over the dark sections where the bar first appears. */
.site-header .brand__word { text-shadow: 0 1px 2px rgba(12, 14, 16, .5); }

.nav__menu { display: flex; align-items: center; gap: clamp(1.4rem, 3vw, 2.4rem); line-height: 1.2; }
.nav__menu a {
  font-size: .82rem;
  letter-spacing: .08em;
  color: rgba(255, 255, 255, .85);   /* light on the gunmetal glass */
  text-transform: uppercase;
  position: relative;
  line-height: 1.2;                   /* tighter than the body's 1.7 → slimmer pill */
  padding-block: .1rem;
  /* contrast insurance over white sections (very translucent glass); invisible over dark. */
  text-shadow: 0 1px 2px rgba(12, 14, 16, .5);
  transition: color var(--t);
}
.nav__menu a:hover { color: #fff; }
.nav__menu a::after {
  content: "";
  position: absolute; left: 0; bottom: -.1rem;
  width: 100%; height: 1px;
  background: var(--c-silver);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--t);
}
.nav__menu a:hover::after { transform: scaleX(1); }
/* Contact is a plain nav link (no pill outline) on desktop too — it inherits .nav__menu a
   styling, including the silver underline-on-hover, so it matches the other items. */

/* mobile toggle */
.nav__toggle {
  display: none;
  width: 40px; height: 40px;
  background: none; border: 0; cursor: pointer;
  position: relative; z-index: 90;
}
.nav__toggle span {
  display: block; width: 22px; height: 1.5px;
  background: #ECECEF; margin: 5px auto;       /* light on the gunmetal glass */
  /* keep the light bars visible where the thin glass sits over a white section */
  filter: drop-shadow(0 1px 1.5px rgba(12, 14, 16, .45));
  transition: transform var(--t), opacity var(--t);
}
.nav__toggle[aria-expanded="true"] span { background: #ECECEF; }   /* X stays light on the gunmetal drawer */
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { transform: translateY(-6.5px) rotate(-45deg); }

/* Drawer scrim — inert off mobile; the phone drawer (below) turns it on. */
.nav__scrim { display: none; }

/* =========================================================================
   Hero — scroll-scrubbed video
   .hero is a tall track; .hero__stage pins full-viewport (same sticky-pin pattern as Philosophy).
   scenes.js scrubs the clip forward once and drives --exit (text fly-out) and
   --white (fade-to-white handoff) from scroll progress.
   ========================================================================= */
.hero {
  position: relative;
  width: 100%;
  height: 220vh;                       /* ~120vh of pinned scroll drives the scrub */
  /* Black floor for the handoff. The stage above is opaque, so this is never seen during
     the hero — but it backs the whole section, so the moment the stage blacks out and
     lifts away (and while Philosophy's clip is still fading in over it), any gap shows
     BLACK, never the white page. This is what makes the fade-to-black → clip seamless. */
  background: #000;
}
.hero__stage {
  position: sticky; top: 0;
  /* svh first as a fallback; dvh fills the DYNAMIC viewport so the #000 track floor never
     shows below the sticky stage when the mobile address bar retracts (the bottom black strip).
     Decoupled from the scrub: progress() keys off the TRACK rect + innerHeight, not this. */
  height: 100svh;
  height: 100dvh;
  display: flex; flex-direction: column; justify-content: center;
  overflow: hidden;
  background: var(--c-bg);             /* matches the clip's bright base + poster */
  isolation: isolate;                  /* contain the stage's own blend layers */
}
/* full-bleed clip — graded to silver/grey so the brand stays monochrome: the warm
   floral footage is desaturated to sit with Philosophy's B&W and the site palette.
   NOTE: a CSS-function filter (grayscale/contrast/brightness), NOT an SVG url() filter —
   Chrome applies url(#id) filters to the poster but NOT to a PLAYING <video>, so url()
   would leave the clip in colour the moment it scrubs. grayscale() applies to both. */
.hero__media {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
  filter: grayscale(1) contrast(1.05) brightness(1.03);
}
/* Static fallback behind the clip (z-0). Shown ONLY when the clip can't play — no-JS,
   reduced-motion, or a failed load (.clip-failed) — so the hero is never empty. With JS the
   clip will play, so it's hidden during a healthy load (no still→clip swap); the clip covers
   it once it paints. Same grayscale grading as the clip so it reads identically. */
.hero__fallback {
  position: absolute; inset: 0; z-index: 0;
  background: var(--c-bg) url("../img/hero-poster.jpg") center / cover no-repeat;
  filter: grayscale(1) contrast(1.05) brightness(1.03);
  transition: opacity .5s ease;
}
.js .hero__fallback { opacity: 0; }                     /* JS will play the clip → hide during load */
.js .hero.clip-failed .hero__fallback { opacity: 1; }   /* …unless the clip actually failed to load */
@media (prefers-reduced-motion: reduce) {
  .js .hero__fallback { opacity: 1; }                   /* no clip loads on reduced motion → keep the still */
}
/* left→right white scrim — keeps the dark ink title legible over the marble/florals */
.hero__scrim {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(90deg, rgba(255,255,255,.82) 0%, rgba(255,255,255,.45) 30%, rgba(255,255,255,0) 62%);
}
/* top & bottom edge fades melt the video into the white page — no hard seam */
.hero__stage::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  /* gentle dark base at the very bottom that eases toward the black-out handoff;
     top stays clear so the frosted header reads as glass over the video */
  background: linear-gradient(to bottom,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 86%, rgba(0,0,0,.6) 100%);
}
/* fine film grain — `overlay` leaves pure white/black untouched (matches scenes) */
.hero__stage::after {
  content: ""; position: absolute; inset: 0; z-index: 4; pointer-events: none;
  opacity: .12; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='nh'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23nh)'/></svg>");
  background-size: 200px 200px;
}
/* Hero glints — soft white circles over the B&W hero that match the footage's own bokeh.
   main.js injects a balanced field and toggles `.is-scrolling` on the hero: the field
   fades out the instant you scroll and drifts back in once the scroll settles. Decorative,
   sits behind the text, and is swallowed by the black-out handoff (--white, z-6). */
.hero__glints {
  position: absolute; inset: 0; z-index: 4; pointer-events: none;
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: blur(0);
  /* settle back in gently once the scroll stops */
  transition: opacity .9s ease, transform 1s cubic-bezier(.2, .7, .2, 1), filter .9s ease;
}
/* elegant dissolve: the instant you scroll, the whole field lifts, softens and fades away */
.hero.is-scrolling .hero__glints {
  opacity: 0;
  transform: translateY(-16px) scale(.975);
  filter: blur(3px);
  transition: opacity .5s ease, transform .6s cubic-bezier(.2, .7, .2, 1), filter .5s ease;
}
/* one glint: a small soft white circle that matches the video's bokeh — a radial
   white→transparent fill so the edge is soft, not a hard dot. Position/size/timing
   come from per-element vars set in JS. */
.glint {
  position: absolute;
  left: var(--x); top: var(--y);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, #fff 0%, rgba(255, 255, 255, .92) 36%, rgba(255, 255, 255, 0) 72%);
  opacity: 0;
  transform: translate(-50%, -50%) scale(.6);
  filter: drop-shadow(0 0 calc(var(--s) * .35) rgba(255, 255, 255, .55)); /* faint glow */
  animation: glint var(--dur, 4s) ease-in-out var(--delay, 0s) infinite,
             glint-float var(--fdur, 16s) ease-in-out var(--delay, 0s) infinite;
  will-change: transform, translate, opacity;
}
/* soft fade in/out, like bokeh drifting into focus — mostly invisible, a brief gentle peak
   so only a few circles show at once */
@keyframes glint {
  0%, 100% { opacity: 0;  transform: translate(-50%, -50%) scale(.6); }
  50%      { opacity: .9; transform: translate(-50%, -50%) scale(1); }
}
/* a slow, gentle positional drift on the independent `translate` property (composes with
   the twinkle's transform) — so the dots float around the page rather than sit in place */
@keyframes glint-float {
  0%, 100% { translate: 0 0; }
  50%      { translate: var(--fx, 0) var(--fy, -14px); }
}

.hero__inner { position: relative; z-index: 5; text-align: center; }
/* fade-to-black overlay — scenes.js ramps --white over the final stretch, handing
   off to the black-and-white Philosophy clip below */
.hero__white {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: #000;
  opacity: var(--white, 0);
}
/* Hero text choreography:
   - brand name "Pūrify Beauty" builds in once the intro veil lifts (.intro-done),
     then fades out as you scroll away (scenes.js drives --namefade) — the name is
     handed off to the sticky nav, which slides in at the same moment.
   - tagline + subhead fade up with scroll (scenes.js drives --tag / --sub).
   First paint (all hidden) is masked behind the intro veil, so no flash. */
.js .hero__title {
  opacity: calc(1 - var(--namefade, 0));
  will-change: opacity;
}
.hero__brand { display: block; }
/* elegant wordmark reveal — a slow focus-pull (blur→sharp) that resolves in place,
   beginning just as the loading veil finishes lifting (no rise) */
.js .hero__brand {
  opacity: 0;
  filter: blur(12px);
  transition: opacity 1.4s ease, filter 1.4s ease;
  transition-delay: .45s;
}
.intro-done .hero__brand { opacity: 1; filter: blur(0); }

/* tagline reveals as an elegant left-to-right wipe — a soft-edged gradient mask
   whose position is driven by scroll (--tag), so the line sweeps in from the left.
   subhead still fades up in place. */
.js .hero__tagline {
  -webkit-mask-image: linear-gradient(to right, #000 0% 40%, transparent 57%);
          mask-image: linear-gradient(to right, #000 0% 40%, transparent 57%);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 300% 100%;   mask-size: 300% 100%;
  -webkit-mask-position: calc((1 - var(--tag, 1)) * 100%) 0;
          mask-position: calc((1 - var(--tag, 1)) * 100%) 0;
  will-change: mask-position;
}
.js .hero__sub {
  opacity: var(--sub, 1);
  will-change: opacity;
}
.hero__title {
  font-family: var(--font-serif);
  color: var(--c-ink);
  font-size: clamp(3.6rem, 11vw, 7rem);   /* confident hero wordmark — owns the page */
  line-height: 1.02;
  letter-spacing: .04em;        /* a touch of wordmark air for the brand name */
  font-weight: 600;
}
.hero__tagline {
  margin-top: clamp(1rem, 2.2vw, 1.6rem);
  font-family: var(--font-serif);
  color: var(--c-ink);
  font-size: clamp(1.05rem, 2.4vw, 1.55rem);   /* smaller — a refined subtitle under the wordmark */
  line-height: 1.2;
  letter-spacing: .06em;        /* a touch more tracking now that it's smaller caps */
  font-weight: 600;
}
.hero__sub {
  margin: 1.8rem auto 0;        /* centered, capped width */
  max-width: 38ch;
  font-family: var(--font-body);
  text-transform: none;
  font-size: clamp(1rem, 1.8vw, 1.25rem);   /* smaller, quieter supporting line */
  line-height: 1.5;
  font-weight: 500;
  color: var(--c-grey);
}

/* =========================================================================
   Sections
   ========================================================================= */
/* Philosophy — centered moment that fades up with scroll as the hero whitens out.
   No border-top: the white edge-fade hands off seamlessly. scenes.js drives --in;
   default 0 under JS (hidden until scrolled — no flash), full + static otherwise. */
/* Philosophy — pinned fade. The section is a tall track; the inner stage pins at the
   viewport centre while scenes.js fades the copy in (--in). Because it's pinned, the
   text fades in IN PLACE at centre instead of travelling up the screen with the scroll. */
/* Pulled up to overlap the hero's tail: the section pins at viewport centre exactly
   as the hero finishes its black-out (no blank "travel" scroll between them). It stays
   transparent with its copy hidden through the overlap, so the hero plays untouched;
   its own background gradient (below) is the black floor that keeps the handoff seamless. */
/* height − 100vh = the scroll distance the whole moment plays over. The clip scrubs
   nonstop across all of it (forward + ping-pong, data-scrub-loops=4), so the track is
   tall on purpose: at 500vh that's 400vh of scrub ÷ 4 passes = ~100vh of scroll per full
   play of the clip (was ~50vh) — a calm, deliberate scrub. Want it slower/faster? Change
   this height alone (keep it a multiple that keeps each pass ~100vh: +100vh per extra
   ping-pong pair). The −100vh overlap with the hero is set by the margin, not the
   height, so growing this only extends the moment downward. */
/* The background is the real fix for the hero→Philosophy white flash. The stages move
   with native scroll (instant); any JS-set cover is painted a frame later, so on a fast
   scroll the hero lifts and exposes the gap one frame before JS can black it out — white
   shows from the bottom. This gradient is pure layout, so it can't lag: TRANSPARENT through
   the top 100vh (exactly the overlap with the hero, so the hero plays untouched), then
   SOLID BLACK below — which is precisely the region the gap opens into. Behind Philosophy's
   stage is therefore always black, never the white page, at any scroll speed. */
.philosophy { background: linear-gradient(180deg, transparent 0, transparent 95vh, #000 99vh); position: relative; z-index: 1; height: 500vh; margin-top: -100vh; padding-block: 0; }
.philosophy__stage {
  position: sticky; top: 0;
  /* svh fallback, then dvh — same dynamic-viewport fix as the hero stage so the black floor
     gradient can't peek below the sticky stage when the address bar retracts. */
  height: 100svh;
  height: 100dvh;
  display: flex; flex-direction: column; justify-content: center;
  overflow: hidden;
  background: #000;                 /* no-JS base; JS drives this via --media */
}
/* Black-and-white clip scrubs with scroll behind the copy. Full-bleed, true tones. */
.philosophy__media {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* darkening so the white copy stays legible over any frame: a soft halo centred
   behind the text (the bright hair drifts through centre) over a vertical base */
.philosophy__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(135% 100% at 50% 50%, rgba(0,0,0,.52) 0%, rgba(0,0,0,.18) 56%, rgba(0,0,0,0) 100%),
    linear-gradient(180deg, rgba(0,0,0,.42) 0%, rgba(0,0,0,.24) 45%, rgba(0,0,0,.6) 100%);
}
.philosophy .measure { position: relative; z-index: 2; }
/* fade-to-white overlay — scenes.js ramps --white over the final stretch, handing
   off to the white "Three commitments" section below */
.philosophy__white {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: #fff;
  opacity: var(--white, 0);
}
/* light copy over the dark footage */
.philosophy .section-title { color: #fff; text-shadow: 0 1px 40px rgba(0,0,0,.45); }
.philosophy .lede { color: rgba(255,255,255,.84); text-shadow: 0 1px 30px rgba(0,0,0,.45); }
/* JS holds the backdrop hidden through the overlap with the hero (so the hero plays
   untouched), then makes it solid black the instant the section pins.
   The black BACKING is a HARD STEP, not a fade: --cover is 1 the moment the section is
   pinned and 0 before. There's no partly-transparent moment, so the white page can never
   show through as the hero lifts away (the flash). It's invisible at the pin itself —
   the hero is solid black directly behind it. The clip + scrim then dissolve in gently
   over that solid black on --media, which is what reads as the cinematic handoff. */
.js .philosophy__stage { background: rgb(0 0 0 / var(--cover, 0)); }
.js .philosophy__media,
.js .philosophy__scrim { opacity: var(--media, 0); }
/* The headline gets an elegant focus-pull (soft blur → sharp, faint scale settle) that
   resolves in place at centre as --in advances. The body copy below reveals differently
   — word by word. Default 0 under JS so nothing flashes before the scroll reaches it. */
.js .philosophy .measure { will-change: opacity, transform, filter; }
.js .philosophy .section-title {
  opacity: var(--in, 0);
  transform: scale(calc(0.984 + var(--in, 0) * 0.016));
  filter: blur(calc((1 - var(--in, 0)) * 10px));
}
/* Body copy — split into words (scenes.js) and revealed in reading order: each word
   lights up when --in2 (0→1) passes its position (--i / --n), so the line fades in
   left→right, line by line, as you scroll. --ww is each word's fade width: a handful of
   words sit mid-fade at once so the leading edge sweeps softly across the line instead of
   snapping word-to-word. Smaller --ww = crisper sweep; larger = a whole line dissolves at
   once. The container shows once split (.is-ready); held hidden until then so the whole
   line never flashes in unsplit. */
.philosophy .lede { --ww: 0.10; }
.js .philosophy .lede:not(.is-ready) { opacity: 0; }
.js .philosophy .lede.is-ready { opacity: 1; transform: none; filter: none; }
.js .philosophy .lede .rt-w {
  opacity: clamp(0, (var(--in2, 0) - var(--i, 0) / var(--n, 1) * (1 - var(--ww))) / var(--ww), 1);
  will-change: opacity;
}

/* Mobile: make the scroll clip far more visible. The dimming scrim normally rides in
   with the clip (--media), darkening it the whole time. But the clip now plays its full
   solo pass before any copy appears — so here the scrim instead waits for the HEADLINE
   (--in), letting the footage play completely clear/bright during that pass, then fades
   in for legibility exactly when the copy arrives. A small brightness lift helps the B&W
   read on a phone. Framing stays full-bleed (cover) so the copy can sit over it. */
@media (max-width: 720px) {
  .js .philosophy__scrim { opacity: var(--in, 0); }
  /* Trimmed the brightness lift (was 1.12): the bright blonde hair was punching through the
     scrim as an ugly light smudge below the copy on phones. */
  .philosophy__media { filter: brightness(1.06) contrast(1.04); }
  /* …and deepen the scrim on phones so that bright wisp sits back into the dark instead of
     reading as a stray spot. Desktop keeps the lighter base scrim (it darkens throughout via
     --media anyway, so it never showed the smudge). */
  .philosophy__scrim {
    background:
      radial-gradient(135% 104% at 50% 47%, rgba(0,0,0,.56) 0%, rgba(0,0,0,.27) 55%, rgba(0,0,0,.07) 100%),
      linear-gradient(180deg, rgba(0,0,0,.46) 0%, rgba(0,0,0,.32) 44%, rgba(0,0,0,.78) 100%);
  }
}

/* Approach — a calm, contained section (the full-bleed video scenes were removed). */
.approach { background: var(--c-bg); position: relative; }
/* Approach steps — three black hexagons (the brand mark's silhouette), each showing a
   step word. Tap blows one up to a large hexagon with the PB logo as its background and
   the one-liner readable on top (.zoom-orb). Each hexagon drifts on its own slow
   elliptical orbit + periodically wobbles, so they read as floating in space. */
.flip-steps {
  margin-top: clamp(2rem, 4.5vw, 3rem);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;                 /* a tidy centered row, not spread full-width */
  gap: clamp(1.25rem, 4vw, 2.5rem);
}
.flip-step { flex: 0 0 auto; }
/* the float wrapper carries the orbital drift — small fixed hexagons (the brand mark's
   silhouette) that shrink a touch on narrow screens but stay side by side. */
.flip-step__float {
  width: clamp(104px, 27vw, 172px);
  aspect-ratio: 481 / 421;                 /* the logo hexagon's true proportions */
  --rx: 13px; --ry: 11px;                  /* drift radii (elliptical orbit) */
  animation: orbit 20s linear infinite;
}
/* a different ellipse, speed, phase + direction per orb → organic, random-looking drift
   that never syncs up */
.flip-step:nth-child(1) .flip-step__float { --rx: 14px; --ry: 10px; animation-duration: 19s; animation-delay: -3s; }
.flip-step:nth-child(2) .flip-step__float { --rx: 11px; --ry: 16px; animation-duration: 25s; animation-delay: -11s; animation-direction: reverse; }
.flip-step:nth-child(3) .flip-step__float { --rx: 16px; --ry: 12px; animation-duration: 22s; animation-delay: -7s; }
/* a circle traced through 8 points — the var radii resolve to fixed px per keyframe and
   `translate` interpolates between them; constant-speed linear = a calm, weightless glide */
@keyframes orbit {
  0%    { translate: 0                       calc(var(--ry) * -1);    }
  12.5% { translate: calc(var(--rx) *  .707) calc(var(--ry) * -.707); }
  25%   { translate: var(--rx)               0;                       }
  37.5% { translate: calc(var(--rx) *  .707) calc(var(--ry) *  .707); }
  50%   { translate: 0                       var(--ry);               }
  62.5% { translate: calc(var(--rx) * -.707) calc(var(--ry) *  .707); }
  75%   { translate: calc(var(--rx) * -1)    0;                       }
  87.5% { translate: calc(var(--rx) * -.707) calc(var(--ry) * -.707); }
  100%  { translate: 0                       calc(var(--ry) * -1);    }
}

.flip-step__inner {
  position: relative; width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0; background: none;
  cursor: pointer;
  /* the hexagon faces clip box-shadow away, so the floating depth lives here as a
     drop-shadow (which follows the clipped silhouette and wobbles with it) */
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, .20)) drop-shadow(0 12px 20px rgba(0, 0, 0, .26));
  /* a gentle periodic wobble invites the tap; staggered so the hexagons take turns.
     orbit drift lives on .flip-step__float (translate) — this rotate stays independent. */
  animation: step-wobble 3.4s ease-in-out infinite;
}
.flip-step:nth-child(1) .flip-step__inner { animation-delay: .2s; }
.flip-step:nth-child(2) .flip-step__inner { animation-delay: 1.4s; }
.flip-step:nth-child(3) .flip-step__inner { animation-delay: 2.6s; }
/* mostly at rest, then a quick soft shimmy back to centre */
@keyframes step-wobble {
  0%, 58%, 100% { transform: rotate(0deg); }
  63% { transform: rotate(-4.5deg); }
  69% { transform: rotate(3.5deg); }
  75% { transform: rotate(-2deg); }
  81% { transform: rotate(1deg); }
  88% { transform: rotate(0deg); }
}
/* focus ring on the (unclipped) button, so it isn't cut away by the hexagon clip */
.flip-step__inner:focus-visible { outline: 2px solid var(--c-ink); outline-offset: 4px; }

.flip-step__face {
  position: absolute; inset: 0;
  clip-path: var(--hex);                   /* the brand hexagon silhouette */
  display: grid; place-items: center; text-align: center;
  padding: clamp(.7rem, 2.6vw, 1.35rem);
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
/* dark hexagon — a top-left radial highlight catches light over a per-step tonal base
   (var --orb-top/--orb-bottom, set below) so each hexagon is a different black.
   No border/box-shadow here: clip-path would cut them — depth is the inner drop-shadow. */
.flip-step__front {
  background:
    radial-gradient(120% 120% at 32% 26%, rgba(255, 255, 255, .14) 0%, rgba(255, 255, 255, 0) 44%),
    linear-gradient(160deg, var(--orb-top, #2E2E2E) 0%, var(--orb-bottom, #0C0C0C) 100%);
}
/* the brand mark watermarked into the front face — the same subtle silver
   treatment as the zoomed hexagon (.zoom-orb__logo), scaled to the small faces */
.flip-step__front::before {
  content: "";
  position: absolute; inset: 0; margin: auto;
  width: 100%; aspect-ratio: 560 / 499;
  pointer-events: none;
  background: rgba(255, 255, 255, .13);
  -webkit-mask: url(../img/pb-mark.png) center / contain no-repeat;
          mask: url(../img/pb-mark.png) center / contain no-repeat;
}
.flip-step__word { position: relative; z-index: 1; }
.flip-step__back {
  transform: rotateY(180deg);
  background:
    radial-gradient(120% 120% at 32% 26%, rgba(255, 255, 255, .12) 0%, rgba(255, 255, 255, 0) 44%),
    linear-gradient(160deg, var(--orb-top, #2E2E2E) 0%, var(--orb-bottom, #0C0C0C) 100%);
}
/* each hexagon a different shade of black — charcoal → mid → near-pure-black */
.flip-step:nth-child(1) { --orb-top: #3F3F3F; --orb-bottom: #1C1C1C; }
.flip-step:nth-child(2) { --orb-top: #2A2A2A; --orb-bottom: #0D0D0D; }
.flip-step:nth-child(3) { --orb-top: #151515; --orb-bottom: #000000; }
/* engraved into the orb: a dark groove wall above each glyph + a faint lit lower lip,
   so the lettering reads as carved into the surface rather than printed on top */
.flip-step__word {
  font-family: var(--font); text-transform: uppercase; font-weight: 700;
  font-size: clamp(1.05rem, 2.4vw, 1.5rem); letter-spacing: .05em;
  color: rgba(255, 255, 255, .82);
  text-shadow: 0 -1px 1px rgba(0, 0, 0, .7), 0 1px 0 rgba(255, 255, 255, .16);
}
.flip-step__line {
  font-family: var(--font-body); text-transform: none;
  font-weight: 600; letter-spacing: -.01em;
  font-size: clamp(.72rem, 1.4vw, .98rem); line-height: 1.28;
  color: rgba(255, 255, 255, .8);
  text-shadow: 0 -1px 1px rgba(0, 0, 0, .6), 0 1px 0 rgba(255, 255, 255, .12);
}

/* --- Tap-to-zoom: the tapped orb blows up to a large, legible circle centered in the
   section. The veil softly dims the rest; the circle keeps the orb's look at readable size. --- */
.approach__zoom {
  position: fixed; inset: 0; z-index: 100;   /* full-viewport modal, above the nav */
  display: grid; place-items: center;
  padding: 1.25rem;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .3s ease, visibility 0s linear .3s;
}
.approach__zoom.is-open {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity .3s ease;
}
.approach__zoom-veil {
  position: absolute; inset: 0;
  background: rgba(251, 251, 250, .76);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.approach__zoom-close {
  position: absolute; top: clamp(.6rem, 2.5vw, 1.4rem); right: clamp(.6rem, 2.5vw, 1.4rem);
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(255, 255, 255, .85); border: 1px solid var(--c-line);
  color: var(--c-ink); font-size: 1.5rem; line-height: 1;
  font-family: var(--font-body); cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background var(--t), box-shadow var(--t);
}
.approach__zoom-close:hover { background: #fff; box-shadow: var(--shadow-md); }
/* the blown-up hexagon — same dark surface as the small ones, sized to read easily, with
   the brand mark embossed into it. --zoom-top/--zoom-bottom are set per tap (main.js). */
.zoom-orb {
  position: relative;
  width: min(86vw, 60vh, 430px); aspect-ratio: 481 / 421;
  display: grid; place-items: center; align-content: center; text-align: center;
  padding: clamp(1.2rem, 4vw, 2.2rem) clamp(2.8rem, 11vw, 5.5rem);
  clip-path: var(--hex);
  background:
    radial-gradient(120% 120% at 32% 26%, rgba(255, 255, 255, .14) 0%, rgba(255, 255, 255, 0) 44%),
    linear-gradient(160deg, var(--zoom-top, #2A2A2A) 0%, var(--zoom-bottom, #060606) 100%);
  cursor: default;                            /* the hexagon is content — tap OUTSIDE to close */
  /* clip-path cuts box-shadow, so the floating depth is a drop-shadow (follows the hexagon) */
  filter: drop-shadow(0 22px 48px rgba(0, 0, 0, .42));
  transform: scale(.55);
  transition: transform .34s cubic-bezier(.2, .7, .2, 1);
}
.zoom-orb:focus-visible { outline: none; }
.approach__zoom.is-open .zoom-orb { transform: scale(1); }
/* the actual brand mark (assets/img/pb-mark.png) as the card's background — the SAME image
   the sticky-nav logo uses, masked here in a subtle silver tone so it reads as a watermark
   on the black hexagon, filling the shape behind the text. */
.zoom-orb__logo {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%; z-index: 0;
  width: 100%; aspect-ratio: 560 / 499; pointer-events: none;
  background: rgba(255, 255, 255, .15);                 /* subtle silver on the black hexagon */
  -webkit-mask: url(../img/pb-mark.png) center / contain no-repeat;
          mask: url(../img/pb-mark.png) center / contain no-repeat;
}
.zoom-orb__word {
  position: relative; z-index: 1;
  display: block;
  font-family: var(--font); text-transform: uppercase; font-weight: 700;
  letter-spacing: .07em; font-size: clamp(1.5rem, 5vw, 2.2rem);
  color: rgba(255, 255, 255, .94);
  text-shadow: 0 -1px 1px rgba(0, 0, 0, .8), 0 1px 0 rgba(255, 255, 255, .16), 0 2px 6px rgba(0, 0, 0, .5);
  margin-bottom: clamp(.5rem, 1.6vw, .9rem);
}
.zoom-orb__line {
  position: relative; z-index: 1;
  margin: 0; max-width: 22ch;
  font-family: var(--font-body); text-transform: none;
  font-weight: 600; letter-spacing: -.01em;
  font-size: clamp(1rem, 2.8vw, 1.3rem); line-height: 1.42;
  color: rgba(255, 255, 255, .9);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
}

/* Products — the tools + the serum science floating over a silver stage: the same
   full-bleed light-silver band as the press strip (the gradient IS the border — its
   edges dissolve into the white page) with the hero's glint field drifting behind
   the photos (.products__glints, injected by main.js). The pair stays side by side
   at EVERY viewport — on phones the gap tightens instead of stacking. */
.products {
  position: relative;
  overflow: hidden;                       /* contain the drifting glints */
  background: linear-gradient(180deg,
    rgba(231, 234, 237, 0) 0%,
    #E7EAED 26%,
    #E7EAED 74%,
    rgba(231, 234, 237, 0) 100%);
}
/* same layering pattern as .press: the glint field is injected as the section's
   first child; the .wrap is positioned, so it paints above with no z-index */
.products > .wrap { position: relative; }
.products__glints { position: absolute; inset: 0; pointer-events: none; }
.products__grid {
  margin-top: clamp(2rem, 4.5vw, 3rem);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 4vw, 2.5rem);
  align-items: start;
}
.products__item { margin: 0; display: grid; justify-items: center; }
/* the frame: the brand mark's hexagon with the founder portrait's polished silver
   rim, floating over the band. clip-path cuts box-shadow → depth is a drop-shadow
   (house rule); filter applies post-clip, so it follows the hexagon silhouette. */
.products__hex {
  position: relative;
  display: block;
  width: 100%;
  max-width: 30rem;
  aspect-ratio: 481 / 421;
  clip-path: var(--hex);
  background: linear-gradient(160deg, #EDEFF1 0%, #C9CED3 100%);  /* silver rim + photo fallback */
  filter: drop-shadow(0 4px 8px rgba(17, 17, 17, .10)) drop-shadow(0 20px 40px rgba(17, 17, 17, .20));
  transition: filter var(--t), translate .5s cubic-bezier(.2, .7, .2, 1);
}
.products__item:hover .products__hex {
  translate: 0 -3px;                      /* the cards' gentle lift, same curve */
  filter: drop-shadow(0 6px 12px rgba(17, 17, 17, .11)) drop-shadow(0 26px 52px rgba(17, 17, 17, .24));
}
/* the photo window inside the rim — its own hexagon clip crops the settle zoom */
.products__crop {
  position: absolute; inset: 4px;
  display: block;
  clip-path: var(--hex);
}
.products__crop img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.03);
}
/* per-shot framing — the hexagon is landscape, the photos portrait, so each crop
   window is anchored on what matters: the tools + their boxes, the serum bottle */
.products__item:nth-child(1) .products__crop img { object-position: 50% 62%; }
.products__item:nth-child(2) .products__crop img { object-position: 50% 85%; }
/* slow Ken-Burns settle as the section reveals: the photos ease from a gentle
   zoom to rest. Keyed off the scroll engine's .in (so it replays on re-entry);
   the global reduced-motion rule zeroes it out. */
.js .products.in .products__crop img {
  animation: productSettle 6.5s cubic-bezier(.2, .7, .2, 1) both;
}
@keyframes productSettle {
  from { transform: scale(1.05); }
  to   { transform: scale(1); }
}
.products__cap {
  margin-top: .9rem;
  text-align: center;
  font-family: var(--font);
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .22em;
  color: var(--c-grey-soft);
}
@media (max-width: 600px) {
  /* still side by side — tighter gap, smaller caption */
  .products__grid { gap: .9rem; }
  .products__cap { font-size: .66rem; letter-spacing: .16em; }
}

/* Founder / proof — copy beside the founder's portrait in a hexagon frame */
.founder__copy { display: flow-root; }   /* contain the floated portrait */
/* Badge-scale portrait, floated into the copy: the text wraps along the
   hexagon's own angled silhouette (shape-outside mirrors the --hex polygon). */
.founder__portrait {
  float: right;
  width: clamp(140px, 15vw, 185px);
  margin: .4rem 0 .9rem clamp(1.1rem, 2.6vw, 1.75rem);
  shape-outside: polygon(24% 0%, 76% 0%, 100% 49%, 76% 100%, 24% 100%, 0% 49%);
  shape-margin: 1rem;
}
/* the frame: the brand mark's hexagon with a polished silver rim; the photo is
   inset and clipped by the same hexagon, so it sits fitted edge to edge with no
   stray corners. clip-path cuts box-shadow → depth is a drop-shadow (house rule). */
.founder__hex {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 481 / 421;
  clip-path: var(--hex);
  background: linear-gradient(160deg, #EDEFF1 0%, #C9CED3 100%);  /* silver rim + photo fallback */
  filter: drop-shadow(0 1px 3px rgba(17, 17, 17, .08)) drop-shadow(0 10px 20px rgba(17, 17, 17, .13));
}
.founder__hex img {
  position: absolute; inset: 4px;
  width: calc(100% - 8px); height: calc(100% - 8px);
  clip-path: var(--hex);
  object-fit: cover;
  object-position: 50% 32%;          /* keep the face centred in the hexagon's heart */
  filter: grayscale(1) contrast(1.04);   /* monochrome, like every photo on the site */
}

@media (max-width: 600px) {
  .founder__portrait { width: clamp(120px, 36vw, 145px); shape-margin: .75rem; }
}

/* Desktop: split copy and portrait into two balanced columns so the section fills
   the page width instead of stranding the right side. The copy keeps a readable
   measure in column one; the portrait grows to a generous badge in its own column
   and centres vertically against the copy. Below 900px the compact floated-badge
   layout above still applies — mobile is unchanged. */
@media (min-width: 900px) {
  .founder__copy {
    max-width: none;                /* release the 680px measure; the wrap caps width */
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(280px, 30vw, 380px);
    column-gap: clamp(2.5rem, 5vw, 4.5rem);
    align-items: center;
  }
  .founder__copy > h2 { grid-column: 1; }
  .founder__copy > .body {
    grid-column: 1;
    max-width: var(--measure);      /* keep body lines at a comfortable measure */
  }
  .founder__portrait {
    grid-column: 2;
    grid-row: 1 / span 4;           /* heading + three paragraphs → centre against the copy */
    align-self: center;
    float: none;
    width: 100%;
    margin: 0;
    shape-outside: none;
    shape-margin: 0;
  }
}

.pullquote {
  margin: clamp(1.6rem, 3.2vw, 2.2rem) 0;
  padding-left: 1.4rem;
  border-left: 2px solid var(--c-silver);
  font-family: var(--font-body);
  text-transform: none;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(1.7rem, 3.6vw, 2.4rem);
  line-height: 1.3;
  color: var(--c-ink);
}
.founder__sign {
  margin-top: 1.25rem;
  font-family: var(--font-body);
  font-style: italic;
  text-transform: none;
  font-size: 1.15rem;
  letter-spacing: .01em;
  color: var(--c-grey);
}

/* Press / "Find us at" — a full-bleed light-silver band whose top and bottom edges
   dissolve into the white page (the gradient IS the border — no hairlines). One logo
   at a time blur-dissolves through a fixed-height slot; logos stack absolutely so the
   outgoing/incoming fades overlap cleanly (no reflow). main.js floats the hero's
   white glint field over the silver (.press__glints, behind the content). */
.press {
  position: relative;
  overflow: hidden;
  text-align: center;
  margin-top: clamp(2.25rem, 5.5vw, 3.5rem);
  padding-block: clamp(2.75rem, 6.5vw, 4.25rem);
  background: linear-gradient(180deg,
    rgba(231, 234, 237, 0) 0%,
    #E7EAED 26%,
    #E7EAED 74%,
    rgba(231, 234, 237, 0) 100%);
}
/* Layering by DOM order alone (the glint field is injected as .press's first child):
   both are positioned with z-index:auto, so the wrap paints above the glints. No
   z-index here on purpose — a stacking context on .wrap would isolate the logos'
   mix-blend-mode from the band's silver background and the white JPEG boxes return. */
.press > .wrap { position: relative; }
.press__glints { position: absolute; inset: 0; pointer-events: none; }
.press__cap {
  margin: 0 0 1.25rem;
  font-family: var(--font);
  font-size: .78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .22em;
  color: var(--c-grey-soft);
}
.press__stage {
  position: relative;
  height: clamp(5.5rem, 14vw, 8.5rem);   /* fixed slot height keeps layout still */
}
.press__logo {
  position: absolute;
  inset: 0;
  margin: auto;
  max-height: 100%;
  width: auto;
  max-width: min(90vw, 640px);
  object-fit: contain;
  opacity: 0;
  /* the PNGs are pre-processed: black ink + true alpha (luminance-keyed from the
     original white-box JPEG art) — they sit on any background with no box, no blend */
  filter: blur(8px);
  transform: scale(.985);
  /* outgoing: a quicker, soft dissolve (the incoming logo overlaps it below) */
  transition: opacity .7s ease, filter .7s ease, transform .7s ease;
  pointer-events: none;
  will-change: opacity, filter, transform;
}
/* incoming: slow focus-pull — blur resolves and the logo settles to full size */
.press__logo.is-active {
  opacity: .82;                          /* readable first — the silver keeps it soft */
  filter: blur(0);
  transform: scale(1);
  transition: opacity 1.1s cubic-bezier(.2, .7, .2, 1),
              filter 1.1s cubic-bezier(.2, .7, .2, 1),
              transform 1.1s cubic-bezier(.2, .7, .2, 1);
}
/* aspect outliers — very wide/short artwork gets more width so it doesn't read
   as a sliver beside the squarer marks */
.press__logo--wide { max-width: min(92vw, 760px); }

/* Contact */
.contact .lede { margin-inline: auto; max-width: 46ch; }
.contact__actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
.btn {
  display: inline-block;
  padding: .9rem 2rem;
  background: var(--c-ink);
  color: #fff;
  font-size: .82rem;
  text-transform: uppercase;
  letter-spacing: .14em;
  border: 1px solid var(--c-ink);
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  transition: background var(--t), color var(--t), box-shadow var(--t);
}
.btn:hover { background: transparent; color: var(--c-ink); box-shadow: var(--shadow-md); }
.btn--ghost { background: transparent; color: var(--c-ink); box-shadow: none; }
.btn--ghost:hover { background: var(--c-ink); color: #fff; box-shadow: var(--shadow-sm); }

/* =========================================================================
   Footer
   ========================================================================= */
.site-footer {
  padding-block: clamp(2.5rem, 5.5vw, 3.75rem);
  /* clear the home indicator on gesture-nav iPhones */
  padding-bottom: max(clamp(2.5rem, 5.5vw, 3.75rem), calc(env(safe-area-inset-bottom) + 2rem));
}
.site-footer__inner { display: grid; gap: 1rem; text-align: center; justify-items: center; }
.brand--footer .brand__mark { width: 34px; height: 30px; }
.site-footer__tag {
  font-family: var(--font-body);
  text-transform: none;
  font-style: italic;
  font-size: 1.4rem;
  letter-spacing: .01em;
  color: var(--c-grey);
}
.site-footer__links { display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; }
.site-footer__links a {
  font-size: .82rem; letter-spacing: .06em; color: var(--c-ink-2);
  /* extra top room makes a comfier tap target; underline stays tight to the text */
  border-bottom: 1px solid transparent; padding: .5rem 0 2px; transition: border-color var(--t);
}
.site-footer__links a:hover { border-bottom-color: var(--c-silver); }
.site-footer__legal { font-size: .76rem; letter-spacing: .04em; color: var(--c-grey-soft); }

/* =========================================================================
   Cinematic scroll engine
   Start state comes from per-element CSS vars (set by JS per section preset);
   one transition animates them to neutral. The section toggles `.in` on enter
   AND removes it on leave, so motion replays every time it re-enters view.
   ========================================================================= */
.rt-word { display: inline-block; }
.js .split { opacity: 0; }            /* hide headline until JS wraps words (no flash) */
.js .split.is-ready { opacity: 1; }

.js [data-anim] .reveal,
.js [data-anim] .rt-word {
  opacity: 0;
  transform: translate3d(var(--dx, 0), var(--dy, 0), 0) scale(var(--sc, 1));
  filter: blur(var(--bl, 0));
  transition:
    opacity .8s ease,
    transform .9s cubic-bezier(.2, .7, .2, 1),
    filter .9s ease;
  transition-delay: var(--d, 0ms);
  will-change: transform, opacity;
}
.js [data-anim].in .reveal,
.js [data-anim].in .rt-word {
  opacity: 1;
  transform: none;
  filter: none;
}

/* =========================================================================
   Responsive
   ========================================================================= */
@media (max-width: 860px) {
  .flip-steps { gap: clamp(.5rem, 2.4vw, 1.1rem); }   /* stay 3 across, just tighter */
}

/* Phones: the orbs scale fluidly with the viewport (no fixed floor) so all three stay
   side by side — and the word shrinks with them — down to the smallest phones (~320px).
   Tapping any orb still blows it up to the large, fully legible zoom circle. */
@media (max-width: 600px) {
  .flip-step__float { width: min(172px, 26vw); }
  .flip-steps { gap: 2.5vw; }
  .flip-step__face { padding: clamp(.5rem, 2.4vw, 1.1rem); }
  .flip-step__word { font-size: clamp(.85rem, 4vw, 1.4rem); letter-spacing: .03em; }
}

@media (max-width: 720px) {
  /* Drop the fixed full-page grain on phones: its mix-blend-mode forces the whole
     page through an extra compositing pass every scrolled frame (the main mobile
     jank source). The hero/Philosophy stages keep their own contained grain. */
  body::after { display: none; }

  /* Drop the hero's dark bottom edge-fade on phones — on the small screen it reads as an
     ugly black band along the bottom of the opening shot. The black-out handoff is driven
     by .hero__white (z-6), NOT this gradient, so removing it doesn't affect the transition. */
  .hero__stage::before { display: none; }

  /* The 40px toggle is absolutely placed so it doesn't inflate the pill height — the
     mobile bar is then driven by the same content as desktop (the brand), matching its
     shortness, while the tap target stays a full 40px. */
  .nav__toggle {
    display: block;
    position: absolute;
    top: 50%;
    right: clamp(.9rem, 2.4vw, 1.2rem);
    transform: translateY(-50%);
  }

  /* Liquid-glass pull-out drawer (was a full-screen opaque ink panel). position:fixed
     escapes to the viewport because body.menu-open strips the header's backdrop-filter
     — the containing-block trap (§6.3). z-85: above the scrim (84), below the toggle (90). */
  .nav__menu {
    position: fixed;
    /* a compact glass panel that drops in below the header, sized to its content in BOTH
       dimensions (no full-height slab, no fixed wide panel) — fitted to the three items.
       Anchored to the header's own right inset; max-width keeps it sane on tiny phones. */
    top: calc(env(safe-area-inset-top, 0px) + 4.25rem);
    left: auto;
    right: .7rem;
    width: 11rem;
    aspect-ratio: 481 / 421;   /* the brand mark's hexagon proportions */
    z-index: 85;
    /* the glass — gunmetal tint + blur, matching the header but more transparent (the
       scrim behind darkens the page, so the light links still read through it) */
    background: linear-gradient(160deg, rgba(60,63,68,.50), rgba(44,46,50,.40));
    -webkit-backdrop-filter: blur(26px) saturate(170%);
            backdrop-filter: blur(26px) saturate(170%);
    /* the brand's hexagon silhouette (same --hex as the cards/products/founder) — clip-path
       cuts border + box-shadow, so depth comes from a drop-shadow (the house rule). */
    clip-path: var(--hex);
    filter: drop-shadow(0 2px 6px rgba(17,17,17,.30)) drop-shadow(0 16px 34px rgba(17,17,17,.44));
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: .3rem;
    padding: 0 1.3rem;
    transform: translateX(calc(100% + 1.5rem));
    visibility: hidden;
    transition: transform .42s cubic-bezier(0.22,0.61,0.36,1), visibility 0s linear .42s;
  }
  .nav__menu.is-open {
    transform: translateX(0);
    visibility: visible;
    transition: transform .42s cubic-bezier(0.22,0.61,0.36,1);
  }
  /* light links read on the gunmetal glass drawer */
  .nav__menu a { font-size: 1rem; letter-spacing: .1em; line-height: 1.2; padding-block: .1rem; color: rgba(255,255,255,.92); white-space: nowrap; }
  .nav__menu a:hover { color: #fff; }
  /* Contact reads as a plain link in the drawer — no pill outline, aligned with the rest */
  .nav__menu .nav__cta { border: 0; padding: .12rem 0 !important; color: rgba(255,255,255,.92) !important; }
  .nav__menu .nav__cta:hover { background: transparent; border: 0; color: #fff !important; }

  /* the left brand would sit over the dim scrim while open — fade it out */
  body.menu-open .brand { opacity: 0; pointer-events: none; transition: opacity .25s ease; }

  /* Scrim — dims the page behind the drawer and catches an outside tap to close.
     Tied to the drawer's own .is-open (general sibling) so it fades in/out in sync. */
  .nav__scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 84;
    background: rgba(18,18,20,.40);
    -webkit-backdrop-filter: blur(2px);
            backdrop-filter: blur(2px);
    opacity: 0;
    visibility: hidden;
    transition: opacity .42s ease, visibility 0s linear .42s;
  }
  .nav__menu.is-open ~ .nav__scrim {
    opacity: 1;
    visibility: visible;
    transition: opacity .42s ease;
  }
}

/* =========================================================================
   Reduced motion — calm by default, motion off entirely
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; animation-delay: 0s !important; transition-duration: .001ms !important; }
  #intro { display: none; }
  .intro__fill { transform: none; }
  .flip-step__float { animation: none !important; translate: 0 !important; }   /* orbs rest still + centered */
  .flip-step__inner { animation: none !important; transform: none !important; } /* no wobble */
  .js [data-anim] .reveal,
  .js [data-anim] .rt-word { opacity: 1 !important; transform: none !important; filter: none !important; }
  .js .split { opacity: 1 !important; }

  /* Hero — collapse the scrub track to a single static poster screen, no fade-out */
  .hero { height: auto; }
  .hero__stage { position: static; height: 100svh; height: 100dvh; }
  .js .hero__title, .js .hero__brand, .js .hero__tagline, .js .hero__sub { opacity: 1 !important; transform: none !important; filter: none !important; }
  .js .hero__tagline { -webkit-mask-image: none !important; mask-image: none !important; }
  .hero__white { display: none; }

  /* Philosophy — collapse the pinned track; static dark clip with the copy centered */
  .philosophy { height: auto; margin-top: 0; padding-block: 0; }
  .philosophy__stage { position: static; min-height: 100svh; min-height: 100dvh; }
  .js .philosophy__stage { background: #000; }
  .js .philosophy__media,
  .js .philosophy__scrim { opacity: 1 !important; }
  .js .philosophy .measure,
  .js .philosophy .section-title,
  .js .philosophy .lede { opacity: 1 !important; transform: none !important; filter: none !important; }
  .js .philosophy .lede .rt-w { opacity: 1 !important; }   /* static layout: words not scrub-revealed */

  /* Pillars — collapse the pinned track; commitments simply visible and centered */
  .pillars { height: auto; margin-top: 0; padding-block: var(--section-y); }
  .pillars__stage { position: static; min-height: 0; padding-block: 0; }
  .js .pillars__inner,
  .js .pillars .swiper,
  .js .pillars .card { opacity: 1 !important; transform: none !important; filter: none !important; }

  /* Press — no rotation: show every logo at once in a calm, centered, wrapped row */
  .press__stage {
    position: static; height: auto;
    display: flex; flex-wrap: wrap; gap: 1.4rem 2rem;
    justify-content: center; align-items: center;
  }
  .press__logo {
    position: static; inset: auto; opacity: .82 !important;
    filter: none !important;   /* no resting blur */
    transform: none !important;
    max-height: clamp(3rem, 8vw, 4.5rem); max-width: 44vw;
  }
}

/* =========================================================================
   Premium polish
   ========================================================================= */

/* Scroll progress — a hairline pinned at the very top (JS sets --p: 0..1) */
.scroll-progress {
  position: fixed; inset: 0 0 auto 0;
  height: 2px; z-index: 130;
  background: var(--c-silver);
  transform: scaleX(var(--p, 0));
  transform-origin: left;
  pointer-events: none;
}

/* Editorial section indices — auto-numbered labels: (01) PHILOSOPHY … */
main { overflow-x: clip; }   /* contain horizontal slide-in offsets */

/* Header stays fully consistent on scroll — no colour, shadow, or size change. */


/* Footer links — silver underline wipe-in */
.site-footer__links a { border-bottom: 0 !important; }
.site-footer__links a::after {
  content: ""; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px;
  background: var(--c-silver); transform: scaleX(0); transform-origin: left;
  transition: transform var(--t);
}
.site-footer__links a { position: relative; }
.site-footer__links a:hover::after { transform: scaleX(1); }

/* Branded "U" — the PŪRIFY macron, snug just above any U in section titles.
   inline-block so the macron resolves against THIS letter's box, not the line. */
.brand-u { position: relative; display: inline-block; line-height: 1; }
.brand-u::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -.02em;                /* snug just above the cap U */
  width: .52em; height: .06em;
  background: currentColor;
  transform: translateX(-50%);
}

/* =========================================================================
   "Three Commitments" — split-flap board
   ========================================================================= */
.pillars__inner { text-align: center; }
.pillars__inner .section-title { margin-inline: auto; max-width: none; }

/* Pinned center-fade — the section pins and the commitments fade in at viewport
   centre (no rise). Pulled up to overlap the Philosophy white-out so it lands the
   moment that section finishes; transparent through the overlap so the clip plays.
   Heading (--in) then the cards (--in2) each take their own scroll moment. */
/* Two independent dials live in this height value:
   1. Fade-in distance = height − 100vh (scenes.js drives --in/--in2 over this scroll
      span). Keep it generous (~20vh) or the commitments pop in instead of fading.
   2. Clearance: height MUST stay ≥ 100vh. With margin-top -100vh, this box's bottom is
      where .approach begins; below 100vh .approach slides up under the Philosophy box
      (z-index 1) and its white fade-overlay clips the Approach heading. */
.pillars { background: transparent; position: relative; z-index: 1; height: 120vh; margin-top: -100vh; padding-block: 0; }
/* Stage is intentionally shorter than the viewport (~60svh) so the next section
   (Approach) rises into view beneath the commitments instead of them filling the
   whole screen — independent of the fade distance above. */
.pillars__stage {
  position: sticky;
  top: clamp(2.5rem, 8vh, 5.5rem);   /* nudged down from the top so the commitments don't sit too high when the moment begins */
  min-height: 56svh;
  display: flex; flex-direction: column; justify-content: center;
  padding-block: clamp(1rem, 3vh, 1.75rem);
}
.js .pillars__inner {
  opacity: var(--in, 0);
  transform: translateY(calc((1 - var(--in, 0)) * 1.5rem)) scale(calc(0.984 + var(--in, 0) * 0.016));
  filter: blur(calc((1 - var(--in, 0)) * 8px));
  will-change: opacity, transform, filter;
}
/* The swiper reveals as ONE unit as --in2 advances (cards 2/3 sit off-screen in
   the horizontal track, so per-card staggering would be invisible anyway).
   Opacity ONLY — a transform/filter here would make .swiper a backdrop root and
   cut the glass cards off from the page behind them. */
.js .pillars .swiper { opacity: var(--in2, 0); }

/* ----- What we stand for — swipeable glass hexagons ----- */
.swiper { margin-top: clamp(1rem, 2.2vw, 1.5rem); }
/* Horizontal scroll-snap carousel: swipe on touch, trackpad-scroll on desktop,
   arrows + dots (main.js) for mouse. The hexagons are wide enough that the
   track always overflows, so it's swipeable at every viewport. */
.swiper__track {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* room for first/last card to reach centre + for the hexagons' drop-shadows */
  padding: 1.25rem max(calc(50% - 210px), 1rem);
  scrollbar-width: none;
  /* soft edge fade so clipped cards dissolve at the track's sides, no hard cut */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 5% 95%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 5% 95%, transparent);
}
.swiper__track::-webkit-scrollbar { display: none; }
/* The card is the brand mark's hexagon, rendered as clear glass: the element
   itself is the bright rim; the ::before inset carries the frosted fill.
   clip-path cuts box-shadow/border, so depth is a drop-shadow (same pattern
   as the ritual hexagons). */
.card {
  position: relative;
  flex: none;
  width: clamp(300px, 80vw, 420px);
  aspect-ratio: 481 / 421;
  clip-path: var(--hex);
  scroll-snap-align: center;
  display: grid;
  place-items: center;
  align-content: center;
  text-align: center;
  padding: clamp(1.5rem, 4vw, 2.4rem) clamp(2.6rem, 9vw, 4.6rem);
  background: rgba(255, 255, 255, .8);            /* the glass rim (revealed 1.5px wide) */
  filter: drop-shadow(0 2px 4px rgba(17, 17, 17, .06)) drop-shadow(0 16px 30px rgba(17, 17, 17, .12));
  transition: translate .5s cubic-bezier(.2, .7, .2, 1), filter .5s ease;
}
.card:hover {                              /* gentle lift on pointer devices */
  translate: 0 -3px;
  filter: drop-shadow(0 3px 6px rgba(17, 17, 17, .07)) drop-shadow(0 22px 44px rgba(17, 17, 17, .16));
}
/* the glass itself — frosted translucent pane (the header's frost recipe) with
   a top-left light catch, inset behind the rim */
.card::before {
  content: "";
  position: absolute; inset: 1.5px;
  clip-path: var(--hex);
  background:
    radial-gradient(120% 120% at 30% 24%, rgba(255, 255, 255, .55) 0%, rgba(255, 255, 255, 0) 48%),
    linear-gradient(160deg, rgba(255, 255, 255, .55) 0%, rgba(240, 242, 245, .30) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
          backdrop-filter: blur(14px) saturate(140%);
}
/* the brand mark watermarked into the glass — dark-on-light sibling of the
   ritual hexagons' watermark */
.card::after {
  content: "";
  position: absolute; inset: 0; margin: auto;
  width: 100%; aspect-ratio: 560 / 499;
  pointer-events: none;
  background: rgba(20, 20, 25, .06);
  -webkit-mask: url(../img/pb-mark.png) center / contain no-repeat;
          mask: url(../img/pb-mark.png) center / contain no-repeat;
}
.card__title {
  position: relative; z-index: 1;
  font-family: var(--font-serif);
  font-size: clamp(1.25rem, 2.6vw, 1.5rem);
  color: var(--c-ink); margin: 0 0 .4rem; line-height: 1.12;
}
.card__desc {
  position: relative; z-index: 1;
  margin: 0; max-width: 24ch;
  font-family: var(--font-body); text-transform: none;
  font-weight: 500; color: var(--c-grey);
  line-height: 1.4; font-size: clamp(.9rem, 2vw, 1rem);
}
/* faint silver depth blobs behind the track — gives the frosted panes something
   to diffuse so the glass reads on the white page */
.pillars__stage::before {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background:
    radial-gradient(34% 42% at 26% 64%, rgba(198, 203, 209, .38) 0%, rgba(198, 203, 209, 0) 100%),
    radial-gradient(30% 38% at 74% 30%, rgba(213, 217, 222, .34) 0%, rgba(213, 217, 222, 0) 100%),
    radial-gradient(26% 32% at 56% 78%, rgba(230, 232, 235, .45) 0%, rgba(230, 232, 235, 0) 100%);
}

/* swipe controls — ghost arrows + silver dots, centered under the track */
.swiper__nav {
  display: flex; align-items: center; justify-content: center;
  gap: .6rem; margin-top: .75rem;
}
.swiper__arrow {
  width: 44px; height: 44px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--c-line); border-radius: 999px;
  background: var(--c-bg); color: var(--c-ink);
  font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: border-color var(--t), color var(--t), opacity var(--t);
}
.swiper__arrow:hover { border-color: var(--c-ink); }
.swiper__arrow:disabled { opacity: .3; cursor: default; }
.swiper__dots { display: flex; align-items: center; }
.swiper__dot {
  /* the visible 8px dot is the ::after; the button itself is a 36px hit area */
  width: 36px; height: 36px; padding: 0; border: 0;
  background: transparent; cursor: pointer;
  display: grid; place-items: center;
}
.swiper__dot::after {
  content: "";
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--c-silver);
  opacity: .5;
  transition: opacity var(--t), transform var(--t), background var(--t);
}
.swiper__dot.is-active::after { opacity: 1; background: var(--c-ink); transform: scale(1.3); }




/* Subtext treatment — heavier weight + tighter tracking so the Cormorant reads
   bolder and narrower (denser), to sit closer to the condensed Oswald headings.
   Placed after all the subtext rules so it overrides their per-element weights. */
.lede, .hero__sub, .card__desc, .flip-step__line, .body,
.pullquote, .founder__sign, .site-footer__tag {
  font-weight: 600;
  letter-spacing: -0.02em;
}
