/* ============================================================
   Dani Maldonado · portfolio v3
   Type: Fraunces variable (opsz, ital, SOFT, WONK) + JetBrains Mono
   Color: 60/30/10 modular per surface
   Motion: editorial pacing, reduced-motion honored
   ============================================================ */

:root {
  --paper: #FAFAF7;
  --ink: #0F0F0F;
  /* RGB triple behind --ink, so ink-based hairlines/tints can flip wholesale
     when a surface inverts (e.g. a dark-themed case). */
  --ink-rgb: 15, 15, 15;
  --orange: #FE5000;
  --starry: #FFD300;
  --lime: #B8E000;

  --hairline: rgba(var(--ink-rgb), 0.15);
  --ink-50: rgba(var(--ink-rgb), 0.5);
  --ink-70: rgba(var(--ink-rgb), 0.7);

  --gutter: clamp(20px, 6vw, 12vw);

  /* ── Spacing — hybrid 8-step, 16px magic unit ── */
  --space-3xs: 4px;
  --space-2xs: 8px;
  --space-xs: 12px;
  --space-sm: 16px;   /* magic unit */
  --space-md: 24px;
  --space-lg: 32px;
  --space-xl: 48px;
  --space-2xl: 64px;
  --space-3xl: 96px;  /* section folds */
  --space-4xl: 128px;
  --space-5xl: 160px;

  /* Semantic: vertical rhythm between case-study chapters (complexity /
     clarity / beauty). Fixed so the between-block gap is a consistent
     2 × --fold-pad-y, not viewport-height-driven. */
  --fold-pad-y: var(--space-3xl);
  /* Semantic: gap from a chapter eyebrow to the content it labels. One token
     so every chapter reads the same, with no additive margin stacking. */
  --eyebrow-gap: var(--space-lg);

  /* ── Radius ── */
  --radius-xs: 2px;
  --radius-sm: 8px;
  --radius-pill: 999px;
  --radius-circle: 50%;

  /* ── Fluid type roles — clamp min/max snapped to scale, vw term verbatim ── */
  --fs-micro: clamp(10px, 0.8vw, 12px);
  --fs-mono: clamp(11px, 0.85vw, 13px);
  --fs-meta: clamp(13px, 1vw, 16px);
  --fs-body: clamp(16px, 1.4vw, 20px);
  --fs-lede: clamp(18px, 1.9vw, 24px);
  --fs-title: clamp(20px, 2vw, 28px);   /* masthead name / anchor intro */
  --fs-h-sm: clamp(20px, 2.4vw, 32px);
  --fs-h-md: clamp(28px, 3.4vw, 44px);
  --fs-h-lg: clamp(28px, 4.4vw, 60px);
  --fs-h-xl: clamp(32px, 4.6vw, 72px);
  --fs-display: clamp(40px, 6vw, 80px);
  --fs-hero: clamp(48px, 9vw, 144px);

  --ease-out-emph: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.83, 0, 0.17, 1);
}

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

html,
body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Fraunces", Georgia, serif;
  font-weight: 400;
  font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: "ss01", "ss02";
}

body {
  min-height: 100vh;
  /* No persistent header chrome. No global cursor effects. */
  cursor: auto;
}

/* Legacy cursor system retired — global spotlight cursor handles it */

/* Mono utility */
.mono {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-mono);
  letter-spacing: 0.04em;
  text-transform: lowercase;
  font-weight: 400;
  color: var(--ink-50);
}

.mono--ink {
  color: var(--ink);
}

.mono--hi {
  color: var(--ink-70);
}

a {
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

a.mono {
  display: inline-block;
  transition: color 220ms var(--ease-out-emph);
}

a.mono:hover {
  color: var(--orange);
}

/* Routed screens ---------------------------------------------- */

.screen {
  display: none;
  min-height: 100vh;
  width: 100%;
  position: relative;
}

.screen.is-active {
  display: block;
}

/* 200ms paper pause between routes — §6.8 */
.route-cover {
  position: fixed;
  inset: 0;
  background: var(--paper);
  z-index: 9998;
  opacity: 0;
  pointer-events: none;
  transition: opacity 80ms linear;
}

.route-cover.is-on {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0ms linear;
}

/* ============================================================
   HOME
   ============================================================ */

.home {
  background: var(--paper);
}

.top-strip {
  position: absolute;
  top: clamp(24px, 3vh, 32px);
  left: var(--gutter);
  right: var(--gutter);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  z-index: 5;
}

.masthead {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  line-height: 1;
}

.masthead-name {
  font-family: "Fraunces", Georgia, serif;
  font-size: var(--fs-title);
  line-height: 1;
  letter-spacing: -0.022em;
  font-style: italic;
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 50, "WONK" 0;
  color: var(--ink);
}

.masthead-role {
  color: var(--ink-50);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
}

.top-strip .nav {
  display: flex;
  gap: clamp(16px, 2vw, 24px);
}

.top-strip .nav .sep {
  color: var(--ink-50);
}

/* Fold 1 — anchor */

.fold-anchor {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: clamp(64px, 9vh, 96px) var(--gutter) clamp(48px, 6vh, 64px);
  position: relative;
}

.anchor-intro {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-size: var(--fs-title);
  line-height: 1;
  letter-spacing: -0.022em;
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 50, "WONK" 0;
  color: var(--ink);
  /* margin-top: auto + nav's margin-top: auto split the free space evenly,
     centering the intro+h1 group vertically while keeping nav pinned at the
     viewport bottom. */
  margin: auto 0 clamp(16px, 2vh, 24px);
  /* Intro: drops in from above with a slight overshoot. Final state
     applied by applyHeroFinalState() for reduced-motion / returning
     visitors / touch. */
  opacity: 0;
  transform: translateY(-12px);
  transition:
    opacity 480ms cubic-bezier(0.4, 0, 0.2, 1),
    transform 600ms cubic-bezier(0.34, 1.36, 0.64, 1);
}

.anchor-intro.is-on {
  opacity: 1;
  transform: none;
}

.anchor-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: clamp(24px, 3vw, 48px);
  width: 100%;
  /* The nav used to be the flow's last item with margin-top:auto — its bottom
     auto-margin (paired with .anchor-intro's top auto-margin) centered the
     intro+headline group. Now the nav is fixed (out of flow), so this element
     carries the bottom auto-margin instead, keeping the group centered. */
  margin-bottom: auto;
}

/* Primary nav lives in the upper-right corner of the home, fixed so it stays
   reachable across the hero and the decisions fold. It fades in once the intro
   has settled (body.hero-ready) — leaving the lower-left free for the reel
   teaser. */
.home-nav {
  position: fixed;
  top: clamp(20px, 3vh, 32px);
  right: var(--gutter);
  z-index: 40;
  display: flex;
  gap: clamp(16px, 2vw, 24px);
  align-items: baseline;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 480ms var(--ease-out-emph),
              transform 480ms var(--ease-out-emph);
}

body.hero-ready.on-home .home-nav {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* ------------------------------------------------------------
   Anchor: spotlight reveal
   ------------------------------------------------------------ */

@property --disc-r {
  syntax: '<length-percentage>';
  initial-value: 0px;
  inherits: true;
}

@property --mx {
  syntax: '<length-percentage>';
  initial-value: -200px;
  inherits: true;
}

@property --my {
  syntax: '<length-percentage>';
  initial-value: -200px;
  inherits: true;
}

.fold-anchor {
  position: relative;
  overflow: hidden;
}

.anchor {
  position: relative;
  z-index: 2;
  font-size: var(--fs-hero);
  line-height: 1.05;
  letter-spacing: -0.022em;
  font-variation-settings: "opsz" 144, "SOFT" 30, "WONK" 0;
  margin: 0;
  max-width: 18ch;
  text-wrap: balance;
}

.anchor-line {
  display: block;
}

/* Forced break after "complex" so the headline always reads
   "I make complex" / "things clear" from tablet up to ultra-wide cinema
   screens. Without it, the sentence collapses onto a single line on big
   monitors and the (wider) particle "complex" overlaps "things". Hidden
   on mobile (<720px), where the layout is handled separately/later. */
.anchor-cut { display: none; }
@media (min-width: 720px) {
  .anchor-cut { display: inline; }
}

.anchor-line.two {
  /* Override the h1 clamp — line 2 is intentionally smaller (52px fixed)
     to sit as a secondary line under the primary statement. Base color
     is ink (black); the last word ("beautiful") flips to orange. */
  font-size: 52px;
  line-height: 1.1;
  margin-top: 0.4em;
  color: var(--ink);
  font-style: italic;
  font-variation-settings: "opsz" 60, "ital" 1, "SOFT" 50, "WONK" 0;
}

/* Word weights: "and clear things" semibold, "beautiful" bold + orange. */
.anchor-line.two .word {
  font-weight: 600; /* semibold */
}
.anchor-line.two .word:last-of-type {
  font-weight: 700;        /* bold — "beautiful" + its trailing period */
  color: var(--orange);
}

.anchor-line em {
  font-style: italic;
  font-variation-settings: "opsz" 144, "ital" 1, "SOFT" 40, "WONK" 0;
}

/* Mobile (<720px) hero fold sizing. The h1 statement stays as-is for now;
   only the secondary type shrinks: the greeting -30%, line two -50%. */
@media (max-width: 720px) {
  .anchor-intro {
    /* clamp resolves to its 20px floor on phones → 20 * 0.7 = 14px */
    font-size: 14px;
  }
  .anchor-line.two {
    /* 52px base → 50% smaller */
    font-size: 26px;
  }
}

/* Cinematic reveal: clip-path wipes the text in from the left (like a
   curtain pull) instead of a plain opacity fade. The negative insets
   on top/right/bottom/left extend the visible area beyond the box,
   which is critical for italic text — descenders and slanted ascenders
   overhang the bounding box and would otherwise crop ("clear" missing
   the r, "beautiful" missing the period, etc). */
.anchor .reveal {
  display: inline-block;
  opacity: 0;
  clip-path: inset(-20% 100% -20% -15%);
  -webkit-clip-path: inset(-20% 100% -20% -15%);
  transform: translateY(8px);
  transition:
    clip-path 700ms cubic-bezier(0.77, 0, 0.175, 1),
    -webkit-clip-path 700ms cubic-bezier(0.77, 0, 0.175, 1),
    opacity 280ms cubic-bezier(0.4, 0, 0.2, 1),
    transform 600ms cubic-bezier(0.34, 1.16, 0.64, 1);
}

.anchor .reveal.is-on {
  opacity: 1;
  clip-path: inset(-20% -15% -20% -15%);
  -webkit-clip-path: inset(-20% -15% -20% -15%);
  transform: translateY(0);
}

/* Letter-by-letter INK reveal (cinematic intro). Any element tagged
   .lettered is shown plainly (no clip-path wipe) and each glyph fades
   opacity 0→100% + blur bleed→sharp, driven by GSAP. Works on line-1 words,
   line-2 words and the greeting alike. */
.lettered {
  opacity: 1 !important;
  clip-path: none !important;
  -webkit-clip-path: none !important;
  transform: none !important;
}
.lettered .ltr {
  display: inline-block;
  opacity: 0;
  filter: blur(6px);            /* ink bleed → sharpens to 0 on reveal */
  white-space: pre;
  will-change: opacity, filter;
}

/* Line 2 — each word wipes in from the left with a hint of vertical
   travel. Same generous insets so italic overhangs don't crop. */
.anchor-line.two .word {
  display: inline-block;
  opacity: 0;
  clip-path: inset(-25% 100% -25% -15%);
  -webkit-clip-path: inset(-25% 100% -25% -15%);
  transform: translateY(12px);
  transition:
    clip-path 600ms cubic-bezier(0.77, 0, 0.175, 1),
    -webkit-clip-path 600ms cubic-bezier(0.77, 0, 0.175, 1),
    opacity 280ms cubic-bezier(0.4, 0, 0.2, 1),
    transform 550ms cubic-bezier(0.34, 1.16, 0.64, 1);
}

.anchor-line.two .word.is-on {
  opacity: 1;
  clip-path: inset(-25% -15% -25% -15%);
  -webkit-clip-path: inset(-25% -15% -25% -15%);
  transform: translateY(0);
}

.anchor .and {
  display: inline-block;
  transition: font-variation-settings 240ms ease-out;
}

.anchor-line.two:hover .and {
  font-variation-settings: "opsz" 144, "ital" 1, "SOFT" 80, "WONK" 1;
}

/* Replace the font glyph with a perfect circle. The `.` character itself
   is pushed off-screen via text-indent so that currentColor still resolves
   correctly from the inherited color, and the background follows it.
   (line 1 no longer has a static period — the spotlight cursor IS the
   period after intro. This rule kept for any future use + line 2's
   two-period if we choose to circle-ify it later.) */
.anchor-line.one .period {
  /* Desktop: the spotlight cursor IS the period after the intro, so the static
     dot stays hidden. Touch devices (no cursor) re-enable it below and the
     intro finale settles the orange protagonist here. */
  display: none;
  width: 0.18em;
  height: 0.18em;
  border-radius: var(--radius-circle);
  background-color: currentColor;
  overflow: hidden;
  text-indent: 100%;
  white-space: nowrap;
  vertical-align: baseline;
  margin-left: 0.04em;
  transform-origin: 50% 50%;
  color: inherit;
  opacity: 0;
  transition: color 400ms var(--ease-out-emph),
              background-color 400ms var(--ease-out-emph),
              opacity 400ms var(--ease-out-emph);
}
@media (hover: none) {
  .anchor-line.one .period { display: inline-block; }
}
.anchor-line.one .period.is-on { opacity: 1; }

.anchor-line.one .period.is-signed {
  color: var(--orange);
  animation: period-call 600ms ease-out 1 both;
}

@keyframes period-call {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 rgba(254, 80, 0, 0);
  }
  50% {
    transform: scale(1.18);
    box-shadow: 0 0 22px rgba(254, 80, 0, 0.30);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 rgba(254, 80, 0, 0);
  }
}

/* ---- Particle word "complex" ---- */
.complex-word {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}

.cw-text {
  display: inline-block;
  color: inherit;
}

.fold-anchor[data-mode="spotlight"] .cw-text {
  color: transparent;
}

.cw-particles {
  /* Full-viewport canvas so particles can scatter across the entire
     screen on abrupt mouse movement — no local clipping. The canvas
     is drawn at viewport-relative coords (no offset math needed). */
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms ease-out;
  /* Sits above default layout but below the spotlight reveal (z-index
     9990) so the disc still covers the particles when it passes over. */
  z-index: 5;
}

.fold-anchor[data-mode="spotlight"] .cw-particles {
  opacity: 1;
}

/* ===== Mode 1: SPOTLIGHT ============================================== */

.anchor-back {
  position: fixed;
  inset: 0;
  z-index: 9990;
  pointer-events: none;
  display: none;
}

.anchor-back-disc {
  position: absolute;
  inset: 0;
  background: var(--orange);
}

/* Soft orange halo around the disc — only during the intro choreography,
   where the disc is bigger and benefits from depth. Post-intro the disc
   is just a small 10px cursor and the halo would be noise. */
.fold-anchor.is-intro .anchor-back-disc {
  filter: drop-shadow(0 6px 14px rgba(254, 80, 0, 0.28));
}

/* Line 2 ("And clear things beautiful") is now part of the sentence —
   visible in spotlight mode too. Word-by-word fade-in handled by .word
   opacity rules + intro choreographer. */
