:root {
  /* ORYZO design system — colors */
  --color-warm-cream: #ffedd7;
  --color-walnut-shadow: #100904;
  --color-cork-border: #4a1f1f;
  --color-driftwood: #c99494;
  --color-brand-red: #7c1e1e;
  --color-case-accent: #7c1e1e;
  --color-off-white: #ffffff;
  --color-image-stroke: rgba(16, 9, 4, 0.2);
  --color-grey: #737373; /* darkened from #8c8c8c: 3.36:1 on white failed WCAG AA; #737373 gives 4.74:1 */
  --color-hairline-on-white: #e4e0db; /* dividers on the off-white case-detail background; --color-image-stroke composited on white renders ~#cfcecd, a visibly cooler/darker grey, so kept distinct */

  /* Line weights — dividers, image frames, underlines, icon strokes, rails
     use --line-hairline; quote/modal accent borders use --line-accent. */
  --line-hairline: 1px;
  --line-accent: 2px;

  /* White text opacity hierarchy — for text sitting on dark/photo
     backgrounds only (not backgrounds/borders, which keep --color-off-white). */
  --text-primary: rgba(255, 255, 255, 1);    /* headlines, names, project titles */
  --text-secondary: rgba(255, 255, 255, 0.7); /* subtitles, role labels, section labels, taglines */
  --text-tertiary: rgba(255, 255, 255, 0.8);  /* small functional UI: hints, nav cues, footnotes, timestamps */

  --font-serif: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --font-statement: 'Bodoni Moda', 'Cormorant Garamond', Georgia, serif;
  --font-accent: 'Schibsted Grotesk', var(--font-display);

  /* Typography */
  --font-display: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  --leading-display: 0.9;
  --text-heading: 41px;
  --leading-heading: 0.9;
  --leading-heading-sm: 1.09;
  --outcome-label-size: clamp(20px, 1.9vw, 36px);

  /* Fluid type scale for small UI text — labels, captions, badges, nav
     meta, ledger values. These were previously fixed px (tuned for a
     ~1440px viewport) with no viewport-relative component, so unlike the
     rest of the type system above they never grew on larger screens: past
     ~1770-1900px everything else here has already scaled up and leveled
     off, but this text stayed pinned at its mobile-tuned size, reading as
     disproportionately tiny on a big monitor. Same clamp(min, Nvw, max)
     shape as the tokens above, tuned so each tier finishes growing in the
     same ~1770-1900px range. */
  --text-3xs: clamp(11px, 0.75vw, 13px);
  --text-2xs: clamp(12px, 0.8vw, 14px);
  --text-xs: clamp(13px, 0.9vw, 16px);
  --text-sm: clamp(14px, 0.95vw, 17px);
  --text-md: clamp(16px, 1.05vw, 19px);
  --text-lg: clamp(18px, 1.2vw, 22px);
  --text-xl: clamp(22px, 1.55vw, 28px);

  /* Body-copy "Lead" tier — the single size/style for all grey body
     paragraphs across case studies: intro pairs (Overview/Problem/Context,
     Research/Exploration/Validation), numbered-decision-list item
     descriptions (Onboarding's key-decisions, Portfolio's Approach/Final
     Solution, Configurator's Design Evolution), and reflection copy.
     Previously split into three near-identical tiers (Lead/Secondary/Detail)
     that read as inconsistent sizes for the same grey body-copy role;
     collapsed to one so all grey paragraphs match. */
  --body-lead-size: clamp(17px, 1.3vw, 23px);
  --leading-body-lead: 1.5;

  /* Spacing */
  --spacing-6: 6px;
  --spacing-8: 8px;
  --spacing-10: 10px;
  --spacing-12: 12px;
  --spacing-18: 18px;
  --spacing-24: 24px;
  --spacing-31: 31px;
  --spacing-48: 48px;
  --spacing-62: 62px;
  --spacing-68: 68px;
  --spacing-96: 96px;

  /* Distance from the viewport edge to the vertical rail labels
     ("Case study" left, "Scroll down to case study" / "Back to top"
     right) — shared by .case-nav__label (styles.css) and
     .story__progress-hint/.story__progress-top (scroll-story.css) so
     both stay equidistant from their edge at any width. */
  --rail-label-gap: 22px;

  /* Radius */
  --radius-cards: 0px;
  --radius-inputs: 0px;
  --radius-buttons-pill: 0px;
  --radius-buttons-outlined: 0px;
  --radius-full: 0px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  font-family: var(--font-display);
  color: var(--color-warm-cream);
  background: var(--color-walnut-shadow);
}

a { text-decoration: none; color: inherit; }
button { font-family: inherit; border: none; cursor: pointer; background: none; }
ul { list-style: none; margin: 0; padding: 0; }

/* ---------- HERO ---------- */

.hero {
  position: relative;
  min-height: 100vh;
  width: 100%;
  overflow: hidden;
  isolation: isolate;
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: -3;
  background: var(--color-walnut-shadow);
}

.hero__photo {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform-origin: 50% 50%;
  opacity: 1;
  transition: opacity 0.25s ease, transform 0.4s ease;
}

.hero__photo.is-loading { opacity: 0; }

.hero__video {
  /* Tune here to re-grade the composited screen content against the desk
     photo's warmer, softer lighting — see .hero-video__grade below for the
     matching overlay that carries the actual warm tint + vignette. */
  --hero-video-filter: brightness(0.97) contrast(0.95) saturate(0.9);

  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  filter: var(--hero-video-filter);
  transition: opacity 0.5s ease;
  pointer-events: none;
}

.hero__video.is-visible { opacity: 1; }

/* Portfolio Overview (case 2)'s composite sits against the same warm,
   golden-amber curtain/desk scene as .hero-video__grade--portfolio above.
   The Configurator default above reads slightly washed out there, so it's
   recalibrated warmer/less saturated. Toggled by hero-video-overlay.js's
   positionHitbox() via .is-portfolio-grade on .hero. */
.hero.is-portfolio-grade .hero__video {
  --hero-video-filter: brightness(0.97) contrast(0.95) saturate(0.92);
}

.hero__screen {
  position: absolute;
  z-index: 1;
  overflow: hidden;
  object-fit: cover;
  background: #e4e4e0;
  opacity: 0;
  transition: opacity 0.35s ease;
}

.hero__screen.is-visible {
  opacity: 1;
}

/* Case 3's screen content: sized/positioned in JS (positionScreenVideo() in
   main.js) to cover the whole hero exactly like a full-bleed case video,
   then clip-path'd down to just the screen's quad so the real photo shows
   through everywhere else (bezel, keyboard, desk, background). The video
   itself is already perspective-warped (see scratchpad/composite_onboarding.py)
   so its content matches the photographed screen's tilt, not just a
   straight clip. The recording is a crisp, evenly-lit screen capture; the
   photo behind it is a warm, sunlit desk scene, so a subtle filter is
   applied directly here to knock down brightness/contrast/saturation and
   shift it toward the same beige/honey cast. */
.hero__screen-video {
  position: absolute;
  z-index: 1;
  overflow: hidden;
  object-fit: cover;
  background: #e4e4e0;
  opacity: 0;
  filter: brightness(0.93) contrast(0.94) saturate(0.82);
  transition: opacity 0.35s ease;
  pointer-events: none;
}

.hero__screen-video.is-visible {
  opacity: 1;
}

/* Mobile-only Configurator (case 1) screen overlay - see the @media
   (max-width: 980px) block below for its visible/positioned rules. Hidden
   here so desktop (where no rule below applies) never lays it out. */
.hero__mobile-screen-window { display: none; }

/* Mobile-only ghost title (see @media (max-width: 980px) below) — straddles
   the hero photo's bottom edge and the white section beneath it. Not part
   of the desktop layout. */
.hero__ghost-title { display: none; }

/* ---------- HERO VIDEO OVERLAY ---------- */
/* Hover/tap overlay scoped to just the "screen" rect of whichever case's
   video mockup is active (the monitor/laptop glass, not the desk/bezel
   around it) — positioned in JS (hero-video-overlay.js) via the same
   LaptopMockup.computeScreenRect() math used for .hero__screen, against
   per-case rects measured from each video's native 1920x1080 frame.
   Disabled below the 980px breakpoint since hover-scrub doesn't apply on
   touch — unlike .hero__screen/.hero__screen-video, this isn't shown on
   mobile at all, so its cover-fit positioning math is never exercised
   there. */

.hero-video__hitbox {
  position: absolute;
  z-index: 2;
}

/* Color-grade overlay: sits directly on top of the video, clipped in JS
   (hero-video-overlay.js, same call as .hero-video__scrim below) to the
   exact same quad/rect as the video's screen area, so it's registered to
   the skewed monitor glass with no transform math duplicated here. All the
   tunable knobs live in this one block — adjust these to live-iterate the
   warmth/strength without touching the clip-path or positioning logic. */
.hero-video__grade {
  --grade-tint: 196, 168, 138; /* warm tan/beige sampled from the desk/wall behind the monitor */
  --grade-tint-strength: 0.12; /* tint opacity at the 135deg gradient's start (top-left) */
  --grade-tint-fade: 0.04; /* tint opacity at the 40% stop, fading out before the vignette */
  --grade-vignette: 20, 20, 20; /* vignette color, darkening toward all four corners */
  --grade-vignette-strength: 0.10; /* vignette opacity at the corners */
  --grade-vignette-reach: 55%; /* how far the vignette stays fully transparent before darkening in */

  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      120% 120% at 50% 50%,
      rgba(0, 0, 0, 0) var(--grade-vignette-reach),
      rgba(var(--grade-vignette), var(--grade-vignette-strength)) 100%
    ),
    linear-gradient(
      135deg,
      rgba(var(--grade-tint), var(--grade-tint-strength)) 0%,
      rgba(var(--grade-tint), var(--grade-tint-fade)) 40%,
      rgba(var(--grade-tint), 0) 100%
    );
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* Portfolio Overview (case 2) grade: this scene's laptop sits against a
   golden-amber curtain + wood desk, warmer and more saturated than the
   Configurator's tan wall, and its screen has a near-black-green sidebar
   rail (~9% of the screen width, measured on the composite's native frame)
   that would muddy toward brown under the same tint strength used over the
   white content panel. So the warmth is split into two horizontal zones
   instead of one flat diagonal gradient: a restrained tint over the
   sidebar, a fuller one over the content panel. Also has no `quad` in
   SCREEN_RECTS (js/hero-video-overlay.js) — the laptop is shot near
   head-on with negligible lid skew, so the shared .hero-video__grade rect
   needs no extra keystone/perspective transform, just this background
   override. Toggled onto the shared .hero-video__grade element by
   positionHitbox() in hero-video-overlay.js whenever case 2 is active. */
.hero-video__grade--portfolio {
  --grade-tint-sidebar: 168, 150, 112; /* muted honey, kept light so the dark green rail doesn't shift brown */
  --grade-tint-sidebar-strength: 0.05;
  --grade-tint-content: 214, 166, 104; /* golden-amber sampled from the curtain/wood behind the laptop */
  --grade-tint-content-strength: 0.17; /* tint opacity where the content-panel zone starts */
  --grade-tint-content-fade: 0.05; /* tint opacity at the 60% stop, fading out before the right edge */
  --grade-sidebar-edge: 9%; /* sidebar/content boundary measured on the composite's screen rect */
  --grade-vignette: 46, 30, 14; /* warm dark-amber vignette, rather than neutral, to match the golden light */
  --grade-vignette-strength: 0.13;
  --grade-vignette-reach: 52%;

  background:
    radial-gradient(
      120% 120% at 50% 50%,
      rgba(0, 0, 0, 0) var(--grade-vignette-reach),
      rgba(var(--grade-vignette), var(--grade-vignette-strength)) 100%
    ),
    linear-gradient(
      to right,
      rgba(var(--grade-tint-sidebar), var(--grade-tint-sidebar-strength)) 0%,
      rgba(var(--grade-tint-sidebar), var(--grade-tint-sidebar-strength)) var(--grade-sidebar-edge),
      rgba(var(--grade-tint-content), var(--grade-tint-content-strength)) calc(var(--grade-sidebar-edge) + 2%),
      rgba(var(--grade-tint-content), var(--grade-tint-content-fade)) 60%,
      rgba(var(--grade-tint-content), 0) 100%
    );
}

.hero-video__scrim {
  position: absolute;
  inset: 0;
  background: rgba(16, 9, 4, 0.15);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

.hero-video__hitbox:hover .hero-video__scrim {
  opacity: 1;
}

.hero-video__controls {
  position: absolute;
  right: var(--spacing-12);
  bottom: var(--spacing-12);
  display: flex;
  align-items: flex-end;
  gap: var(--spacing-8);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

.hero-video__hitbox:hover .hero-video__controls,
.hero-video__controls:focus-within {
  opacity: 1;
  pointer-events: auto;
}

/* Frame scrubber (prev/indicator/next): a horizontal pill, only relevant
   once a frame is paused, so it's absent (not just faded) the rest of the
   time — an empty bordered box would otherwise peek out next to the
   always-present actions pill below. */
.hero-video__scrubber {
  display: none;
  align-items: stretch;
  background: rgba(16, 9, 4, 0.55);
  border: var(--line-hairline) solid rgba(255, 255, 255, 0.5);
}

.hero.is-video-paused .hero-video__scrubber {
  display: flex;
}

/* Pause/expand: a horizontal pill, separate from the scrubber's row so it
   reads as a fixed corner control independent of whether the scrubber is
   showing. */
.hero-video__actions {
  display: flex;
  flex-direction: row;
  background: rgba(16, 9, 4, 0.55);
  border: var(--line-hairline) solid rgba(255, 255, 255, 0.5);
}

.hero-video__btn,
.hero-video__arrow {
  width: 32px;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-tertiary);
  transition: background 0.2s ease, color 0.2s ease;
}

.hero-video__scrubber > *:not(:first-child) {
  border-left: var(--line-hairline) solid rgba(255, 255, 255, 0.35);
}

.hero-video__actions > *:not(:first-child) {
  border-left: var(--line-hairline) solid rgba(255, 255, 255, 0.35);
}

/* Driftwood hover bg is light — bump back to full-strength text so the icon
   doesn't drop further below the already-tight contrast against it. */
.hero-video__btn:hover,
.hero-video__arrow:hover {
  background: var(--color-driftwood);
  color: var(--text-primary);
}

.hero-video__arrow:disabled {
  opacity: 0.3;
  pointer-events: none;
  background: transparent;
}

.hero-video__indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--spacing-8);
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-medium);
  color: var(--text-tertiary);
  white-space: nowrap;
}

