/* ── CleanHub Case Study ─────────────────────────────────────────── */

/* ── HERO ────────────────────────────────────────────────────────── */
/* Structure/behavior mirrors .mind-hero (mind.css) — tucks under the
   fixed nav the same way (negative margin-top cancelling .page's own
   padding-top, padding-top adding it back), same viewport-fit-capped-
   at-746px height, same full-bleed hero (side-pad only, no max-width
   of its own) with .ch-hero-top doing the actual --max-w centering.
   padding-top departs from .mind-hero's own 96px+nav-h figure (this
   hero's own vertical rhythm reads better tighter, at a plain 2x
   nav-h) and the right side differs: a wide report screenshot instead
   of a phone mockup, so it's sized by height and bled past the edge
   (below) instead of mind's fixed-aspect-ratio phone frame. */
.ch-hero {
  /* position: relative (no z-index) — containing block for
     .ch-hero-blob-zone below, same reasoning as .mind-hero's own note
     in mind.css: a child .bg-blob's z-index: -1 needs to resolve
     against .page's ONE shared stacking context, not a local one, so
     nothing here should open its own stacking context. overflow:
     hidden keeps the blob contained AND, since this hero now spans the
     full viewport width (no max-width of its own), doubles as the
     crop for the screenshot's own intentional bleed past the right
     edge below — no separate body-level overflow-x: clip needed (and
     unlike overflow on body, this can't couple into overflow-y or
     touch position: sticky elsewhere on the page, since it's scoped to
     just this box). */
  position: relative;
  overflow: hidden;
  margin-top: calc(-1 * var(--nav-h));
  padding: calc(2 * var(--nav-h)) var(--side-pad) 0;
  min-height: min(100vh, calc(746px + var(--nav-h)));
  display: flex;
  flex-direction: column;
}
/* Figma node 1325:38678 ("Rectangle 208") — the same masked/blurred
   .bg-blob component 7Mind's hero uses (see .mind-hero-blob-zone,
   mind.css), just the sage→blue gradient instead of wine→rust and
   positioned toward the top-right instead of bottom. Unlike
   .mind-hero-blob-zone (inset skips the nav-h slice under the nav,
   since that blob is bottom-anchored and never reaches that high
   anyway), this one insets to the true top (0, not var(--nav-h)) —
   this blob deliberately pokes into the hero's top-right corner, so
   clipping its zone at nav-h would cut it off with a hard, visible
   edge floating above a flat gap instead of fading out naturally
   under the (transparent-until-scrolled) nav. */
.ch-hero-blob-zone { position: absolute; inset: 0; overflow: hidden; }
.ch-hero-top {
  max-width: var(--max-w);
  margin: 0 auto;
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 500px) 1fr;
  gap: 80px;
  align-items: center;
  flex: 1;
}
/* Type lives in the shared .section-eyebrow component in style.css.
   Even 20px gap across all 3 hero text bits (eyebrow → title →
   subtitle), matching .mind-hero-left's own rhythm (mind.css) instead
   of the old 12px/40px pairing. .ch-tag/.ch-opener both pick up the
   same warm --brown .mind-tag/.mind-opener use. */
.ch-tag {
  margin-bottom: 20px;
  color: var(--brown);
}
.ch-title {
  margin-bottom: 20px;
}
.ch-opener {
  font-family: var(--font-sans);
  font-size: 20px;
  line-height: 1.5;
  color: var(--brown);
  font-weight: 400;
  max-width: 480px;
}
.ch-hero-right {
  display: flex;
  align-items: center;
  justify-content: center;
}
.ch-screen-frame {
  width: 100%;
  max-width: 480px;
}
/* No overflow:hidden here — .ch-screen-inner carries the .frame class
   (style.css), whose glass bleed relies on overflow staying visible.
   The image already fills the box exactly via object-fit: cover, so
   nothing needed the clip anyway. */
