/* =================================================================
   CUMI — base stylesheet
   Clean white background, near-black type, no accent-colour system —
   colour comes from the work itself. Editorial, minimal, restrained.
   Reference: plusoneamsterdam.com (mood/treatment, not literal copy).

   This file currently covers: reset, type scale, header/nav, footer,
   project template, breadcrumbs, gallery, video facade, stub pages,
   404, accessibility/motion baseline, and the homepage (hero/showreel,
   overlay header, selected work, disciplines, about, scroll-reveal).
   ================================================================= */

/* ---------------------------------------------------------------
   0. Typography — Switzer (self-hosted)
   Client decision, 13 Aug 2026 — replaces the system-font placeholder
   (DECISIONS.md §9.3). Self-hosted per that same decision: files live
   in assets/fonts/, not fetched yet as of this comment (client needs
   to download them from fontshare.com/fonts/switzer — free, no
   account needed — and drop them into that folder; see the delivery
   note for exact filenames). Until then these @font-face rules simply
   fail to match and every browser quietly falls through to the system
   fallback already in --font-body below — nothing breaks, nothing
   flashes unstyled, the page just looks exactly as it does today.
   --------------------------------------------------------------- */

@font-face {
  font-family: 'Switzer';
  src: url('../fonts/Switzer-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Switzer';
  src: url('../fonts/Switzer-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Switzer';
  src: url('../fonts/Switzer-Semibold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------
   1. Reset
   --------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Safety net against horizontal overflow (client report, 17-18 Aug
     2026 — "have to scroll so much to the right to find anything" on
     mobile). Already found and fixed one specific cause (the work-page
     filter row), but rather than hunt every future one individually,
     this stops ANY element that ever overflows the viewport width —
     whether from something already on the page or something added
     later — from creating a horizontal scrollbar / forcing the whole
     page to zoom out to fit it, which is what actually made text look
     "too small" and content "cut off" in these reports. Deliberately
     `clip`, not `hidden`: `overflow: hidden` on html/body is a common
     fix for this same problem elsewhere, but it also breaks EVERY
     position:sticky element on the page in Safari/iOS specifically
     (the header, the pinned project hero, the homepage hero all rely
     on it) — `clip` prevents the same overflow without establishing a
     scroll container, so sticky positioning elsewhere is unaffected. */
  overflow-x: clip;
}

body, h1, h2, h3, h4, p, dl, dd, figure {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

img, svg, video, iframe {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* ---------------------------------------------------------------
   2. Design tokens
   --------------------------------------------------------------- */

:root {
  --colour-bg: #ffffff;
  --colour-ink: #0a0a0a;
  --colour-ink-soft: #4a4a4a;
  --colour-line: #e2e2e2;
  --colour-surface: #f2f2f2;

  /* Cumi brand turquoise, pulled from assets/img/logo/cumi-logo-colour.png
     (client decision, 13 Aug 2026) — the one deliberate exception to
     "no accent colour in the UI": used for small functional accents
     like an active filter state, not as a background/decorative
     palette colour.

     Darkened 14 Sep 2026 (accessibility review) from the logo-exact
     #43c1c3 — that value was only 2.18:1 against white, failing both
     WCAG 1.4.3 text contrast (4.5:1) and 1.4.11 non-text contrast
     (3:1) everywhere it was used as selected-state text/border (work
     filter, Start a Project radio-chips). Same hue/saturation, just
     darker — #257374 is 5.55:1 against white. Flagged to the client:
     this no longer matches the logo swatch exactly; revisit if an
     exact-brand-match matters more than AA contrast here. */
  --colour-brand-turquoise: #257374;

  /* Switzer first, system stack as the safe fallback while the actual
     font files are pending (see the @font-face rules above). */
  --font-body: 'Switzer', -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, Helvetica, Arial, sans-serif;

  /* Archivo Expanded — the hero title only (client decision,
     13 Aug 2026), loaded via Google Fonts in inc/head.php. Everywhere
     else (h1–h3, section headings) stays in --font-body at a lighter
     weight than before — the brief was specifically that headings were
     reading too thick/bold; one deliberately bold, wide display moment
     for the hero plus a lighter touch everywhere else. */
  --font-display: 'Archivo', -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, Helvetica, Arial, sans-serif;

  /* =================================================================
     Type scale, letter-spacing, line-height and --space-5 — LOCKED IN
     16 Sep 2026 as the client's chosen permanent values, replacing the
     original 13 Aug 2026 numbers. These were the "New styling" side of
     a live staging toggle (assets/css/typography-preview.css, see
     project memory / chat history — the "FEEDBACK 8Sept26 Fonts and
     styling" doc) that the client compared against today's site for
     several rounds before asking to lock in New styling permanently.
     The toggle, the switch UI and typography-preview.css have all been
     removed now that there's nothing left to compare — this IS the
     site's styling going forward, not an override sitting on top of it.
     --ls-* and --lh-* are new tokens (the original file never had named
     letter-spacing/line-height custom properties — every rule below
     that uses them previously had the value written inline).
     ================================================================= */
  --step-body: clamp(1rem, 0.98rem + 0.08vw, 1.0625rem);      /* was clamp(1rem, 0.94rem + 0.3vw, 1.125rem) — 16px -> 17px */
  --step-lead: clamp(1.0625rem, 0.98rem + 0.4vw, 1.375rem);   /* was clamp(1.125rem, 1.02rem + 0.55vw, 1.5rem) — 17px -> 22px */
  --step-h2: clamp(1.875rem, 1.7rem + 0.67vw, 2.375rem);      /* was clamp(1.75rem, 1.5rem + 1.3vw, 2.75rem) — 30px -> 38px */
  --step-h1: clamp(2.375rem, 2rem + 1.5vw, 3.5rem);           /* was clamp(2.5rem, 1.9rem + 3vw, 5rem) — 38px -> 56px */
  --step-display: clamp(2.875rem, 2.25rem + 2.5vw, 4.75rem);  /* was clamp(2.75rem, 1.9rem + 4.2vw, 6.5rem) — 46px -> 76px */
  --step-small: 0.875rem;                                     /* was clamp(0.8125rem, 0.79rem + 0.1vw, 0.875rem) — flat 14px */

  --ls-display: -0.025em;
  --ls-heading: -0.015em;
  --ls-lead: -0.015em;
  --ls-label: 0.04em;

  --lh-display: 0.90;
  --lh-heading: 1.05;
  --lh-lead: 1.25;
  --lh-body: 1.50;

  --space-1: clamp(0.5rem, 0.46rem + 0.2vw, 0.625rem);
  --space-2: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
  --space-3: clamp(1.75rem, 1.5rem + 1.2vw, 2.75rem);
  --space-4: clamp(2.5rem, 2rem + 2.5vw, 4.5rem);
  --space-5: clamp(5rem, 3.75rem + 5vw, 8.75rem); /* was clamp(4rem, 3rem + 5vw, 8rem) — major section spacing, 80px -> 140px */

  --container-max: 90rem;
  --container-pad: clamp(1.25rem, 1rem + 2vw, 3rem);

  --focus-ring: 3px solid var(--colour-ink);
  --focus-offset: 2px;

  /* ⚠️ ADDED 16 Sep 2026 — client, once told the grey-logo/dark-mode
     report (see inc/head.php's own 16 Sep comment) traced to this site
     having no dark theme of its own at all: "if the background goes
     dark then the logo and menu going white is a no brainer." These two
     are DELIBERATELY NOT redefined in the `prefers-color-scheme: dark`
     block below — every other colour token in this file describes the
     page's own chrome (bg/text/borders), which is exactly what should
     flip with the visitor's system theme, but a good few places on this
     site (the homepage/project hero overlaying a video or photo, the
     video footer, the "Play showreel" pill) composite text directly
     against footage instead of against page chrome — that text's
     colour has to track the FOOTAGE's own brightness, a completely
     different, unrelated axis from the visitor's OS theme (real
     investigation already spent getting that right — see section 12's
     15/16 Sep comments — for a problem that has nothing to do with
     light/dark mode and everything to do with what a given video frame
     looks like). --colour-scrim-dark/light give those specific spots a
     fixed pair of values to reference instead of --colour-ink/bg, so
     dark mode can't reopen a footage-contrast bug that took real
     back-and-forth to close. Both currently equal light mode's own
     ink/bg (there's no reason yet for the frozen scrim colours to differ
     from light mode's, since the footage they sit on doesn't care what
     the rest of the page is doing) — kept as separate named tokens
     rather than reusing --colour-ink/--colour-bg directly so it's
     obvious at every use site that "this one's pinned on purpose," not
     an oversight if dark mode ever looks wrong on the hero. */
  --colour-scrim-dark: #0a0a0a;
  --colour-scrim-light: #ffffff;

  /* Changed from `light` to `light dark` 16 Sep 2026 alongside adding a
     real dark theme below — tells any browser (and native form
     controls/scrollbars) this page genuinely supports both and to pick
     using `prefers-color-scheme`, rather than either assuming
     light-only or, worse, silently auto-recolouring the page with its
     own dark-mode heuristics on top of colours this file already
     controls (Android/Samsung Internet's "dark mode for websites" is
     exactly what was turning the logo an inconsistent grey — see
     inc/head.php's matching comment). */
  color-scheme: light dark;
}

/* ⚠️ ADDED 16 Sep 2026 — client-requested real dark theme: "if the
   background goes dark then the logo and menu going white is a no
   brainer. If light we already have black." Every colour token above
   (bg/ink/ink-soft/line/surface/brand-turquoise) is used consistently
   enough throughout this file — including the CTA/pill "invert on
   hover" pattern (fill with --colour-ink, text in --colour-bg), which
   still makes perfect sense flipped — that redefining just the tokens
   here reskins the whole site correctly with no other changes needed,
   EXCEPT the handful of "always composited against video/photo
   footage, not page chrome" spots that deliberately use
   --colour-scrim-dark/light instead (see that pair's own comment above)
   and so are unaffected by this block on purpose.
   Values picked and checked for real WCAG contrast against the new
   #121212 background (not just eyeballed): ink #f5f5f5 (17.2:1),
   ink-soft #b0b0b0 (8.6:1, comfortably clears the 4.5:1 text minimum),
   line #2a2a2a (kept LOW-contrast, ~1.3:1, deliberately matching light
   mode's own --colour-line's low ~1.3:1 against white — it's a purely
   decorative divider colour in both themes, see that token's own
   original comment), surface #1c1c1c (~1.1:1 above the base bg, same
   "barely-there" proportion as light mode's --colour-surface against
   white, used only as an image/video loading-placeholder tint). Brand
   turquoise reverts to the exact logo hue #43c1c3 rather than staying
   at light mode's darkened #257374: that darker shade was chosen 14 Sep
   2026 specifically for contrast against a WHITE background (5.55:1) —
   against this dark background it only manages 3.37:1 (fails the 4.5:1
   text minimum), while the original brighter, exact-brand #43c1c3 gives
   8.61:1 here, so dark mode gets to have both the accessible contrast
   AND the exact logo colour the 14 Sep note flagged as a compromise. */
@media (prefers-color-scheme: dark) {
  :root {
    --colour-bg: #121212;
    --colour-ink: #f5f5f5;
    --colour-ink-soft: #b0b0b0;
    --colour-line: #2a2a2a;
    --colour-surface: #1c1c1c;
    --colour-brand-turquoise: #43c1c3;
  }

  /* The logo file itself (assets/img/logo/cumi-logo.png) is a plain
     solid-black wordmark on transparent (confirmed: every opaque pixel
     is exactly rgb(0,0,0)) — inverting a pure black raster gives pure
     white, i.e. exactly assets/img/logo/cumi-logo-white.png's own
     colour, with no separate asset/request needed and no risk of the
     two ever drifting out of sync with each other. */
  .site-logo__img {
    filter: invert(1);
  }

  /* Cancels the invert above specifically where the logo still sits
     directly over unpredictable video/photo footage rather than solid
     page chrome (homepage hero pre-scroll, a project page's own hero
     pre-scroll) — same "always black over footage" reasoning as
     --colour-scrim-dark/light above, and the same two contexts that
     token pair covers everywhere else in this file. Once either header
     goes solid (scrolled past the footage), this no longer matches and
     the plain rule above takes back over, correctly going white. */
  body.template-home .site-header:not(.site-header--solid) .site-logo__img,
  body.template-project.has-hero .site-header:not(.site-header--solid) .site-logo__img {
    filter: none;
  }
}

html, body {
  background: var(--colour-bg);
  color: var(--colour-ink);
}

/* ⚠️ ADDED 15 Sep 2026 — client report: text selection ("highlighted"
   text) was showing as an "inky blue", which the client says should be
   turquoise instead, "the same colour as before but slightly darker".
   No ::selection rule existed anywhere in this stylesheet before this
   change (checked) — with none defined, the browser falls back to its
   own default selection colour, which on Chrome/Safari/Edge is exactly
   that dark/inky blue, not anything this codebase ever set. There is
   also no earlier build of this site on file to confirm the exact
   "before" turquoise shade the client remembers, so this uses the
   site's one existing brand-turquoise token (--colour-brand-turquoise,
   #257374 — see ~line 132) darkened by about a fifth, since that's the
   only turquoise value anywhere in the codebase and darker is exactly
   what was asked for. Flagging here rather than guessing silently: if
   this specific shade isn't the one the client remembers, tell us the
   hex and it's a one-line change. */
::selection {
  background: #1e5c5d;
  color: #ffffff;
}

::-moz-selection {
  background: #1e5c5d;
  color: #ffffff;
}

body {
  font-family: var(--font-body);
  font-size: var(--step-body);
  line-height: var(--lh-body, 1.55);
  min-height: 100svh;
  min-height: 100dvh;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

h1, h2, h3 {
  /* Default tier — "navigation / captions" (client's own wording,
     13 Aug 2026): card titles, news items, subheadings, anything not
     explicitly bumped up below. Was 600 (Semibold); dropped to 500
     (Medium) because headings were reading too thick/bold. */
  font-family: var(--font-body);
  font-weight: 500;
  line-height: var(--lh-heading, 1.05);
  letter-spacing: var(--ls-heading, -0.01em);
}

/* Tier 2 — "headlines / hero statements" (client's wording): every
   page's actual <h1> (project titles, the Work page's opening
   statement, the hero title, etc.) plus the homepage's major section
   headings, which read at the same visual weight even though they're
   marked up as <h2> — see .section-heading below. Archivo Expanded,
   loaded in inc/head.php. Font-size is untouched here; each context
   (.hero__title, .work-header__title, .project__title, ...) already
   sets its own via the type scale. */
h1, .section-heading {
  /* TEMPORARY, client request 13 Aug 2026: swapped from Archivo
     Expanded (--font-display) to Switzer (--font-body) at its
     heaviest available weight, so headlines can be compared as
     Switzer-only. To restore Archivo Expanded: font-family:
     var(--font-display); font-stretch: expanded; font-weight: 700; */
  font-family: var(--font-body);
  font-weight: 600;
}

/* ---------------------------------------------------------------
   3. Layout helpers
   --------------------------------------------------------------- */

.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-2);
  z-index: 100;
  background: var(--colour-ink);
  color: var(--colour-bg);
  padding: var(--space-1) var(--space-2);
  transition: top 0.15s ease;
}

.skip-link:focus {
  top: var(--space-2);
}

/* ---------------------------------------------------------------
   4. Accessibility / focus / motion baseline
   --------------------------------------------------------------- */

a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* Visually hidden but present for screen readers/document outline —
   used where a section deliberately has no visible heading text on
   screen (e.g. "What We Do", brief §29 semantic-HTML requirement still
   needs a real <h2> even if it isn't shown). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------
   5. Header / navigation
   --------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--colour-bg);
  border-bottom: 1px solid var(--colour-line);
}

/* ⚠️ CHANGED BACK 16 Sep 2026 — client report: "the menu is now too far
   to the left, it should still be to the right but not cut off... I
   don't want it so far to the left, it looks silly." This reverts the
   15 Sep 2026 change noted below: justify-content is space-between
   again, so the logo sits on the far left and the nav/CTA/toggle group
   sits on the far right, with the row's own width filling the space
   between them — the normal "logo left, menu right" layout the client
   is asking for again, not the two groups clustered together on the
   left with empty space on the right.
   This does NOT bring back the Fold4/narrow-phone cut-off bug fixed
   the same day (see the @media (max-width: 26rem) block right below):
   that fix works by shrinking the row's total content width (tighter
   gaps, smaller CTA), which is what actually stops it overflowing the
   viewport — it doesn't depend on which value justify-content has, so
   it's left untouched and still applies here.
   (Previous comment, now superseded: "justify-content changed from
   space-between to flex-start 15 Sep 2026 (client instruction: 'move
   [the menu] over to the left instead of the right') — the logo and
   the nav/CTA/toggle group now sit clustered together on the left...
   Nothing on the right any more; that's the intended look, not an
   accidental gap.") */
.site-header__inner {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-2) var(--container-pad);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.site-logo {
  display: inline-flex;
  flex-shrink: 0;
}

/* Doubled 14 Sep 2026 (client instruction: "much bigger, ~doubled" —
   see inc/header.php's own note on the logo swap this same pass). Was
   clamp(2rem, 1.8rem + 0.8vw, 2.5rem). */
.site-logo__img {
  width: clamp(4rem, 3.6rem + 1.6vw, 5rem);
  height: auto;
}

/* Nav list + "Start a Project" CTA + mobile toggle, grouped as one unit
   on the right of the logo. Keeping the CTA and toggle inside this same
   flex row (rather than as siblings of it) is what keeps them adjacent
   at every width — the CTA never gets swallowed into the hamburger menu,
   it just sits immediately to its left, always visible. */
.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.site-header__cta {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  min-height: 36px;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding-inline: var(--space-2);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  white-space: nowrap;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

/* Default (non-homepage, or homepage once solid): dark text on white
   header, so hover fills solid ink with white text — matches .hero__play. */
.site-header__cta:hover,
.site-header__cta:focus-visible {
  background: var(--colour-ink);
  color: var(--colour-bg);
}

/* ⚠️ FIXED 16 Sep 2026 — client report: "on a narrower phone such as
   fold 4 the menu gets cut off slightly so three lines are too far to
   the right again." Measured directly (real element positions at
   several viewport widths, not a guess): the logo + "Start a Project"
   pill + mobile toggle are one flex row (see the "one unit on the
   right of the logo" comment above .site-header__actions) that never
   wraps and never shrinks below its own content width. That row's
   natural width is ~387px once you add up the logo, both gaps, the
   pill's own padding/text and the toggle — so it only fits with room
   to spare from roughly 390px upward. Below that it starts eating into
   .site-header__inner's own right-hand padding first, then the toggle
   itself: overflowing by ~5px at 360px (a genuinely common Android
   width, not just a fold's cover screen), ~20px at 344px, and ~43px at
   320px — with nothing to absorb it, `overflow-x: clip` on <html>
   (section 1) just clips the toggle's right edge instead of ever
   showing a scrollbar, which is exactly "cut off... too far to the
   right". ("Again" because a past fix for a footer video box's own
   height on narrow phones — a different element entirely, see
   .footer-video's own 15 Sep comment — apparently got remembered as
   covering this too; it didn't touch the header row at all.)
   Fix: below 26rem (416px, comfortably above the ~390px point real
   phones stop needing it, so this never engages on a normal-width
   phone) tighten the row's own spacing and the CTA's own padding/size
   — not the logo, which the client deliberately doubled in size 14 Sep
   and asked to stay that way. That alone frees ~75px, enough for the
   whole row (toggle included) to fit down to 320px with margin left
   over, at the cost of the logo/pill/toggle sitting a bit closer
   together than at wider widths — a tight header being the trade-off
   for nothing ever being clipped off-screen. */
@media (max-width: 26rem) {
  .site-header__inner {
    gap: 0.75rem;
    padding-inline: 0.875rem;
  }

  .site-header__actions {
    gap: 0.75rem;
  }

  .site-header__cta {
    padding-inline: 0.625rem;
    font-size: 0.75rem;
  }

  /* 21 Sep 2026, added alongside .site-header__back itself: this is the
     exact width band the 16 Sep fix above had to fight to fit into with
     only logo + CTA pill + toggle already at capacity down to 320px
     (see that fix's own comment) — a fourth element with its own text
     label would blow that budget straight back open. Dropping to just
     the arrow here keeps the tap target (still the full-height <a>,
     just narrower) without reintroducing the cut-off bug that fix was
     written to solve. */
  .site-header__back-label {
    display: none;
  }
}

.site-nav--desktop {
  display: none;
}

.site-nav__list {
  display: flex;
  gap: var(--space-3);
}

.site-nav__list a {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  padding-block: var(--space-1);
  position: relative;
}

.site-nav__list a[aria-current="page"] {
  color: var(--colour-ink);
}

.site-nav__list a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: currentColor;
}

.site-nav__list a:hover,
.site-nav__list a:focus-visible {
  color: var(--colour-ink-soft);
}

/* "Work" submenu — desktop: revealed on hover/focus-within, no JS.
   opacity + pointer-events (not display/visibility) keep the links in
   the tab order while hidden, so :focus-within fires correctly when a
   keyboard user tabs into them (brief §27 — keyboard nav must reach
   everything a mouse can). */
.site-nav__item {
  position: relative;
}

.site-nav__item--has-children > a::after {
  content: "";
  display: inline-block;
  width: 0.4em;
  height: 0.4em;
  margin-left: 0.4em;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}

.site-nav__submenu-list {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 60;
  min-width: 14rem;
  background: var(--colour-bg);
  color: var(--colour-ink);
  border: 1px solid var(--colour-line);
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
}

.site-nav__item--has-children:hover .site-nav__submenu-list,
.site-nav__item--has-children:focus-within .site-nav__submenu-list {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.site-nav__submenu-list a {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--step-body);
  text-transform: none;
  letter-spacing: normal;
  white-space: nowrap;
}

.site-nav__submenu-list a:hover,
.site-nav__submenu-list a:focus-visible {
  color: var(--colour-ink-soft);
}

/* Mobile: always visible under the parent link, indented, no separate
   toggle — one tap opens the whole menu including these. */
.site-nav__submenu-list--mobile {
  position: static;
  opacity: 1;
  pointer-events: auto;
  transform: none;
  border: 0;
  box-shadow: none;
  padding: var(--space-1) 0 var(--space-1) var(--space-3);
  margin-top: var(--space-1);
}

.site-nav__submenu-list--mobile a {
  min-height: 40px;
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

/* Mobile disclosure menu — works with zero JavaScript (brief §16) */
.site-nav--mobile {
  position: relative;
}

.site-nav__toggle {
  display: inline-flex;
  cursor: pointer;
  list-style: none;
  min-height: 44px;
  min-width: 44px;
  align-items: center;
  justify-content: center;
}

.site-nav__toggle::-webkit-details-marker { display: none; }

/* Three-line icon, not a text label — morphs into a close (×) mark
   when the disclosure is open. Colour comes from currentColor, so it
   automatically matches the header's white-on-hero / ink-on-white
   states with no extra rules needed. */
.site-nav__toggle-icon,
.site-nav__toggle-icon::before,
.site-nav__toggle-icon::after {
  display: block;
  width: 22px;
  height: 2px;
  background: currentColor;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.site-nav__toggle-icon {
  position: relative;
}

.site-nav__toggle-icon::before,
.site-nav__toggle-icon::after {
  content: "";
  position: absolute;
  left: 0;
}

.site-nav__toggle-icon::before { top: -7px; }
.site-nav__toggle-icon::after { top: 7px; }

.site-nav--mobile[open] .site-nav__toggle-icon {
  background: transparent;
}

.site-nav--mobile[open] .site-nav__toggle-icon::before {
  top: 0;
  transform: rotate(45deg);
}

.site-nav--mobile[open] .site-nav__toggle-icon::after {
  top: 0;
  transform: rotate(-45deg);
}

.site-nav__list--mobile {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  min-width: 12rem;
  background: var(--colour-bg);
  border: 1px solid var(--colour-line);
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
}

.site-nav__list--mobile a {
  display: block;
  padding-block: var(--space-1);
  min-height: 44px;
  display: flex;
  align-items: center;
}

@media (min-width: 64rem) {
  .site-nav--desktop { display: block; }
  .site-nav--mobile { display: none; }
}

/* ---------------------------------------------------------------
   6. Footer — light bar only (client decision, 13 Aug 2026): the
   sticky header keeps Contact one click away on every page, so this
   doesn't repeat it as a large CTA — see inc/footer.php.
   --------------------------------------------------------------- */

.site-footer {
  /* Same stacking-context fix as .disciplines/.about: shared sitewide,
     but only matters on the homepage where the pinned hero sits behind
     everything — harmless on every other page. */
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  border-top: 1px solid var(--colour-line);
}

.site-footer__bar {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-3) var(--container-pad);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

.site-footer__contact {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.site-footer__contact:hover,
.site-footer__contact:focus-visible {
  color: var(--colour-ink);
}

/* Privacy link + "Cookie settings" reopen control (pre-launch technical
   audit, FIX 02/FIX 03, 23 Sep 2026) — same treatment as
   .site-footer__contact above, so it reads as one consistent row of
   footer links rather than a bolted-on afterthought. The cookie-settings
   control is a <button>, not a link (it has no href, it reopens
   assets/js/consent.js's banner in place — see inc/footer.php), but is
   styled identically for visual consistency. */
.site-footer__privacy {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  color: inherit;
}

.site-footer__cookie-settings {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

.site-footer__privacy:hover,
.site-footer__privacy:focus-visible {
  color: var(--colour-ink);
}

/* UK trading disclosure line (/studio/ only, inc/footer.php) — small
   print beneath the usual copyright/contact bar, not part of it, so it
   doesn't compete with those for attention. */
.site-footer__legal {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: 0 var(--container-pad) var(--space-3);
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  opacity: 0.8;
}

/* Homepage-only alternative footer: a full-width video, no
   copyright/contact text (client decision, 13 Aug 2026 — see the doc
   comment in inc/footer.php for how/when this switches on). Border
   removed since the video itself provides the visual break from the
   section above it. */
.site-footer--video {
  border-top: 0;
  /* Frozen scrim colour, not the theme-tracking --colour-ink — see that
     token pair's own comment in section 2. This is the fallback fill
     behind the actual video (visible only while it loads), so it needs
     to always read as "dark, like the video", not flip light in dark
     mode. */
  background: var(--colour-scrim-dark);
}

.footer-video {
  /* width: 100% added 19 Aug 2026 — this was the REAL, confirmed cause
     of "have to scroll right a long way to find the menu" on the
     homepage specifically (client report, mobile only). Reproduced and
     root-caused live: with no explicit width, aspect-ratio (21/9)
     combined with min-height (22rem) triggers a specific, documented
     part of the CSS box-sizing algorithm — when a block box's height
     ends up constrained by min-height to be TALLER than its aspect
     ratio would naturally give for the available width, the browser
     recalculates WIDTH from that min-height via the ratio instead of
     the other way round. Measured on a real 369px-wide viewport: this
     produced a used width of 821.33px (= 22rem's 352px × 21/9, exactly)
     — a DIV, not an img/video/iframe, so the sitewide `max-width: 100%`
     reset (section 1) never applied to it either. That 821px box,
     uncontained, is what widened the whole page and dragged the sticky
     header/menu sideways with it — nothing to do with the hero
     placeholder shapes I'd suspected and fixed on 19 Aug (that fix was
     harmless but not the actual cause; left in place regardless, it's
     still a correct improvement in its own right).
     Confirmed fixed live: adding width: 100% here took the same test
     from 452px of overflow to 0px, instantly. */
  width: 100%;
  position: relative;
  aspect-ratio: 21 / 9;
  max-height: 44rem;
  overflow: hidden;
}

/* ⚠️ FIXED 15 Sep 2026 — client report: "the footer doesn't resize
   properly .. its cut off when its a narrower phone". Root cause: the
   min-height: 22rem above (added 19 Aug 2026, see that rule's own
   comment) used to apply unconditionally at every width. On a normal
   phone, 21:9 at the actual viewport width comes out much shorter than
   22rem (e.g. ~10rem at a 375px-wide screen) — min-height was
   overriding that, forcing the box noticeably TALLER than its own
   aspect-ratio says it should be for that width. .footer-video__media
   uses object-fit: cover to fill whatever box it's given, so a box
   forced taller than its native aspect makes `cover` zoom in and crop
   MORE off the left/right than the 21:9 framing intends — on the
   client's phone this cropped the footer video's own in-frame caption
   text half off the left edge, reading as the footer being "cut off".
   Fix: only apply the 22rem floor once the viewport is already wide
   enough that 21:9 alone would reach 22rem or more on its own (22rem
   tall at 21:9 needs a 22rem × 21/9 ≈ 51.3125rem-wide box) — below that
   width, aspect-ratio is left alone to size the box correctly with no
   extra cropping, matching every narrower phone/tablet width. */
@media (min-width: 51.3125rem) {
  .footer-video {
    min-height: 22rem;
  }
}

.footer-video__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.footer-video__media--photo img {
  animation: hero-photo-drift 26s ease-in-out infinite alternate;
}

/* ⚠️ ADDED 16 Sep 2026 — the video footer (.site-footer--video) now
   shows on every page, not just the homepage (client request: "the
   video footer was designed for all the pages... Can you get that
   working pls?" — see inc/footer.php's own doc comment for the full
   story). The one page that also needs its UK trading disclosure text
   (.site-footer__legal) is /studio/, and that text's own colour
   (--colour-ink-soft, tuned for the plain footer's light background)
   would be close to unreadable sitting directly on the video footer's
   dark background (--colour-ink). Rather than give .site-footer__legal
   a second dark-mode colour variant, this wrapper gives it back a light
   strip of its own underneath the video — same look/legibility as the
   plain footer's version, just positioned after the video instead of
   after the copyright bar (client's own instruction: "that page must
   have the legal info either above or below the video footer"). */
.site-footer__legal-bar {
  background: var(--colour-bg);
  border-top: 1px solid var(--colour-line);
}

.site-footer__legal-bar .site-footer__legal {
  padding-top: var(--space-3);
}

/* Privacy/cookie-settings row under the video footer (FIX 02/FIX 03,
   23 Sep 2026) — the video footer has no plain-footer bar of its own
   for these links to join, and (same reasoning as .site-footer__legal-bar
   just above) sitting them directly on the dark video would be close to
   unreadable, so this gets the identical light strip treatment. Shown on
   every page with the video footer, not just /studio/ — unlike the legal
   disclosure bar, a privacy link and cookie control need to be
   findable everywhere, not just one page. */
.site-footer__video-links {
  background: var(--colour-bg);
  border-top: 1px solid var(--colour-line);
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-2) var(--container-pad);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

/* Seasonal footer-video overlay (21 Sep 2026) — a small <canvas>
   layered on top of the footer video/photo above, animated by
   assets/js/footer-seasons.js (snow/blossom/light/leaves — see
   'footer_seasons' in content/site.php for how it gets switched on).
   Absolutely positioned inside the same .footer-video box
   (position: relative, set above), so it inherits that box's 21:9/
   44rem sizing for free — no media queries of its own needed.
   pointer-events: none — purely decorative, never intercepts clicks;
   there's nothing clickable in the footer video anyway, but this keeps
   it inert by contract rather than by accident. */
.footer-video__seasonal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The sitewide reduced-motion rule (section 4) already zeroes CSS
   transitions/animations, but these particles are drawn frame-by-frame
   in JS, not via CSS animation, so they need their own opt-out here.
   assets/js/footer-seasons.js also checks the same media query itself
   before ever drawing a frame — this is the CSS-side belt-and-braces
   twin of that check, so the canvas can never visibly show motion even
   if the script runs before this rule is parsed. */
@media (prefers-reduced-motion: reduce) {
  .footer-video__seasonal {
    display: none;
  }
}

/* "Previewing: ..." switcher (cumi_footer_season_switcher() in
   inc/media.php) — plain links, no JS required, so it still works with
   JS blocked. Never printed on the real production domain, whatever the
   URL; on any other host it only prints once a ?season= value has
   already been used once (fixed 22 Sep 2026 — it used to print on every
   visit to any non-production host, including the client's own day-to-
   day preview domain, which is why she kept seeing it). No CSS to strip
   out before launch either way, since it's simply never in the markup
   on the real site. */
.footer-season-switcher {
  position: absolute;
  right: var(--space-2);
  bottom: var(--space-2);
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  max-width: calc(100% - var(--space-2) * 2);
  padding: 0.35rem 0.5rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
  font-size: 0.6875rem;
  line-height: 1;
}

.footer-season-switcher__label {
  color: rgba(255, 255, 255, 0.6);
  padding-inline-end: 0.25rem;
  white-space: nowrap;
}

.footer-season-switcher a {
  color: rgba(255, 255, 255, 0.85);
  text-decoration: none;
  padding: 0.3rem 0.55rem;
  border-radius: 999px;
  white-space: nowrap;
}

.footer-season-switcher a:hover,
.footer-season-switcher a:focus-visible {
  background: rgba(255, 255, 255, 0.15);
}

.footer-season-switcher a.is-active {
  background: #fff;
  color: #111;
  font-weight: 600;
}

@media (max-width: 30rem) {
  .footer-season-switcher {
    left: var(--space-2);
    right: var(--space-2);
    justify-content: center;
  }
}

/* ---------------------------------------------------------------
   7. Project back link
   (Was a full breadcrumb trail — "Home / Work / Title" — until 15 Aug
   2026, when client feedback called it clutter the reference site
   doesn't show either. Replaced with a single minimal back link;
   BreadcrumbList structured data is unaffected, see templates/project.php.
   Styles for the heroed-page variant live in section 12b, alongside the
   overlay header they work with.)
   --------------------------------------------------------------- */

/* ---------------------------------------------------------------
   8. Project template
   --------------------------------------------------------------- */

/* Every block that can appear after a pinned/sticky hero (.project__intro,
   .project__body, .project__gallery, .project-media-row, .project-marquee,
   .project-hero--inline, .project__related, .project-chapter) needs to
   actively paint OVER it as the page scrolls, not just happen to come
   later in the HTML — position:sticky content with an explicit z-index
   (see .project-hero--pinned and .project__hero above) establishes its
   own stacking context, so later unpositioned siblings can end up
   painting BEHIND it regardless of document order. Same fix, same
   reasoning, as the homepage's .work/.disciplines/.about/.site-footer
   treatment (13 Aug 2026) — shared sitewide here rather than repeated
   per-layout since every one of the 8 layouts is built from this same
   shared set of blocks (inc/project-components.php). Background colour
   is unchanged visually (these all already sat on the page's plain white
   background) — this only adds the positioning needed to guarantee they
   paint correctly above a sticky hero on pages that have one; harmless on
   the pages that don't.

   .project-chapter added 21 Aug 2026 after the client reported the
   caption card on a multi-video-grid page still showing the pinned hero
   bleeding through around it ("the title is smaller than the video and
   around it is a see through layer so you can see the video under it
   that is playing"). Root cause: .project-chapter itself (the OUTER
   wrapper added the same day for the caption/video grouping fix, see
   its own rule below) has a top border and top padding that sit ABOVE
   its children — .project__body and .project-hero--inline paint their
   OWN backgrounds, but that gap belongs to .project-chapter itself,
   which had no background of its own, so the sticky hero still showed
   through that strip and its full-bleed side gutters every time.
   Adding .project-chapter to this shared list closes that gap the same
   way as every other block here.

   .project__section added 6 Sep 2026, same root cause again: the client
   reported sections still turning see through as they scroll up over a
   project's pinned hero. This is the base wrapper class every layout's
   own heading and paragraph sections are built on (see
   cumi_project_text() and the "The Film" section in
   templates/project-layouts/brand-reveal.php, process-explainer.php,
   product-story.php and digital-art.php) — it was never added to this
   list itself, only specific descendants of it were (.project__body,
   .project__gallery and so on), so any section using the PLAIN
   .project__section class on its own, most visibly the Film heading
   right after the hero on four different layouts, had no background of
   its own and let the pinned hero paint straight through its heading and
   padding. Covering .project__section itself here, rather than naming
   each variant one at a time, closes this for every current use and any
   layout added later.

   .project-marquee swapped for .project-marquee-wrap, and .project__related
   swapped for .project__related-wrap, 7 Sep 2026 — client reported the
   bug AGAIN, live, after both previous passes above, with a screenshot
   of /work/axa/ showing the pinned hero visibly bleeding through directly
   around the marquee's own text line. Real root cause, missed by both
   earlier passes: .project-marquee's (and .project__related's) OUTER
   spacing was a margin, not padding — background-color only ever paints
   an element's own border/padding/content box, NEVER its margin, so
   adding a class to this list makes ITS OWN box opaque but does nothing
   for empty margin space around it, which is exactly what was still
   showing the pinned hero through. inc/project-components.php now wraps
   the marquee (and templates/project.php wraps Related Work) in a plain
   div that carries that same spacing as padding instead, so the space is
   inside a painted box this time — see the wrapper comment at each call
   site for the full explanation. .project-media-row had the identical
   margin-not-padding issue and is fixed directly below (no wrapper
   needed there — it has no border to reposition, so its vertical margin
   folds straight into its existing padding rule with zero visual
   change). */
.project__intro,
.project__body,
.project__gallery,
.project-media-row,
.project-marquee-wrap,
.project-hero--inline,
.project__related-wrap,
.project-chapter,
.project__section {
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
}

/* ⚠️ FOURTH pass on this exact bug, 7 Sep 2026 — client reported it AGAIN,
   live, on Alpha and other pages that were already fixed by the three
   passes documented above, this time as a gap that appears further down
   the page while scrolling, on the left/right edges rather than top or
   bottom. Real root cause, missed by all three earlier passes: every
   block in the list above is only as WIDE as its own max-width content
   box (var(--container-max), centred with margin-inline:auto) — its
   background is genuinely opaque, but only across that centred box, not
   the full browser width. On any window wider than that max-width (a
   normal desktop monitor easily exceeds it), the two leftover strips
   either side of the centred content are still just the plain, unstyled
   <article>/<body> background showing through, and the pinned hero
   sitting underneath paints straight into them as the page scrolls —
   for the ENTIRE height of every one of these sections, not a single
   thin margin gap like before. This is why hard refreshes, clearing
   cache and private windows never fixed it: it isn't a caching issue at
   all, it's that the fix so far only ever widened each element's OWN
   opaque box, never the two gutters beside it.

   Fixed here with the standard "full-bleed section, centred content"
   technique: a ::before pseudo-element on each of the same blocks,
   sized to the full viewport width and centred behind the element's own
   (still max-width, still padded) content, painted at z-index:-1 so it
   stays behind that content but — because it lives inside the parent's
   own z-index:1 stacking context — still stays above the pinned hero's
   z-index:0 underneath. html already has overflow-x:clip (see base
   reset), so the 100vw-wide pseudo-element can never cause a horizontal
   scrollbar even on a page with a vertical scrollbar of its own. */
.project__intro::before,
.project__body::before,
.project__gallery::before,
.project-media-row::before,
.project-marquee-wrap::before,
.project-hero--inline::before,
.project__related-wrap::before,
.project-chapter::before,
.project__section::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  background: var(--colour-bg);
  z-index: -1;
}

.project__intro {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-3) var(--container-pad) var(--space-4);
}

.project__title {
  font-size: var(--step-display);
  max-width: 20ch;
  margin-bottom: var(--space-3);
}

.project__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--colour-line);
  margin-bottom: var(--space-3);
}

.project__meta-item dt {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-1);
}

.project__meta-item dd {
  font-size: var(--step-body);
  margin: 0;
}

.project__meta-item--services {
  flex: 1 1 20rem;
}

.project__summary {
  font-size: var(--step-lead);
  max-width: 42ch;
  color: var(--colour-ink-soft);
}

/* Full-bleed + pinned, same treatment and reasoning as
   .project-hero--pinned above (added 15 Aug 2026) — this is
   brand-reveal's and product-story's own hero mode (a static image
   instead of video), and needed the identical pin-and-cover movement,
   not a smaller contained image like before. */
.project__hero {
  max-width: none;
  margin: 0;
  position: sticky;
  top: 0;
  z-index: 0;
  isolation: isolate;
  min-height: 100svh;
  min-height: 100dvh;
  overflow: hidden;
}

.project__hero img,
.project__hero picture {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
}

.project__body {
  max-width: 48rem;
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.project__section {
  padding-block: var(--space-4);
  border-top: 1px solid var(--colour-line);
}

.project__section h2 {
  font-size: var(--step-h2);
  margin-bottom: var(--space-2);
  max-width: 16ch;
}

.project__section p {
  max-width: 60ch;
}

.project__section p + p {
  margin-top: var(--space-2);
}

/* Groups one multi-video-grid chapter's heading+text+video into a single
   visual unit (templates/project-layouts/multi-video-grid.php), added
   21 Aug 2026 — client feedback: with heading+text and video as separate,
   evenly-spaced siblings, a caption looked just as "attached" to the
   video before it as the one after it ("titles are not clear... they
   look like they are for the next film, not the one before"). The
   ordering (caption-then-video, reinstated the same day per client
   request) isn't what fixes that on its own — this wrapper is: ONE
   divider on the outside of the whole group (not on the inner text
   section, which would otherwise look like ITS OWN section start), and
   the inner section's own bottom padding removed so the caption sits
   tight against its own video immediately below, while the video's
   existing bottom margin (.project-hero--inline) provides the larger gap
   before the next chapter's divider. That makes the pairing
   unambiguous regardless of scroll position or viewport width. */
.project-chapter {
  border-top: 1px solid var(--colour-line);
  padding-top: var(--space-4);
}

.project-chapter .project__section {
  border-top: none;
  padding-block: 0 var(--space-2);
}

/* Found and fixed, 18 Aug 2026 (client: "have to scroll so much to the
   right to find anything," page cut off on mobile) — the negative
   margin below was written to cancel out a PARENT's padding, breaking
   the gallery out to full-bleed from inside a narrower reading column.
   But every one of the 8 layouts (templates/project-layouts/*.php)
   actually calls the gallery at the TOP level of the page, as a
   sibling of .project__body, never nested inside it — so there was no
   parent padding to cancel, and the negative margin instead pulled the
   gallery straight past the edge of the viewport itself (confirmed:
   .project__gallery--grid measured 48px wider than the viewport on
   both sides, exactly matching --container-pad's value at that width).
   Harmless-looking on a wide desktop window where the extra width
   silently ran off the edge of the screen with nothing else to notice
   it against, but a real, forced horizontal scrollbar on any narrower
   one — which is also what makes a mobile browser zoom the whole page
   out to compensate, reading as "everything's too small" independently
   of this. Fixed by giving it the exact same
   max-width/margin-inline/padding-inline pattern every other top-level
   section on this page already uses (.project__intro etc.) instead of
   a breakout meant for a context that never actually exists here. */
.project__gallery {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.project__gallery-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
}

@media (min-width: 48rem) {
  .project__gallery-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

.project__gallery-item figcaption {
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  padding-top: var(--space-1);
}

.project__credits dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
}

.project__credit {
  display: contents;
}

.project__credit dt {
  color: var(--colour-ink-soft);
}

/* Related work spans full width again */
/* .project__related-wrap carries the space-4 gap this used to add via
   margin-top (see the opaque-background list's comment in section 8) —
   .project__related itself is otherwise unchanged: same border-top,
   same inner padding-top, so the line sits in exactly the same place
   as before. */
.project__related-wrap {
  padding-top: var(--space-4);
}

.project__related {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-4) var(--container-pad) 0;
  border-top: 1px solid var(--colour-line);
}

.project__related h2 {
  font-size: var(--step-h2);
  margin-bottom: var(--space-3);
}

.project__related-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}

@media (min-width: 48rem) {
  .project__related-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.project__related-link {
  display: block;
}

.project__related-link img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  margin-bottom: var(--space-1);
  transition: opacity 0.2s ease;
}

.project__related-link:hover img,
.project__related-link:focus-visible img {
  opacity: 0.85;
}

.project__related-title {
  display: block;
  font-weight: 600;
}

.project__related-client {
  display: block;
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

/* ---------------------------------------------------------------
   9. Video facade
   --------------------------------------------------------------- */

.video-facade {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--colour-surface);
  overflow: hidden;
}

.video-facade picture,
.video-facade img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Portrait modifier (added 22 Sep 2026, BBC: Digital Posters — client's
   own source footage for this project is genuinely portrait, 576x1024,
   not the site's usual landscape 16:9). Set 'orientation' => 'portrait'
   on any project's video block (inc/media.php's cumi_video_facade()) to
   get this instead of the default box — reusable for any future project
   with real vertical source video, no per-project CSS needed.
   .project-hero--inline .video-facade already forces 16/9 with higher
   specificity (two classes) than .video-facade--portrait alone, so this
   override repeats that same two-class shape to win over it; the
   standalone rule above covers a portrait facade used anywhere else. */
.video-facade--portrait,
.project-hero--inline .video-facade--portrait {
  aspect-ratio: 9 / 16;
  max-width: 28rem;
  margin-inline: auto;
}

.video-facade__play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: clamp(3.5rem, 3rem + 2vw, 5rem);
  height: clamp(3.5rem, 3rem + 2vw, 5rem);
  border-radius: 50%;
  background: var(--colour-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.2s ease;
  /* Explicit z-index added 15 Aug 2026 alongside the .video-facade__bg-*
     fix above — once that background layer has its own explicit
     z-index, it always paints above any sibling still on the default
     z-index:auto (DOM order stops applying once z-index values are
     mixed), so the play button needs its own explicit value higher
     than the background's (1) to stay on top and clickable. */
  z-index: 2;
}

.video-facade__play:hover,
.video-facade__play:focus-visible {
  transform: scale(1.06);
}

.video-facade__play-icon {
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0.6em 0 0.6em 1em;
  border-color: transparent transparent transparent var(--colour-ink);
  margin-left: 0.15em;
}

.video-facade__noscript {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
  text-decoration: underline;
}

.video-facade__iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Added 6 Sep 2026 alongside the temporary self-hosted stand-in path
   (assets/js/video-facade.js, inc/media.php's cumi_video_facade()) — the
   real <video> with native controls that plays in place of the Vimeo
   iframe above for a project with no vimeo_id yet. object-fit: contain
   rather than cover, unlike the poster underneath: cropping into a real
   film with its own native control bar reads wrong in a way it doesn't
   for a decorative poster photo, so this letterboxes on an aspect ratio
   mismatch instead. */
.video-facade__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
  border: 0;
}

/* Autoplay upgrade (assets/js/project.js) — a silent self-hosted loop or
   a muted Vimeo "background" embed sitting behind the same play button,
   which now means "watch with sound" instead of "start playing". Only
   ever applied by JS, and only when prefers-reduced-motion allows it —
   see project.js. Without JS, or with reduced motion, the facade above
   is exactly what renders: poster + play button, nothing autoplays. */
.video-facade__bg-video,
.video-facade__bg-iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Cancels the sitewide "img, svg, video, iframe { max-width: 100% }"
     reset (section 1) for this element specifically — found 19 Aug 2026,
     client's own instinct ("maybe it's a video size thing... limitations
     on the size of the video") was exactly right. That reset is correct
     and wanted everywhere else on the site (stops any image/video/iframe
     ever overflowing its own box), but project.js's sizeBackgroundIframe()
     deliberately sets this ONE iframe's width LARGER than its container on
     purpose — that's the entire mechanism behind cropping it to fill the
     screen without gaps (see that function's own comment). max-width
     always wins over width regardless of how width was set — even a JS
     inline style with far higher specificity/priority still gets silently
     clamped back down to the container's own width by max-width — so
     every oversized width that script calculated was being capped back to
     exactly 100%, leaving the video too small for its box and exposing
     the plain background around it (the "borders above and below" the
     client saw) — worse the further the box's shape differed from the
     video's own 16:9, which lines up with it only showing at certain
     window sizes rather than being reliably reproducible at any one. */
  max-width: none;
  object-fit: cover;
  border: 0;
  pointer-events: none;
  /* z-index added 15 Aug 2026 — real bug found live on ninakessler.uk:
     assets/js/project.js inserts this element as the facade's FIRST
     child (facade.insertBefore(background, facade.firstChild)), which
     puts the existing poster <picture> AFTER it in DOM order. With both
     at the default z-index:auto, later-in-DOM wins the paint order, so
     the still-visible poster image was sitting on top of the actually-
     playing video/iframe underneath it — the upgrade was working the
     entire time, just invisibly. Explicit z-index here guarantees the
     video always paints above the poster regardless of DOM order. */
  z-index: 1;
}

/* No CSS rule for the pinned hero's .video-facade__bg-iframe sizing
   here — that used to live in this file as a pure-CSS "oversize using
   vw/vh units + center" hack (Safari/iOS doesn't support object-fit on
   <iframe> at all, so Vimeo's background embed needs SOME manual
   cropping technique). Removed 18 Aug 2026: it assumed the pinned
   hero's box is always exactly the size of the viewport, which isn't
   reliably true across browsers (scrollbar width, a mobile browser's
   toolbar changing the real viewport height after the page has already
   loaded, zoom level) — client reported the video rendering cut off in
   a corner rather than covering the screen, on both desktop and
   mobile, which this assumption breaking down explains. Replaced with
   JS that measures the facade's own actual rendered box directly
   (assets/js/project.js, sizeBackgroundIframe()) and sizes/centres the
   iframe against those real numbers instead of assumed viewport units
   — recalculated on resize too, so it can't drift out of sync with
   whatever the box actually turns out to be, in any browser. The base
   .video-facade__bg-video/__bg-iframe rule above (width/height: 100%)
   is what actually renders until that script sets its own inline
   sizing on top, and stays the used value for the self-hosted <video>
   path unconditionally — <video> supports object-fit everywhere
   already, it never needed this. */

.video-facade--autoplay .video-facade__play {
  width: clamp(2.75rem, 2.4rem + 1.2vw, 3.5rem);
  height: clamp(2.75rem, 2.4rem + 1.2vw, 3.5rem);
  bottom: var(--space-2);
  right: var(--space-2);
  top: auto;
  left: auto;
  margin: 0;
  background: rgba(255, 255, 255, 0.9);
}

/* ---------------------------------------------------------------
   9b. Project-page layout components (14 Aug 2026)
   Shared across the 8 layouts in templates/project-layouts/ — see
   inc/project-components.php for the PHP that outputs this markup.
   --------------------------------------------------------------- */

/* Pinned full-bleed hero, added 15 Aug 2026 (client feedback: wanted
   the reference's own pin-and-cover movement — see
   plusoneamsterdam.com/edge-net-zero and /spdrs, both built on the
   identical position:sticky idea, confirmed by inspecting their live
   DOM — rather than a hero that just scrolls away like a normal block).
   Exact same technique already proven on the homepage (.hero, section
   13 below): sticky + top:0 + an explicit z-index so later content can
   deliberately paint above it (see the shared .project__intro /
   .project__body / etc. rule just above section 8, mirroring
   .work/.disciplines/.about/.site-footer's treatment on the homepage)
   — isolation:isolate scopes that z-index battle to this element's own
   descendants only, so it can't leak out and affect unrelated stacking
   elsewhere on the page. */
/* min-height moved onto THIS wrapper directly (17 Aug 2026) — it used
   to live on .video-facade below instead, which meant the pinned box's
   real height depended on a descendant that video-facade.js later
   wipes and rebuilds (facade.innerHTML = '' when the play button is
   clicked, swapping the autoplay poster/loop for the real Vimeo
   player). Client found that clicking play snapped the video into the
   right place but broke the header back to solid/huge — the hero had
   briefly lost its height during that swap in at least one browser
   (Firefox), which was enough to un-stick the sticky positioning and
   throw off the scroll-based header/solid IntersectionObserver in
   project.js, which watches where .project__intro actually sits.
   Giving the wrapper its own guaranteed height, independent of
   whatever's currently inside .video-facade, means that swap can never
   affect this element's size again, in any browser. */
.project-hero--pinned {
  max-width: none;
  margin: 0;
  position: sticky;
  top: 0;
  z-index: 0;
  isolation: isolate;
  min-height: 100svh;
  min-height: 100dvh;
}

.project-hero--pinned .video-facade {
  aspect-ratio: auto;
  position: absolute;
  inset: 0;
}

/* REVERSED 21 Aug 2026 — this gap below every inline video used to be a
   bottom MARGIN specifically so the still-playing pinned/sticky hero
   behind it would show through as the page scrolled past (client's own
   "see-through, shows the hero playing" request, 19 Aug 2026). Client
   has since asked for solid white here instead: "remove the see through
   sections between the videos... just be solid white... might be
   cleaner". Padding (not margin) makes the gap part of this element's
   own opaque background (.project-hero--inline already gets
   background: var(--colour-bg) from the shared rule above, so a
   padding-bottom area paints solid over whatever's stacked behind it,
   where a margin never would). */
.project-hero--inline {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad) var(--space-5);
}

