/* ==========================================================================
   Scroll Site Studio — base styles
   One set of decisions, reused everywhere (coherence is cheaper than talent):
   a type scale, a spacing scale, a small color set from tokens.css.
   The page must read perfectly with no JavaScript: every animated start state
   is scoped to .motion, which only the inline <head> script adds.
   Viewport units: svh for full-screen scenes and pins (the small viewport, which
   doesn't change while the mobile toolbars slide, so trigger positions hold);
   lvh only for backgrounds that must cover behind the toolbars; dvh only for
   fixed overlays. dvh resizes during scroll and moves every trigger.
   ========================================================================== */

/* ---------- Scales ---------- */
:root {
  --step--1: clamp(0.83rem, 0.8rem + 0.15vw, 0.94rem);
  --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.15rem);
  --step-1: clamp(1.25rem, 1.15rem + 0.5vw, 1.55rem);
  --step-2: clamp(1.6rem, 1.4rem + 1vw, 2.2rem);
  --step-3: clamp(2.1rem, 1.7rem + 2vw, 3.4rem);
  --step-4: clamp(2.8rem, 2rem + 4vw, 5.6rem);
  --step-5: clamp(3.4rem, 2.2rem + 6.5vw, 8.5rem);

  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-s: 1rem;
  --space-m: 1.5rem;
  --space-l: 2.5rem;
  --space-xl: 4rem;
  --space-2xl: clamp(5rem, 4rem + 6vw, 10rem);

  --gutter: clamp(1rem, 0.5rem + 3vw, 3rem);
  --header-h: 4rem;   /* fixed header height: pinned scenes pad by this so it never covers them */
  --max: 1320px;
  --lcp-offset: 0.14; /* how far (em) the data-lcp hero headline sits low before it settles */
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
/* scroll-padding: anything scrolled into view by focus or an anchor clears the fixed header
   (WCAG 2.2 SC 2.4.11, focus not obscured) */
html { -webkit-text-size-adjust: 100%; scroll-padding-top: calc(var(--header-h) + 0.5rem); }
body, h1, h2, h3, h4, p, figure, blockquote, ul, ol { margin: 0; }
ul[role="list"], ol[role="list"] { list-style: none; padding: 0; }
img, video, canvas, svg { display: block; max-width: 100%; }
img, video { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }

/* Lenis */
html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent] { overscroll-behavior: contain; }

body {
  background: var(--color-bg);
  color: var(--color-fg);
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  font-size: var(--step-0);
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

[id] { scroll-margin-top: var(--header-h); }

h1, h2, h3, .display {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-2); line-height: 1.15; }
p { max-width: var(--measure); text-wrap: pretty; }
strong { font-weight: var(--weight-strong); }
a { color: inherit; }

