/* =====================================================================
   Beauty by Kocer — "The Mirror Edit" (design 08)
   Mobile-first baseline = the safe, stacked document (also used for
   no-JS, JS-disabled, reduced motion and the motion toggle set to off).
   The pinned/morphing desktop experience is layered on top, scoped
   entirely under html.pin-ready so removing that one class always
   collapses the page back to this baseline. See mirror.js buildMotion().
   ===================================================================== */

/* ---------- Tokens ---------- */
:root {
  /* Colour: every colour used anywhere on the page is one of these. */
  --porcelain: #F4F1EC;
  --paper: #FBF9F6;
  --graphite: #1B1A1A;
  --ash: #6B6763;
  --mist: #E6E9EE;
  --lip: #E0355F;
  --lip-deep: #B31E45;
  --chrome: linear-gradient(135deg, #DFE3EA 0%, #F7F8FA 28%, #B9C0CC 52%, #EEF0F4 74%, #C9CFD8 100%);
  --chrome-solid: #C9CFD8;
  --chrome-mid: #DFE3EA;
  --rule: rgba(27, 26, 26, .14);
  --focus: #E0355F;
  /* .72 (not the more typical .5-.55) so the small captions painted on top
     of it — BEFORE/AFTER labels at 11px — clear 4.5:1 against their busy
     photo backgrounds; the same token also dims the dialog backdrop, where
     a darker scrim only helps the modal content read more clearly. */
  --scrim: rgba(27, 26, 26, .72);

  /* Type */
  --font-display: 'Italiana', Georgia, serif;
  --font-body: 'Jost', 'Segoe UI', sans-serif;
  --fs-11: 11px; --fs-14: 14px; --fs-16: 16px; --fs-18: 18px;
  --fs-24: 24px; --fs-48: 48px;
  --fs-hero: clamp(48px, 9vw, 160px);

  /* Spacing (4px base) */
  --sp-4: 4px; --sp-8: 8px; --sp-12: 12px; --sp-16: 16px; --sp-24: 24px;
  --sp-32: 32px; --sp-48: 48px; --sp-64: 64px; --sp-96: 96px;

  /* Radii + the one allowed shadow family */
  --radius-round: 999px;
  --radius-frame: 28px;
  --radius-small: 8px;
  --shadow-mirror: 0 40px 120px -40px rgba(27, 26, 26, .35);

  /* Easings: every transition/animation in this file names one of these. */
  --ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-iris: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-liquid: cubic-bezier(0.35, 0.35, 0, 1);
  --ease-sine: cubic-bezier(0.445, 0.05, 0.55, 0.95);
  --ease-out: cubic-bezier(0.33, 1, 0.68, 1);

  /* The mirror's current target shape, read by both the rim and whichever
     scene-media is active; mirror.js tweens these per scene. 62svh is
     scenes 1/2/7's shared resting circle — it clears the owner's ≥60svh
     floor for the scene 1 artist film, and the title/badge offsets (below)
     are derived from --ring-r so they always clear the wider ring around
     it regardless of this value. */
  --mirror-w: 62svh;
  --mirror-h: 62svh;
  --mirror-radius: 50%;
}

/* ---------- Reset ---------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--porcelain);
  color: var(--graphite);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-16);
  line-height: 1.6;
  /* Never overflow-x on body (past bug on other pages) — the stage and any
     wide decorative rows clip themselves instead. */
}
img { max-width: 100%; display: block; }
h1, h2 { text-wrap: balance; margin: 0; font-family: var(--font-display); letter-spacing: .04em; }
p { margin: 0; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
svg { display: block; }

/* Visible focus ring everywhere, including inside dialogs and the pinned stage. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 2px; }

.skip-link {
  position: absolute; left: var(--sp-16); top: -60px;
  background: var(--graphite); color: var(--paper);
  padding: 0 var(--sp-16); min-height: 44px; display: inline-flex; align-items: center;
  border-radius: var(--radius-small); font-size: var(--fs-14); z-index: 400;
  transition: top .2s var(--ease-standard);
}
.skip-link:focus-visible { top: var(--sp-16); }

.defs-only { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ---------- Fixed chrome (header, nav, dots) — always position:fixed,
   always siblings of .stage, never inside it (a fixed element inside a
   GSAP-pinned/transformed ancestor stops being fixed to the viewport). ---------- */
.chrome-header {
  position: fixed; inset: 0 0 auto 0; z-index: 200;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-8);
  padding: var(--sp-16) var(--sp-16);
  pointer-events: none; /* re-enabled on the direct children below */
}
.chrome-header > * { pointer-events: auto; min-width: 0; } /* let flex children shrink below their content size so nav-pills' own max-width (below) can actually clip instead of the header overflowing */
.wordmark {
  font-family: var(--font-display); font-size: var(--fs-24); letter-spacing: .04em;
  display: flex; flex-direction: column; line-height: 1.1; color: var(--graphite);
  min-height: 44px; justify-content: center; flex: none;
}
.wordmark-eyebrow { font-family: var(--font-body); font-size: var(--fs-11); letter-spacing: .18em; color: var(--ash); }
.chrome-actions { display: flex; align-items: center; gap: var(--sp-12); min-width: 0; }

#motion-toggle {
  border: 1px solid var(--rule); border-radius: var(--radius-round);
  padding: var(--sp-8) var(--sp-16); font-size: var(--fs-11); letter-spacing: .12em;
  text-transform: uppercase; background: var(--paper); min-height: 44px;
  transition: border-color .2s var(--ease-standard), background .2s var(--ease-standard);
}
#motion-toggle:hover { border-color: var(--lip); }
/* --lip on paper is 4.13:1 at this size — under the 4.5:1 text gate;
   --lip-deep clears 6.3:1. */
#motion-toggle span { color: var(--lip-deep); margin-left: var(--sp-4); }

/* Gooey nav: mobile-first baseline is a plain always-visible pill row.
   The collapsible chrome blob is a desktop+JS enhancement (see .js.pin-ready). */