/* The "watch the full film" section (cumi_project_film(), inc/
   project-components.php) already gets solid white space above it (the
   header's own padding-bottom, --space-4, cumi_project_header()/
   .project__intro) — this combined selector (both classes on the one
   element — see cumi_project_film()) adds the same amount again below,
   so the film sits in a symmetrical white frame. Now that the base rule
   above is solid too, this is simply extra breathing room for that one
   section, not a special case any more. */
.project-hero--inline.project-hero--film {
  padding-bottom: calc(var(--space-5) + var(--space-4));
}

.project-hero--inline .video-facade {
  aspect-ratio: 16 / 9;
}

/* Visible caption under the play button — icon-only everywhere else
   (hero, homepage video) by design, but this smaller in-page block
   isn't obviously "a video" without one (client feedback, 19 Aug 2026:
   "nothing on there to say 'Play full video'"). Only renders when
   cumi_video_facade() is called with a $play_label — currently just
   cumi_project_film(). Positioned below the centred play button rather
   than inside it, so the button's circular shape/size is unaffected
   everywhere else that reuses it. */
.video-facade__play-label {
  position: absolute;
  left: 50%;
  top: calc(50% + (clamp(3.5rem, 3rem + 2vw, 5rem) / 2) + var(--space-2));
  transform: translateX(-50%);
  z-index: 2;
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: #fff;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.65);
  white-space: nowrap;
  pointer-events: none;
}

