/* ══════════════════════════════════════════════════════════════════════════
   ID8Films case study — .csx-*

   The reference layout is FLUID, not fixed. Measured across 13 viewports it
   scales continuously: type is a single vw ratio (h1 = 7.5vw → 144px at 1920,
   108px at 1440), and only the page padding/gutter steps at breakpoints.
   Pinning pixel values to one viewport is wrong at every other width, so the
   whole system is driven from the tokens below.

   Structural constants that hold at every width:
     · 12 columns; spans of 4 and 6
     · stills bands have NO vertical padding; 48px between consecutive bands
     · 16/9 editorial stills, 5/4 project-card media
     · frame radius 4px; card media radius 8px
   ══════════════════════════════════════════════════════════════════════════ */

/* Registered so it can be transitioned — a plain custom property cannot be.
   Drives the card sheen sweep; see .csx-card__media::before. */
@property --sheen-pos {
	syntax: "<number>";
	inherits: false;
	initial-value: 0;
}

.csx {
	--csx-bg: #000000;
	--csx-ink: #ffffff;
	/* Two muted steps: .75 stays legible for labels, .4 is the decorative
	   subtitle weight used for the client name and card eyebrows. */
	--csx-label: rgba(255, 255, 255, 0.75);
	--csx-faint: rgba(255, 255, 255, 0.4);
	--csx-hairline: rgba(255, 255, 255, 0.14);
	/* Media/placeholder fill (the reference's `bg-dark-shade`). Light enough
	   that the gradient at the media→text seam has something to fade from. */
	--csx-shade: #0a0a0a;
	--csx-shade-hi: #1a1a1a;

	/* Padding and gutter move together and step at breakpoints. */
	--csx-gutter: 48px;
	--csx-pad: 48px;
	--csx-radius: 4px;

	/* Fluid type, measured off the reference's computed styles at 1920, 1440,
	   1024 and 390 rather than estimated:

	     element      1920     1440     1024     390      ratio
	     h1           144px    108px    76.8px   47.6px   7.5vw
	     h2 / client   96px     72px    51.2px   31.8px   5vw
	     card title    38.4px   28.8px  20.5px   19.8px   2vw
	     eyebrow       28.8px   21.6px  15.4px   15.9px   1.5vw
	     body          14px     14px    14px     14px     fixed
	     label         12px     12px    12px     12px     fixed

	   The vw ratios were already right; the caps were not. The old 180px cap
	   let the title run 25% larger than the reference ever does — it tops out
	   at 144px — which is what made the headings read as oversized. Each cap
	   is now the reference's own large-screen value.

	   Below ~640px the reference stops scaling linearly and holds a floor, so
	   the lower clamps are its measured 390px sizes rather than round numbers.
	   Body and label are flat: the reference never scales them at all. */
	--csx-fs-title:  clamp(2.975rem, 7.5vw, 144px);
	--csx-fs-client: clamp(1.985rem, 5vw, 96px);
	--csx-fs-h2:     clamp(1.985rem, 5vw, 96px);
	--csx-fs-card:   clamp(1.24rem, 2vw, 38.4px);
	/* Floor set below the 1024px value (15.36px) so the vw ramp, not the
	   clamp, is what governs down to the 768px breakpoint. */
	--csx-fs-eyebrow: clamp(0.9rem, 1.5vw, 28.8px);
	--csx-fs-label: 0.75rem;
	/* The reference sets body copy at a flat 14px. That is kept for prose, but
	   the synopsis/category fields below use a larger step: our meta columns
	   are far wider than the reference's, and 14px over that measure reads as
	   undersized where the same size looks correct in a narrow column. This is
	   a deliberate departure from strict reference parity. */
	--csx-fs-body: 0.875rem;
	/* The synopsis paragraph and the category value. In the reference this is
	   14px, the same as prose; here it steps up so it holds its own against
	   the much wider meta columns. Fluid rather than fixed so it does not
	   overpower narrow viewports — 18px at 1440, 20px at 1920. */
	--csx-fs-body-sm: clamp(1rem, 1.05vw, 1.25rem);

	/* Drop from the media seam to the title. Scales with the viewport
	   (54px at 1920, 45px at 1440) rather than sitting at a constant. */
	--csx-fade-drop: clamp(28px, 2.8vw, 68px);

	/* The reference sets headings in Aeonik Condensed Black and body copy in
	   Aeonik Fono — both proprietary. These are the closest free equivalents,
	   chosen by measuring rendered string widths against the real faces at
	   100px rather than by eye:

	     Aeonik Condensed 900  →  Archivo 900 @ wdth 75   (-0.2% width)
	     Aeonik Fono 400       →  Space Grotesk 400       (-6.6% width)

	   Archivo is a variable font with a width axis (62–125); dialling it to 75
	   matches Aeonik Condensed's proportions almost exactly, where the static
	   condensed families miss by 3–13%. Clash Display, the old heading face,
	   is a normal-width display font — that width is what made the headings
	   look wide as well as large.

	   Space Grotesk is already loaded site-wide by _base.html, so the body
	   face costs no additional request. */
	--csx-display: "Archivo", "Archivo Narrow", "Arial Narrow", sans-serif;
	--csx-display-wdth: 75;
	--csx-body: "Space Grotesk", "Inter", sans-serif;

	--csx-ease: cubic-bezier(0.16, 1, 0.3, 1);
	/* The reference drives its card hover on an expo ease over a full second,
	   while the title colour snaps on the much faster standard curve. */
	--csx-ease-expo: cubic-bezier(0.19, 1, 0.22, 1);
	--csx-ease-std: cubic-bezier(0.4, 0, 0.2, 1);
	--csx-accent: var(--accent-green);

	background: var(--csx-bg);
	color: var(--csx-ink);
	font-family: var(--csx-body);
	/* Contain the full-bleed hero without introducing a horizontal
	   scrollbar on narrow viewports. */
	overflow-x: clip;
}

