/* =================================================================
   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;

  --step-body: clamp(1rem, 0.94rem + 0.3vw, 1.125rem);
  --step-lead: clamp(1.125rem, 1.02rem + 0.55vw, 1.5rem);
  --step-h2: clamp(1.75rem, 1.5rem + 1.3vw, 2.75rem);
  --step-h1: clamp(2.5rem, 1.9rem + 3vw, 5rem);
  --step-display: clamp(2.75rem, 1.9rem + 4.2vw, 6.5rem);
  --step-small: clamp(0.8125rem, 0.79rem + 0.1vw, 0.875rem);

  --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(4rem, 3rem + 5vw, 8rem);

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

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

  color-scheme: light;
}

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: 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: 1.05;
  letter-spacing: -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);
}

/* 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, with the gap below doing the spacing between
   them instead of the row's leftover width. 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: flex-start;
  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: 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);
}

.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: 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);
}

/* 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;
  background: var(--colour-ink);
}

.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);
}

/* ---------------------------------------------------------------
   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: 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;
}

.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: 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 {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 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;
  color: var(--colour-bg);
  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 {
  color: var(--colour-ink);
}

/* 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 {
  background: var(--colour-bg);
  color: var(--colour-ink);
}

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));
}

/* .project needs to be a positioning context for .project__back below;
   harmless everywhere else on the page (nothing else inside it relies
   on the default static positioning). */
body.template-project.has-hero .project {
  position: relative;
}

.project__back {
  display: block;
  max-width: var(--container-max);
  margin-inline: auto;
  padding: var(--space-2) var(--container-pad) 0;
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--colour-ink-soft);
}

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

/* Heroed pages only: lift the back link out of normal flow and place it
   over the hero itself, clearing the fixed header's own height (~5.5rem
   — logo + padding, section 5) rather than sitting flush under it. */
body.template-project.has-hero .project__back {
  position: absolute;
  top: 5.5rem;
  /* Same left edge as every max-width-centred block below it (.project__intro
     etc.), even once the viewport exceeds --container-max and that
     centred content stops hugging the true screen edge. */
  left: max(var(--container-pad), calc((100% - var(--container-max)) / 2 + var(--container-pad)));
  z-index: 40;
  padding: 0;
  color: var(--colour-ink);
}

/* ---------------------------------------------------------------
   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;
  background: var(--colour-ink);
  color: var(--colour-bg);
  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: 0.96;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-2);
  color: var(--colour-ink);
}

.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: 0.08em;
  color: var(--colour-ink);
  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-ink);
  border-color: var(--colour-ink);
  color: var(--colour-bg);
}

.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: 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);
  color: var(--colour-bg);
  padding: 0.3em 0.85em;
  border-radius: 999px;
}

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

.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 {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 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: -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: 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 {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--colour-ink-soft);
  margin-bottom: var(--space-2);
}

.studio-hero__title {
  font-family: var(--font-display);
  font-size: var(--step-display);
  line-height: 1.05;
  letter-spacing: -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 {
  font-size: var(--step-small);
  color: var(--colour-ink-soft);
}

.studio-scatter__text {
  font-size: var(--step-lead);
  color: var(--colour-ink-soft);
  max-width: 48ch;
  overflow-wrap: break-word;
}

.studio-scatter__text--a {
  font-size: var(--step-h2);
  line-height: 1.3;
  color: var(--colour-ink);
  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;
}

@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;
  }

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

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

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

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

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

/* --- 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: var(--font-display);
  font-size: var(--step-h1);
  line-height: 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);
}

.studio-panel--dark .studio-panel__sheet {
  background: var(--colour-ink);
  color: var(--colour-bg);
  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: var(--font-display);
  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: var(--font-display);
  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 {
  font-size: var(--step-h2);
}

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

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

.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: var(--font-display);
  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: 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 img,
.about__figure picture,
.about__figure video {
  aspect-ratio: 1 / 1;
  object-fit: contain;
}

.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);
  line-height: 1;
  letter-spacing: -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: -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: 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: -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);
  color: var(--colour-bg);
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 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 {
  background: var(--colour-bg);
  color: var(--colour-ink);
}

/* ---------------------------------------------------------------
   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 {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 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;
}

.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: 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: 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: 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 {
  font-size: var(--step-small);
  text-transform: uppercase;
  letter-spacing: 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;
}