.eyebrow {
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.lede { font-size: var(--step-1); color: var(--color-muted); }

/* ---------- Accessibility ---------- */
.skip-link {
  position: absolute; left: var(--space-s); top: -100px; z-index: 2000;
  background: var(--color-accent); color: var(--color-on-accent);
  padding: var(--space-2xs) var(--space-s); border-radius: var(--radius);
}
.skip-link:focus { top: var(--space-s); }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- Layout ---------- */
/* Clipping boxes use overflow: clip (with hidden as the fallback for browsers without it): hidden also makes the box a
   scroll container, which scroll-driven animations inside it then track instead of the page, so they never move
   (Bramus, course episode 5; verified in Chrome 155). The reset above zeroes block margins and spacing is applied
   between siblings, so clip's lack of a new formatting context changes no layout here. */
.wrap { width: min(100% - 2 * var(--gutter), var(--max)); margin-inline: auto; }
.section { padding-block: var(--space-2xl); }
.full { min-height: 100svh; display: grid; align-content: center; }
.flow > * + * { margin-top: var(--space-m); }   /* vertical rhythm utility (not the stack scene) */

.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 50;
  display: flex; justify-content: space-between; align-items: center;
  min-height: var(--header-h); padding: var(--space-xs) var(--gutter);
  background: color-mix(in srgb, var(--color-bg) 88%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.site-header a { text-decoration: none; font-weight: var(--weight-strong); }
.site-header nav { display: flex; gap: var(--space-m); align-items: center; }
.btn-small { padding: 0.6em 1.1em; font-size: var(--step--1); }  /* the primary action, always on screen */

/* "Pause motion" (WCAG 2.2.2): hidden until something loops; core.js reveals it.
   At least 24x24 CSS px (2.5.8); 44px where there's room. */
.motion-toggle {
  min-height: 2.75rem; min-width: 2.75rem; padding: 0 0.9em; border-radius: 999px;
  border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
  background: transparent; color: inherit; font: inherit; font-size: var(--step--1); cursor: pointer;
}
.motion-toggle[aria-pressed="true"] { background: var(--color-fg); color: var(--color-bg); }
.motion-toggle[hidden] { display: none; }
/* Paused: CSS animations (marquees, pulses) stop too. Scroll-driven motion stays: the visitor drives it.
   A paused scroll-driven CSS animation stops following the scroll and holds wherever it was (a reveal half-way,
   a header with no backdrop), so the engine's scroll-driven features are exempt by name. */
.motion-paused *, .motion-paused *::before, .motion-paused *::after { animation-play-state: paused !important; }
.motion-paused [data-css-reveal], .motion-paused [data-scroll-progress],
.motion-paused .site-header[data-header="clear"]::before { animation-play-state: running !important; }

/* ---------- Scroll-driven support features (references/css-timelines.md) ----------
   CSS animations on scroll timelines: the browser runs them off the main thread, in step with the scroll.
   Each is detected as a whole (ranges included) and has a readable state without it. */

/* A header that clears over the hero: transparent at the top, its backdrop fades in over the first
   --header-clear of scroll (default 60vh). <header class="site-header" data-header="clear">. Opacity only, so it
   is a fade (allowed under reduced motion). Without scroll-driven animations the backdrop is simply there.
   Pick a header colour that reads on the hero AND on the backdrop; check both ends. */
.site-header[data-header="clear"] { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
.site-header[data-header="clear"]::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: color-mix(in srgb, var(--color-bg) 88%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
  .site-header[data-header="clear"]::before {
    animation: studio-header-clear linear both; animation-timeline: scroll(root);
    animation-range: 0 var(--header-clear, 60vh);
  }
}
@keyframes studio-header-clear { from { opacity: 0; } }

/* Reading progress: a bar that fills as the page is read. <div class="scroll-progress" data-scroll-progress
   aria-hidden="true"></div>, inside the header. Inside a [data-scroll-chapter] section it tracks that chapter
   instead (fully on screen to fully past; make the bar sticky in a long chapter). Hidden where unsupported (a
   full bar would claim the page was read). A progress indicator like a scrollbar, so it stays under reduced motion. */
.scroll-progress { position: absolute; inset: auto 0 0 0; height: 2px; background: var(--color-accent); }
[data-scroll-progress] { display: none; transform-origin: 0 50%; }
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
  [data-scroll-progress] { display: block; animation: studio-progress linear both; animation-timeline: scroll(root); }
  [data-scroll-chapter] { view-timeline: --studio-chapter block; }
  [data-scroll-chapter] [data-scroll-progress] { animation-timeline: --studio-chapter; animation-range: contain 0% contain 100%; }
}
@keyframes studio-progress { from { transform: scaleX(0); } }

/* ---------- Buttons & links (hover in 0.25s, out 0.15s: exits faster) ---------- */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-2xs);
  padding: 0.95em 1.6em; border-radius: 999px;
  background: var(--color-accent); color: var(--color-on-accent);
  font-weight: var(--weight-strong); text-decoration: none;
  transition: background-color 0.15s ease-out, color 0.15s ease-out;
}
.btn:hover { background: var(--color-fg); color: var(--color-bg); transition-duration: 0.25s; }
.btn:focus-visible { outline: 2px solid var(--color-fg); outline-offset: 4px; }

/* Header dropdown (menu.js): a floating rounded panel under its trigger, disconnected from the bar by a gap the
   pointer can cross (the ::before bridges it) */
.has-menu { position: relative; }
.has-menu > .menu-panel {
  position: absolute; top: calc(100% + 0.8rem); left: 50%; translate: -50% 0; z-index: 60; transform-origin: 50% 0;
  min-width: 17rem; padding: 0.45rem; border-radius: 1.1rem; display: grid; gap: 0.1rem; text-align: left;
  background: color-mix(in srgb, var(--color-bg) 84%, transparent); -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  border: 1px solid color-mix(in srgb, var(--color-fg) 12%, transparent); box-shadow: 0 1.4rem 2.6rem -1rem rgb(0 0 0 / 0.55);
}
.has-menu > .menu-panel::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 1rem; }
.menu-panel[hidden] { display: none; }
.menu-panel a { display: grid; gap: 0.08rem; padding: 0.55rem 0.8rem; border-radius: 0.7rem; text-decoration: none; font-weight: var(--weight-strong); line-height: 1.25; white-space: nowrap; transition: background-color 0.08s ease-out, color 0.08s ease-out; }
.menu-panel a:hover, .menu-panel a:focus-visible { background: color-mix(in srgb, var(--color-fg) 10%, transparent); }
.menu-panel .menu-type { font-size: 0.78rem; font-weight: 400; color: var(--color-muted); }
.menu-panel .menu-foot { display: flex; gap: 0.4rem; align-items: baseline; margin-top: 0.3rem; padding-top: 0.6rem; border-top: 1px solid color-mix(in srgb, var(--color-fg) 10%, transparent); border-radius: 0 0 0.7rem 0.7rem; color: var(--color-muted); }
.menu-panel .menu-foot:hover, .menu-panel .menu-foot:focus-visible { color: inherit; }
.link-underline { position: relative; text-decoration: none; }
.link-underline::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -0.15em; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform 0.15s ease-out;
}
.link-underline:hover::after, .link-underline:focus-visible::after { transform: scaleX(1); transition-duration: 0.25s; }

