/* ============================================================
   Case-study pages (e.g. ak-bars.html)
   Loaded AFTER styles.css so it reuses the same tokens/fonts but
   overrides the homepage's dark, fixed, overflow:hidden canvas.
   Light theme, vertical scroll, 1440-wide fixed canvas.
   ============================================================ */

:root {
  --cs-bg-top: #e3e0f5;     /* lavender (page top) */
  --cs-bg-bottom: #ffffff;  /* white (page bottom) */
  --cs-ink: #04001f;        /* dark navy — all text on this page */
  --cs-hero: #6b0a43;       /* hero placeholder = homepage frame pink */
  --cs-edge: 70px;          /* left/right content edge on the 1440 canvas */
  --cs-gap: 157px;          /* universal spacing: page→header, header→hero, hero→content */

  /* Vertical rhythm around the top nav (Task 1/3, client-specified):
     page-top → .cs-top-nav → page-top gap again → CLIENT/PROJECT → the
     SAME gap again → hero. Desktop keeps that gap equal to --cs-gap
     (157px, unchanged); the ≤1199px media query overrides THIS token
     only, to a value larger than --cs-gap's own mobile override, so the
     nav/header/hero rhythm can grow independently of every other gap on
     the page that still reads --cs-gap (hero→content, mid photo, etc.).
     Aliased to --cs-gap here so desktop needs no separate declaration. */
  --cs-top-gap: var(--cs-gap);

  /* Big-photo heights, zoned like the homepage: the 1440 design stays compact,
     growing to the 1920 treatment between 1440 and 1920, then scaling with vw.
     --cs-bleed-h: full-viewport-width bands (hero, end photo)
     --cs-big-h:   canvas-width (1440) full-bleed photos
     Both are capped by --cs-vh-cap below so hero + marquee always fit one screen. */
  --cs-bleed-h: min(48.611vw, var(--cs-vh-cap));   /* 700 @1440 */
  --cs-big-h: min(700px, var(--cs-vh-cap));

  /* Screen-fit cap: hero + the marquee band below it must fit one viewport
     (marquee band ≈ 64px incl. its top margin, plus a little slack). */
  --cs-vh-cap: calc(100vh - 80px);
}

/* Undo the homepage's full-viewport lock so the page can scroll.
   The html reset matters too: with html stuck at 100% (1 viewport) the
   propagated body gradient paints at viewport height and visibly bands
   mid-page. Scoped via :has so homepage html/body rules stay untouched. */
html:has(body.case-study) {
  height: auto;
  /* Pin the root font-size: styles.css makes it fluid for the homepage,
     but case-study pages keep a fixed 1440 canvas, so rem must equal
     16px here (large-screen treatment pending user direction). */
  font-size: 16px;
}

body.case-study {
  background: #ffffff; /* all case studies: plain white page (was lavender→white) */
  color: var(--cs-ink);
  height: auto;
  overflow: visible;
  min-width: 1200px;
}

/* Fixed 1440 canvas, centered. Gradient is vertical-only, so the
   centered side-margins read seamlessly against the body background. */
.cs-page {
  position: relative;
  width: 1440px;
  margin: 0 auto;
  padding-top: var(--cs-top-gap);   /* page top → .cs-top-nav = 157px (was --cs-gap directly;
                                        now routed through --cs-top-gap so the mobile/tablet
                                        rhythm pass — Task 3 — can grow this independently of
                                        every other gap still keyed to --cs-gap) */
  padding-bottom: 239px;        /* tail below the bottom nav */
}

/* ---- Language toggle: a <button> that otherwise carries .cs-nav-link
   (or, on the homepage, .footer-link) so it inherits that link's type
   treatment. This only strips native button chrome — placed BEFORE
   .cs-nav-link below so that rule's font-family/font-size win the
   cascade over this rule's `font: inherit` (same specificity, later
   wins); if the order were reversed the toggle would silently lose the
   mono/17px treatment and fall back to the browser's default button font. */
.lang-toggle {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: inherit;  /* a <button> doesn't inherit color by default (UA gives
                      it `buttontext`) — without this it only LOOKS right
                      because buttontext happens to sit close to the
                      case-study ink, not because it's actually inheriting */
}

/* Split-flap character slot (i18n.js) — a fixed slot on a real split-flap
   board: i18n.js sets an inline `width` per span (canvas-measured advance
   width of that character's FINAL glyph, plus letter-spacing), so a
   cycling glyph can never shift its neighbours — needed here since
   case-study body copy (.cs-intro, stat lines, nav labels, …) flaps too.
   inline-block (not the span default of inline) so that inline width
   actually takes effect; text-align: center keeps the glyph centered as
   it cycles through different widths within its own fixed box. Kept
   consistent with styles.css's identical .flap-char rule. */
.flap-char {
  display: inline-block;
  text-align: center;
}

/* ---- Bottom nav link ---- */
.cs-nav-link {
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  transition: opacity var(--dur-out) var(--ease-snap);
}

.cs-nav-link:hover,
.cs-nav-link:focus-visible {
  opacity: 0.55;
}

/* Bottom nav sits in the gradient tail after the last photo:
   "← HOME" left, "NEXT PROJECT: … →" right. */
.cs-bottom-nav {
  display: flex;
  justify-content: space-between;
  padding: 0 var(--cs-edge);
  margin-top: 180px;
}

/* ---- Top nav (client-added, Task 1): "← HOME" + the language toggle,
   first element in .cs-page's flow, above the header. Coexists with
   .cs-bottom-nav below — this does not replace it, both navs are live at
   once. flex-start (NOT space-between, which would spread the two links
   to the header's full 1300px content width) + an explicit 200px gap is
   the client-specified geometry: the two links sit together at the left
   edge, exactly 200px apart, rather than pinned to opposite margins.
   margin-bottom reads the SAME --cs-top-gap token as .cs-page's own
   padding-top (see that rule and the :root comment above it) so the two
   gaps — page-top→nav and nav→header — can never drift apart, and both
   grow together under the Task 3 mobile/tablet rhythm pass below. */
.cs-top-nav {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 200px;
  padding: 0 var(--cs-edge);
  margin-bottom: var(--cs-top-gap);
}

/* ---- Header row: CLIENT | PROJECT (first element; page padding sets the top gap) ---- */
.cs-header {
  display: grid;
  grid-template-columns: 1fr 1fr;
  padding: 0 var(--cs-edge);
}

.cs-label {
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  margin-bottom: 28px;
}

.cs-value {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: 400;
  letter-spacing: var(--tracking-display);
  line-height: normal;
}

/* Optional line under a header value (e.g. what the client actually is). */
.cs-subtitle {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 16px;
  text-transform: uppercase;
  margin-top: 12px;
}

/* ---- Hero: full-width frame (543 → 1243, h700). A <div> renders the
   colored placeholder; an <img> (real case studies) cover-crops. ---- */
.cs-hero {
  display: block;
  width: 100%;
  height: 700px;
  margin-top: var(--cs-gap);   /* header → hero = 157px */
  background: var(--cs-hero);
  object-fit: cover;
  object-position: center;
}

/* ---- Body: intro paragraph + staggered 2-col gallery ---- */
.cs-body {
  display: flex;
  align-items: flex-start;
  gap: 20px;               /* gutter between the 710 and 730 columns */
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap); /* hero → content = 157px */
}

.cs-col-left {
  width: 640px;
}

.cs-col-right {
  width: 637px;
  margin-top: 16px; /* right gallery starts at y≈1588, 16px below the intro */
}

.cs-intro {
  width: 587px;
  font-family: var(--font-mono);
  font-size: 21px;
  line-height: 1.3;
  letter-spacing: 0.3em; /* 6.3px @ 21px */
  text-transform: uppercase;
}

.cs-intro p + p {
  margin-top: 27px; /* one blank line between paragraphs */
}