.fold-anchor[data-mode="spotlight"] .anchor-back {
  -webkit-mask-image: radial-gradient(circle var(--disc-r) at var(--mx) var(--my), #000 96%, transparent 100%);
  mask-image: radial-gradient(circle var(--disc-r) at var(--mx) var(--my), #000 96%, transparent 100%);
}

/* Post-intro: the cursor stays small (10px ø) at all times. No grow-on-h1
   behavior — the orange spotlight is purely the page cursor now, not a
   reveal mechanic. (Was: clamp(110px, 14vw, 180px) during anchor hover.) */
body.gcursor-on.cur-anchor {
  --disc-r: 10px;
}

/* anchor-back is the global spotlight cursor while activated, AND during
   the intro choreography (so the orange disc shows even on touch devices
   that never set body.gcursor-on). */
body.gcursor-on .fold-anchor[data-mode="spotlight"] .anchor-back { display: block; }
.fold-anchor.is-intro[data-mode="spotlight"] .anchor-back { display: block; }
body.gcursor-on.cur-carousel .fold-anchor[data-mode="spotlight"] .anchor-back { display: none; }

/* Hide the small gcursor element while the intro plays — the WebGL orange
   dot is the only spotlight during the choreography. !important + the
   is-glintro rule below beat the later `body.gcursor-on .gcursor{opacity:1}`
   so a moved mouse can't leak a second orange dot into the scene. */
body.is-intro .gcursor { opacity: 0 !important; }

body {
  --mx: -200px;
  --my: -200px;
  --disc-r: 10px;
}

body.gcursor-on {
  /* enabled — values driven from JS */
}

body.gcursor-on.cur-anchor {
  /* target value computed in JS for smooth tween */
}

/* ---- Spotlight cursor styling ---- */
.fold-anchor[data-mode="spotlight"] { cursor: default; }
.fold-anchor[data-mode="spotlight"] .anchor-line.one .period { cursor: pointer; }
body.gcursor-on,
body.gcursor-on * { cursor: none; }

.anchor-hint {
  opacity: 0;
  pointer-events: none;
  transition: opacity 300ms ease-out;
}

.anchor-hint.is-shown {
  opacity: 1;
}

.anchor-hint.is-hiding {
  opacity: 0;
  transition: opacity 200ms ease-out;
}

@media (hover: none) {
  .anchor-hint {
    opacity: 1;
    transition: none;
  }
}

/* ---- Replay intro button (revealed after intro completes) ---- */
.anchor-replay {
  position: absolute;
  right: clamp(16px, 2vw, 24px);
  bottom: clamp(16px, 2vh, 24px);
  width: 32px;
  height: 32px;
  border-radius: var(--radius-circle);
  border: 1px solid var(--ink-30, rgba(var(--ink-rgb), 0.18));
  background: transparent;
  color: var(--ink-60, rgba(var(--ink-rgb), 0.55));
  font-size: 16px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity 320ms ease-out 200ms,
              color 200ms ease-out,
              border-color 200ms ease-out,
              transform 200ms ease-out;
  z-index: 3;
  -webkit-tap-highlight-color: transparent;
}

.anchor-replay[hidden] {
  display: none !important;
}

.anchor-replay.is-shown {
  opacity: 1;
}

.anchor-replay:hover,
.anchor-replay:focus-visible {
  color: var(--orange);
  border-color: var(--orange);
  transform: rotate(-90deg);
  outline: none;
}

/* ---- Intro: physics scene canvases (2D) ---- */
/* Gray "multiplier ball" field sits BEHIND the headline so the text reads
   on top of it as it inks in. */
.intro-balls {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100svh;
  pointer-events: none;
  z-index: 1;             /* behind the headline (.anchor is z-index 2) */
  opacity: 0;             /* JS fades in on intro start, out at handoff */
  transition: opacity 520ms ease-out;
}
/* The orange spotlight-dome sits IN FRONT of the headline so it passes over
   the text, but below the cursor/disc. */
.intro-dome {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100svh;
  pointer-events: none;
  z-index: 9985;
  opacity: 0;
  transition: opacity 520ms ease-out;
}

/* Film grain — subtle moving texture during the intro for cinematic feel.
   Multiply blend darkens the off-white paper just slightly. */
.intro-grain {
  position: fixed;
  inset: -50%;            /* oversized so the drift never reveals an edge */
  pointer-events: none;
  z-index: 9986;
  opacity: 0;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%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)' opacity='0.5'/%3E%3C/svg%3E");
  will-change: opacity, transform;
  display: none;
}
/* Skip control — discreet, bottom-center, only during play */
.intro-skip {
  position: fixed;
  left: 50%;
  bottom: clamp(24px, 4vh, 48px);
  transform: translateX(-50%) translateY(6px);
  z-index: 9992;
  font-family: var(--mono-font, ui-monospace, "SF Mono", Menlo, monospace);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--ink-50, rgba(var(--ink-rgb), 0.5));
  background: transparent;
  border: 0;
  padding: var(--space-2xs) var(--space-2xs);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 360ms ease-out, color 200ms ease-out, transform 360ms ease-out;
  -webkit-tap-highlight-color: transparent;
}
.intro-skip[hidden] { display: none; }
.intro-skip.is-shown {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}
.intro-skip:hover,
.intro-skip:focus-visible {
  color: var(--orange);
  outline: none;
}
.intro-skip span {
  display: inline-block;
  transition: transform 200ms ease-out;
}
.intro-skip:hover span { transform: translateX(3px); }

/* ============================================================
   Global cursor (orange spotlight follower) + carousel + mobile hold
   ============================================================ */

.gcursor {
  /* One token behind the dot, so anything that needs the dot's current colour
     (the is-ring hollow, below) tracks the per-screen swap without restating
     every screen's rule. */
  --gc-color: var(--orange);
  position: fixed;
  top: 0; left: 0;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: var(--radius-circle);
  background: var(--gc-color);
  pointer-events: none;
  z-index: 9991;
  opacity: 0;
  transform: translate3d(-200px, -200px, 0);
  transition:
    opacity 240ms ease-out,
    width 380ms cubic-bezier(0.16, 1, 0.3, 1),
    height 380ms cubic-bezier(0.16, 1, 0.3, 1),
    margin 380ms cubic-bezier(0.16, 1, 0.3, 1),
    background 240ms ease-out,
    box-shadow 240ms ease-out;
  will-change: transform;
  overflow: hidden;
}

body.gcursor-on .gcursor { opacity: 1; }

/* On a job page the cursor dot inherits the job's color (Starry = yellow).
   The .gcursor lives outside .case, so the color is keyed off the
   body's per-screen class. Home keeps the orange identity dot. The base
   `background 240ms` transition makes the orange↔job swap smooth on nav. */
body.on-starry .gcursor { --gc-color: var(--starry); }

/* (Removed: cur-anchor used to hide the small dot in favor of the big
   anchor-back disc. Now the cursor stays small at 10px ø regardless of
   hover target, so the gcursor element is always visible.) */

body.gcursor-on.cur-carousel .gcursor {
  width: 240px;
  height: 240px;
  margin: -120px 0 0 -120px;
  background: var(--spotlight-color, var(--starry));
  opacity: 1;
}

/* Dim the spotlight when it passes over title/client text so the type
   underneath stays legible. Higher specificity beats the cur-carousel
   opacity:1 rule above. */
body.gcursor-on .gcursor.is-dimmed,
body.gcursor-on.cur-carousel .gcursor.is-dimmed {
  opacity: 0.5;
}

/* Over a small labelled control the dot hollows out into a ring. `cursor: none`
   is set site-wide, so this dot is the only pointer there is — a solid 20px of
   it parked on a control means aiming at something you can't see. The ring
   keeps the dot's presence and hands the middle back, so the label and its
   arrow stay readable while you click them. --gc-color keeps it the dot's own
   colour on every screen. */
body.gcursor-on .gcursor.is-ring {
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--gc-color);
}

.gcursor-carousel {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.gcursor-carousel .ci {
  position: absolute;
  inset: 14%;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  opacity: 0;
  transition: opacity 700ms ease-in-out;
}

.gcursor-carousel .ci.is-on { opacity: 1; }

/* Uploaded gallery images fill the disc (vs the contained product PNGs). */
.gcursor-carousel .ci.ci-img {
  inset: 0;
  background-size: cover;
  border-radius: var(--radius-circle);
}

/* Fold 2 — Selected decisions */

.fold-decisions {
  padding: clamp(96px, 14vh, 160px) var(--gutter);
}

.decisions-intro {
  font-family: "Fraunces", Georgia, serif;
  font-size: var(--fs-lede);
  line-height: 1.32;
  letter-spacing: -0.012em;
  color: var(--ink-70);
  max-width: 44ch;
  margin: 12px 0 clamp(48px, 6vh, 64px);
  text-wrap: balance;
}

.decisions {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 0.5px solid var(--hairline);
}

.decision {
  --row-color: var(--ink-50);
  border-bottom: 0.5px solid var(--hairline);
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: clamp(24px, 3vw, 48px);
  align-items: center;
  padding: clamp(24px, 2.4vw, 32px) clamp(16px, 2vw, 24px);
  transition: border-color 220ms ease-out;
}

.decision:hover {
  border-bottom-color: rgba(var(--ink-rgb), 0.55);
}

/* Make the whole row clickable. Cursor: pointer only when the gcursor
   spotlight isn't taking over (otherwise the OS pointer would show on
   top of the orange spotlight, doubling cursors). */
body:not(.gcursor-on) .decision[data-carousel] {
  cursor: pointer;
}

/* Hover wash: the work takes on its OWN accent color (the case color that
   travels with the decision — Adendo §05), fading in from the right edge.
   Lives on a ::after pseudo so the row's grid layout and content stay
   untouched. */
.decision[data-carousel] {
  position: relative;
  isolation: isolate;
}

.decision[data-carousel]::after {
  content: "";
  position: absolute;
  /* Gradient extends to the row's full edges — the row's horizontal
     padding keeps the content (dot, num, title, client) inset so the
     dot floats over the gradient with breathing room. */
  inset: 0;
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--case-row-color, var(--orange)) 24%, transparent) 0%,
    transparent 62%
  );
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms ease-out;
  z-index: -1;
}

.decision[data-carousel]:hover::after {
  opacity: 1;
}

.decision .num {
  color: var(--row-color);
  transition: color 200ms ease-out;
  font-variant-numeric: tabular-nums;
}

.decision:hover .num {
  color: var(--case-row-color, var(--orange));
}

.decision .decision-title {
  font-size: var(--fs-h-sm);
  line-height: 1.18;
  letter-spacing: -0.01em;
  font-style: italic;
  font-variation-settings: "opsz" 72, "ital" 1, "SOFT" 35;
  font-weight: 400;
  margin: 0;
  color: var(--ink);
  max-width: 22ch;
}

.decision .client {
  color: var(--ink-50);
  white-space: nowrap;
}

.decision .dot {
  width: 11px;
  height: 11px;
  border-radius: var(--radius-pill);
  background: var(--case-row-color, var(--hairline));
  display: inline-block;
  align-self: center;
}

.decision.is-locked .decision-title,
.decision.is-locked .client {
  color: var(--ink-50);
  font-style: italic;
  font-variation-settings: "opsz" 72, "ital" 1, "SOFT" 35;
}

.decision a.row-link {
  display: contents;
}

/* Mobile: stack the mono client label beneath the title phrase, so the serif
   title gets the full row width instead of being squeezed into the 1fr column
   (which forced one-word-per-line wrapping). */
@media (max-width: 720px) {
  .decision {
    grid-template-columns: auto 1fr;
    column-gap: var(--space-md);
    row-gap: var(--space-2xs);
    align-items: start;
  }
  .decision .num            { grid-column: 1; grid-row: 1; }
  .decision .decision-title { grid-column: 2; grid-row: 1; max-width: none; }
  .decision .client         { grid-column: 2; grid-row: 2; white-space: normal; }
  .decision .dot            { grid-column: 1; grid-row: 2; align-self: start; }
}

/* Fold 3 — About teaser + email */



.email {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-meta);
  letter-spacing: 0.02em;
  color: var(--ink);
  transition: color 220ms var(--ease-out-emph);
}

.email:hover {
  color: var(--orange);
}

/* ============================================================
   STARRY case study
   60% = #FFD300 · 30% = ink · 10% = orange
   ============================================================ */

.case {
  /* Entering a job, the accent IS the job's color (Starry = yellow): cursor,
     pagination, eyebrows, fold, outcome, next — everything inherits it. Page
     stays paper (Adendo §05). Trade-off: the job color as TEXT on paper is
     low-contrast — a deliberately quiet accent. */
  --case-color: var(--starry);
  --case-accent: var(--case-color);
  /* Readable text color ON the case color (not on the page surface). Default
     dark suits light accents (Starry yellow); cases whose color is itself dark
     override it (see DEW below). Single source of truth for on-color contrast. */
  --case-ink: #0F0F0F;
  --case-accent-soft: color-mix(in srgb, var(--case-color) 22%, transparent);
  background-color: var(--paper);
  color: var(--ink);
  /* The open animation fills the overlay with the case color; once the page
     settles it dissolves to its resting surface (--paper). */
  transition: background-color 900ms ease-in-out;
}

/* Per-case theme intelligence: paper is the default surface. When the job's
   color has no contrast on paper (e.g. Starry's yellow), the case flips to a
   dark surface so the accent becomes the protagonist. We invert the surface
   and the ink tokens wholesale; the accent (--case-color) is untouched, so it
   now pops. Add `is-dark` to a case whose color fails on paper. */
.case.is-dark {
  --paper: #0F0F0F;            /* surface → near-black */
  --ink: #FAFAF7;              /* text → paper */
  --ink-rgb: 250, 250, 247;    /* in-rule ink hairlines/tints flip light */
  /* --ink-50/70 and --hairline nest var(--ink-rgb) but are declared at :root,
     so they resolve there and don't re-flip — redefine them directly. */
  --ink-50: rgba(250, 250, 247, 0.5);
  --ink-70: rgba(250, 250, 247, 0.7);
  --hairline: rgba(250, 250, 247, 0.15);
}

/* During the case-color flash (open expand / close collapse) the surface IS
   the job color, so text must read on it — force ink dark regardless of the
   case theme. Resolves back to the theme's ink the instant the flash ends. */
.case.is-active.is-collapsed,
.case.is-active.is-expanding,
.case.is-active.morph-target,
.case.is-active.is-collapsing {
  --ink: #0F0F0F;
  --ink-rgb: 15, 15, 15;
  --ink-50: rgba(15, 15, 15, 0.5);
  --ink-70: rgba(15, 15, 15, 0.7);
  --hairline: rgba(15, 15, 15, 0.15);
}

/* Surfaces painted with the case color (the can tile, the current thumb) carry
   text in --case-ink, so it reads ON that color regardless of the page theme —
   including non-is-dark cases whose color is itself dark (e.g. DEW blue). The
   label/num stay muted; the name reads at full strength. */
.case .beauty-tile.has-can .tile-label,
.case .thumb-current .mono {
  color: color-mix(in srgb, var(--case-ink) 55%, transparent);
}

.case .thumb-current .thumb-name {
  color: var(--case-ink);
}

/* ============================================================
   Per-case identity — accent color + theme per job.
   Starry keeps the base --case-color (yellow). Each job's accent is its own;
   the theme (dark vs paper) follows the contrast rule via the `is-dark` class
   in markup: yellow / orchid / champagne have no contrast on paper → dark; the
   DEW blue placeholder does → paper (no is-dark). Colors §05 a definir.
   ============================================================ */
#screen-dew   { --case-color: #2D52EE; --case-ink: #FAFAF7; }   /* cor §05 a definir · royal-blue placeholder · paper theme · dark color → light ink */
#screen-brisk { --case-color: #E472DE; --case-ink: #0F0F0F; }   /* cor §05 a definir · orchid placeholder · dark theme · light color → dark ink */
#screen-fdp   { --case-color: #E4D2B4; --case-ink: #0F0F0F; }   /* cor §05 a definir · champagne placeholder · dark theme · light color → dark ink */

/* Cursor inherits each job's color (the .gcursor lives outside the screen).
   Through --gc-color rather than background, so the is-ring hollow can draw
   itself in the same colour without restating a rule per case. */