/* Icons — hand-drawn with borders/pseudo-elements, matching .side-tab__arrow
   rather than pulling in an icon set. */

.hero-video__icon { display: block; }
.hero-video__icon[hidden] { display: none; }

.hero-video__icon--play {
  width: 0;
  height: 0;
  margin-left: 2px;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 9px solid currentColor;
}

.hero-video__icon--pause {
  position: relative;
  width: 9px;
  height: 11px;
}

.hero-video__icon--pause::before,
.hero-video__icon--pause::after {
  content: '';
  position: absolute;
  top: 0;
  width: 3px;
  height: 11px;
  background: currentColor;
}

.hero-video__icon--pause::before { left: 0; }
.hero-video__icon--pause::after { right: 0; }

.hero-video__icon--expand {
  position: relative;
  width: 12px;
  height: 12px;
}

.hero-video__icon--expand::before,
.hero-video__icon--expand::after {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  border: var(--line-hairline) solid currentColor;
}

.hero-video__icon--expand::before {
  top: 0;
  left: 0;
  border-right: none;
  border-bottom: none;
}

.hero-video__icon--expand::after {
  bottom: 0;
  right: 0;
  border-left: none;
  border-top: none;
}

.hero-video__icon--chevron {
  width: 8px;
  height: 8px;
  border-top: var(--line-hairline) solid currentColor;
  border-right: var(--line-hairline) solid currentColor;
}

.hero-video__arrow--prev .hero-video__icon--chevron { transform: rotate(-135deg); }
.hero-video__arrow--next .hero-video__icon--chevron { transform: rotate(45deg); }

.hero-video__icon--close {
  position: relative;
  width: 14px;
  height: 14px;
}

.hero-video__icon--close::before,
.hero-video__icon--close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: var(--line-hairline);
  background: currentColor;
}

.hero-video__icon--close::before { transform: translateY(-50%) rotate(45deg); }
.hero-video__icon--close::after { transform: translateY(-50%) rotate(-45deg); }

/* Touch: no hover, so keep controls always visible but subdued instead of
   requiring a tap-to-reveal step. (Phones fall under the 980px breakpoint
   below, where the hitbox is disabled entirely — this mainly covers wider
   touch devices like tablets/touch laptops.) */
@media (hover: none) {
  .hero-video__scrim {
    opacity: 0.6;
  }
  .hero-video__controls {
    opacity: 0.7;
    pointer-events: auto;
  }
  .hero.is-video-paused .hero-video__scrim,
  .hero.is-video-paused .hero-video__controls {
    opacity: 1;
  }
}

/* ---------- VIDEO LIGHTBOX ---------- */

.video-lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-lightbox[hidden] { display: none; }

.video-lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(16, 9, 4, 0.9);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.video-lightbox.is-visible .video-lightbox__backdrop {
  opacity: 1;
}

.video-lightbox__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-18);
  max-width: 90vw;
  max-height: 90vh;
  padding: var(--spacing-24);
  background: var(--color-walnut-shadow);
  border: var(--line-accent) solid var(--color-case-accent);
  opacity: 0;
  transform: scale(0.98);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.video-lightbox--image .video-lightbox__content {
  border-color: transparent;
}

.video-lightbox.is-visible .video-lightbox__content {
  opacity: 1;
  transform: scale(1);
}

.video-lightbox__stage {
  /* No fixed aspect-ratio frame: source screenshots aren't all the same
     ratio (mostly 16:9, some closer to 4:3), and letterboxing them inside
     a fixed-ratio box leaves uneven left/right vs top/bottom whitespace.
     Sizing the media itself to the max bounds lets each slide fill the
     frame edge-to-edge on every axis, so the surrounding padding on
     .video-lightbox__content reads as consistent space on all sides. */
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-lightbox__video,
.video-lightbox__image {
  display: block;
  width: auto;
  height: auto;
  max-width: min(84vw, 1200px);
  max-height: 70vh;
  object-fit: contain;
}

.video-lightbox__video[hidden],
.video-lightbox__image[hidden] { display: none; }

.video-lightbox__close {
  position: absolute;
  top: var(--spacing-12);
  right: var(--spacing-12);
  z-index: 2;
  width: 36px;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  transition: color 0.2s ease;
}

.video-lightbox__close:hover { color: var(--color-driftwood); }

.video-lightbox__controls {
  display: flex;
  align-items: center;
  gap: var(--spacing-18);
}

.video-lightbox__indicator {
  min-width: 56px;
  text-align: center;
  font-size: var(--text-xs);
  font-weight: var(--font-weight-medium);
  color: var(--text-tertiary);
}

.video-lightbox__controls .hero-video__arrow {
  display: flex;
  opacity: 1;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .hero-video__scrim,
  .hero-video__controls,
  .hero-video__arrow,
  .hero-video__indicator,
  .video-lightbox__backdrop,
  .video-lightbox__content {
    transition: none;
  }

  .intro__name,
  .intro__role,
  .about,
  .about-heading,
  .about-lead,
  .about-portrait,
  .about-meta {
    transition: none;
  }
}

/* ---------- TOP-LEFT INTRO ---------- */

.intro {
  position: fixed;
  top: 0;
  right: 0;
  left: auto;
  z-index: 11;
  max-width: 600px;
  height: 100px;
  padding: 0 var(--spacing-48);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  text-align: right;
  opacity: 1;
  visibility: visible;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

/* Hidden once #case-detail has risen to fully cover the hero (see
   initProgressTrail() in scroll-story.js) — same handoff point as
   .side-nav, otherwise this fixed header would float above the case
   study content. */
body.nav-hidden .intro {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.intro__name,
.intro__role {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--font-weight-regular);
  line-height: 1.2;
  letter-spacing: normal;
  color: var(--text-primary);
  white-space: nowrap;
  transition: font-size 0.7s cubic-bezier(.33, 1, .68, 1);
}

.intro__role {
  color: var(--text-tertiary);
}

.intro__name {
  font-family: var(--font-serif);
  font-size: var(--text-xl);
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin-bottom: 2px;
}

/* .about can scroll internally at narrow viewports (see the mobile
   overflow-y:auto below), but .intro/.side-nav stay position:fixed above
   it — without an opaque backing here, the panel's own scrolled bio text
   passes underneath and shows straight through, double-exposed with the
   header. A full-width bar (rather than just .intro/.side-nav's own
   content-sized boxes, which don't span the gap between them) painted the
   same red the panel already sits on makes the header correctly occlude
   whatever scrolls behind it, at every viewport (a no-op wherever nothing
   needs to scroll underneath). */
/* Attached to .intro rather than .side-nav: .intro comes first in the
   markup, so this bar paints (and its own name/role text repaints over it)
   before .side-nav's "Close" — putting it on the later element would have
   this opaque bar paint on top of .intro's text instead of behind it. */
body.about-open .intro::before {
  content: '';
  position: fixed;
  z-index: -1;
  inset: 0 0 auto 0;
  height: 100px;
  background: var(--color-brand-red);
}

body.about-open .intro {
  max-width: none;
}

body.about-open .intro__name {
  font-size: clamp(32px, 5.5vw, 86px);
  line-height: var(--leading-display);
}

body.about-open .intro__role {
  font-size: var(--text-lg);
}

/* ---------- SIDE NAV ---------- */

.side-nav {
  position: fixed;
  top: 0;
  left: var(--spacing-48);
  right: auto;
  z-index: 11;
  height: 100px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--spacing-8);
  opacity: 1;
  visibility: visible;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

/* Hidden once #case-detail has risen to fully cover the hero (see
   initProgressTrail() in scroll-story.js) — otherwise the fixed nav would
   float above the case study content. */
body.nav-hidden .side-nav {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.side-tab {
  font-size: var(--text-md);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  background: transparent;
  border: none;
  padding: 0;
  transition: color 0.2s ease;
}

/* position:relative + inline-block (not flex) so the container's height is
   driven only by .side-tab__text — the arrow is positioned absolutely (below
   in "About", beside in "Close") and never contributes to that height. Doing
   this any other way (e.g. flex-direction: column -> row) changes the
   container's own height between states, and since .side-nav vertically
   centers this single child, a height change drags the "About"/"Close" text
   to a different y — text position stays fixed only because height doesn't. */
.side-tab--about {
  position: relative;
  display: inline-block;
}

.side-tab__text {
  padding-bottom: 2px;
}

/* The underline and the "Close" cross are the same two strokes
   (.side-tab__arrow::before/::after), not two different elements
   cross-fading. Closed, both strokes lie flat and overlap exactly - one
   line, the same width as the text above it. Opening rotates them to
   ±45deg and slides/shrinks the box out to the right, so the line is
   visibly the thing that folds into the cross rather than vanishing
   while an unrelated X fades in beside it. */
.side-tab__arrow {
  position: absolute;
  /* +8px to skip .about-label's own left padding, else the underline
     starts 8px before the visible "A". */
  left: 8px;
  /* +1.5px beyond the text's own padding-bottom so the closed-state
     underline sits the same distance from "ABOUT" as LINKEDIN's
     underline sits from its own label (see .about-meta a). */
  top: calc(100% + 1.5px);
  /* -16px for .about-label's left+right padding, -0.02em to cancel
     .side-tab's letter-spacing trailing the last letter, else underline
     runs wider than "About" itself. */
  width: calc(100% - 16px - 0.02em);
  height: 18px;
  transition: width 0.3s ease, left 0.3s ease, top 0.3s ease,
              margin-top 0.3s ease, transform 0.3s ease;
}

body.about-open .side-tab__arrow {
  top: 50%;
  /* This box positions from the padded .about-label's 100% edge, which
     sits ~8px past the visible "E" (the label's own right padding) - and
     the X's two 14px diagonals are then centered in this 14px-wide box,
     landing their visible strokes ~2px further in still. Both of those
     are invisible offsets the arrow icon doesn't have (no padding on its
     link, stroke flush to its own box edge), so this needs to be pulled
     well left of 100% to make the visible text-to-glyph gap match
     LINKEDIN's ~7px gap to its arrow. */
  left: calc(100% - 3.3px);
  margin-top: 0;
  transform: translateY(-50%);
  width: 14px;
}

.side-tab__arrow::before,
.side-tab__arrow::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: var(--line-hairline);
  background: currentColor;
  transform: translateY(-50%) rotate(0deg);
  transition: top 0.3s ease, transform 0.3s ease, opacity 0.2s ease,
              color 280ms ease;
}

body.about-open .side-tab__arrow::before {
  top: 50%;
  transform: translateY(-50%) rotate(45deg);
}

body.about-open .side-tab__arrow::after {
  top: 50%;
  transform: translateY(-50%) rotate(-45deg);
}

/* On hover the underline picks up the same maroon as the word above it
   (see .about-label's text-clip fill just below) instead of disappearing -
   one continuous top-down wipe through the word and into its own
   underline. Triggered off the whole link (#about-tab), not just the
   label span, so hovering anywhere on "About" - including the underline
   itself - fires it, matching how Email/LinkedIn's whole link hovers
   together. */
/* (hover: hover) guard: touch devices have no pointer to "leave", so a tap
   leaves #about-tab stuck matching :hover after the panel closes, which
   would otherwise strand the label/arrow in their hover colors on the
   landing page instead of resting state. */
@media (hover: hover) {
  body:not(.about-open) #about-tab:hover .side-tab__arrow::before,
  body:not(.about-open) #about-tab:hover .side-tab__arrow::after {
    color: rgba(139, 40, 40, 0.85);
  }

  /* Once open, these same strokes are the "Close" cross - it fills to
     driftwood alongside the word (below) on hover, for the same reason
     Email/LinkedIn's arrow icon does: an icon that stays the old color
     while the word beside it changes reads as broken, not intentional. */
  body.about-open #about-tab:hover .side-tab__arrow::before,
  body.about-open #about-tab:hover .side-tab__arrow::after {
    color: var(--color-driftwood);
  }
}