/* Same 16px combo (.frame's own 8px default doubled) 7Mind's hero
   mockup uses — .mind-hero-mockup .frame in mind.css: border-radius
   is .frame's own box, --frame-radius matches it on the wrapped img,
   --frame-bleed keeps the halo's outer curve the same 16px past that
   so both arcs share one center point instead of reading as a ring of
   uneven thickness. Scoped to .ch-hero-right — .ch-screen-inner also
   wraps other media slots elsewhere on the page, which keep the
   component's plain 8px default. Only at >=1360px — below that the
   frame goes back to the component's own 8px default, same as every
   other breakpoint. */
@media (min-width: 1360px) {
  .ch-hero-right .ch-screen-inner.frame {
    border-radius: 16px;
    --frame-radius: 16px;
    --frame-bleed: 16px;
  }
}

/* ── META ROW ────────────────────────────────────────────────────── */
/* Sibling after </header>, not nested inside it — matches
   .mind-meta-row (mind.css) exactly. */
.ch-meta-row {
  max-width: var(--max-w-outer);
  margin: 64px auto 0;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px 40px;
  padding: 24px var(--side-pad) 56px;
}
/* Type for both lives in the shared .section-eyebrow and .body-regular
   components in style.css — see the .ch-meta-label.section-eyebrow /
   .ch-meta-value.body-regular pairing in the markup. */
.ch-meta-label {
  margin-bottom: 8px;
}

/* Desktop hero: the mockup is sized by height, not by its grid column,
   so it keeps its own ratio and is free to run past the column's (and
   the viewport's) right edge instead of being squeezed to fit. Mobile
   keeps the box-fit/centered treatment above untouched for now. */
@media (min-width: 1101px) {
  /* The row's height comes from .ch-hero-left's natural content (the
     text) alone — .ch-hero-right contributes nothing to it, because
     its content is taken out of flow with position: absolute below.
     That matters: a percentage height nested this many levels inside
     a grid item can't resolve during row-sizing, so the browser falls
     back to the *image's own* intrinsic pixel size to measure the row
     instead — which is how the image ended up ~1500px tall. With the
     image out of flow, the row is sized only by the text, and the
     image's height: 100% resolves against that (now genuinely
     definite) row height instead. */
  .ch-hero-top {
    align-items: stretch; /* .ch-hero-right needs a definite height to give its absolute child */
  }
  .ch-hero-left {
    align-self: center; /* the row is now as tall as .ch-hero-right, so re-center the text within it */
  }
  .ch-hero-right {
    position: relative;
    overflow: visible;
  }
  .ch-screen-frame {
    /* top: 50% + the translateY below centers the frame vertically in
       its cell instead of pinning it to the top — matters once
       max-height (below) caps it shorter than the row itself. */
    position: absolute;
    top: 50%;
    left: 0;
    transform: translateY(-50%);
    height: 100%;
    max-height: 576px;
    width: auto;
    max-width: none;
  }
  .ch-screen-inner {
    height: 100%;
    width: fit-content; /* shrink-wrap the img's own rendered width, don't stretch to fill .ch-screen-frame */
  }
  /* Extra .ch-hero-right qualifier beats .ch-media-slot img's width:100%
     (report.css, defined later, same specificity otherwise) — this img
     needs to size itself from its own ratio, not fill its container.
     max-width: none overrides the global img{max-width:100%} reset
     (style.css) — that rule is what was actually capping the image to
     its container's width even with width:auto correctly set, since
     max-width clamps the final used value regardless of how width was
     derived. */
  .ch-hero-right .ch-screen-inner img {
    width: auto;
    max-width: none;
    height: 100%;
    object-fit: contain;
  }
}
/* Narrower text column + shorter image below 1360px — keeps the
   screenshot from crowding the text as the row's own available width
   shrinks. Comes after the base >=1101px rule above so its max-height
   wins over that rule's own 576px in the overlapping 1101-1359px
   range. */
@media (min-width: 1101px) and (max-width: 1359px) {
  .ch-hero-top { grid-template-columns: minmax(0, 390px) 1fr; }
  .ch-hero-right .ch-screen-frame { max-height: 432px; }
}

