/* =========================================================================
   Beauty by Kocer — The Depth Edit
   A 3D fly-through: eight layers sit at increasing depth inside a pinned
   perspective world; scrolling dollies a camera through them. Default (no
   JS, reduced motion, or narrow viewport) renders the same eight sections
   as a plain stacked document — the 3D rig is progressive enhancement only,
   applied via html.depth-live once depth.js confirms it can run.
   ========================================================================= */

/* ---------- Tokens ---------- */
:root {
  /* Colour: opal is the only accent, always via gradient text-fill, a rim,
     the dock highlight or the CTA — never a background fill. */
  --ink: #0A0A0C;
  --ink-2: #141419;
  --white: #F7F7F9;
  --white-muted: #A7A7B3;
  /* The opal gradient's three stops, named once so the conic-gradient glow
     button reuses the exact same source colours instead of re-spelling
     the same three hex values in a different rule. */
  --opal-mint: #B8F3E6;
  --opal-lilac: #CDB9FF;
  --opal-peach: #FFC7A8;
  --opal: linear-gradient(120deg, var(--opal-mint) 0%, var(--opal-lilac) 40%, var(--opal-peach) 75%, var(--opal-mint) 100%);
  --opal-glow: rgba(205, 185, 255, .35);
  --rule: rgba(247, 247, 249, .14);
  --focus: var(--opal-lilac);
  --ink-glass: rgba(20, 20, 25, .65);
  --ink-scrim: rgba(10, 10, 12, .6);
  --ink-backdrop: rgba(5, 5, 6, .7);

  /* Type scale, px, per brief. */
  --fs-2xs: 11px;
  --fs-xs: 14px;
  --fs-sm: 16px;
  --fs-md: 18px;
  --fs-lg: 24px;
  --fs-xl: 32px;
  --fs-2xl: 48px;
  --fs-3xl: 88px;
  /* Mobile/base value: viewport-width-led (not height-led like the desktop
     override further down) so five wide "Anybody" caps can never outrun a
     narrow phone's width — confirmed live (getBBox vs .letters' own box)
     that even at wdth 110 this needs to stay under ~23vw to keep the glyph
     shape within .letters-video's own box, or the outer edges of K/R clip
     against the video element's edge with nothing to reveal past it.
     >=1024px is overridden further down for the owner's "artist film shown
     BIG, >=60svh" demand. */
  --fs-kocer: min(20vw, 96px);

  --font-display: 'Anybody', sans-serif;
  --font-body: 'Onest', sans-serif;

  /* Spacing, 4px base. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;
  --sp-9: 96px;
  --sp-10: 128px;

  --radius-frame: 20px;
  --radius-pill: 999px;

  /* Named eases: every animation in this file references one of these
     rather than a bare "ease"/"linear" (linear is reserved for scroll-bound
     scrub values, per the motion rule). */
  --ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-warp: var(--ease-standard); /* byte-identical; aliased rather than duplicated */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-gauge: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---------- Reset & base ---------- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--ink);
  color: var(--white);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  line-height: 1.5;
  /* overflow-x must stay default (visible/clip only via layout, never
     hidden) so a mis-measured child cannot silently eat horizontal scroll
     the reviewer needs to see. */
}
h1, h2, h3, p, ul, figure { margin: 0; }
img { display: block; max-width: 100%; height: auto; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
/* main is a JS-focused, not a JS-clicked, target (the skip link moves
   focus there) — the visible ring belongs on whatever's actually inside,
   not on main's own (browser-dependent) programmatic-focus outline. */
main:focus { outline: none; }
h1, h2 { text-wrap: balance; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}
.skip-link {
  position: absolute;
  left: var(--sp-3);
  top: -100px;
  z-index: 60;
  padding: var(--sp-2) var(--sp-4);
  background: var(--ink-2);
  color: var(--white);
  border-radius: var(--radius-pill);
  transition: top .2s var(--ease-standard);
}
.skip-link:focus { top: var(--sp-3); }

.eyebrow {
  font: 500 var(--fs-2xs)/1.4 var(--font-body);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--white-muted);
  margin-bottom: var(--sp-2);
}

.text-link {
  display: inline-flex;
  gap: .35em;
  font: 500 var(--fs-sm)/1.4 var(--font-body);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition: border-color .2s var(--ease-standard);
}
.text-link:hover, .text-link:focus-visible { border-color: var(--white); }

/* Buttons/anchors styled as "play" pills — shared by the artist film and
   transformation triggers (real <a> elements: they navigate to Instagram
   with JS off, and are progressively intercepted to open a dialog). */
.play-film {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font: 500 var(--fs-sm)/1 var(--font-body);
  min-height: 44px;
  transition: transform .2s var(--ease-standard), border-color .2s var(--ease-standard);
}
.play-film:hover, .play-film:focus-visible { border-color: var(--white); transform: scale(1.03); }

/* =========================================================================
   Chrome — motes canvas, preloader, header, dock
   All fixed-position: they live on body, never inside .world/.camera, so
   they can never be captured by that subtree's 3D transform.
   ========================================================================= */