body.about-open .side-tab {
  color: var(--text-primary);
}

/* ---------- CASE CARD (full-hero overlay wrapper) ---------- */

.case-card {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

/* ---------- SHARED HOVER-FILL (top-down maroon wipe: 01/02/03 tabs + ABOUT label) ---------- */

.hover-fill {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.hover-fill::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(139, 40, 40, 0.85);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 280ms ease;
  /* isolation above creates a fresh stacking context, so a negative
     z-index here paints before this element's own (in-flow) text —
     no extra wrapper span needed to keep the label readable on top. */
  z-index: -1;
}

.hover-fill:not(.is-active):hover::before {
  transform: scaleY(1);
}

.about-label {
  padding: 4px 8px;
}

/* Neither state uses the shared .hover-fill box wipe (that's still used by
   the 01/02/03 case tabs) - kill it here unconditionally so it can't paint
   a rectangle behind/around either "About" or "Close". */
.about-label::before {
  content: none;
}

/* ABOUT (closed) state: fill the word itself with color, not a box around
   it. A two-tone vertical gradient is clipped to the text's own glyphs
   (background-clip: text) and slid into view on hover, so only the letter
   shapes pick up color - same top-down wipe motif as the 01/02/03 tabs,
   just shaped like the word instead of a padded rectangle. */
body:not(.about-open) .about-label {
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-image: linear-gradient(
    to bottom,
    rgba(139, 40, 40, 0.85) 50%,
    var(--text-tertiary) 50%
  );
  background-size: 100% 200%;
  background-position: 0 100%;
  -webkit-background-clip: text;
  background-clip: text;
  transition: background-position 280ms ease;
}

@media (hover: hover) {
  body:not(.about-open) #about-tab:hover .about-label {
    background-position: 0 0;
  }
}

/* CLOSE: same word-shaped, top-down fill as "About" and Email/LinkedIn -
   filling to driftwood (this panel's own accent) rather than About's
   maroon, since maroon-on-maroon would vanish against this background. */
body.about-open .about-label {
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-image: linear-gradient(
    to bottom,
    var(--color-driftwood) 50%,
    var(--text-primary) 50%
  );
  background-size: 100% 200%;
  background-position: 0 100%;
  -webkit-background-clip: text;
  background-clip: text;
  transition: background-position 280ms ease;
}

@media (hover: hover) {
  body.about-open #about-tab:hover .about-label {
    background-position: 0 0;
  }
}

/* ---------- CASE NAV (vertical 01 02 03) ---------- */

.case-nav {
  position: absolute;
  left: var(--spacing-48);
  top: 33.08vh;
  pointer-events: none;
}

.case-nav__label {
  pointer-events: auto;
  position: absolute;
  /* .case-nav itself sits at left: var(--spacing-48), so subtracting
     that back out lands this label's own left edge at exactly
     --rail-label-gap from the viewport edge — same distance the
     right-hand rail label keeps via the mirrored calc() in
     scroll-story.css. */
  left: calc(var(--rail-label-gap) - var(--spacing-48));
  top: 0;
  bottom: 0;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-regular);
  letter-spacing: normal;
  color: var(--text-tertiary);
  display: flex;
  align-items: flex-end;
}

.case-nav__list {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.case-tab {
  width: 50px;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: var(--line-hairline) solid var(--text-tertiary);
  color: var(--text-tertiary);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--font-weight-medium);
  transition: background 0.2s ease, color 0.2s ease, border-color 280ms ease;
}

.case-tab.hover-fill:not(.is-active):hover {
  border-color: transparent;
}

/* Kept at full strength (not tertiary) — needs max contrast against the
   solid red active background rather than the hierarchy tier that would
   otherwise apply to a nav tab. */
.case-tab.is-active {
  background: var(--color-brand-red);
  border-color: var(--color-brand-red);
  color: var(--text-primary);
}

/* ---------- CASE TITLE + DESCRIPTION (bottom-left) ---------- */

.case-card__bottom {
  position: absolute;
  left: var(--spacing-48);
  right: var(--spacing-48);
  bottom: 5.45vh;
  pointer-events: none;
}

/* Second line of defense behind the title/desc text, on top of the
   image-coverage guarantee that already keeps photo/video under them at
   every breakpoint (see the @media (max-width: 980px) block) - not a fix
   for an observed gap, just cheap insurance against any future layout
   change landing this text on plain page background. Anchored bottom-left
   (where the text actually sits, not the container's own center - this
   box spans the full width between left/right spacing-48) and faded out
   fast enough that it reads as nothing at normal viewing distance. */
.case-card__bottom::before {
  content: '';
  position: absolute;
  inset: -10%;
  background: radial-gradient(ellipse 65% 75% at 20% 80%, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0) 70%);
  pointer-events: none;
}

.case-card__title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(40px, 6.5vw, 145px);
  font-weight: var(--font-weight-medium);
  line-height: 0.95;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--text-primary);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.case-card__desc {
  margin: 0.7vh 0 0;
  font-size: clamp(14px, 1.05vw, 21px);
  font-weight: var(--font-weight-regular);
  line-height: 1.4;
  color: var(--text-primary);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  max-width: 40vw;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.case-card__title.is-switching,
.case-card__desc.is-switching {
  opacity: 0;
  transform: translateY(8px);
}

/* ---------- ABOUT ---------- */

/* The red screen slides down from above like a blind being drawn — the
   same top-covers-bottom idea as #case-detail rising from below to cover
   the pinned hero (see .case-detail in scroll-story.css), just mirrored:
   here the panel drops in from the top instead of rising from the bottom.
   Sequence: name zoom (.intro__name font-size, above) and the red panel's
   slide start together, on the same gentle ease-out-cubic curve and
   duration as the name, so the zoom hands off into the slide as one
   unhurried, continuous motion instead of the name settling and then the
   panel starting from rest -> content follows once the panel has mostly
   settled (staggered delays below). */
.about {
  position: fixed;
  inset: 0;
  z-index: 10;
  padding: var(--spacing-48);
  background: var(--color-brand-red);
  color: var(--color-warm-cream);
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-100%);
  /* ease-in-out rather than the open's ease-out: matching ease-out here
     would yank the panel upward abruptly the instant you click close
     (fast start) before gently trailing off — ease-in-out eases into the
     motion first, so the retreat reads as smooth rather than rushed. */
  transition: transform 0.6s cubic-bezier(.65, 0, .35, 1), visibility 0s 0.6s;
}

body.about-open .about {
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
  transition: transform 0.7s cubic-bezier(.33, 1, .68, 1), visibility 0s;
}

body.about-open { overflow: hidden; }

/* Content waits for the red screen (.about's translateY slide, above) to
   mostly fill the viewport before it starts appearing, then reveals itself
   heading -> portrait -> lead -> meta via staggered transition-delays. On
   close, everything drops the delay and fades out quickly (golden rule:
   exits faster than enters) ahead of the red screen sliding back up. */
.about-heading,
.about-lead,
.about-portrait,
.about-meta {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.2s ease-in, transform 0.2s ease-in;
}

body.about-open .about-heading {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.45s cubic-bezier(.33, 1, .68, 1) 0.55s,
              transform 0.45s cubic-bezier(.33, 1, .68, 1) 0.55s;
}

body.about-open .about-portrait {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.5s cubic-bezier(.33, 1, .68, 1) 0.63s,
              transform 0.5s cubic-bezier(.33, 1, .68, 1) 0.63s;
}

body.about-open .about-lead {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.45s cubic-bezier(.33, 1, .68, 1) 0.7s,
              transform 0.45s cubic-bezier(.33, 1, .68, 1) 0.7s;
}

body.about-open .about-meta {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.4s cubic-bezier(.33, 1, .68, 1) 0.8s,
              transform 0.4s cubic-bezier(.33, 1, .68, 1) 0.8s;
}

.about-heading {
  position: relative;
  z-index: 2;
  max-width: min(650px, 48vw);
  margin: 190px 0 0 8vw;
}

.about-heading h1 {
  margin: 0;
  font-size: clamp(22px, 1.8vw, 28px);
  font-weight: var(--font-weight-regular);
  line-height: 1.5;
  letter-spacing: normal;
  color: var(--text-primary);
}

.about-lead {
  position: relative;
  z-index: 2;
  max-width: min(680px, 51vw);
  margin: 34px 0 0 3vw;
  font-size: clamp(22px, 1.8vw, 28px);
  font-weight: var(--font-weight-regular);
  line-height: 1.5;
  letter-spacing: normal;
  color: #ffffff;
}

/* Box edges, each pinned independently:
   - left: was the visible left edge of the old 65%-wide, centered crop
     (58vw container start + half of the leftover 35% of container width) —
     kept exactly so the photo doesn't shift horizontally.
   - top: .about-heading's own top offset (its 190px margin + the panel's
     spacing-48 padding), so the box starts level with the bio text.
   - right: the panel's standard content margin, which is also exactly
     .intro__name's own right inset (right:0 + padding spacing-48) — so
     this already right-aligns the box with the "Anna Iarinovskaia" heading.
   - bottom: 0, unchanged, runs to the bottom edge of the panel. */
.about-portrait {
  position: absolute;
  z-index: 1;
  left: calc(65.35vw - 8.4px);
  right: var(--spacing-48);
  top: calc(var(--spacing-48) + 190px);
  bottom: 0;
  margin: 0;
}

/* Box is now taller and narrower than the photo's own aspect ratio, so
   cover-crop it; anchoring the crop to the bottom-left keeps the same
   portion of the image visible along those two edges as before. */
.about-portrait img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left 40%;
  filter: brightness(0.88);
}

.about-meta {
  position: absolute;
  z-index: 3;
  /* +8px matches .about-label's own left padding, so this lines up with
     the "Close" text above rather than the side-nav's outer edge. */
  left: calc(var(--spacing-48) + 8px);
  bottom: var(--spacing-48);
  display: flex;
  gap: 72px;
}

.about-meta a {
  display: inline-flex;
  align-items: center;
  gap: 6.5px;
  font-size: var(--text-sm);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: normal;
  color: var(--text-tertiary);
  /* currentColor, not a fixed var, so the underline rides along with
     whatever color .about-meta a itself (and the arrow, via inheritance)
     transitions to on hover - same as CLOSE's underline. */
  border-bottom: var(--line-hairline) solid currentColor;
  transition: color 0.2s ease;
}

.about-meta a:hover {
  /* Drives the arrow icon (background: currentColor) and the underline
     above. The label text itself doesn't use color at all anymore - see
     .about-meta__label below for its own top-down fill. */
  color: var(--color-driftwood);
}

/* EMAIL / LINKEDIN: same word-shaped, top-down fill as "About" and
   "Close" (background-clip: text, two-tone gradient slid into view) -
   just filling to the panel's own accent (driftwood) rather than About's
   maroon, since maroon-on-maroon would vanish against this panel's
   background. */
.about-meta__label {
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-image: linear-gradient(
    to bottom,
    var(--color-driftwood) 50%,
    var(--text-tertiary) 50%
  );
  background-size: 100% 200%;
  background-position: 0 100%;
  -webkit-background-clip: text;
  background-clip: text;
  transition: background-position 280ms ease;
}

.about-meta a:hover .about-meta__label {
  background-position: 0 0;
}