/* ── MEDIA SLOTS ─────────────────────────────────────────────────── */
.ch-media-slot img,
.ch-media-slot video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ch-placeholder-media {
  width: 100%;
  min-height: 280px;
  background: linear-gradient(135deg, #e8e4de 0%, #d8d2ca 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--mid);
  opacity: 0.6;
}
.ch-screen-frame .ch-placeholder-media { min-height: 320px; }

/* ── BODY ────────────────────────────────────────────────────────── */
.ch-body {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 0 var(--side-pad) 120px;
}
/* position: relative (no z-index) — a containing block for a .bg-blob
   child's top/left%, but its z-index:-1 needs to resolve against
   .page's ONE shared stacking context (style.css), not a context
   scoped to just this section (see .page's own note on why). */
.ch-section {
  margin-top: 100px;
  margin-bottom: 0;
  position: relative;
}
.ch-section-label {
  margin-bottom: 12px;
}
.ch-h2 {
  margin-bottom: 24px;
}
.ch-h2 em { font-style: italic; }
.ch-prose-col { max-width: 798px; }
.ch-p {
  margin-bottom: 24px;
  max-width: 798px;
}

/* ── STATS ───────────────────────────────────────────────────────── */
/* Columns/gap/padding come from the shared .stats component now
   (style.css, auto-fit + minmax(200px)) — only this row's own
   placement stays here. */
.ch-stats {
  max-width: var(--content-w-md);
  margin: 40px auto;
}

/* ── BULLETS ─────────────────────────────────────────────────────── */
.ch-bullets {
  list-style: none;
  margin: 8px 0 20px;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.ch-bullets li {
  font-family: var(--font-sans);
  font-size: 20px;
  line-height: 1.2;
  color: var(--black);
  font-weight: 400;
  letter-spacing: -0.02em;
  padding-left: 21px;
  position: relative;
}
.ch-bullets li::before {
  content: '•';
  position: absolute;
  left: 0;
  color: var(--black);
}

/* ── PROBLEM CARDS ───────────────────────────────────────────────── */
/* Skin (.card + .card--negative + .card--compact) lives in the shared
   .card component in style.css, same badge-led card 7Mind's Problem
   section uses — this only sets the grid this particular instance needs. */
.ch-problem-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  max-width: var(--content-w-md);
  margin: 40px auto;
}

/* ── ANNOTATED SCROLL (problem section) ──────────────────────────── */
.ch-annotated-scroll {
  position: relative;
  margin: 60px -64px;
  height: 3200px;
}
.ch-annotated-inner {
  position: sticky;
  top: var(--nav-h);
  height: calc(100vh - var(--nav-h));
  display: grid;
  grid-template-columns: 180px 1fr 180px;
  gap: 20px;
  align-items: center;
  padding: 0 24px;
}
.ch-annotations { position: relative; height: 520px; z-index: 2; }

/* Visual skin (border/background/text/blur) lives in the shared
   .annotation-card component in style.css — this only positions it
   within the scroll-linked layout and drives its reveal animation. */
.ch-annotation {
  position: absolute;
  width: 100%;
  opacity: 0;
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: none;
}
.ch-annotation.is-visible { opacity: 1; transform: translateX(0) !important; }
.ch-annotation-left { right: 0; transform: translateX(20px); }
.ch-annotation-right { left: 0; transform: translateX(-20px); }

.ch-screen-sticky-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.ch-screen-sticky {
  background: rgba(255,255,255,0.55);
  backdrop-filter: blur(20px);
  border-radius: 20px;
  padding: 10px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1), 0 32px 48px rgba(0,0,0,0.08);
  width: 100%;
  height: 80vh;
  max-height: 1000px;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.8s ease, transform 0.8s ease;
}
.ch-screen-sticky.is-visible { opacity: 1; transform: translateY(0); }
.ch-screen-viewport {
  height: 100%;
  overflow: hidden;
  border-radius: 10px;
  background: #fff;
  position: relative;
}
.ch-screen-scroll-content {
  position: absolute;
  top: 0; left: 0; right: 0;
  will-change: transform;
}
.ch-screen-scroll-content img { display: block; width: 100%; height: auto; }