/* Ambient preview mosaic (Layout 9's small unlabelled preview grid) was
   removed 21 Aug 2026 at the client's explicit request — live testing
   showed it duplicated every film on the page (once unlabelled here,
   again fully labelled in the full stacked list below), which read as
   confusing rather than helpful. See templates/project-layouts/
   multi-video-grid.php's own doc comment for the full story. The
   .project-video-mosaic rules that used to live here were removed along
   with the markup/PHP that rendered it — nothing on the site emits that
   class any more. */

/* Media row — 1-3 images side by side (the PlusOne "column-set" beat).
   Contained like the page's other mid-width blocks (wider than the
   48rem reading column, not full-bleed like the hero/marquee/gallery). */
.project-media-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
  max-width: var(--container-max);
  /* 7 Sep 2026 — was `margin: var(--space-4) auto`. The horizontal half
     (auto) stays as margin, it only centres the box and never creates a
     paint-through gap; the vertical half (space-4) is now padding
     instead, folded into the padding shorthand below, because margin is
     never covered by this element's own opaque background (see the
     opaque-background list's comment in section 8 for the full
     explanation) and was letting the pinned hero bleed through above
     and below every media row. No border here to reposition, so this
     is a like-for-like swap with no visible layout change. */
  margin-inline: auto;
  padding: var(--space-4) var(--container-pad);
}