/* Hand-drawn to a 1px stroke so its weight matches the 1px underline above
   exactly - a text "↗" glyph renders heavier than that at this size. */
.about-meta__arrow {
  position: relative;
  width: 9px;
  height: 9px;
  flex-shrink: 0;
}

.about-meta__arrow::before {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  /* Diagonal of the 9px box (9 * sqrt(2)), not 9px - the shaft is rotated
     -45deg from the bottom-left corner, so it must span the box's full
     corner-to-corner distance to reach the arrowhead's tip at top-right.
     A 9px length (the box's side, not its diagonal) fell short and left a
     visible gap between the shaft and the arrowhead. */
  width: calc(9px * 1.41421356);
  height: var(--line-hairline);
  background: currentColor;
  transform-origin: left bottom;
  transform: rotate(-45deg);
}

.about-meta__arrow::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 6px;
  height: 6px;
  border-top: var(--line-hairline) solid currentColor;
  border-right: var(--line-hairline) solid currentColor;
}

/* ---------- CASE DETAIL (full write-up, normal flow after #story) ---------- */

.case-detail {
  display: none;
  background: var(--color-off-white);
  /* Exposed as custom properties (rather than only baked into the shorthand
     below) so full-bleed band sections nested inside — see the Configurator
     case study's Design Evolution / Impact overrides — can cancel exactly
     this padding with a matching negative margin at any viewport. */
  --case-pad-l: var(--spacing-48);
  --case-pad-r: calc(var(--spacing-48) + 56px);
  /* No bottom padding here — .case-detail__footer supplies its own
     symmetric top/bottom padding around the footer band itself, and the
     footer is always the section's last child, so its own bottom padding
     is the page's true end. Padding here would stack on top of that and
     leave a dead gap below the footer band. */
  padding: var(--spacing-68) var(--case-pad-r) 0 var(--case-pad-l);
}

.case-detail.is-active {
  display: block;
}

.case-detail__inner {
  max-width: 1300px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  /* 220px inter-section rhythm, shared by all three case studies. */
  gap: 220px;
}

.case-detail__heading {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(28px, 4vw, 56px);
  font-weight: var(--font-weight-medium);
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--color-walnut-shadow);
}

/* ---------- SECTION ROW (label + content, shared by Context/Goal/Approach) ---------- */

.case-detail__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-48);
  align-items: baseline;
}

.case-detail__label {
  grid-column: 1;
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: var(--text-2xs);
  line-height: var(--leading-heading);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-case-accent);
}

.case-detail__columns {
  display: contents;
}

/* Force the two children into columns 2 and 3 explicitly — without this,
   auto-placement lands them wherever the grid cursor happens to be (e.g.
   column 1, under the section label) whenever a full-width row like the
   intro heading precedes them. */
.case-detail__columns > *:nth-child(1) {
  grid-column: 2;
}

.case-detail__columns > *:nth-child(2) {
  grid-column: 3;
}

.case-detail__columns > div {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

.case-detail__columns p {
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

/* ---------- GOAL ---------- */

.case-detail__goal-content {
  display: contents;
}

/* ---- Goal band: full-bleed accent band, statement left / grid right. Shared
   component — apply `.case-detail__row--goal` to any row that needs a
   principle statement called out this way, not just case 3. ---- */

.case-detail__row--goal {
  display: block;
  margin-inline: calc(-1 * var(--case-pad-l)) calc(-1 * var(--case-pad-r));
  padding: clamp(80px, 15vh, 170px) var(--case-pad-r) clamp(80px, 15vh, 170px) var(--case-pad-l);
  background: var(--color-case-accent);
}

.case-detail__row--goal .case-detail__goal-content {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-48);
  align-items: start;
}

.case-detail__row--goal .case-detail__goal-intro {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

.case-detail__row--goal .case-detail__label {
  color: var(--text-primary);
}

.case-detail__row--goal .case-detail__principle {
  grid-column: auto;
  max-width: none;
  font-family: var(--font-statement);
  font-weight: var(--font-weight-regular);
  color: var(--text-primary);
}

.case-detail__bullets {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
  margin: 0;
  padding: 0;
}

.case-detail__bullets li {
  position: relative;
  padding-left: var(--spacing-18);
  font-size: clamp(15px, 1.1vw, 20px);
  line-height: 1.4;
  color: var(--color-cork-border);
}

.case-detail__bullets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 6px;
  height: 6px;
  background: var(--color-case-accent);
}

.case-detail__principle {
  grid-column: 2 / 4;
  margin: 0;
  max-width: 640px;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
  font-size: clamp(20px, 2vw, 32px);
  line-height: 1.3;
  color: var(--color-case-accent);
}

.case-detail__goal-grid {
  grid-column: 2 / 4;
  display: grid;
  grid-template-columns: 1fr;
  row-gap: var(--spacing-18);
}

.case-detail__goal-item {
  margin: 0;
  padding-top: var(--spacing-12);
  border-top: var(--line-hairline) solid var(--color-image-stroke);
  font-size: clamp(15px, 1.1vw, 20px);
  line-height: 1.4;
  color: var(--color-cork-border);
}

.case-detail__row--goal .case-detail__goal-item {
  position: relative;
  border-top: none;
  padding-top: 0;
  padding-left: 20px;
  color: var(--color-grey);
}

/* Small square marker echoing .story__progress-marker's rectangular
   scroll-line dot, so the two motifs read as the same visual language. */
.case-detail__row--goal .case-detail__goal-item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 5px;
  height: 5px;
  background: var(--color-off-white);
}

/* ---- Hypothesis & Constraints: drop the shared goal band's full-bleed
   accent treatment for this row only — reads as a normal in-flow section,
   text sized to match the rest of the case study instead of the callout
   scale/color used elsewhere for .case-detail__row--goal. ---- */
.case-detail__row--hypothesis.case-detail__row--goal {
  display: block;
  margin-inline: 0;
  padding: 0;
  background: none;
}

.case-detail__row--hypothesis .case-detail__goal-content {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-48);
  align-items: start;
}

.case-detail__row--hypothesis .case-detail__goal-grid {
  grid-column: 2;
  margin-top: calc(var(--spacing-24) + (12px * var(--leading-heading)));
}

.case-detail__row--hypothesis .case-detail__label {
  color: var(--color-case-accent);
}

.case-detail__row--hypothesis .case-detail__principle {
  font-family: var(--font-serif);
  font-weight: var(--font-weight-regular);
  font-size: clamp(26px, 3.6vw, 52px);
  line-height: 1.15;
  color: var(--color-walnut-shadow);
}

.case-detail__row--hypothesis .case-detail__goal-item {
  position: static;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
  padding-top: var(--spacing-12);
  padding-left: 0;
  border-top: var(--line-hairline) solid var(--color-image-stroke);
}

.case-detail__row--hypothesis .case-detail__goal-item:first-child {
  padding-top: 0;
  border-top: none;
}

.case-detail__goal-item-heading {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: clamp(16px, 1.3vw, 19px);
  color: var(--color-walnut-shadow);
}

.case-detail__goal-item-body {
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

.case-detail__row--hypothesis .case-detail__goal-item::before {
  display: none;
}

/* ---------- APPROACH ---------- */

.case-detail__decisions {
  grid-column: 2 / 4;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-96);
  margin: 0;
  padding: 0;
}

.case-detail__decision {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

/* ---------- DESIGN EVOLUTION MODULE ----------
   Shared numbered-list module: originally Configurator's Design Evolution
   (V0/V1/V2), now reused as-is by Portfolio's Final Solution and
   Onboarding's Approach — apply `.case-detail__row--evolution` to any row
   that needs this "big serial numeral + label, text left / screenshot
   right, hairline rule above the text, caption below the image" pattern.
   The row itself drops out of the shared 3-column .case-detail__row grid
   (label stacks above the content instead of beside it). */
.case-detail__row--evolution {
  display: block;
}

.case-detail__row--evolution .case-detail__label {
  margin: 0 0 clamp(28px, 5vh, 56px);
}

.case-detail__decisions {
  gap: var(--spacing-68);
}

.case-detail__row--evolution .case-detail__decision {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-18);
  align-items: start;
}

.case-detail__row--evolution .case-detail__decision-text {
  grid-column: 1;
  grid-row: 1;
  border-top: var(--line-hairline) solid var(--color-image-stroke);
  padding-top: var(--spacing-24);
}

/* Configurator's Design Evolution (case 1) drops the hairline rule that
   the shared module otherwise draws above each decision's text. */
#case-detail-1 .case-detail__row--evolution .case-detail__decision-text {
  border-top: none;
  padding-top: 0;
}

.case-detail__row--evolution .case-detail__decision-figure {
  grid-column: 2;
  grid-row: 1;
}

.case-detail__row--evolution .case-detail__decision-image {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 0;
}

.case-detail__version-row {
  display: flex;
  flex-direction: column;
  /* Same figure-to-heading spacing as the Impact section's stat items
     (.case-detail__impact-figure margin-bottom + .case-detail__impact-item gap). */
  gap: calc(var(--spacing-18) + var(--spacing-8));
}

.case-detail__version-num {
  font-family: var(--font-serif);
  font-size: clamp(44px, 6vw, 86px);
  line-height: 0.9;
  font-variant-numeric: lining-nums;
  color: var(--color-brand-red);
}

.case-detail__version-label {
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: clamp(16px, 1.3vw, 19px);
  color: var(--color-walnut-shadow);
}

.case-detail__row--evolution .case-detail__decision-body {
  max-width: 42ch;
}

/* Every numbered-decision-list body paragraph (Configurator's Design
   Evolution, Portfolio's Final Solution, Onboarding's Approach) reads at
   the same size as the Impact section's stat paragraphs
   (.case-detail__impact-item .case-detail__decision-body), not the
   quieter base .case-detail__decision-body tier. */
.case-detail__row--evolution .case-detail__decision-body,
.case-detail__row--approach .case-detail__decision-body {
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

/* Same label-to-body gap as the Impact section's stat items
   (.case-detail__impact-item's flex gap). */
.case-detail__row--evolution .case-detail__decision-text > * {
  margin-top: var(--spacing-8);
}

.case-detail__row--evolution .case-detail__decision-text > *:first-child {
  margin-top: 0;
}

.case-detail__row--evolution .case-detail__decision-caption--accent {
  color: var(--color-case-accent);
}

.case-detail__decision-heading {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: clamp(15px, 1.4vw, 20px);
  line-height: var(--leading-heading-sm);
  color: var(--color-case-accent);
}

.case-detail__decision-dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  margin: 0 8px;
  background: currentColor;
  vertical-align: middle;
}

.case-detail__decision-body {
  margin: 0;
  max-width: 60ch;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

.case-detail__decision-image {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  margin-top: var(--spacing-12);
  border: var(--line-hairline) solid var(--color-image-stroke);
}

.case-detail__decision-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

.case-detail__decision-image--missing {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  padding: var(--spacing-24);
  border: var(--line-hairline) dashed var(--color-cork-border);
  font-size: var(--text-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: normal;
  text-transform: uppercase;
  text-align: center;
  color: var(--color-brand-red);
}

/* ---------- IMPACT ---------- */

.case-detail__impact-content {
  grid-column: 2 / 4;
}

.case-detail__metrics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-24);
  margin: var(--spacing-24) 0 0;
  padding: 0;
}

.case-detail__metric {
  flex: 1 1 200px;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
  padding-top: var(--spacing-12);
  border-top: var(--line-hairline) solid var(--color-cork-border);
}

.case-detail__metric-number {
  font-family: var(--font-serif);
  font-size: clamp(36px, 4vw, 64px);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--color-walnut-shadow);
}

.case-detail__metric-label {
  font-size: var(--text-sm);
  font-weight: var(--font-weight-medium);
  letter-spacing: normal;
  text-transform: uppercase;
  color: var(--color-brand-red);
}

/* ---------- OVERVIEW / PROBLEM (shared two-column intro pattern) ---------- */

.case-detail__intro-heading {
  grid-column: 2 / 4;
  margin: 0;
  font-family: var(--font-serif);
  font-weight: var(--font-weight-regular);
  font-size: clamp(26px, 3.6vw, 52px);
  line-height: 1.15;
  color: var(--color-walnut-shadow);
}