.cs-photo {
  display: block;
  object-fit: cover;
  object-position: center;
}

/* Left-column photo sits lower than the intro — the stagger.
   Margin measured against the rendered intro height to land at y≈2576. */
.cs-photo--left {
  width: 640px;
  height: 691px;
  margin-top: 459px;
}

/* Right column: two tall photos stacked. */
.cs-photo--right {
  width: 637px;
  height: 967px;
}

.cs-photo--right + .cs-photo--right {
  margin-top: 28px; /* gap between right A and right B */
}

/* ---- Full-bleed photos (1440 × 1024) ---- */
.cs-fullbleed {
  display: block;
  width: 100%;
  height: 1024px;
  object-fit: cover;
  object-position: center;
}

.cs-fullbleed--1 {
  margin-top: 120px; /* top ≈ y3670 */
}

.cs-fullbleed--2 {
  margin-top: 201px; /* top ≈ y5705 */
}

/* ---- Stats: three centered display lines (first baseline ≈ y5207) ---- */
.cs-stats {
  margin-top: 475px;
  text-align: center;
}

.cs-stat {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: 400;
  letter-spacing: var(--tracking-display);
  line-height: 110px;      /* ~110px between baselines */
  /* pre-wrap (not pre): still preserves the double space before AWARD in
     the English copy, but — unlike pre — still allows a normal line break
     when a line doesn't fit, which several of the new Russian stat lines
     need (see i18n pass: e.g. "700 ТЫСЯЧ ВЗАИМОДЕЙСТВИЙ НА ВСЕХ ПЛОЩАДКАХ").
     Plain pre never wraps at all, so a too-long line would silently
     overflow the fixed 1440px canvas instead of dropping to a second
     line — worse than the two-line result the spec calls acceptable. */
  white-space: pre-wrap;
}

/* ---- Mobile-only "BRIEF"/"RESULTS" wayfinding labels (see spec K). Hidden
   here in the base sheet so desktop never shows them regardless of which
   page adds the markup; the mobile media query below re-enables and styles
   them. Keeping the display:none in the base sheet (not inside the media
   query) is what guarantees zero desktop pixels move when this class is
   added to a page — the reveal is additive, scoped entirely to mobile. ---- */
.cs-eyebrow {
  display: none;
}

/* ============================================================
   AK BARS case study — page-specific components (body.cs-akbars):
   full-viewport hero slider, cursor-switching red boxes, hover-swap
   green boxes. Geometry from Figma 32:268 (1440×5856), reproduced
   with document flow + margins (flow wins over pixel-exactness).
   ============================================================ */

/* Hero: breaks out of the 1440 canvas to full viewport width.
   Height rides the zoned --cs-bleed-h token (compact at 1440, growing to
   the 1920 treatment by 1920 — see "Scaling zones" at the end of this file).

   Breakout formula: width: max(100vw, 100%); margin-left: min(0px,
   calc(50% - 50vw)). The old plain `width: 100vw; margin-left: calc(50% -
   50vw)` broke below the fixed 1440 canvas: `50%` resolves against the
   1440px .cs-page, so between 1200–1439px that calc() goes POSITIVE
   (e.g. +71px at 1298px) and pushes the element right instead of
   bleeding it left — a gap the client saw and reported (it also clipped
   the marquee's first word). The max()/min() form is safe at every
   width this site supports: ≥1440 the 100vw arm wins and margin-left
   stays negative (true bleed, unchanged); 1200–1439 margin-left clamps
   to 0 and width falls back to 100% of the canvas, so the element sits
   flush with the canvas edge instead of gapped; ≤1199 the page is
   narrower than the viewport so calc(50% - 50vw) is already negative and
   min() leaves it alone, 100vw still wins. This same pair replaces every
   `100vw` / `calc(50% - 50vw)` breakout in this file and in
   kazan-racecourse.css — not just the desktop rules, since the ≤1199
   media query re-declares width/margin-left on some elements too. */
.ab-hero {
  position: relative;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap); /* header → hero = 157px */
  overflow: hidden;
  background: #000;      /* neutral base while a slide loads */
}

.ab-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 600ms ease;
}

.ab-slide.is-active {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .ab-slide {
    transition: none;
  }
}

/* Body: two 638px columns at x=70 / x=730 (gap 22px).
   Left: intro then drop-2 box. Right: drop-1 then drop-3. */
.ab-body {
  display: flex;
  gap: 22px;
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap); /* hero → content = 157px */
}

.ab-col-left,
.ab-col-right {
  width: 638px;
}

/* Drop-2 sits far below the intro (its top ≈ y1901). Margin measured
   against the rendered intro height — approximate by design. */
.ab-redbox--d2 {
  margin-top: 175px;   /* drop-2 top ≈ y1901 (Figma delta 720 below intro) */
}

/* Drop-3 follows drop-1 with a 20px gap (d1 bottom y2138, d3 top y2158). */
.ab-redbox--d3 {
  margin-top: 20px;
}

/* Cursor-position photo switcher: 638×957, five stacked photos.
   Imagery only — no cursor change, not focusable. */
.ab-redbox {
  position: relative;
  width: 638px;
  height: 957px;
  overflow: hidden;
  background: var(--cs-hero); /* frame pink base until photo 1 paints */
}

.ab-switch {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 90ms linear; /* fast crossfade, compositor-only */
}

.ab-switch.is-shown {
  opacity: 1;
}

/* Green full-bleed boxes: 1440 wide (canvas width, NOT viewport).
   Height rides the zoned --cs-big-h token (compact at 1440, growing to the
   1920 treatment by 1920 — see "Scaling zones" at the end of this file).
   Instant rest/hover photo swap (no transition, like the homepage frame). */
.ab-green {
  position: relative;
  width: 100%;      /* = 1440 on the fixed canvas */
  height: var(--cs-big-h);
  overflow: hidden;
}

.ab-green--1 {
  margin-top: 157px;   /* green #1 top ≈ y3272 */
}

/* Showreel: full-viewport band of three INDEPENDENT rows of 380px square
   tiles. Rows 1 & 3 scroll left → right, the middle row right → left, each at
   a constant 40px/s — driven by a rAF loop (see script) that writes
   transform: translateX(...) directly so the rows are grab-and-drag
   scrollable; not a CSS animation. Two copies of each row's sequence keep
   it seamless. Decorative, not links. */
.ab-showreel {
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  margin-top: 96px;
}

.ab-sr-row {
  overflow: hidden;
  cursor: grab;
  touch-action: pan-y;   /* keep vertical page scroll on touch; JS owns the horizontal axis */
  user-select: none;     /* dragging shouldn't select page text */
}

.ab-sr-row.is-dragging {
  cursor: grabbing;
}

.ab-sr-row + .ab-sr-row {
  margin-top: 28px;
}

.ab-sr-track {
  display: flex;
  width: max-content;
}

.ab-sr-seq {
  display: flex;
  gap: 28px;
  padding-right: 28px;   /* completes the loop junction */
}

.ab-sr-seq img {
  display: block;
  width: 380px;
  height: 380px;
  object-fit: cover;
  object-position: center;
  transition: transform 120ms ease-out;
  /* Belt-and-suspenders alongside the draggable="false" HTML attribute:
     without this a native image drag (complete with ghost image) could
     still pre-empt the pointer sequence the rAF grab-scroller depends on.
     pointer-events stays default (not none) — these are decorative-only
     already, no click/hover behaviour to preserve here beyond the popup. */
  -webkit-user-drag: none;
  user-drag: none;
}

/* Hover popup like the rail words — transform only, lifted above neighbours. */
.ab-sr-seq img:hover {
  transform: scale(1.06);
  position: relative;
  z-index: 2;
}

.ab-swap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.ab-swap--hover {
  opacity: 0;
}