body.on-dew   .gcursor { --gc-color: #2D52EE; }
body.on-brisk .gcursor { --gc-color: #E472DE; }
body.on-fdp   .gcursor { --gc-color: #E4D2B4; }

/* DEW's accent (blue placeholder) is dark, so its open/close flash needs LIGHT
   text — overrides the default dark-on-flash (which suits the light accents). */
#screen-dew.is-active.is-collapsed,
#screen-dew.is-active.is-expanding,
#screen-dew.is-active.morph-target,
#screen-dew.is-active.is-collapsing {
  --ink: #FAFAF7;
  --ink-rgb: 250, 250, 247;
  --ink-50: rgba(250, 250, 247, 0.5);
  --ink-70: rgba(250, 250, 247, 0.7);
  --hairline: rgba(250, 250, 247, 0.15);
}

/* Hero — top padding clears the fixed masthead + toolbar (~140px tall) and
   leaves breathing room before the content. */
.case-hero {
  min-height: 100svh;
  padding: clamp(160px, 18vh, 200px) var(--gutter) clamp(32px, 5vh, 64px);
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(24px, 5vh, 48px);
}

/* Decision block (Adendo §01) — paper surface, no image, left-aligned. */
.case-decision {
  position: relative;
  z-index: 4;
  max-width: 760px;
}

.case-eyebrow {
  color: var(--case-accent);
  margin: 0 0 clamp(16px, 2.6vh, 32px);
}

.case-title {
  font-size: var(--fs-h-xl);
  line-height: 1.06;
  letter-spacing: -0.022em;
  font-style: italic;
  font-variation-settings: "opsz" 144, "ital" 1, "SOFT" 30, "WONK" 0;
  font-weight: 400;
  margin: 0;
  text-wrap: balance;
  max-width: 16ch;
}

/* Stakes line — what was at stake, not the brief (Adendo §01). */
.case-stakes {
  font-size: clamp(16px, 1.5vw, 20px);
  line-height: 1.5;
  font-variation-settings: "opsz" 24, "SOFT" 35;
  color: var(--ink-70);
  max-width: 34ch;
  margin: clamp(24px, 3vh, 32px) 0 0;
}

/* Metadata strip — BRAND · ROLE · OUTCOME. Inline row on desktop; on mobile
   each cell stacks (label over value) so the whole block fits one screen. */
.case-meta {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(16px, 2vh, 24px);
  margin: clamp(24px, 4vh, 48px) 0 0;
}

@media (min-width: 720px) {
  .case-meta {
    grid-template-columns: repeat(3, auto);
    gap: clamp(24px, 5vw, 64px);
    align-items: start;
    justify-content: start;
  }
}

.meta-cell dt {
  color: var(--ink-50);
  margin: 0 0 var(--space-2xs);
}

.meta-cell dd {
  margin: 0;
  font-size: var(--fs-meta);
  line-height: 1.35;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  max-width: 24ch;
}

/* The fold — a thin accent rule where the scroll (the reasoning) begins. */
.case-fold {
  height: 0;
  border-top: 1.5px dashed var(--case-accent);
  opacity: 0.85;
  margin-top: clamp(4px, 1.5vh, 16px);
}

@keyframes hero-can-drift {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(-1.2deg); }
  50% { transform: translate3d(0, -10px, 0) rotate(-0.4deg); }
}

/* low-gravity flow — used by the floating chrome stars inside "the beauty" */
@keyframes low-grav {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  33% { transform: translate3d(10px, -14px, 0) rotate(2deg); }
  66% { transform: translate3d(-8px, 10px, 0) rotate(-1.6deg); }
}

/* Reading rail (sticky lateral right) */
.reading-rail {
  position: fixed;
  top: 50%;
  right: clamp(12px, 2vw, 24px);
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  z-index: 6;
  width: 32px;
}

@media (max-width: 720px) {
  .reading-rail {
    display: none;
  }
}

.rail-dots {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  position: relative;
}

.rail-dots::before {
  content: "";
  position: absolute;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background: rgba(var(--ink-rgb), 0.15);
}

.rail-dot {
  display: block;
  width: 9px;
  height: 9px;
  border-radius: var(--radius-pill);
  background: rgba(var(--ink-rgb), 0.25);
  position: relative;
  z-index: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 220ms ease-out, box-shadow 320ms ease-out;
}

.rail-dot.is-past {
  background: var(--ink);
}

.rail-dot.is-current {
  background-color: var(--case-accent);
  box-shadow: 0 0 0 4px var(--case-accent-soft);
  animation: rail-pulse 3s ease-in-out infinite;
  cursor: default;
  /* Slower transition so it syncs with the 1200ms case bg fade */
  transition: background-color 1200ms ease-in-out,
              box-shadow 1200ms ease-in-out;
}

.rail-dot:not(.is-current):hover {
  background: var(--ink);
}

@keyframes rail-pulse {
  0%, 100% { opacity: 0.85; }
  50% { opacity: 1; }
}

/* Tooltip on hover — chapter name floats to the left of the rail */
.rail-dot::before {
  content: attr(data-chapter);
  position: absolute;
  right: calc(100% + 14px);
  top: 50%;
  transform: translateY(-50%);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: lowercase;
  color: var(--ink);
  background: var(--paper);
  padding: 5px 9px;
  border: 0.5px solid var(--hairline);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease-out;
  z-index: 2;
}

.rail-dot:hover::before,
.rail-dot:focus-visible::before {
  opacity: 1;
}

/* Layer 01 — complexity */
.layer-complexity {
  background: transparent;
  padding: var(--fold-pad-y) var(--gutter);
  position: relative;
}

.layer-eyebrow {
  color: var(--case-accent);
  margin: 0 0 var(--eyebrow-gap);
  transition: color 1200ms ease-in-out;
}

.complexity-intro {
  font-size: var(--fs-body);
  line-height: 1.65;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  max-width: 44ch;
  margin: 0 0 clamp(64px, 9vh, 96px);
  font-weight: 400;
}

/* Layer 02 — clarity */
.layer-clarity {
  background: transparent;
  padding: var(--fold-pad-y) var(--gutter);
}

/* The page's only pull-quote (Adendo §03). Serif, large, left-aligned;
   weight equal to the work in "the beauty". */
.clarity-quote {
  /* Gap to the eyebrow above is owned solely by --eyebrow-gap (on .layer-eyebrow),
     so the quote adds no margin-top of its own — keeps clarity consistent with
     the other chapters instead of stacking two margins. */
  margin: 0;
  max-width: 24ch;
  font-size: var(--fs-h-lg);
  line-height: 1.12;
  font-style: italic;
  font-variation-settings: "opsz" 72, "ital" 1, "SOFT" 35;
  letter-spacing: -0.015em;
  font-weight: 400;
  text-wrap: balance;
}

.clarity-quote .period {
  color: var(--case-accent);
}

/* Layer 03 — beauty tile: real can + chrome stars */
.beauty-tile.has-can {
  background: var(--case-color);
  border: 0.5px solid rgba(var(--ink-rgb), 0.18);
  position: relative;
  overflow: hidden;
  aspect-ratio: 4/3;
}

.beauty-can {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotate(-2deg);
  height: 86%;
  width: auto;
  filter: drop-shadow(0 24px 48px rgba(var(--ink-rgb), 0.22));
  animation: hero-can-drift 16s ease-in-out infinite -2s;
  z-index: 2;
}

.beauty-tile .float-star {
  position: absolute;
  pointer-events: none;
  user-select: none;
  z-index: 3;
}

.beauty-tile .float-star img { width: 100%; height: auto; display: block; }

.beauty-tile .float-star.fs-a {
  width: 22%;
  top: 8%;
  left: 6%;
  animation: low-grav 11s ease-in-out infinite;
}

.beauty-tile .float-star.fs-b {
  width: 14%;
  bottom: 14%;
  right: 8%;
  animation: low-grav 13s ease-in-out infinite -5s reverse;
}

.layer-beauty {
  background: transparent;
  padding: var(--fold-pad-y) var(--gutter);
}

.beauty-eyebrow {
  margin: 0 0 var(--eyebrow-gap);
  color: var(--case-accent);
  transition: color 1200ms ease-in-out;
}

.beauty-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(16px, 2vw, 24px);
  margin-bottom: clamp(48px, 6vh, 64px);
}

@media (min-width: 720px) {
  .beauty-grid {
    grid-template-columns: 1.4fr 1fr;
  }
}

.beauty-tile {
  background: var(--paper);
  border: 0.5px solid rgba(var(--ink-rgb), 0.15);
  aspect-ratio: 4/3;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

.beauty-tile .tile-label {
  position: absolute;
  bottom: 14px;
  left: 16px;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.05em;
  color: var(--ink-50);
  text-transform: lowercase;
}

/* Uploaded media gallery — fills the tile; replaces placeholder/art tiles. */
.beauty-tile.media-tile { padding: 0; background: #000; }
.beauty-tile.media-tile .media-el,
.beauty-tile.media-tile .media-play {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
}
.beauty-tile.media-tile img.media-el { object-fit: cover; display: block; }
.beauty-tile.media-tile video.media-el { object-fit: contain; background: #000; }
.beauty-tile.media-tile .media-play {
  border: 0;
  cursor: pointer;
  background: #000;
  display: block;
}
.beauty-tile.media-tile .media-play img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.beauty-tile.media-tile .media-play .play-icon {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 30px;
  color: #fff;
  background: rgba(0, 0, 0, 0.18);
  transition: background 0.2s ease;
}
.beauty-tile.media-tile .media-play:hover .play-icon { background: rgba(0, 0, 0, 0.34); }
.beauty-tile.media-tile .tile-label {
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--space-md) var(--space-sm) var(--space-xs);
  color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.5));
}

/* Stylized can mock */


.beauty-tile.shelf-row {
  aspect-ratio: 4/3;
  background: var(--paper);
  flex-direction: row;
  justify-content: space-evenly;
  gap: 4%;
  padding: 12% 6%;
}

.shelf-can {
  width: 12%;
  aspect-ratio: 1/2.4;
  border-radius: 8px / 6px;
  border: 0.5px solid rgba(var(--ink-rgb), 0.4);
  position: relative;
}