@media (min-width: 48rem) {
  .project-media-row--2 {
    grid-template-columns: repeat(2, 1fr);
  }
  .project-media-row--3 {
    grid-template-columns: repeat(3, 1fr);
  }
}

.project-media-row__item img,
.project-media-row__item picture {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* Scrolling statement banner — pure CSS, paused/static automatically
   under prefers-reduced-motion via the sitewide rule in §4 above.

   .project-marquee-wrap added 7 Sep 2026 — carries the space-4 gap this
   section used to add via margin-block (see the opaque-background list
   above for the full root-cause explanation); .project-marquee itself
   is otherwise completely unchanged, same border + inner padding as
   always, so the visible layout is identical to before. */
.project-marquee-wrap {
  /* position/z-index/background come from the shared opaque-background
     list above (section 8) — this rule only adds the wrap's own spacing. */
  padding-block: var(--space-4);
}

.project-marquee {
  overflow: hidden;
  padding-block: var(--space-3);
  border-top: 1px solid var(--colour-line);
  border-bottom: 1px solid var(--colour-line);
}

.project-marquee__track {
  display: flex;
  width: max-content;
  gap: var(--space-4);
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: var(--step-h2);
  font-weight: 500;
  animation: project-marquee-scroll 28s linear infinite;
}

@keyframes project-marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Bold standalone closing statement (Digital Art layout) */
.project-statement {
  font-size: var(--step-h1);
  max-width: 24ch;
  margin-block: var(--space-4);
}

/* Gallery variants — the same .project__gallery-grid/.project__gallery-item
   markup as the original single-layout gallery (§8 above), reshaped per
   type. 'grid' needs no extra rules; it's already the default. */

.project__gallery--justify .project__gallery-grid,
.project__gallery--slideshow .project__gallery-grid {
  display: flex;
  grid-template-columns: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: var(--space-2);
  gap: var(--space-2);
  -webkit-overflow-scrolling: touch;
}

.project__gallery--justify .project__gallery-item {
  flex: 0 0 auto;
  scroll-snap-align: start;
  height: clamp(14rem, 12rem + 10vw, 24rem);
  width: auto;
}

.project__gallery--justify .project__gallery-item img,
.project__gallery--justify .project__gallery-item picture {
  height: 100%;
  width: auto;
  max-width: none;
}

.project__gallery--slideshow .project__gallery-item {
  flex: 0 0 min(85vw, 60rem);
  scroll-snap-align: center;
}

.project__gallery--slideshow .project__gallery-item img,
.project__gallery--slideshow .project__gallery-item picture {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.project__gallery--columnized .project__gallery-grid {
  display: block;
  column-count: 1;
  column-gap: var(--space-2);
}

@media (min-width: 48rem) {
  .project__gallery--columnized .project__gallery-grid {
    column-count: 2;
  }
}

@media (min-width: 64rem) {
  .project__gallery--columnized .project__gallery-grid {
    column-count: 3;
  }
}

.project__gallery--columnized .project__gallery-item {
  break-inside: avoid;
  margin-bottom: var(--space-2);
}

/* ---------------------------------------------------------------
   10. Stub / placeholder pages (removed as real templates land)
   --------------------------------------------------------------- */

.stub {
  max-width: 42rem;
  margin-inline: auto;
  padding: var(--space-5) var(--container-pad);
}

.stub__eyebrow {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — same sitewide eyebrow fix, see
     .section-eyebrow above. */
  font-size: var(--step-lead);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.1em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.stub h1 {
  font-size: var(--step-h1);
  margin-bottom: var(--space-2);
}

.stub p {
  max-width: 55ch;
  margin-bottom: var(--space-2);
}

.stub__note {
  color: var(--colour-ink-soft);
  border-left: 2px solid var(--colour-line);
  padding-left: var(--space-2);
}

.stub__project-list li {
  border-top: 1px solid var(--colour-line);
}

.stub__project-list a {
  display: block;
  padding-block: var(--space-2);
  min-height: 44px;
}

/* ---------------------------------------------------------------
   11. 404
   --------------------------------------------------------------- */

.not-found {
  max-width: 34rem;
  margin-inline: auto;
  padding: var(--space-5) var(--container-pad);
  text-align: left;
}

.not-found__eyebrow {
  font-size: var(--step-display);
  color: var(--colour-line);
  margin-bottom: var(--space-2);
}

.not-found h1 {
  font-size: var(--step-h1);
  margin-bottom: var(--space-2);
}

.not-found__links {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.not-found__links a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* ---------------------------------------------------------------
   12. Homepage — overlay header
   The header (inc/header.php) is shared and unchanged; on the
   homepage only, it floats transparently over the hero and turns
   solid once scrolled past (assets/js/home.js toggles the
   .site-header--solid class via IntersectionObserver — no scroll
   listener, no scroll-jacking, native scrolling untouched).
   Every other template keeps the header's normal in-flow, solid,
   bordered default.
   --------------------------------------------------------------- */

/* Root cause of both the "white writing on white background" report and
   the "white band + black bar above the video" report: the base header
   rule (section 5) is position:sticky, which keeps it in normal document
   flow — it still reserves its own real height at the top of the page,
   background or not. On every other template that's correct (there's no
   full-bleed hero to sit over). On the homepage it meant <main>, and so
   .hero, only started AFTER that reserved header-height gap — you were
   seeing the plain white <body> background through the "transparent"
   header, not the video, and the header's white-styled text sat on that
   white gap rather than on footage. position:fixed removes the header
   from flow entirely so .hero can start at true y=0 and run full-bleed
   underneath it, on the homepage only. */
body.template-home .site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  border-bottom-color: transparent;
  /* Frozen scrim colour (see section 2's --colour-scrim-dark/light
     comment) — this is the transparent-over-showreel state's own
     colour, tracking the footage's brightness rather than the
     visitor's system theme, so it must not flip with dark mode. */
  color: var(--colour-scrim-light);
  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

body.template-home .site-header .site-logo__img {
  transition: filter 0.3s ease;
}

/* ⚠️ FIXED 15 Sep 2026 — client report: "why is the LOGO white when I
   gave you a black one? It's white on white instead of black like it
   should be. I have to scroll down to have the logo go black as it
   should be." This rule used to force the logo to
   `filter: brightness(0) invert(1)` (white) any time the header was
   transparent-over-hero, on the same "assume the showreel is always
   dark enough for white-on-transparent to read" assumption already
   found and fixed for the nav links/menu icon just above (15 Sep 2026,
   "change it from white to black") and for .hero__title (14 Aug 2026)
   — this logo rule was simply missed in that same pass. The current
   showreel's actual opening frame is a near-white radial gradient
   (right where the logo sits, top-left), so a white logo was invisible
   against it — exactly the reported "white on white", not a caching or
   asset problem. Fix: stop inverting the logo at all: it now stays its
   natural black, same as the nav text, and gets the same contrast
   guarantee from the drop-shadow filter on .site-header__inner two
   rules down (already proven to work for black-on-bright-frame text).
   No filter is applied here any more — .site-header--solid's own
   `filter: none` rule further down is therefore also a no-op now, but
   left in place as it's harmless and self-documenting. */

/* Scoped to the desktop bar's own top-level links only (direct <li>
   children) — NOT ".site-nav__list a" generally, which would also
   match the mobile popup and the "Work" dropdown, both of which have
   their own solid white backgrounds and must always stay ink-on-white
   regardless of what the transparent header behind them is doing.
   Changed from white (var(--colour-bg)) to black (var(--colour-ink))
   15 Sep 2026 (client instruction: "change it from white to black"),
   matching the same white->black change already made to .hero__title
   on 14 Aug 2026. Same caveat as that change and as the hero scrim
   removal right below this block: the drop-shadow two rules down was
   tuned to make WHITE content pop against dark video (a dark shadow
   behind light content reads on any frame); it does nothing for BLACK
   content over a bright frame. Revisit with a light backing behind the
   nav/icon if any part of the new footage makes this hard to read. */
body.template-home .site-header:not(.site-header--solid) .site-nav--desktop .site-nav__list > li > a,
body.template-home .site-header:not(.site-header--solid) .site-nav__toggle {
  /* Frozen scrim colour, not theme-tracking — see section 2's
     --colour-scrim-dark/light comment. This exact rule is the one the
     15 Sep 2026 fix above is about: black was chosen because the
     showreel's own frames are sometimes pale, nothing to do with the
     visitor's system theme, so dark mode must not undo it by flipping
     --colour-ink light. */
  color: var(--colour-scrim-dark);
}

/* Originally added to guarantee the (then white) logo/links/menu icon
   stayed visible over ANY showreel frame, not just the darker ones
   (client report, 17 Aug 2026 — "on mobile I don't even have a menu
   showing"; the real cause was relying on the footage always being
   dark enough for white-on-transparent to read, with no fallback for
   lighter frames/moments — not a caching or code-visibility bug, the
   menu icon really was rendering, just effectively invisible: white on
   white). ⚠️ Logo and nav text are both black/ink now (15 Sep 2026
   fixes, see the two rules above), and a dark drop-shadow does NOT
   give black content the same all-frames guarantee it gave white
   content — it adds a touch of definition over light/bright frames but
   does nothing over a dark one (same open caveat already flagged in
   the comment above the nav-colour rule). Left in place because it's
   still a net improvement over nothing, not because it fully solves
   dark-on-dark; revisit with a light backing behind the header content
   if a dark stretch of the current showreel ever makes it unreadable.

   ⚠️ REPLACED 15 Sep 2026 — client report: the nav menu font looked
   "hazzy"/blurred instead of crisp like the rest of the site, AND
   there was still "grey stuff" sitting over the top of the homepage
   even after the separate grey-gradient scrim was removed (see the
   "REMOVED 14 Sep 2026" comment further down — that was a different,
   already-fixed source of grey; this was a second, undiagnosed one).
   Root cause of both reports at once: the rule below used to put
   `filter` on the whole .site-header__inner CONTAINER rather than on
   one element. A CSS `filter` forces the browser to rasterise
   everything inside that container into a single offscreen layer
   before painting the shadow — including the live nav text — so the
   nav font was being redrawn through that rasterised layer instead of
   with the browser's normal crisp glyph rendering; exactly the
   reported "hazzy" look. The same two stacked shadows (one with an
   8px blur radius), spread across the full width of the header, is
   also what read as a soft grey wash sitting over the video.
   Fix: give contrast to each child individually instead of filtering
   the whole container. Text gets `text-shadow` below (shadows the
   glyphs without re-rasterising them, so it stays crisp).

   ⚠️ REMOVED 15 Sep 2026 (later same day) — client report: "still a
   fuzzy black around the header... not crisp and nice." The fix above
   solved the text/wide-container blur, but the logo and menu-icon each
   still carried their own scoped `filter: drop-shadow(...)`, and a CSS
   drop-shadow is inherently a BLURRED shadow — zoomed screenshots (3x
   scale) confirmed a genuine soft dark halo bleeding around the crisp
   logo shape, smaller in area than the original bug but the same kind
   of fuzz the client keeps flagging. Since this drop-shadow only ever
   gave a partial contrast guarantee anyway (see this rule's own former
   comment: "does NOT give the same all-frames guarantee... adds a
   touch of definition over light frames but does nothing over a dark
   one"), and the client has now flagged blur here twice, removed
   outright rather than tuned further. Logo and menu icon render at
   their natural crisp edges again; contrast against the showreel is
   whatever it is, same tradeoff the site already accepts elsewhere.

   ⚠️ REMOVED 16 Sep 2026 — client report: "when you land on [the
   homepage] there is a drop shadow that hasn't been removed... once you
   scroll down its good but when you land its hazy." This rule below is
   the actual, still-live cause, missed in the two rounds above: it's a
   dark text-shadow (rgba(0,0,0,0.55)/(0,0,0,0.35), 8px blur) — left over
   from when this same nav text was WHITE (a dark halo genuinely helps
   white text pop against a light video frame). The nav/CTA text colour
   was separately changed white→black two rules above this same day
   (client instruction: "change it from white to black"), but this
   shadow's own colour was never revisited for that change. A dark blur
   behind BLACK text on a light background gives no contrast benefit at
   all — it's pure haze, confirmed live via getComputedStyle on the
   deployed site (textShadow present pre-scroll, gone the instant
   `.site-header--solid` applies post-scroll — exactly matching "hazy on
   landing, fine after scrolling", nothing to do with the showreel
   footage). Removed outright, same call already made for the logo/menu-
   icon's own drop-shadow above: nav/CTA text now renders at its natural
   crisp black in both states, pre- and post-scroll alike; contrast
   against the showreel is whatever it is, same accepted tradeoff. */
body.template-home .site-header:not(.site-header--solid) .site-nav--desktop .site-nav__list > li > a,
body.template-home .site-header:not(.site-header--solid) .site-header__cta {
  text-shadow: none;
}

/* CTA over the transparent hero: white border/text (inherits via
   currentColor from the rule above), hover inverts to a white fill with
   ink text instead of the default ink-fill/white-text hover, since the
   header itself is still transparent here. */
body.template-home .site-header:not(.site-header--solid) .site-header__cta:hover,
body.template-home .site-header:not(.site-header--solid) .site-header__cta:focus-visible {
  /* Frozen scrim colours, not theme-tracking — see section 2's
     --colour-scrim-dark/light comment; same transparent-over-showreel
     context as the rules above. */
  background: var(--colour-scrim-light);
  color: var(--colour-scrim-dark);
}

body.template-home .site-header--solid {
  background: var(--colour-bg);
  border-bottom-color: var(--colour-line);
  color: var(--colour-ink);
}

body.template-home .site-header--solid .site-logo__img {
  filter: none;
}

/* The mobile disclosure panel always has its own solid white
   background (see section 5), so its text must always stay dark —
   independent of whether the bar behind it is currently transparent
   or solid. */
.site-nav__list--mobile {
  color: var(--colour-ink);
}

/* If JavaScript never runs, the header simply stays in its
   transparent/white-text state permanently. That's still fully
   readable over the hero; it just never gains the solid background
   further down the page — an acceptable, non-blocking degradation
   (brief §16: site must stay usable if animation scripts fail). */

/* ---------------------------------------------------------------
   12b. Project pages — overlay header + back link
   Added 15 Aug 2026 (client feedback: the header sat in its normal
   solid bar ABOVE the project hero instead of overlaying it, unlike
   the reference and unlike the homepage). Same floating-then-solid
   idea as section 12 above, scoped to project pages that actually
   have a hero (the 'has-hero' body class — see templates/project.php;
   two of the 8 layouts never render one at all, so this must never
   apply unconditionally to every .template-project page or a
   heroless layout would be left with a permanently transparent header
   over blank white — the exact historical bug documented in section 12).

   Deliberately uses dark ink text/logo here rather than the homepage's
   white-inverted treatment: the example projects' hero images are all
   pale placeholder graphics right now, so white text would be close to
   invisible — and it also matches the client's own already-confirmed
   preference for dark hero text over white on the homepage (14 Aug
   2026 decision, see DECISIONS.md/session notes), rather than
   introducing a third convention. Worth revisiting once real project
   photography/footage exists and it's clear whether dark text stays
   legible against it.

   The project hero itself is a plain in-flow block, NOT position:sticky
   like the homepage's — so none of section 13's stacking-context
   gymnastics are needed here; a fixed header with a high z-index simply
   sits on top of normally-flowing content underneath, no extra
   position/z-index treatment required on later sections. */
body.template-project.has-hero .site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  border-bottom-color: transparent;
  transition: background-color 0.3s ease, border-color 0.3s ease;
}

body.template-project.has-hero .site-header--solid {
  background: var(--colour-bg);
  border-bottom-color: var(--colour-line);
}

/* Same contrast guarantee as the homepage's section 12 (see its own
   note, 17 Aug 2026) — dark ink logo/links/menu icon over a hero that
   won't always be pale placeholder art forever (this file's own
   original comment above already flagged that as an open risk once
   real footage/photography lands). Light drop-shadow instead of dark,
   since this state is dark-on-transparent rather than white-on-
   transparent. */
body.template-project.has-hero .site-header:not(.site-header--solid) .site-header__inner {
  filter: drop-shadow(0 1px 2px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 8px rgba(255, 255, 255, 0.55));
}

/* ⚠️ ADDED 16 Sep 2026, alongside real dark-mode support (section 2's
   --colour-scrim-dark/light) — this state never had its own explicit
   `color`, so it was quietly inheriting plain `var(--colour-ink)` from
   <body>. That's correct in light mode (reads as the same dark ink the
   comment above describes) but would flip light the moment dark mode
   redefines --colour-ink, reopening exactly the "pale placeholder hero
   art" contrast problem that comment flags — over a photo/video hero,
   same as the homepage's equivalent state a few lines up. Pinning it
   explicitly to the frozen scrim colour keeps this identical in both
   themes, same as everywhere else that composites against footage. */
body.template-project.has-hero .site-header:not(.site-header--solid) {
  color: var(--colour-scrim-dark);
}

/* ⚠️ MOVED 21 Sep 2026 (client report: "the back button under WORK
   sometimes disappears... put the back button in the sticky header so
   you can always find it"). Used to be .project__back — a block-level
   link positioned over the hero on heroed project pages (position:
   absolute, calculated to clear the logo), which scrolled away with the
   rest of the page exactly like everything else in normal flow, on
   every project page, heroed or not. That entire old ruleset (including
   the .project positioning-context rule it needed) is gone; it's
   replaced by this, a flex child of .site-header__inner itself (see
   inc/header.php) — the same element that's already sticky on every
   ordinary page and fixed-to-the-viewport on heroed project pages (this
   section, further up), so the back link now shares its parent's
   always-visible behaviour instead of trying to reimplement it.
   Deliberately no extra color/contrast rule for the transparent-over-
   hero state: living inside .site-header__inner, it already inherits
   both the drop-shadow legibility filter AND the scrim colour that
   state applies to that whole element (this section, above) — same
   free contrast the logo and nav links get, nothing bespoke needed. */
/* ⚠️ REPOSITIONED 22 Sep 2026 (client feedback: "the position of the
   back button is not great, can it be under the logo?"). .site-logo and
   .site-header__back are now grouped in this column wrapper instead of
   sitting side by side as two of .site-header__inner's own flex
   children — that's what actually moves the link underneath the logo
   rather than next to it. .site-header__inner is back to exactly two
   flex children (this wrapper + .site-header__actions), so its own
   justify-content: space-between still does all the work of pushing the
   actions group to the far right, same as before the back link existed
   — no auto-margin trick needed any more. align-items: flex-start keeps
   both children hugging the left edge rather than centring/stretching
   to the wrapper's own width. */
.site-header__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}

.site-header__back {
  display: inline-flex;
  align-items: center;
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: var(--colour-ink-soft);
  white-space: nowrap;
}

.site-header__back:hover,
.site-header__back:focus-visible {
  color: var(--colour-ink);
  text-decoration: underline;
}

/* ---------------------------------------------------------------
   13. Homepage — hero / showreel
   --------------------------------------------------------------- */

/* Pinned-hero scroll (matches the plusoneamsterdam.com reference,
   13 Aug 2026 client request): the hero stays put while Selected Work
   scrolls up and visually covers it, rather than scrolling away like an
   ordinary block. position:sticky on the hero holds it at top:0 for as
   long as its parent (<main>, much taller than one viewport) has room;
   .work, positioned immediately after it with a higher z-index and an
   opaque background, slides up over it in completely normal document
   flow — no JS, no scroll-jacking. Once .work has scrolled far enough
   to cover the viewport the hero is invisible underneath it, which is
   the same visual result as it "unsticking". */
.hero {
  position: sticky;
  top: 0;
  z-index: 0;
  min-height: 100svh;
  min-height: 100dvh;
  /* clip, not hidden (fixed 19 Aug 2026) — client reported the mobile
     menu only reachable after scrolling a long way right, on the
     homepage specifically, not on any project page. Root cause:
     overflow: hidden on the SAME element as position: sticky is a
     documented Safari/iOS bug — WebKit can fail to establish it as a
     proper clipping boundary for this exact combination, letting the
     decorative placeholder shapes below (.hero__placeholder-shape,
     sized in vmax and offset outside the box on purpose) paint past the
     hero's edges and widen the whole document instead of being
     contained — which is exactly what pushes the header/nav sideways
     with it, since position:sticky only pins vertically, not
     horizontally. clip fixes the same problem the html-level safety net
     below was already using clip for, for the same reason. */
  overflow: clip;
  /* Frozen scrim colours, not theme-tracking (section 2's
     --colour-scrim-dark/light) — this is the fallback fill/text colour
     behind and over the showreel video itself, so it must always read
     as "dark video, light text", regardless of the visitor's system
     theme. */
  background: var(--colour-scrim-dark);
  color: var(--colour-scrim-light);
  isolation: isolate;
}

.hero__media,
.hero__media video,
.hero__placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Hero showreel crop position — client-reported 7 Sep 2026: the current
   showreel footage (HOMEPAGE TOPBAR.mp4) keeps most of its animated
   content toward the right two-thirds of frame, with plain background
   filling the left. On a narrow phone viewport, object-fit: cover's
   default centred crop only shows a thin vertical strip around the
   video's horizontal centre — for several scenes that strip lands
   mostly on the empty background, making the video read as "not
   visible" even though it's actually playing. Shifting the crop
   further right (checked against real frames pulled from the film
   across several points in its 17.56s runtime) keeps the actual
   animated content in view. Desktop/tablet were already fine as
   reported, so this only applies below the 48rem tablet breakpoint. */
.hero__media video {
  object-position: 75% center;
}

@media (min-width: 48rem) {
  .hero__media video {
    object-position: center;
  }
}

.hero__placeholder--photo picture,
.hero__placeholder--photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Slow Ken Burns drift on the fallback poster photo, so the hero
   still reads as "alive" before real showreel footage exists. */
.hero__placeholder--photo img {
  animation: hero-photo-drift 26s ease-in-out infinite alternate;
}

@keyframes hero-photo-drift {
  from { transform: scale(1); }
  to   { transform: scale(1.08); }
}

/* No footage and no poster yet — decorative animated placeholder.
   Purely CSS, aria-hidden in the markup, upgrades automatically the
   moment real files exist (see cumi_hero_showreel() in inc/media.php). */
.hero__placeholder--motion {
  background: radial-gradient(120% 120% at 50% 15%, #171717 0%, #050505 75%);
}

.hero__placeholder-shape {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.35;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 70%);
}

.hero__placeholder-shape--a {
  width: 46vmax;
  height: 46vmax;
  top: -10%;
  left: -10%;
  animation: hero-shape-a 24s ease-in-out infinite;
}

.hero__placeholder-shape--b {
  width: 38vmax;
  height: 38vmax;
  bottom: -12%;
  right: -8%;
  animation: hero-shape-b 30s ease-in-out infinite;
}

.hero__placeholder-shape--c {
  width: 26vmax;
  height: 26vmax;
  top: 40%;
  left: 55%;
  animation: hero-shape-c 20s ease-in-out infinite;
}

@keyframes hero-shape-a {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(8%, 6%) scale(1.15); }
}

@keyframes hero-shape-b {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(-6%, -8%) scale(1.1); }
}