.ab-green.is-hover .ab-swap--rest {
  opacity: 0;
}

.ab-green.is-hover .ab-swap--hover {
  opacity: 1;
}

/* Stats offsets for this page (override the shared .cs-stats spacing). */
.ab-stats {
  margin-top: 129px;   /* first line ≈ y4425 (Figma g1→stats delta 1153) */
}

/* ============================================================
   Scaling zones — big case-study photos (--cs-bleed-h / --cs-big-h)
   Same three-zone pattern as the homepage (see styles.css "Scaling
   zones" and CLAUDE.md): the 1440 design stays compact, interpolates
   up to the 1920 treatment between 1440–1920, then scales with vw
   above 1920. Continuous at both boundaries — no jump either side.
   ============================================================ */

/* Zone B, 1440–1920: interpolate up to the 1920 treatment. Still capped by
   --cs-vh-cap so a short-but-wide window (e.g. 1920×900) can't blow past
   one screen's worth of hero + marquee. */
@media (min-width: 1440px) {
  :root {
    --cs-bleed-h: min(calc(700px + (100vw - 1440px) * 1.3861111), var(--cs-vh-cap));
    --cs-big-h: min(calc(700px + (100vw - 1440px) * 0.675), var(--cs-vh-cap));
  }
}

/* Zone C, ≥1920: the 1920 treatment — bands scale with vw, canvas-width
   photos hold the 1024 height (the canvas itself is pinned at 1440).
   Same --cs-vh-cap ceiling applies. */
@media (min-width: 1920px) {
  :root {
    --cs-bleed-h: min(71.111vw, var(--cs-vh-cap));   /* 1024 @1440-ratio, 1365.33 @1920 */
    --cs-big-h: min(1024px, var(--cs-vh-cap));
  }
}

/* ============================================================
   Sub-1440 shrink band (1200–1439.98px), spec D task 1.
   The 1440 canvas (.cs-page, width:1440, margin:0 auto) overflows
   horizontally below 1440 — the client wants the WHOLE design shrunk to
   fit, not restacked to the tablet layout that starts at 1199px. `zoom`
   (not `transform: scale`) because zoom reflows: it shrinks the page's
   own layout height along with it, so no dead space is left below the
   footer the way a non-reflowing transform would leave.
   `zoom` needs a unitless number; that can't be produced in pure CSS
   (calc(100vw / 1440) yields a length, not a number), so case-study.js
   computes innerWidth / 1440 on load + debounced resize and writes it to
   --cs-zoom on the root. The var()'s fallback of 1 means a browser with
   JS disabled — or older than Firefox 126 / pre-zoom Safari & Chrome —
   just falls back to the old behaviour (a horizontal scrollbar) instead
   of breaking.
   --cs-vh-cap correction: viewport units computed INSIDE a zoomed
   element resolve against the viewport divided by the zoom factor (the
   same mechanism that keeps a 100vw hero exactly full-bleed under zoom),
   so the "100vh" term of --cs-vh-cap self-corrects — it already comes
   out to the real viewport height once zoom is applied to the rendered
   result. The literal "80px" term does NOT get that treatment: it's a
   plain length, so it renders as 80px * zoom (less than 80 real px),
   which under-subtracts and leaves the cap taller than one screen — the
   exact bug --cs-vh-cap exists to prevent. Dividing the constant by the
   same --cs-zoom compensates: it renders as (80px / zoom) * zoom = 80px
   real, regardless of the current zoom factor. */
@media (min-width: 1200px) and (max-width: 1439.98px) {
  .cs-page {
    zoom: var(--cs-zoom, 1);
  }

  :root {
    --cs-vh-cap: calc(100vh - (80px / var(--cs-zoom, 1)));
  }
}

/* ============================================================
   SPORT FOR GOOD case study — page-specific components (body.cs-sfg):
   single static hero, intro + three photo boxes (one cursor switcher,
   two instant hover-swaps), a static mid photo, punched-up stats and a
   static end photo. No slider, no showreel — this page has neither.
   ============================================================ */

/* Hero: single static photo, full viewport width (no slider — only one
   hero photo exists for this project). Height rides --cs-bleed-h. */
.sfg-hero {
  display: block;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);      /* header → hero = 157px */
  object-fit: cover;
  object-position: 50% 45%;
}

/* Horizontal marquee (decorative), directly under the hero — same
   structure as the Kazan marquee (kazan-racecourse.css .kz-marquee), just
   page-scoped sfg- names so both pages' inline scripts stay independent. */
.sfg-marquee {
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  overflow: hidden;
  margin-top: 14px;
  padding: 14px 0;               /* band ≈ 46px tall */
}

.sfg-marquee-track {
  display: flex;
  width: max-content;
  color: var(--color-accent);    /* entire marquee — words AND dots — accent */
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  white-space: nowrap;
  animation: sfg-scroll var(--sfg-marquee-dur, 60s) linear infinite;
}

@keyframes sfg-scroll {
  to { transform: translateX(-50%); }
}

.sfg-marquee-seq {
  display: flex;
  align-items: center;
  gap: var(--rail-gap);
  padding-right: var(--rail-gap); /* completes the loop junction */
}

.sfg-word {
  display: inline-block;
  transition: transform 120ms ease-out;
}

.sfg-word:hover { transform: scale(1.06); }

.sfg-dot {
  flex: none;
  width: var(--rail-dot);
  height: var(--rail-dot);
  border-radius: 50%;
  background: currentColor;
}

@media (prefers-reduced-motion: reduce) {
  .sfg-marquee-track { animation: none; }
}

/* Body: two 638px columns at x=70 / x=730 (gap 22px), same DNA as .ab-body.
   Left column is a flex column so the bottom-left box can pin its bottom
   to the right column's bottom via margin-top: auto (see .sfg-box--bl). */
.sfg-body {
  display: flex;
  align-items: stretch;
  gap: 22px;
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap); /* hero → content = 157px */
}

.sfg-col-left {
  width: 638px;
  display: flex;
  flex-direction: column;
}

.sfg-col-right {
  width: 638px;
}

/* Bottom-right box follows the top-right box with a 20px gap. */
.sfg-box--br {
  margin-top: 20px;
}

/* Photo boxes: relative-positioned stacks, imagery only — no cursor
   change, not focusable. Frame pink base until the photo(s) paint. */
.sfg-box {
  position: relative;
  width: 638px;
  overflow: hidden;
  background: var(--cs-hero);
}

.sfg-box--tr { height: 957px; }           /* cursor switcher, portrait */
.sfg-box--bl,
.sfg-box--br { height: 425px; }           /* instant hover-swap, 3:2 landscape */
.sfg-box--bl { margin-top: auto; }        /* bottoms align with the right column */

.sfg-switch,
.sfg-swap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.sfg-switch {
  opacity: 0;
  transition: opacity 90ms linear;   /* fast crossfade, compositor-only */
}

.sfg-switch.is-shown {
  opacity: 1;
}

.sfg-swap--hover {
  opacity: 0;
}

.sfg-box.is-hover .sfg-swap--rest {
  opacity: 0;
}

.sfg-box.is-hover .sfg-swap--hover {
  opacity: 1;
}

/* Mid photo: canvas width (1440), replaces AK BARS' green box slot.
   Single static photo, no hover. Height rides --cs-big-h. The source
   (2880×1700, 1.694:1) is wider than the 1440-band aspect at either zone
   height, so the crop is biased up/right to keep both runners' heads
   in frame instead of centering into their shoulders. */