.motes-canvas {
  position: fixed;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.preloader {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  transition: opacity .5s var(--ease-gauge), transform .5s var(--ease-gauge);
}
.preloader[hidden] { display: none; }
/* Without pointer-events:none the page swallowed clicks through a fully
   transparent overlay for the whole 500ms fade (it's still `hidden` only
   at 520ms, per depth.js) — inert (set in JS while it's showing) covers
   the equivalent problem for Tab. */
.preloader.is-done { opacity: 0; transform: scale(1.4); pointer-events: none; }
/* No JS means no progress events and no completion handler ever runs, so
   without this the gauge would sit at 0% forever, covering the page. */
html:not(.js) .preloader { display: none; }
.preloader-ring { width: 120px; height: 120px; transform: rotate(-90deg); }
.ring-track, .ring-fill {
  fill: none;
  stroke-width: 3;
}
.ring-track { stroke: var(--rule); }
.ring-fill {
  stroke: var(--focus);
  stroke-linecap: round;
  stroke-dasharray: 326.7;
  stroke-dashoffset: calc(326.7 * (1 - var(--progress, 0)));
}
.preloader-k {
  position: absolute;
  font: 900 var(--fs-2xl)/1 var(--font-display);
  font-variation-settings: 'wdth' 90;
  background: var(--opal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.chrome-top {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* env() falls back to the plain length when a device has no notch/inset
     to report, so this is safe on every browser, not just notched ones. */
  padding: max(var(--sp-4), env(safe-area-inset-top)) max(var(--sp-5), env(safe-area-inset-right)) var(--sp-4) max(var(--sp-5), env(safe-area-inset-left));
}
.wordmark {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 44px; /* touch target: the two stacked lines alone measured 42px */
  font: 700 var(--fs-lg)/1 var(--font-display);
  font-variation-settings: 'wdth' 90;
  text-transform: lowercase;
}
.wordmark span {
  order: -1;
  font: 500 var(--fs-2xs)/1.6 var(--font-body);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--white-muted);
}
#motion-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-4);
  min-height: 44px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font: 500 var(--fs-xs)/1 var(--font-body);
}
#motion-toggle span { color: var(--focus); }
#motion-toggle[aria-pressed="true"] span { color: var(--white-muted); }

.dock {
  position: fixed;
  left: 50%;
  bottom: var(--sp-5);
  z-index: 20;
  transform: translateX(-50%);
  display: none; /* shown only at >=1024px, see media query below */
  align-items: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border-radius: var(--radius-pill);
  background: var(--ink-glass);
  border: 1px solid var(--rule);
  backdrop-filter: blur(20px);
}
/* Below 1024px .dock is already display:none via the media query further
   down; this specifically covers scripting-disabled at >=1024px, where
   every one of the dock's eight buttons would otherwise do nothing on
   click (no JS to wire jumpToLayer) while still being announced as a
   labelled navigation landmark. */
html:not(.js) .dock { display: none; }
.dock-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--sp-1);
  border-radius: var(--radius-pill);
  transform: scale(var(--mag, 1)) translateY(calc((var(--mag, 1) - 1) * -10px));
  transition: transform .22s var(--ease-spring);
}
.dock-thumb {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid var(--rule);
}
.dock-thumb img { width: 100%; height: 100%; object-fit: cover; }
.dock-thumb-letter {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 24px: the design system's own floor for an opal background-clip:text
     fill (a gradient fill has no single measurable contrast ratio, which
     is exactly why the floor exists) — 18px was below it. Still comfortably
     inside the 44px circle. */
  font: 700 var(--fs-lg)/1 var(--font-display);
  font-variation-settings: 'wdth' 90;
  background: var(--opal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.dock-item[aria-current="true"] .dock-thumb { border-color: var(--focus); }
.dock-label {
  position: absolute;
  bottom: calc(100% + var(--sp-2));
  left: 50%;
  transform: translateX(-50%);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--ink-2);
  font: 500 var(--fs-2xs)/1.6 var(--font-body);
  letter-spacing: .1em;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .2s var(--ease-standard);
}
.dock-item:hover .dock-label, .dock-item:focus-visible .dock-label { opacity: 1; }

/* Below 1024px the fly-through never runs (see "world" rules further down),
   so the camera-jump dock has nothing to jump to; it stays hidden and the
   page is a plain scroll. */
@media (min-width: 1024px) {
  .dock { display: flex; }
}

/* =========================================================================
   World / camera / layers
   Default state = a fully accessible stacked document (no JS, reduced
   motion, or <1024px). html.depth-live (set only once depth.js confirms
   GSAP + ScrollTrigger + a wide screen + motion allowed) switches on the
   pinned 3D rig.
   ========================================================================= */
.world { display: contents; }
.camera { display: contents; }

.layer {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-5);
  padding: var(--sp-9) var(--sp-5);
  border-bottom: 1px solid var(--rule);
}
.layer:last-child { border-bottom: 0; }
/* The stacked document (phones, no JS, reduced motion) clips each layer's
   sideways overflow. The film frame's running light moves by translating a
   frame-sized box by 100% of its width, which ran 260px past a phone's edge,
   widened the page and pushed the header's Motion toggle off-screen. In the
   pinned world the .world box already clips it. */
html:not(.depth-live) .layer { overflow-x: clip; }

html.depth-live .world {
  /* display:contents above makes position/height/perspective inert until
     this switches it back to a real box — GSAP needs an actual rendered
     element to measure and pin, not one that generates no box at all. */
  display: block;
  position: relative;
  height: 100svh;
  overflow: hidden;
  perspective: 1200px;
  perspective-origin: 50% 50%;
}
html.depth-live .camera {
  display: block;
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}
html.depth-live .layer {
  position: absolute;
  inset: 0;
  display: grid;
  /* Explicit (grid tracks are always minmax(0,1fr) — see the project
     rule): with no grid-template-columns, the implicit column sizes via
     "auto"/fit-content of its item's own content, which measured live as
     a fixed 377px track (fit to the layer's *other*, non-percentage-width
     children) rather than the layer's full width — every percentage width
     further down (.kocer-stage, .letters) was silently resolving against
     that undersized track instead of the intended near-full-width box. */
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  min-height: 0;
  border-bottom: 0;
  padding: var(--sp-6);
  /* Deliberately flat, not preserve-3d: each layer's *own* translateZ
     already comes from .camera's preserve-3d, and every nested effect
     (pin tilt, vanity lid, sphere) opens its own local perspective further
     down anyway. Giving eight siblings their own preserve-3d on top of
     that made cross-layer paint order unreliable — flat lets the browser's
     ordinary (and reliable) "flat planes at different Z" sort handle it. */
  transform-style: flat;
  pointer-events: none;
  /* Blur is the one documented per-layer exception: a class-based step
     (three fixed values), never an animated numeric blur. Transform and
     opacity are written per scroll frame by depth.js instead. */
  transition: filter .3s var(--ease-standard);
}
html.depth-live .layer.depth-active { filter: none; }
html.depth-live .layer.depth-near { filter: blur(4px); }
html.depth-live .layer.depth-far { filter: blur(6px); }
/* Only the nearest-to-camera layer accepts pointer/keyboard focus — see
   setActiveLayer() in depth.js, which also sets [inert] on the rest. */