.gooey-nav { display: flex; min-width: 0; }
.gooey-blob { display: flex; align-items: center; min-width: 0; }
.nav-trigger { display: none; }
.nav-pills {
  display: flex; gap: var(--sp-8); overflow-x: auto; max-width: 100%;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.nav-pills::-webkit-scrollbar { display: none; }
.nav-pill {
  flex: none; padding: var(--sp-8) var(--sp-12); border-radius: var(--radius-round);
  background: var(--paper); border: 1px solid var(--rule); font-size: var(--fs-11);
  letter-spacing: .1em; text-transform: uppercase; white-space: nowrap;
  min-height: 44px; display: inline-flex; align-items: center;
  transition: border-color .2s var(--ease-standard), color .2s var(--ease-standard);
}
.nav-pill:hover, .nav-pill[aria-current="true"] { border-color: var(--lip); color: var(--lip-deep); }

.scene-dots { display: none; } /* desktop-pinned only, see far below */

/* ---------- Preloader ---------- */
.preloader {
  display: none; position: fixed; inset: 0; z-index: 500;
  background: var(--porcelain); align-items: center; justify-content: center;
  flex-direction: column; gap: var(--sp-24);
}
.preloader.is-visible { display: flex; }
/* Defensive: display:none is already the unconditional default above (only
   .is-visible, added by JS, ever shows it), but this makes the no-JS
   guarantee explicit rather than implicit. */
html:not(.js) .preloader { display: none; }
.preloader-orb { position: relative; width: 140px; height: 140px; filter: url(#gooey-menu-filter); }
.orb-blob {
  position: absolute; border-radius: 50%; background: var(--chrome);
  width: 76px; height: 76px; top: 50%; left: 50%;
}
.orb-blob-a { transform: translate(-70%, -50%) scale(1); animation: orb-breathe 1600ms var(--ease-sine) infinite alternate; }
.orb-blob-b { transform: translate(-30%, -50%) scale(1); animation: orb-breathe 1600ms var(--ease-sine) infinite alternate-reverse; }
@keyframes orb-breathe {
  from { transform: translate(-70%, -50%) scale(.82); }
  to   { transform: translate(-70%, -50%) scale(1.14); }
}
.preloader-count { font-family: var(--font-body); font-size: var(--fs-14); letter-spacing: .18em; color: var(--ash); }
.preloader.is-leaving { animation: preloader-iris 700ms var(--ease-iris) forwards; }
@keyframes preloader-iris { to { clip-path: circle(0% at 50% 50%); opacity: 0; } }

/* ---------- Cursor (fine pointers, motion on, desktop only — see JS gate) ---------- */
.cursor-dot, .cursor-ring { display: none; position: fixed; top: 0; left: 0; z-index: 450; pointer-events: none; }
html.custom-cursor-active .cursor-dot { display: block; }
html.custom-cursor-active .cursor-ring { display: flex; }
.cursor-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--lip); }
.cursor-ring {
  /* Fixed 40px box; the "grows to 56px" effect is transform:scale (1.4x,
     applied in mirror.js alongside the ring's own per-frame lerp/squash
     transform, since JS overwrites the full inline transform every frame
     and a separate CSS width/height transition would fight that) rather
     than animating width/height, which are layout properties outside the
     brief's one documented morph exception. */
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--chrome-solid);
  background: rgba(255, 255, 255, .08); align-items: center; justify-content: center;
  transition: border-color .2s var(--ease-standard);
}
.cursor-ring.state-view { border-color: var(--lip); }
.cursor-label {
  font-size: var(--fs-11); letter-spacing: .16em; color: var(--lip); opacity: 0;
  transition: opacity .15s var(--ease-standard);
}
.cursor-ring.state-view .cursor-label { opacity: 1; }

/* ---------- Grain texture: fixed, decorative, outside the pinned stage ---------- */
.grain {
  position: fixed; inset: 0; z-index: 440; opacity: .04; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
}

/* =====================================================================
   Stage baseline — stacked document. This is what renders with no JS,
   below 1024px, with motion off, and under prefers-reduced-motion.
   ===================================================================== */
.stage { position: relative; padding-top: 84px; }
.mirror-rim { display: none; } /* pin-ready only */

.scene {
  position: relative;
  min-height: 100svh;
  padding: var(--sp-64) var(--sp-16) var(--sp-96);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-24);
  text-align: center;
  border-bottom: 1px solid var(--rule);
}
.scene:last-child { border-bottom: 0; }
/* The reveal-on-scroll starting state is JS-only: html.js is added as the
   very first init statement in mirror.js and never appears at all without
   JS, so without it this rule never applies and every scene simply renders
   at its natural opacity:1 — full content, no animation, no dependency on
   an IntersectionObserver ever firing.
   :not(.pin-ready) matters once a scene has ever been revealed (.is-in is
   added once and never removed): html.js .scene.is-in has one more class
   than html.pin-ready .scene, so without this exclusion it would outrank
   and permanently defeat pin-ready's own opacity:0/.is-active toggling —
   which is exactly what happened (scene 1 stuck visible behind every
   later scene) before this line was added. */
html.js:not(.pin-ready) .scene {
  opacity: 0; transform: translateY(24px);
  transition: opacity .6s var(--ease-standard), transform .6s var(--ease-standard);
}
html.js:not(.pin-ready) .scene.is-in { opacity: 1; transform: translateY(0); }

.scene-eyebrow, .lookbook-label {
  font-size: var(--fs-11); letter-spacing: .18em; text-transform: uppercase; color: var(--ash);
}
.scene-hero-title { font-size: var(--fs-hero); line-height: 1.02; max-width: 18ch; }
.h1-half { display: inline; }
.scene h2 { font-size: var(--fs-48); line-height: 1.08; max-width: 20ch; }
.scene p:not(.scene-eyebrow):not(.lookbook-label) { max-width: 48ch; color: var(--graphite); font-size: var(--fs-18); line-height: 1.6; }
.scene-hint { font-size: var(--fs-14); color: var(--ash); }
/* Baseline sizing/colour already comes from the generic ".scene p" rule
   above (this is a plain <p>); this class exists as a hook for the
   pin-ready bottom placement below. */
.scene-1-copy .scene-link { margin-left: var(--sp-4); }