.sfg-mid {
  display: block;
  width: 100%;                   /* = 1440 on the fixed canvas */
  height: var(--cs-big-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: 55% 28%;
}

/* Stats offsets for this page (override the shared .cs-stats spacing). */
.sfg-stats {
  margin-top: var(--cs-gap);
}

/* End photo: full-viewport band replacing AK BARS' showreel — this
   project has no showreel, just another big picture. */
.sfg-end {
  display: block;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: 50% 45%;
}

/* ============================================================
   KHL MEDIA case study — page-specific components (body.cs-khl):
   real hero + green box (client-supplied), marquee, intro + three
   story-proportion cursor switchers (638×1134, ALL photos per folder —
   not just 5), punched-up stats, and the "matrix": a single full-viewport
   row of story-tiles standing in for the showreel slot.
   ============================================================ */

/* Hero: real photo (client-supplied). Same shape as .ab-hero/.sfg-hero —
   full-viewport width, rides --cs-bleed-h. */
.khl-hero {
  display: block;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: center;
}

/* Horizontal marquee (decorative) — identical structure/routine to the
   Kazan and Sport for Good marquees, khl- class names. */
.khl-marquee {
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  overflow: hidden;
  margin-top: 14px;
  padding: 14px 0;               /* band ≈ 46px tall */
}

.khl-marquee-track {
  display: flex;
  width: max-content;
  color: var(--color-accent);    /* entire marquee — words AND dots — accent */
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  white-space: nowrap;
  animation: khl-scroll var(--khl-marquee-dur, 60s) linear infinite;
}

@keyframes khl-scroll {
  to { transform: translateX(-50%); }
}

.khl-marquee-seq {
  display: flex;
  align-items: center;
  gap: var(--rail-gap);
  padding-right: var(--rail-gap); /* completes the loop junction */
}

.khl-word {
  display: inline-block;
  transition: transform 120ms ease-out;
}

.khl-word:hover { transform: scale(1.06); }

.khl-dot {
  flex: none;
  width: var(--rail-dot);
  height: var(--rail-dot);
  border-radius: 50%;
  background: currentColor;
}

@media (prefers-reduced-motion: reduce) {
  .khl-marquee-track { animation: none; }
}

/* Body: two 638px columns, same DNA as .sfg-body/.ab-body. Left column is
   a flex column so the bottom-left box can pin to the right column's
   bottom via margin-top: auto. */
.khl-body {
  display: flex;
  align-items: stretch;
  gap: 22px;
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap);
}

.khl-col-left {
  width: 638px;
  display: flex;
  flex-direction: column;
}

.khl-col-right {
  width: 638px;
}

/* Bottom-right box follows the top-right box with a 20px gap. */
.khl-box--br {
  margin-top: 20px;
}

.khl-box--bl {
  margin-top: auto;   /* bottom aligns with the right column */
}

/* Story-proportion cursor switcher: 638×1134 (638 / 0.5625), ALL photos
   in the folder (not capped at 5 like AK BARS) — N equal columns pick
   1 of N, mouseleave → photo 1. Imagery only, not focusable. */
.khl-box {
  position: relative;
  width: 638px;
  height: 1134px;
  overflow: hidden;
  background: var(--cs-hero); /* frame pink base until photo 1 paints */
}

.khl-switch {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 90ms linear; /* fast crossfade, compositor-only */
}

.khl-switch.is-shown {
  opacity: 1;
}

/* Green full-bleed box: real photo (client-supplied), canvas width. */
.khl-green {
  display: block;
  width: 100%;      /* = 1440 on the fixed canvas */
  height: var(--cs-big-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: center;
}

/* Stats offsets for this page (override the shared .cs-stats spacing). */
.khl-stats {
  margin-top: var(--cs-gap);
}

/* ---- The matrix: one full-viewport row of story-tiles (replaces the
   AK BARS showreel slot). Geometry is driven off viewport height so the
   row always fits the screen: tile height 87vh (~13% less than the
   screen), width holds the 1080/1920 story ratio, gap is 10% of the tile
   width. Scrolls right → left at the house 40px/s — driven by a rAF loop
   (see script) that writes transform: translateX(...) directly so the row
   is grab-and-drag scrollable; not a CSS animation. Same padded two-copy
   loop as the marquees above. ---- */
.khl-matrix {
  --khl-tile-h: 87vh;
  --khl-tile-w: calc(var(--khl-tile-h) * 0.5625); /* 1080/1920 story ratio */
  --khl-tile-gap: calc(var(--khl-tile-w) * 0.1);  /* 10% of the tile width */
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  overflow: hidden;
  margin-top: var(--cs-gap);
  cursor: grab;
  touch-action: pan-y;   /* keep vertical page scroll on touch; JS owns the horizontal axis */
  user-select: none;     /* dragging shouldn't select page text */
}

.khl-matrix.is-dragging {
  cursor: grabbing;
}

.khl-matrix-track {
  display: flex;
  width: max-content;
}

.khl-matrix-seq {
  display: flex;
  gap: var(--khl-tile-gap);
  padding-right: var(--khl-tile-gap); /* completes the loop junction */
}

.khl-tile {
  position: relative;
  width: var(--khl-tile-w);
  height: var(--khl-tile-h);
  overflow: hidden;
  flex: 0 0 auto;
  cursor: default;
  transition: transform 120ms ease-out;
}

/* Hover pop — raised above neighbours, like the rail words/showreel tiles. */
.khl-tile.is-hover {
  transform: scale(1.04);
  z-index: 2;
}

.khl-mx-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 90ms linear; /* fast crossfade, compositor-only */
  /* Belt-and-suspenders alongside the draggable="false" HTML attribute:
     without this a native image drag (complete with ghost image) could
     still pre-empt the pointer sequence the rAF grab-scroller depends on.
     pointer-events stays default (not none) — the tiles need it for the
     hover pop and the cursor-x photo scrub. */
  -webkit-user-drag: none;
  user-drag: none;
}

.khl-mx-photo.is-shown {
  opacity: 1;
}

/* No reduced-motion CSS override here: the row's auto-scroll is now a JS
   rAF loop, so prefers-reduced-motion is handled in script by zeroing the
   ambient speed — drag and momentum still work under reduced motion, since
   a deliberate user drag isn't the kind of motion that flag is about. */

/* ============================================================
   GGM MAG case study — page-specific components (body.cs-ggm):
   single static hero, marquee, intro + three STATIC photo boxes (one
   photo each — no cursor switcher, no hover swap, only one mockup exists
   per slot), a static mid photo, punched-up stats, and a client
   testimonial quote closing the page. No end photo, no showreel/matrix —
   this project has neither (deliberate).
   ============================================================ */

/* Hero: single static photo, full viewport width. Height rides
   --cs-bleed-h, same shape as .sfg-hero/.ab-hero. */
.ggm-hero {
  display: block;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);      /* header → hero = 157px */
  object-fit: cover;
  object-position: center;
}

/* Horizontal marquee (decorative) — identical structure/routine to the
   Kazan/Sport for Good/KHL marquees, ggm- class names so this page's
   inline script stays independent. */
.ggm-marquee {
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  overflow: hidden;
  margin-top: 14px;
  padding: 14px 0;               /* band ≈ 46px tall */
}

.ggm-marquee-track {
  display: flex;
  width: max-content;
  color: var(--color-accent);    /* entire marquee — words AND dots — accent */
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  white-space: nowrap;
  animation: ggm-scroll var(--ggm-marquee-dur, 60s) linear infinite;
}

@keyframes ggm-scroll {
  to { transform: translateX(-50%); }
}

.ggm-marquee-seq {
  display: flex;
  align-items: center;
  gap: var(--rail-gap);
  padding-right: var(--rail-gap); /* completes the loop junction */
}

.ggm-word {
  display: inline-block;
  transition: transform 120ms ease-out;
}

.ggm-word:hover { transform: scale(1.06); }

.ggm-dot {
  flex: none;
  width: var(--rail-dot);
  height: var(--rail-dot);
  border-radius: 50%;
  background: currentColor;
}

@media (prefers-reduced-motion: reduce) {
  .ggm-marquee-track { animation: none; }
}