html.depth-live .layer.is-active-layer { pointer-events: auto; }

/* Everything below this line composes each layer's own content; every rule
   works unchanged whether the layer is a plain stacked <section> or an
   absolutely-positioned, perspective-scaled camera target. */

/* ---------- Layer 1 — KOCER ---------- */
.kocer-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
  text-align: center;
  max-width: 1500px;
  /* Without this, .letters' width:100% had nothing definite to resolve
     against (place-items:center doesn't stretch grid items, and this flex
     column doesn't stretch centered children either) — the browser fell
     back to sizing .letters near-zero and .kocer-stage shrank to whatever
     its other (text) children needed, measured live at 377px instead of
     the intended near-full-width box. Every sibling *-stage container
     already carries this same width:100%. */
  width: 100%;
}
.kocer-eyebrow {
  font: 500 var(--fs-2xs)/1.4 var(--font-body);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--white-muted);
}
.letters {
  position: relative;
  /* 100% of .kocer-stage's own (already-gutter'd, via the layer's own
     padding) width, not a raw vw guess — vw ignored the layer's padding
     and left less room than intended on narrow phones. */
  width: 100%;
  max-width: 2400px;
  height: calc(var(--fs-kocer) * 1.15);
}
.letters-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  clip-path: url(#kocer-clip);
}
.letters-clip { position: absolute; width: 0; height: 0; }
.kocer-clip-text {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--fs-kocer);
  font-variation-settings: 'wdth' var(--kocer-wdth, 150);
  text-transform: uppercase;
}
/* Below 1024px the pinned world (and its wdth scrub) never runs, so the
   clip text would otherwise sit at its widest (150) default — narrowing
   it here keeps "KOCER" from running past a narrow phone's edge. Also
   applied to the no-JS fallback word below, for the same reason. */
@media (max-width: 1023px) {
  .kocer-clip-text {
    font-variation-settings: 'wdth' 110;
  }
}
/* No-JS fallback: the clip-path letters need JS (layoutKocerClip in
   depth.js) to position the SVG clip text in pixels, so without it they
   clip nothing visible. html:not(.js) swaps in a plain opal-filled "KOCER"
   above the poster in a normal frame instead — same content, no clip. */
.letters-fallback { display: none; }
html:not(.js) .letters { display: none; }
html:not(.js) .letters-fallback {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
}
.letters-fallback-word {
  /* Independent of --fs-kocer (that token is height-led on desktop, sized
     for the SVG clip which simply reveals nothing past its own box — this
     is plain text with no such containment, so it needs its own width-led,
     always-narrow-wdth formula that's safe at any viewport width, not just
     the two breakpoints --fs-kocer is tuned for). */
  font-family: var(--font-display);
  font-weight: 900;
  font-size: min(16vw, 180px);
  font-variation-settings: 'wdth' 110;
  line-height: 1;
  max-width: 100%;
  text-align: center;
  text-transform: uppercase;
  background: var(--opal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.letters-fallback-frame {
  width: min(70vw, 340px);
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-frame);
  overflow: hidden;
  box-shadow: 0 60px 160px -60px var(--opal-glow);
}
.letters-fallback-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.kocer-sub { color: var(--white-muted); font-size: var(--fs-md); }
.kocer-hint {
  font: 500 var(--fs-2xs)/1.4 var(--font-body);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--white-muted);
}
.media-error {
  font-size: var(--fs-xs);
  color: var(--white-muted);
  text-align: center;
}
.media-error[hidden] { display: none; }
.media-error a { text-decoration: underline; color: var(--white); }