.scene-media {
  position: relative; width: 100%; max-width: 420px; margin: 0 auto;
  border-radius: var(--radius-frame); overflow: hidden; background: var(--mist);
  box-shadow: var(--shadow-mirror);
}
.scene-media img, .scene-media video { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-1, .media-2, .media-5, .media-6, .media-8 { aspect-ratio: 1; border-radius: var(--radius-round); }
.media-4-frame { aspect-ratio: 3 / 4; }
.media-6 img { object-position: top; }
/* after-portrait.jpg is 9:16, not 3:4 — cover previously cropped it in
   every placement, so Kocer's own before/after pair was never complete
   anywhere (content rule 2). contain on the baseline shows it whole; the
   pinned circle still crops it (a circle can't show a 9:16 rectangle
   uncropped), so the button below also opens the lightbox with both
   portraits complete. */
.media-7 { aspect-ratio: 9 / 16; }
.media-7 img { object-fit: contain; }
.media-7-btn { display: block; width: 100%; height: 100%; }

.scene-copy { display: flex; flex-direction: column; align-items: center; gap: var(--sp-16); max-width: 56ch; }
.scene-link {
  display: inline-flex; align-items: center; gap: var(--sp-4); min-height: 44px;
  font-size: var(--fs-14); letter-spacing: .06em; border-bottom: 1px solid var(--lip); padding: 0 var(--sp-4);
  transition: color .2s var(--ease-standard), border-color .2s var(--ease-standard);
}
.scene-link:hover { color: var(--lip-deep); }

/* Scene 1 extras: ring + play badge. On the baseline layout the badge is a
   normal in-flow sibling right after the media (not overlaid on the video —
   see the pin-ready override below for why it moved out of .scene-media). */
.play-badge {
  display: inline-flex; align-items: center; gap: var(--sp-8);
  background: var(--paper); border-radius: var(--radius-round);
  padding: var(--sp-12) var(--sp-16); font-size: var(--fs-11); letter-spacing: .1em;
  text-transform: uppercase; box-shadow: var(--shadow-mirror);
  min-height: 44px;
}
.ring-marquee { display: none; } /* decorative, desktop-pinned only */
.media-error { font-size: var(--fs-14); color: var(--ash); margin-top: var(--sp-8); }
.media-error a { text-decoration: underline; }
/* Inside a .scene-media box specifically, overflow:hidden clips the shaped
   frame to its circle/rounded-rect — an in-flow error paragraph would be
   clipped away right when it's needed, so it overlays as a caption instead. */
.scene-media .media-error {
  position: absolute; inset: auto var(--sp-12) var(--sp-12) var(--sp-12); margin: 0;
  background: var(--scrim); color: var(--paper); padding: var(--sp-8) var(--sp-12);
  border-radius: var(--radius-small); font-size: var(--fs-14);
}
.scene-media .media-error a { color: var(--paper); }

/* Scene 2: letter-scrub paragraph reads as plain text until pin-ready splits it. */
.scrub-text .char { opacity: 1; filter: none; transform: none; }
.scrub-text .char-word { white-space: nowrap; }

/* Scene 3: baseline shows two complete, uncropped 3:4 frames stacked — the
   gooey circles are a desktop-pinned-only treatment (see far below). */
.split-goo { display: flex; flex-direction: column; gap: var(--sp-16); width: 100%; max-width: 420px; }
.split-circle {
  position: relative; display: block; width: 100%; aspect-ratio: 3 / 4;
  border-radius: var(--radius-frame); overflow: hidden; box-shadow: var(--shadow-mirror);
}
.split-circle img { width: 100%; height: 100%; object-fit: contain; background: var(--paper); }
.split-caption {
  position: absolute; left: var(--sp-12); bottom: var(--sp-12);
  font-size: var(--fs-11); letter-spacing: .14em; color: var(--paper);
  background: var(--scrim); padding: var(--sp-4) var(--sp-8); border-radius: var(--radius-small);
}

/* Scene 4: ripple badge sits over the poster until played. */
.ripple-badge {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.ripple-badge b {
  position: relative; z-index: 1; width: 64px; height: 64px; border-radius: 50%;
  background: var(--paper); display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-18); color: var(--lip); box-shadow: var(--shadow-mirror);
}
.ripple-badge span {
  position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--chrome-solid);
  animation: ripple-wave 3s var(--ease-out) infinite; animation-delay: calc(var(--i, 0) * 750ms);
}
.ripple-badge span:nth-child(1) { --i: 0; } .ripple-badge span:nth-child(2) { --i: 1; }
.ripple-badge span:nth-child(3) { --i: 2; } .ripple-badge span:nth-child(4) { --i: 3; }
@keyframes ripple-wave { from { transform: scale(.8); opacity: .8; } to { transform: scale(2.4); opacity: 0; } }
.client-video.is-playing ~ .ripple-badge, .media-4-frame.is-playing .ripple-badge { display: none; }
.still-chip { margin-top: var(--sp-16); width: 96px; }
.still-chip img { border-radius: var(--radius-small); height: auto; }
.still-chip figcaption { font-size: var(--fs-11); color: var(--ash); margin-top: var(--sp-4); }

/* Scene 5: two complete, uncropped squares beside the mirror photo. */
.side-square { width: 100%; max-width: 320px; margin: 0 auto; }
.side-square button { display: block; width: 100%; border-radius: var(--radius-small); overflow: hidden; position: relative; }
/* #165 magnetic directional glow: mirror.js sets .dir-0..3 (top/right/bottom/left)
   from the pointer's entry angle on pointerenter; each direction just points
   the same sweep gradient at a different edge, 500ms, chrome-toned. */