/* Body: two 638px columns at x=70 / x=730 (gap 22px), same DNA as
   .sfg-body/.ab-body. Plain row — no stretch/flex-column machinery here:
   that existed only to pin the now-deleted bottom-left box to the right
   column's bottom, and with just one box left (top-right) there's nothing
   left to pin. */
.ggm-body {
  display: flex;
  gap: 22px;
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap); /* hero → content = 157px */
}

.ggm-col-left {
  width: 638px;
}

.ggm-col-right {
  width: 638px;
}

/* Photo box: STATIC — a single photo (no cursor switcher, no hover swap).
   Frame pink base shows through until the photo paints. The two 638×425
   boxes that used to sit alongside this one (bl/br) are gone per client
   request — that artwork now closes the page as .ggm-end instead. */
.ggm-box {
  position: relative;
  width: 638px;
  overflow: hidden;
  background: var(--cs-hero);
}

.ggm-box--tr { height: 957px; }           /* portrait */

.ggm-box img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Mid photo: canvas width (1440), static, no hover. Height rides
   --cs-big-h, same shape as .sfg-mid. */
.ggm-mid {
  display: block;
  width: 100%;                   /* = 1440 on the fixed canvas */
  height: var(--cs-big-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: center;
}

/* Client testimonial — closes the page (no end photo/showreel on this
   project; the four-line stats block was removed per client request —
   the mid photo → testimonial gap is now carried entirely by this rule's
   own margin-top, still --cs-gap = 157px). Same Fragment Mono treatment
   as .cs-intro, centered in a 915px band on the 1440 canvas — matches
   .kz-intro's actual max-width (kazan-racecourse.css:105); the 933px
   figure in CLAUDE.md is Kazan's min-height, not a width. */
.ggm-quote {
  width: 915px;
  margin: var(--cs-gap) auto 0;
  font-family: var(--font-mono);
  font-size: 21px;
  line-height: 1.3;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  text-align: center;
}

.ggm-quote p + p {
  margin-top: 27px; /* one blank line between paragraphs, matches .cs-intro */
}

.ggm-quote cite {
  display: block;
  margin-top: 24px;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 16px;
  text-transform: none;
}

/* End photo: full-viewport band, closes the page (replaces the deleted
   bottom-left/bottom-right boxes' artwork). Same shape as .sfg-end. */
.ggm-end {
  display: block;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);
  object-fit: cover;
  object-position: center;
}

/* ============================================================
   KHL ANNUAL REPORT case study — page-specific components
   (body.cs-khl-annual). Cut down (client, this round) to only what
   actually exists: the diagonal barricade-tape "under construction" hero
   (CSS + markup, no image, THREE rows — see .kar-tape below), the
   description + one real photo (.kar-box--tr), and a SECOND copy of the
   same hero graphic closing the page in place of the earlier
   placeholder bands and stats block. Everything that had no
   photography behind it — boxes B/C, both full-width bands, the
   three-line stats section — is deleted, not hidden; see CLAUDE.md
   "Case-study pages" for the client's own framing ("text as is, hero as
   is, description and 1 photo on the right... duplicate hero... that's
   all"). No marquee, no showreel/matrix.
   ============================================================ */

/* Hero: full-viewport width, navy band, height rides --cs-bleed-h like
   every other hero. Three rotated white tape strips cross — see .kar-tape
   below. overflow: hidden clips all three strips to the band. */
.kar-hero {
  position: relative;
  width: max(100vw, 100%);
  margin-left: min(0px, calc(50% - 50vw));
  height: var(--cs-bleed-h);
  margin-top: var(--cs-gap);   /* header → hero = 157px */
  background: var(--color-bg-top);
  overflow: hidden;
}

/* Each tape: an OPAQUE white strip, twice the viewport width (200%,
   offset -50% left) so both ends run off-screen at any viewport width,
   rotated so the three strips cross each other. An explicit z-index (not
   implicit DOM order) makes which strip physically occludes which at
   every crossing deterministic — tape--1 sits topmost per the Figma
   frame, with a subtle drop shadow so the crossings read as physical
   strips instead of the lower tapes' text bleeding through into a smear.
   Figma frames 115:41/115:49/115:56 draw the three tapes at different
   vertical offsets AND different text sizes (not just different angles);
   tape--3's own top/rotation/font-size below are set to reproduce that
   variety — see the case-study.css comment history / build report for
   the exact values, since the frame wasn't pixel-measured for this pass. */
.kar-tape {
  position: absolute;
  top: 50%;
  left: -50%;
  width: 200%;
  height: 64px;
  background: #fff;
  overflow: hidden;
}

.kar-tape--1 {
  transform: translateY(-50%) rotate(18deg);
  z-index: 3;
  box-shadow: 0 2px 10px rgba(4, 0, 31, 0.35);
}

.kar-tape--2 {
  transform: translateY(-50%) rotate(-18deg);
  z-index: 2;
}

/* Third row: a different vertical offset (32% instead of the other two's
   centered 50%) and a shallower, opposite-leaning angle so it reads as a
   distinct third strip rather than a copy of tape--2 — still wide/long
   enough at 200vw that it crosses both other tapes somewhere in the band
   regardless of the offset. Sits behind both (z-index: 1). */
.kar-tape--3 {
  top: 32%;
  transform: translateY(-50%) rotate(-9deg);
  z-index: 1;
}

/* Flex row of repeated phrases (30 copies, generous — overflow: hidden on
   both this and the strip above clips whatever doesn't fit, so erring
   long is free and covers even very wide viewports; case-study pages have
   no upper width cap). Markup repetition, not background-image, per spec. */
.kar-tape-track {
  display: flex;
  align-items: center;
  height: 100%;
  width: max-content;
}

.kar-tape-track span {
  flex: none;
  padding: 0 28px;
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--cs-ink);          /* dark ink on the white strip */
  white-space: nowrap;
}

/* Third tape's text runs smaller than the other two (13px vs the house
   17px mono) — the "different text sizes" the Figma frame draws. */
.kar-tape--3 .kar-tape-track span {
  font-size: 13px;
}

/* Body: two 638px columns, plain flex — same DNA as .ab-body. Just the
   intro (left) and one real photo (right) now; the extra box margin-top
   offsets and full-width placeholder bands this comment used to
   describe are gone along with the markup they positioned. */
.kar-body {
  display: flex;
  gap: 22px;
  padding: 0 var(--cs-edge);
  margin-top: var(--cs-gap); /* hero → content = 157px; box A and the intro
    sit at the same y in the Figma frame, so no extra offset is needed
    beyond the universal hero→content gap every other page also uses. */
}

.kar-col-left,
.kar-col-right {
  width: 638px;
}

/* The one real photo on this page. STATIC — no cursor switcher, no
   hover swap. */
.kar-box {
  position: relative;
  width: 638px;
  overflow: hidden;
  background: var(--cs-hero);
}

.kar-box--tr {
  height: 957px;
}

.kar-box img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* ============================================================
   Mobile + tablet case study (≤1199px) — sample build: Sport for Good
   (spec K), extended to ak-bars/khl/ggm-mag/khl-annual-report (spec N)
   and up to 1199px (spec P, task P2). Same authoring convention as the
   homepage's mobile breakpoint (see CLAUDE.md "Mobile homepage"): a
   402×874 reference, root font-size clamped
   (clamp(12px, 3.9801vw, 20px)) so lengths authored in rem ride it.
   Every rule below lives inside this query — desktop (≥1200px) is
   untouched (verified: nothing outside this block was edited for
   spec P).

   Upper bound was 767px through spec N; spec P raised it to 1199px to
   close the 768–1199px sideways-scroll gap (fixed 1440px canvas, no rule
   below 1200px otherwise). Safe to just extend upward: everything in
   this block is rem-against-the-clamped-root or 100vw, and the clamp is
   already pinned at its 20px ceiling well before 1199px, so the layout
   simply renders as a wider single-column (phone) layout rather than
   breaking — see the separate 768–1199px-only block further down for
   the two things that specifically need adjusting at the wider end of
   that range (photo heights, reading measure).

   Shared rules (used by every page) are scoped under body.case-study;
   each page's own layout/photo rules are scoped under its own body
   class (body.cs-sfg, body.cs-ggm, body.cs-khl, body.cs-akbars,
   body.cs-khl-annual). Kazan Racecourse's mobile rules live in its own
   sheet, kazan-racecourse.css, inside the identical
   @media (max-width: 1199px) query — it already has its own file
   because its desktop layout does too.
   ============================================================ */