/* ---------- Layer 2 — At work (#116 3D pin & floating perspective) ---------- */
.work-stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-7);
  max-width: 1200px;
  width: 100%;
  align-items: center;
}
.pin-card {
  position: relative;
  perspective: 1000px;
  justify-self: center;
  /* Default (stacked/mobile): a simple, safe width-led size — the >=62svh
     "BIG" floor is a desktop-only requirement (see html.depth-live below).
     Height-leading this unconditionally on a narrow, TALL mobile viewport
     (>=62svh of e.g. an 844px-tall phone is ~523px) pushed the 3:4-derived
     width past the viewport by a couple of px, which was enough to trip
     the same mobile-viewport-widening heuristic .vanity did more severely. */
  width: min(100%, 360px);
  aspect-ratio: 3 / 4;
}
html.depth-live .pin-card {
  /* Sized explicitly, matching .pin-plate exactly — leaving this wrapper
     to size from content (its default) measured wrong once nested this
     deep in the camera's own transformed/3D stack, the same way the
     aspect-ratio approach did on .pin-plate itself; see the note there. */
  --frame-h: max(62svh, 320px);
  height: var(--frame-h);
  width: calc(var(--frame-h) * 3 / 4);
  aspect-ratio: auto;
}
.pin-needle {
  position: absolute;
  left: 50%;
  top: -28px;
  width: 2px;
  height: 28px;
  background: var(--opal);
  transform: translateX(-50%);
}
.pin-needle::before {
  content: '';
  position: absolute;
  top: -5px;
  left: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--opal);
  transform: translateX(-50%);
  box-shadow: 0 0 20px 4px var(--opal-glow);
}
.pin-plate {
  /* Fills .pin-card, which now carries the actual sizing — see the note
     there. The tilt rotates alone: .pin-card's own `perspective`
     (property, on the parent) already supplies the 3D context, so a
     *second* perspective() transform function here was redundant and,
     empirically nested this deep in the camera's own 3D stack, part of
     what threw the size off before. */
  height: 100%;
  width: 100%;
  border-radius: var(--radius-frame);
  overflow: hidden;
  transform: rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  transition: transform .3s var(--ease-standard);
  box-shadow: 0 60px 160px -60px var(--opal-glow);
}
.pin-plate img { width: 100%; height: 100%; object-fit: cover; }
.work-copy h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-3); }
.work-copy p { color: var(--white-muted); margin-bottom: var(--sp-4); max-width: 46ch; }

/* ---------- Layer 3 — Before/after (#119 lid opening) ---------- */
.vanity-stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-7);
  max-width: 1200px;
  width: 100%;
  align-items: center;
}
.vanity-copy h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-3); }
.vanity-copy p { color: var(--white-muted); margin-bottom: var(--sp-4); max-width: 46ch; }
/* Default: "a plain before/after pair" (mobile/motion-off, per brief) —
   two normal, flat, fully-visible images stacked in flow. No rotation, no
   perspective: those belong to the depth-live 3D card only, further down.
   Giving the rotated lid its -75deg tilt unconditionally used to leave it
   permanently near-closed outside the pinned world, and the resulting
   perspective projection of that steep angle also measured wider than the
   viewport, which is exactly the kind of overflow that made mobile
   viewport emulation quietly widen the whole page past its real 390/768px. */
/* .vanity-wrap is the actual grid item now (figure + disclosure button
   stacked); .vanity itself dropped justify-self accordingly. */
.vanity-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  justify-self: center;
}
.vanity-open {
  padding: var(--sp-2) var(--sp-4);
  min-height: 44px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  font: 500 var(--fs-xs)/1 var(--font-body);
  transition: border-color .2s var(--ease-standard);
}
.vanity-open:hover, .vanity-open:focus-visible { border-color: var(--white); }
.vanity {
  position: relative;
  width: 100%;
  max-width: 340px;
}
.vanity-base, .vanity-lid {
  position: relative;
  border-radius: var(--radius-frame);
  overflow: hidden;
  box-shadow: 0 60px 160px -60px var(--opal-glow);
}
.vanity-lid { margin-top: var(--sp-3); }
.vanity-base img, .vanity-lid img { display: block; width: 100%; height: 100%; object-fit: cover; }
html.depth-live .vanity {
  /* Side by side, not stacked inset:0 — stacking the lid directly over the
     base meant only whichever wasn't rotated away was ever visible at any
     one rotation angle, so the pair was never seen together in the pinned
     world (confirmed live: only the AFTER lid showed at rest). Laid out
     side by side, the lid's rotateX is purely a decorative flourish (it
     shrinks toward a sliver as it "closes") that never covers the base,
     so both stills are always fully visible regardless of --lid-angle. */
  display: flex;
  gap: var(--sp-4);
  --frame-h: max(56svh, 300px);
  height: var(--frame-h);
  width: calc((var(--frame-h) * 3 / 4) * 2 + var(--sp-4));
  max-width: 100%;
  perspective: 1400px;
}
html.depth-live .vanity-base, html.depth-live .vanity-lid {
  position: relative;
  flex: 0 0 calc(var(--frame-h) * 3 / 4);
  height: 100%;
  margin-top: 0;
}
html.depth-live .vanity-lid {
  transform-origin: 50% 100%;
  transform: rotateX(var(--lid-angle, -75deg));
}
.vanity-tag {
  position: absolute;
  left: var(--sp-3);
  bottom: var(--sp-3);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--ink-scrim);
  font: 500 var(--fs-2xs)/1.4 var(--font-body);
  letter-spacing: .1em;
}