@keyframes hero-shape-c {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(-10%, 5%) scale(1.2); }
}

/* ⚠️ REMOVED 14 Sep 2026 — designer flagged a "grey gradient" washing
   over the homepage hero video and asked for it gone. This used to be
   two black linear-gradient scrims (top band for nav contrast, bottom
   band for H1/descriptor contrast) sized for WHITE hero text. The H1
   was switched to black on 14 Aug 2026 (see .hero__title's own comment
   below) and these scrims were never revisited then, so for the last
   month they've been doing nothing useful for legibility (a dark scrim
   doesn't help black text) while still visibly darkening the footage —
   exactly the unwanted grey wash reported. The top band was also
   already redundant: the sticky nav gets its own contrast from a
   drop-shadow filter on .site-header__inner (see that rule's comment,
   ~line 1600), added specifically so it wouldn't need this gradient.
   Removed outright rather than re-tinted, per the explicit request.

   Net effect: the video now shows completely clean, with nothing
   dimming it. The H1/descriptor are still black-on-video with no
   scrim or panel behind them (unchanged from the 14 Aug decision) —
   if a bright/light frame of the new footage ever makes that text hard
   to read, that's a pre-existing risk this removal doesn't add, but it
   does remove the (mismatched) scrim that was partially masking it. */