.case-detail__overview-secondary {
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

.case-detail__row--overview .case-detail__overview-secondary:nth-of-type(1),
.case-detail__row--problem .case-detail__overview-secondary:nth-of-type(1) {
  grid-column: 2;
}

.case-detail__row--overview .case-detail__overview-secondary:nth-of-type(2),
.case-detail__row--problem .case-detail__overview-secondary:nth-of-type(2) {
  grid-column: 3;
}

/* ---------- PROBLEM VISUAL ---------- */

.case-detail__problem-visual {
  grid-column: 2 / 4;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

.case-detail__problem-visual img {
  display: block;
  width: 100%;
  max-width: 420px;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

.case-detail__problem-visual .case-detail__bullets {
  padding-top: var(--spacing-12);
}

/* ---------- ROW VISUAL (standalone supporting image + caption) ---------- */

.case-detail__row-visual {
  grid-column: 2 / 4;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
}

.case-detail__row-visual img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

.case-detail__row-visual--sm {
  max-width: 520px;
}

.case-detail__row-visual--compact {
  max-width: 320px;
}

/* ---------- EXPLORATION (Portfolio): same text-left/image-right pattern
   as the Configurator's Problem row and the Portfolio Problem row above —
   label + body stacked in the left column, image right, both columns
   starting at the same top edge. Scoped to #case-detail-2. ---------- */

#case-detail-2 .case-detail__row--exploration {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

.case-detail__exploration-text {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

.case-detail__exploration-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

.case-detail__exploration-figure img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

/* ---------- KEY FINDING (Portfolio): same pattern — label + heading +
   body left, image right. Scoped to #case-detail-2. ---------- */

#case-detail-2 .case-detail__row--validation {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

.case-detail__validation-text {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

.case-detail__validation-visual {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

.case-detail__validation-visual img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

/* ---------- RESEARCH & INSIGHTS ---------- */

.case-detail__research-intro {
  grid-column: 2 / 4;
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

.case-detail__research-subhead {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: clamp(16px, 1.3vw, 19px);
  color: var(--color-walnut-shadow);
}

/* ---- Product Evolution before/after: label stacked above the two
   columns (like the Impact row's label-above-grid treatment) rather than
   beside them, before points in grey and after points in near-black. ---- */
.case-detail__row--before-after {
  display: block;
}

.case-detail__row--before-after .case-detail__label {
  grid-column: auto;
  margin: 0 0 clamp(28px, 5vh, 56px);
}

.case-detail__row--before-after .case-detail__columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-48);
}

.case-detail__row--before-after .case-detail__columns > *:nth-child(1) {
  grid-column: 1;
}

.case-detail__row--before-after .case-detail__columns > *:nth-child(2) {
  grid-column: 2;
}

.case-detail__row--before-after .case-detail__bullets li {
  font-size: var(--body-lead-size);
}

.case-detail__row--before-after .case-detail__columns > div:first-child .case-detail__bullets li {
  color: var(--color-grey);
}

.case-detail__row--before-after .case-detail__columns > div:last-child .case-detail__bullets li {
  color: var(--color-walnut-shadow);
}

/* ---------- DESIGN EVOLUTION ---------- */

.case-detail__decision-caption,
.case-detail__decision-insight,
.case-detail__compare-caption {
  margin: 0;
  line-height: 1.4;
}

.case-detail__decision-insight {
  font-size: var(--text-sm);
}

.case-detail__decision-caption {
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-regular);
  color: var(--color-grey);
}

.case-detail__decision-caption-label {
  margin-right: var(--spacing-6);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-brand-red);
}

.case-detail__decision-insight {
  font-weight: var(--font-weight-medium);
  color: var(--color-case-accent);
}

/* ---------- KEY DESIGN DECISIONS ---------- */

/* 01 — before/after compare: text in the second column, before/after crops
   stacked together in the third. */
.case-detail__key-decision-compare {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

/* 04 — onboarding-into-dashboard before/after: full-width row, side by side.
   Gap matches the text/image column-gap used elsewhere in this row
   (.case-detail__row--evolution .case-detail__decision) so each before/after
   image renders at the same width as point 02's single decision image. */
.case-detail__decision .case-detail__key-decision-compare {
  flex-direction: row;
  gap: var(--spacing-62);
}

.case-detail__decision .case-detail__compare-item {
  flex: 1;
  min-width: 0;
}

.case-detail__compare-item {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
}

.case-detail__compare-media {
  border: var(--line-hairline) solid var(--color-image-stroke);
  background: var(--color-off-white);
  display: flex;
  align-items: center;
}

.case-detail__compare-media img {
  display: block;
  width: 100%;
  height: auto;
}

.case-detail__compare-caption {
  display: flex;
  align-items: center;
  gap: var(--spacing-8);
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-regular);
  color: var(--color-grey);
}

.case-detail__compare-badge {
  font-size: var(--text-3xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-grey);
}

.case-detail__compare-badge--accent {
  color: var(--color-case-accent);
}

/* 04 — closes the section on a quieter note: image in the second column,
   text in the third. A thin strip of the tab bar itself, captioned with
   the sequence it represents. */
.case-detail__sequence-media {
  max-width: 620px;
}

.case-detail__sequence-labels {
  margin: var(--spacing-12) 0 0;
  padding: var(--spacing-12) 0 0;
  border-top: var(--line-hairline) solid var(--color-cork-border);
  font-size: var(--text-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-case-accent);
}

/* ---------- IMPACT GRID ---------- */

.case-detail__impact-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--spacing-62) var(--spacing-62);
}

#case-detail-3 .case-detail__row--problem {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: var(--spacing-24);
  align-items: start;
}

#case-detail-3 .case-detail__row--problem .case-detail__label {
  grid-column: 1;
  grid-row: 1;
}

/* Heading stays in the left column, the same track width as the Goal
   section's left column below (#case-detail-3 .case-detail__row--goal
   .case-detail__goal-content: identical repeat(2, minmax(0, 1fr)) split). */
#case-detail-3 .case-detail__row--problem .case-detail__intro-heading {
  grid-column: 1;
  grid-row: 2;
}

#case-detail-3 .case-detail__row--problem .case-detail__impact-content {
  grid-column: 2;
  grid-row: 2;
}

#case-detail-3 .case-detail__row--impact {
  display: block;
}

#case-detail-3 .case-detail__row--impact .case-detail__label {
  grid-column: auto;
  margin: 0 0 clamp(28px, 5vh, 56px);
}

#case-detail-3 .case-detail__row--impact .case-detail__impact-content {
  grid-column: auto;
}

#case-detail-3 .case-detail__row--impact .case-detail__impact-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

#case-detail-3 .case-detail__row--impact .case-detail__impact-item {
  padding-top: 0;
  border-top: none;
}

#case-detail-3 .case-detail__row--problem .case-detail__impact-item {
  gap: var(--spacing-24);
}

/* ---- Portfolio case study reflection: stack the section label above the
   three closing statements instead of the shared side-by-side row grid ---- */
#case-detail-2 .case-detail__row--impact {
  display: block;
}

#case-detail-2 .case-detail__row--impact .case-detail__label {
  grid-column: auto;
  margin: 0 0 clamp(28px, 5vh, 56px);
}

#case-detail-2 .case-detail__row--impact .case-detail__impact-content {
  grid-column: auto;
}

#case-detail-2 .case-detail__row--impact .case-detail__impact-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

/* Shared "impact item" body text — a number/short heading + a sentence,
   repeated in a grid. Used by every case study's numbered problem/impact
   grids, not just case 3's. */
.case-detail__impact-item {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8);
  padding-top: var(--spacing-24);
  border-top: var(--line-hairline) solid var(--color-image-stroke);
}

.case-detail__impact-item .case-detail__decision-heading {
  font-family: var(--font-display);
  font-weight: var(--font-weight-semibold);
  font-size: clamp(16px, 1.3vw, 19px);
  line-height: 1.2;
  letter-spacing: -0.015em;
  max-width: none;
  color: var(--color-walnut-shadow);
}

.case-detail__impact-item .case-detail__decision-body {
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  max-width: 42ch;
  color: var(--color-grey);
  opacity: 1;
  text-wrap: pretty;
}

/* ---------- REFLECTION ---------- */

.case-detail__reflection-body {
  grid-column: 2 / 4;
  max-width: 70ch;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

.case-detail__reflection-body p {
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

/* ---------- ONBOARDING CASE STUDY OVERRIDES ---------- */
/* Goal: drop the shared goal band's full-bleed red accent treatment for
   this case study — reads as a normal in-flow section, text sized to
   match the rest of the case study instead of the callout scale/color
   used elsewhere for .case-detail__row--goal. */
#case-detail-3 .case-detail__row--goal {
  margin-inline: 0;
  padding: 0;
  background: none;
}

/* Split the label/principle stack out of its flex wrapper so both become
   direct grid items of .case-detail__goal-content — lets the principle
   sit in its own row, with the bullet grid placed alongside it (row 2)
   instead of alongside the label (row 1). */
#case-detail-3 .case-detail__row--goal .case-detail__goal-intro {
  display: contents;
}

#case-detail-3 .case-detail__row--goal .case-detail__label {
  grid-column: 1;
  grid-row: 1;
  color: var(--color-case-accent);
}

#case-detail-3 .case-detail__row--goal .case-detail__principle {
  grid-column: 1;
  grid-row: 2;
  font-family: var(--font-serif);
  font-weight: var(--font-weight-regular);
  font-size: clamp(26px, 3.6vw, 52px);
  line-height: 1.15;
  color: var(--color-walnut-shadow);
}

#case-detail-3 .case-detail__row--goal .case-detail__goal-item {
  position: relative;
  padding-top: 0;
  padding-left: 20px;
  border-top: none;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

/* Same small rectangle marker used for bullets elsewhere on the page
   (.case-detail__bullets li::before), recolored for the accent bullet
   motif now that the item no longer sits on the red band. */
#case-detail-3 .case-detail__row--goal .case-detail__goal-item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 6px;
  height: 6px;
  background: var(--color-case-accent);
}

/* Match the Approach section's 50/50 text/image split below, so the goal
   list's left edge lines up with the image column instead of the shared
   component's default 3-column (1/3 + 2/3) split. */
#case-detail-3 .case-detail__row--goal .case-detail__goal-content {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

#case-detail-3 .case-detail__row--goal .case-detail__goal-grid {
  grid-column: 2;
  grid-row: 2;
}

/* Approach reuses the same shared .case-detail__row--evolution module —
   only this case study's module padding is specific here. */
#case-detail-3 .case-detail__row--approach {
  padding: 0;
}

/* Approach item 03 (onboarding-into-dashboard before/after): drop the
   text/image side-by-side split for this item only — the point sits full
   width on its own row, and the before/after compare images move below
   it, also full width, staying side by side with each other. */
#case-detail-3 .case-detail__row--approach .case-detail__decisions > .case-detail__decision:nth-child(3) {
  grid-template-columns: 1fr;
  row-gap: var(--spacing-31);
}

#case-detail-3 .case-detail__row--approach .case-detail__decisions > .case-detail__decision:nth-child(3) .case-detail__decision-text {
  grid-column: 1;
  grid-row: 1;
}

#case-detail-3 .case-detail__row--approach .case-detail__decisions > .case-detail__decision:nth-child(3) .case-detail__decision-figure {
  grid-column: 1;
  grid-row: 2;
}

/* Final Solution and Approach both carry .case-detail__row--approach on top
   of the shared --evolution module (Design Evolution does not), so this
   opts just those two out of the module's hairline rule above the text. */
.case-detail__row--approach .case-detail__decision-text {
  border-top: none;
  padding-top: 0;
}

/* ---------- CONFIGURATOR CASE STUDY (case-detail-1) — SECTION LAYOUT ----------
   Reworks the shared case-detail row pattern for this case study only: a
   numbered header ("01 / Overview" etc, the shared .case-detail__label
   style used by every case study) sits above hand-built section layouts
   (not the shared 3-column .case-detail__row grid). Everything below is
   scoped with the #case-detail-1 id so case studies 2 and 3, which reuse
   many of the same class names, are unaffected. */

/* The shared .case-detail__row is a 3-column grid; every row in this case
   study lays out its own content instead, so the row itself just stacks
   its direct children (section-head, then whatever the section needs). */
#case-detail-1 .case-detail__row {
  display: block;
}

/* The 220px inter-section rhythm is shared (see .case-detail__inner above)
   across all three case studies. Evolution/Impact/Footer in this case
   study no longer add their own top/bottom padding on top of it (see
   below) — one mechanism per boundary. */