.glow-sweep { position: absolute; inset: 0; opacity: 0; pointer-events: none; transition: opacity .5s var(--ease-standard); }
button.dir-0 .glow-sweep, button.dir-1 .glow-sweep, button.dir-2 .glow-sweep, button.dir-3 .glow-sweep { opacity: 1; }
button.dir-0 .glow-sweep { background: linear-gradient(to bottom, rgba(255, 255, 255, .55), transparent 55%); }
button.dir-2 .glow-sweep { background: linear-gradient(to top, rgba(255, 255, 255, .55), transparent 55%); }
button.dir-3 .glow-sweep { background: linear-gradient(to right, rgba(255, 255, 255, .55), transparent 55%); }
button.dir-1 .glow-sweep { background: linear-gradient(to left, rgba(255, 255, 255, .55), transparent 55%); }
.side-square img { width: 100%; height: auto; aspect-ratio: 1; object-fit: contain; background: var(--paper); }
.side-square figcaption { font-size: var(--fs-11); letter-spacing: .1em; color: var(--ash); margin-top: var(--sp-8); display: flex; flex-direction: column; gap: var(--sp-4); }
.side-square figcaption a { text-decoration: underline; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

/* Scene 6: certificate + orbit portraits become a horizontal snap row. */
.tilt-frame { position: relative; }
.tilt-glare { display: none; } /* desktop-pinned only */
/* width:100% is required here: as a flex item in .scene's column layout
   (align-items:center, not stretch), .orbit would otherwise size to fit its
   own 480px-wide content instead of shrinking to the viewport, overflowing
   past both edges — overflow-x:auto alone only clips content that's larger
   than the element's OWN box, and without this the box itself was the
   content's full size. */
.orbit { width: 100%; display: flex; gap: var(--sp-16); overflow-x: auto; padding: var(--sp-8) 0; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; }
/* No transform reset needed here: mirror.js only ever sets a transform on
   these elements once html.pin-ready is active, and that never happens
   below 1024px where this baseline row layout applies — an !important
   reset would (and previously did) also cancel mirror.js's own JS-set
   inline transform under pin-ready, since !important beats inline style. */
.orbit-ring { display: flex; gap: var(--sp-16); }
.orbit-item {
  flex: none; width: 108px; scroll-snap-align: start; display: flex; flex-direction: column; gap: var(--sp-8);
}
.orbit-item img { width: 108px; height: 144px; object-fit: cover; border-radius: var(--radius-small); aspect-ratio: 3 / 4; }
.orbit-caption { font-size: var(--fs-11); letter-spacing: .06em; color: var(--ash); opacity: 1; }

/* Scene 7: before/after frames stacked, path SVG shown as a static connector. */
.frame-916 { width: 100%; max-width: 280px; margin: 0 auto; position: relative; }
.frame-916 img { width: 100%; height: auto; aspect-ratio: 9 / 16; object-fit: contain; border-radius: var(--radius-frame); background: var(--paper); box-shadow: var(--shadow-mirror); }
.frame-916 figcaption, .frame-caption {
  position: absolute; left: var(--sp-12); bottom: var(--sp-12); font-size: var(--fs-11);
  letter-spacing: .14em; color: var(--paper); background: var(--scrim); padding: var(--sp-4) var(--sp-8); border-radius: var(--radius-small);
}
.media-7 { position: relative; }
.brush-paths { width: 100%; max-width: 240px; height: 60px; margin: 0 auto; }
.brush-stroke { fill: none; stroke: var(--lip); stroke-width: 3; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 0; }
.shimmer-btn {
  /* max-width:100% + normal wrapping: .copy-right's flex align-items:flex-start
     sizes children to their own content instead of stretching them to the
     column's max-width, so without a cap of its own this button's natural
     (single-line) width could exceed the narrow mirror-clearing column and
     run past the viewport's right edge instead of wrapping inside it. */
  max-width: 100%; white-space: normal; text-align: inherit;
  margin-top: var(--sp-8); font-size: var(--fs-14); letter-spacing: .08em; min-height: 44px;
  /* graphite/ash (not ash/paper): the highlight stop used to paint --paper
     text on the porcelain page — ~1.05:1, invisible — as it swept across a
     real interactive control three times a second. Worst case is now 4.9:1. */
  background: linear-gradient(110deg, var(--graphite) 20%, var(--ash) 50%, var(--graphite) 80%);
  background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: shimmer-move 3s var(--ease-sine) infinite;
}
@keyframes shimmer-move { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* Scene 8: bloom becomes a small, deliberately modest porcelain-to-chrome
   disc behind the CTA on the stacked layout — full width here read as an
   oversized empty circle rather than a considered decoration. */
.bloom-fill { width: 40vw; max-width: 180px; aspect-ratio: 1; background: radial-gradient(circle at 50% 35%, var(--paper) 0%, var(--chrome-mid) 70%, var(--chrome-solid) 100%); }
.copy-bloom { position: relative; }
.cta-pill {
  display: inline-flex; align-items: center; gap: var(--sp-8); min-height: 44px;
  padding: var(--sp-16) var(--sp-32); border-radius: var(--radius-round); background: var(--chrome);
  font-size: var(--fs-14); letter-spacing: .08em; color: var(--graphite); position: relative; overflow: visible;
}
.particle-field { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 5; }
.particle { position: absolute; width: 6px; height: 6px; border-radius: 50%; will-change: transform, opacity; }

/* ---------- Footer + rubber-band dividers ---------- */
.site-footer {
  padding: var(--sp-48) var(--sp-16); display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-16); text-align: center; font-size: var(--fs-14); color: var(--ash);
}
.site-footer a { min-height: 44px; display: inline-flex; align-items: center; gap: .35em; text-decoration: underline; }
.rubber-divider { width: 100%; max-width: 240px; height: 20px; }
.rubber-path { fill: none; stroke: var(--rule); stroke-width: 1; }

/* =====================================================================
   Dialogs (film + lightbox) — native <dialog>, chrome iris open/close.
   ===================================================================== */
dialog {
  border: 0; border-radius: var(--radius-frame); padding: 0; max-width: min(92vw, 720px);
  max-height: 92dvh; background: var(--paper); box-shadow: var(--shadow-mirror);
  clip-path: circle(0% at var(--ox, 50%) var(--oy, 50%));
  opacity: 0;
}
dialog[open] { clip-path: circle(150% at var(--ox, 50%) var(--oy, 50%)); opacity: 1; transition: clip-path .5s var(--ease-standard), opacity .5s var(--ease-standard); }
dialog[data-state="opening"] { clip-path: circle(0% at var(--ox, 50%) var(--oy, 50%)); opacity: 0; transition: none; }
dialog[data-state="closing"] { clip-path: circle(0% at var(--ox, 50%) var(--oy, 50%)); opacity: 0; }
dialog::backdrop { background: var(--scrim); }
body.dialog-open { overflow: hidden; }

.dialog-bar { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-16); border-bottom: 1px solid var(--rule); }
.dialog-bar h2 { font-size: var(--fs-24); }
.dialog-close { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; font-size: var(--fs-18); }

.film-video { width: 100%; max-height: 70dvh; background: var(--graphite); display: block; }
/* Always visible (not nested in the hidden .media-error) — the source post
   link needs to be reachable every time a film dialog opens, not only when
   the video fails to load. */
.film-source { display: flex; align-items: center; min-height: 44px; padding: 0 var(--sp-16); text-decoration: underline; font-size: var(--fs-14); }
.film-dialog .media-error, .lightbox-dialog .media-error { padding: 0 var(--sp-16) var(--sp-16); }
.film-source-error { text-decoration: underline; }

.lightbox-body { display: flex; gap: var(--sp-8); padding: var(--sp-16); overflow: auto; max-height: 70dvh; }
.lightbox-body img { width: 100%; height: auto; object-fit: contain; }
.lightbox-source { display: block; padding: 0 var(--sp-16) var(--sp-16); text-decoration: underline; font-size: var(--fs-14); min-height: 44px; }

/* =====================================================================
   Below 1024px: the wordmark/toggle stay on one row, but the nav pills
   wrap to their own full-width scrollable row underneath rather than
   competing for space beside the wordmark — the header also gets a solid
   background and enough height that .stage's own top padding (below)
   never lets a scene's content start underneath it.
   ===================================================================== */
@media (max-width: 1023.98px) {
  .chrome-header { flex-wrap: wrap; background: var(--porcelain); padding: var(--sp-12) var(--sp-16); row-gap: var(--sp-8); }
  .wordmark { order: 1; }
  .chrome-actions { order: 2; }
  .gooey-nav { order: 3; flex: 1 0 100%; width: 100%; }
  .gooey-blob { width: 100%; }
  .nav-pills { width: 100%; }
  .stage { padding-top: 132px; }
  /* scroll-margin-top matters beyond just the first scene: scrollIntoView
     (used by the nav pills' native anchor jump, and by anyone tapping a
     #scene-N link directly) aligns a target's top edge with the absolute
     viewport top by default, with no awareness that a fixed header
     overlays that same band — every scene needs this, not just whichever
     one .stage's own top padding happens to clear. */
  .scene { scroll-margin-top: 132px; }
  @media all { html:not(.motion-off) .chrome-header { transition: transform .35s var(--ease-standard); } }
  html.header-away .chrome-header:not(:focus-within) { transform: translateY(-100%); }

  /* Scenes size to their content: a forced full-screen height left whole
     empty screens between short scenes while scrolling on a phone. */
  .scene { min-height: auto; padding: var(--sp-48) var(--sp-16) var(--sp-64); }

  /* The opening film is an oval vanity mirror rather than a circle: the
     circle was only 42% of a phone screen tall (the owner's floor for a
     film is 52%). It is sized to sit whole on the first screen under the
     header, eyebrow and title (the 320px), and its 3:4 shape matches the
     film, so nothing of it is cropped away but the oval's corners. */
  .scene-1 { padding-top: var(--sp-16); }
  .media-1 { aspect-ratio: 3 / 4; width: min(100%, calc((100svh - 320px) * .75)); border-radius: 50%; }

  /* Shimmer text alone read as a caption on a phone: a pill edge makes it
     look like the button it is. */
  .shimmer-btn { border: 1px solid var(--rule); border-radius: var(--radius-round); padding: var(--sp-8) var(--sp-24); }
}