.hero__overlay {
  position: relative;
  z-index: 2;
  min-height: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-4);
  padding: calc(var(--space-5) + 3rem) var(--container-pad) calc(var(--space-4) + env(safe-area-inset-bottom));
}

.hero__content {
  max-width: 60rem;
}

.hero__title {
  /* font-family/stretch/weight come from the shared "h1, .section-heading"
     headline rule above — this just sets the size/spacing specific to
     the hero. Colour flipped to black (client request, 14 Aug 2026).
     ⚠️ UPDATED 14 Sep 2026 — the dark .hero::before/::after scrims that
     used to sit behind this (sized for white text, never updated after
     the colour flip) have been removed at the designer's request — see
     that removal's own comment a little above. Black text now sits
     directly on the raw video with no scrim or panel behind it at all;
     revisit with a light/blurred backing if any frame of the new
     footage makes it hard to read. */
  font-size: var(--step-display);
  line-height: var(--lh-display, 0.96);
  letter-spacing: var(--ls-display, -0.01em);
  margin-bottom: var(--space-2);
  /* ⚠️ FIXED 16 Sep 2026, alongside real dark-mode support — this was
     still on the theme-tracking --colour-ink, missed when the rest of
     the hero (section 12/13) moved to the frozen --colour-scrim-dark.
     Same reasoning as this rule's own comment above: black text sits
     directly on the raw video with no scrim behind it, a deliberate
     choice independent of the visitor's system theme — dark mode
     flipping it white would silently undo the 14 Aug 2026 decision and
     could make it unreadable against the wrong frame, exactly the class
     of bug already fixed for the logo/nav in this same section. */
  color: var(--colour-scrim-dark);
}

.hero__descriptor {
  font-size: var(--step-lead);
  color: rgba(10, 10, 10, 0.85);
  max-width: 40ch;
}

/* Bottom-left "Play showreel" + centred scroll indicator, sharing one
   row instead of the button being stacked under the headline (client
   decision, 13 Aug 2026). Grid, not flex + space-between, so the
   scroll indicator stays visually centred regardless of the button's
   width or whether it's even present (showreel.vimeo_id can be empty). */
.hero__bottom-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-2);
}

.hero__play {
  grid-column: 1;
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  border: 1px solid rgba(10, 10, 10, 0.45);
  border-radius: 999px;
  padding-inline: var(--space-3);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  /* Frozen scrim colour, not theme-tracking — sits directly on the
     showreel video, see section 2's --colour-scrim-dark/light comment. */
  color: var(--colour-scrim-dark);
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.hero__play:hover,
.hero__play:focus-visible {
  background: var(--colour-scrim-dark);
  border-color: var(--colour-scrim-dark);
  color: var(--colour-scrim-light);
}

.hero__play-icon {
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0.45em 0 0.45em 0.75em;
  border-color: transparent transparent transparent currentColor;
}

.hero__scroll {
  grid-column: 2;
  justify-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.1em);
  transition: color 0.2s ease;
}

/* Given its own solid backing rather than relying on transparency
   over the raw video, 14 Sep 2026 accessibility review: real showreel
   footage isn't uniformly dark, and this label previously stayed
   white-on-transparent (rgba(255,255,255,0.78)) with only the hero's
   general bottom scrim behind it, which isn't reliably dark enough on
   its own — and that scrim has since been removed entirely (see the
   .hero::before/::after removal comment above), so this pill's own
   solid backing is now the only thing guaranteeing contrast here.
   0.85 opacity black keeps at least 4.5:1 for the white text even
   against a fully white video frame directly behind it. */
.hero__scroll-label {
  display: inline-block;
  background: rgba(10, 10, 10, 0.85);
  /* Frozen scrim colour, not theme-tracking — white text on a solid
     dark pill over the showreel, see section 2's
     --colour-scrim-dark/light comment. */
  color: var(--colour-scrim-light);
  padding: 0.3em 0.85em;
  border-radius: 999px;
}

.hero__scroll:hover .hero__scroll-label,
.hero__scroll:focus-visible .hero__scroll-label {
  background: var(--colour-scrim-dark);
}

.hero__scroll-line {
  width: 1px;
  height: 2.5rem;
  background: rgba(255, 255, 255, 0.35);
  position: relative;
  overflow: hidden;
}

.hero__scroll-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--colour-bg);
  animation: hero-scroll-line 2.2s ease-in-out infinite;
}

@keyframes hero-scroll-line {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(100%); }
}

/* ---------------------------------------------------------------
   14. Homepage — shared section heading
   --------------------------------------------------------------- */

.section-heading {
  font-size: var(--step-h1);
  max-width: 20ch;
}

.section-eyebrow {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — same sitewide fix as .studio-hero__eyebrow:
     client instruction is that a kicker/eyebrow line above a heading
     should never be smaller than body text. Bumped from --step-small
     (14px, the site's smallest token) to --step-lead. */
  font-size: var(--step-lead);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.12em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

/* ---------------------------------------------------------------
   15. Homepage — Featured Work
   A 3-up grid of short looping preview clips (client reference,
   13 Aug 2026), not the large-format stacked rows this section used
   to be — heading and "Go to Work" share one line, matching the
   supplied screenshot.
   --------------------------------------------------------------- */

.work {
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  padding-block: var(--space-5) var(--space-4);
}

.work__intro {
  padding-bottom: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.work__all-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--step-small);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.work-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4) var(--space-3);
}

@media (min-width: 56rem) {
  .work-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.work-card__link {
  display: block;
}

.work-card__media {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--colour-surface);
  margin-bottom: var(--space-2);
}

.work-card__media picture,
.work-card__media img,
.work-card__media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.work-card__media img {
  transition: transform 0.6s ease;
}

.work-card__link:hover .work-card__media img,
.work-card__link:focus-visible .work-card__media img {
  transform: scale(1.03);
}

.work-card__video {
  opacity: 0;
  transition: opacity 0.4s ease;
}

.work-card__video.is-active {
  opacity: 1;
}

.work-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--space-3);
}

.work-card__title {
  font-size: var(--step-body);
  letter-spacing: var(--ls-heading, -0.01em);
}

.work-card__line {
  display: flex;
  gap: var(--space-2);
  font-size: var(--step-body);
  color: var(--colour-ink-soft);
}

.work-card__services {
  flex-basis: 100%;
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

.work__empty {
  padding-block: var(--space-3);
  color: var(--colour-ink-soft);
}

.work__empty a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------------------------------------------------------------
   15a. Work archive page (templates/work.php)
   Reuses .work-list/.work-card as-is (same grid, same cards as the
   homepage's Featured Work) — just this page's own header and filter
   row, and a --archive modifier for the unbounded (not capped at 3)
   grid. Filter active-state deliberately uses ink/white, not the red
   accent in the client's reference screenshot — colour on this site
   comes from the work itself, not the UI (agreed design direction).
   --------------------------------------------------------------- */

.work-header {
  padding-block: var(--space-4) var(--space-3);
}

.work-header__title {
  font-size: var(--step-h1);
  max-width: 32ch;
}

.work-filter {
  padding-block: var(--space-3);
  border-top: 1px solid var(--colour-line);
  border-bottom: 1px solid var(--colour-line);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
}

.work-filter__label {
  /* Bumped from --step-small (client feedback, 17 Aug 2026 — filter text
     read too small, wanted it closer to the reference site's sizing).
     plusoneamsterdam.com/work doesn't have a direct one-to-one filter-
     chip equivalent to compare against — its closest matches are either
     large standalone category links (Branding/Motion/Digital Art, ~36px,
     which read as a stand-alone heading/nav rather than a tight in-row
     filter) or a mobile-only "Filter" button that opens a separate
     picker. --step-lead is a clear, legible step up from the old size
     without going as far as heading-scale, which wouldn't suit 6 filters
     sitting in one row/grid. */
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  flex-shrink: 0;
}

/* Mobile (default): a simple wrapping row of chips. The 3-columns x
   2-rows arrangement below needs several hundred px of width to fit
   without spilling past the edge of the screen — on a phone that grid
   doesn't wrap, so it made the whole page render wider than the
   viewport, and mobile browsers respond to that by zooming the entire
   page out to fit, which is what actually made every bit of text look
   "too small" (client report, 17 Aug 2026) — not the type scale itself,
   which is unchanged and was already sized correctly. Fixed by making
   the mobile layout wrap normally like any other list of buttons. */
.work-filter__list {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
}

/* 3 columns x 2 rows, filled column-first (client reference screenshot,
   13 Aug 2026) — e.g. with 6 filters, column 1 gets items 1 & 2 stacked,
   column 2 gets items 3 & 4, column 3 gets items 5 & 6. Breakpoint raised
   from 40rem to 56rem (17 Aug 2026) alongside the --step-lead font-size
   bump above — the larger text needs more width per column for 3 columns
   to comfortably fit without crowding back toward the same overflow this
   was built to prevent. */
@media (min-width: 56rem) {
  .work-filter__list {
    display: grid;
    grid-template-rows: repeat(2, auto);
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    column-gap: var(--space-4);
    row-gap: 0;
  }
}

.work-filter__button {
  /* Font size bumped from --step-small to --step-lead (client feedback,
     17 Aug 2026 — see .work-filter__label above for the full note on
     why this size and not the reference's much larger 36px category
     links). min-height raised to match — 44px is the WCAG 2.2 AAA
     target and now fits naturally at this larger text size, where the
     old 32px (deliberately under the 44px target, chosen only because
     the text was small) no longer applies. */
  min-height: 44px;
  display: flex;
  align-items: center;
  padding-inline: var(--space-1);
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  transition: color 0.2s ease;
}

.work-filter__button:hover,
.work-filter__button:focus-visible {
  color: var(--colour-ink);
}

.work-filter__button[aria-pressed="true"] {
  color: var(--colour-brand-turquoise);
}

.work-list--archive {
  padding-block: var(--space-4);
}

/* ---------------------------------------------------------------
   15b. Reels archive page (templates/reels.php)
   Reuses .work-header for the page title and .project-hero--inline/
   .video-facade (already used on every project page) for the video
   itself — just this page's own year/title caption above each one.
   One block per year, stacked full-width down the page rather than
   a grid — a showreel is the flagship piece, not one tile among many.

   26 Aug 2026: split into two parts (client instruction) — a single
   "current" slot up top (.reel--current, no border-top of its own
   since it's the first thing on the page under the H1), which can
   either hold a real video (once one exists) or the plain
   .reel__placeholder note seen today; then "Past Showreels"
   (.reel-archive) below it, oldest at the bottom.
   --------------------------------------------------------------- */

.reel__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  padding-block: var(--space-3) var(--space-2);
}

.reel__title {
  /* The only label per reel now (client instruction, 26 Aug 2026: no
     year shown anywhere on this page) — set as the section's own small
     heading rather than the muted secondary line it was when paired
     with a large year number. */
  font-size: var(--step-h2);
  font-weight: 600;
}

.reel__description {
  /* NEW 7 Sep 2026 — real reel-by-reel description, reused/rewritten
     from the client's old "Historical Showreels" write-ups (see
     content/reels.php's header note). Forced onto its own full-width
     line below the title rather than sitting inline in the flex row. */
  flex-basis: 100%;
  max-width: 46rem;
  color: var(--colour-ink-soft);
  font-size: var(--step-body);
  line-height: var(--lh-body, 1.5);
}

.reel__placeholder {
  padding-block: var(--space-3) var(--space-5);
  color: var(--colour-ink-soft);
  font-size: var(--step-lead);
}

.reel-archive {
  border-top: 1px solid var(--colour-line);
  padding-top: var(--space-2);
}

.reel-archive__heading {
  padding-block: var(--space-4) var(--space-1);
  font-size: var(--step-h2);
}

.reel-archive .reel + .reel {
  border-top: 1px solid var(--colour-line);
}

/* ---------------------------------------------------------------
   16. Studio page (templates/studio.php) — About/origin story merged
   with the discipline by discipline "What We Do" breakdown (client
   decision, 26 Aug 2026). Rebuilt a second time, same day, after the
   client asked for plusoneamsterdam.com/about's actual layout: a large
   statement headline with a small eyebrow, a label paired with a lead
   paragraph, real photography placed beside the text rather than in
   its own gallery, and a plain client list under one intro line. What
   We Do and Trusted By follow the same quieter, typographic idea the
   first build already had; the hero, origin section and CTA below are
   new in this pass. "Trusted By" and the CTA keep the client's own
   liked heading/button styling, just extended with one supporting line
   each.
   --------------------------------------------------------------- */

.studio-hero {
  padding-block: var(--space-5) var(--space-3);
}

.studio-hero__eyebrow {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — client: "titles ... should be larger
     than the body text ... we shouldn't ever have titles that are
     smaller than the body text." This eyebrow line ("SINCE 2008") was
     set to --step-small (14px, the site's smallest token, shared with
     things like pill buttons and captions) — smaller than every body
     paragraph on the page. Bumped to --step-lead so it reads as a
     small heading, not a caption, per that instruction. */
  font-size: var(--step-lead);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.studio-hero__title {
  /* ⚠️ FONT-FAMILY REMOVED 22 Sep 2026 — client report: "font on the
     Studio header is incorrect... text is way too large and not
     matching new styling we agreed." Root cause: this was the only
     h1 on the site still explicitly set to Archivo Expanded
     (--font-display). Every OTHER h1/.section-heading (including
     .hero__title on the homepage) has used Switzer since the 13 Aug
     2026 "h1, .section-heading" swap (see that rule's own comment) —
     this element was never updated to match because it set its own
     font-family directly, bypassing that shared rule. Removing the
     override here lets it inherit Switzer 600 like every other
     heading on the site, matching .hero__title's own approach
     (font-size/line-height/letter-spacing stay on the shared --step-*
     tokens, only font-family/weight now come from the shared rule). */
  font-size: var(--step-display);
  line-height: var(--lh-display, 1.05);
  letter-spacing: var(--ls-display, -0.01em);
  max-width: 18ch;
}

/* --- Scattered origin: images and text on alternating sides ---------
   Rebuilt 26 Aug 2026 (fifth pass): the client said the previous tidy
   two row layout still did not scatter images and text on different
   sides the way the reference does. Mobile is a single column, reading
   top to bottom in source order (label, paragraph A, frog figure,
   characters figure, paragraph B, paragraph C, statement, pills) — the
   scattering only applies once there is width to actually stagger
   things sideways. */

.studio-scatter {
  border-top: 1px solid var(--colour-line);
  padding-block: var(--space-4);
}

.studio-scatter__grid {
  display: grid;
  /* minmax(0, 1fr) rather than the implicit auto track: fixes text
     clipping at narrow viewports (accessibility review, 14 Sep 2026)
     — grid items default to min-width:auto, so an "auto" track sizes
     itself to the content's intrinsic min width and can overflow the
     320px viewport. The min-width:0 below on each child is the other
     half of the same fix. Overridden by the 12-column layout at 56rem
     and up, which sets its own explicit column widths instead. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
}

.studio-scatter__grid > * {
  min-width: 0;
}

.studio-scatter__label {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — same client instruction and same fix as
     .studio-hero__eyebrow above: this label ("Animation and
     Illustration Studio, UK") was smaller than the body copy beneath
     it — bumped from --step-small to --step-lead. */
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
}

.studio-scatter__text {
  /* ⚠️ CONSOLIDATED 23 Sep 2026 — client: "make all three sections
     the same... its not just size thats different, its the line
     spacing the font spacing etc." Font-size, colour AND line-height
     now all live on this one shared rule, so the three paragraphs in
     this section (--a/--b/--c) are identical by construction. Before
     this, --a alone carried its own line-height (var(--lh-lead)) and
     colour (full ink) while --b/--c silently fell back to this rule's
     original lighter ink-soft colour and the PAGE-WIDE default
     line-height (var(--lh-body), 1.55 — noticeably looser than --a's
     1.3) — three different per-variant patches producing three
     slightly different results instead of one shared source of truth.
     The per-variant rules below now carry layout only (max-width),
     nothing typographic. */
  font-size: var(--step-lead);
  line-height: var(--lh-lead, 1.3);
  color: var(--colour-ink);
  max-width: 48ch;
  overflow-wrap: break-word;
}

.studio-scatter__text--a {
  max-width: 34ch;
}

/* Also covers <video> — figure--a became a real video clip (frog.mp4)
   15 Sep 2026, replacing the placeholder illustration this rule was
   originally written for; both share the same "fill the figure, keep
   its own aspect ratio" sizing. */
.studio-scatter__figure img,
.studio-scatter__figure video {
  width: 100%;
  height: auto;
  display: block;
}

/* ⚠️ FIXED 16 Sep 2026 — client report: "the frog on the front page has
   horrible lines at the top and bottom... its a problem on home page
   and about us page." Same frog.mp4 file, same 1px baked-in black line
   on its very top and bottom rows — see the matching .about__figure
   comment in this file for the full frame-by-frame investigation. This
   figure (templates/studio.php, the About Us page) shows the video at
   its native width/height with no crop at all, so the two 1px lines
   were visible here from the moment this clip was first wired in, not
   introduced by any recent change. Same CSS-only fix, deliberately not
   touching frog.mp4 itself (client's explicit preference that video
   edits stay their end): clip the figure and scale the video up ~1% so
   both edge lines land just outside the visible box. */
.studio-scatter__figure {
  overflow: hidden;
}

.studio-scatter__figure video {
  transform: scale(1.008);
}

@media (min-width: 56rem) {
  .studio-scatter__grid {
    grid-template-columns: repeat(12, 1fr);
    align-items: start;
    row-gap: var(--space-5);
  }

  .studio-scatter__label {
    grid-column: 1 / 6;
    grid-row: 1;
  }

  .studio-scatter__text--a {
    grid-column: 1 / 6;
    grid-row: 2;
  }

  .studio-scatter__figure--a {
    grid-column: 7 / 13;
    grid-row: 1 / 3;
    margin-top: clamp(0px, 4vw, 3.5rem);
    max-width: 24rem;
    justify-self: end;
  }

  /* ADDED 23 Sep 2026 -- new "How We Work" heading (see
     templates/studio.php) sits above figure--b/text--b as its own
     full-width row; everything from figure--b down shifts one row,
     hence the explicit grid-row on statement/pills below (previously
     left to grid auto-placement). */
  .studio-scatter__subheading {
    grid-column: 1 / 13;
    grid-row: 3;
  }

  .studio-scatter__figure--b {
    grid-column: 1 / 6;
    grid-row: 4;
    max-width: 20rem;
  }

  .studio-scatter__text--b {
    grid-column: 7 / 13;
    grid-row: 4;
    align-self: center;
  }

  .studio-scatter__subheading--process {
    grid-column: 1 / 13;
    grid-row: 5;
  }

  .studio-scatter__text--c {
    grid-column: 3 / 12;
    grid-row: 6;
    max-width: 62ch;
    margin-top: var(--space-2);
  }

  .studio-statement {
    grid-column: 1 / 13;
    grid-row: 7;
  }

  .studio-pills {
    grid-column: 1 / 13;
    grid-row: 8;
  }
}

/* --- Pulled out statement + Services/Process pills -----------------
   Added 26 Aug 2026: the reference page pulls one sentence out at a
   larger size, distinct from the regular paragraphs around it, then
   offers Services/Process as slide in panels rather than more of the
   same page. See .studio-panel below and assets/js/studio.js. */

.studio-statement {
  /* ⚠️ FONT-FAMILY REMOVED 22 Sep 2026 — same fix and same reason as
     .studio-hero__title above (client: "Same with font lower down").
     This is a <p>, not an h1, so it now inherits Switzer at the plain
     body weight from the shared body/p rule, same treatment as the
     project-page equivalent of this pattern, .project-statement
     (also unweighted Switzer at --step-h1). */
  font-size: var(--step-h1);
  line-height: var(--lh-heading, 1.15);
  max-width: 26ch;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--colour-line);
}

.studio-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.studio-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  border: 1px solid var(--colour-line);
  border-radius: 999px;
  padding-inline: var(--space-3);
  font-size: var(--step-small);
  background: none;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.studio-pill:hover,
.studio-pill:focus-visible {
  background: var(--colour-ink);
  border-color: var(--colour-ink);
  color: var(--colour-bg);
}

.studio-pill__arrow {
  transition: transform 0.2s ease;
}

.studio-pill:hover .studio-pill__arrow,
.studio-pill:focus-visible .studio-pill__arrow {
  transform: translateX(0.2em);
}

/* --- Services/Process slide in panels -------------------------------
   Hidden via the [hidden] attribute server side (works with no JS/CSS
   at all — the buttons that open them just do nothing); assets/js/
   studio.js removes [hidden] and adds .is-open on click, which this CSS
   then transitions in. */

.studio-panel {
  position: fixed;
  inset: 0;
  z-index: 200;
}

.studio-panel__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 10, 0.4);
  opacity: 0;
  transition: opacity 0.24s ease;
}