/* ---- Overview: shared component (label, headline, lead/secondary body
   copy, ledger rail) — primary content left, ledger right, the same
   left-primary/right-secondary column logic as Problem (text left, image
   right) and Reflection (quote left, commentary right). The three case
   studies share .case-detail__overview-col-text, .case-detail__overview-
   ledger and the .case-detail__ledger-* component below, but each defines
   its own #case-detail-N .case-detail__row--overview grid immediately
   following, aligned to whatever that case's own Problem-image and/or
   Impact-grid geometry gives it to align with (see each block's comment).

   #case-detail-N .case-detail__row above (ID + class, specificity 1-1-0)
   resets every row in that case study to display:block, which beats a
   shared class-only rule on specificity alone regardless of source order
   — unlike Problem/Research, which opt back out via their own
   #case-detail-N-scoped rules, Overview needs its grid declared ID-scoped
   directly to win that fight. */
#case-detail-1 .case-detail__overview-col-text,
#case-detail-2 .case-detail__overview-col-text,
#case-detail-3 .case-detail__overview-col-text {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
  min-width: 0;
  /* Caps the heading + body measure to match Problem/Research's column
     width (~620px) instead of the full 1fr text track, which is wider
     here only because it's sized off the ledger/Impact-grid math above. */
  max-width: 620px;
}

.case-detail__overview-col-text .case-detail__body-stack {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

.case-detail__overview-lead {
  margin: 0;
  font-size: var(--body-lead-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--leading-body-lead);
  letter-spacing: normal;
  color: var(--color-grey);
}

.case-detail__overview-secondary--dim {
  margin: 0;
  font-size: var(--body-lead-size);
  line-height: var(--leading-body-lead);
  color: var(--color-grey);
}

/* margin-top skips past the "01 / Overview" label + its gap so the rail's
   top rule lands level with the intro-heading next to it, using the same
   measured label-height + gap formula as .case-detail__row--hypothesis
   .case-detail__goal-grid above. grid-column defaults to 2 (the plain
   text|ledger split used by case studies 2 and 3); case 1's 3-column grid
   overrides it to 3. */
#case-detail-1 .case-detail__overview-ledger,
#case-detail-2 .case-detail__overview-ledger,
#case-detail-3 .case-detail__overview-ledger {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  align-self: start;
  margin-top: calc(var(--spacing-24) + (12px * var(--leading-heading)));
}

/* ---- Overview grid (Configurator): three tracks, not two. Text is pinned
   to the exact same formula as Problem's own column (row width minus one
   column-gap, halved) so the two columns are pixel-identical at every
   viewport, not just near the ~1300px reference width a fixed max-width
   cap would only happen to match. The middle track is a flexible spacer
   that absorbs whatever's left; the ledger keeps its original formula so
   its label column still lines up with "Static to interactive", the
   third column of the Impact grid (#case-detail-1 .case-detail__impact-
   grid — repeat(auto-fit, minmax(240px, 1fr)) resolving to 3 equal tracks
   at this width, gap clamp(40px, 4vw, 60px)) — see
   .case-detail__overview-ledger's grid-column:3 override below. ---- */
#case-detail-1 .case-detail__row--overview {
  --impact-col-gap: clamp(40px, 4vw, 60px);
  display: grid;
  grid-template-columns: calc((100% - var(--spacing-62)) / 2) 1fr calc((100% / 3) - (var(--impact-col-gap) * 2 / 3));
  column-gap: var(--spacing-62);
}

#case-detail-1 .case-detail__overview-ledger {
  grid-column: 3;
}

/* ---- Overview grid (Portfolio): case 2 has neither a same-page Impact
   grid nor a reason of its own to set the ledger's width, so instead of
   deriving one from local geometry (as Configurator and Onboarding do),
   it just reuses Configurator's ledger-width formula directly — same
   clamp(40px, 4vw, 60px) column-gap constant — so all three case studies'
   ledgers render at the same width. Text is 1fr, filling whatever's left
   of the row's width once that fixed ledger column and the gap between
   them are accounted for. ---- */
#case-detail-2 .case-detail__row--overview {
  --impact-col-gap: clamp(40px, 4vw, 60px);
  display: grid;
  grid-template-columns: 1fr calc((100% / 3) - (var(--impact-col-gap) * 2 / 3));
  column-gap: var(--spacing-62);
}

/* ---- Overview grid (Onboarding): the mirror image of Portfolio's — case
   3 has an Impact grid (#case-detail-3 .case-detail__row--impact
   .case-detail__impact-grid) but no Problem image to align text to. Its
   Impact grid only overrides grid-template-columns, not gap, so — unlike
   Configurator's, which sets its own clamp(40px, 4vw, 60px) column-gap —
   it falls back to the base .case-detail__impact-grid gap, var(--spacing-
   62) both ways; --impact-col-gap here matches that. The ledger column is
   set to that Impact grid's own column-3 width; with only two columns
   here, "1fr" for the text column necessarily fills everything left over,
   which pins the ledger's start to exactly the same position (100% minus
   its own width) regardless of the column-gap chosen — the column-gap
   below is just the standard breathing room, not part of the alignment
   math (it happens to reuse the same var(--spacing-62) token as
   --impact-col-gap, but the two are independent). ---- */
#case-detail-3 .case-detail__row--overview {
  --impact-col-gap: var(--spacing-62);
  display: grid;
  grid-template-columns: 1fr calc((100% / 3) - (var(--impact-col-gap) * 2 / 3));
  column-gap: var(--spacing-62);
}

/* ---- Ledger rail: a single-column right-hand spec list replacing the
   old three-column Timeline/Scope/Team meta grid, shared by all three
   case studies' Overview sections above. Each row is its own label/value
   grid with a hairline rule between rows only — :first-child opts out so
   there's no divider above Timeline, and with no trailing closer row
   there's none below Team either. ---- */
.case-detail__ledger-row {
  display: grid;
  grid-template-columns: var(--spacing-96) minmax(0, 1fr);
  gap: var(--spacing-24);
  padding: 16px 0 var(--spacing-18);
  border-top: var(--line-hairline) solid var(--color-hairline-on-white);
}

.case-detail__ledger-row:first-child {
  border-top: none;
}

.case-detail__ledger-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-grey);
}

.case-detail__ledger-value {
  font-size: var(--text-md);
  color: var(--color-walnut-shadow);
}

.case-detail__ledger-value--list {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.case-detail__ledger-highlight {
  color: var(--color-walnut-shadow);
}

/* ---- Problem: text column (label, headline, body, limitations) beside a portrait image ---- */

#case-detail-1 .case-detail__row--problem {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

#case-detail-1 .case-detail__problem-col-text {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

#case-detail-1 .case-detail__problem-col-text .case-detail__body-stack {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

#case-detail-1 .case-detail__row--problem .case-detail__overview-secondary {
  max-width: 46ch;
}

#case-detail-1 .case-detail__row--problem .case-detail__overview-secondary:not(.case-detail__overview-secondary--dim) {
  font-size: var(--body-lead-size);
  color: var(--color-grey);
}

/* Limitations list: hairline rule per row, rectangle glyph prefix — overrides
   the shared dot-bullet .case-detail__bullets treatment for this case only. */
#case-detail-1 .case-detail__row--problem .case-detail__bullets {
  gap: 0;
}

#case-detail-1 .case-detail__row--problem .case-detail__bullets li {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-12);
  padding: var(--spacing-12) 0 0;
  border-top: var(--line-hairline) solid var(--color-image-stroke);
  font-size: clamp(15px, 1.1vw, 20px);
  line-height: 1.4;
  color: var(--color-cork-border);
}

#case-detail-1 .case-detail__row--problem .case-detail__bullets li + li {
  margin-top: var(--spacing-12);
}

/* Same 5x5 square as .story__progress-marker / .case-detail__row--goal
   .case-detail__goal-item::before, so the marker reads as one consistent
   scroll-line motif across the page. */
#case-detail-1 .case-detail__row--problem .case-detail__bullets li::before {
  content: '';
  display: inline-block;
  position: static;
  flex-shrink: 0;
  width: 5px;
  height: 5px;
  background: var(--color-case-accent);
}

#case-detail-1 .case-detail__problem-col-image {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

#case-detail-1 .case-detail__problem-col-image img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

#case-detail-1 .case-detail__problem-col-image .case-detail__decision-caption {
  max-width: 40ch;
}

/* ---- Problem (Portfolio): same text-left/image-right pattern as the
   Configurator's Problem row above — label + headline + body stacked in
   the left column, image right. Scoped to #case-detail-2 so case studies
   1 and 3, which reuse the same class names, are unaffected. ---- */
#case-detail-2 .case-detail__row--problem {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

#case-detail-2 .case-detail__problem-col-text {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

#case-detail-2 .case-detail__problem-col-text .case-detail__body-stack {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-18);
}

#case-detail-2 .case-detail__row--problem .case-detail__overview-secondary {
  max-width: 46ch;
  font-size: var(--body-lead-size);
  color: var(--color-grey);
}

#case-detail-2 .case-detail__problem-col-image {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-12);
}

#case-detail-2 .case-detail__problem-col-image img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--line-hairline) solid var(--color-image-stroke);
}

#case-detail-2 .case-detail__problem-col-image .case-detail__decision-caption {
  max-width: 40ch;
}

/* ---- Research & Insights: label + serif headline, then a two-column
   split (quotes left, findings right) matching the Problem row's equal
   two-column grid ---- */

#case-detail-1 .case-detail__row--research {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--spacing-24);
}

#case-detail-1 .case-detail__research-heading {
  max-width: none;
  white-space: nowrap;
}

#case-detail-1 .case-detail__research-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--spacing-62);
  align-items: start;
}

#case-detail-1 .case-detail__research-col {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

#case-detail-1 .case-detail__research-lead {
  margin: 0;
  max-width: 46ch;
  font-size: var(--body-lead-size);
  line-height: 1.5;
  color: var(--color-grey);
}

#case-detail-1 .case-detail__research-quote {
  margin: 0;
  padding-left: 16px;
  border-left: var(--line-accent) solid var(--color-case-accent);
}

#case-detail-1 .case-detail__research-quote p {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(18px, 1.5vw, 24px);
  line-height: 1.4;
  color: var(--color-walnut-shadow);
}

#case-detail-1 .case-detail__research-quote cite {
  display: block;
  margin-top: var(--spacing-8);
  font-style: normal;
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-grey);
}

#case-detail-1 .case-detail__research-findings {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
}

#case-detail-1 .case-detail__research-findings li {
  position: relative;
  padding-left: 18px;
  font-size: var(--body-lead-size);
  line-height: 1.5;
  color: var(--color-grey);
}

#case-detail-1 .case-detail__research-findings li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 6px;
  height: 6px;
  background: var(--color-case-accent);
}

/* ---- Design Evolution: badge + text beside image, images shown uncropped ----
   Item layout itself lives in the shared, unscoped .case-detail__row--evolution
   module above (reused by Portfolio's Final Solution and Onboarding's
   Approach) — only this case study's padding is specific here. */

/* No top/bottom padding of its own — the 220px .case-detail__inner gap
   above already supplies the full boundary on both sides, so this row
   doesn't need to add anything on top of it. */
#case-detail-1 .case-detail__row--evolution {
  padding-top: 0;
  padding-bottom: 0;
}

/* ---- Impact: generous padding, plain eyebrow ---- */

/* No top/bottom padding of its own — the 220px .case-detail__inner gap
   above already supplies the full boundary on both sides. */
#case-detail-1 .case-detail__row--impact {
  padding-top: 0;
  padding-bottom: 0;
}

#case-detail-1 .case-detail__row--impact .case-detail__label {
  margin-bottom: var(--spacing-48);
}

#case-detail-1 .case-detail__impact-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--spacing-62) clamp(40px, 4vw, 60px);
}

#case-detail-1 .case-detail__impact-figure {
  display: flex;
  align-items: flex-end;
  height: clamp(44px, 6vw, 86px);
  margin-bottom: var(--spacing-18);
}

#case-detail-1 .case-detail__impact-stat {
  font-family: var(--font-serif);
  /* Same size/line-height as Design Evolution's version numerals
     (#case-detail-1 .case-detail__version-num). */
  font-size: clamp(44px, 6vw, 86px);
  line-height: 0.9;
  color: var(--color-brand-red);
}

#case-detail-1 .case-detail__impact-stat--icon {
  font-size: clamp(27px, 3.75vw, 54px);
  line-height: 0.9;
}

#case-detail-1 .case-detail__impact-item {
  padding-top: 0;
  border-top: none;
}

/* No Configurator-specific override needed here — the sitewide
   .case-detail__impact-item .case-detail__decision-heading default is now
   DM Sans/18.72px/600, the same value this used to require a scoped
   override to get. Matches Onboarding's and Portfolio's Impact-grid
   modules, and .case-detail__version-label below, directly. */

/* ---- Reflection: pull quote with the closing paragraphs beside it ---- */