/* ─── Shared grid shell ─────────────────────────────────────────────────── */
/* Every band uses the same 12-column grid so media edges align down the page. */
/* The reference bands are full-bleed with padding — no max-width — so the
   grid keeps growing with the viewport instead of centring in a fixed rail. */
.csx-band {
	display: grid;
	grid-template-columns: repeat(12, 1fr);
	column-gap: var(--csx-gutter);
	row-gap: var(--csx-gutter);
	width: 100%;
	margin-inline: auto;
	padding-inline: var(--csx-pad);
}

/* 48px between consecutive stills bands, at every viewport. */
.csx-band + .csx-band { margin-top: 48px; }
/* The title and meta bands inside the vignette own their own rhythm (the meta
   band's 112px padding), so the generic band gap must not add to it. */
.csx-fade .csx-band + .csx-band { margin-top: 0; }

.csx-full { grid-column: 1 / -1; }
.csx-half { grid-column: span 6; }

/* ─── Hero ──────────────────────────────────────────────────────────────── */
/* The hero is a band that *holds* a player, not a full-bleed video backdrop:
   the reference clears the fixed header (80px) and letterboxes the footage
   inside the band rather than cropping it to fill the viewport. The player
   sits below the header rather than running underneath it, so nothing of the
   film is ever hidden behind the nav. */
/* The header carries a hairline bottom border site-wide. On the dark case
   study it reads as a rule cutting across the top of the film, so it is
   dropped here only — base.css keeps it for every other page. */
.header-wrapper.header--dark { border-bottom: 0; }

/* The site header overlays the page (absolute, z-index 100) and is 124px tall
   — 100px below 768px. Clear it with the site's own token rather than a
   hardcoded guess, so the hero keeps clearing it if that height ever changes.
   The extra 24px matches the reference's gap between header and player. */
/* Hero sizing, taken from the reference (identical on /brand/concert-visuals
   and /brand/stitched-together, so it is the site-wide rule):
   `h-[88vh] min-h-[40vw] max-h-[100vw]`, which holds a constant
   hero-height/viewport-width ratio of 0.495 at every size.

   The container is full-bleed — no inline padding — and the film inside is
   `object-fit: contain`. Because the box is always wider than the footage's
   16/9, the picture centres with even black bars either side, and those bars
   are what the back pill sits in. */
.csx-hero {
	position: relative;
	width: 100%;
	height: 88vh;
	min-height: 40vw;
	max-height: 100vw;
	display: flex;
	flex-direction: column;
	padding-top: calc(var(--header-h, 124px) + 32px);
	background: var(--csx-bg);
}

/* The back pill is pinned to the left gutter at the top of the hero, clear of
   the centred logo in the fixed header above it. It sits at the same height
   as the top of the player rather than taking a row of its own. */