/* ---------- Hero ---------- */
.hero { min-height: 100svh; display: grid; align-content: end; padding-bottom: var(--space-xl); }
.hero h1 { max-width: 14ch; }
.hero .lede { margin-top: var(--space-m); }

/* ---------- Claim ---------- */
.claim .statement { font-family: var(--font-display); font-size: var(--step-3); line-height: 1.15; max-width: 22ch; }

/* ---------- Steps (pinned story) ---------- */
.steps-layout { display: grid; gap: var(--space-l); }
.steps { display: grid; gap: var(--space-l); }
[data-step] h3 { margin-bottom: var(--space-xs); }
@media (min-width: 900px) {
  .steps-layout { grid-template-columns: 1fr 1fr; align-items: center; }
}
.is-pinned-steps { padding-block: 0; }
.is-pinned-steps .steps-layout { height: 100svh; padding-top: var(--header-h); }
.is-pinned-steps [data-step] { grid-area: 1 / 1; }
/* The scroll picks the step (steps.js); the handover is a short TIMED fade, so a reader can never
   stop on two half-faded captions (a scrubbed cross-fade held mid-way measured ~2.9:1, L38). */
.is-pinned-steps [data-step] {
  opacity: 0; visibility: hidden; transform: translateY(40px);
  transition: opacity calc(.3s * var(--motion-speed, 1)) ease-in, transform calc(.3s * var(--motion-speed, 1)) ease-in,
              visibility 0s linear calc(.3s * var(--motion-speed, 1));
}
.is-pinned-steps [data-step].is-before { transform: translateY(-30px); }
.is-pinned-steps [data-step].is-active {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity calc(.45s * var(--motion-speed, 1)) cubic-bezier(.215, .61, .355, 1) calc(.12s * var(--motion-speed, 1)),
              transform calc(.45s * var(--motion-speed, 1)) cubic-bezier(.215, .61, .355, 1) calc(.12s * var(--motion-speed, 1)),
              visibility 0s;
}