.shelf-can.s-1 { background: #2EB872; }
.shelf-can.s-2 { background: #66CC33; }
.shelf-can.s-3 { background: var(--starry); border-color: var(--ink); transform: translateY(-6%); }
.shelf-can.s-4 { background: #97D700; }
.shelf-can.s-5 { background: #00A551; }

.shelf-can .shelf-cap {
  position: absolute;
  inset: 0 0 auto 0;
  height: 6%;
  background: rgba(var(--ink-rgb), 0.2);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.shelf-can.s-3::after {
  content: "";
  position: absolute;
  inset: 35% 18% auto 18%;
  height: 18%;
  background: var(--ink);
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

.metrics {
  border-top: 0.5px solid rgba(var(--ink-rgb), 0.3);
  padding-top: clamp(24px, 3vh, 32px);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-sm);
}

@media (min-width: 720px) {
  .metrics {
    grid-template-columns: repeat(4, 1fr);
  }
}

.metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.metric .m-value {
  font-family: "Fraunces", serif;
  font-size: var(--fs-h-md);
  line-height: 1;
  letter-spacing: -0.02em;
  font-variation-settings: "opsz" 72, "SOFT" 30;
  font-weight: 400;
}

.metric .m-label {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: lowercase;
  color: var(--ink-70);
  line-height: 1.4;
  max-width: 22ch;
}

.case-foot {
  padding: clamp(48px, 6vh, 80px) var(--gutter) clamp(24px, 4vh, 48px);
  display: flex;
  justify-content: space-between;
  border-top: 0.5px solid rgba(var(--ink-rgb), 0.3);
}

/* ============================================================
   Case shell: masthead + toolbar + thumbs strip
   ============================================================ */

.case-masthead {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: clamp(16px, 2.4vh, 24px) var(--gutter) 12px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  z-index: 60;
  background-color: color-mix(in srgb, var(--paper) 75%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  transition: background-color 1200ms ease-in-out;
}

.case-masthead .masthead-name {
  font-family: "Fraunces", Georgia, serif;
  font-size: var(--fs-title);
  line-height: 1;
  letter-spacing: -0.022em;
  font-style: italic;
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 50, "WONK" 0;
  color: var(--ink);
}

.case-masthead .masthead-role {
  color: rgba(var(--ink-rgb), 0.6);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  position: relative;
  display: inline-block;
}

/* Hover swap: tagline morphs from "I make complex things clear." into
   "And clear things beautiful." on hover of the masthead. Both texts have
   the same character count in mono, so the swap is seamless. */
.case-masthead .masthead-role .role-base,
.case-masthead .masthead-role .role-alt {
  display: inline-block;
  transition: opacity 240ms ease-out;
}

.case-masthead .masthead-role .role-alt {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  white-space: nowrap;
}

.case-masthead .masthead:hover .role-base {
  opacity: 0;
}

.case-masthead .masthead:hover .role-alt {
  opacity: 1;
}

.case-masthead .nav {
  display: flex;
  gap: clamp(12px, 1.6vw, 24px);
  padding-top: var(--space-2xs);
}

.case-toolbar {
  position: fixed;
  top: clamp(74px, 9vh, 96px);
  left: 0;
  right: 0;
  padding: 12px var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-md);
  z-index: 59;
  background-color: color-mix(in srgb, var(--paper) 75%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border-top: 0.5px solid rgba(var(--ink-rgb), 0.25);
  border-bottom: 0.5px solid rgba(var(--ink-rgb), 0.1);
  transition: background-color 1200ms ease-in-out;
}

.case-toolbar > :first-child {
  justify-self: start;
}

.case-toolbar .case-counter {
  text-align: center;
}

/* Counter nav: wraps the 01/06 + prev/next arrows + works dropdown. */
.case-counter-wrap {
  justify-self: center;
  position: relative;
}

.case-counter-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

.counter-arrow {
  background: none;
  border: none;
  color: var(--ink);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  opacity: 0;
  transition: opacity 200ms ease-out, color 200ms ease-out;
  -webkit-tap-highlight-color: transparent;
}

.case-counter-wrap:hover .counter-arrow,
.case-counter-wrap:focus-within .counter-arrow {
  opacity: 1;
}

.counter-arrow:disabled {
  cursor: not-allowed;
  color: var(--ink-50);
}

.counter-arrow:not(:disabled):hover {
  color: var(--case-accent);
}

/* Dropdown — hover-with-intent: requires the user to dwell on the
   counter area for 1s before it appears. Leaving is instant (no delay
   on the out-transition) so the dropdown vanishes immediately when
   the cursor leaves the wrap. */
.case-dropdown {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  margin-top: var(--space-xs);
  background: var(--paper);
  border: 0.5px solid rgba(var(--ink-rgb), 0.18);
  min-width: 460px;
  max-width: 560px;
  padding: var(--space-3xs) 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 220ms ease-out,
              transform 220ms ease-out,
              visibility 0s linear 220ms;
  z-index: 70;
  box-shadow: 0 18px 36px rgba(var(--ink-rgb), 0.10);
}

.case-counter-wrap:hover .case-dropdown,
.case-counter-wrap:focus-within .case-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  /* 1s dwell before any in-animation starts */
  transition: opacity 220ms ease-out 1000ms,
              transform 220ms ease-out 1000ms,
              visibility 0s linear 1000ms;
}

.case-dropdown-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-dropdown-item {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: var(--space-sm);
  align-items: baseline;
  padding: var(--space-xs) var(--space-md);
  border-bottom: 0.5px solid rgba(var(--ink-rgb), 0.08);
  transition: background 180ms ease-out;
}

.case-dropdown-item:last-child {
  border-bottom: none;
}

.case-dropdown-item .num {
  color: var(--ink-50);
  font-variant-numeric: tabular-nums;
}

.case-dropdown-item .dropdown-title {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-size: 14px;
  line-height: 1.2;
  margin: 0;
  color: var(--ink);
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 35;
  letter-spacing: -0.005em;
}

.case-dropdown-item .client {
  color: var(--ink-50);
  white-space: nowrap;
}

.case-dropdown-item .dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: rgba(var(--ink-rgb), 0.18);
  align-self: center;
}

/* Current row — the job's own color */
.case-dropdown-item.is-current {
  background: color-mix(in srgb, var(--case-accent) 9%, transparent);
}

.case-dropdown-item.is-current .num,
.case-dropdown-item.is-current .client {
  color: var(--case-accent);
}

.case-dropdown-item.is-current .dot {
  background: var(--case-accent);
}

/* Locked rows — non-interactive, gray italic */
.case-dropdown-item.is-locked .dropdown-title,
.case-dropdown-item.is-locked .client {
  color: var(--ink-50);
}

.case-dropdown-item.is-locked .dropdown-title {
  font-style: italic;
}

/* Hover on a non-current, non-locked row (future cases) */
.case-dropdown-item:not(.is-locked):not(.is-current):hover {
  background: rgba(var(--ink-rgb), 0.04);
  cursor: pointer;
}

.case-close {
  color: var(--ink);
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  justify-self: end;
  transition: transform 220ms var(--ease-out-emph);
  -webkit-tap-highlight-color: transparent;
}

.case-close svg {
  width: 24px;
  height: 24px;
  display: block;
}

.case-close:hover {
  transform: rotate(90deg);
}

/* Title morph: the case-title slides + resizes from the row's
   .decision-title position to its natural case-hero position. Base rule
   keeps transition always declared so the morph fires when morph-target
   is removed (forward) or added (reverse). Forward's initial pin uses
   inline style="transition:none" to avoid the natural→row jump. */
.case .case-title {
  transform-origin: top left;
  transition: transform 800ms cubic-bezier(0.83, 0, 0.17, 1),
              font-size 800ms cubic-bezier(0.83, 0, 0.17, 1),
              line-height 800ms cubic-bezier(0.83, 0, 0.17, 1),
              letter-spacing 800ms cubic-bezier(0.83, 0, 0.17, 1),
              font-variation-settings 800ms cubic-bezier(0.83, 0, 0.17, 1),
              /* ease the ink↔paper flip so the dark-case yellow flash doesn't
                 snap the title color mid-dissolve */
              color 700ms ease-in-out;
  will-change: transform;
}

.case.morph-target .case-title {
  transform: translate(var(--title-dx, 0px), var(--title-dy, 0px));
  font-size: var(--fs-h-sm);
  line-height: 1.18;
  letter-spacing: -0.01em;
  font-variation-settings: "opsz" 72, "ital" 1, "SOFT" 35;
  /* Match .decision-title's wrap width AND wrap mode so the pinned case-title
     has the exact same line breaks as the row's title — otherwise the case
     title (text-wrap: balance) and the row title (normal) break in different
     places and ghost as "two elements" on the way back. */
  max-width: 22ch;
  text-wrap: wrap;
}

/* Decision-block siblings are hidden during the title morph so the pinned
   title sits cleanly in the row strip without other case content peeking in.
   Revealed by case-stage-in after the expansion. */
.case.morph-target .case-eyebrow,
.case.morph-target .case-stakes,
.case.morph-target .case-meta,
.case.morph-target .case-fold {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* Sequence footer (Adendo §04) — paper, full width: counter left, next right. */
.case-foot-counter {
  color: var(--ink-50);
}

.case-foot-next {
  color: var(--case-accent);
}

.case-foot-next.is-disabled {
  color: var(--ink-50);
  opacity: 0.6;
  pointer-events: none;
}

/* Thumbs strip at bottom */
.case-thumbs {
  background: var(--paper);
  display: grid;
  /* One horizontal row: up to ~6 works share the width; beyond that each holds a
     min width and the strip scrolls sideways, so any number of works stays
     reachable without stacking into tall rows. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(160px, 1fr);
  gap: clamp(8px, 1.2vw, 16px);
  padding: clamp(16px, 2.4vw, 24px) var(--gutter);
  border-top: 0.5px solid rgba(var(--ink-rgb), 0.2);
  overflow-x: auto;
  scrollbar-width: thin;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}
.case-thumbs > .thumb { scroll-snap-align: start; }

.thumb {
  aspect-ratio: 4/3;
  background: rgba(var(--ink-rgb), 0.06);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-xs) var(--space-xs);
  position: relative;
  border-radius: var(--radius-xs);
  transition: background 220ms ease-out;
}

.thumb-current {
  background: var(--case-color);
}

.thumb .thumb-name {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 50, "WONK" 0;
  font-size: var(--fs-meta);
  line-height: 1.1;
  letter-spacing: -0.012em;
  color: var(--ink);
}

/* Forthcoming cases — name reads as a quiet placeholder, not live work. */
.thumb-empty .thumb-name {
  color: var(--ink-50);
}

/* Clickable thumbs — on hover the box fills with the LINKED project's accent
   color (each carries its own --thumb-color / contrast ink, keyed by href so
   no per-thumb markup is needed). */
a.thumb {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
a.thumb[href$="/starry"]              { --thumb-color: #FFD300; --thumb-ink: #0F0F0F; }
a.thumb[href$="/mtn-dew-got"]         { --thumb-color: #2D52EE; --thumb-ink: #FAFAF7; } /* cor §05 a definir */
a.thumb[href$="/brisk-black-panther"] { --thumb-color: #E472DE; --thumb-ink: #0F0F0F; } /* cor §05 a definir */
a.thumb[href$="/food-deserves-pepsi"] { --thumb-color: #E4D2B4; --thumb-ink: #0F0F0F; } /* cor §05 a definir */

a.thumb .thumb-name,
a.thumb .mono {
  transition: color 220ms ease-out;
}

a.thumb:hover {
  background: var(--thumb-color, var(--case-color));
}
a.thumb:hover .thumb-name,
a.thumb:hover .mono {
  color: var(--thumb-ink, var(--ink));
}

@media (max-width: 720px) {
  .case-thumbs {
    grid-auto-columns: minmax(140px, 42%);
  }
}

/* ============================================================
   Case page open/close animation — clip-path from row strip
   ============================================================ */

.case {
  --row-top: 0px;
  --row-h: 100vh;
}

.case.is-active.is-collapsed {
  clip-path: inset(var(--row-top) 0 calc(100vh - var(--row-top) - var(--row-h)) 0);
  -webkit-clip-path: inset(var(--row-top) 0 calc(100vh - var(--row-top) - var(--row-h)) 0);
  opacity: 0;
  animation: case-overlay-fade-in 280ms ease-out forwards;
  /* Fill with the case color the moment the overlay appears (no fade-in here);
     it dissolves to paper later, when the .case base rule takes over. */
  background-color: var(--case-color);
  transition: none;
}

@keyframes case-overlay-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.case.is-active.is-expanding {
  clip-path: inset(0 0 0 0);
  -webkit-clip-path: inset(0 0 0 0);
  /* Stay filled with the case color through the expand (no bg change here, so
     the clip-path transition is the only thing animating). */
  background-color: var(--case-color);
  transition: clip-path 1100ms cubic-bezier(0.77, 0, 0.18, 1),
              -webkit-clip-path 1100ms cubic-bezier(0.77, 0, 0.18, 1);
}

/* The way back (reverse of the open). While the title morphs natural → row
   (morph-target, ~800ms), the bg dissolves paper → case color via the .case
   base transition — the mirror of the open's color → paper dissolve. On the
   OPEN this rule is harmless: morph-target there coexists with is-collapsed
   (transition: none), so the fill is instant, and it's dropped at stage-in
   exactly when paper takes over. */
.case.is-active.morph-target {
  background-color: var(--case-color);
}

.case.is-active.is-collapsing {
  clip-path: inset(var(--row-top) 0 calc(100vh - var(--row-top) - var(--row-h)) 0);
  -webkit-clip-path: inset(var(--row-top) 0 calc(100vh - var(--row-top) - var(--row-h)) 0);
  /* Stay filled with the case color through the collapse. */
  background-color: var(--case-color);
  transition: clip-path 1100ms cubic-bezier(0.77, 0, 0.18, 1),
              -webkit-clip-path 1100ms cubic-bezier(0.77, 0, 0.18, 1);
  /* In the last ~280ms of the collapse, fade the whole overlay out so the
     case-color bg and pinned title soften their disappearance. The home row
     text is already visible underneath (z-index keeps the overlay on top from
     t=0), so it "fades in" naturally as the overlay fades out. */
  animation: case-collapse-tail 280ms ease-out 820ms forwards;
}

@keyframes case-collapse-tail {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Masthead + toolbar fade in after the yellow expansion settles */
.case.is-active.is-collapsed .case-masthead,
.case.is-active.is-collapsed .case-toolbar,
.case.is-active.is-expanding .case-masthead,
.case.is-active.is-expanding .case-toolbar {
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
}

.case .case-masthead,
.case .case-toolbar {
  transition: opacity 360ms ease-out 700ms,
              transform 360ms ease-out 700ms;
}

.case.is-active.is-collapsing .case-masthead,
.case.is-active.is-collapsing .case-toolbar {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 220ms ease-out, transform 220ms ease-out;
}

/* ============================================================
   ABOUT
   ============================================================ */

.about {
  background-color: var(--paper);
  /* The contact flow may darken the page (focus) or flash green
     (success); transition the bg so the shift is smooth. */
  transition: background-color 600ms ease-in-out;
}

/* Focus mode: clicking "get in touch" darkens the page so the paper
   form panel reads as a bright focal point. */
.about.is-contact-focus {
  background-color: #0F0F0F;
}

/* Success mode: form submit flashes the bg to a softer celebratory
   green — desaturated so it feels affirming rather than alarming. */
.about.is-contact-success {
  background-color: #5BAD7A;
}

/* Use the case-masthead structure on about, but with a paper-tinted
   backdrop instead of the case's yellow/green surface. */
.about-masthead {
  background-color: color-mix(in srgb, var(--paper) 75%, transparent);
  transition: background-color 600ms ease-in-out;
}

.about.is-contact-focus .about-masthead {
  background-color: color-mix(in srgb, #0F0F0F 75%, transparent);
}

.about.is-contact-focus .masthead-name,
.about.is-contact-focus .role-base,
.about.is-contact-focus .role-alt,
.about.is-contact-focus .about-nav a {
  color: var(--paper);
  transition: color 600ms ease-in-out;
}

.about.is-contact-focus .about-nav-current {
  color: var(--orange);
}

.about.is-contact-success .about-masthead {
  background-color: color-mix(in srgb, #5BAD7A 75%, transparent);
}

/* Orange and green have similar luminance — on the success green bg,
   orange text loses contrast. Flip orange accents to ink while the
   success state is active. The 600ms color transition is declared
   only on the .is-contact-success rule (matches the bg's 600ms fade);
   leaving success falls back to each element's own default transition
   so hover and other interactions stay snappy. */
.about.is-contact-success .about-contact-trigger,
.about.is-contact-success .about-nav-current,
.about.is-contact-success .about-coda .period.is-signed {
  color: var(--ink);
  transition: color 600ms ease-in-out;
}

.about-nav-current {
  color: var(--orange);
}

.about-body {
  max-width: 1000px;
  margin: 0 auto;
  padding: clamp(160px, 22vh, 220px) 24px clamp(96px, 14vh, 160px);
}

.about-header {
  margin-bottom: clamp(48px, 8vh, 80px);
}

.about-eyebrow {
  margin: 0 0 clamp(12px, 2vh, 24px);
  color: var(--ink-50);
}

.about-locations {
  margin: clamp(16px, 2vh, 24px) 0 0;
  color: var(--ink-50);
}

.about-title {
  font-size: var(--fs-display);
  line-height: 1.04;
  letter-spacing: -0.02em;
  font-style: italic;
  font-variation-settings: "opsz" 96, "ital" 1, "SOFT" 35, "WONK" 0;
  font-weight: 400;
  margin: 0;
}

.about-passages {
  border-top: 0.5px solid var(--hairline);
  padding-top: clamp(48px, 6vh, 64px);
}

.about-passage {
  font-size: var(--fs-body);
  line-height: 1.7;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  margin: 0 0 clamp(24px, 4vh, 48px);
  text-wrap: pretty;
}

.about-passage em {
  font-style: italic;
  font-variation-settings: "opsz" 18, "ital" 1, "SOFT" 50;
}

.about-coda {
  margin-top: clamp(48px, 7vh, 80px);
  font-size: clamp(18px, 1.6vw, 24px);
  line-height: 1.5;
  font-style: italic;
  font-variation-settings: "opsz" 36, "ital" 1, "SOFT" 40;
  text-wrap: pretty;
}

.about-coda .period {
  transition: color 400ms var(--ease-out-emph);
}

.about-coda .period.is-signed {
  color: var(--orange);
}

/* Contact: "get in touch" trigger + form panel. The panel mirrors the
   case-dropdown component (paper bg, hairline border, soft shadow) but
   is click-to-toggle rather than hover-with-intent — forms need stable
   interaction state. */
.about-contact {
  position: relative;
  margin-top: clamp(48px, 7vh, 80px);
}

.about-contact-trigger {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-meta);
  letter-spacing: 0.04em;
  text-transform: lowercase;
  color: var(--orange);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 220ms var(--ease-out-emph);
}

.about-contact-trigger:hover,
.about-contact-trigger:focus-visible {
  color: var(--ink);
}

.about-contact-panel {
  margin-top: clamp(16px, 2vh, 24px);
  background: var(--paper);
  border: 0.5px solid rgba(var(--ink-rgb), 0.18);
  box-shadow: 0 18px 36px rgba(var(--ink-rgb), 0.08);
  padding: clamp(24px, 4vh, 32px);
  max-width: 480px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 220ms ease-out, transform 220ms ease-out;
}

.about-contact-panel[hidden] {
  display: none;
}

.about-contact-panel.is-open {
  opacity: 1;
  transform: translateY(0);
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2vh, 24px);
}

.contact-form-eyebrow {
  margin: 0 0 clamp(4px, 0.5vh, 8px);
  color: var(--ink-50);
}

.contact-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.contact-row label {
  color: var(--ink-50);
  letter-spacing: 0.04em;
  text-transform: lowercase;
}

.contact-row input,
.contact-row textarea {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(15px, 1.2vw, 17px);
  line-height: 1.4;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  color: var(--ink);
  background: transparent;
  border: none;
  border-bottom: 0.5px solid var(--hairline);
  padding: 6px 0;
  outline: none;
  width: 100%;
  resize: none;
  transition: border-color 200ms ease-out;
}

.contact-row textarea {
  font-family: "Fraunces", Georgia, serif;
  resize: vertical;
  min-height: 80px;
}

.contact-row input:focus,
.contact-row textarea:focus {
  border-bottom-color: var(--ink);
}

.contact-row input:invalid:not(:placeholder-shown),
.contact-row textarea:invalid:not(:placeholder-shown) {
  border-bottom-color: var(--orange);
}

.contact-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: clamp(8px, 1vh, 12px);
}

.contact-submit {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--paper);
  background: var(--ink);
  border: none;
  padding: var(--space-xs) var(--space-md);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 200ms ease-out;
}

.contact-submit:hover,
.contact-submit:focus-visible {
  background: var(--orange);
}

.contact-submit:disabled,
.contact-submit[disabled] {
  background: var(--ink-50);
  cursor: not-allowed;
  pointer-events: none;
}

.contact-feedback {
  color: var(--ink-50);
}

/* Particle word generic — reusable container for the canvas-rendered
   "complex" and "arc" treatments. */
.particle-word {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}

.particle-word .pw-text {
  display: inline-block;
  color: transparent;
}

.particle-word .pw-canvas {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  opacity: 1;
  z-index: 5;
}

.about-foot {
  padding: clamp(48px, 6vh, 80px) var(--gutter) clamp(24px, 4vh, 48px);
  display: flex;
  justify-content: space-between;
  border-top: 0.5px solid var(--hairline);
}

/* ============================================================
   About — portrait (WebGL shader + mouse-reveal trail)
   ============================================================ */

/* Two-column lede: portrait (sticky) left, passages right. */
.about-lede {
  display: grid;
  grid-template-columns: minmax(260px, 340px) 1fr;
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
  margin-bottom: clamp(48px, 8vh, 80px);
}

.about-portrait {
  margin: 0;
  /* Stays in view while her passages scroll beside it. */
  position: sticky;
  top: clamp(88px, 14vh, 132px);
}

/* The stage holds three stacked layers: the texture <img> (also the no-WebGL
   fallback), the shader <canvas>, and a CSS grain veil. The photo is a circle
   on a transparent field; the shader composites it over the page paper, so no
   frame/backdrop is needed — she reads as floating on the page. */
.portrait-stage {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  cursor: crosshair;
}

.portrait-img,
.portrait-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Canvas fills the stage (the shader does its own fit); the fallback <img> is
   contained so the circular portrait is never cropped. */
.portrait-canvas { object-fit: fill; }

/* Base state: the plain <img> is shown, editorially graded so it never looks
   like a raw snapshot even without the shader. */
.portrait-img {
  object-fit: contain;
  filter: grayscale(0.12) contrast(1.04) saturate(0.92) brightness(1.01);
  transition: opacity 600ms var(--ease-out-emph);
}

/* No image yet → hide the empty <img> so only the wash/procedural shader shows. */
.about-portrait[data-portrait-empty] .portrait-img {
  opacity: 0;
}

/* The canvas is transparent until the shader is live. */
.portrait-canvas {
  opacity: 0;
  transition: opacity 700ms var(--ease-out-emph);
}

.about-portrait.is-shader .portrait-canvas {
  opacity: 1;
}

/* When the shader owns the frame, drop the <img> and CSS grain (the shader
   carries its own grain + grade). */
.about-portrait.is-shader .portrait-img,
.about-portrait.is-shader .portrait-grain {
  opacity: 0;
}

/* CSS film-grain veil — the texture for the no-WebGL fallback. Tiny tiled
   noise via an inline SVG turbulence, kept faint. */
.portrait-grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: multiply;
  opacity: 0.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  transition: opacity 600ms var(--ease-out-emph);
}

@media (prefers-reduced-motion: reduce) {
  .portrait-stage { cursor: default; }
}

/* Narrow screens: stack the lede — portrait on top (left-aligned), passages
   below. Sticky is released so it scrolls normally. */
@media (max-width: 860px) {
  .about-lede {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .about-portrait {
    position: static;
    max-width: 340px;
    margin: 0 0 clamp(36px, 6vh, 56px);
  }
}

/* ============================================================
   About — capabilities (animated skills ledger)
   ============================================================ */

.about-skills {
  max-width: 720px;
  margin-top: clamp(64px, 10vh, 112px);
  padding-top: clamp(40px, 6vh, 56px);
  border-top: 0.5px solid var(--hairline);
}

.skills-head {
  margin-bottom: clamp(28px, 4vh, 44px);
}

.skills-eyebrow {
  margin: 0 0 clamp(8px, 1.4vh, 14px);
  color: var(--ink-50);
}

.skills-title {
  margin: 0;
  font-size: clamp(24px, 2.6vw, 34px);
  line-height: 1.08;
  letter-spacing: -0.015em;
  font-style: italic;
  font-variation-settings: "opsz" 60, "ital" 1, "SOFT" 35, "WONK" 0;
  font-weight: 400;
}

.skills-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(18px, 2.6vh, 28px);
}

.skill {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(12px, 1.6vw, 18px);
  align-items: start;
  /* Rows lift in with their bar; the observer adds .is-in. */
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 640ms var(--ease-out-emph), transform 640ms var(--ease-out-emph);
}

.skill.is-in {
  opacity: 1;
  transform: none;
}

.skill-index {
  padding-top: 0.35em;
  color: var(--ink-50);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
}

.skill-main {
  min-width: 0;
}

.skill-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(10px, 2vw, 20px);
  margin-bottom: clamp(8px, 1.2vh, 12px);
}

.skill-name {
  font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.2;
  font-variation-settings: "opsz" 24, "SOFT" 30;
  letter-spacing: -0.01em;
}

.skill-tag {
  flex: none;
  color: var(--ink-50);
  text-align: right;
}

/* Track + fill. The fill draws from 0 to (--level)% when .is-in lands, and
   the leading edge carries a small orange node — the signature detail. */
.skill-track {
  position: relative;
  height: 2px;
  background: var(--hairline);
  overflow: visible;
}

.skill-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0;
  background: linear-gradient(
    90deg,
    rgba(var(--ink-rgb), 0.55),
    var(--ink) 70%,
    var(--orange)
  );
  transition: width 1100ms var(--ease-out-emph);
  transition-delay: var(--fill-delay, 0ms);
}

.skill.is-in .skill-fill {
  width: calc(var(--level, 80) * 1%);
}

/* Leading-edge node: a small orange dot riding the end of the fill. */
.skill-fill::after {
  content: "";
  position: absolute;
  right: -3px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-circle);
  background: var(--orange);
  transform: translateY(-50%) scale(0);
  transition: transform 320ms var(--ease-out-emph);
  transition-delay: calc(var(--fill-delay, 0ms) + 700ms);
  box-shadow: 0 0 0 3px rgba(254, 80, 0, 0.16);
}

.skill.is-in .skill-fill::after {
  transform: translateY(-50%) scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .skill {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .skill-fill {
    transition: none;
  }
  .skill.is-in .skill-fill { width: calc(var(--level, 80) * 1%); }
  .skill-fill::after { transition: none; }
}

/* ============================================================
   Fourth-wall flash — §6.5
   ============================================================ */

.fourth-wall {
  position: fixed;
  bottom: clamp(24px, 4vh, 48px);
  right: clamp(24px, 4vw, 48px);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-mono);
  letter-spacing: 0.04em;
  text-transform: lowercase;
  color: var(--ink);
  background: var(--paper);
  padding: var(--space-2xs) var(--space-xs);
  border: 0.5px solid var(--hairline);
  z-index: 50;
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms var(--ease-out-emph);
  white-space: nowrap;
  max-width: calc(100vw - 40px);
}

.fourth-wall .yellow { color: var(--starry); background: var(--ink); padding: 1px 5px; }
.fourth-wall .lime { color: var(--lime); background: var(--ink); padding: 1px 5px; }

.fourth-wall.is-on {
  opacity: 1;
}

@media (max-width: 520px) {
  .fourth-wall {
    left: 16px;
    right: 16px;
    white-space: normal;
    text-align: center;
  }
}

/* ============================================================
   Reduced motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .anchor .reveal {
    opacity: 1;
    transform: none;
  }
  .fourth-wall {
    display: none;
  }
}

/* ============================================================
   Row-expand transition · home → case (and reverse)
   The clicked row's borders push outward to fill the viewport.
   Neighboring rows fade + translate away (above ↑, below ↓).
   Title-block stays anchored to the row's original position
   during expansion, then settles into the case hero layout.
   ============================================================ */

/* Overlay state: starry sits on top of home during expand/collapse.
   clip-path: inset(0) is the "fully open" state — CRITICAL for the
   collapse to animate, because clip-path won't interpolate from `none`
   to inset(). The is-collapsing rule (higher specificity) overrides
   this with the row-strip inset when applied. */
.screen.is-overlay {
  position: fixed !important;
  inset: 0;
  z-index: 100;
  display: block;
  clip-path: inset(0 0 0 0);
  -webkit-clip-path: inset(0 0 0 0);
}

/* Push-aside: neighboring decision rows fade + translate during launch */
.decisions.is-launching .decision {
  transition: opacity 480ms ease-out,
              transform 700ms cubic-bezier(0.83, 0, 0.17, 1);
}

.decisions.is-launching .decision.is-pushed-up {
  opacity: 0;
  transform: translateY(-32px);
  pointer-events: none;
}

.decisions.is-launching .decision.is-pushed-down {
  opacity: 0;
  transform: translateY(32px);
  pointer-events: none;
}

.decisions.is-launching .decision.is-launching-source {
  /* the source row keeps its layout space; only the content vanishes,
     so the case overlay's pinned title is the only visible identity */
}

/* .row-link has display: contents (so the grid sees its children directly),
   which means opacity on .row-link itself has no rendering effect. Target
   the actual rendered children instead. */

/* Persistent transition on the children — declared OUTSIDE the
   is-launching-source rule so opacity still animates back to 1 when the
   class is removed mid-reverse. */
.decisions.is-launching .decision .num,
.decisions.is-launching .decision .decision-title,
.decisions.is-launching .decision .client,
.decisions.is-launching .decision .dot {
  transition: opacity 240ms ease-out;
}

.decisions.is-launching .decision.is-launching-source .num,
.decisions.is-launching .decision.is-launching-source .decision-title,
.decisions.is-launching .decision.is-launching-source .client,
.decisions.is-launching .decision.is-launching-source .dot {
  opacity: 0;
}

.decisions.is-launching .decision.is-launching-source .row-link {
  pointer-events: none;
}

/* Case content staging — runs after row-expand completes.
   Note: case-title is NOT pre-faded; it morphs in place via transform
   during the expansion. Logos and chapter menu fade in via case-stage-in. */
.case.case-prefade .case-eyebrow,
.case.case-prefade .case-stakes,
.case.case-prefade .case-meta,
.case.case-prefade .case-fold,
.case.case-prefade > section.layer-complexity,
.case.case-prefade > section.layer-clarity,
.case.case-prefade > section.layer-beauty,
.case.case-prefade > .case-thumbs,
.case.case-prefade > .case-masthead,
.case.case-prefade > .case-toolbar,
.case.case-prefade > .reading-rail {
  opacity: 0 !important;
  pointer-events: none;
}

.case.case-stage-in .case-eyebrow { animation: case-fade-in 400ms ease-out 60ms both; }
.case.case-stage-in .case-stakes { animation: case-fade-in 400ms ease-out 160ms both; }
.case.case-stage-in .case-meta { animation: case-fade-in 400ms ease-out 260ms both; }
.case.case-stage-in .case-fold { animation: case-fade-in 400ms ease-out 360ms both; }
.case.case-stage-in > .case-masthead,
.case.case-stage-in > .case-toolbar { animation: case-fade-in 400ms ease-out 220ms both; }
.case.case-stage-in > section.layer-complexity { animation: case-fade-in 500ms ease-out 200ms both; }
.case.case-stage-in > section.layer-clarity { animation: case-fade-in 500ms ease-out 320ms both; }
.case.case-stage-in > section.layer-beauty { animation: case-fade-in 500ms ease-out 440ms both; }
.case.case-stage-in > .case-thumbs { animation: case-fade-in 500ms ease-out 540ms both; }
.case.case-stage-in > .reading-rail { animation: case-fade-in 500ms ease-out 600ms both; }

@keyframes case-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Reverse: kept as a class hook for potential future use. Currently the
   OUT animation lets the clip-path collapse do all the visual work and
   the case-collapse-tail animation fades the whole overlay at the end. */


/* ============================================================
   Work → work transition — clip-path text wipe
   The origin's hero text un-reveals, the destination page sweeps in over
   it (masking the dark↔paper surface swap), then the new hero text reveals
   with the same left-to-right clip wipe as the home headline (.anchor .reveal).
   ============================================================ */

/* Destination page sweep: the overlay clip-path wipes left → right, carrying
   the new surface + content in over the old page. */
.screen.is-overlay.case-sweep-in {
  clip-path: inset(0 100% 0 0);
  -webkit-clip-path: inset(0 100% 0 0);
}
.screen.is-overlay.case-sweep-in.is-sweeping {
  clip-path: inset(0 0 0 0);
  -webkit-clip-path: inset(0 0 0 0);
  transition: clip-path 540ms cubic-bezier(0.77, 0, 0.18, 1),
              -webkit-clip-path 540ms cubic-bezier(0.77, 0, 0.18, 1);
}

/* Hero text wipe — baseline (revealed) state + transition. The explicit
   clip-path here lets the wipe interpolate (none → inset would not animate). */
.case.is-wiping .case-eyebrow,
.case.is-wiping .case-title,
.case.is-wiping .case-stakes,
.case.is-wiping .case-meta {
  opacity: 1;
  clip-path: inset(-20% -15% -20% -15%);
  -webkit-clip-path: inset(-20% -15% -20% -15%);
  transform: translateY(0);
  transition: clip-path 720ms cubic-bezier(0.77, 0, 0.175, 1),
              -webkit-clip-path 720ms cubic-bezier(0.77, 0, 0.175, 1),
              opacity 300ms ease-out,
              transform 700ms cubic-bezier(0.34, 1.16, 0.64, 1);
}
/* Reveal stagger (enter): eyebrow → title → stakes → meta. */
.case.is-wiping .case-eyebrow { transition-delay: 0ms; }
.case.is-wiping .case-title   { transition-delay: 90ms; }
.case.is-wiping .case-stakes  { transition-delay: 210ms; }
.case.is-wiping .case-meta    { transition-delay: 310ms; }

/* Enter — pre-reveal hidden state (clipped on the right; appears L→R). Set
   instantly so the destination starts hidden, then animates to baseline when
   .case-wipe-in is removed. */
.case.is-wiping.case-wipe-in .case-eyebrow,
.case.is-wiping.case-wipe-in .case-title,
.case.is-wiping.case-wipe-in .case-stakes,
.case.is-wiping.case-wipe-in .case-meta {
  opacity: 0;
  clip-path: inset(-20% 100% -20% -15%);
  -webkit-clip-path: inset(-20% 100% -20% -15%);
  transform: translateY(12px);
  transition: none;
}

/* Exit — origin hero un-reveals left→right: a clean clip wipe with opacity
   held (so it reads as a directional un-reveal, not a fade), mirroring the
   enter reveal's easing. The clip erases from the left edge rightward. */
.case.is-wiping.case-wipe-out .case-eyebrow,
.case.is-wiping.case-wipe-out .case-title,
.case.is-wiping.case-wipe-out .case-stakes,
.case.is-wiping.case-wipe-out .case-meta {
  opacity: 1;
  clip-path: inset(-20% -15% -20% 100%);
  -webkit-clip-path: inset(-20% -15% -20% 100%);
  transform: translateY(0);
  transition: clip-path 460ms cubic-bezier(0.77, 0, 0.175, 1),
              -webkit-clip-path 460ms cubic-bezier(0.77, 0, 0.175, 1);
}
/* Exit stagger — top-down (eyebrow → meta), mirroring the reveal order. */
.case.is-wiping.case-wipe-out .case-eyebrow { transition-delay: 0ms; }
.case.is-wiping.case-wipe-out .case-title   { transition-delay: 70ms; }
.case.is-wiping.case-wipe-out .case-stakes  { transition-delay: 150ms; }
.case.is-wiping.case-wipe-out .case-meta    { transition-delay: 210ms; }


@media (prefers-reduced-motion: reduce) {
  .decisions.is-launching .decision.is-pushed-up,
  .decisions.is-launching .decision.is-pushed-down {
    opacity: 0;
    transform: none;
    transition: none;
  }
  .case.case-prefade,
  .case.case-stage-in{
    opacity: 1 !important;
  }
  .case.case-prefade *,
  .case.case-stage-in *{
    opacity: 1 !important;
    animation: none !important;
    transition: none !important;
  }
}

.asset-starry-can { background-image: url("assets/starry-can.png"); background-repeat: no-repeat; background-position: center; background-size: contain; }
.asset-stars-chrome { background-image: url("assets/stars-chrome.png"); background-repeat: no-repeat; background-position: center; background-size: contain; }
.asset-lime-wedge { background-image: url("assets/lime-wedge.png"); background-repeat: no-repeat; background-position: center; background-size: contain; }
.asset-lime-full { background-image: url("assets/lime-full.png"); background-repeat: no-repeat; background-position: center; background-size: contain; }
.asset-lemon-wedge { background-image: url("assets/lemon-wedge.png"); background-repeat: no-repeat; background-position: center; background-size: contain; }

.asset { display: block; width: 100%; height: 100%; background-repeat: no-repeat; background-position: center; background-size: contain; }
span.beauty-can { position: absolute; left: 50%; top: 50%; height: 86%; width: 70%; aspect-ratio: 960 / 1920; transform: translate(-50%, -50%) rotate(-2deg); filter: drop-shadow(0 24px 48px rgba(var(--ink-rgb), 0.22)); animation: hero-can-drift 16s ease-in-out infinite -2s; z-index: 2; }

/* ============================================================
   HOME REEL — hero teaser · docked player · 80% stage
   A single video, surfaced in two moments: a teaser that fades in after the
   intro (lower-left of the hero, inviting the scroll) and a docked player
   beside the "decisions" eyebrow. Either opens the shared stage, which grows
   out of the clicked thumbnail (FLIP, driven in app.js) to ~80% of the
   viewport and shrinks back when the clip ends or is closed.
   ============================================================ */

.home { --reel-w: clamp(150px, 18vw, 196px); }

/* Docking slot in the decisions header — reserves the footprint the traveling
   reel lands in, so the copy layout doesn't shift. Collapsed with no video. */
.reel-slot {
  flex: 0 0 auto;
  width: var(--reel-w);
  aspect-ratio: 16 / 9;
}
body:not(.has-hero-video) .reel-slot { display: none; }

/* The reel — a single element, fixed, positioned by app.js. It rests in the
   hero's lower-left and slides up into .reel-slot as the page scrolls. */
.reel-travel {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
  opacity: 0;
  pointer-events: none;
  transition: opacity 560ms var(--ease-out-emph);
}
.reel-travel[hidden] { display: none; }
/* Only on the home, and only after the intro has settled. */
body.hero-ready.on-home .reel-travel { opacity: 1; pointer-events: auto; }

.reel-thumb {
  position: relative;
  flex: 0 0 auto;
  display: block;
  width: var(--reel-w);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: rgba(var(--ink-rgb), 0.06);
  box-shadow: 0 16px 38px rgba(var(--ink-rgb), 0.16);
  transition: box-shadow 260ms var(--ease-out-emph);
}
.reel-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.reel-play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-circle);
  background: rgba(var(--ink-rgb), 0.55);
  color: #fff;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  transition: background 220ms var(--ease-out-emph), transform 220ms var(--ease-out-emph);
}
.reel-play svg { transform: translateX(1px); }
.reel-label {
  color: var(--ink-70);
  white-space: nowrap;
  pointer-events: none;
  /* app.js fades this out (via --reel-label-o) as the reel docks. */
  opacity: var(--reel-label-o, 1);
  transition: color 220ms var(--ease-out-emph);
}
.reel-travel:hover .reel-thumb,
.reel-travel:focus-visible .reel-thumb { box-shadow: 0 22px 52px rgba(var(--ink-rgb), 0.24); }
.reel-travel:hover .reel-play,
.reel-travel:focus-visible .reel-play { background: var(--orange); transform: scale(1.06); }
.reel-travel:hover .reel-label { color: var(--orange); }
.reel-travel:focus-visible { outline: none; }

/* ---- Decisions header ---- */
.decisions-head {
  display: flex;
  align-items: flex-start;
  gap: clamp(20px, 3vw, 40px);
  margin-bottom: clamp(48px, 6vh, 64px);
}
.decisions-head-copy { min-width: 0; }
/* The eyebrow + intro keep their own rhythm; the wrapper owns the bottom gap. */
.decisions-head-copy .decisions-intro { margin-bottom: 0; }

/* ---- The stage (≈80% viewport) ---- */
.video-stage {
  position: fixed;
  inset: 0;
  z-index: 9990;
  display: grid;
  place-items: center;
}
.video-stage[hidden] { display: none; }
.video-stage-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 8, 8, 0.86);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 340ms var(--ease-out-emph), background 480ms var(--ease-out-emph);
}
.video-stage.is-open .video-stage-backdrop { opacity: 1; }
/* Cinema mode — while the reel is actually PLAYING every bit of interface
   leaves: the backdrop goes fully opaque (no page glowing through) and the
   close ✕ fades out. Pausing (click the video) or ending brings it all back. */
.video-stage.is-playing .video-stage-backdrop { background: rgb(8, 8, 8); }

.video-stage-frame {
  position: relative;
  width: min(80vw, calc(80vh * 16 / 9));
  aspect-ratio: 16 / 9;
  transform-origin: center center;
  will-change: transform;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: #000;
  box-shadow: 0 40px 120px rgba(0, 0, 0, 0.5);
}
.video-stage-el {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}
.video-stage-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-circle);
  background: rgba(8, 8, 8, 0.55);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  transition: opacity 240ms var(--ease-out-emph) 260ms, background 200ms var(--ease-out-emph);
  -webkit-tap-highlight-color: transparent;
}
.video-stage.is-open .video-stage-close { opacity: 1; }
.video-stage.is-open.is-playing .video-stage-close { opacity: 0; pointer-events: none; transition-delay: 0ms; }
.video-stage-close:hover,
.video-stage-close:focus-visible { background: var(--orange); outline: none; }