@media (max-width: 1199px) {

  /* ---- 1. Root and canvas ---- */
  html:has(body.case-study) {
    font-size: clamp(12px, 3.9801vw, 20px); /* 16px at the 402 reference width */
  }

  body.case-study {
    /* The desktop 1200px floor would force horizontal scroll at 402px —
       same fix/reasoning as styles.css's own mobile override for the
       homepage body (see its "desktop's 1200px floor..." comment). Not
       named in spec K's text but required for acceptance criterion 1
       (scrollWidth === 402); flagging it as my own catch in the report. */
    min-width: 0;
  }

  body.case-study .cs-page {
    width: 100%;
    max-width: none;
    margin: 0;
    /* Side gutter matches the homepage mobile image box's inset (20px
       @402). Individual sections below drop their desktop --cs-edge
       padding in favour of this single page-level gutter. */
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }

  /* ---- 1b. Top nav — same gutter-instead-of-edge swap as every other
     row below (.cs-header, .cs-bottom-nav). The 200px link gap itself
     (desktop AND mobile — the client's spec gives one flat number, no
     per-breakpoint variant) is unchanged; only the outer padding moves
     from --cs-edge to .cs-page's own gutter. --cs-top-gap is not
     re-declared here — its mobile value comes from the :root override in
     "10. Vertical rhythm" below, and .cs-top-nav's margin-bottom already
     reads that same token, so it grows automatically. */
  body.case-study .cs-top-nav {
    padding: 0;
  }

  /* ---- 2. Header — stack the two columns ---- */
  body.case-study .cs-header {
    grid-template-columns: 1fr;
    row-gap: 2.625rem; /* was 1.75rem — client: double the CLIENT→PROJECT
      gap on tablet, ×1.5 on phone (Task 3). This value (42px at the
      16px-at-402 reference) is the phone number; the tablet band
      overrides it again below since 42→70px isn't a clean multiple of
      the same rem value once the root pins at 20px past ~503px. */
    padding: 0; /* was 0 var(--cs-edge) — gutter now comes from .cs-page */
  }

  body.case-study .cs-label {
    font-size: 0.6875rem;
  }

  body.case-study .cs-value {
    font-size: 1.75rem;
    line-height: 1.05;
  }

  /* ---- 5. Body copy — the readability crux (shared — .cs-intro is a
     shared class defined in the shared part of the sheet, used by every
     page, not a Sport for Good-only one). Desktop's 21px/uppercase/
     +0.3em/1.3 measures ~14 characters per line at 402px; 0.3em tracking
     on a 362px column is the actual break (see report for the
     characters-per-line calculation at the new size).
     Was mis-scoped to body.cs-sfg when Sport for Good was the only
     mobile page — every other in-scope page fell through to the desktop
     rule and kept its fixed 587px column width (607px right edge on a
     402px viewport, blowing scrollWidth past the viewport on ggm-mag,
     khl, ak-bars and khl-annual-report). Promoted to body.case-study. ---- */
  body.case-study .cs-intro {
    width: 100%;   /* stop being a column beside the photos */
    font-size: 1rem;
    letter-spacing: 0.06em;
    line-height: 1.65;
  }

  body.case-study .cs-intro p + p {
    margin-top: 1.25rem;
  }

  /* ---- 6. BRIEF/RESULTS eyebrows (base-sheet display:none reveals here) ---- */
  body.case-study .cs-eyebrow {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: var(--tracking-mono);
    text-transform: uppercase;
    opacity: 0.5;             /* "ink at opacity: .5" — color is the
                                  inherited --cs-ink, dimmed, not restated */
    margin-bottom: 0.75rem;
  }

  /* ---- 8. Results block (shared — every page's .cs-stats/.cs-stat) ---- */
  body.case-study .cs-stats {
    text-align: left;
  }

  body.case-study .cs-stat {
    font-size: 1.375rem;
    line-height: 1.15;
    margin-bottom: 1.5rem;
    white-space: normal; /* desktop's pre-wrap forces the "3 BEST LICENSING
      LEAGUE  AWARD" double-space to render literally, which reads as a
      layout bug at 22px on a narrow column; normal collapses it back to a
      single space and wraps freely — wrapping is fine/expected here, this
      just avoids the specific double-space artifact, it does not add
      nowrap or fight wrapping. */
  }

  /* ---- 9. Bottom nav — 3-item space-between doesn't fit at 402px ---- */
  body.case-study .cs-bottom-nav {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    row-gap: 1rem;
    padding: 0; /* was 0 var(--cs-edge) — gutter now comes from .cs-page */
  }

  body.case-study .cs-nav-link {
    font-size: 0.6875rem;
  }

  /* ---- 10. Vertical rhythm ---- */
  :root {
    /* Every section margin on every real page already reads var(--cs-gap)
       (verified by grep) EXCEPT .ab-green--1 in the AK BARS section, which
       hardcodes margin-top: 157px instead of the token — out of scope for
       this pass (AK BARS isn't part of spec K), but flagged here since
       it's exactly the kind of thing this comment was written to catch,
       and it'll need its own override whenever AK BARS gets this treatment. */
    --cs-gap: 4rem;

    /* Task 3 (client): more air around the stacked header specifically —
       page-top→nav, nav→header and header→hero should grow MORE than the
       rest of the page's --cs-gap-keyed spacing (×1.5 here on phone, ×2
       on tablet below). 6rem = 96px at the 16px-at-402 reference, i.e.
       the phone number from the client's table; every element that reads
       --cs-top-gap (.cs-page's padding-top, .cs-top-nav's margin-bottom,
       and each page's hero margin-top, overridden per-page below) picks
       this up automatically without being re-declared per breakpoint. */
    --cs-top-gap: 6rem;
  }

  /* ============================================================
     Sport for Good — page-specific mobile layout (body.cs-sfg)
     ============================================================ */

  /* ---- 3. Hero — the one dominant photo ---- */
  body.cs-sfg .sfg-hero {
    height: 56vh; /* was var(--cs-bleed-h) */
    margin-top: var(--cs-top-gap); /* Task 3: was var(--cs-gap) via the desktop
      rule — text→hero now grows with the header rhythm, not the page's
      other --cs-gap-keyed spacing */
  }

  /* ---- 4. Marquee — keep it, just smaller. Structure/JS untouched; the
     existing pad/clone/measure routine already handles a 402px viewport
     (it measures window.innerWidth at runtime, not a fixed breakpoint). ---- */
  body.cs-sfg .sfg-marquee-track {
    font-size: 0.71875rem; /* 11.5px — tracking/color/speed all inherited unchanged */
  }

  /* ---- 5. Body copy: .cs-intro is a shared class — its mobile treatment
     now lives in the body.case-study group above, not here. ---- */

  /* ---- 7. Photos — one per screen, stacked ---- */
  body.cs-sfg .sfg-body {
    display: flex;
    flex-direction: column;
    gap: 0; /* was 22px (a hardcoded column gutter, not a token) — spacing
               between stacked elements is now explicit margins below */
    padding: 0; /* was 0 var(--cs-edge) — gutter now comes from .cs-page */
  }

  body.cs-sfg .sfg-col-left,
  body.cs-sfg .sfg-col-right {
    width: 100%; /* was 638px */
  }

  body.cs-sfg .sfg-col-right {
    margin-top: 2.5rem; /* gap from col-left's last item once stacked —
                            desktop has no such rule since the columns sit
                            side by side, not one after another */
  }

  /* Every SURVIVING photo box: full-bleed width, uniform 62vh height, 40px
     apart. Overrides the desktop per-box heights (957/425px) and bl's
     margin-top: auto pin-to-bottom trick (meaningless in a single stacked
     column — replaced with the same explicit 2.5rem every box uses).
     --br is NOT in this list — see below, it's hidden outright (spec N
     correction: hero+3 boxes+end was 5 photos and 3 boxes in a row; the
     client rule is never more than 2 consecutive, so --br goes). */
  body.cs-sfg .sfg-box--tr,
  body.cs-sfg .sfg-box--bl {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh;
  }

  body.cs-sfg .sfg-box--bl {
    margin-top: 2.5rem; /* was margin-top: auto */
  }

  /* Spec N correction: the client marked this box for removal on mobile
     specifically (it stays on desktop) — a third consecutive photo box
     after --tr/--bl, on top of the hero and .sfg-end, put the page over
     the 3–4 photo budget and broke the "never 2+ in a row" rule. Hidden,
     not deleted — .sfg-col-right's own margin-top (below) still creates
     the gap before --tr regardless of --br's display value. */
  body.cs-sfg .sfg-box--br {
    display: none;
  }

  /* Interactive photo sets collapse to their rest photo — there's no
     pointer on mobile. The JS's mousemove/mouseenter handlers simply never
     fire on touch (inert, not edited), so this is pure CSS: keep only the
     switcher's .is-shown photo (already the first photo in the source
     markup, independent of JS ever running) and the swap boxes' rest
     layer (already opacity:1 by default — .sfg-swap--hover is what's
     hidden). display:none (not opacity) so nothing decorative competes
     for paint. */
  body.cs-sfg .sfg-switch:not(.is-shown) {
    display: none;
  }

  body.cs-sfg .sfg-swap--hover {
    display: none;
  }

  /* Spec N correction: the client marked the green-shirt race-number mid
     photo for removal on mobile specifically — it stays on desktop
     (.sfg-mid keeps its own desktop rule, canvas-width, untouched; this
     is purely an additive mobile override). Simple display:none — no
     viewport-bleed treatment needed for a hidden element. */
  body.cs-sfg .sfg-mid {
    display: none;
  }

  /* .sfg-end already bleeds full viewport width on desktop (width:
     max(100vw, 100%); margin-left: min(0px, calc(50% - 50vw)), same as
     .sfg-hero/.sfg-box) — only the height needs a mobile override. */
  body.cs-sfg .sfg-end {
    height: 62vh; /* was var(--cs-bleed-h) */
  }

  /* ============================================================
     GGM MAG — page-specific mobile layout (body.cs-ggm). Running order:
     hero → BRIEF + intro → .ggm-box--tr → .ggm-mid → quote → .ggm-end.
     4 photos, never 2+ in a row (intro then the quote each break up a
     pair). No RESULTS eyebrow on this page — there's no stats block to
     introduce (removed this round); nothing else takes its place.
     ============================================================ */

  body.cs-ggm .ggm-hero {
    height: 56vh; /* was var(--cs-bleed-h) */
    margin-top: var(--cs-top-gap); /* Task 3: was var(--cs-gap) via the desktop rule */
  }

  body.cs-ggm .ggm-marquee-track {
    font-size: 0.71875rem;
  }

  body.cs-ggm .ggm-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
  }

  body.cs-ggm .ggm-col-left,
  body.cs-ggm .ggm-col-right {
    width: 100%;
  }

  body.cs-ggm .ggm-col-right {
    margin-top: 2.5rem;
  }

  body.cs-ggm .ggm-box--tr {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh;
  }

  /* Same trap as .sfg-mid last round: .ggm-mid is canvas-width (width:
     100%) on desktop, so left uncontested it resolves against the 362px
     mobile content box instead of the viewport. */
  body.cs-ggm .ggm-mid {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh; /* was var(--cs-big-h) */
  }

  /* .ggm-quote has a WORSE version of the same trap: a hardcoded 915px
     width (not even a percentage), which would overflow the 402px
     viewport outright. Not named in spec N's text, but required for
     acceptance criterion 1 (scrollWidth === 402) — fixed here alongside
     the same readability treatment .cs-intro got in spec K, since the
     quote is a comparable-length paragraph block and leaving it at
     21px/uppercase/0.3em would reproduce the exact illegibility that
     pass fixed for .cs-intro. */
  body.cs-ggm .ggm-quote {
    width: 100%;
    font-size: 1rem;
    letter-spacing: 0.06em;
    line-height: 1.65;
  }

  body.cs-ggm .ggm-quote p + p {
    margin-top: 1.25rem;
  }

  /* .ggm-end already bleeds full viewport width on desktop — only the
     height needs a mobile override. */
  body.cs-ggm .ggm-end {
    height: 62vh; /* was var(--cs-bleed-h) */
  }

  /* ============================================================
     KHL MEDIA — page-specific mobile layout (body.cs-khl). Running
     order: hero → BRIEF + intro → --bl (switcher 1) → --tr (switcher 2)
     → RESULTS + stats → matrix. 4 photos, never 2+ in a row.
     ============================================================ */

  body.cs-khl .khl-hero {
    height: 56vh; /* was var(--cs-bleed-h) */
    margin-top: var(--cs-top-gap); /* Task 3: was var(--cs-gap) via the desktop rule */
  }

  body.cs-khl .khl-marquee-track {
    font-size: 0.71875rem;
  }

  body.cs-khl .khl-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
  }

  body.cs-khl .khl-col-left,
  body.cs-khl .khl-col-right {
    width: 100%;
  }

  body.cs-khl .khl-col-right {
    margin-top: 2.5rem;
  }

  /* Keep switcher 1 (--bl, first in stacking order after the intro) and
     switcher 2 (--tr) full-bleed at 62vh, overriding the fixed 638×1134
     story-ratio desktop box. --br is hidden below (spec doesn't name
     which two switchers to keep — these two need no reordering, since
     natural DOM/stacking order already puts them first). */
  body.cs-khl .khl-box--bl,
  body.cs-khl .khl-box--tr {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh;
  }

  body.cs-khl .khl-box--bl {
    margin-top: 2.5rem; /* was margin-top: auto — the pin-to-bottom trick
      is meaningless once the two surviving boxes are simply stacked */
  }

  /* Third consecutive photo box, over the 2-in-a-row budget. */
  body.cs-khl .khl-box--br {
    display: none;
  }

  body.cs-khl .khl-green {
    display: none;
  }

  /* Switchers collapse to their rest photo — no pointer on mobile, same
     treatment as Sport for Good's .sfg-switch. */
  body.cs-khl .khl-switch:not(.is-shown) {
    display: none;
  }

  /* The matrix survives — drag-scroll works on touch, and it's the
     page's signature element. Scale the tile-size token trio off a
     smaller --khl-tile-h so a tile plus part of its neighbour is
     visible: at 46vh, --khl-tile-w = 46vh × 0.5625 ≈ 25.875vh and
     --khl-tile-gap = that × 0.1 ≈ 2.5875vh, preserving the 1080/1920
     story ratio exactly as the desktop 87vh/48.9vh/4.89vh trio does.
     Restating all three (not just --khl-tile-h) is technically
     redundant — --khl-tile-w/--khl-tile-gap already derive from
     --khl-tile-h via var(), so CSS custom-property "live" substitution
     means overriding --khl-tile-h alone would recompute both — but
     spelling out the formula here keeps it self-documenting rather than
     relying on that behaviour implicitly. */
  body.cs-khl .khl-matrix {
    --khl-tile-h: 46vh;
    --khl-tile-w: calc(var(--khl-tile-h) * 0.5625);
    --khl-tile-gap: calc(var(--khl-tile-w) * 0.1);
  }

  /* ============================================================
     AK BARS — page-specific mobile layout (body.cs-akbars). Running
     order: hero → BRIEF + intro → drop-2 (switcher 1) → drop-1
     (switcher 2) → RESULTS + stats → showreel row 1. 4 photos (the
     showreel counts as one), never 2+ in a row.
     ============================================================ */

  /* The 6-slide auto-slider keeps running unmodified — automatic, needs
     no pointer, one photo at a time by definition. Height and (Task 3)
     top margin change; the slider mechanics themselves don't. */
  body.cs-akbars .ab-hero {
    height: 56vh; /* was var(--cs-bleed-h) */
    margin-top: var(--cs-top-gap); /* Task 3: was var(--cs-gap) via the desktop rule */
  }

  body.cs-akbars .ab-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
  }

  body.cs-akbars .ab-col-left,
  body.cs-akbars .ab-col-right {
    width: 100%;
  }

  body.cs-akbars .ab-col-right {
    margin-top: 2.5rem;
  }

  /* Keep switcher 1 (drop 2, first in stacking order after the intro)
     and switcher 2 (drop 1), full-bleed at 62vh. Drop 3 is hidden below. */
  body.cs-akbars .ab-redbox--d2,
  body.cs-akbars .ab-redbox--d1 {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh;
  }

  body.cs-akbars .ab-redbox--d2 {
    margin-top: 2.5rem; /* was 175px, a Figma-approximate desktop value */
  }

  /* Third consecutive photo box, over the 2-in-a-row budget. */
  body.cs-akbars .ab-redbox--d3 {
    display: none;
  }

  /* Spec N describes "two hover-swap boxes" on this page; the markup
     actually has only one (.ab-green--1) — hidden either way, per "hide
     both hover-swap boxes." Its hardcoded margin-top: 157px (flagged in
     the Sport for Good build report as needing its own override
     whenever AK BARS got this treatment) is moot now that the element is
     hidden — a display:none element's margin has no visible effect. */
  body.cs-akbars .ab-green {
    display: none;
  }

  /* Showreel: keep row 1, hide rows 2 and 3. No per-row class exists
     beyond scroll direction, so target by position within .ab-showreel.
     The surviving row's tiles scale to a mobile height (~30vh, still
     square, matching the desktop 380×380 aspect). */
  body.cs-akbars .ab-showreel .ab-sr-row:nth-child(2),
  body.cs-akbars .ab-showreel .ab-sr-row:nth-child(3) {
    display: none;
  }

  body.cs-akbars .ab-sr-seq img {
    width: 30vh;
    height: 30vh;
  }

  /* ============================================================
     KHL ANNUAL REPORT — page-specific mobile layout
     (body.cs-khl-annual). Running order: hero (tape graphic, not a
     photo) → BRIEF + intro → box A → RESULTS + stats → nav. One photo
     total — the two blank placeholder boxes and both blank bands are
     dead weight on a phone (they'd read as broken, not deliberate).
     ============================================================ */

  /* No change needed for the tape geometry itself beyond the shared
     hero-height override: .kar-tape's width (200% of the hero, i.e.
     200vw) and offset (-50%) are both expressed relative to the hero's
     own width, so the "always covers 100vw" math holds at 402px exactly
     as at 1920px — re-derived: a 200vw strip offset -50vw always spans
     -50vw to +150vw (50vw of overflow each side before rotation), and
     rotating up to 18° only shortens the effective horizontal footprint
     by cos(18°) ≈ 0.95, leaving ~190vw of coverage for a 100vw target —
     no gap at any viewport width. The repeated-phrase count (30/36) was
     calibrated for desktop's much larger requirement, so it's already
     generously oversized at 402px; nothing to change there either. */
  body.cs-khl-annual .kar-hero {
    height: 56vh; /* was var(--cs-bleed-h) */
    margin-top: var(--cs-top-gap); /* Task 3: was var(--cs-gap) via the desktop rule */
  }

  body.cs-khl-annual .kar-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
  }

  body.cs-khl-annual .kar-col-left,
  body.cs-khl-annual .kar-col-right {
    width: 100%;
  }

  body.cs-khl-annual .kar-col-right {
    margin-top: 2.5rem;
  }

  body.cs-khl-annual .kar-box--tr {
    width: max(100vw, 100%);
    margin-left: min(0px, calc(50% - 50vw));
    height: 62vh;
  }
}