.studio-panel__sheet {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(100%, 34rem);
  overflow-y: auto;
  background: var(--colour-bg);
  color: var(--colour-ink);
  padding: var(--space-3) var(--space-3) var(--space-5);
  transform: translateX(100%);
  transition: transform 0.28s ease;
  box-shadow: -1px 0 0 var(--colour-line);
}

.studio-panel.is-open .studio-panel__backdrop {
  opacity: 1;
}

.studio-panel.is-open .studio-panel__sheet {
  transform: translateX(0);
}

/* Frozen scrim colours, not theme-tracking (section 2's
   --colour-scrim-dark/light) — the "--dark" here is its own deliberate
   style variant (an always-dark panel, as distinct from the default
   sheet just above which correctly stays on the theme-tracking tokens),
   not a statement about the visitor's system theme, so it shouldn't
   flip light just because dark mode redefines --colour-ink. */
.studio-panel--dark .studio-panel__sheet {
  background: var(--colour-scrim-dark);
  color: var(--colour-scrim-light);
  box-shadow: none;
}

.studio-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-block: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--colour-line);
  margin-bottom: var(--space-2);
}

.studio-panel--dark .studio-panel__head {
  border-bottom-color: rgba(255, 255, 255, 0.2);
}

.studio-panel__heading {
  /* font-family removed 22 Sep 2026 — same sitewide Archivo-to-Switzer
     drift fix as .studio-hero__title above; see that comment. */
  font-size: var(--step-h2);
}

.studio-panel__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: none;
  border: 0;
  font-size: var(--step-h2);
  line-height: 1;
  cursor: pointer;
  color: inherit;
}

.studio-panel__row {
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-3);
  border-top: 1px solid var(--colour-line);
}

.studio-panel--dark .studio-panel__row {
  border-top-color: rgba(255, 255, 255, 0.15);
}

.studio-panel__row:first-child {
  border-top: 0;
  padding-top: 0;
}

.studio-panel__name {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-weight: 600;
  font-size: var(--step-lead);
}

.studio-panel__step {
  font-variant-numeric: tabular-nums;
  color: var(--colour-ink-soft);
}

.studio-panel--dark .studio-panel__step {
  color: rgba(255, 255, 255, 0.6);
}

.studio-panel__detail {
  font-size: var(--step-body);
  color: var(--colour-ink-soft);
  max-width: 44ch;
}

.studio-panel--dark .studio-panel__detail {
  color: rgba(255, 255, 255, 0.75);
}

/* Belt and braces: while a panel is open, stop the page underneath
   from also scrolling. */
body.studio-panel-open {
  overflow: hidden;
}

/* --- Marquee ---------------------------------------------------------
   Large moving line between sections (client: "the moving message is
   also nice"), built from real content (the site's disciplines), not a
   decorative slogan. Two identical sets sit side by side and the whole
   track slides left by exactly one set's width, so the loop is seamless. */

.studio-marquee {
  overflow: hidden;
  border-top: 1px solid var(--colour-line);
  border-bottom: 1px solid var(--colour-line);
  padding-block: var(--space-3);
}

.studio-marquee__track {
  display: flex;
  width: max-content;
  animation: studio-marquee-scroll 28s linear infinite;
}

.studio-marquee__set {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  padding-right: var(--space-3);
}

.studio-marquee__word {
  /* font-family removed 22 Sep 2026 — same sitewide Archivo-to-Switzer
     drift fix as .studio-hero__title above; see that comment. */
  font-size: var(--step-h1);
  white-space: nowrap;
}

.studio-marquee__dot {
  font-size: var(--step-h1);
  color: var(--colour-ink-soft);
  padding-inline: var(--space-3);
}

@keyframes studio-marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .studio-marquee__track {
    animation: none;
  }

  .studio-marquee__set:last-child {
    display: none;
  }
}

.studio-section {
  border-top: 1px solid var(--colour-line);
  padding-block: var(--space-4);
}

.studio-section__heading {
  /* ⚠️ WEIGHT FIXED 23 Sep 2026 — client report: newly-visible "Our
     Story" heading (see templates/studio.php's origin-heading comment)
     "doesn't look in keeping with what we selected." Root cause:
     .studio-section__heading was never wired into the shared h1/
     .section-heading rule (assets/css/style.css, ~line 354) that gives
     every other real section heading on the site font-weight 600 —
     it was only catching the generic h1,h2,h3{font-weight:500} reset,
     one step lighter than the site's actual heading standard. This
     also silently affected "Trusted By" (same class), which has looked
     one weight lighter than intended the whole time — fixed for both
     at once. */
  font-size: var(--step-h2);
  font-weight: 600;
}

.studio-section__intro {
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  max-width: 48ch;
  margin-top: var(--space-2);
}

/* --- Trusted By ----------------------------------------------------- */

#trusted-by-heading {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — client report: "the Trusted By title has
     shrunk and is not laid out properly ... make that title larger
     please as before." It shares .studio-section__heading with "Our
     Story"/"What We Do" (--step-h2, 30-38px) — deliberately smaller
     per this pass's "quieter, typographic idea" for those two — but
     this section's own history note just above says Trusted By's
     heading was meant to stay exactly as the client already liked it,
     not shrink along with them. Scoped to this one heading (its own
     id, already on the element) so "Our Story"/"What We Do" are
     untouched; restored to the site's next tier up. */
  font-size: var(--step-h1);
}

.studio-clients__row {
  display: grid;
  gap: var(--space-2);
}

@media (min-width: 56rem) {
  .studio-clients__row {
    grid-template-columns: minmax(14rem, 18rem) 1fr;
    align-items: baseline;
    gap: var(--space-4);
  }
}

.studio-clients__list {
  display: grid;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3);
}