#case-detail-1 .case-detail__row--reflection,
#case-detail-2 .case-detail__row--reflection,
#case-detail-3 .case-detail__row--reflection {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  column-gap: var(--spacing-62);
  row-gap: var(--spacing-24);
  align-items: start;
}

/* Configurator's Reflection sits directly under Impact
   (#case-detail-1 .case-detail__impact-grid: 3 equal tracks, gap
   var(--spacing-62) clamp(40px, 4vw, 60px)) — quote spans the first two
   of those tracks (+ the gap between them), body takes the third, so
   both edges land on the same column lines as the stat cards above. */
#case-detail-1 .case-detail__row--reflection {
  --impact-col-gap: clamp(40px, 4vw, 60px);
  grid-template-columns:
    calc((100% * 2 / 3) - (var(--impact-col-gap) / 3))
    calc((100% / 3) - (var(--impact-col-gap) * 2 / 3));
  column-gap: var(--impact-col-gap);
}

/* Onboarding's Reflection sits directly under Impact too
   (#case-detail-3 .case-detail__row--impact .case-detail__impact-grid:
   3 equal tracks, but unlike Configurator's it never overrides the base
   .case-detail__impact-grid gap, so both axes stay var(--spacing-62)) —
   same quote-spans-two/body-takes-third split as case 1, just with the
   row's own gap standing in for --impact-col-gap. */
#case-detail-3 .case-detail__row--reflection {
  grid-template-columns:
    calc((100% * 2 / 3) - (var(--spacing-62) / 3))
    calc((100% / 3) - (var(--spacing-62) * 2 / 3));
  column-gap: var(--spacing-62);
}

#case-detail-1 .case-detail__row--reflection > .case-detail__label,
#case-detail-2 .case-detail__row--reflection > .case-detail__label,
#case-detail-3 .case-detail__row--reflection > .case-detail__label {
  grid-column: 1 / -1;
}

#case-detail-1 .case-detail__reflection-quote,
#case-detail-2 .case-detail__reflection-quote,
#case-detail-3 .case-detail__reflection-quote {
  grid-column: 1;
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(26px, 3.6vw, 52px);
  line-height: 1.15;
  max-width: 32ch;
  color: var(--color-walnut-shadow);
}

#case-detail-1 .case-detail__reflection-body,
#case-detail-2 .case-detail__reflection-body,
#case-detail-3 .case-detail__reflection-body {
  grid-column: 2;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-24);
  margin-top: 0;
}

/* Portfolio's Reflection sits directly under Product Evolution
   (#case-detail-2 .case-detail__row--before-after — no local override, so
   it's the shared base .case-detail__row grid: label / Before / After as
   3 equal 1fr tracks, gap var(--spacing-62)) — same quote-spans-two/
   body-takes-third split as cases 1 and 3: quote covers the label +
   Before tracks, body lands exactly on the After track. */
#case-detail-2 .case-detail__row--reflection {
  grid-template-columns:
    calc((100% * 2 / 3) - (var(--spacing-62) / 3))
    calc((100% / 3) - (var(--spacing-62) * 2 / 3));
  column-gap: var(--spacing-62);
}

/* ---- Footer: hairline rule, two zones on one shared baseline ----
   Full-bleed like .case-detail__row--goal: cancels the section's own
   left/right padding via negative margin, then re-pads to the same
   --case-pad-l/--case-pad-r edges so the hairline rule spans the full
   screen width while the row/link/label content still lines up with
   every other row above it. Shared across all three case studies.

   "Back to top" isn't part of this grid — it's the page-level scroll
   progress rail (see .story__progress in scroll-story.css), a
   position:fixed element that docks itself near this footer's right edge
   once scrolled to the bottom, vertical text kept intentionally. ---- */

.case-detail__footer {
  margin-inline: calc(-1 * var(--case-pad-l)) calc(-1 * var(--case-pad-r));
  /* Top spacing is padding-only (no margin-top) so there's one spacing
     mechanism for this edge, not two stacked on top of the shared 220px
     flex gap. Top and bottom padding match (--spacing-48) so the footer's
     content band has equal whitespace above and below it. */
  padding: var(--spacing-48) var(--case-pad-r) var(--spacing-48) var(--case-pad-l);
  border-top: var(--line-hairline) solid var(--color-image-stroke);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: end;
  column-gap: var(--spacing-48);
  row-gap: var(--spacing-24);
}

/* Email/LinkedIn links reuse .about-meta exactly (see .about-meta /
   .about-meta__arrow in the About section below) — only its absolute
   overlay positioning is overridden here, since the footer sits in normal
   document flow rather than over the About panel. */
.case-detail__footer .about-meta {
  position: static;
  align-self: center;
  gap: 72px;
  /* Cancel the About overlay's own reveal-animation initial state (shared
     .about-meta rule further down hides it at opacity:0 until
     body.about-open) — this instance isn't part of that overlay. */
  opacity: 1;
  transform: none;
  transition: none;
}

/* .about-meta a's own color is --text-tertiary — a light, near-white tone
   meant to sit over the About panel's dark photo background. The footer
   sits on the case study's off-white background instead, so it needs the
   same dark ink the rest of the footer/case content uses. */
.case-detail__footer .about-meta a {
  color: var(--color-cork-border);
  border-bottom-color: var(--color-cork-border);
}

.case-detail__footer .about-meta a:hover {
  color: var(--color-driftwood);
  border-bottom-color: var(--color-driftwood);
}

/* Same top-down text fill as the About panel's Email/LinkedIn, but
   filling from this footer's dark ink (cork-border) instead of the
   panel's near-white text-tertiary, since that's this context's own
   "off" color. */
.case-detail__footer .about-meta__label {
  background-image: linear-gradient(
    to bottom,
    var(--color-driftwood) 50%,
    var(--color-cork-border) 50%
  );
}

.case-detail__footer-next {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-self: end;
  text-align: right;
}

.case-detail__footer-next-label {
  margin-bottom: 2px;
  font-size: var(--text-2xs);
  font-weight: var(--font-weight-regular);
  letter-spacing: 0.08em;
  color: var(--color-grey);
}

.case-detail__footer-next-title {
  font-family: var(--font-serif);
  font-size: var(--text-xl);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--color-walnut-shadow);
}

/* ---------- RESPONSIVE ---------- */