/* ============================================================
   Tablet band (768–1199px), case studies only (spec P, task P2).
   Three adjustments for the wider end of the extended query above:
   1. .cs-page gets a comfortable reading measure (760px, centred) so
      .cs-intro at 16px doesn't stretch into a 45-word line across
      nearly 1200px. Full-bleed elements (width: max(100vw, 100%);
      margin-left: min(0px, calc(50% - 50vw)) — see the breakout-gap fix
      below) are unaffected by this — that formula self-corrects for the
      containing block's actual width/centring (re-derived when .cs-page
      first gained a gutter, spec K), so hero/photo bleed still reaches
      the true viewport edges, not just the 760px column; only text
      content that relies on .cs-page's own width (header, .cs-intro,
      stats, nav) gets the narrower measure.
   2. The 56vh hero / 62vh photo heights from the block above would be
      very wide-and-short at e.g. 1199×800 (a 1199-wide, ~500-tall photo
      reads as a banner, not "one dominant photo") — raised to 70vh here,
      a judgement call, not a spec-given number.
   3. (Task 3, client) --cs-top-gap and .cs-header's row-gap get their OWN
      tablet numbers here rather than just inheriting the ≤1199 block's
      phone values — the client asked for tablet = ×2 the original,
      phone = ×1.5, which aren't the same rem value at two different root
      font-sizes (unlike most of this file's rem-authored lengths, which
      ride the root font-size on purpose). Root is pinned flat at 20px
      everywhere in this 768–1199 band (see CLAUDE.md "Open items"), so
      these rem values below resolve to exact pixel targets with no drift
      across the band.
   Kazan Racecourse's own version of this block lives in
   kazan-racecourse.css, same media condition.
   ============================================================ */