/* Lock page scroll while the stage is open. */
body.video-open { overflow: hidden; }

/* While the stage is open the custom orange cursor (z-index above the stage)
   is interface too — hand control back to the native cursor. */
body.gcursor-on.video-open,
body.gcursor-on.video-open * { cursor: auto; }
body.gcursor-on.video-open .video-stage [data-close],
body.gcursor-on.video-open .video-stage-close,
body.gcursor-on.video-open .video-stage-el,
body.gcursor-on.video-open .video-ctl .vc-btn,
body.gcursor-on.video-open .video-ctl .vc-scrub,
body.gcursor-on.video-open .video-ctl .reel-sound { cursor: pointer; }
body.gcursor-on.video-open .gcursor { opacity: 0; }

/* ---- Photo stage (lightbox) ----
   The gallery shows every photo whole but small; this is where it gets read.
   Sits at the video stage's level — both are modal, and neither can be open
   while the other is.

   The figure is sized from the photo's ratio (--lb-ar, set per slide in
   app.js) rather than left to the image's intrinsic size, so arrowing to the
   next photo resizes the frame to the shape that's coming instead of jumping
   when the file decodes. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 9990;
  display: grid;
  place-items: center;
  padding: clamp(48px, 7vh, 80px) clamp(16px, 7vw, 96px);
}
.lightbox[hidden] { display: none; }

/* Opening and closing. The stage doesn't cut — the backdrop washes in and the
   photo settles the last 3% of its size, which is enough to read as "this came
   from the page" without staging a production about it.

   .is-open is added a frame after `hidden` comes off (and removed for the whole
   exit, which app.js waits out before hiding) — while it's off, the stage must
   not eat the clicks meant for the page underneath it. */