/* =====================================================================
   ≥1024px, motion allowed: chrome scales up. The pin itself only turns
   on once mirror.js confirms GSAP loaded + measures ≥1024px (html.pin-ready).
   ===================================================================== */
@media (min-width: 1024px) {
  .chrome-header { padding: var(--sp-24) var(--sp-32); }
  .scene-dots {
    display: flex; flex-direction: column; gap: var(--sp-12); position: fixed;
    right: var(--sp-24); top: 50%; transform: translateY(-50%); z-index: 200;
  }
  .scene-dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--chrome-solid);
    transition: transform .3s var(--ease-standard), background .3s var(--ease-standard);
  }
  .scene-dot.is-active { transform: scale(1.6); background: var(--lip); }

  /* The collapsible gooey blob (#131) only makes sense once JS confirms the
     pin can run; everywhere else the plain always-visible row above stands. */
  html.js.pin-ready .gooey-blob { filter: url(#gooey-menu-filter); }
  html.js.pin-ready .nav-trigger {
    display: flex; width: 44px; height: 44px; border-radius: 50%; background: var(--chrome);
    align-items: center; justify-content: center; box-shadow: var(--shadow-mirror);
  }
  .nav-trigger-mark { width: 14px; height: 14px; border-radius: 50%; background: var(--paper); }
  html.js.pin-ready .nav-pills {
    position: absolute; right: 52px; top: 50%; transform: translateY(-50%);
    max-width: none; flex-direction: row;
  }
  /* Collapsed: every pill sits scaled to nothing directly behind the
     trigger; opening staggers each one out 30ms apart, 420ms, per the brief.
     pointer-events:none on the CONTAINER itself (not just the pills) matters:
     max-width:none above means .nav-pills keeps its full natural flex width
     for layout/hit-testing regardless of its children's transform:scale(0)
     (a transform never changes a box's own layout size) — without this, that
     invisible full-width box sat on top of #motion-toggle and silently ate
     every click meant for the toggle. */
  html.js.pin-ready .nav-pills { pointer-events: none; }
  html.js.pin-ready .nav-pills.is-open { pointer-events: auto; }
  html.js.pin-ready .nav-pills .nav-pill {
    opacity: 0; transform: translateX(0) scale(0); pointer-events: none;
    transition: opacity .42s var(--ease-standard), transform .42s var(--ease-standard);
  }
  html.js.pin-ready .nav-pills.is-open .nav-pill {
    opacity: 1; transform: translateX(0) scale(1); pointer-events: auto;
  }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(1) { transition-delay: 0ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(2) { transition-delay: 30ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(3) { transition-delay: 60ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(4) { transition-delay: 90ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(5) { transition-delay: 120ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(6) { transition-delay: 150ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(7) { transition-delay: 180ms; }
  html.js.pin-ready .nav-pills.is-open .nav-pill:nth-child(8) { transition-delay: 210ms; }
}

/* =====================================================================
   Desktop pinned experience — everything below is scoped to
   html.pin-ready, set by mirror.js only when GSAP + ScrollTrigger loaded,
   the viewport is ≥1024px, and motion is allowed. Removing the class
   (resize below 1024px, toggle off, reduced-motion change) reverts every
   rule here and the baseline above takes back over.
   ===================================================================== */
html.pin-ready .stage { padding-top: 0; height: 100svh; overflow: hidden; }
html.pin-ready .mirror-rim {
  display: block; position: absolute; top: 50%; left: 50%; z-index: 2; pointer-events: none;
  width: calc(var(--mirror-w) + 4px); height: calc(var(--mirror-h) + 4px);
  border-radius: var(--mirror-radius) / var(--mirror-radius-y, var(--mirror-radius)); transform: translate(-50%, -50%) scale(var(--mirror-scale, 1));
  background: var(--chrome); box-shadow: var(--shadow-mirror);
}

html.pin-ready .scene {
  position: absolute; inset: 0; min-height: 0; padding: var(--sp-64) 9vw;
  border-bottom: 0; opacity: 0; pointer-events: none; transition: opacity .3s var(--ease-standard);
  /* html.js .scene sets transform:translateY(24px) for the stacked-layout
     reveal; pin-ready manages visibility via .is-active/opacity instead, so
     without this explicit reset every scene here silently kept that 24px
     offset (the two rules don't conflict on the same value, so nothing
     forced it back to none) and every mirror-relative overlap calculation
     below was quietly off by it. */
  transform: none;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
}
html.pin-ready .scene.is-active { opacity: 1; pointer-events: auto; z-index: 3; }

/* Copy placement: three named grid columns, left / center / right / bloom.
   copy-left/right's max-width is computed, not guessed: 50vw - var(--mirror-w)/2
   is the distance from the viewport's centre (where the mirror is centred)
   out to the mirror's own edge; subtracting the 9vw side padding and a
   32px breathing gap gives exactly the space actually free between the
   padding edge and the mirror at the CURRENT mirror size, at any of the
   tested viewport widths — so the column's content can never reach the
   mirror, rather than merely surviving a graze via z-index/text-shadow. */
html.pin-ready .copy-left {
  grid-column: 1 / 2; justify-self: start; text-align: left; align-items: flex-start;
  max-width: calc(50vw - var(--mirror-w) / 2 - 9vw - 32px);
}
html.pin-ready .copy-right {
  grid-column: 3 / 4; justify-self: end; text-align: left; align-items: flex-start;
  max-width: calc(50vw - var(--mirror-w) / 2 - 9vw - 32px);
}
/* copy-center (scene 3) sits at the bottom, not dead centre — the split
   circles occupy the vertical middle, and .scene's own align-items:center
   would otherwise land this copy directly behind them. Scene 3's own
   bottom padding is trimmed (below) to make enough room for four lines of
   copy under a 44svh circle without needing this to be flush against the
   scene's very edge. */
html.pin-ready .copy-center { grid-column: 1 / -1; justify-self: center; align-self: end; text-align: center; align-items: center; max-width: 56ch; }
html.pin-ready .scene-3 { padding-bottom: var(--sp-16); }
html.pin-ready .copy-top { grid-column: 1 / -1; justify-self: center; text-align: center; align-items: center; max-width: 56ch; }
html.pin-ready .copy-bloom { grid-column: 1 / -1; justify-self: center; text-align: center; align-items: center; z-index: 6; max-width: 56ch; }
html.pin-ready .scene-copy { gap: var(--sp-24); position: relative; z-index: 5; }

/* Every scene's own media frame is centred at the mirror's exact position
   (reads the same --mirror-* custom properties as the rim) so it lands
   inside the rim and the two morph in sync — the rim never moves, the
   frame underneath swaps and reshapes as mirror.js drives it per scene. */
html.pin-ready .scene-media {
  position: absolute; top: 50%; left: 50%; grid-column: 1 / -1;
  transform: translate(-50%, -50%) scale(var(--mirror-scale, 1));
  width: var(--mirror-w); height: var(--mirror-h); border-radius: var(--mirror-radius) / var(--mirror-radius-y, var(--mirror-radius));
  max-width: none; margin: 0; box-shadow: none; opacity: 0; z-index: 3;
  transition: none;
}
/* Media opacity is NOT tied to .is-active: mirror.js crossfades two scenes'
   media in overlapping one-shot timelines (liquid/gooey/morph), so it owns
   this property directly and sets each element's resting opacity on init. */
html.pin-ready .media-1, html.pin-ready .media-2, html.pin-ready .media-5,
html.pin-ready .media-6, html.pin-ready .media-8 { aspect-ratio: auto; }
html.pin-ready .media-4-frame, html.pin-ready .media-7 { aspect-ratio: auto; }
/* The baseline's uncropped contain (above) is deliberately only for the
   stacked/no-JS layout — poured into the pinned circle, contain would
   leave visible porcelain gaps inside the mirror; cover keeps the circle
   filled, and the lightbox (data-lightbox="transformation") is where the
   complete, uncropped 9:16 pair lives once pinned. */
html.pin-ready .media-7 img { object-fit: cover; }

/* Scene 1: the h1 splits into two halves that sit entirely in the outer
   columns, flanking the mirror at its own vertical centre — display:contents
   removes the <h1> box itself so .h1-half-left/right become direct grid
   items of .scene-1, each with the same mirror-clearing max-width formula
   used for copy-left/right. The play badge and ring are decorative
   siblings of .scene-media (not nested inside it — its overflow:hidden
   circular clip would otherwise clip the badge once it sits below). */
html.pin-ready .scene-hero-title { display: contents; }
/* Absolutely positioned off the mirror's own edge (not grid-column +
   justify-self:end) — a grid column's boundary is a fixed 1/3 of the
   viewport, unrelated to the mirror's actual current pixel edge, so
   right-aligning "into" that column let the text's edge sit past the
   mirror regardless of how narrow max-width made it. Anchoring the
   position itself to var(--mirror-w) guarantees the gap instead. */
/* --ring-r is the textPath ring's own radius (its diameter is
   var(--mirror-w) + 140px, see .ring-marquee below) — the title and play
   badge need to clear the RING, not the mirror underneath it, since the
   ring is the larger of the two. Clearing only the mirror (an earlier
   version of this) left both text and button touching/inside the ring. */
html.pin-ready .scene-1 { --ring-r: calc(var(--mirror-w) / 2 + 70px); }
html.pin-ready .h1-half {
  position: absolute; top: 50%; transform: translateY(-50%);
  font-size: clamp(40px, 6vw, 96px); line-height: 1.05;
  max-width: calc(41vw - var(--ring-r) - 48px);
}
html.pin-ready .h1-half-left { right: calc(50% + var(--ring-r) + 48px); text-align: right; }
html.pin-ready .h1-half-right { left: calc(50% + var(--ring-r) + 48px); text-align: left; }
html.pin-ready .h1-half .word { display: inline-block; }

html.pin-ready .ring-marquee {
  display: block; position: absolute; top: 50%; left: 50%; z-index: 1; pointer-events: none;
  width: calc(var(--ring-r) * 2); height: calc(var(--ring-r) * 2);
  transform: translate(-50%, -50%);
}
.ring-marquee text { font-family: var(--font-body); font-size: var(--fs-11); letter-spacing: .22em; fill: var(--ash); }
.ring-spin { transform-box: fill-box; transform-origin: 50% 50%; will-change: transform; }
html.pin-ready .scene-1 .scene-eyebrow { grid-column: 1 / -1; justify-self: center; align-self: start; }
/* Not bottom-stacked: at the tested viewport heights the ring already
   claims almost the entire top/bottom margin (hint + badge alone fill
   the gap below it), so a multi-line paragraph placed there sits at the
   ring's own vertical centre — its widest point — and renders behind the
   mirror with only slivers escaping either edge. Reusing the h1-half
   formula instead (no grid-column: an absolutely positioned grid item
   with one keeps that area, sized to its own auto row, as its containing
   block instead of the full scene box) clears the ring at every height,
   not just its equator, because that offset is sized for the ring's
   widest point already. Sits under the shorter "again." half, below the
   title's own line.
   Selector needs the extra "p" type qualifier: the generic ".scene
   p:not(.scene-eyebrow):not(.lookbook-label)" rule above is (0,0,3,1) —
   "html.pin-ready .scene-1-copy" alone is only (0,0,2,1) and silently lost
   its max-width to that rule (measured ~327px, not ~204px, running flush
   to the viewport edge). "html.pin-ready .scene-1 p.scene-1-copy" is
   (0,0,3,2): it wins outright instead of tying on source order. */
html.pin-ready .scene-1 p.scene-1-copy {
  position: absolute; top: calc(50% + 64px);
  left: calc(50% + var(--ring-r) + 48px);
  max-width: calc(41vw - var(--ring-r) - 48px);
  text-align: left; font-size: var(--fs-14); line-height: 1.55;
}
/* Below the mirror, not over it: the badge is a plain sibling positioned
   off the mirror's own current half-height, so it tracks scene 1's actual
   size instead of a guessed offset. Gap to the ring trimmed 24px -> 8px:
   at 62svh the ring alone leaves only ~76px below it at 1366x768, and the
   hint (below the badge, see next rule) now has to share that margin too. */
html.pin-ready .play-badge {
  grid-column: 1 / -1; justify-self: center;
  position: absolute; top: calc(50% + var(--ring-r) + 8px); left: 50%; transform: translateX(-50%);
}
/* Was grid align-self:end (stacked in .scene-1's flow, above the badge) —
   at 1440x900 that put the hint's box high enough to overlap the ring's
   own bounding circle (measured: distance from ring centre 357px < a
   349px radius). Anchoring it below the badge instead, with the same
   ring-r-derived math, guarantees it starts under the badge which is
   itself already clear of the ring — so the hint clears the ring for the
   same reason, not by a second independent margin. 8+44(badge's own
   min-height)+8 = 60px; line-height trimmed so it still fits inside the
   ~76px margin left below the ring at 1366x768 alongside the badge. */
html.pin-ready .scene-1 p.scene-hint {
  position: absolute; top: calc(50% + var(--ring-r) + 58px); left: 50%; transform: translateX(-50%);
  font-size: var(--fs-11); line-height: 1.2; white-space: nowrap;
}

/* Scene 2: letter-scrub paragraph characters animate individually via inline
   styles set in JS. No will-change: ~90 spans is enough that permanently
   promoting all of them to their own compositor layer for the whole 800%
   pin outweighs the benefit at this size. */
html.pin-ready .scrub-text .char { display: inline-block; }

/* Scene 3: the mirror hides; two circles split apart with the gooey filter.
   No explicit grid-column/row: an UNPLACED absolutely-positioned grid item
   uses the grid container's own padding box as its containing block, so
   top/left:50% centre on the full scene — giving it explicit placement
   into row 1 instead (tried first) constrained it to that row's own
   auto-sized track, which (row 1 having no in-flow content to size it by)
   was noticeably shorter than the scene, throwing off every centred child. */
html.pin-ready .split-goo {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: block; width: 1px; height: 1px; max-width: none; filter: url(#gooey-menu-filter); z-index: 3;
}
html.pin-ready .split-circle {
  /* No transform here: mirror.js tweens x directly via GSAP (which then
     owns the whole inline transform), so a resting translateX would only
     ever be immediately overwritten — the margin trick alone centres it.
     44svh (not 56svh) leaves the copy below room to sit clear of both
     circles once they're apart. */
  position: absolute; top: 50%; left: 50%; width: 44svh; height: 44svh; aspect-ratio: auto;
  border-radius: 50%; margin-left: calc(-1 * 44svh / 2); margin-top: calc(-1 * 44svh / 2);
  box-shadow: none;
}
html.pin-ready .split-circle img { object-fit: cover; }

/* Scene 4: ripple badge already centred by its own absolute rule above. */

/* Scene 5: two full, uncropped squares flank the shrunken mirror. #109's
   staggered rotateY entrance needs a perspective context on the ancestor. */
html.pin-ready .scene-5 { perspective: 1200px; }
html.pin-ready .side-square { grid-row: 1; max-width: none; }
html.pin-ready .side-square-left { grid-column: 1 / 2; justify-self: end; align-self: center; width: min(62svh, 30vw); }
html.pin-ready .side-square-right { grid-column: 3 / 4; justify-self: start; align-self: center; width: min(62svh, 30vw); }
html.pin-ready .side-square img { aspect-ratio: 1; object-fit: contain; }
html.pin-ready .copy-top { align-self: start; margin-top: var(--sp-24); }

/* Scene 6: tilt certificate + two counter-rotating orbit rings. The label
   sits in the top-left corner (not top-centre) — at any rotation angle the
   orbit's own reach (mirror.js caps outer at 40svh, tile half at 8svh, so
   at most 48svh from centre) stays inside a viewport's centre band, but a
   CORNER is offset on both axes from that centre, clear regardless. */
html.pin-ready .scene-6 .lookbook-label {
  grid-column: 1 / 2; grid-row: 1; justify-self: start; align-self: start; text-align: left; max-width: 24ch;
}
html.pin-ready .tilt-glare { display: block; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; mix-blend-mode: overlay; background: radial-gradient(circle at var(--glare-x, 50%) var(--glare-y, 50%), rgba(255, 255, 255, .55), transparent 45%); }
html.pin-ready .orbit {
  z-index: 4; /* above the rim (2) and media (3) */
  position: absolute; top: 50%; left: 50%; width: 1px; height: 1px; overflow: visible;
  display: block; padding: 0; scroll-snap-type: none;
}
html.pin-ready .orbit-ring { position: absolute; top: 0; left: 0; display: block; gap: 0; touch-action: none; }
html.pin-ready .orbit-item {
  /* .orbit/.orbit-ring collapse to a single point at the mirror's centre (no
     explicit size, no percentage offsets) so this negative margin alone
     centres the 12svh x 16svh tile on that point; mirror.js then only ever
     writes a plain translate(x,y) here — never a rotate — so items stay
     upright by construction instead of needing a counter-rotation transform. */
  position: absolute; top: 0; left: 0; width: 12svh; margin: -8svh 0 0 -6svh;
  will-change: transform;
}
html.pin-ready .orbit-item img { width: 12svh; height: 16svh; }
html.pin-ready .orbit-caption {
  position: absolute; left: 50%; top: 100%; transform: translateX(-50%); white-space: nowrap;
  opacity: 0; transition: opacity .2s var(--ease-standard); background: var(--paper); padding: 2px var(--sp-8);
  border-radius: var(--radius-small); margin-top: var(--sp-4);
}
html.pin-ready .orbit-item:hover .orbit-caption, html.pin-ready .orbit-item:focus-visible .orbit-caption { opacity: 1; }

/* Scene 7: before-frame + path SVG sit to the left of the mirror. Its own
   label stays top-centre (only scene 6's moves to a corner, above, to
   clear that scene's orbit — scene 7 has no orbit to avoid). */
html.pin-ready .scene-7 .lookbook-label { grid-column: 1 / -1; justify-self: center; align-self: start; margin-top: var(--sp-48); }
html.pin-ready .frame-916 { grid-column: 1 / 2; justify-self: end; width: min(62svh, 26vw); aspect-ratio: 9 / 16; align-self: center; }
html.pin-ready .frame-916 img { height: 100%; object-fit: cover; }
html.pin-ready .brush-paths {
  pointer-events: none; /* decorative (aria-hidden); the painted stroke would otherwise be hit-testable */
  position: absolute; top: 50%; left: 50%; transform: translate(-72%, -50%);
  width: min(30vw, 420px); height: 40vh; max-width: none; z-index: 1; overflow: visible;
}
html.pin-ready .frame-caption { position: absolute; left: var(--sp-12); bottom: var(--sp-12); }

/* Scene 8: bloom fills the viewport; CTA + copy sit centred inside it.
   particle-field stays position:absolute (never fixed) — .scene-8 already
   spans the full pinned viewport via inset:0, and a fixed descendant
   inside the pinned stage is exactly the bug class the brief calls out. */
html.pin-ready .scene-media.media-8 {
  top: 0; left: 0; width: 100%; height: 100%; transform: none; border-radius: 0; max-width: none; aspect-ratio: auto;
  clip-path: circle(var(--bloom-r, 0px) at 50% 50%);
  background: radial-gradient(circle at 50% 44%, var(--paper) 0%, var(--porcelain) 72%);
}
html.pin-ready .particle-field { position: absolute; inset: 0; }

/* ---------- Scene 1's one cinematic beat (#154) ----------
   Gated on html:not(.motion-off) rather than the raw media query, since
   that class already tracks both the OS preference AND the in-page
   toggle (see mirror.js applyMotionOffClass()) — checking it alone covers
   both without duplicating the condition. Mirror width/height/radius are
   NOT put on a CSS transition here: mirror.js already writes a fresh,
   eased value on every animation frame via GSAP's onUpdate, so a second
   CSS transition on top of those per-frame writes would just re-smooth
   already-smoothed values and make every morph feel laggy. */
html:not(.motion-off) .scene.is-in .scene-hero-title .word { animation: word-spacing-reveal .8s var(--ease-iris) both; }
html:not(.motion-off) .scene.is-in .scene-hero-title .word:nth-child(2) { animation-delay: .12s; }
html:not(.motion-off) .scene.is-in .scene-hero-title .word:nth-child(3) { animation-delay: .24s; }
@keyframes word-spacing-reveal {
  from { letter-spacing: -.5em; opacity: 0; filter: blur(8px); }
  to   { letter-spacing: .04em; opacity: 1; filter: blur(0); }
}

/* =====================================================================
   Reduced motion / motion off: identical stacked document, no filters
   animating, orbit laid flat, dialogs fade instead of iris.
   ===================================================================== */
/* Off on purpose: motion is always on, whatever the device's reduced-motion setting (owner's call, 2026-09-23). */
@media not all {
  * { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; }
  .preloader { display: none !important; }
  .ring-marquee, .tilt-glare { display: none !important; }
}
/* !important: html.js:not(.pin-ready) .scene (three class-likes) outranks
   this by specificity otherwise, which would leave any scene the visitor
   hasn't scrolled to yet (no .is-in) stuck at opacity:0 even with motion
   explicitly turned off — exactly the "content missing at rest" this
   toggle exists to prevent. */
html.motion-off .scene { opacity: 1 !important; transform: none !important; transition: none !important; }
html.motion-off .ripple-badge span,
html.motion-off .orb-blob,
html.motion-off .shimmer-btn { animation: none; }
html.motion-off dialog { transition: opacity .15s var(--ease-standard); clip-path: none !important; }
html.motion-off dialog[data-state="opening"], html.motion-off dialog[data-state="closing"] { opacity: 0; clip-path: none !important; }

/* =====================================================================
   Phones and tablets held upright (html.pin-narrow, set by mirror.js next
   to pin-ready): the same pinned mirror story as the desktop, arranged top
   to bottom. The mirror moves as well as reshapes (--mirror-cx/--mirror-cy),
   and every scene's words and pieces sit at tops mirror.js measures from
   the real rendered copy (--copy-top and friends). Later in the file than
   the desktop rules on purpose, so equal-specificity overrides win.
   ===================================================================== */
html.pin-ready.pin-narrow .chrome-header { flex-wrap: nowrap; padding: var(--sp-16); }
html.pin-ready.pin-narrow .gooey-nav { display: none; }
html.pin-ready.pin-narrow .scene-dots {
  display: flex; flex-direction: column; gap: 10px; position: fixed; right: 5px; top: 50%;
  transform: translateY(-50%); z-index: 200;
}
html.pin-ready.pin-narrow .scene-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--chrome-solid);
  transition: transform .3s var(--ease-standard), background .3s var(--ease-standard);
}
html.pin-ready.pin-narrow .scene-dot.is-active { transform: scale(1.6); background: var(--lip); }
html.pin-ready.pin-narrow .scene { display: block; padding: 0; }
html.pin-ready.pin-narrow .mirror-rim,
html.pin-ready.pin-narrow .scene-media:not(.media-8) { top: var(--mirror-cy); left: var(--mirror-cx); }
html.pin-ready.pin-narrow .scene-copy {
  position: absolute; top: var(--copy-top); left: var(--sp-24); right: var(--sp-24);
  max-width: none; margin: 0; gap: var(--sp-12); align-items: center; text-align: center;
}
html.pin-ready.pin-narrow .scene h2 { font-size: clamp(26px, 7.4vw, 40px); line-height: 1.1; max-width: none; }
html.pin-ready.pin-narrow .scene p:not(.scene-eyebrow):not(.lookbook-label) { font-size: 15px; line-height: 1.5; max-width: 44ch; }

/* Scene 1: "See yourself," above the oval film, "again." below it. */
html.pin-ready.pin-narrow .scene-1 .scene-eyebrow,
html.pin-ready.pin-narrow .scene-1 p.scene-1-copy,
html.pin-ready.pin-narrow .ring-marquee { display: none; }
html.pin-ready.pin-narrow .h1-half {
  left: 0; right: 0; top: var(--t1-top); max-width: none; transform: none; text-align: center;
  font-size: clamp(32px, 9vw, 48px); line-height: 1.05;
}
html.pin-ready.pin-narrow .h1-half-right { top: var(--t2-top); }
html.pin-ready.pin-narrow .play-badge { top: var(--badge-top); }
html.pin-ready.pin-narrow .scene-1 p.scene-hint { top: var(--hint-top); }

/* Scene 3: the two circles split sideways from the mirror's own spot. */
html.pin-ready.pin-narrow .split-goo { top: var(--mirror-cy); left: var(--mirror-cx); }
html.pin-ready.pin-narrow .split-circle {
  width: var(--split-d); height: var(--split-d);
  margin-left: calc(var(--split-d) / -2); margin-top: calc(var(--split-d) / -2);
}

/* Scene 4: the small still chip has no room beside a phone-width film. */
html.pin-ready.pin-narrow .still-chip { display: none; }

/* Scene 5: both before/after squares whole, side by side, under the mirror. */
html.pin-ready.pin-narrow .side-square { position: absolute; top: var(--sq-top); width: var(--sq); max-width: none; margin: 0; }
html.pin-ready.pin-narrow .side-square-left { left: var(--sp-16); }
html.pin-ready.pin-narrow .side-square-right { right: var(--sp-16); }
html.pin-ready.pin-narrow .side-square figcaption { font-size: 10px; line-height: 1.3; gap: 0; margin-top: var(--sp-8); text-align: center; }
html.pin-ready.pin-narrow .side-square figcaption a { min-height: 36px; justify-content: center; }

/* Scenes 6 and 7: the label across the top. */
html.pin-ready.pin-narrow .lookbook-label {
  position: absolute; top: var(--label-top); left: var(--sp-24); right: var(--sp-24);
  max-width: none; margin: 0; text-align: center;
}

/* Scene 6: the orbit circles the mirror wherever it rests; a vertical swipe
   that starts on a ring still scrolls the page. */
html.pin-ready.pin-narrow .orbit { top: var(--mirror-cy); left: var(--mirror-cx); }
html.pin-ready.pin-narrow .orbit-ring { touch-action: pan-y; }
html.pin-ready.pin-narrow .orbit-item { width: var(--tile-w); margin: calc(var(--tile-h) / -2) 0 0 calc(var(--tile-w) / -2); }
html.pin-ready.pin-narrow .orbit-item img { width: var(--tile-w); height: var(--tile-h); }

/* Scene 7: the before-frame on the left, the brush strokes towards the mirror. */
html.pin-ready.pin-narrow .frame-916 { position: absolute; left: var(--sp-16); top: var(--bf-top); width: var(--bf-w); max-width: none; margin: 0; }
html.pin-ready.pin-narrow .brush-paths {
  top: var(--brush-cy); left: var(--brush-left); width: var(--brush-w); height: var(--brush-h);
  transform: translateY(-50%);
}

/* Inside a round mirror a corner label is cut off by the circle, so on the
   pinned stage (both layouts) BEFORE/AFTER sit centred near the bottom. */
html.pin-ready .split-caption,
html.pin-ready .media-7 .frame-caption { left: 50%; bottom: 9%; transform: translateX(-50%); white-space: nowrap; }