@media (min-width: 768px) and (max-width: 1199px) {
  body.case-study .cs-page {
    max-width: 760px;
    margin: 0 auto;
  }

  :root {
    --cs-top-gap: 8rem; /* 160px at the pinned 20px root — client: double
      the original 80px (page-top→nav, nav→header, header→hero) */
  }

  body.case-study .cs-header {
    row-gap: 3.5rem; /* 70px at the pinned 20px root — was 2.625rem (the
      phone value); client: double the original 35px here specifically,
      not the same ×1.5 the phone band gets */
  }

  body.cs-sfg .sfg-hero,
  body.cs-ggm .ggm-hero,
  body.cs-khl .khl-hero,
  body.cs-akbars .ab-hero,
  body.cs-khl-annual .kar-hero {
    height: 70vh; /* was 56vh */
  }

  body.cs-sfg .sfg-box--tr,
  body.cs-sfg .sfg-box--bl,
  body.cs-sfg .sfg-end,
  body.cs-ggm .ggm-box--tr,
  body.cs-ggm .ggm-mid,
  body.cs-ggm .ggm-end,
  body.cs-khl .khl-box--bl,
  body.cs-khl .khl-box--tr,
  body.cs-akbars .ab-redbox--d2,
  body.cs-akbars .ab-redbox--d1,
  body.cs-khl-annual .kar-box--tr {
    height: 70vh; /* was 62vh */
  }
}