/* ---------- Layer 4 — The film (#132 border trail) ---------- */
.film-stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-7);
  max-width: 1300px;
  width: 100%;
  align-items: center;
}
.film-copy h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-3); }
.film-copy p { color: var(--white-muted); margin-bottom: var(--sp-4); max-width: 46ch; }
.film-still { width: 140px; margin-top: var(--sp-5); }
.film-still img { border-radius: var(--radius-frame); aspect-ratio: 3 / 4; object-fit: cover; width: 100%; height: 100%; }
.film-still figcaption { margin-top: var(--sp-2); font: 500 var(--fs-2xs)/1.4 var(--font-body); letter-spacing: .1em; color: var(--white-muted); }
.film-frame {
  /* Default = stacked/mobile: width-led, 3:4 via aspect-ratio, never
     cropped. The 72svh height-led sizing below is a desktop-world-only
     requirement — applying it unconditionally left the stacked column
     narrower than frame-h*0.75, so max-width:100% won it on width while
     height stayed at 72svh, cropping ~25% of the frame under object-fit:
     cover (measured live: 336x585 at 390px, ratio 0.57 not 0.75). */
  position: relative;
  justify-self: center;
  width: min(100%, 420px);
  aspect-ratio: 3 / 4;
  /* Grid items get an automatic min-height:auto (their own content's
     min-content size) unless overridden — measured live as a real 7px
     excess over the aspect-ratio-derived height (463px rendered vs the
     456px the ratio itself implies at 342px wide), same underlying class
     of bug as .seq-strip's min-width:0 fix, just the block-axis version. */
  min-height: 0;
  border-radius: var(--radius-frame);
  padding: 3px;
}
html.depth-live .film-frame {
  /* Explicit height via calc(), same reasoning as .pin-plate above. 72svh
     (not just the >=70svh floor) so it reads comfortably above the floor
     at both 1440x900 and 1366x768, not right on the edge of it. */
  --frame-h: max(72svh, 390px);
  height: var(--frame-h);
  width: calc(var(--frame-h) * 3 / 4);
  aspect-ratio: auto;
  max-width: 100%;
}
.film-frame video { width: 100%; height: 100%; }
.film-frame video { object-fit: cover; border-radius: calc(var(--radius-frame) - 3px); background: var(--ink-2); }
.film-frame .media-error {
  /* Overlays the dead frame instead of stacking below it in normal flow —
     .film-frame has a fixed height and html.depth-live .world is
     overflow:hidden, so a media-error in normal flow rendered a full
     frame-height below the visible frame and was clipped out of sight. */
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}
.film-frame .media-error[hidden] { display: none; }
.border-trail {
  position: absolute;
  inset: 0;
  pointer-events: none;
  animation: border-trail 5s linear infinite;
  animation-play-state: paused;
}
.border-trail.is-running { animation-play-state: running; }
.border-trail::before {
  content: '';
  position: absolute;
  top: -5px;
  left: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--opal);
  box-shadow: 0 0 18px 4px var(--opal-glow);
}
/* Percentages sized for the frame's fixed 3:4 ratio, so each keyframe stop
   lands on an actual corner instead of drifting off the edge. */
@keyframes border-trail {
  0%   { transform: translate(0%, 0%); }
  21%  { transform: translate(100%, 0%); }
  50%  { transform: translate(100%, 100%); }
  71%  { transform: translate(0%, 100%); }
  100% { transform: translate(0%, 0%); }
}

/* ---------- Layer 5 — Two faces (#117 grid spotlight) ---------- */
.faces-stage { display: flex; flex-direction: column; align-items: center; max-width: 1100px; width: 100%; }
.faces-stage h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-6); text-align: center; }
.faces-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6);
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-frame);
}
.spotlight {
  position: absolute;
  top: 0;
  left: 0;
  width: 40px;
  height: 40px;
  margin: -20px;
  border-radius: 50%;
  background: var(--opal);
  opacity: 0;
  filter: blur(50px);
  pointer-events: none;
  transition: transform .2s var(--ease-standard), opacity .2s var(--ease-standard);
}
/* The JS half (pointermove) is already gated to (hover:hover) and
   (pointer:fine); this CSS half wasn't — on a touch device a latched
   :hover showed the blurred opal blob parked at the grid's default
   top-left corner, since transform is only ever written by that gated
   pointermove. */
@media (hover: hover) and (pointer: fine) {
  .faces-grid:hover .spotlight { opacity: .5; }
}
.faces-grid:focus-within .spotlight { opacity: .5; }
.face-card {
  position: relative;
  /* Square, capped at the brief's <=60svh — min() lets it shrink with its
     grid column on narrow viewports instead of overflowing. Sized on the
     <figure>, not the <button>, since a form control's own UA sizing
     rules measured unreliably against a percentage width in testing. */
  width: min(100%, 60svh);
  margin-inline: auto;
}
.face-trigger {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-frame);
  overflow: hidden;
}
.face-trigger img { width: 100%; height: 100%; object-fit: cover; }
.face-card figcaption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: var(--sp-3);
  font: 500 var(--fs-2xs)/1.4 var(--font-body);
  letter-spacing: .1em;
  color: var(--white-muted);
}
.face-card figcaption a { text-decoration: underline; }
/* Every standalone link below 768px, at 44x44 — these aren't inline in a
   sentence, so WCAG 2.5.8's inline exception doesn't cover them, and each
   measured under the floor at its own font-size/padding. */
@media (max-width: 767px) {
  .text-link, .face-card figcaption a, .sphere-grid-fallback figcaption a, .site-footer a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ---------- Layer 6 — Lookbook sphere (#148) ---------- */
.sphere-stage-wrap { display: flex; flex-direction: column; align-items: center; max-width: 1300px; width: 100%; }
.sphere-stage-wrap h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-6); text-align: center; }
.sphere-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-7); width: 100%; align-items: center; justify-items: center; }
.sphere-stage { display: none; width: min(80vw, 60svh); height: min(80vw, 60svh); perspective: 1400px; }
.sphere {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: rotateX(var(--sphere-rx, 0deg)) rotateY(var(--sphere-ry, 0deg));
  /* pan-y (not none): the pinned world only requires >=1024px, not a fine
     pointer, so a touch-capable tablet at that width still needs a
     vertical swipe that starts on the sphere to reach the scroller — the
     only way to leave this layer besides the dock/arrow keys. Horizontal
     drag still spins the sphere. */
  touch-action: pan-y;
}
.tile {
  position: absolute;
  top: 50%;
  left: 50%;
  /* 12svh tall per brief; width derives from the 3:4 ratio (9svh), and the
     margin is exactly half of each so the tile centres on its anchor. */
  height: 12svh;
  width: 9svh;
  aspect-ratio: 3 / 4;
  margin: -6svh 0 0 -4.5svh;
  border-radius: var(--sp-2);
  overflow: hidden;
  transform: translate3d(var(--tx, 0), var(--ty, 0), var(--tz, 0));
  transition: opacity .3s var(--ease-standard);
}
.tile img { width: 100%; height: 100%; object-fit: cover; }
.sphere-grid-fallback {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
  width: 100%;
  max-width: 460px;
}
.sphere-grid-fallback img { border-radius: var(--radius-frame); aspect-ratio: 3 / 4; object-fit: cover; width: 100%; height: 100%; }
.sphere-grid-fallback figcaption { margin-top: var(--sp-2); font-size: var(--fs-2xs); letter-spacing: .1em; }
.sphere-grid-fallback figcaption a { text-decoration: underline; }
html:not(.depth-live) .sphere-links { display: none; }
/* A visually-hidden link still has to show where focus landed (WCAG
   2.4.7) — under html.depth-live this sr-only list is the *only* tabbable
   route to the four lookbook posts (the 16 tiles are tabindex="-1"), so
   without this a keyboard user tabbing through layer 6 got four focus
   stops on a 1x1px clipped box with the focus ring drawn around nothing. */