/* ── ITERATION TITLES ────────────────────────────────────────────── */
.ch-iteration-title {
  margin-top: 56px;
  margin-bottom: 24px;
}

/* ── BEFORE / AFTER PAIRS ────────────────────────────────────────── */
.ch-ba-section { margin: 32px 0; }
.ch-ba-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  align-items: start;
}
/* Type lives in the shared .section-eyebrow component in style.css. */
.ch-ba-label {
  margin-bottom: 12px;
}
.ch-media-frame img,
.ch-media-frame video {
  width: 100%;
  height: auto;
  display: block;
}
.ch-ba-annotations {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
}

/* Text / media / annotations in three columns (e.g. v1-secondary). The
   breakpoints below key off the trio's own width rather than the
   viewport's, so the parent is the query container. The trio itself is
   the positioning context for the connector line report.js draws — an
   absolutely positioned child, so it stays out of the grid flow. */
.ch-ba-section:has(.ch-ba-trio) {
  container-type: inline-size;
  container-name: ch-ba;
}
.ch-ba-trio {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 260px) minmax(0, 1fr) 180px;
  gap: 32px;
  align-items: start;
}
/* The annotations sit beside the image here, not under it, so they don't
   need .ch-ba-annotations' default top spacing. */
.ch-ba-trio .ch-ba-annotations { margin-top: 0; }
/* Grid gap owns the spacing between rows once the paragraph stacks. */
.ch-ba-trio > .ch-p { margin-bottom: 0; }
.ch-ba-media { min-width: 0; }
/* The "After" eyebrow sits above the image inside the same grid cell, so
   .ch-ba-media's own top (and therefore the row's align-items:start line)
   lands on the eyebrow, not the image. Pinning the eyebrow's line-height
   makes its box height known (12px), so the paragraph can be pushed down
   by exactly that plus its 12px margin-bottom to line up with the image
   edge instead. */
.ch-ba-trio .ch-ba-label { line-height: 1; }
.ch-ba-trio > .ch-p { margin-top: 24px; }

/* Below 900px viewport width, the precise annotation/connector-line combo
   stops being legible against a phone-width shot — swapped for a single
   flattened image instead of trying to keep the desktop layout readable. */
.ch-ba-mobile-media { display: none; }
@media (max-width: 900px) {
  .ch-ba-trio .ch-ba-media,
  .ch-ba-trio .ch-ba-annotations,
  .ch-ba-trio .ch-shot-lines { display: none; }
  .ch-ba-trio .ch-ba-mobile-media {
    display: block;
    grid-column: 1 / -1;
  }
}

/* Below 1200px the paragraph moves above the image, which keeps the shot
   usable while the annotations stay alongside it — no longer beside the
   image, so the top-edge offset above no longer applies. It now reads as
   a standalone narrow content block, same as the page's other prose
   (.section-narrow), rather than stretching the full trio width. */
@container ch-ba (max-width: 1199px) {
  .ch-ba-trio { grid-template-columns: minmax(0, 1fr) 180px; }
  .ch-ba-trio > .ch-p {
    grid-column: 1 / -1;
    margin-top: 0;
    max-width: var(--content-w);
    margin-left: auto;
    margin-right: auto;
  }
}
/* Too narrow for a 180px column beside the shot — stack everything. The
   connector line only makes sense pointing sideways at the image, so it's
   hidden here too, same as .ch-shot-canvas does at its own mobile
   breakpoint — report.js's sameRow check already stops positioning the
   cards against it, but leaves the (now meaningless) line itself drawn. */
@container ch-ba (max-width: 700px) {
  .ch-ba-trio { grid-template-columns: minmax(0, 1fr); }
  .ch-ba-trio .ch-ba-annotations { margin-top: 12px; }
  .ch-ba-trio .ch-shot-lines { display: none; }
}