@media (max-width: 980px) {
  .intro { padding: var(--spacing-24) var(--spacing-24) var(--spacing-18); max-width: 80vw; }
  /* Mobile: nav scrolls away with page instead of staying fixed, so ABOUT
     doesn't hover over content below the hero. */
  .intro { position: absolute; }
  /* Offset by -8px (.about-label's own left padding) so the "ABOUT" glyph
     itself lands flush at spacing-24, matching the Case study label, the
     case title, and the description below it - not the box edge. */
  .side-nav { position: absolute; left: calc(var(--spacing-24) - 8px); }
  .side-tab { font-size: var(--text-sm); }

  /* Scoped to :not(.about-open) — the About panel has its own red backing
     bar behind .intro/.side-nav (body.about-open .intro::before) and needs
     the header to stay light/white against that. This dark-ink swap is
     only for the white hero's closed state. */
  body:not(.about-open) .intro,
  body:not(.about-open) .side-tab { color: var(--color-walnut-shadow); }
  /* .intro__name/.intro__role each carry their own explicit `color` (not
     inherited), so the swap on .intro above doesn't reach them — set both
     directly. */
  body:not(.about-open) .intro__name { color: var(--color-walnut-shadow); }
  body:not(.about-open) .intro__role { color: var(--color-grey); }

  /* .about-label's resting state is painted via background-clip: text
     against --text-tertiary (translucent white, tuned for the dark hero
     photo) rather than plain `color`, so the swap above doesn't reach it —
     without this override "ABOUT" is white-on-white and invisible here. */
  body:not(.about-open) .about-label {
    background-image: linear-gradient(
      to bottom,
      rgba(139, 40, 40, 0.85) 50%,
      var(--color-walnut-shadow) 50%
    );
  }

  /* Configurator (case 1) / Portfolio Overview (case 2) / Onboarding
     (case 3) mobile: the dark-ink header swap above assumes the hero sits
     on the shared mobile layout's off-white page background - these three
     cases' hero is now a full-bleed photo instead (see the .hero:has(...)
     overrides further below), so the header needs to stay light/white
     here, same as desktop.
     :has() on body (an ancestor of both .hero and .intro/.side-nav) reaches
     this from outside .hero, and out-specifies the plain
     body:not(.about-open) rules above regardless of source order. */
  body:not(.about-open):has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .intro,
  body:not(.about-open):has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .side-tab {
    color: var(--text-primary);
  }
  body:not(.about-open):has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .intro__name {
    color: var(--text-primary);
  }
  body:not(.about-open):has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .intro__role {
    color: var(--text-tertiary);
  }
  body:not(.about-open):has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .about-label {
    background-image: linear-gradient(
      to bottom,
      rgba(139, 40, 40, 0.85) 50%,
      var(--text-tertiary) 50%
    );
  }

  /* display:grid (not the flex column a first pass might reach for) so the
     photo/video crossfade layers below can overlap in one 58vh cell —
     .hero__photo, both crossfade <video>s, and .hero__screen(-video) all
     share grid-row/column 1. A flex column would instead stack each of
     those as its own full-height sibling box (three 58vh bands, one mostly
     empty/transparent) since only one carries opacity:1 at a time; nothing
     here otherwise takes it out of flow. Ghost title and case-card auto-
     place into the implicit rows that follow, below the image. */
  .hero {
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 48vh;
    min-height: 0;
    /* Matches .intro/.side-nav's own 100px height plus a small buffer —
       trimmed from an earlier 140px so image+title+desc fit in one
       viewport instead of pushing the description past the fold. */
    padding-top: 104px;
    padding-bottom: var(--spacing-12);
    background: var(--color-off-white);
  }

  .hero__bg { display: none; }

  /* Height-driven box, not a fixed aspect ratio — claims as much vertical
     space as the layout can spare and crops in from the sides to fill it.
     overflow:hidden here (not just on .hero, which is full-bleed width) is
     load-bearing: .hero__screen-video is deliberately oversized (cover-fit
     against the WHOLE frame, then clip-path'd to just the screen quad) and
     without a boundary exactly matching this padded box, that oversized
     video bleeds into the side gutters as a visible black rectangle
     wherever the quad's math lands outside it. */
  .hero__media {
    grid-row: 1;
    grid-column: 1;
    position: relative;
    overflow: hidden;
    width: calc(100% - var(--spacing-24) * 2);
    height: 100%;
    margin: 0 auto;
  }

  .hero__photo, .hero__video {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
  }

  /* Configurator (case 1) / Portfolio Overview (case 2) / Onboarding
     (case 3) mobile: desktop keeps its full-bleed composite video/photo
     pairing unchanged (baked-in or quad-warped backdrop, no separate
     background layer to swap on desktop). Mobile instead hides that
     existing layer (.hero__photo/.hero__video/.hero__screen-video below)
     and shows a dedicated portrait mockup photo with more room around the
     device - the same untouched video keeps playing, but only its screen
     content is cropped out and overlaid onto this photo's own blank screen
     (see #hero-mobile-screen-video positioning in main.js), rather than its
     full scene/quad (which would double up against the photo's own
     device). :has() keys all of this to cases 1/2/3, matching .case-tab's
     is-active state with no extra JS state needed. */
  .hero:has(.case-tab[data-case="1"].is-active) .hero__media {
    /* Fallback fit until JS overrides background-size/-position with the
       zoomed-to-screen-width fit (see positionMobileScreenOverlay() in
       main.js) — keeps this from flashing unstyled if JS is slow/disabled. */
    background: url('../assets/configurator_mobile.jpeg') center / cover no-repeat;
  }

  .hero:has(.case-tab[data-case="2"].is-active) .hero__media {
    background: url('../assets/portofolio_mobile.jpeg') center / cover no-repeat;
  }

  .hero:has(.case-tab[data-case="3"].is-active) .hero__media {
    background: url('../assets/onboarding_mobile.png') center / cover no-repeat;
  }

  .hero:has(.case-tab[data-case="1"].is-active) .hero__photo,
  .hero:has(.case-tab[data-case="1"].is-active) .hero__video,
  .hero:has(.case-tab[data-case="2"].is-active) .hero__photo,
  .hero:has(.case-tab[data-case="2"].is-active) .hero__video,
  .hero:has(.case-tab[data-case="3"].is-active) .hero__photo,
  .hero:has(.case-tab[data-case="3"].is-active) .hero__screen,
  .hero:has(.case-tab[data-case="3"].is-active) .hero__screen-video {
    display: none;
  }

  .hero__mobile-screen-window {
    display: none;
    position: absolute;
    overflow: hidden;
    background: #e4e4e0;
  }

  .hero:has(.case-tab[data-case="1"].is-active) .hero__mobile-screen-window,
  .hero:has(.case-tab[data-case="2"].is-active) .hero__mobile-screen-window,
  .hero:has(.case-tab[data-case="3"].is-active) .hero__mobile-screen-window {
    display: block;
  }

  .hero__mobile-screen-video {
    position: absolute;
    pointer-events: none;
    /* Chrome's default object-fit for a bare <video> is `contain`, not
       `fill` - without this, the browser letterboxes the video's native
       frame inside the width/height positionMobileScreenOverlay() sets in
       js/main.js (which already do the stretch-to-crop math themselves),
       leaving a gap on whichever axis the native and target aspect ratios
       disagree on (most visible on case 2 - right edge). */
    object-fit: fill;
  }

  .hero__ghost-title {
    display: block;
    margin: -0.5em 0 0;
    padding: 0 var(--spacing-24);
    font-family: var(--font-serif);
    font-size: clamp(40px, 16vw, 72px);
    font-weight: var(--font-weight-medium);
    line-height: 1;
    letter-spacing: -0.02em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.92);
    white-space: nowrap;
    overflow: hidden;
    pointer-events: none;
    position: relative;
    z-index: 2;
  }

  /* Hover-scrub controls don't apply on touch. */
  .hero-video__hitbox { display: none; }

  .case-card {
    position: static;
    z-index: 3;
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    gap: var(--spacing-12);
    padding: var(--spacing-12) var(--spacing-24) var(--spacing-8);
  }

  .case-nav {
    position: static;
    height: auto;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--spacing-8);
  }

  .case-nav::before { display: none; }
  /* Base rule renders this as a rotated vertical sidebar label
     (position:absolute; left:calc(--rail-label-gap - --spacing-48);
     writing-mode:vertical-rl) for desktop's side rail — reset all of
     that here so it sits as a normal horizontal line above the tabs,
     same size/color as .intro__role. */
  .case-nav__label {
    position: static;
    left: auto;
    top: auto;
    bottom: auto;
    width: auto;
    writing-mode: horizontal-tb;
    transform: none;
    display: block;
    align-items: normal;
    justify-content: normal;
    font-size: var(--text-xs);
    color: var(--color-grey);
  }
  .case-nav__list { flex-direction: row; height: auto; gap: var(--spacing-8); }
  .case-tab { width: 44px; font-size: 18px; border-color: var(--color-grey); color: var(--color-grey); }

  .case-card__bottom { position: static; max-width: none; }
  /* No cushion/shadow here - this is the plain-card-on-off-white fallback
     layout (dark text on light background already), and position:static
     above means ::before would no longer be contained to this box anyway. */
  .case-card__bottom::before { content: none; }
  .case-card__title { font-size: clamp(32px, 12vw, 64px); color: var(--color-walnut-shadow); text-shadow: none; }
  .case-card__desc { max-width: none; color: var(--color-grey); text-shadow: none; }

  /* Configurator (case 1) / Portfolio Overview (case 2) / Onboarding
     (case 3) mobile: matches the desktop concept instead of the shared
     mobile layout above (small card + text below on off-white). The mockup
     photo fills the whole viewport edge-to-edge and case-nav / case-card
     overlay it directly in white, same as desktop - no off-white section,
     no ghost-title (that element exists only to bridge a card-then-
     white-section layout none of these cases use anymore). */
  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) {
    display: block;
    min-height: 100vh;
    padding: 0;
    background: none;
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .hero__media {
    position: absolute;
    inset: 0;
    width: 100%;
    margin: 0;
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .hero__ghost-title {
    display: none;
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-card {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
    pointer-events: none;
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-nav {
    position: absolute;
    left: var(--spacing-24);
    top: 104px;
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-nav__label {
    color: var(--text-tertiary);
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-tab:not(.is-active) {
    border-color: var(--text-tertiary);
    color: var(--text-tertiary);
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-card__bottom {
    position: absolute;
    left: var(--spacing-24);
    right: var(--spacing-24);
    bottom: var(--spacing-24);
  }

  /* Re-declared, not inherited from the base rule: the mobile-default
     fallback block above sets `content: none` at the same specificity and
     comes later in the cascade than the base rule, so without this the
     cushion would stay switched off here too. */
  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-card__bottom::before {
    content: '';
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-card__title {
    color: var(--text-primary);
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  }

  .hero:has(.case-tab[data-case="1"].is-active, .case-tab[data-case="2"].is-active, .case-tab[data-case="3"].is-active) .case-card__desc {
    color: var(--text-primary);
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  }

  .about {
    padding: var(--spacing-24);
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .about::-webkit-scrollbar { display: none; }
  .about-heading { margin: 140px 0 0; max-width: 100%; }
  .about-lead { margin: 24px 0 0; max-width: 100%; }
  .about-portrait {
    position: static;
    width: 100%;
    height: 50vh;
    margin: var(--spacing-24) 0;
  }
  .about-meta { position: static; margin-top: var(--spacing-48); }

  .case-detail {
    --case-pad-l: var(--spacing-24);
    --case-pad-r: var(--spacing-24);
    padding: var(--spacing-62) var(--case-pad-r) 0 var(--case-pad-l);
    /* Stacked mobile layout (no pin-and-cover) — same full-bleed rule as
       .case-detail__footer, sitting between the landing hero and Overview. */
    border-top: var(--line-hairline) solid var(--color-image-stroke);
  }
  /* Same inter-section rhythm as desktop's 220px, trimmed for the shorter
     mobile viewport so consecutive sections don't read as disconnected. */
  .case-detail__inner { gap: 150px; }
  .case-detail__row { grid-template-columns: 1fr; column-gap: var(--spacing-18); row-gap: var(--spacing-18); }
  .case-detail__principle,
  .case-detail__decisions,
  .case-detail__impact-content,
  .case-detail__reflection-body,
  .case-detail__problem-visual,
  .case-detail__row-visual,
  .case-detail__intro-heading,
  .case-detail__research-intro { grid-column: 1; }

  #case-detail-2 .case-detail__row--exploration,
  #case-detail-2 .case-detail__row--validation { grid-template-columns: 1fr; }

  .case-detail__validation-visual {
    width: 100%;
    margin-top: 0;
  }

  .case-detail__row--overview .case-detail__overview-secondary:nth-of-type(1),
  .case-detail__row--overview .case-detail__overview-secondary:nth-of-type(2),
  .case-detail__row--problem .case-detail__overview-secondary:nth-of-type(1),
  .case-detail__row--problem .case-detail__overview-secondary:nth-of-type(2) { grid-column: 1; }

  .case-detail__columns > *:nth-child(1),
  .case-detail__columns > *:nth-child(2) { grid-column: 1; }

  .case-detail__problem-visual { grid-template-columns: 1fr; row-gap: var(--spacing-18); }
  .case-detail__row--evolution .case-detail__decision { grid-template-columns: 1fr; }
  .case-detail__row--evolution .case-detail__decision-text,
  .case-detail__row--evolution .case-detail__decision-figure { grid-column: 1; grid-row: auto; }
  .case-detail__impact-grid { grid-template-columns: 1fr; }
  #case-detail-1 .case-detail__impact-grid,
  #case-detail-2 .case-detail__impact-grid,
  #case-detail-3 .case-detail__row--impact .case-detail__impact-grid { grid-template-columns: 1fr; }
  .case-detail__decision .case-detail__key-decision-compare { flex-direction: column; gap: var(--spacing-24); }
  #case-detail-3 .case-detail__row--problem { grid-template-columns: 1fr; }
  #case-detail-3 .case-detail__row--problem .case-detail__label,
  #case-detail-3 .case-detail__row--problem .case-detail__intro-heading,
  #case-detail-3 .case-detail__row--problem .case-detail__impact-content { grid-column: 1; grid-row: auto; }
  #case-detail-3 .case-detail__row--problem .case-detail__impact-grid { grid-template-columns: 1fr; }
  #case-detail-3 .case-detail__row--problem .case-detail__intro-heading,
  #case-detail-3 .case-detail__row--problem .case-detail__overview-lead { max-width: none; }
  #case-detail-1 .case-detail__row--reflection,
  #case-detail-2 .case-detail__row--reflection,
  #case-detail-3 .case-detail__row--reflection { grid-template-columns: 1fr; }
  #case-detail-1 .case-detail__reflection-quote,
  #case-detail-1 .case-detail__reflection-body,
  #case-detail-2 .case-detail__reflection-quote,
  #case-detail-2 .case-detail__reflection-body,
  #case-detail-3 .case-detail__reflection-quote,
  #case-detail-3 .case-detail__reflection-body { grid-column: 1; max-width: none; }

  #case-detail-1 .case-detail__row--overview,
  #case-detail-2 .case-detail__row--overview,
  #case-detail-3 .case-detail__row--overview { grid-template-columns: 1fr; row-gap: var(--spacing-24); column-gap: 0; }
  #case-detail-1 .case-detail__overview-col-text,
  #case-detail-2 .case-detail__overview-col-text,
  #case-detail-3 .case-detail__overview-col-text { grid-column: 1; grid-row: 1; max-width: none; }
  #case-detail-1 .case-detail__overview-ledger,
  #case-detail-2 .case-detail__overview-ledger,
  #case-detail-3 .case-detail__overview-ledger { grid-column: 1; grid-row: 2; margin-top: 0; }

  #case-detail-1 .case-detail__row--problem,
  #case-detail-2 .case-detail__row--problem { grid-template-columns: 1fr; }
  #case-detail-1 .case-detail__row--problem .case-detail__intro-heading,
  #case-detail-1 .case-detail__row--problem .case-detail__overview-secondary,
  #case-detail-2 .case-detail__row--problem .case-detail__intro-heading,
  #case-detail-2 .case-detail__row--problem .case-detail__overview-secondary { max-width: none; }

  #case-detail-1 .case-detail__research-heading { max-width: none; white-space: normal; }
  #case-detail-1 .case-detail__research-columns { grid-template-columns: 1fr; row-gap: var(--spacing-24); }
  #case-detail-1 .case-detail__research-lead { max-width: none; }

  .case-detail__footer {
    /* Pulls the footer up against the 150px .case-detail__inner gap above,
       which is sized for rhythm between content sections, not for the
       Reflection-to-footer transition. */
    margin-top: -80px;
    padding-top: var(--spacing-48);
    padding-bottom: var(--spacing-48);
    column-gap: 18px;
  }
  .case-detail__footer .about-meta { gap: 18px; }
  .case-detail__footer-next-title { font-size: clamp(14px, 4.2vw, 19px); }
}

@media (max-width: 560px) {
  .intro__name { font-size: 19px; }
  .intro__role { font-size: 11px; }
  .case-detail__footer-next-title { font-size: 19px; }
}

/* Past ~1800px every fluid clamp on the hero (--text-xl chief among them)
   has hit its own ceiling and stops growing, wasting the extra room on a
   large/4K display. Raise the ceiling for everything except .intro__name /
   .intro__role, which keep their existing cap on purpose. .case-tab shares
   --text-xl with .intro__name, so its size is overridden directly here
   rather than through the token — bumping the token would grow the header
   too.
   .case-card__title is deliberately NOT raised here. Its font-size grows
   against a fixed-vh bottom offset (.case-card__bottom's `bottom: 5.45vh`)
   while the hero photo behind it is object-fit: cover — on a wide-but-short
   viewport (a 21:9 ultrawide monitor, e.g. 2560x1080) `cover` crops enough
   of the photo's height that the laptop-mockup screen content sits close
   to the bottom edge already; pushing the title past its existing
   clamp(40px, 6.5vw, 145px) ceiling there measurably closes that gap until
   the title's own ascenders overlap the mockup image above it (confirmed
   visually via the QA agent at 2560x1100 — worse on Case 2's two-line
   title). Fixing that properly needs the mockup's screen-rect positioning
   to account for vertical crop, which is a separate change from this
   font-scaling pass. */
@media (min-width: 1800px) {
  .side-tab { font-size: clamp(19px, 1.05vw, 26px); }
  .case-nav__label { font-size: clamp(14px, 0.8vw, 18px); }
  .case-tab { width: clamp(50px, 3vw, 66px); font-size: clamp(28px, 1.55vw, 38px); }
  .case-card__desc { font-size: clamp(21px, 1.3vw, 28px); max-width: 36vw; }
}