.lightbox:not(.is-open) { pointer-events: none; }
.lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 8, 8, 0.92);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  cursor: zoom-out;
  opacity: 0;
  transition: opacity 240ms var(--ease-out-emph);
}
.lightbox.is-open .lightbox-backdrop { opacity: 1; }
.lightbox-figure {
  /* No --lb-ar default here. It would shadow the value app.js sets on
     #lightbox for every descendant — the img inherits from its parent, not
     from the element the JS wrote to — and pin the frame at 3:2 whatever the
     photo's real shape. The fallback rides on the var() at each use site. */
  position: relative;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 100%;
  min-width: 0;
  pointer-events: none; /* the backdrop keeps the click-out everywhere but the photo */
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 220ms var(--ease-out-emph), transform 260ms var(--ease-out-emph);
}
.lightbox.is-open .lightbox-figure {
  opacity: 1;
  transform: none;
}
/* The controls arrive after the photo — they're the offer, not the point. */
.lightbox-nav,
.lightbox-close {
  opacity: 0;
  transition: opacity 200ms var(--ease-out-emph) 120ms,
    background 200ms var(--ease-out-emph),
    color 200ms var(--ease-out-emph);
}
.lightbox.is-open .lightbox-nav,
.lightbox.is-open .lightbox-close { opacity: 1; }
/* Nothing before the first photo. */
.lightbox-nav:disabled {
  opacity: 0.25;
  pointer-events: none;
}
.lightbox.is-open .lightbox-nav:disabled { opacity: 0.25; }
.lightbox-img {
  display: block;
  /* Whichever of the two limits binds first — so the photo is as large as it
     can be without ever being cropped or overflowing its padding. */
  width: min(100%, (100vh - clamp(150px, 20vh, 210px)) * var(--lb-ar, 1.5));
  height: auto;
  max-height: calc(100vh - clamp(150px, 20vh, 210px));
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.04);
  aspect-ratio: var(--lb-ar, 1.5);
  opacity: 1;
  transition: opacity 200ms var(--ease-out-emph);
}
.lightbox.is-loading .lightbox-img { opacity: 0.25; }
.lightbox-cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  color: rgba(255, 255, 255, 0.72);
  font-size: 12px;
  line-height: 1.4;
}
.lightbox-cap .lb-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
/* Which gallery this photo belongs to. The sequence runs across all of them,
   so this is what tells the reader they've crossed from one into the next —
   it's the only thing on the stage that says where they are. */
.lightbox-cap .lb-group {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
}
.lightbox-cap .lb-group[hidden] { display: none; }
.lightbox-cap .lb-count {
  color: rgba(255, 255, 255, 0.5);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.lightbox-nav,
.lightbox-close {
  position: absolute;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-circle);
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  cursor: pointer;
  /* transition lives with the enter/exit rules above — one source for it */
  -webkit-tap-highlight-color: transparent;
}
/* The stage belongs to whichever case opened it, so its controls answer in that
   case's colour rather than the house orange — Starry's ✕ is yellow, Dew's is
   blue. The stage lives outside .case (it's a sibling of every screen), so the
   token can't be inherited: app.js copies it in from the tile that was clicked.
   --lb-accent-ink comes along because the accents don't share a readable
   foreground — white on Starry's yellow is a smudge. Orange is the fallback,
   which is also what the home reel's stage uses. */
.lightbox-nav:hover,
.lightbox-close:hover,
.lightbox-nav:focus-visible,
.lightbox-close:focus-visible {
  background: var(--lb-accent, var(--orange));
  color: var(--lb-accent-ink, #fff);
  outline: none;
}
.lightbox-nav[hidden] { display: none; }
.lightbox-nav {
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 52px;
}
.lightbox-prev { left: clamp(10px, 2vw, 28px); }
.lightbox-next { right: clamp(10px, 2vw, 28px); }
.lightbox-close {
  top: clamp(14px, 2.4vh, 26px);
  right: clamp(14px, 2vw, 28px);
  width: 44px;
  height: 44px;
}

/* On a phone the arrows would sit on top of the photo, so the swipe carries
   navigation and they move to the base, clear of it. */
@media (max-width: 720px) {
  .lightbox { padding: 60px 12px 84px; }
  .lightbox-nav {
    top: auto;
    bottom: 16px;
    transform: none;
    width: 46px;
    height: 46px;
  }
  .lightbox-prev { left: 28%; }
  .lightbox-next { right: 28%; }
}

body.lb-open { overflow: hidden; }
body.gcursor-on.lb-open,
body.gcursor-on.lb-open * { cursor: auto; }
body.gcursor-on.lb-open .lightbox-backdrop { cursor: zoom-out; }
body.gcursor-on.lb-open .lightbox-nav,
body.gcursor-on.lb-open .lightbox-close { cursor: pointer; }
body.gcursor-on.lb-open .gcursor { opacity: 0; }

/* The stage still appears and still leaves — it just does it at once. The exit
   stays inside the LB_EXIT_MS app.js waits out before hiding, and the stage is
   already pointer-events: none by then, so nothing is left catching clicks. */
@media (prefers-reduced-motion: reduce) {
  .lightbox-img,
  .lightbox-backdrop,
  .lightbox-figure,
  .lightbox-nav,
  .lightbox-close { transition: none; }
  .lightbox-figure { transform: none; }
}

@media (max-width: 720px) {
  .home { --reel-w: min(46vw, 190px); }
  .decisions-head { flex-direction: column; gap: 16px; }
  .reel-label { display: none; }
  .video-stage-frame { width: min(92vw, calc(78vh * 16 / 9)); }
}

@media (prefers-reduced-motion: reduce) {
  .reel-travel,
  .home-nav { transition-duration: 200ms; }
  .video-stage-frame,
  .reel-travel { will-change: auto; }
}

/* ============================================================
   STARRY · project page (v2) — hero reel · article-style layers ·
   justified galleries · metrics band · press blocks.
   Scoped to #screen-starry so the other three cases keep their layout.
   New component classes (.ph, .gal-grid, .clarity-block, .metrics-band,
   .press-block …) only appear in the Starry markup.
   ============================================================ */

/* ---- Media placeholders (swapped for real assets later) ---- */
.ph {
  position: relative;
  display: block;
  width: 100%;
  min-height: 96px;
  border-radius: var(--radius-sm);
  background: rgba(var(--ink-rgb), 0.055);
  border: 1px solid rgba(var(--ink-rgb), 0.10);
  overflow: hidden;
}
.ph::after {
  content: attr(data-label);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 14px;
  text-align: center;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-mono);
  letter-spacing: 0.03em;
  line-height: 1.4;
  color: var(--ink-50);
}
.ph--accent {
  background: color-mix(in srgb, var(--case-color) 15%, transparent);
  border-color: color-mix(in srgb, var(--case-color) 32%, transparent);
}
.ph--accent::after { color: color-mix(in srgb, var(--case-color) 80%, var(--ink)); }
.ph--4x3 { aspect-ratio: 4 / 3; }
.ph--square { aspect-ratio: 1 / 1; }
.ph--wide { aspect-ratio: 16 / 9; }
.ph--tall { aspect-ratio: 3 / 4; }
.ph--video::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 54px; height: 54px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-circle);
  background: rgba(var(--ink-rgb), 0.14);
  border: 1px solid rgba(var(--ink-rgb), 0.25);
  z-index: 1;
}

/* ---- Hero: full-width reel → tags → premise h1 → 4-up meta ---- */
/* Restructured case hero (v2, all cases) — editorial split: eyebrow + premise
   on the left, the reel smaller on the right (~55% of the content width, so a
   lighter/lower-res file reads fine), and the 4-up meta row across the full
   width underneath. The .case-decision wrapper is flattened (display:contents)
   so eyebrow / title / meta can take their own grid cells; the copy block is
   TOP-aligned with the reel — eyebrow and premise start on the reel's top edge
   and the slack falls to the bottom of the column (the flexible row 3), which
   reads cleaner than hanging the copy off the base of the video. */
.case-hero--reel {
  min-height: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 55%);
  grid-template-rows: auto auto 1fr auto auto;
  gap: 0 clamp(28px, 4vw, 72px);
}
.case-hero--reel .case-decision { display: contents; }
.case-hero--reel .case-reel {
  grid-column: 2;
  grid-row: 1 / 4;
  align-self: start;
}
.case-hero--reel .case-eyebrow { grid-column: 1; grid-row: 1; }
.case-hero--reel .case-title--lead { grid-column: 1; grid-row: 2; }
.case-hero--reel .case-meta--3,
.case-hero--reel .case-meta--4 { grid-column: 1 / -1; grid-row: 4; }
/* The proof band closes the hero: full-bleed across both columns, and pulled
   down over the hero's bottom padding so it sits flush on the fold rather than
   floating above a strip of paper. */
.case-hero--reel .proof-band { grid-column: 1 / -1; grid-row: 5; }

/* Stacked below tablet — reel on top, copy underneath (the previous flow). */
@media (max-width: 859px) {
  .case-hero--reel {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    gap: clamp(28px, 4vh, 52px);
  }
  .case-hero--reel .case-decision { display: block; }
}
.case-reel { width: 100%; position: relative; }
/* Ambient glow — a 32×18 canvas (sampled from the reel in app.js) stretched
   past the frame and heavily blurred, so the video's own colors bleed into
   the page around the thumb while it plays. Display-only; fades in on the
   first sampled frame (.is-glowing). */