.csx-back {
	position: absolute;
	/* Sits below the header with enough clearance to read as part of the hero
	   rather than as a second row of navigation. */
	top: calc(var(--header-h, 124px) + 32px);
	left: var(--csx-pad);
	z-index: 10;
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 10px 18px;
	border: 1px solid var(--csx-hairline);
	border-radius: 999px;
	background: rgba(255, 255, 255, 0.06);
	backdrop-filter: blur(8px);
	-webkit-backdrop-filter: blur(8px);
	color: var(--csx-ink);
	transition: background 0.3s var(--csx-ease), border-color 0.3s var(--csx-ease);
}

.csx-back:hover {
	background: rgba(255, 255, 255, 0.12);
	border-color: rgba(255, 255, 255, 0.35);
}

/* ─── Hero player ───────────────────────────────────────────────────────── */
/* Fills the hero edge to edge (the reference's `w-full … h-full`). The box is
   deliberately wider than the film, so `contain` centres the picture and
   leaves matching black bars left and right — consistent on every project
   because the box is driven by the viewport, not by each film's dimensions. */
.csx-player {
	position: relative;
	flex: 1;
	width: 100%;
	min-height: 0;
	background: var(--csx-bg);
	overflow: hidden;
}

/* `contain`, not `cover`: the reference letterboxes the film so no part of
   the frame is cropped, which is the point of a player rather than a bg. */
.csx-player__video,
.csx-player__poster {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
	background: var(--csx-bg);
}

.csx-player__poster { z-index: 1; }
.csx-player.is-started .csx-player__poster { display: none; }

/* Centred play affordance over the poster, ~92px in the reference. */
.csx-player__big {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	z-index: 2;
	width: 92px;
	height: 92px;
	display: grid;
	place-items: center;
	border: 0;
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.35);
	backdrop-filter: blur(6px);
	-webkit-backdrop-filter: blur(6px);
	color: var(--csx-ink);
	cursor: pointer;
	transition: background 0.3s var(--csx-ease), transform 0.3s var(--csx-ease);
}

.csx-player__big:hover { background: rgba(0, 0, 0, 0.6); transform: translate(-50%, -50%) scale(1.06); }
.csx-player.is-started .csx-player__big { display: none; }

/* Control bar: Play · seek · time · Sound · Fullscreen */
.csx-ctrls {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 3;
	display: flex;
	align-items: center;
	gap: 16px;
	padding: 14px var(--csx-pad);
	background: linear-gradient(to top, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0));
	opacity: 0;
	transition: opacity 0.3s var(--csx-ease);
}

/* Controls stay visible until playback starts, then reveal on hover/focus so
   they never sit on top of the film while it plays. */
.csx-player:not(.is-started) .csx-ctrls,
.csx-player:hover .csx-ctrls,
.csx-player:focus-within .csx-ctrls { opacity: 1; }

.csx-ctrls__btn {
	flex: none;
	padding: 0;
	border: 0;
	background: none;
	color: var(--csx-ink);
	font-family: var(--csx-body);
	font-size: 0.75rem;
	font-weight: 500;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	cursor: pointer;
	transition: opacity 0.3s var(--csx-ease);
}

.csx-ctrls__btn:hover { opacity: 0.65; }

.csx-ctrls__time {
	flex: none;
	font-size: 0.75rem;
	font-weight: 500;
	letter-spacing: 0.05em;
	font-variant-numeric: tabular-nums;
	color: var(--csx-ink);
}

/* Seek bar. `appearance: none` + explicit track/thumb so it reads as part of
   the design system rather than a platform control. */
.csx-ctrls__seek {
	flex: 1;
	min-width: 0;
	height: 3px;
	appearance: none;
	-webkit-appearance: none;
	border-radius: 999px;
	background: rgba(255, 255, 255, 0.28);
	cursor: pointer;
}

.csx-ctrls__seek::-webkit-slider-thumb {
	-webkit-appearance: none;
	width: 12px;
	height: 12px;
	border-radius: 50%;
	background: var(--csx-ink);
}

.csx-ctrls__seek::-moz-range-thumb {
	width: 12px;
	height: 12px;
	border: 0;
	border-radius: 50%;
	background: var(--csx-ink);
}