/* ── V2 STICKY BLOCK SHOTS (single flattened before/after image, no
   frame — the image itself is the whole visual, not a screenshot being
   dressed up) ──────────────────────────────────────────────────────── */
.ch-v2-shot {
  position: relative;
  width: 100%;
  margin: 32px 0;
}
.ch-v2-shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* ── SHOT CONNECTOR LINES (v1-secondary trio; see .ch-ba-trio above) ── */
.ch-shot-lines {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.ch-shot-lines line {
  stroke-width: 1;
}
/* Line + dot fade in with their annotation, same timing as .ch-reveal's
   own opacity transition, so the pair reads as one object appearing
   together rather than the line being a static fixture on the image. */
.ch-shot-line-item {
  opacity: 0;
  transition: opacity 0.5s ease;
}
.ch-shot-line-item.is-visible { opacity: 1; }
.ch-shot-lines .line-prob { stroke: var(--neutral-border); }
.ch-shot-lines .line-sol { stroke: var(--neutral-border); }
.ch-shot-lines circle.line-prob { fill: var(--neutral-border); }
.ch-shot-lines circle.line-sol { fill: var(--neutral-border); }
@media (max-width: 900px) {
  .ch-shot-lines { display: none; }
}

/* ── ANNOTATIONS (solution + problem) ──────────────────────────────
   Skin (border/background/text/blur) comes from the shared
   .annotation-card component in style.css — this only keeps the
   layout concerns specific to this shot-canvas/before-after context. */
.ch-sol-annotation,
.ch-prob-annotation {
  display: inline-flex;
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.38;
  max-width: 260px;
}
.ch-sol-annotation p,
.ch-prob-annotation p { margin: 0; }

/* ── SCROLL REVEAL ───────────────────────────────────────────────── */
.ch-reveal {
  opacity: 0;
  transition: opacity 0.5s ease, transform 0.5s ease;
}
.ch-reveal[data-direction="right"] { transform: translateX(-16px); }
.ch-reveal[data-direction="left"] { transform: translateX(16px); }
.ch-reveal.is-visible { opacity: 1; transform: translateX(0); }

/* ── RESULTS CARDS ───────────────────────────────────────────────── */
.ch-results-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  max-width: var(--content-w);
  margin: 32px auto;
}
/* Skin is just the eyebrow label (pairs with the shared .section-eyebrow
   component in style.css — this only keeps its margin and color variant)
   plus a plain list. No card box (padding/border/background) anymore. */
.ch-results-label {
  margin-bottom: 16px;
  display: block;
}
.ch-results-label--worked { color: var(--green); }
.ch-results-label--learned { color: var(--red); }
.ch-results-list { list-style: none; display: flex; flex-direction: column; gap: 16px; }
.ch-results-list li {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--black);
  font-weight: 400;
  display: flex;
  gap: 10px;
  align-items: flex-start;
}
.ch-check { color: var(--green); flex-shrink: 0; }
.ch-check svg { width: 18px; height: 18px; display: block; margin-top: 3px; }
.ch-num { color: var(--red); font-weight: 500; flex-shrink: 0; }

/* ── PULL QUOTE (bordered highlight box — design challenge statements
   and pull quotes alike). Optional .ch-pull-quote-label pairs with the
   shared .section-eyebrow component in style.css for type. ─────────── */
.ch-pull-quote {
  border: 1px solid var(--neutral-border);
  border-radius: 10px;
  padding: 48px 72px;
  margin: 200px auto;
  max-width: var(--content-w-md);
  /* position: relative (no z-index) — a containing block for a .bg-blob
     child's top/left%, but its z-index:-1 needs to resolve against
     .page's ONE shared stacking context (style.css), not a context
     scoped to just this box (see .page's own note on why). */
  position: relative;
}
.ch-pull-quote-label {
  margin-bottom: 15px;
}
/* Layout only — pairs with .h2-section for type. color is a legitimate
   override: .h2-section defaults to --black, but a pull quote wants the
   warmer --dark-brown. */
.ch-pull-quote-text {
  color: var(--dark-brown);
  margin: 0;
}