.reel-glow {
  position: absolute;
  inset: 0;
  /* Canvas is a replaced element — inset alone won't stretch it, so size it
     explicitly and scale past the frame for the halo. */
  width: 100%;
  height: 100%;
  transform: scale(1.32);
  /* Fixed blur radius — Safari drops the whole filter if the argument is a
     math function (clamp/calc), which left the canvas visible unblurred. */
  filter: blur(44px) saturate(1.6);
  /* And fade the halo out radially so the element's own edge can never show
     as a hard border, whatever the browser does with the blur. */
  -webkit-mask-image: radial-gradient(closest-side, rgba(0, 0, 0, 1) 62%, transparent 100%);
  mask-image: radial-gradient(closest-side, rgba(0, 0, 0, 1) 62%, transparent 100%);
  opacity: 0;
  transition: opacity 1200ms var(--ease-out-emph);
  pointer-events: none;
  z-index: 0;
}
.case-reel.is-glowing .reel-glow { opacity: 0.8; }
.reel-frame {
  position: relative;
  z-index: 1;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: #000;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
}
.reel-video { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The author `display:flex` below beats the UA [hidden] rule, so the JS
   `ph.hidden = true` (set once a real video is wired in) needs an explicit
   author rule to actually remove the placeholder. */
.reel-ph[hidden] { display: none; }
.reel-ph {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background:
    radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--case-color) 26%, transparent), transparent 60%),
    linear-gradient(135deg, #141414, #0b0b0b);
}
.reel-ph-tag {
  color: var(--case-color);
  font-size: var(--fs-meta);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.reel-ph-meta { color: rgba(250, 250, 247, 0.4); letter-spacing: 0.1em; }
.reel-sound {
  position: absolute;
  right: clamp(12px, 1.5vw, 20px);
  bottom: clamp(12px, 1.5vw, 20px);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid rgba(250, 250, 247, 0.3);
  border-radius: var(--radius-pill);
  background: rgba(8, 8, 8, 0.5);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  color: #fff;
  cursor: pointer;
  transition: border-color 200ms var(--ease-out-emph), background 200ms var(--ease-out-emph);
}
.reel-sound[hidden] { display: none; }

/* "Play again" — surfaces centered over a video once it finishes (the reel
   and article/gallery videos play once instead of looping). */
.video-replay {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: rgba(8, 8, 8, 0.35);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 320ms var(--ease-out-emph);
}
.case-reel.is-ended .video-replay,
.ph.is-ended .video-replay { opacity: 1; pointer-events: auto; }
.vr-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid rgba(250, 250, 247, 0.35);
  border-radius: var(--radius-pill);
  background: rgba(8, 8, 8, 0.55);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  transition: border-color 200ms var(--ease-out-emph), background 200ms var(--ease-out-emph);
}
.video-replay:hover .vr-pill,
.video-replay:focus-visible .vr-pill { border-color: var(--case-color); }
.vr-ico {
  width: 14px; height: 14px;
  background: currentColor;
  -webkit-mask: no-repeat center / contain;
  mask: no-repeat center / contain;
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z'/></svg>");
}
.reel-sound:hover { border-color: var(--case-color); }
.reel-sound.is-on { border-color: var(--case-color); background: color-mix(in srgb, var(--case-color) 22%, rgba(8,8,8,0.5)); }
.reel-sound-ico {
  width: 14px; height: 14px;
  border-radius: 2px;
  background: currentColor;
  -webkit-mask: no-repeat center / contain;
  mask: no-repeat center / contain;
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M4 9v6h4l5 5V4L8 9H4zm11.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M4 9v6h4l5 5V4L8 9H4zm11.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z'/></svg>");
}

/* ---- Video transport bar (see makeVideoControls in app.js) ----
   Play/pause · ±10s · scrub · clock, along the base of any player. Sits above
   the "play again" overlay (z 3) so a finished video can still be scrubbed
   back into rather than only restarted from zero. The unmute pill is moved
   into this bar, so its absolute corner positioning has to be undone. */
.video-ctl {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: clamp(6px, 0.8vw, 12px);
  padding: clamp(20px, 4vh, 44px) clamp(12px, 1.5vw, 20px) clamp(10px, 1.4vh, 16px);
  background: linear-gradient(to top, rgba(8, 8, 8, 0.72), rgba(8, 8, 8, 0));
  color: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity 240ms var(--ease-out-emph);
}
/* Reveal on hover/keyboard focus; a stopped player keeps it up, since that's
   exactly when you're reaching for it. */
.reel-frame:hover .video-ctl,
.reel-frame:focus-within .video-ctl,
.case-reel.is-ended .video-ctl,
.video-stage-frame:hover .video-ctl,
.video-stage-frame:focus-within .video-ctl,
.ph.is-playing:hover .video-ctl,
.ph.is-playing:focus-within .video-ctl,
.ph.is-ended .video-ctl {
  opacity: 1;
  pointer-events: auto;
}
.reel-frame .video-ctl:not(.is-playing),
.video-stage-frame .video-ctl:not(.is-playing) { opacity: 1; pointer-events: auto; }
.video-ctl .reel-sound {
  position: static;
  flex: none;
  padding: 6px 10px;
  opacity: 1;
  pointer-events: auto;
}
/* Small tiles: icon-only mute, and the scrub carries the rest (see the
   compact branch in makeVideoControls). */
.video-ctl--compact .reel-sound .reel-sound-label { display: none; }
.video-ctl--compact .reel-sound { padding: 6px; }
.video-ctl--compact { gap: 6px; padding: clamp(16px, 3vh, 30px) 10px 8px; }

.vc-btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-circle);
  background: transparent;
  color: #fff;
  cursor: pointer;
  transition: background 180ms var(--ease-out-emph);
}
.vc-btn:hover,
.vc-btn:focus-visible { background: rgba(250, 250, 247, 0.16); }
.vc-ico {
  width: 17px; height: 17px;
  background: currentColor;
  -webkit-mask: no-repeat center / contain;
  mask: no-repeat center / contain;
}
.vc-play .vc-ico {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M8 5v14l11-7z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M8 5v14l11-7z'/></svg>");
}
.video-ctl.is-playing .vc-play .vc-ico {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M6 5h4v14H6zm8 0h4v14h-4z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M6 5h4v14H6zm8 0h4v14h-4z'/></svg>");
}
.vc-back .vc-ico {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12.5 5V1.5L7 7l5.5 5.5V9a5.5 5.5 0 1 1-5.5 5.5H4.5A8 8 0 1 0 12.5 5z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12.5 5V1.5L7 7l5.5 5.5V9a5.5 5.5 0 1 1-5.5 5.5H4.5A8 8 0 1 0 12.5 5z'/></svg>");
  transform: scaleX(-1);
}
.vc-fwd .vc-ico {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12.5 5V1.5L7 7l5.5 5.5V9a5.5 5.5 0 1 1-5.5 5.5H4.5A8 8 0 1 0 12.5 5z'/></svg>");
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12.5 5V1.5L7 7l5.5 5.5V9a5.5 5.5 0 1 1-5.5 5.5H4.5A8 8 0 1 0 12.5 5z'/></svg>");
}

/* The scrub target is deliberately taller than the 3px it draws — a hairline
   is not something you can reliably grab. */
.vc-scrub {
  flex: 1 1 auto;
  min-width: 40px;
  height: 22px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}
.vc-scrub:focus-visible { outline: 2px solid var(--case-color); outline-offset: 3px; border-radius: 2px; }
.vc-track {
  position: relative;
  flex: 1 1 auto;
  height: 3px;
  border-radius: var(--radius-pill);
  background: rgba(250, 250, 247, 0.28);
  transition: height 160ms var(--ease-out-emph);
}
.vc-scrub:hover .vc-track,
.vc-scrub.is-scrubbing .vc-track { height: 5px; }
.vc-played {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  border-radius: inherit;
  background: var(--case-color, var(--orange));
}
.vc-knob {
  position: absolute;
  top: 50%;
  left: var(--vc-pct, 0%);
  width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px;
  border-radius: var(--radius-circle);
  background: var(--case-color, var(--orange));
  opacity: 0;
  transition: opacity 160ms var(--ease-out-emph);
  pointer-events: none;
}
.vc-scrub:hover .vc-knob,
.vc-scrub:focus-visible .vc-knob,
.vc-scrub.is-scrubbing .vc-knob { opacity: 1; }
.vc-time {
  flex: none;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: rgba(250, 250, 247, 0.75);
  font-variant-numeric: tabular-nums;
}
/* The video stage is a neutral overlay with no case accent in scope. */
.video-stage-frame .vc-played,
.video-stage-frame .vc-knob { background: var(--orange); }

@media (hover: none) {
  /* No hover to reveal on, so the bar stays up wherever a video is playing. */
  .video-ctl { opacity: 1; pointer-events: auto; }
}

.case-hero--reel .case-decision { max-width: none; }
.case-title--lead {
  font-size: clamp(22px, 2.6vw, 34px);
  line-height: 1.28;
  font-style: normal;
  font-weight: 500;
  letter-spacing: -0.01em;
  font-variation-settings: "opsz" 40, "SOFT" 40, "WONK" 0;
  max-width: 34ch;
  margin: clamp(16px, 2.2vh, 28px) 0 0;
}
/* Three cells since the proof band took over "Key Outcome" — but the strip is
   built from whatever the CMS holds, so both counts are laid out. */
.case-meta--3,
.case-meta--4 {
  grid-template-columns: 1fr 1fr;
  gap: clamp(20px, 3vh, 28px) clamp(20px, 3vw, 40px);
  border-top: 0.5px solid var(--hairline);
  padding-top: clamp(20px, 3vh, 32px);
}
@media (min-width: 860px) {
  .case-meta--4 { grid-template-columns: repeat(4, 1fr); }
  .case-meta--3 { grid-template-columns: repeat(3, 1fr); }
}
.case-meta--3 .meta-cell dd,
.case-meta--4 .meta-cell dd { max-width: 28ch; }

/* ---- 01 · the complexity — text + supporting image ---- */
.complexity-body {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
}
@media (min-width: 860px) {
  .complexity-body { grid-template-columns: 1.05fr 0.95fr; }
}
.complexity-text p {
  font-size: var(--fs-body);
  line-height: 1.65;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  max-width: 46ch;
  margin: 0 0 1.1em;
  color: var(--ink);
}
.complexity-text p + p { color: var(--ink-70); }
.complexity-media { margin: 0; }
.complexity-media figcaption { margin-top: 10px; color: var(--ink-50); }

/* ---- 02 · the clarity — pull quote + article blocks ---- */
.clarity-sub {
  font-size: var(--fs-lede);
  line-height: 1.4;
  color: var(--ink-70);
  font-style: italic;
  margin: clamp(16px, 2.4vh, 24px) 0 0;
  max-width: 40ch;
}
.clarity-blocks {
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 7vh, 88px);
  margin-top: clamp(48px, 8vh, 96px);
}
.clarity-block {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(20px, 3vw, 44px);
  align-items: center;
}
@media (min-width: 860px) {
  .clarity-block--media-right { grid-template-columns: 1fr 0.85fr; }
  .clarity-block--media-left { grid-template-columns: 0.85fr 1fr; }
  .clarity-block--media-left .clarity-block-text { order: 2; }
  .clarity-block--media-left .clarity-block-media { order: 1; }
}
.clarity-block--full { grid-template-columns: 1fr; }
.clarity-block-text p {
  font-size: var(--fs-body);
  line-height: 1.65;
  font-variation-settings: "opsz" 18, "SOFT" 35;
  margin: 0;
  max-width: 52ch;
}
.clarity-block-media { margin: 0; }
.clarity-block--full .clarity-block-media { margin-top: clamp(16px, 2.4vh, 28px); }

/* ---- 03 · the beauty — intro · gallery · metrics band · galleries ---- */
.beauty-intro {
  font-size: var(--fs-lede);
  line-height: 1.5;
  color: var(--ink);
  max-width: 62ch;
  margin: 0 0 clamp(28px, 5vh, 48px);
  text-wrap: pretty;
}

/* ---- Justified gallery (cvGallery in app.js) ----
   Rows of photos at their own ratios, flush to both margins, nothing cropped.

   The whole layout rests on one number: --ph-ar, the photo's real width/height,
   set per tile from the CMS (or measured on load for older items). Because both
   the basis AND the growth of a tile are proportional to its ratio, every width
   in a wrapped row stays proportional to its ratio — which is the definition of
   a row sharing one height. So flexbox does the justifying; there's no layout
   pass in JS.

   --gal-row is the height a row lands on when the basis widths fill the line
   exactly; in practice a row grows a little past it to close the line. */
.gal-grid {
  --gal-row: clamp(150px, 17vw, 240px);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(10px, 1.2vw, 16px);
}
.gal-grid > .ph {
  --ph-ar: 1.5;
  min-height: 0;
  min-width: 0;
  flex-grow: var(--ph-ar);
  flex-shrink: 1;
  flex-basis: calc(var(--ph-ar) * var(--gal-row));
  aspect-ratio: var(--ph-ar);
}
/* A hero takes the row to itself, still at its own ratio — the one authoring
   lever the flow honours. */
.gal-grid > .gal-full { flex-basis: 100%; }

/* The last row's brake (appended by cvGallery). A trailing row of one photo
   would otherwise grow to the full width — a 3:2 blown up to a 850px-tall
   accident, purely because it happened to be last. This spacer sits at the end
   of the line and takes the slack instead, so a short final row stays at the
   row height the rest of the gallery is using and simply runs out.

   basis:0 keeps it on whatever line still has room (always the last one), and
   the huge grow makes it out-argue every photo for the free space. It carries
   no height of its own, so it never becomes a row. */
.gal-fill {
  flex: 999999 0 0;
  align-self: flex-start;
  height: 0;
  pointer-events: none;
}
/* "2up": a taller target row, so fewer photos fit per line. Not two columns —
   the rows decide the count — but it's the "show these big" the option meant. */
.gal-grid--2up { --gal-row: clamp(220px, 26vw, 360px); }

/* Below the tablet breakpoint a row can only hold one photo without shrinking
   it past reading size, so the grid becomes a stack and each tile simply wears
   its photo. */
@media (max-width: 720px) {
  .gal-grid > .ph {
    flex-basis: 100%;
    max-width: 100%;
  }
}

/* Nothing is cropped, so the media just fills the tile its ratio already
   shaped. (This replaced object-fit: cover, which was the crop.) */
.gal-grid > .ph.has-media > img,
.gal-grid > .ph.has-media > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: inherit;
}

/* Clickable photos. The old hover floated the media out of its cell to show the
   part being cropped — there's no crop left to answer for, so hover is now an
   invitation to open the stage, and it has to carry that on its own: `cursor`
   says nothing here, because body.gcursor-on sets `cursor: none` on everything
   and the orange dot IS the pointer. So the tile answers instead — it lifts a
   little and offers a badge in the case's colour.
   (zoom-in is still declared for the contexts without the custom cursor.) */
.gal-grid > .ph.is-zoomable { cursor: zoom-in; }

/* The badge: a dark disc (::before) under an expand glyph (::after, drawn as a
   mask in the house idiom — see .vr-ico). ::before is free here; it's the play
   glyph only on .ph--video, and a video is never zoomable.

   The disc is neutral, not the case's accent, and that's deliberate: it sits on
   top of a photograph, and the accent has no contrast to promise against one.
   Starry's yellow badge landed on Starry's yellow key art and vanished — the
   one thing an affordance can't do. The accent gets its say in the ring. */