/* ---------- Horizontal ---------- */
.h-head { margin-bottom: var(--space-l); }
.h-track { display: grid; gap: var(--space-m); padding-inline: var(--gutter); }
@media (min-width: 768px) { .h-track { grid-template-columns: 1fr 1fr; } }
.panel {
  background: var(--color-surface); border-radius: var(--radius);
  padding: var(--space-l); display: grid; align-content: end; gap: var(--space-xs);
  min-height: 18rem;
}
.panel .num { font-family: var(--font-display); font-size: var(--step-4); color: var(--color-accent); line-height: 1; }
/* flex column, not grid: a max-content track would widen a grid column and push the heading off-screen */
.is-horizontal { height: 100svh; padding-block: var(--header-h) 0; overflow: hidden; overflow: clip; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
.is-horizontal .h-head { margin-bottom: var(--space-m); }
.is-horizontal .h-track {
  display: flex; width: max-content; gap: var(--space-m);
  padding-inline: var(--gutter);
}
.is-horizontal .panel { flex: 0 0 80vw; max-width: 1100px; height: 68svh; min-height: 0; }
/* data-phone="swipe" below 900 x 520: a native swipe row that snaps each panel to the centre (horizontal.js) */
.is-swipe .h-track {
  display: flex; gap: var(--space-s); overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-padding-inline: var(--gutter); scrollbar-width: none;
}
.is-swipe .h-track::-webkit-scrollbar { display: none; }
.is-swipe .h-track:focus-visible { outline: 2px solid var(--ring); outline-offset: 4px; }
.is-swipe .panel { flex: 0 0 min(84vw, 34rem); scroll-snap-align: center; }
.swipe-steps { height: 2px; margin: var(--space-s) var(--gutter) 0; background: var(--color-accent); transform-origin: 0 50%; }

/* ---------- Parallax bands ---------- */
.band { position: relative; overflow: hidden; overflow: clip; min-height: 70svh; display: grid; align-content: end; padding: var(--space-l) var(--gutter); }
.band + .band { margin-top: 2px; }
.band-bg { position: absolute; inset: -10% 0; z-index: -1; }  /* 20% taller: no gaps */
.band-bg img { width: 100%; height: 100%; object-fit: cover; }
.band-label { position: relative; color: #fff; text-shadow: 0 1px 24px rgba(0,0,0,0.35); }
.band { isolation: isolate; }

/* Parallax anywhere (e.g. a full-bleed hero image): put class="has-depth" on the
   holder and data-depth on the layer. The layer is 20% taller so no gap ever shows. */
.has-depth { position: relative; overflow: hidden; overflow: clip; isolation: isolate; }
.has-depth > [data-depth] { position: absolute; inset: -10% 0; z-index: -1; }
.has-depth > [data-depth] img { width: 100%; height: 100%; object-fit: cover; }

/* Alternate surface (e.g. light sections on a dark site): add class="theme-alt" to a section.
   Its colors come from brand.json "alt" (generated into tokens.css). */
.theme-alt { background: var(--color-bg); color: var(--color-fg); }  /* tokens.css redefines the colors inside .theme-alt */

/* ---------- Sequence (product spin) ---------- */
.seq { display: grid; gap: var(--space-l); align-items: center; }
@media (min-width: 900px) { .seq { grid-template-columns: 1fr 1.2fr; } }
/* seq-held = the pin exists (from boot): layout. seq-live = frames are in: paint only (canvas over still). */
.seq-held { padding-block: 0; }
.seq-held .seq { height: 100svh; padding-top: var(--header-h); }
.seq-media { position: relative; aspect-ratio: 1; width: 100%; max-height: 82svh; margin-inline: auto; }
.seq-media > * { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.seq-canvas { display: none; }
.seq-live .seq-canvas { display: block; }
.seq-live .seq-still { display: none; }
/* Sequence cues (sequence.js): the stage picks the state; the change is a short TIMED fade with no delay,
   so the type keeps up with the frames and no scroll position shows half-faded text. Opacity only: the
   text stays in the accessibility tree and find-in-page. Cues holding a control also leave the tab order. */
.cues-live:not(.scene-off) [data-cue] {
  opacity: 0;
  transition: opacity calc(.25s * var(--motion-speed, 1)) ease-in;
}
.cues-live:not(.scene-off) [data-cue]:has(a, button, input, select, textarea, [tabindex]):not(.is-cued) {
  visibility: hidden;
  transition: opacity calc(.25s * var(--motion-speed, 1)) ease-in, visibility 0s linear calc(.25s * var(--motion-speed, 1));
}
.cues-live:not(.scene-off) [data-cue].is-cued {
  opacity: 1;
  transition: opacity calc(.25s * var(--motion-speed, 1)) cubic-bezier(.215, .61, .355, 1);
}

/* ---------- Scrubbed video ---------- */
.scrub-video { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: var(--radius); background: var(--color-surface); }
.video-live { padding-block: 0; height: 100svh; display: grid; align-content: center; }
.caption { margin-top: var(--space-s); color: var(--color-muted); font-size: var(--step--1); }

/* ---------- 3D model ---------- */
.model-stage { position: relative; aspect-ratio: 1; width: min(100%, 80svh); margin-inline: auto; }
.model-stage > * { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.model-canvas { display: none; }
.model-live .model-canvas { display: block; }
.model-live .model-still { display: none; }
[data-scene~="model"].model-live { padding-block: 0; min-height: 100svh; display: grid; align-content: center; }

/* ---------- Detail (shader) ---------- */
.detail-grid { display: grid; gap: var(--space-m); }
@media (min-width: 768px) { .detail-grid { grid-template-columns: 1fr 1fr; } }
.detail-grid img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; border-radius: var(--radius); }
.detail-grid figcaption { margin-top: var(--space-xs); color: var(--color-muted); font-size: var(--step--1); }
.distort-canvas { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.distort-live img[data-distort] { opacity: 0; } /* still receives the pointer */

/* ---------- Buy ---------- */
.buy { text-align: center; }
.buy .price { font-family: var(--font-display); font-size: var(--step-3); margin-block: var(--space-s) var(--space-l); }
.buy .actions { display: flex; gap: var(--space-l); justify-content: center; align-items: center; flex-wrap: wrap; }

/* ---------- Footer ---------- */
.site-footer { padding: var(--space-xl) var(--gutter); color: var(--color-muted); font-size: var(--step--1); display: flex; justify-content: space-between; gap: var(--space-m); flex-wrap: wrap; }

/* ---------- Loader & page wipe ---------- */
#loader {
  position: fixed; inset: 0; z-index: 1000; display: none;
  background: var(--color-bg); color: var(--color-fg);
  place-content: center; justify-items: center; gap: var(--space-s);
}
.motion:not(.no-loader) #loader { display: grid; }
.loader-name { font-family: var(--font-display); font-size: var(--step-2); }
.loader-bar { width: 160px; height: 1px; background: color-mix(in srgb, var(--color-fg) 20%, transparent); overflow: hidden; }
.loader-fill { display: block; height: 100%; background: var(--color-accent); transform: scaleX(0); transform-origin: left; }

#wipe {
  position: fixed; inset: 0; z-index: 999; pointer-events: none;
  background: var(--color-surface); transform: translateY(100%);
}
.is-arriving #wipe, .is-leaving #wipe { pointer-events: auto; }
.is-arriving #wipe { transform: none; }

/* Native page transition (Chrome 126+, Safari 18.2+): the new page wipes up over the old,
   the header holds still. transition.js runs its panel wipe only where this isn't supported. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  .site-header { view-transition-name: site-header; }
  ::view-transition-old(root) { animation: 0.5s cubic-bezier(0.7, 0, 0.3, 1) both studio-vt-out; }
  ::view-transition-new(root) { animation: 0.6s cubic-bezier(0.7, 0, 0.3, 1) both studio-vt-in; }
}
@keyframes studio-vt-out { to { opacity: 0.4; transform: translateY(-6%); } }
@keyframes studio-vt-in { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }

/* ---------- Motion start states (only when JS + motion are on) ---------- */
.motion [data-scene~="reveal"] [data-reveal] { opacity: 0; }
/* Masked headings stay hidden until split. The data-lcp hero is the exception: it is painted
   at first paint (Largest Contentful Paint), just sitting slightly low until it settles. */
.motion [data-scene~="lines"]:not([data-lcp]):not(.is-split):not(.is-static):not(.scene-off) { visibility: hidden; }
.motion [data-scene~="lines"][data-lcp]:not(.is-split):not(.is-static):not(.scene-off) { transform: translateY(calc(var(--lcp-offset) * 1em)); }
[data-split-visual] { display: block; text-wrap: wrap; }   /* balance + split measure different lines */

/* CSS-only hero rise (lines.js comment): authored lines rise out of their masks at first paint, so the
   hero never waits on script (LCP = FCP measured, course L42). No start state to fail, so no failsafe.
   The padding gives ascenders/descenders room inside the clip; the negative margin cancels it.
   .is-arrived (core.js) stops it replaying when the motion preference changes. */
.rise-mask { display: block; overflow: clip; padding-block: 0.08em 0.2em; margin-block: -0.08em -0.2em; }
.rise-line { display: block; }
@media (prefers-reduced-motion: no-preference) {
  .rise:not(.is-arrived) .rise-line { animation: studio-rise calc(0.9s * var(--motion-speed, 1)) cubic-bezier(0.25, 1, 0.5, 1) both; }
  .rise:not(.is-arrived) .rise-mask:nth-child(2) .rise-line { animation-delay: calc(0.12s * var(--motion-speed, 1)); }
  .rise:not(.is-arrived) .rise-mask:nth-child(3) .rise-line { animation-delay: calc(0.24s * var(--motion-speed, 1)); }
}
@keyframes studio-rise { from { transform: translateY(115%); } }
/* A scene that failed (core.js markFailed) shows its content as the static page: every hide-until-ready
   rule above carries :not(.scene-off), and anything a scene had hidden inline is cleared. */
.scene-off [data-reveal], .scene-off[data-reveal], .scene-off [data-step], .scene-off [data-cue] { opacity: 1 !important; visibility: visible !important; transform: none !important; }

/* CSS scroll-driven reveal: progressive polish, no JavaScript, runs off the main thread in
   Chrome 115+ and Safari 26+. Elsewhere, and under reduced motion, the element is simply there.
   <p data-css-reveal>…</p> for small supporting elements, never a scene's main behaviour.
   Transform only: a scroll-linked opacity can be held half-way, and half-faded text fails contrast (L38).
   Detected as a whole, ranges included: a partial implementation would play it at the wrong time.
   Never inside an overflow: hidden box (it becomes the tracked scroller and the reveal never runs): use clip. */
@supports ((animation-timeline: view()) and (animation-range: entry 5% entry 70%)) {
  @media (prefers-reduced-motion: no-preference) {
    .motion [data-css-reveal] { animation: studio-css-reveal linear both; animation-timeline: view(); animation-range: entry 5% entry 70%; }
  }
}
@keyframes studio-css-reveal { from { transform: translateY(2rem); } }

/* ---------- Clip (film plays when reached) ---------- */
.clip { display: grid; gap: var(--space-s); padding-block: var(--space-2xl); }
.clip-frame { position: relative; border-radius: var(--radius); overflow: hidden; overflow: clip; transform-origin: 50% 60%; will-change: transform; }   /* its own layer: a rounded, clipped, scrubbed frame around a playing film cost 15–26% dropped frames at 2x without it (measured 2026-10-15), 0% with */
.clip-frame video { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: var(--color-surface); }
.clip-toggle {
  position: absolute; right: var(--space-s); bottom: var(--space-s); min-height: 2.75rem; min-width: 2.75rem;
  padding: 0 1em; border: 0; border-radius: 999px; cursor: pointer; font: inherit; font-size: var(--step--1);
  background: rgb(0 0 0 / 0.55); color: #fff; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.clip-toggle[hidden] { display: none; }
.clip figcaption { color: var(--color-muted); font-size: var(--step--1); }

/* ---------- Hold (press and hold to reveal) ---------- */
.hold { display: grid; justify-items: center; gap: var(--space-l); text-align: center; padding-block: var(--space-2xl); }
.hold-stage { position: relative; width: min(100%, 72rem); aspect-ratio: 16 / 10; border-radius: var(--radius); overflow: hidden; overflow: clip; }
.hold-stage img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hold-btn {
  --ring: var(--color-accent);
  min-height: 3.25rem; padding: 0 1.6em; border-radius: 999px; cursor: pointer; font: inherit; font-weight: var(--weight-strong);
  color: var(--color-fg); border: 1px solid color-mix(in srgb, var(--color-fg) 30%, transparent);
  background: linear-gradient(90deg, color-mix(in srgb, var(--ring) 45%, transparent) calc(var(--hold-progress, 0) * 100%), transparent 0);
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; touch-action: manipulation;
}
.is-held .hold-btn { border-color: var(--ring); }
/* No JS / no hold: both images are DOM content; show the after state and the revealed text */
.hold:not(.hold-live) .hold-before { display: none; }

/* ---------- Proof dossier (the plain facts, inside the art direction) ----------
   The 2026 commercial winners (ERA Residence, Son Daven) carry a chapter of hard facts:
   availability, prices or price per m², returns, specs, an FAQ. Composed as type on the
   page's own grid, not cards. Rows scrub in (level 4); the action sits right under them. */
.dossier { padding-block: var(--space-2xl); }
.dossier-list { margin: var(--space-l) 0 var(--space-xl); border-top: 1px solid color-mix(in srgb, currentColor 22%, transparent); }
.dossier-row { display: grid; gap: var(--space-2xs) var(--space-l); padding-block: var(--space-m); border-bottom: 1px solid color-mix(in srgb, currentColor 22%, transparent); }
@media (min-width: 768px) { .dossier-row { grid-template-columns: minmax(10rem, 1fr) 2fr; align-items: baseline; } }
.dossier-row dt { color: var(--color-muted); font-size: var(--step--1); }
.dossier-row dd { margin: 0; font-family: var(--font-display); font-size: var(--step-2); line-height: 1.15; font-variant-numeric: tabular-nums; }

/* ==========================================================================
   Scenes: full-bleed, one idea per screen. This is the default layout language
   of a scroll-driven site: no card grids, no boxed sections. Each scene owns the
   whole viewport and is played by the scroll.
   ========================================================================== */
.scene { position: relative; min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; overflow: clip; }  /* minmax(0,1fr): a nowrap drift line can't widen the column */
.scene-dark { background: var(--color-fg); color: var(--color-bg); }
.scene-dark .eyebrow, .scene-dark .lede { color: color-mix(in srgb, var(--color-bg) 72%, transparent); }

/* Statement lit word by word (scrubtext) */
.statement { min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr); place-content: center; padding-inline: var(--gutter); text-align: center; }
.statement-text { font-family: var(--font-display); font-size: clamp(2.4rem, 1.2rem + 4.6vw, 6.2rem); line-height: 1.06; letter-spacing: -0.02em; max-width: 17ch; margin-inline: auto; text-wrap: balance; }

/* Expanding window (expand) */
.expand { position: relative; height: 100svh; overflow: hidden; overflow: clip; display: grid; place-items: center; }
.expand-frame { position: absolute; inset: 0; overflow: hidden; overflow: clip; transform-origin: 50% 50%; will-change: transform; }
.expand-media { position: absolute; inset: 0; transform-origin: 50% 50%; will-change: transform; }
.expand-media img, .expand-media video { width: 100%; height: 100%; object-fit: cover; }
.expand-media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(0deg, rgb(0 0 0 / .45), transparent 60%); }
.expand-copy { position: relative; z-index: 1; color: #fff; text-align: center; padding: var(--gutter); align-self: end; margin-bottom: 12svh; }
.expand-copy h2 { font-size: clamp(2.8rem, 1.6rem + 5vw, 7rem); }

/* Stacking panels (stack) */
.stack-panel { position: relative; min-height: 100svh; background: var(--color-bg); overflow: hidden; overflow: clip; }
.stack-panel:nth-of-type(even) { background: var(--color-surface); }  /* articles only: an sr-only heading first doesn't shift it */
.stack-inner { min-height: 100svh; display: grid; gap: var(--space-l); align-content: center; padding: calc(var(--header-h) + var(--space-l)) var(--gutter) var(--space-l); transform-origin: 50% 0; }
.stack-inner img { width: 100%; height: 100%; max-height: 70svh; object-fit: cover; border-radius: var(--radius); }
@media (min-width: 900px) { .stack-inner { grid-template-columns: 1fr 1.1fr; align-items: center; gap: var(--space-2xl); } }
/* Sideways stack (data-direction="left"): the section holds one screen; panels share it and the next slides in from
   the right over the one before, which sinks toward the left edge. The arriving panel casts a shadow on the one below. */
.is-stacking-left { position: relative; height: 100svh; overflow: hidden; overflow: clip; }
.is-stacking-left .stack-panel { position: absolute; inset: 0; min-height: 0; }
.is-stacking-left .stack-panel + .stack-panel { box-shadow: -40px 0 80px rgb(0 0 0 / 0.45); }
.is-stacking-left .stack-inner { min-height: 100svh; transform-origin: 0 50%; }

/* Ring: a loop of screens (ring.js). Without the scene (no JS, a scene error) the items are a plain list, each screen
   above its copy. With it, every screen sits on one centre point and ring.js places it on the loop; the nearest
   item's copy shows under the ring (the others stay focusable: focus turns their item to the front). */
.ring { padding-block: var(--space-2xl); }
.ring-stage { display: grid; gap: var(--space-2xl); padding-inline: var(--gutter); }
.ring-item { display: grid; gap: var(--space-m); align-content: start; }
.ring-card { margin: 0; padding-block: 0; }
.ring-card video, .ring-card > img { display: block; width: 100%; height: auto; }
.ring-copy > * + * { margin-top: var(--space-s); }
/* the wall (≥ 900 x 520, motion allowed): the section holds; the ring sits between the header and the copy band
   (--ring-copy is the tallest copy, measured by ring.js). The far side of the loop rises ~0.7 screen widths above
   the front screen's centre. */
.is-ring {
  position: relative; height: 100svh; padding: 0; overflow: hidden; overflow: clip;
  --ring-top: calc(var(--header-h) + 2svh);
  --ring-copy: 12rem;
  --ring-nav: 2rem;   /* room under the front screen: 4.5rem with the arrows (.has-nav) */
  --ring-w: min(36vw, calc((96svh - var(--ring-copy) - var(--ring-top) - var(--ring-nav)) / 1.01));
  --ring-cy: calc(var(--ring-top) + var(--ring-w) * 0.7);
}
.is-ring .ring-stage {
  position: absolute; inset: 0; display: block; padding: 0;
  cursor: grab; touch-action: pan-y; -webkit-user-select: none; user-select: none;
}
.is-ring.is-dragging .ring-stage { cursor: grabbing; }
.is-ring:not(.is-dragging) .ring-item:not(.is-front) .ring-card { cursor: pointer; }   /* a side screen turns to the front */
.is-ring .ring-item { display: block; }
.is-ring .ring-card {
  position: absolute; left: 50%; top: var(--ring-cy); will-change: transform;
  display: block; width: var(--ring-w); aspect-ratio: 16 / 10; margin: calc(var(--ring-w) * -0.3125) 0 0 calc(var(--ring-w) * -0.5);
  border-radius: calc(var(--radius) * 1.5); overflow: hidden; overflow: clip; background: #000;
  box-shadow: 0 2.4rem 3rem -1.6rem rgb(0 0 0 / 0.85), 0 0 0 1px rgb(255 255 255 / 0.07);
}
.is-ring .ring-card::after { content: ""; position: absolute; inset: 0; background: #000; opacity: var(--ring-dim, 0); pointer-events: none; }
.is-ring .ring-card .clip-frame { height: 100%; border-radius: 0; }
.is-ring .ring-card video, .is-ring .ring-card > img { height: 100%; aspect-ratio: auto; object-fit: contain; background: #000; -webkit-user-drag: none; }
.is-ring .ring-card figcaption { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.is-ring .ring-item:not(.is-front) .clip-toggle { visibility: hidden; }
.ring-soft { display: none; }
/* depth of field: pre-blurred snapshots over each screen, crossfaded by ring.js (opacity only) */
.is-ring .ring-soft { display: block; position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
/* floor mirror (data-mirror): a canvas per screen, flipped by ring.js (scaleY -1), so the mask's strong end (its own
   bottom) lands at the visual top, against the screen, and fades away down the floor */
.ring-mirror { display: none; }
.is-ring .ring-mirror {
  display: block; position: absolute; left: 50%; top: var(--ring-cy); width: var(--ring-w); aspect-ratio: 16 / 10;
  margin: calc(var(--ring-w) * -0.3125) 0 0 calc(var(--ring-w) * -0.5); border-radius: calc(var(--radius) * 1.5);
  opacity: 0; pointer-events: none; will-change: transform;
  -webkit-mask-image: linear-gradient(to top, #000 0%, transparent 34%); mask-image: linear-gradient(to top, #000 0%, transparent 34%);
}
.is-ring .ring-copy {
  position: absolute; z-index: 300; left: 50%; bottom: 4svh; width: min(72rem, calc(100% - 2 * var(--gutter)));
  transform: translateX(-50%); pointer-events: none;
}
/* ring.js fades each copy's lines in and out (GSAP, staggered); only the nearest item's copy takes the pointer */
.is-ring .ring-item.is-near .ring-copy { pointer-events: auto; }
/* Previous / Next (added by ring.js) under the front screen */
.ring-nav {
  position: absolute; z-index: 300; left: 50%; top: calc(var(--ring-cy) + var(--ring-w) * 0.3125 + 0.75rem);
  display: flex; gap: var(--space-xs); transform: translateX(-50%);
}
.ring-nav button {
  inline-size: 2.75rem; block-size: 2.75rem; border-radius: 50%; border: 1px solid currentColor; background: transparent;
  color: inherit; font: inherit; font-size: 1.1rem; line-height: 1; cursor: pointer; opacity: 0.8;
}
.ring-nav button:hover { opacity: 1; }
.ring-nav button[aria-disabled="true"] { opacity: 0.3; cursor: default; }
/* free (phones, short windows, reduced motion): no hold; the ring keeps its shape in the flow of the page and turns by
   swipe, tap or the buttons */
.is-ring.is-ring-free { height: auto; padding-block: var(--space-xl); --ring-w: min(60vw, 34rem); --ring-cy: calc(var(--ring-w) * 0.7); }
.is-ring-free .ring-stage { position: relative; inset: auto; height: calc(var(--ring-w) * 1.02 + var(--ring-nav) + 0.25rem + var(--ring-copy)); }
.is-ring.has-nav { --ring-nav: 4.5rem; }
.is-ring-free .ring-copy { bottom: 0; }
.stack-num { font-family: var(--font-display); font-size: var(--step-5); color: var(--color-accent); line-height: 1; }

/* Drifting giant type (scrub on a single line) */
.drift-band { overflow: clip; padding-block: var(--space-xl); }
.drift { font-family: var(--font-display); font-size: clamp(4rem, 1.5rem + 11vw, 15rem); line-height: 1; white-space: nowrap; letter-spacing: -0.03em; margin: 0; max-width: none; }

/* Full-height image panels for horizontal rows */
.panel-media { padding: 0; overflow: hidden; overflow: clip; align-content: end; position: relative; color: #fff; min-height: 70svh; }
.panel-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.panel-media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(0deg, rgb(0 0 0 / .6), transparent 55%); }
.panel-media .panel-copy { position: relative; z-index: 1; padding: var(--space-l); display: grid; gap: var(--space-2xs); }

/* Hero over a full-bleed image/canvas: light text, scrim where the words sit */
.hero.has-depth { color: #fff; }
.hero.has-depth::after { content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg, rgb(0 0 0 / .62), rgb(0 0 0 / .18) 70%), linear-gradient(0deg, rgb(0 0 0 / .4), transparent 50%); }
.hero.has-depth .eyebrow, .hero.has-depth .lede { color: rgb(255 255 255 / .82); }
.hero-inner { position: relative; }

/* Story beats with an image */
[data-step] img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius); margin-bottom: var(--space-m); }
.is-pinned-steps [data-step] img { max-height: 46svh; }

/* Assembling object (assemble): the stage's layout IS the assembled object; parts are placed in % */
.assemble-stage { position: relative; z-index: 1; width: min(80vw, 520px); aspect-ratio: 1; }
.assemble-stage .part { position: absolute; display: block; height: auto; max-width: none; }

/* Kinetic cut (cut): without motion the images simply stack; with motion they share one full screen */
.cut { position: relative; }
.cut-layer img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.motion [data-scene~="cut"]:not(.scene-off) { height: 100svh; overflow: hidden; overflow: clip; display: grid; place-items: center; }
.motion [data-scene~="cut"]:not(.scene-off) .cut-frame { position: absolute; inset: 0; overflow: hidden; overflow: clip; }
.motion [data-scene~="cut"]:not(.scene-off) .cut-layer { position: absolute; inset: 0; transform-origin: 50% 50%; will-change: transform; }
.motion [data-scene~="cut"]:not(.scene-off) .cut-layer img { height: 100%; aspect-ratio: auto; }
.motion [data-scene~="cut"]:not(.scene-off) .cut-layer + .cut-layer { visibility: hidden; }          /* start state; the scene takes over */
.cut-copy { position: relative; z-index: 1; color: #fff; text-align: center; padding: var(--gutter); }
.motion [data-scene~="cut"]:not(.scene-off) .cut-frame::after { content: ""; position: absolute; inset: 0; background: rgb(0 0 0 / .28); pointer-events: none; }

/* Strategic 3D (model data-swap): stills at rest, live model only during the turn */
.model-still-end { display: none; }
.model-live.model-swap .model-still { display: block; }
.model-live.model-swap .model-still-end { opacity: 0; }

/* Fly (fly): slots in a headline hand their media down to cells. Without motion the slots collapse
   and the headline reads normally. Nothing between the slots and the cells may clip (no overflow: hidden). */
.fly-slot { display: inline-block; width: 0; height: 1em; vertical-align: -0.12em; }
.motion .is-fly-source .fly-slot { width: 1.1em; height: 1.1em; margin-inline: 0.06em; }
.fly-track { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(140px, 40%), 1fr)); gap: var(--space-m); }
.fly-cell { aspect-ratio: 1; }
.fly-cell > img, .fly-cell > video { position: relative; z-index: 2; width: 100%; height: 100%; object-fit: cover;
  border-radius: 24%; transform-origin: 0 0; will-change: transform; }
.motion [data-scene~="fly"]:not(.is-flying):not(.scene-off) .fly-cell > * { visibility: hidden; }   /* until the scene places them in the slots */

/* Tactile type (weight): hidden until split so it never flashes at the wrong weight */
.motion [data-scene~="weight"]:not(.is-split):not(.scene-off) { visibility: hidden; }

/* Reactive environment (react): accent tint over each [data-react], plus the optional invert dot */
.react { position: relative; }
.is-reactive [data-react] { position: relative; }
.is-reactive [data-react]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: var(--react-color, var(--color-accent)); mix-blend-mode: color; opacity: var(--react, 0); }
.react-dot { position: absolute; left: 0; top: 0; z-index: 5; width: 18px; height: 18px; margin: -9px 0 0 -9px; border-radius: 50%;
  background: #fff; mix-blend-mode: difference; pointer-events: none; opacity: 0; transition: opacity .2s; }
.react-dot.is-on { opacity: 1; }