/* ── V2 RESEARCH INPUTS (competitive analysis + interview insights) ──
   Two-column layout, its column widths and 900px stacking breakpoint
   matching .ch-sticky-scroll right below it so the two read as one
   continuous rhythm. Vertical spacing above comes from .ch-pull-quote's
   own 200px bottom margin, and below from .ch-sticky-scroll's own
   100px top margin — no margin of its own needed either side. */
.ch-research-row {
  display: grid;
  grid-template-columns: 340px 1fr;
  gap: 80px;
}
.ch-research-text { min-width: 0; }
.ch-research-grid {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 0;
}
/* Row 1: two images at a shared height, each keeping its own natural
   aspect ratio uncropped. flex-basis: 0 plus flex-grow set to each
   image's own width/height ratio (--ar, declared per .ch-research-item)
   makes the flex algorithm solve for a common height with no JS
   measurement: width_i = row-width * ar_i / Σar, and since the image's
   own intrinsic ratio then sets its height from that width, height_i =
   width_i / ar_i = row-width / Σar for every item — the same value
   regardless of i. */
.ch-research-row-1 {
  display: flex;
  gap: 18px;
}
.ch-research-row-1 .ch-research-item {
  flex: var(--ar) 0 0;
  min-width: 0;
}
/* Eyebrow-to-image spacing matches .ch-ba-label's own 12px elsewhere
   on this page — reused directly rather than a second identical rule. */
.ch-research-item img {
  display: block;
  width: 100%;
  height: auto;
}
@media (max-width: 1100px) {
  .ch-research-row { grid-template-columns: 280px 1fr; gap: 48px; }
}
@media (max-width: 900px) {
  .ch-research-row { grid-template-columns: 1fr; }
}

/* ── STICKY SCROLL (V2 changes) ────────────────────────────────────
   100px top margin (the space above the whole block, per the eyebrow
   spacing request) now lives on .ch-sticky-scroll itself, since the
   eyebrow moved from being a sibling above the grid to being the
   first in-flow child of .ch-sticky-left — see the markup. That move
   is what actually solves the "align with Before" ask: both are now
   the first thing in their column, on the same grid row, so their
   tops coincide without any margin math. */
.ch-sticky-scroll {
  display: grid;
  grid-template-columns: 340px 1fr;
  gap: 80px;
  margin-top: 100px;
  /* position: relative was already needed here for other reasons; kept
     WITHOUT a z-index — a .bg-blob descendant's z-index:-1 needs to
     resolve against .page's ONE shared stacking context (style.css),
     not a context scoped to just this grid (see .page's own note on
     why). */
  position: relative;
}
.ch-sticky-left {
  position: sticky;
  top: calc(var(--nav-h) + 40px);
  height: fit-content;
  align-self: start;
}
/* Positioning context for the absolutely-stacked panels below — kept
   separate from .ch-sticky-left itself (also positioned, for the
   sticky behavior) so the panels anchor to this wrapper's top, not
   .ch-sticky-left's, and don't sit underneath .ch-sticky-lead. */
.ch-sticky-panels {
  position: relative;
  height: fit-content;
}
/* All panels stacked, shown via JS */
.ch-sticky-panel {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  opacity: 0;
  transition: opacity 0.5s ease;
  pointer-events: none;
}
.ch-sticky-panel.is-active {
  opacity: 1;
  position: relative;
  pointer-events: auto;
}
.ch-sticky-title {
  margin-bottom: 16px;
}
/* Type lives in the shared .body-big component in style.css — see
   the .ch-sticky-desc.body-big pairing in the markup. */

.ch-sticky-right { display: flex; flex-direction: column; gap: 80px; }
.ch-sticky-block { scroll-margin-top: calc(var(--nav-h) + 40px); }
/* .ch-v2-shot's own margin: 32px 0 (for its general use elsewhere) would
   otherwise push the first block's image 32px below .ch-sticky-right's
   top, breaking the alignment with .ch-sticky-lead on the left — both
   need to start flush with the grid row. */