.sphere-links:focus-within {
  position: static;
  width: auto;
  height: auto;
  clip: auto;
  display: flex;
  gap: var(--sp-3);
  list-style: none;
  padding: 0;
  font-size: var(--fs-2xs);
}
.sphere-links:focus-within a { text-decoration: underline; }
.mirror-plate { width: min(60vw, 220px); }
.mirror-plate img { border-radius: var(--radius-frame); aspect-ratio: 3 / 4; object-fit: cover; width: 100%; height: 100%; }
.mirror-plate figcaption { margin-top: var(--sp-2); font: 500 var(--fs-2xs)/1.4 var(--font-body); letter-spacing: .1em; color: var(--white-muted); text-align: center; }

/* ---------- Layer 7 — Transformation (#80 step sequencer) ---------- */
.sequencer-stage { display: flex; flex-direction: column; align-items: center; max-width: 900px; width: 100%; }
.sequencer-stage h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-6); text-align: center; }
.sequencer { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); width: 100%; }
.seq-dynamic { display: none; flex-direction: column; align-items: center; gap: var(--sp-3); width: min(70vw, 340px); }
.seq-frame {
  width: 100%;
  aspect-ratio: 405 / 720;
  min-height: 62svh;
  border-radius: var(--radius-frame);
  background-image: url('assets/transformation-frames.jpg');
  background-size: 600% 400%;
  background-position: 0% 0%;
  box-shadow: 0 60px 160px -60px var(--opal-glow);
}
.seq-readout { font: 500 var(--fs-sm)/1 var(--font-body); letter-spacing: .08em; }
.seq-readout span:first-child { color: var(--white-muted); }
.seq-bar { width: 100%; height: 2px; background: var(--rule); border-radius: var(--radius-pill); overflow: hidden; }
.seq-fill { display: block; width: 100%; height: 100%; background: var(--opal); transform-origin: left; transform: scaleX(var(--seq-progress, 0)); }
.seq-static { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); width: 100%; }
.transformation-video, .seq-static .media-error {
  width: min(70vw, 340px);
  aspect-ratio: 9 / 16;
}
.transformation-video { border-radius: var(--radius-frame); object-fit: cover; background: var(--ink-2); }
.seq-strip {
  display: flex;
  gap: var(--sp-1);
  width: 100%;
  max-width: 640px;
  overflow-x: auto;
  padding-bottom: var(--sp-2);
  /* Flex items get an automatic min-width of their content's max-content
     size (here, 24 non-wrapping tiles) unless overridden — that fought
     overflow-x:auto and, under mobile viewport emulation specifically,
     widened the whole page's layout viewport past the true 100%. */
  min-width: 0;
}
.seq-tile {
  flex: 0 0 auto;
  width: 60px;
  aspect-ratio: 405 / 720;
  border-radius: var(--sp-1);
  background-image: url('assets/transformation-frames.jpg');
  background-size: 600% 400%;
}

/* ---------- Layer 8 — Contact ---------- */
.contact-stage { display: flex; flex-direction: column; align-items: center; text-align: center; max-width: 700px; margin-inline: auto; }
.contact-stage h2 { font: 700 var(--fs-2xl)/1.1 var(--font-display); font-variation-settings: 'wdth' 90; margin-bottom: var(--sp-4); }
/* inline-grid, not inline-block: out-of-flow (absolute) children
   contribute nothing to an inline-block's width, so the box used to size
   to min-width alone (4.5ch) while "moment" (six glyphs) advances further
   than that and got cut by overflow:hidden — "Make it your mome." was the
   actual rendered text. grid-area:1/1 stacks every item in one cell, so
   the box sizes to the widest one instead of an arbitrary ch guess, and
   height comes from the tallest item's own line-height (not a fixed
   1.1em, which also clipped descenders on "glow"/"day"). */