.gal-grid > .ph.is-zoomable::before,
.gal-grid > .ph.is-zoomable::after {
  content: "";
  position: absolute;
  inset: auto 10px 10px auto;
  width: 34px;
  height: 34px;
  z-index: 1;
  opacity: 0;
  transform: translateY(6px) scale(0.9);
  pointer-events: none;
  transition: opacity 260ms var(--ease-out-emph), transform 260ms var(--ease-out-emph);
}
.gal-grid > .ph.is-zoomable::before {
  left: auto; top: auto;
  border-radius: var(--radius-circle);
  background: rgba(10, 10, 10, 0.58);
  border: 1.5px solid var(--case-color);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 6px 18px -6px rgba(0, 0, 0, 0.55);
}
/* Overrides .ph::after, which is the placeholder's label — a full-bleed centred
   grid of type. None of that applies to a tile that has its photo. */
.gal-grid > .ph.is-zoomable::after {
  display: block;
  padding: 0;
  background: #fff;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 3H3v6M15 21h6v-6M3 15v6h6M21 9V3h-6'/></svg>") no-repeat center / 15px;
  mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 3H3v6M15 21h6v-6M3 15v6h6M21 9V3h-6'/></svg>") no-repeat center / 15px;
}
@media (hover: hover) and (pointer: fine) {
  .gal-grid > .ph.is-zoomable {
    transition: box-shadow 380ms var(--ease-out-emph), transform 380ms var(--ease-out-emph);
  }
  /* The whole tile grows, frame and photo together — scaling the photo inside a
     fixed frame would crop it on hover, which is the one thing this gallery
     exists not to do. */
  .gal-grid > .ph.is-zoomable:hover {
    transform: scale(1.025);
    box-shadow: 0 22px 46px -18px rgba(0, 0, 0, 0.5);
    z-index: 2;
  }
  .gal-grid > .ph.is-zoomable:hover::before,
  .gal-grid > .ph.is-zoomable:hover::after { opacity: 1; transform: none; }
}
/* Keyboard gets the same offer, badge and all. */
.gal-grid > .ph.is-zoomable:focus-visible {
  outline: 2px solid var(--case-accent);
  outline-offset: 3px;
}
.gal-grid > .ph.is-zoomable:focus-visible::before,
.gal-grid > .ph.is-zoomable:focus-visible::after { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .gal-grid > .ph.is-zoomable,
  .gal-grid > .ph.is-zoomable::before,
  .gal-grid > .ph.is-zoomable::after { transition: none; }
  .gal-grid > .ph.is-zoomable:hover { transform: none; }
  /* The badge still appears — it's the affordance, not the flourish. */
  .gal-grid > .ph.is-zoomable::before,
  .gal-grid > .ph.is-zoomable::after { transform: none; }
}

/* ---- Proof band (cvProofBand in app.js) ----
   The hook: three numbers and the award tiers, on the accent surface, closing
   the hero. Deliberately one line — the full metrics band and awards block
   below are the record, and this is the beat that earns the scroll to them. So
   it stays slim: values a third of the metrics band's size, labels on one line,
   nothing that turns it into a table.

   Full-bleed out of the hero's gutter, and pulled down over its bottom padding
   so it lands flush on the fold instead of floating on a strip of paper. */
.proof-band {
  margin: clamp(28px, 4vh, 48px) calc(-1 * var(--gutter)) calc(-1 * clamp(32px, 5vh, 64px));
  padding: clamp(20px, 2.6vh, 30px) var(--gutter);
  background: var(--case-color);
  color: var(--case-ink);
}
.proof-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(18px, 2.4vw, 40px);
}
/* The metrics row. On desktop this is exactly what it looks like — a flex row —
   and .proof-track / .proof-set are inert layers the phone's marquee needs (see
   the max-width: 719px block). The track's second copy is display:none here. */
.proof-metrics {
  display: flex;
  align-items: baseline;
  min-width: 0;
}
.proof-track {
  display: flex;
  align-items: baseline;
}
.proof-set {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: clamp(18px, 2.6vw, 44px);
}
.proof-set[aria-hidden] { display: none; }
.proof-metric {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.proof-metric .pm-value {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(22px, 2.1vw, 32px);
  line-height: 1;
  letter-spacing: -0.02em;
  font-variation-settings: "opsz" 96, "SOFT" 30;
  font-weight: 400;
  color: var(--case-ink);
}
.proof-metric .pm-label {
  font-size: clamp(9px, 0.72vw, 10.5px);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.3;
  color: color-mix(in srgb, var(--case-ink) 62%, transparent);
  /* A metric is only as wide as its label, and the labels aren't a matched set
     — "Household penetration" against "Facebook reveal-video impressions · 6.4×
     average". Left to run, the long ones push the awards onto a second row and
     the band stops being a line. Wrapping the caption is the cheaper loss. */
  max-width: 22ch;
}

/* Each side is its own cluster: content, then the control that opens the full
   version of it. Both sides end in the same .proof-link so the two reads the
   same way. */
.proof-side {
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.6vw, 26px);
  flex-wrap: wrap;
}
.proof-side--awards { gap: clamp(10px, 1vw, 16px); }
.proof-tiers {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.proof-tier {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(12px, 1vw, 14px);
  font-variation-settings: "opsz" 40, "SOFT" 30;
  font-style: italic;
  line-height: 1;
  padding: 6px 11px 7px;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid color-mix(in srgb, var(--case-ink) 30%, transparent);
  white-space: nowrap;
}
/* The control both sides end in. Reads as a target rather than a glyph: a
   labelled pill that says where it goes, and large enough to aim the cursor
   dot at. Inverts on hover — ink fills, the label reads back in the case's
   colour — so the affordance is unmissable on a saturated surface. */
.proof-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 7px 13px 8px;
  border-radius: var(--radius-pill);
  border: 1px solid color-mix(in srgb, var(--case-ink) 32%, transparent);
  color: var(--case-ink);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: clamp(9px, 0.72vw, 10.5px);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background 200ms var(--ease-out-emph), border-color 200ms var(--ease-out-emph), color 200ms var(--ease-out-emph);
}
.proof-link:hover,
.proof-link:focus-visible {
  background: var(--case-ink);
  border-color: var(--case-ink);
  color: var(--case-color);
  outline: none;
}
.proof-arrow {
  display: inline-block;
  transition: transform 200ms var(--ease-out-emph);
}
.proof-link:hover .proof-arrow { transform: translateY(2px); }
@media (prefers-reduced-motion: reduce) {
  .proof-arrow, .proof-link { transition: none; }
  .proof-link:hover .proof-arrow { transform: none; }
}
/* Stacked hero: the band is the last thing in a flex column, so it only needs
   its bleed back — the grid row placement above doesn't apply. */
@media (max-width: 859px) {
  .proof-inner { justify-content: flex-start; }
}

/* ---- The band on a phone ----
   Stacking everything turned a one-line hook into a ~370px wall: three metrics
   in a column, then award tiers wrapping onto their own rows. So the line is
   kept and the metrics travel it as a marquee.

   The two sides collapse (display: contents) so the metrics take a row of their
   own and the two controls share the next one — two rows total, not five.

   The tiers go. On a phone they were the tallest part of the awards and the
   least of it; the count is the fact, and it's already the control. */
@media (max-width: 719px) {
  .proof-side { display: contents; }
  .proof-inner {
    flex-wrap: wrap;
    gap: 14px 10px;
  }
  .proof-tiers { display: none; }

  /* The metrics keep their line and travel it themselves. Both edges fade,
     because on a marquee there is always something arriving on one side and
     leaving on the other — the gradient is the horizon, not a hint. */
  .proof-metrics {
    flex: 0 0 100%;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 32px, #000 calc(100% - 44px), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 32px, #000 calc(100% - 44px), transparent 100%);
  }

  /* The loop. The track holds the metrics twice and slides by half of itself,
     which puts the copy exactly where the original began — the seam never
     lands anywhere you can see it. Being a percentage of its own width, it
     needs no measurement and survives any number of metrics at any length.
     (Which is why the sets carry their own trailing gap instead of the track
     having one: a gap between the sets would make half the track ≠ one set,
     and the loop would jump every pass.) */
  .proof-track {
    width: max-content;
    flex-wrap: nowrap;
    will-change: transform;
    animation: proof-marquee var(--marquee-dur, 19s) linear infinite;
  }
  .proof-track.is-paused { animation-play-state: paused; }
  .proof-set {
    flex-wrap: nowrap;
    padding-right: clamp(18px, 2.6vw, 44px);
  }
  .proof-set[aria-hidden] { display: flex; }
  .proof-metric { flex: 0 0 auto; }
  /* Each metric is a column that travels past, so here the label sets the width
     instead of being capped by it — there's no neighbour to crowd. */
  .proof-metric .pm-label {
    white-space: nowrap;
    max-width: none;
  }
}

@keyframes proof-marquee {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

/* Reduced motion can't just stop the marquee: the track is wider than its
   frame and the frame clips, so a frozen loop would hide two of the three
   metrics behind an edge with no way to reach them. The row goes back to being
   swipeable instead — same line, same numbers, the reader moves it. */
@media (max-width: 719px) and (prefers-reduced-motion: reduce) {
  .proof-metrics {
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* No leading fade with nothing off-screen to the left at rest — it would
       be eating the best number to answer a question nobody asked yet. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 48px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 48px), transparent 100%);
  }
  .proof-metrics::-webkit-scrollbar { display: none; }
  .proof-track {
    width: auto;
    animation: none;
  }
  .proof-set[aria-hidden] { display: none; }
  .proof-metric { scroll-snap-align: start; }
}

/* Metrics band — accent surface, the full record, and a visual break. The proof
   band in the hero carries the first three of these as the hook. */
.metrics-band {
  margin: clamp(48px, 8vh, 96px) calc(-1 * var(--gutter));
  padding: clamp(40px, 7vh, 76px) var(--gutter);
  background: var(--case-color);
  color: var(--case-ink);
  scroll-margin-top: 132px;
}
.metrics-band .metrics-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(24px, 3.5vh, 36px) clamp(24px, 5vw, 64px);
  max-width: 1120px;
}
@media (min-width: 600px) { .metrics-band .metrics-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .metrics-band .metrics-grid { grid-template-columns: repeat(3, 1fr); } }
.metrics-band .metric {
  gap: 8px;
  padding-top: 16px;
  border-top: 1px solid color-mix(in srgb, var(--case-ink) 22%, transparent);
}
.metrics-band .m-value {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(30px, 4vw, 52px);
  line-height: 0.95;
  letter-spacing: -0.02em;
  font-variation-settings: "opsz" 96, "SOFT" 30;
  font-weight: 400;
  color: var(--case-ink);
}
.metrics-band .m-label {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-mono);
  letter-spacing: 0.03em;
  text-transform: none;
  line-height: 1.4;
  color: color-mix(in srgb, var(--case-ink) 68%, transparent);
  max-width: 26ch;
}

/* Named campaign-asset galleries */
.asset-gallery { margin-top: clamp(40px, 7vh, 80px); }
.gallery-head { margin-bottom: clamp(16px, 2.4vh, 24px); }
.gallery-title {
  font-size: var(--fs-h-sm);
  line-height: 1.1;
  letter-spacing: -0.01em;
  font-style: italic;
  font-variation-settings: "opsz" 60, "ital" 1, "SOFT" 35;
  font-weight: 400;
  margin: 0;
}
.gallery-note { color: var(--ink-50); margin: 6px 0 0; }

/* ---- Press blocks — as featured in · awards · agency partners ---- */
.press-block {
  margin-top: clamp(48px, 8vh, 88px);
  padding-top: clamp(28px, 4vh, 40px);
  border-top: 0.5px solid var(--hairline);
}
/* Clears the fixed masthead + toolbar when the proof band's "N awards ↓" lands
   here — same allowance the metrics band makes. */
.press-block.awards { scroll-margin-top: 132px; }
.block-title {
  color: var(--ink-50);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 0 0 clamp(18px, 3vh, 28px);
}
.logo-row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: clamp(12px, 1.6vw, 20px);
}
.logo-chip { display: block; }
.logo-chip .ph--logo {
  width: clamp(120px, 15vw, 168px);
  height: clamp(56px, 7vw, 76px);
  min-height: 0;
  transition: border-color 220ms var(--ease-out-emph), background 220ms var(--ease-out-emph);
}
.logo-row--sm .ph--logo { width: clamp(104px, 12vw, 140px); height: clamp(48px, 6vw, 64px); }
.logo-chip:hover .ph--logo {
  border-color: color-mix(in srgb, var(--case-color) 55%, transparent);
  background: color-mix(in srgb, var(--case-color) 12%, transparent);
}
.logo-chip:hover .ph--logo::after { color: var(--ink); }

/* Agency partners as low-key running text (no chips/buttons). */
.partners-line {
  color: var(--ink-70);
  margin: 0;
  line-height: 1.65;
  max-width: 60ch;
}

.awards-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: clamp(14px, 2.2vh, 20px); }
.award { display: flex; align-items: center; gap: clamp(14px, 1.6vw, 20px); }
.award .ph--award { flex: 0 0 auto; width: 64px; height: 64px; min-height: 0; border-radius: var(--radius-sm); }
.award-copy { display: flex; flex-direction: column; gap: 3px; }
.award-tier {
  font-family: "Fraunces", Georgia, serif;
  font-size: var(--fs-lede);
  font-style: italic;
  color: var(--case-accent);
  line-height: 1.1;
}
.award-name { font-size: var(--fs-meta); color: var(--ink-70); line-height: 1.4; max-width: 60ch; }

/* Real media swapped into a placeholder box (once a url is set via the admin). */
.ph.has-media { background: #000; border-color: transparent; }
.ph.has-media::after { content: none; }
.ph.has-media > img,
.ph.has-media > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Article/gallery videos autoplay (muted, once) when they scroll into view —
   the poster + play badge show until the first frame actually renders, then
   the video fades over them. is-playing sticks after pause/end so the frozen
   frame stays visible. */
.ph.has-media > video {
  opacity: 0;
  transition: opacity 500ms var(--ease-out-emph);
}
.ph.is-playing > video { opacity: 1; }
.ph.is-playing::before,
.ph.is-playing .ph-play { opacity: 0; transition: opacity 300ms var(--ease-out-emph); }
/* The sound pill only appears once the video is actually running. */
.ph .reel-sound {
  z-index: 4;
  opacity: 0;
  pointer-events: none;
  transition: opacity 300ms var(--ease-out-emph), border-color 200ms var(--ease-out-emph), background 200ms var(--ease-out-emph);
}
.ph.is-playing .reel-sound { opacity: 1; pointer-events: auto; }
/* Inside a transport bar the pill is just another control — the bar owns when
   the whole group is visible, so the tile-level fade above must not apply.
   (Restated here because .ph .reel-sound is declared after .video-ctl's rule
   at the same specificity and would otherwise win.) */
.ph .video-ctl .reel-sound { opacity: 1; pointer-events: auto; }
.ph--logo.has-media { background: rgba(var(--ink-rgb), 0.04); padding: 12px; }
.ph--logo.has-media > img { object-fit: contain; }
/* Award thumbnails are logos/badges — show the whole image (contain) rather
   than cropping it (cover). Neutral tile behind so any aspect ratio sits clean. */
.ph--award.has-media { background: rgba(var(--ink-rgb), 0.04); padding: 6px; }
.ph--award.has-media > img { object-fit: contain; }
.ph.has-media .ph-play {
  position: absolute;
  left: 50%; top: 50%;
  width: 54px; height: 54px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-circle);
  background: rgba(8, 8, 8, 0.55);
  border: 1px solid rgba(250, 250, 247, 0.3);
  z-index: 1;
}
.ph.has-media .ph-play::after {
  content: "▶";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  font-size: 15px;
  padding-left: 3px;
}
.award-link {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-mono);
  color: var(--case-accent);
  margin-top: 2px;
}

/* Fade the restructured hero in with the rest during the launch entrance. */
#screen-starry.case-prefade > section.case-hero { opacity: 0; }
#screen-starry.case-stage-in > section.case-hero { animation: case-fade-in 500ms ease-out 120ms both; }