.ch-sticky-right > .ch-sticky-block:first-child .ch-v2-shot {
  margin-top: 0;
}

/* ── OUTCOME METRICS ─────────────────────────────────────────────── */
/* Columns/padding come from the shared .stats component now
   (style.css, auto-fit + minmax(200px)) — only this row's own
   placement stays here, plus gap: 0, since .stat's own border-left
   dividers are the separator here — a gap would double it up. */
.ch-outcome-metrics {
  --stat-gap: 0px;
  max-width: var(--content-w-md);
  margin: 40px auto;
}
/* ── RESPONSIVE ──────────────────────────────────────────────────── */
@media (min-width: 1650px) {
  .ch-annotated-scroll { margin: 60px -200px; }
}
@media (max-width: 1100px) {
  .ch-sticky-scroll { grid-template-columns: 280px 1fr; gap: 48px; }
  .ch-annotated-inner { grid-template-columns: 160px 1fr 160px; }
}
/* Hero stacks earlier than the rest of the page's own ≤900px tier
   below — at this image's own wide aspect ratio, the side-by-side
   treatment runs out of room for the text column well before 900px
   does. */
@media (max-width: 1100px) {
  /* Stacked layout: text stays on top (no order flip — the old
     image-first order didn't match the Figma reference, node
     1381:47859's <900/<600/<440 frames all keep the text first) and
     the screenshot moves below it.
     min-height/padding-top match .mind-hero's own stacked-tier
     override (mind.css) in spirit — the 746px cap is non-stacked-only,
     and padding-top keeps the base rule's added var(--nav-h) while the
     rest of it shrinks to a plain 48px. */
  .ch-hero { min-height: auto; padding-top: calc(48px + var(--nav-h)); }
  .ch-hero-top { grid-template-columns: minmax(0, 1fr); gap: 56px; }
  /* min-width: 0 overrides the flex item's default auto min-width —
     below 600px .ch-screen-frame goes back to a wide, non-shrinking
     bleed (see that tier below); without this override here too, it
     would force this whole column wider than the viewport instead of
     letting the frame bleed past its edge on its own. */
  .ch-hero-right { justify-content: flex-start; min-width: 0; }
  /* Down to 600px, the image fills the column's own width instead of
     bleeding past it (below 600px it's back to the desktop hero's own
     "sized by height, bled past the edge" method — the column itself
     is too narrow by then to show the UI at a legible size any other
     way). max-height caps the frame's own box (what .ch-hero-top's
     row height is based on), while .ch-hero's own overflow: hidden
     crops the image itself where it spills past that — no overflow
     needed here, which also means .ch-screen-inner's own glass halo
     (::before, style.css) never gets clipped early on the sides that
     shouldn't be. margin-bottom is a plain, ordinary 56px (matching
     the gap above) — genuine empty space before the hero's own bottom
     edge, not a crop mechanism. */
  .ch-screen-frame {
    width: 100%;
    max-width: none;
    max-height: 458px;
    padding: 8px 8px 0;
    margin-bottom: 56px;
  }
}
@media (max-width: 900px) {
  .ch-meta-row { grid-template-columns: 1fr 1fr; }
  .ch-problem-cards { grid-template-columns: 1fr; }
  .ch-ba-pair { grid-template-columns: 1fr; }
  /* .ch-ba-trio collapses on its own container width, not the viewport. */
  /* Stacked layout alternates panel/image/panel/image instead of running
     all four text panels before any image — .ch-sticky-left/-panels/-right
     are unwrapped with display:contents so each panel and block becomes a
     direct item of .ch-sticky-scroll's single-column grid, then `order`
     (keyed off the matching data-panel/data-block index already used by
     initStickyPanels) interleaves them regardless of DOM order. */
  .ch-sticky-scroll { grid-template-columns: 1fr; }
  .ch-sticky-left,
  .ch-sticky-panels,
  .ch-sticky-right { display: contents; }
  .ch-sticky-lead { order: -1; }
  .ch-sticky-panel { position: relative; opacity: 1 !important; margin-bottom: 16px; }
  .ch-sticky-panel[data-panel="1"] { order: 1; }
  .ch-sticky-block[data-block="1"] { order: 2; }
  .ch-sticky-panel[data-panel="2"] { order: 3; }
  .ch-sticky-block[data-block="2"] { order: 4; }
  .ch-sticky-panel[data-panel="3"] { order: 5; }
  .ch-sticky-block[data-block="3"] { order: 6; }
  .ch-sticky-panel[data-panel="4"] { order: 7; }
  .ch-sticky-block[data-block="4"] { order: 8; }
  /* The desktop flush-with-lead reset no longer applies once the first
     block sits after panel 1 instead of at the very top of the column. */
  .ch-sticky-right > .ch-sticky-block:first-child .ch-v2-shot { margin-top: 32px; }
  .ch-annotated-scroll { margin: 60px -32px; }
  .ch-annotated-inner { grid-template-columns: 160px minmax(600px, 1fr); top: calc(var(--nav-h) + 60px); }
  .ch-annotations-right { grid-column: 1; }
  .ch-annotation-badge, .ch-annotation p { font-size: 10px; }
  .ch-pull-quote { padding: 32px; }
}
@media (max-width: 700px) {
  .ch-results-cards { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  body { overflow-x: clip; }

  /* Back to the desktop hero's own "sized by height, free to bleed
     past the right edge" method (.ch-hero's own overflow: hidden crops
     it) instead of the 1100-600px tier's fill-width-then-crop-the-
     bottom treatment above — at this image's own wide aspect ratio,
     shrinking it to the column's own (now much narrower) width would
     make the UI inside illegible well before phone-mockup-sized text
     would be. */
  .ch-screen-frame {
    width: auto;
    max-width: none;
    height: 370px;
    max-height: none;
    overflow: visible;
    padding: 0;
  }
  .ch-screen-frame .ch-screen-inner { height: 100%; width: fit-content; }
  .ch-screen-frame .ch-screen-inner img {
    width: auto;
    max-width: none;
    height: 100%;
    object-fit: contain;
  }
  /* Re-anchored for this tier's much narrower frame — the wider
     tiers' own top/right values (set inline, case-study-report.html)
     push the blob almost entirely out of view once the frame itself
     is this narrow, same idea as .mind-hero-blob-zone's own left
     override at ≤600px (mind.css). !important is what lets a
     stylesheet rule win over the inline style. */
  .ch-hero-blob-zone .bg-blob { top: -150px !important; right: -300px !important; }

  /* Annotated scroll: no more crop/scrub — frame the full image height,
     single annotation column, image bleeds past the viewport edge. */
  .ch-annotated-scroll { height: auto; margin: 60px 0; }
  .ch-annotated-inner {
    position: static;
    height: auto;
    grid-template-columns: 120px 1fr;
    gap: 0;
    align-items: stretch;
    padding: 0;
  }
  .ch-annotations-left,
  .ch-annotations-right { grid-column: 1; grid-row: 1; height: 100%; }
  .ch-annotation-left,
  .ch-annotation-right { transform: translateY(10px); }
  .ch-annotation { padding: 8px 12px; }
  /* gap is spec-invalid as a negative value, so the overlap is done as a
     negative margin pulling the image column back into the annotations. */
  .ch-screen-sticky-wrap { display: block; height: auto; min-width: 0; margin-left: -20px; }
  .ch-screen-sticky {
    width: 100vw;
    height: auto;
    max-height: none;
    padding: 0;
    background: none;
    backdrop-filter: none;
    box-shadow: 0 12px 24px rgba(0,0,0,0.12);
  }
  .ch-screen-viewport { height: auto; border-radius: 10px; }
  .ch-screen-scroll-content { position: static; transform: none !important; }

  .ch-meta-row { grid-template-columns: 1fr; }
}
@media (max-width: 440px) {
  /* Smaller still at this tier, matching the Figma <440 frame's own
     value. */
  .ch-screen-frame { height: 300px; }
}