.word-rotate {
  /* Width follows the visible word (--wr-w, set by initWordRotate) so the
     period after it sits flush; unset without JS = auto = the default word.
     overflow stays visible so a wider incoming word is never clipped while
     the width is still growing. */
  display: inline-grid;
  width: var(--wr-w, auto);
  overflow: visible;
  vertical-align: bottom;
  text-align: left;
  transition: width .5s var(--ease-standard);
}
.wr-item {
  grid-area: 1 / 1;
  justify-self: start; /* own width, not the widest word's, so offsetWidth measures it */
  white-space: nowrap;
  background: var(--opal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  opacity: 0;
  transform: translateY(60%) rotateX(80deg);
  transition: transform .5s var(--ease-standard), opacity .5s var(--ease-standard);
}
.wr-item.is-active { opacity: 1; transform: translateY(0) rotateX(0deg); }
.wr-item.is-leaving { opacity: 0; transform: translateY(-60%) rotateX(-80deg); filter: blur(8px); }
.contact-stage p { color: var(--white-muted); margin-bottom: var(--sp-6); max-width: 46ch; }

/* Cosmic galaxy button (#157): rotating conic glow ring + hover sparkle,
   built with transform/opacity only — the ring spins via transform:rotate,
   never an animated background-position. */
.cta-cosmic {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-7);
  min-height: 44px;
  border-radius: var(--radius-pill);
  background: var(--ink-2);
  overflow: hidden;
  isolation: isolate;
  font: 500 var(--fs-sm)/1 var(--font-body);
}
.cta-cosmic::before {
  content: '';
  position: absolute;
  inset: -60%;
  z-index: -1;
  background: conic-gradient(from 0deg, transparent, var(--opal-mint), var(--opal-lilac), var(--opal-peach), transparent);
  opacity: .55;
  animation: cosmic-spin 6s linear infinite;
  animation-play-state: paused;
}
.cta-cosmic::after {
  content: '';
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: var(--radius-pill);
  background: var(--ink-2);
}
.cta-cosmic:hover::before, .cta-cosmic:focus-visible::before { animation-play-state: running; }
.cta-cosmic:hover, .cta-cosmic:focus-visible { transform: scale(1.03); }
.cta-cosmic { transition: transform .2s var(--ease-standard); box-shadow: 0 0 30px var(--opal-glow); }
@keyframes cosmic-spin { to { transform: rotate(360deg); } }

.site-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-9);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
  text-align: center;
  color: var(--white-muted);
  font-size: var(--fs-xs);
}
.site-footer a { text-decoration: underline; gap: .3em; }

/* =========================================================================
   Dialogs — film player (artist/transformation) and the two lightboxes.
   Opening/closing is the "warp": scale + blur + opacity, 450ms.
   ========================================================================= */
/* depth.js toggles this class on open/close (anyDialogOpen() reads it too)
   but nothing styled it — wheel-scrolling behind an open dialog scrubbed
   the pinned ScrollTrigger underneath, flying the camera to a different
   layer while the modal was still up. Matches dist/cherry.css and
   dist/runway.css's own copy of this same rule. */
body.dialog-open { overflow: hidden; }
dialog {
  position: fixed;
  inset: 0;
  margin: auto;
  max-width: min(90vw, 640px);
  max-height: 85dvh;
  overflow-y: auto;
  padding: var(--sp-5);
  border: 1px solid var(--rule);
  border-radius: var(--radius-frame);
  background: var(--ink-2);
  color: var(--white);
  transform: scale(1);
  filter: blur(0);
  opacity: 1;
  transition: transform .45s var(--ease-warp), filter .45s var(--ease-warp), opacity .45s var(--ease-warp);
}
dialog::backdrop { background: var(--ink-backdrop); }
dialog[data-state="opening"], dialog[data-state="closing"] {
  transform: scale(.6);
  filter: blur(12px);
  opacity: 0;
}
.dialog-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); margin-bottom: var(--sp-4); }
.dialog-bar h2 { font: 700 var(--fs-lg)/1.2 var(--font-display); font-variation-settings: 'wdth' 90; }
.dialog-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--rule);
  flex-shrink: 0;
}
/* Default 3:4 (the artist film's ratio, and .film-video's fallback via
   FILM_DATA is the more common trigger) reserves the dialog's own layout
   space before metadata arrives, so opening it doesn't shift its box;
   depth.js overrides it to 9/16 when opening the transformation film. */
.film-video { aspect-ratio: 3 / 4; }
.film-video, .face-dialog-image { width: 100%; height: auto; border-radius: var(--sp-2); background: var(--ink); }
.pair-dialog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
.pair-dialog-grid img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--sp-2); aspect-ratio: 3 / 4; }
.pair-dialog-grid figcaption { margin-top: var(--sp-2); text-align: center; font-size: var(--fs-2xs); letter-spacing: .1em; color: var(--white-muted); }
.pair-dialog a, .face-dialog a { display: inline-block; margin-top: var(--sp-4); text-decoration: underline; font-size: var(--fs-sm); }

/* =========================================================================
   Reduced motion — CSS half of the guard (JS half lives in depth.js).
   Stacked layout is already the default, so this only needs to neutralise
   the handful of always-on CSS animations/transitions that don't depend on
   a JS-added class.
   ========================================================================= */
/* Off on purpose: motion is always on, whatever the device's reduced-motion setting (owner's call, 2026-09-23). */
@media not all {
  .border-trail, .cta-cosmic::before { animation: none; }
  .wr-item, .word-rotate { transition: none; }
  .skip-link, .dock-item, .dock-label, .play-film, .pin-plate, .spotlight,
  .cta-cosmic, dialog { transition: none; }
  .preloader { display: none; }
}

/* =========================================================================
   Responsive
   ========================================================================= */