/* ─── Type primitives ───────────────────────────────────────────────────── */
.csx-label {
	font-family: var(--csx-body);
	font-size: var(--csx-fs-label);
	font-weight: 500;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--csx-label);
}

/* Every display element carries the same three properties: weight 900, the
   condensing width axis, and line-height 1.1 — all measured off the
   reference, which uses one voice for its whole heading scale. Grouped so a
   change to the display face happens in one place.

   font-stretch as well as font-variation-settings: browsers map the former
   onto the wdth axis, and it is the property that also reaches the
   non-variable fallbacks in the stack (Archivo Narrow, Arial Narrow). */
.csx-title,
.csx-client,
.csx-h2,
.csx-card__title,
.csx-card__client {
	font-family: var(--csx-display);
	font-weight: 900;
	font-stretch: 75%;
	font-variation-settings: "wdth" var(--csx-display-wdth);
	line-height: 1.1;
	text-transform: uppercase;
}

.csx-title {
	font-size: var(--csx-fs-title);
	/* -3% of the size, matching the reference: -4.32px at 144px. */
	letter-spacing: -0.03em;
	color: var(--csx-ink);
}

.csx-client {
	font-size: var(--csx-fs-client);
	/* -2% of the size: -1.92px at 96px in the reference. */
	letter-spacing: -0.02em;
	color: var(--csx-faint);
}

/* "Behind The Scenes". The reference sets its section headings at the same
   5vw / 96px as the client line, not at a smaller step. */
.csx-h2 {
	font-size: var(--csx-fs-h2);
	letter-spacing: -0.02em;
	color: var(--csx-ink);
}

/* Body copy. line-height 1.6 matches the reference's 22.4px at 14px. */
.csx-body { font-size: var(--csx-fs-body); line-height: 1.6; color: var(--csx-ink); }
.csx-body--sm { font-size: var(--csx-fs-body-sm); line-height: 1.6; }

/* ─── Content container gradient ────────────────────────────────────────── */
/* The black wash belongs to the content container, not to the hero: it starts
   at that container's top boundary — flush under the video, spanning the full
   width — and falls away downward, carrying the transition from media into
   text. Solid black at the seam so the player's lower edge reads as a soft
   hand-off rather than a hard cut. Wraps the title and meta bands. */