@media (min-width: 40rem) {
  .studio-clients__list {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64rem) {
  .studio-clients__list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.studio-clients__item {
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
}

/* --- CTA ------------------------------------------------------------ */

.studio-cta {
  padding-block: var(--space-5);
  text-align: center;
}

.studio-cta__title {
  /* font-family removed 22 Sep 2026 — same sitewide Archivo-to-Switzer
     drift fix as .studio-hero__title above; see that comment. This is
     an h1 so it already inherits Switzer 600 from the shared
     "h1, .section-heading" rule once this override is gone. */
  font-size: var(--step-h1);
  margin-bottom: var(--space-2);
}

.studio-cta__line {
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  max-width: 42ch;
  margin-inline: auto;
  margin-bottom: var(--space-3);
}

.studio-cta__button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding-inline: var(--space-3);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  transition: background-color 0.2s ease, color 0.2s ease;
}

.studio-cta__button:hover,
.studio-cta__button:focus-visible {
  background: var(--colour-ink);
  color: var(--colour-bg);
}

/* ---------------------------------------------------------------
   17. Homepage — About Cumi teaser
   --------------------------------------------------------------- */

.about {
  /* Same stacking-context fix as .disciplines above. */
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  padding-block: var(--space-5);
  border-top: 1px solid var(--colour-line);
}

.about__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ⚠️ FIXED 16 Sep 2026 — client report: "the frog on the homepage has
   his feet cut off." Root cause: the real frog.mp4 (templates/home.php)
   is 800x1070 (portrait) but this figure is a hard 1:1 square, so
   `object-fit: cover` scales it up to fill the square's width and
   centre-crops the overflow off the top AND bottom equally. The
   character itself (checked via extracted frames) is taller than the
   800px-square crop window even at best — there's no object-position
   that keeps 100% of both the head and the feet — and the character
   sits low in its own frame with little margin below the feet, so a
   centred crop takes a visible bite out of them specifically. Fix:
   object-fit: contain instead of cover. This shows the WHOLE character,
   nothing cropped, feet included; checked that this doesn't introduce
   visible letterboxing — the video's own background (sampled: pure
   #ffffff) exactly matches this section's page background
   (--colour-bg: #ffffff — see :root), so the uncropped edges are
   invisible against the page. Also applies to the static image fallback
   path (origin-illustration.*) for when no video exists, harmlessly —
   that fallback is already exported square, so contain vs cover makes
   no visual difference there either way. */
.about__figure {
  /* ⚠️ ADDED 16 Sep 2026 — see the video-only rule below: clips the
     source file's own baked-in top/bottom edge line back out of view
     now that the fix above stopped cropping it away by accident. */
  overflow: hidden;
}

.about__figure img,
.about__figure picture,
.about__figure video {
  aspect-ratio: 1 / 1;
  object-fit: contain;
}

/* ⚠️ FIXED 16 Sep 2026 — client report: "the frog on the front page has
   horrible lines at the top and bottom." Checked frog.mp4 itself frame
   by frame (ffmpeg + pixel sampling, not a guess from the filename):
   the source file has a solid 1px-wide black line baked into the very
   first and very last row of every frame (row 0 and row 1069 of its
   1070px height) — a hard edge in the original export, not a website
   rendering fault. It was invisible before today only because the old
   `object-fit: cover` (see the comment above) happened to crop those
   exact edge rows away as a side effect while fixing the feet; turning
   on `contain` to show the whole character brought the two 1px lines
   back into view along with the feet.
   Client feedback earlier today was explicit that video files are
   theirs to edit, not something to change from this end ("we don't
   want a change in our video files, we chose that order... that is
   not a website issue. Video we can fix.") — so this is deliberately a
   CSS-only fix, not a re-encode/re-export of frog.mp4 itself. A ~1%
   scale-up on the video element only (not the img/picture fallback,
   which doesn't have this defect), combined with `overflow: hidden` on
   the figure above, pushes both 1px edge lines just outside the visible
   box. At this scale the crop is ~4px off the top/bottom of an 800x1070
   frame (well under 0.5% of the frame) — imperceptible, and the feet
   stay fully in view since the "feet cut off" fix above already backed
   the crop window off them by far more than 4px. */
.about__figure video {
  transform: scale(1.008);
}

.about__lead {
  /* Tier 3 — "very large editorial type" (client's wording, 13 Aug
     2026): still Switzer (--font-body, inherited from the base h1/h2/h3
     rule), just heavier than the standard 500 — a big editorial
     statement rather than a headline, so it stays in the body typeface
     rather than switching to Archivo Expanded. */
  font-weight: 600;
  font-size: var(--step-display);
  /* ⚠️ FIXED 23 Sep 2026 — client report: "the spacing between lines is
     not quite enough" on this section, lines visibly touching. This was
     on --lh-display (0.90 at :root) same as .hero__title — fine there
     since the hero is only ever 1-2 short lines, but this heading is
     three explicit <br>-separated lines of the same large display type
     (see templates/home.php), and 0.90 leading is tight enough that
     descenders on one line ("imagination") sit right on top of the
     ascenders of the line below. Given its own literal value instead of
     sharing --lh-display, so this doesn't also loosen the hero. */
  line-height: 1.15;
  letter-spacing: var(--ls-display, -0.02em);
  max-width: 16ch;
  margin-bottom: var(--space-3);
}

.about__body {
  max-width: 60ch;
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.about__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

@media (min-width: 56rem) {
  .about__inner {
    flex-direction: row;
    align-items: center;
    gap: var(--space-5);
  }

  .about__figure {
    flex: 0 0 34%;
  }

  .about__content {
    flex: 1;
  }
}

/* ---------------------------------------------------------------
   17b. Homepage — What We Do
   Deliberately no visible heading (see the sr-only <h2> in
   templates/home.php) — just a statement and a link to /expertise/.
   --------------------------------------------------------------- */

.what-we-do {
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  padding-block: var(--space-5);
}

.what-we-do__inner {
  max-width: 44rem;
}

.what-we-do__body {
  font-size: var(--step-lead);
  letter-spacing: var(--ls-lead, -0.01em);
  margin-bottom: var(--space-3);
}

.what-we-do__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------------------------------------------------------------
   17c. Homepage — News
   --------------------------------------------------------------- */

.news {
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  padding-block: var(--space-5);
  border-top: 1px solid var(--colour-line);
}

.news-list {
  margin-top: var(--space-3);
  display: grid;
  gap: var(--space-4);
}

@media (min-width: 56rem) {
  .news-list {
    grid-template-columns: repeat(2, 1fr);
    column-gap: var(--space-4);
  }
}

.news-item {
  border-top: 1px solid var(--colour-line);
  padding-top: var(--space-3);
}

.news-item__title {
  font-size: var(--step-h2);
  letter-spacing: var(--ls-heading, -0.01em);
  margin-bottom: var(--space-2);
}

.news-item__body {
  color: var(--colour-ink-soft);
  max-width: 52ch;
  margin-bottom: var(--space-2);
}

.news-item__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--step-small);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------------------------------------------------------------
   17d. Homepage — Video (changeable, brief §4-adjacent)
   Near-full-width, matching the client's plusoneamsterdam.com
   reference — not full-bleed like the hero, a small gutter either
   side so it still reads as one section among others.
   --------------------------------------------------------------- */

.homepage-video {
  position: relative;
  z-index: 1;
  background: var(--colour-bg);
  padding-block: var(--space-5);
}

.homepage-video__inner {
  max-width: 96rem;
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

/* ---------------------------------------------------------------
   18. Scroll-reveal utility
   Progressive enhancement only (brief §25/§26): with no JavaScript,
   html never gains the .js class (see inc/head.php), so [data-reveal]
   elements stay at their normal opacity/position — nothing is ever
   stuck invisible. prefers-reduced-motion zeroes the transition
   duration globally (section 4), so reduced-motion visitors get an
   instant, non-animated reveal rather than none at all.
   --------------------------------------------------------------- */

[data-reveal] {
  opacity: 1;
  transform: none;
}

html.js [data-reveal] {
  opacity: 0;
  transform: translateY(1.5rem);
  transition: opacity 0.7s ease, transform 0.7s ease;
}

html.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* ---------------------------------------------------------------
   19. Showreel lightbox
   Only ever created if content/site.php's showreel.vimeo_id is set
   and a visitor clicks "Play showreel" — see assets/js/home.js.
   --------------------------------------------------------------- */

body.has-lightbox {
  overflow: hidden;
}

.showreel-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(5, 5, 5, 0.94);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
}

.showreel-lightbox__iframe {
  width: min(100%, 108rem);
  aspect-ratio: 16 / 9;
  border: 0;
}

.showreel-lightbox__close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  min-height: 44px;
  min-width: 44px;
  padding-inline: var(--space-2);
  /* Frozen scrim colour, not theme-tracking — white text on this
     button's own always-dark background two lines down, see section
     2's --colour-scrim-dark/light comment. */
  color: var(--colour-scrim-light);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  /* Solid-ish backing of its own, 14 Sep 2026 accessibility review:
     on wide/short viewports the 16:9 iframe can reach nearly to the
     top of the overlay, leaving little of the dark backdrop actually
     behind this button — it was relying on that backdrop rather than
     guaranteeing its own contrast, and read as faint over a bright
     video frame. 0.85 opacity black + a more visible border keep it
     legible regardless of what's directly behind it. */
  background: rgba(10, 10, 10, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
}

.showreel-lightbox__close:hover,
.showreel-lightbox__close:focus-visible {
  /* Frozen scrim colours, not theme-tracking (section 2's
     --colour-scrim-dark/light) — this button floats over the always-
     dark video lightbox backdrop (rgba(5, 5, 5, 0.94) above), not page
     chrome, so its own light/dark inversion shouldn't track the
     visitor's system theme either. */
  background: var(--colour-scrim-light);
  color: var(--colour-scrim-dark);
}

/* ---------------------------------------------------------------
   20. Contact & forms (templates/contact.php, templates/start-project.php)
   Shared by both pages via inc/contact-components.php. Deliberately
   monochrome + the one existing brand accent (--colour-brand-turquoise)
   for a chip's selected state — matching .work-filter__button's own
   active-state treatment (colour on text/border, never a filled
   background) rather than introducing a new UI-colour pattern.
   --------------------------------------------------------------- */

.contact-hero {
  padding-block: var(--space-4) var(--space-3);
}

.contact-hero__eyebrow {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — same sitewide eyebrow fix, see
     .section-eyebrow above. */
  font-size: var(--step-lead);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.1em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.contact-hero__title {
  font-size: var(--step-h1);
  max-width: 20ch;
}

.contact-hero__lead {
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  max-width: 42ch;
  margin-top: var(--space-2);
}

.contact {
  padding-block: 0 var(--space-5);
}

.contact__grid {
  display: grid;
  gap: var(--space-4);
}

@media (min-width: 56rem) {
  .contact__grid {
    grid-template-columns: 1.6fr 1fr;
    gap: var(--space-5);
    align-items: start;
  }
}

/* ---- The form itself ------------------------------------------- */

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 34rem;
}

/* Privacy-notice line above the submit button (pre-launch technical
   audit, FIX 02, 23 Sep 2026) — accessible before either form is
   submitted, per the audit's "done when". Deliberately plain text plus
   a link, not a checkbox: the audit explicitly asked not to add a
   compulsory marketing opt-in, and this isn't consent for anything
   beyond replying to the enquiry itself. */
.contact-form__privacy-notice {
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  margin: 0;
}

.contact-form__privacy-notice a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

fieldset.field {
  border: 0;
  padding: 0;
  margin: 0;
}

.field__label {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: var(--colour-ink-soft);
}

.field__optional {
  text-transform: none;
  letter-spacing: normal;
}

.field__control {
  width: 100%;
  border: 0;
  /* Darkened from --colour-line (#e2e2e2, 1.30:1 against white)
     to a dedicated field-boundary colour, 14 Sep 2026 accessibility
     review: this underline is the only visible indicator of the
     input area, so it needs the 3:1 non-text minimum (WCAG 1.4.11)
     — #808080 is 3.95:1. --colour-line itself is left alone since
     it's also used as a purely decorative divider elsewhere on the
     site, where that minimum doesn't apply. */
  border-bottom: 1px solid #808080;
  background: transparent;
  font-family: var(--font-body);
  font-size: var(--step-body);
  color: var(--colour-ink);
  padding-block: var(--space-1);
  resize: vertical;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.field__control::placeholder {
  color: var(--colour-ink-soft);
}

.field__control:hover {
  border-color: var(--colour-ink-soft);
}

.field__control:focus-visible {
  outline: none;
  border-color: var(--colour-ink);
  box-shadow: 0 1px 0 0 var(--colour-ink);
}

textarea.field__control {
  min-height: 8rem;
}

.field--error .field__control {
  border-color: var(--colour-ink);
}

.field__error {
  font-size: var(--step-small);
  color: var(--colour-ink);
  border-left: 2px solid var(--colour-ink);
  padding-left: var(--space-1);
}

/* Honeypot — invisible to a real visitor (off-screen + aria-hidden +
   tabindex="-1" in the markup, inc/contact-components.php), present in
   the DOM for a bot that fills in every field it finds. See §7 /
   inc/mail.php. */
.hp-field {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---- Chip pickers (project type / budget / timeline) ------------
   Plain radio inputs under the hood — works with no JavaScript and no
   :has()-dependent CSS, just an adjacent-sibling selector on :checked.
   Visually matches the pill language already used for
   .site-header__cta and .work-filter__button elsewhere on the site. */

.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-1);
}

.chip-wrap {
  position: relative;
}

.chip-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--space-2);
  border: 1px solid var(--colour-line);
  border-radius: 999px;
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.chip-wrap:hover .chip {
  border-color: var(--colour-ink);
  color: var(--colour-ink);
}

.chip-input:checked + .chip {
  border-color: var(--colour-brand-turquoise);
  color: var(--colour-brand-turquoise);
}

.chip-input:focus-visible + .chip {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ---- Buttons ------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: flex-start;
  min-height: 48px;
  padding-inline: var(--space-3);
  border-radius: 999px;
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.btn--primary {
  background: var(--colour-ink);
  color: var(--colour-bg);
  border: 1px solid var(--colour-ink);
  margin-top: var(--space-1);
}

.btn--primary:hover,
.btn--primary:focus-visible {
  background: var(--colour-bg);
  color: var(--colour-ink);
}

/* ---- Info column (email / social / cross-link) -------------------- */

.contact__info-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-1);
}

.contact-info-block__label {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.08em);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-1);
}

.contact-info-block__value {
  font-size: var(--step-lead);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.contact-info-block__social {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.contact-info-block__social a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.contact-info-block__note {
  color: var(--colour-ink-soft);
  max-width: 32ch;
}

.contact-info-block__note a {
  color: var(--colour-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---- Success state (post-submit, ?sent=1) -------------------------- */

.contact-success {
  max-width: 34rem;
  padding-top: var(--space-3);
  border-top: 1px solid var(--colour-line);
}

.contact-success__eyebrow {
  /* ⚠️ SIZE FIXED 23 Sep 2026 — same sitewide eyebrow fix, see
     .section-eyebrow above. */
  font-size: var(--step-lead);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.1em);
  color: var(--colour-brand-turquoise);
  margin-bottom: var(--space-1);
}

.contact-success__title {
  font-size: var(--step-h2);
  margin-bottom: var(--space-2);
}

.contact-success p {
  max-width: 48ch;
  margin-bottom: var(--space-2);
}

.contact-success__again a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------------------------------------------------------------
   21. Homepage — Character Design carousel
   New section (client request, 21 Sep 2026): a horizontally
   scrolling strip of the character-design illustration set
   (content/characters.php -> assets/img/characters/), positioned as
   the new LAST section before the footer — supersedes the 13 Aug
   2026 decision that kept .about last; client confirmed 21 Sep 2026
   this section moves ahead of it instead.

   Deliberately NOT a --container-max-capped grid of equal boxes:
   every character was processed to a shared height/baseline (see
   content/characters.php's own comment) and is shown at its own
   natural width, so cards vary size like a real character line-up
   rather than being squashed/padded into identical tiles. Height is
   the only fixed dimension — width follows each <img>'s intrinsic
   aspect ratio.

   Navigation (client decision, 21 Sep 2026): plain overflow-x drag/
   swipe (works with JS absent — brief §16 progressive enhancement)
   plus arrow buttons for non-touch discoverability; loops
   infinitely once assets/js/character-carousel.js runs; purely
   visual — no links, no captions (see templates/home.php's comment
   on why each <img> is correctly alt="" here).
   --------------------------------------------------------------- */

.character-design {
  position: relative;
  background: var(--colour-bg);
  padding-block: var(--space-5) var(--space-4);
}

.character-design__intro {
  padding-bottom: var(--space-3);
}

.character-carousel {
  position: relative;
}

.character-carousel__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none; /* Firefox */
  padding-inline-start: var(--container-pad);
  padding-block: var(--space-1); /* room for a focus ring so it isn't clipped */
  cursor: grab;
}

.character-carousel__viewport::-webkit-scrollbar {
  display: none;
}

.character-carousel__viewport:focus-visible {
  outline: 2px solid var(--colour-ink);
  outline-offset: -2px;
}

.character-carousel__viewport.is-dragging {
  cursor: grabbing;
  scroll-snap-type: none;
}

/* Auto-play (client feedback, 21 Sep 2026 — character-carousel.js
   section 5): snap fights a continuous scrollLeft increment with
   small snap-back jitter, so it's switched off for the duration,
   same idea as .is-dragging above. */
.character-carousel__viewport.is-autoplaying {
  scroll-snap-type: none;
}

.character-carousel__track {
  display: flex;
  align-items: flex-end; /* shared ground line, matches how the artwork was cropped */
  gap: var(--space-2);
  width: max-content;
  list-style: none;
  padding: 0;
  margin: 0;
}

.character-carousel__item {
  flex: none;
  /* ~220px at narrow widths up to ~420px on a wide desktop viewport —
     roughly five comfortably in view at once, per the client's own
     framing of the request; not a strict count, just this default. */
  height: clamp(13.75rem, 9rem + 19vw, 26rem);
  scroll-snap-align: start;
}

.character-carousel__item img,
.character-carousel__item .character-carousel__video {
  display: block;
  height: 100%;
  width: auto;
  -webkit-user-drag: none;
  user-select: none;
}

.character-carousel__video {
  object-fit: contain;
}

.character-carousel__controls {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-1);
  padding-inline-end: var(--container-pad);
  padding-top: var(--space-2);
}

.character-carousel__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  min-width: 44px; /* touch target, brief accessibility criteria */
  min-height: 44px;
  border: 1px solid var(--colour-line);
  border-radius: 50%;
  background: var(--colour-bg);
  color: var(--colour-ink);
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.character-carousel__arrow:hover,
.character-carousel__arrow:focus-visible {
  border-color: var(--colour-ink);
}

.character-carousel__arrow:active {
  background: var(--colour-ink);
  color: var(--colour-bg);
}

.character-carousel__arrow svg {
  width: 1.1rem;
  height: 1.1rem;
}

@media (min-width: 40rem) {
  .character-carousel__track {
    gap: var(--space-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .character-carousel__viewport {
    scroll-behavior: auto;
  }
}

/* ---------------------------------------------------------------
   22. Cookie/privacy consent banner (pre-launch technical audit,
   FIX 03, 23 Sep 2026)
   Built entirely by assets/js/consent.js — see that file's own header
   comment for the full mechanism (default-deny, what it gates, the
   180-day first-party cookie, reopening from the footer's "Cookie
   settings" control). Deliberately plain: black/white/underline, same
   restrained palette as the rest of the site — no accent colour, no
   drop shadow beyond what's needed to separate it from the page
   underneath on scroll.
   --------------------------------------------------------------- */

.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  background: var(--colour-bg);
  border-top: 1px solid var(--colour-line);
  padding: var(--space-3) var(--container-pad);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  box-shadow: 0 -0.5rem 1.5rem rgba(0, 0, 0, 0.08);
}

.cookie-banner__text {
  flex: 1 1 20rem;
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  margin: 0;
}

.cookie-banner__text a {
  color: var(--colour-ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.cookie-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.cookie-banner__btn {
  min-height: 44px;
  padding: 0 var(--space-3);
  border: 1px solid var(--colour-ink);
  border-radius: 999px;
  background: var(--colour-bg);
  color: var(--colour-ink);
  font-size: var(--step-small);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}

.cookie-banner__btn--accept {
  background: var(--colour-ink);
  color: var(--colour-bg);
}

.cookie-banner__btn:hover,
.cookie-banner__btn:focus-visible {
  opacity: 0.85;
}

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

/* ---------------------------------------------------------------
   23. Legal/policy pages (/privacy/) — pre-launch technical audit,
   FIX 02, 23 Sep 2026. Plain prose column, same restrained treatment
   as .not-found (section above): no bespoke components needed for a
   page that's just headings and paragraphs.
   --------------------------------------------------------------- */

.legal-page {
  max-width: 42rem;
  margin-inline: auto;
  padding: var(--space-5) var(--container-pad) var(--space-6);
}

.legal-page__eyebrow {
  font-size: var(--step-small);
  letter-spacing: var(--ls-label, 0.02em);
  text-transform: uppercase;
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.legal-page h1 {
  font-size: var(--step-h1);
  margin-bottom: var(--space-2);
}

.legal-page__updated {
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-4);
}

.legal-page h2 {
  font-size: var(--step-h2);
  margin-top: var(--space-4);
  margin-bottom: var(--space-2);
}

.legal-page p {
  font-size: var(--step-body);
  line-height: var(--lh-body, 1.55);
  color: var(--colour-ink);
  max-width: 60ch;
}

.legal-page p + p {
  margin-top: var(--space-2);
}

.legal-page a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