@media (min-width: 700px) {
  .work-stage, .vanity-stage, .film-stage { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .film-copy { order: 0; }
  .sphere-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}
/* The sphere only replaces its 2x2 grid fallback once the pinned world is
   actually live — motion off (even at desktop widths) must fall back to
   the plain grid, not a static, un-rotating ball of tiles. */
html.depth-live .sphere-stage { display: block; }
html.depth-live .sphere-grid-fallback { display: none; }
html.depth-live .seq-dynamic { display: flex; }
html.depth-live .seq-static { display: none; }

@media (min-width: 1024px) {
  /* Height-led: >=58svh first (the owner's "shown BIG" floor), but never
     wider than 20vw of glyph advance, so five wide caps in Anybody never
     overrun the clip box at narrower desktop widths (e.g. 1366px). */
  :root { --fs-kocer: min(max(58svh, 14vw), 20vw, 460px); }
}

/* Phones and tablets (the stacked document). The fixed header had no
   backdrop, so headings and photos slid under the wordmark, text on text.
   It gets an ink scrim and steps aside while reading down (initHeaderAway). */
@media (max-width: 1023px) {
  .chrome-top {
    background: var(--ink);
  }
  .chrome-top::after {
    content: ''; position: absolute; inset: 100% 0 auto 0; height: 20px; pointer-events: none;
    background: linear-gradient(to bottom, var(--ink), rgba(10, 10, 12, 0));
  }
  @media all { .chrome-top { transition: transform .35s var(--ease-standard); } }
  html.header-away .chrome-top:not(:focus-within) { transform: translateY(-100%); }

  /* The KOCER letters showed the artist film only through a 90px band. The
     film now plays whole in a tall 3:4 frame under the opal KOCER word (the
     same word the no-JS page shows), sized to sit on the first screen at
     over half its height: 215px is the header, eyebrow and word above it. */
  html.js .kocer-layer { justify-content: flex-start; padding-top: calc(var(--sp-8) + var(--sp-4)); }
  html.js .kocer-eyebrow { order: -2; }
  html.js .letters-fallback { display: flex; order: -1; }
  html.js .letters-fallback-frame { display: none; }
  html.js .letters {
    width: min(100%, 460px, calc((100svh - 215px) * .75));
    height: auto;
    aspect-ratio: 3 / 4;
    border-radius: var(--radius-frame);
    overflow: hidden;
    box-shadow: 0 60px 160px -60px var(--opal-glow);
  }
  html.js .letters-video { clip-path: none; }
}
/* Two before/after squares side by side were 143px each on a phone: faces
   too small to read. One per row there. */
@media (max-width: 699px) {
  .faces-grid { grid-template-columns: minmax(0, 1fr); }
}
.face-card figcaption a { gap: .3em; }

/* Landscape phones: a squat viewport must not trap layer content behind
   the fixed header/dock — min-height, not a hard 100svh, on narrow+short. */
@media (max-height: 480px) and (max-width: 900px) {
  .layer { min-height: auto; padding-block: var(--sp-8); }
}

/* =====================================================================
   Phones and tablets held upright (html.depth-narrow, next to depth-live):
   the same camera flight through the eight layers as the desktop. Every
   layer is laid out to fit one screen under the header, one column,
   centred; sizes are svh/vw-led so 360x740 to 768x1024 all fit.
   ===================================================================== */
html.depth-live.depth-narrow .layer { padding: calc(var(--sp-8) + var(--sp-4)) var(--sp-5) var(--sp-5); }
html.depth-live.depth-narrow .layer h2 { font-size: clamp(30px, 8.4vw, 44px); }
html.depth-live.depth-narrow .work-stage,
html.depth-live.depth-narrow .vanity-stage,
html.depth-live.depth-narrow .film-stage { grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); justify-items: center; text-align: center; }
html.depth-live.depth-narrow .work-copy p,
html.depth-live.depth-narrow .vanity-copy p,
html.depth-live.depth-narrow .film-copy p { margin-inline: auto; }
/* 2 · the plate */
html.depth-live.depth-narrow .pin-card { --frame-h: min(40svh, 340px); }
/* 3 · before and after side by side */
html.depth-live.depth-narrow .vanity { --frame-h: min(34svh, calc((100vw - 52px) / 2 * 4 / 3)); gap: 12px; width: calc(var(--frame-h) * 3 / 4 * 2 + 12px); }
/* 4 · the film first, its words under it */
html.depth-live.depth-narrow .film-still { display: none; }
html.depth-live.depth-narrow .film-frame { --frame-h: min(48svh, calc((100vw - 40px) * 4 / 3)); }
html.depth-live.depth-narrow .film-copy { order: 1; }
/* 5 · the two squares one above the other, both whole */
html.depth-live.depth-narrow .faces-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
html.depth-live.depth-narrow .face-card { width: min(100%, calc((100svh - 360px) / 2)); }
html.depth-live.depth-narrow .faces-stage h2 { margin-bottom: var(--sp-4); }
/* 6 · the sphere, sized to the width (tile positions and --tile-h come from depth.js) */
html.depth-live.depth-narrow .sphere-row { grid-template-columns: minmax(0, 1fr); }
html.depth-live.depth-narrow .sphere-stage { width: min(84vw, 44svh); height: min(84vw, 44svh); }
html.depth-live.depth-narrow .mirror-plate { display: none; }
html.depth-live.depth-narrow .tile { height: var(--tile-h); width: calc(var(--tile-h) * .75); margin: calc(var(--tile-h) / -2) 0 0 calc(var(--tile-h) * -.375); }
/* 7 · the frame-by-frame transformation */
html.depth-live.depth-narrow .seq-dynamic { width: min(62vw, calc(46svh * 405 / 720)); }
html.depth-live.depth-narrow .seq-frame { min-height: 0; }
html.depth-live.depth-narrow .sequencer-stage h2 { margin-bottom: var(--sp-4); }
/* 1 · in the pinned world the whole hero (word, film, line, button, hint)
   has to fit one screen: the film frame gives up the difference on short
   phones (still over half the screen at 360x740). */
html.depth-live.depth-narrow .letters { width: min(100%, 460px, calc((100svh - 356px) * .75)); }
/* 5 · a gap keeps the caption and its link apart on a narrow card, and a
   little inset keeps the first letter clear of the grid's rounded corner
   (overflow:hidden for the spotlight cut the S of "SOFT" on the desktop). */
.face-card figcaption { gap: var(--sp-3); padding-inline: var(--sp-3); }