.csx-fade {
	background: linear-gradient(to bottom, #000 10%, rgba(0, 0, 0, 0));
	padding-top: var(--csx-fade-drop);
}

/* ─── Title block ───────────────────────────────────────────────────────── */
/* No vertical padding: the vignette's top pad supplies the drop from the
   hero, and the meta band below owns its own spacing. */
.csx-titleblock { padding-block: 0; }

/* ─── Meta (synopsis / category) ────────────────────────────────────────── */
.csx-meta { padding-block: 112px; }
.csx-meta__col { display: flex; flex-direction: column; gap: 12px; }
/* The reference caps the synopsis at ~610px regardless of viewport, so the
   measure stays readable while the column itself keeps growing. Raised in
   proportion to the larger type above: at 610px the enlarged text wrapped to
   an awkwardly short measure. */
.csx-meta__text { max-width: 720px; }

/* The field labels step up with their values so the pair stays balanced —
   a 12px label under 20px text reads as a mismatch. Still clearly subordinate
   to the value it introduces. */
.csx-meta .csx-label { font-size: clamp(0.75rem, 0.72vw, 0.875rem); }
/* span 2 — the category value is a single short word. */
.csx-meta__aside { grid-column: span 2; }

/* ─── Media frames ──────────────────────────────────────────────────────── */
/* The frame owns the aspect ratio, so a missing asset reserves exactly the
   same space as a real one — no layout shift when media loads. */
.csx-frame {
	position: relative;
	width: 100%;
	aspect-ratio: 16 / 9;
	overflow: hidden;
	border-radius: var(--csx-radius);
	background: var(--csx-shade);
}

.csx-frame > img,
.csx-frame > video {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.csx-frame--5x4 { aspect-ratio: 5 / 4; }

/* Placeholder: a faint diagonal sheen so empty frames look intentional. */
.csx-ph {
	position: absolute;
	inset: 0;
	background: linear-gradient(135deg, var(--csx-shade-hi) 0%, var(--csx-shade) 55%);
}

.csx-ph::after {
	content: attr(data-label);
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 0.75rem;
	font-weight: 500;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: rgba(255, 255, 255, 0.22);
}

/* ─── Behind the scenes ─────────────────────────────────────────────────── */
.csx-note {
	padding-block: 112px;
	text-align: center;
}

.csx-note__inner {
	grid-column: 1 / -1;
	max-width: 62ch;
	margin-inline: auto;
	display: flex;
	flex-direction: column;
	gap: 16px;
}

/* ─── Related projects ──────────────────────────────────────────────────── */
/* Rebuilt directly from the reference's authored classes rather than from
   measured positions, which repeatedly caught a scroll animation mid-flight:

     section  grid grid-cols-12
              px-20 md:px-32 lg:px-40 xl:px-48
              gap-x-16 md:gap-x-32 lg:gap-x-40 xl:gap-x-48
              pt-40 pb-64 md:pt-64 md:pb-80 lg:pt-80 lg:pb-120
     head     col-span-full flex justify-center pb-48 md:pb-64 lg:pb-80
     card     col-span-full sm:col-span-6 lg:col-span-4 mb-40 md:mb-48

   There is NO stagger in that CSS: every grid item is transform: none and the
   cards sit flush in clean rows. The 391px/111px offsets on the wrappers inside
   each item are injected by JS and are the START of a scroll entrance in which
   the middle column lags — they decay to 0 once the section is on screen, which
   a settled capture of the reference confirms. Nothing here should reproduce
   them as a static layout.

   Row rhythm comes from margin-bottom on the cards, not row-gap, so no gap is
   inserted between the head row and the first card row. */
/* The rule dividing the stills from More Projects. Measured off the
   reference: 4px solid, white at 5% — a soft band rather than a hairline,
   which is why a 1px border reads as the wrong weight next to it.

   It sits on the section rather than on a separate element, and the negative
   inline margin cancels the band's own padding so the rule runs full-bleed
   edge to edge, as it does in the reference. */
.csx-more {
	padding-top: 80px;
	padding-bottom: 120px;
	row-gap: 0;
	border-top: 4px solid rgba(255, 255, 255, 0.05);
	/* The rule runs full-bleed while the cards stay on the page gutter.

	   .csx-band already applies `padding-inline: var(--csx-pad)`, so pulling
	   the box out by one pad and putting one pad back merely cancels the
	   band's own padding — the border reached the edges but every card slid
	   flush against it. The padding has to be doubled: one pad replaces what
	   the negative margin cancelled, the second restores the gutter. */
	margin-inline: calc(-1 * var(--csx-pad));
	padding-inline: calc(2 * var(--csx-pad));
}

.csx-card { margin-bottom: 48px; }

.csx-more__head {
	grid-column: 1 / -1;
	display: flex;
	justify-content: center;
	padding-bottom: 80px;
}

/* The "More Projects" marker is a pill, not a bare label — matching the
   reference's own treatment, measured from it:
     12px / 500 / 0.6px tracking, uppercase   (the shared label type)
     white at 10% ground, 4px radius
     32px tall, 8px horizontal padding
   The label type itself already comes from .csx-label; only the pill's box
   is defined here. */
.csx-more__head .csx-label {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	height: 32px;
	padding: 0 8px;
	border-radius: 4px;
	background: rgba(255, 255, 255, 0.1);
	/* The reference sets this label to full white rather than the muted grey
	   the other labels use — on a 10% ground it needs the contrast. */
	color: var(--csx-ink);
	letter-spacing: 0.6px;
}

/* Every card is the same height, driven entirely by its media box: in the
   reference the caption is 97.9px tall while its text measures 151–191px, so
   long titles overflow below the card rather than stretching it. That is what
   keeps all six cards uniform regardless of how the titles wrap.

   align-self: start stops the grid stretching cards to the row's tallest. */
.csx-card {
	grid-column: span 4;
	align-self: start;
}

/* Only the media and the caption take pointer events — the gap between them
   is dead space, so the hover state doesn't flicker as the cursor crosses. */
.csx-card__link {
	display: flex;
	flex-direction: column;
	pointer-events: none;
}

.csx-card__mediawrap,
.csx-card__text { pointer-events: auto; }

.csx-card__mediawrap { width: 100%; }

/* The mover carries the hover transform; the media beneath it clips, so the
   rounded corners hold while the card settles back and tilts. The rotation is
   a flat 2D one — no ancestor establishes perspective. */
.csx-card__mover {
	position: relative;
	width: 100%;
	aspect-ratio: 5 / 4;
	transition: transform 1s var(--csx-ease-expo);
}

.csx-card__link:hover .csx-card__mover {
	transform: scale(0.95) rotate(1.5deg);
}

.csx-card__media {
	position: relative;
	width: 100%;
	height: 100%;
	overflow: hidden;
	border-radius: 8px;
	background: var(--csx-shade);
}

/* A diagonal light sweep travels across the media on hover. The band's
   position is driven by --sheen-pos, a registered <number> — plain custom
   properties can't be transitioned, so the @property block above is what
   makes this animatable at all.

   Alpha follows a triangular envelope: zero at either end of the travel and
   peaking at 0.25 halfway, so the band fades up as it crosses and back out
   before it leaves. The overlay is oversized 130% and offset -15% so the
   band is fully off-frame at rest and clears the far edge cleanly. */
.csx-card__media::before {
	--sheen-pos: 0;
	content: "";
	position: absolute;
	top: -15%;
	left: -15%;
	width: 130%;
	height: 130%;
	z-index: 1;
	pointer-events: none;
	background: linear-gradient(
		to top right,
		transparent,
		rgba(255, 255, 255, calc(0.25 - 0.5 * max(var(--sheen-pos) - 0.5, 0.5 - var(--sheen-pos))))
			calc(var(--sheen-pos) * 100%),
		transparent
	);
	transition: --sheen-pos 2s var(--csx-ease-expo);
}

.csx-card__link:hover .csx-card__media::before { --sheen-pos: 1; }

.csx-card__inner {
	position: absolute;
	inset: 0;
}

/* The cards enter with a per-column vertical offset and converge to a common
   baseline as the section scrolls up; films-case-study-parallax.js writes the
   transform.

   No `will-change` here on purpose. A permanent hint would hold a compositor
   layer for every card for the whole life of the page, for an animation that
   only runs while the section crosses the viewport — and the translate3d()
   the script writes already promotes the card for exactly as long as it is
   moving. */

.csx-card__inner > img,
.csx-card__inner > video {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/* Cards keep a common height whether a project name wraps or not, so the
   rows stay aligned. */
.csx-card__text {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: 4px;
	text-align: center;
	/* The reference spaces the caption off the media with the caption's own
	   padding rather than a flex gap, so the hit area starts at the text. */
	padding-top: 20px;
	/* Fixed height, not min-height: cards must stay uniform however the titles
	   wrap, because the percentage offset above resolves against each card's
	   own height. Composed from the parts it actually has to hold — 20px pad,
	   4px gap, one eyebrow line and two clamped title lines — rather than a
	   guessed multiple, which cut the second line through the middle. */
	height: calc(
		20px + 4px + 2px
		+ var(--csx-fs-eyebrow) * 1.1
		+ var(--csx-fs-card) * 1.1 * 2
	);
	flex: none;
}

/* More Projects cards. The reference runs its card eyebrow at 1.5vw / 28.8px
   and the title at 2vw / 38.4px, both in the same condensed black. */
.csx-card__client {
	font-size: var(--csx-fs-eyebrow);
	/* The reference leaves the eyebrow at normal tracking. */
	letter-spacing: normal;
	color: var(--csx-faint);
}

.csx-card__title {
	font-size: var(--csx-fs-card);
	/* -1%: -0.384px at 38.4px in the reference. */
	letter-spacing: -0.01em;
	color: var(--csx-ink);
	/* Colour snaps far faster than the card's 1s geometry — the two gestures
	   are deliberately unmatched in the reference. */
	transition: color 0.15s var(--csx-ease-std);
	/* `balance` splits the wrap evenly instead of leaving an orphan word. */
	text-wrap: balance;
	/* The caption box is a fixed height so cards stay uniform, which means a
	   long title would otherwise overflow into the row below. The reference
	   never hits this because its titles are short project names, while these
	   come from the CMS as full sentences — so clamp to two lines and ellipsize
	   rather than let them collide. */
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
}

.csx-card__link:hover .csx-card__title { color: var(--csx-accent); }

/* ─── Responsive ────────────────────────────────────────────────────────── */
/* Padding and gutter step with the viewport, mirroring the reference:
   48px ≥1280 → 40px @1024 → 32px @768 → 20px/16px ≤640. */
@media (max-width: 1279px) {
	.csx { --csx-pad: 40px; --csx-gutter: 40px; }
}

@media (max-width: 1023px) {
	.csx { --csx-pad: 32px; --csx-gutter: 32px; }
	.csx-card { grid-column: span 6; }
	.csx-meta__aside { grid-column: span 4; }
}

@media (max-width: 767px) {
	.csx { --csx-pad: 20px; --csx-gutter: 16px; }

	/* Collapse every multi-column band to a single stack. */
	.csx-half,
	.csx-card,
	.csx-meta__aside { grid-column: 1 / -1; }

	/* Below 768 the reference switches to a steeper ratio so the headline
	   still fills the narrow measure. Verified against its computed sizes at
	   390px: 12.2vw = 47.6px (h1), 8.1vw = 31.8px (client/h2), 5vw = 19.5px
	   (card title), 3.8vw = 14.8px (eyebrow) — all within a pixel.

	   h2 shares the client ramp here, matching the desktop scale where the
	   two are the same size. */
	.csx {
		--csx-fs-title: clamp(2.25rem, 12.2vw, 5rem);
		--csx-fs-client: clamp(1.5rem, 8.1vw, 3.5rem);
		--csx-fs-h2: clamp(1.5rem, 8.1vw, 3.5rem);
		--csx-fs-card: clamp(1.1rem, 5vw, 2rem);
		--csx-fs-eyebrow: clamp(0.95rem, 4.07vw, 1.5rem);
		/* The reference drops body copy to 12px on narrow viewports — both
		   runs of it, including the synopsis. */
		--csx-fs-body: 0.75rem;
		--csx-fs-body-sm: 0.75rem;
	}

	.csx-meta { padding-block: 64px; }
	.csx-note { padding-block: 64px; }
	.csx-more { padding-top: 56px; padding-bottom: 72px; }
	.csx-more__head { padding-bottom: 40px; }
	.csx-band + .csx-band { margin-top: 24px; }

	/* --header-h drops to 100px here; the hero keeps its 49.5vw ratio so the
	   pillarboxing stays consistent with every other width. */

	/* Tighter control bar so Play/Sound/Fullscreen still fit one row. */
	.csx-ctrls { gap: 10px; padding: 10px 16px; }
	.csx-player__big { width: 64px; height: 64px; }
}

@media (prefers-reduced-motion: reduce) {
	.csx-card__mover,
	.csx-back,
	.csx-ctrls,
	.csx-player__big { transition: none; }
	/* The card's geometry is suppressed, but the title still changes colour —
	   that carries the hover affordance without any vestibular motion. */
	.csx-card__link:hover .csx-card__mover { transform: none; }
	.csx-player__big:hover { transform: translate(-50%, -50%); }
	/* The sweep is decorative travelling motion; hold it off-frame entirely. */
	.csx-card__media::before { transition: none; }
	.csx-card__link:hover .csx-card__media::before { --sheen-pos: 0; }
}

/* ─── Shader backdrop ───────────────────────────────────────────────────────
   A fullscreen fragment shader (js/films-case-study-bg.js) behind the whole
   article. Opt-in: without .csx--shaderbg on the article, nothing below
   applies and the page keeps its flat black exactly as before.

   The canvas is position:fixed, so it is out of flow and in its own layer —
   the page scrolls over a backdrop that never moves, never relayouts, and
   never triggers paint on the content above it.

   The article's own black is what would hide it, so the opt-in trades the
   opaque fill for transparency. Everything that must stay legible over
   moving fog keeps its own background below. */
/* `relative` so the fade sentinel that the shader module appends (an
   absolutely positioned child at the article's bottom edge) resolves against
   the article rather than the viewport. It creates no stacking surprises: the
   canvas and content are already layered explicitly below. */
.csx--shaderbg { background: transparent; position: relative; }

.csx-bg {
	position: fixed;
	inset: 0;
	z-index: 0;
	display: block;
	width: 100%;
	height: 100%;
	/* Purely decorative, and it sits under real content: never a pointer or
	   a11y target. */
	pointer-events: none;
	/* Painted by the GPU every frame; promoting it keeps that off the layer
	   the article is composited into. */
	will-change: transform;
}

/* Content sits above the backdrop. z-index only applies to positioned
   elements, hence the relative. The canvas is itself a child of the article,
   so it is excluded here — otherwise it would be lifted to the same layer as
   the content it is supposed to sit behind. */
.csx--shaderbg > *:not(.csx-bg) { position: relative; z-index: 1; }

/* Bands that carry text over the fog get a readability floor. The hero and
   the vignette already paint black by design — they are the transition from
   the film into the page — so they keep it. */
.csx--shaderbg .csx-hero { background: var(--csx-bg); }

/* The vignette gradient fades the hero's black into the page. At full
   strength it also paints over the fog for the whole height of the title and
   meta block, so on the shader variant it stops at the hero seam and clears
   to nothing well before the text. */
.csx--shaderbg .csx-fade {
	background: linear-gradient(
		to bottom,
		rgba(0, 0, 0, 0.92) 0%,
		rgba(0, 0, 0, 0.55) 6%,
		rgba(0, 0, 0, 0) 16%
	);
}

/* Below the fold the fog should read through the gutters, so the bands stay
   transparent and only the media itself is opaque.

   The title/synopsis block is the piece the shader is *for* — the fog runs
   live behind the heading, Synopsis and Category rather than being scrimmed
   out. Legibility is bought per-element instead: the text itself carries a
   soft shadow (below), which keeps contrast where the glyphs are without
   laying an opaque plate over the whole band. */
.csx--shaderbg .csx-meta { background: none; }

/* The long-form prose block keeps a scrim: a full paragraph of body copy over
   moving noise is a genuine reading cost, unlike the short labelled fields
   above it. Lighter than before so the fog still shows through. */
.csx--shaderbg .csx-note {
	background: linear-gradient(
		to bottom,
		rgba(0, 0, 0, 0),
		rgba(0, 0, 0, 0.58) 18%,
		rgba(0, 0, 0, 0.58) 82%,
		rgba(0, 0, 0, 0)
	);
}

/* Per-glyph legibility over live fog. Two stacked shadows: a tight dark halo
   for edge contrast against a light wisp, and a wider soft one to separate
   the text from the mid-greys. Cheap — no extra layer, no blur filter. */
.csx--shaderbg .csx-title,
.csx--shaderbg .csx-client,
.csx--shaderbg .csx-meta .csx-label,
.csx--shaderbg .csx-meta__text,
.csx--shaderbg .csx-meta .csx-body--sm {
	text-shadow:
		0 1px 2px rgba(0, 0, 0, 0.85),
		0 2px 14px rgba(0, 0, 0, 0.65);
}

/* ─── Where the fog ends ────────────────────────────────────────────────────
   The canvas is fixed to the viewport, so on its own it would keep painting
   behind the footer once the article has scrolled past. The footer is the end
   of the piece: the fog stops there.

   Two independent guards, because either alone is fragile:

   1. The footer paints an opaque ground. It sits in the normal flow above the
      canvas (z-index below), so once it scrolls up it covers the shader.
      `background-color`, not the `background` shorthand — base.css sets
      `.site-footer { background-color: var(--bg-dark) }` at the same
      specificity and later in the cascade, so a shorthand here loses to it.
      That is what left the footer transparent and let the fog bleed through.

      The selector is `body:has(...)` rather than `.csx--shaderbg ~
      .site-footer`: the article lives inside <main> while the footer is a
      sibling of <main>, so the two are not siblings and `~` never matches.

      The colour is base.css's `--bg-dark`, not `--csx-bg`: the latter is
      declared on `.csx`, and the footer sits outside that subtree, so the
      variable does not inherit there — it would resolve to nothing and the
      whole declaration would be dropped. Both tokens are #000000. */
body:has(.csx--shaderbg) .site-footer {
	position: relative;
	z-index: 1;
	background-color: var(--bg-dark);
}

/*  2. The fog fades out over the last stretch of the article, so it dissolves
      into the footer's black rather than ending at a hard horizontal seam.

      That fade is done in the shader (see uFadeEdge in
      films-case-study-bg.js), NOT with a CSS mask here. A mask on the article
      would fade the article's own content too — the final project cards
      ghost out along with the backdrop — and a mask on the canvas would fade
      at a fixed screen position, so the seam would slide around while
      scrolling instead of staying at the end of the content.

      Fading inside the fragment shader avoids both: it touches only the fog,
      and the cutoff is expressed in the article's coordinate space. */
