/* ===========================================================================
   Rising High Productions — design system
   ---------------------------------------------------------------------------
   Monochrome, no italics. Two rules drive everything below the tokens:

   1. There is no colour. With black, white and grey only, hierarchy has to be
      built from brightness, weight and scale — so nothing is "the accent
      colour"; things are simply nearer to white the more they matter.
      Full white is reserved for headings and the primary button. Everything
      supporting sits at 86%, 55% or --grey, and hover states move a step
      brighter or dimmer rather than changing hue.
   2. There is no font outside Archivo and Inter, and no italic in either.
      Emphasis is weight and brightness. Archivo appears twice: compressed
      extrabold for every headline, normal width at 700 for kickers.
   =========================================================================== */

/* ---------- fonts (self-hosted) ------------------------------------------
   Built by scripts/build-fonts.py from the upstream Google Fonts releases in
   tools/fonts/, subset to Latin plus the punctuation this site actually sets.
   73 KB for all five faces.

   Self-hosted rather than linked from fonts.googleapis.com because the heading
   is the LCP element: via Google it could not paint until three round trips
   finished, and mobile FCP swung between 0.8s and 2.7s run to run. Same
   typefaces, same weights — see SETUP.md.
   ------------------------------------------------------------------------- */
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 300;
  font-display: swap; src: url('../fonts/inter-300.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('../fonts/inter-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('../fonts/inter-600.woff2') format('woff2');
}
/* The display face: Archivo's variable font with the width axis pinned at 62
   and the weight axis left free — a compressed grotesque that sets like a
   poster. It replaced Fraunces, which was correct but quiet: a soft serif at
   400 has nothing to give at 96px on black, and every heading on this site is
   a headline for an event. One file covers the full weight range, so
   `.display em` steps down a real weight rather than only an opacity. */
@font-face {
  font-family: 'Archivo Display'; font-style: normal; font-weight: 200 900;
  font-display: swap; src: url('../fonts/archivo-display.woff2') format('woff2');
}
@font-face {
  font-family: 'Archivo'; font-style: normal; font-weight: 700;
  font-display: swap; src: url('../fonts/archivo-700.woff2') format('woff2');
}

/* ---------- tokens ------------------------------------------------------- */
:root {
  --ink: #0A0A0B;
  --panel: #131315;
  --raised: #1A1A1D;
  --white: #FFFFFF;
  --grey: #9A9AA2;

  /* Monochrome. There is no colour accent on this site: emphasis is carried by
     weight, scale, and how bright a thing is against the black — kickers and
     rules are white at low alpha, the primary button is a white fill. */
  --accent: #FFFFFF;
  --accent-dim: rgba(255, 255, 255, 0.6);

  /* Derived. Body text sits at 86% so it reads a step below headings, which
     are full white. */
  --body: rgba(255, 255, 255, 0.86);
  --hairline: rgba(255, 255, 255, 0.24);
  --edge: rgba(255, 255, 255, 0.09);

  /* Form validation stays monochrome too. An invalid field goes to a solid
     white border against the panel and states the problem in words — colour
     was never carrying that meaning on its own anyway. */
  --alert: #FFFFFF;
  --alert-text: rgba(255, 255, 255, 0.86);

  --font-display: 'Archivo Display', 'Archivo', 'Inter', system-ui, sans-serif;
  --font-ui: 'Inter', 'Archivo', system-ui, sans-serif;
  --font-kicker: 'Archivo', 'Inter', system-ui, sans-serif;

  /* The whole scale. Nothing renders at a size that is not on this list. */
  --t12: 0.75rem;
  --t14: 0.875rem;
  --t16: 1rem;
  --t22: 1.375rem;
  --t32: 2rem;
  --t44: 2.75rem;
  --t64: 4rem;
  --t96: 6rem;

  --gut: clamp(1.25rem, 5vw, 2.5rem);
  --maxw: 1240px;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur: 200ms;
}

/* ---------- reset -------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  font-family: var(--font-ui);
  /* The UA default here is black. Nothing paints it, but it is the one colour
     on the page outside the palette, so it is pinned to a token. */
  color: var(--body);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
body {
  margin: 0;
  background: var(--ink);
  color: var(--body);
  font-size: var(--t16);
  font-weight: 300;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
/* Any rule that sets `display` on a class outranks the UA sheet's
   [hidden] { display: none }, so an element toggled with el.hidden = true stays
   on screen. That bit the admin panel twice (a sign-out button and a button
   pair that both use display:flex), so it is settled once, here. */
[hidden] { display: none !important; }

img, svg, video { display: block; max-width: 100%; }
img { height: auto; }
/* With no accent hue, a bare link is indistinguishable from body text, so
   inline links carry an underline and brighten on hover. Links that are
   already unmistakable — nav items, buttons, cards — opt out via their own
   rules below. */
a {
  color: var(--white);
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, 0.35);
  text-underline-offset: 0.2em;
  transition: color var(--dur) var(--ease), text-decoration-color var(--dur) var(--ease);
}
a:hover { color: var(--white); text-decoration-color: var(--white); }
.nav a, .ftr a, .hdr__logo, .btn, .link, .ecard, .marquee__item, .backlink, .socials a,
.gallery a, .contact-list a, .lang__menu a { text-decoration: none; }
.ftr a:hover, .contact-list a:hover { text-decoration: underline; }
button { font: inherit; color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
ul { margin: 0; padding: 0; list-style: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--accent); color: var(--ink); padding: 0.75rem 1.25rem;
  font: 600 var(--t13, var(--t14))/1 var(--font-ui);
}
.skip:focus { left: 0; }

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- typography --------------------------------------------------- */
/* Headings are set in compressed extrabold caps. Three things make that work
   rather than just shout: the weight is 800 (900 closes the counters up at
   small sizes), the caps get a touch of positive tracking so the compressed
   forms do not collide, and the line-height goes tighter than a lowercase
   setting could take — caps have no descenders, so 0.94 still leaves air. */
.display {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: var(--white);
  text-wrap: balance;
}
h1.display { font-size: clamp(var(--t44), 8.4vw, var(--t96)); line-height: 0.94; letter-spacing: -0.005em; }
h2.display { font-size: clamp(var(--t32), 5.2vw, var(--t64)); line-height: 0.96; }
h3.display { font-size: var(--t32); line-height: 1; }
h4.display { font-size: var(--t22); line-height: 1.1; letter-spacing: 0.01em; }

.display em {
  /* Emphasis inside a heading, without italics. The display face keeps its
     weight axis, so this drops from 800 to 300 — at this width that reads as a
     different voice entirely — and dims with it. */
  font-weight: 300;
  font-style: normal;
  color: rgba(255, 255, 255, 0.62);
}

.kicker {
  display: block;
  font-family: var(--font-kicker);
  font-weight: 700;
  font-size: 0.6875rem; /* 11px, per the brief — kicker-only exception */
  letter-spacing: 0.3em;
  text-transform: uppercase;
  /* Quieter than the heading it labels. At full white a kicker fights the
     display type directly underneath it and the pair reads as one block. */
  color: rgba(255, 255, 255, 0.55);
  margin: 0 0 1.25rem;
}

.lede {
  font-size: var(--t22);
  line-height: 1.5;
  color: var(--body);
  /* 34ch was cut for a one-line standfirst. The current copy is a full
     sentence with three clauses in it, and at 34ch it ran to six lines down
     the left edge of the hero. */
  max-width: 46ch;
}

.prose { max-width: 68ch; }
.prose p + p { margin-top: 1.1em; }

.muted { color: var(--grey); }
.small { font-size: var(--t14); }
.tiny { font-size: var(--t12); }

/* ---------- layout ------------------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gut);
}
.section { padding-block: clamp(5rem, 11vw, 9rem); }
.section--panel { background: var(--panel); }
/* No hairline between sections. The ink/panel alternation already separates
   them, and a rule on top of that chopped the page into visible slices — the
   single biggest reason the home page read as "packed" rather than composed.
   The only place a border is still wanted is where two sections share a
   background, which is just the stats/brands pair. */
.section--tight + .section--tight { border-top: 1px solid var(--edge); }

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(1.5rem, 3vw, 2.5rem);
  margin-bottom: clamp(3rem, 6vw, 5rem);
}
.section-head .kicker { margin-bottom: 0.875rem; }
.section-head p { max-width: 46ch; margin: 0; }

/* A hairline rule — white at low alpha. */
.rule { height: 1px; background: var(--hairline); border: 0; margin: 0; }

/* ---------- buttons ------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.9375rem 1.5rem;
  border-radius: 8px;
  border: 1px solid transparent;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 0.8125rem; /* 13px, per the brief — button-only exception */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.btn--primary { background: var(--white); color: var(--ink); }
/* Hover dims rather than brightens — there is nothing brighter than white to
   go to, so the feedback has to come off the top end. */
.btn--primary:hover { background: rgba(255, 255, 255, 0.82); color: var(--ink); }

.btn--ghost {
  border-color: rgba(255, 255, 255, 0.38); color: var(--white); background: transparent;
}
.btn--ghost:hover { border-color: var(--white); background: rgba(255, 255, 255, 0.08); }

.btn[disabled], .btn[aria-disabled='true'] {
  opacity: 0.45; cursor: not-allowed; transform: none;
}

.btn-row { display: flex; flex-wrap: wrap; gap: 0.875rem; }

/* Standalone link with a rule that grows in from the left on hover. */
.link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-ui); font-weight: 600;
  font-size: var(--t14); letter-spacing: 0.04em;
  color: var(--accent);
  padding-bottom: 2px;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--dur) var(--ease), color var(--dur) var(--ease);
}
.link:hover { background-size: 100% 1px; color: var(--accent); }

/* ---------- header ------------------------------------------------------- */
.hdr {
  position: fixed; inset: 0 0 auto; z-index: 100;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}
.hdr.is-stuck {
  background: rgba(10, 10, 11, 0.86);
  backdrop-filter: blur(14px);
  border-bottom-color: var(--edge);
}
.hdr__in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; height: 76px;
}
.hdr__logo { display: flex; align-items: center; }
/* The boxed logo, not the bare wordmark: it is the mark the client actually
   uses, and a white slab in the top-left is the one place on this site where a
   solid block of white is doing the work of a brand rather than of a button.
   Narrower than the old wordmark because the box adds its own bulk. */
.hdr__logo img { width: 190px; height: auto; }
@media (max-width: 420px) { .hdr__logo img { width: 138px; } }

/* ---------- language switcher --------------------------------------------
   A <details> rather than a scripted menu: it opens, closes and takes keyboard
   focus on its own, so the switcher still works with no JS at all — which
   matters more here than anywhere else on the site, because someone who cannot
   read the page is exactly who needs this control.

   It sits in the header bar at every width, not inside the nav. Behind the
   burger on a phone it was a control nobody could find.

   Every link is absolute and every build ships the same markup; the only thing
   that differs per language is data-lang-current, which selects both the pair
   shown on the button and the row marked as current. That is why the
   translation pass can leave this alone.

   The flags are the one deliberate exception to the monochrome palette. A grey
   flag is not a flag, and being recognisable at a glance is the whole job here.
   -------------------------------------------------------------------------- */
.lang { position: relative; flex: none; }
/* One small box, one flag. No code, no caret — at 22x15 the flag is the label,
   and anything else in there made a control that only ever does one thing look
   like it does several. */
.lang summary {
  display: flex; align-items: center; justify-content: center;
  width: 38px; height: 30px;
  border: 1px solid var(--edge); border-radius: 7px;
  cursor: pointer; list-style: none;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.lang summary::-webkit-details-marker { display: none; }
.lang summary:hover { border-color: var(--hairline); background: var(--raised); }
.lang summary:focus-visible { outline: none; border-color: var(--accent); }
.lang[open] summary { border-color: var(--accent); background: var(--raised); }

.flag { display: block; flex: none; border-radius: 2px; }
.lang summary .flag { width: 22px; height: 15px; }

/* All four flags ship in the button; CSS shows the one that matches. */
.lang__cur { display: none; }
.lang[data-lang-current='en'] .lang__cur[data-lang='en'],
.lang[data-lang-current='es'] .lang__cur[data-lang='es'],
.lang[data-lang-current='nl'] .lang__cur[data-lang='nl'],
.lang[data-lang-current='fr'] .lang__cur[data-lang='fr'] { display: block; }

.lang__menu {
  position: absolute; z-index: 40; top: calc(100% + 0.5rem); right: 0;
  min-width: 10.5rem;
  padding: 0.3125rem;
  background: var(--raised);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.55);
}
.lang__menu a {
  display: flex; align-items: center; gap: 0.625rem;
  padding: 0.5rem 0.625rem;
  border-radius: 6px;
  font-size: var(--t14); font-weight: 300; color: var(--body);
}
.lang__menu a:hover { background: rgba(255, 255, 255, 0.08); color: var(--white); }
/* The current language is marked from the one attribute the build swaps. */
.lang[data-lang-current='en'] a[data-lang='en'],
.lang[data-lang-current='es'] a[data-lang='es'],
.lang[data-lang-current='nl'] a[data-lang='nl'],
.lang[data-lang-current='fr'] a[data-lang='fr'] {
  color: var(--white); background: rgba(255, 255, 255, 0.06);
}

/* Desktop: after the CTA, at the right-hand end of the bar. On mobile the
   order flips so it lands immediately left of the burger. */
.lang { order: 3; }
.nav { order: 2; }

.nav { display: flex; align-items: center; gap: 2rem; }
.nav__list { display: flex; align-items: center; gap: 2rem; }
.nav__foot { display: flex; align-items: center; }
/* Desktop hides the numerals, the contact block, and the duplicate Contact
   link — the "Partner with us" button already goes there. All three exist for
   the mobile panel. */
.nav__n, .nav__meta { display: none; }
.nav__list .nav__item:last-child { display: none; }
.nav__item a {
  font-family: var(--font-ui); font-weight: 400;
  font-size: var(--t14); color: var(--body); letter-spacing: 0.01em;
}
.nav__item a:hover, .nav__item a[aria-current='page'] { color: var(--accent); }

.nav-toggle {
  display: none; background: none; border: 1px solid var(--edge);
  border-radius: 8px; width: 44px; height: 44px; cursor: pointer;
  align-items: center; justify-content: center;
  transition: border-color var(--dur) var(--ease);
}
.nav-toggle:hover { border-color: var(--accent); }
.nav-toggle span {
  display: block; width: 18px; height: 1px; background: var(--white);
  position: relative;
  transition: background var(--dur) var(--ease);
}
.nav-toggle span::before, .nav-toggle span::after {
  content: ''; position: absolute; left: 0; width: 18px; height: 1px;
  background: var(--white);
  transition: transform var(--dur) var(--ease), top var(--dur) var(--ease);
}
.nav-toggle span::before { top: -6px; }
.nav-toggle span::after { top: 6px; }
.nav-toggle[aria-expanded='true'] span { background: transparent; }
.nav-toggle[aria-expanded='true'] span::before { top: 0; transform: rotate(45deg); }
.nav-toggle[aria-expanded='true'] span::after { top: 0; transform: rotate(-45deg); }

/* ---------- hero --------------------------------------------------------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex; align-items: flex-end;
  overflow: hidden;
  padding-block: 8rem 4.5rem;
}
.hero__media {
  position: absolute; inset: 0;
  z-index: 0;
}
.hero__media video, .hero__media img {
  width: 100%; height: 100%; object-fit: cover;
}
/* Two overlays: a flat knock-down so any frame of the footage stays dark
   enough for ivory type, then a gradient that lands the bottom edge exactly on
   --ink so the hero dissolves into the stats bar rather than butting against
   it. */
.hero__media::after {
  content: '';
  position: absolute; inset: 0;
  background:
    linear-gradient(to bottom, rgba(10,10,11,0.72) 0%, rgba(10,10,11,0.55) 38%, rgba(10,10,11,0.94) 82%, var(--ink) 100%),
    linear-gradient(to right, rgba(10,10,11,0.7) 0%, rgba(10,10,11,0.15) 60%);
}
.hero__in { position: relative; z-index: 1; width: 100%; }
.hero h1 { max-width: 15ch; margin-bottom: 1.75rem; }
.hero .lede { margin-bottom: 2.5rem; }

.hero__scroll {
  position: absolute; bottom: 1.75rem; right: var(--gut); z-index: 1;
  display: flex; align-items: center; gap: 0.75rem;
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey);
}
.hero__scroll i {
  display: block; width: 1px; height: 40px;
  background: linear-gradient(var(--accent), transparent);
}

/* Ken Burns fallback: used when there is no hero video, and it is the only
   thing the slideshow does — no crossfade tricks, just a slow push.

   The first frame is opaque in the *base* style and the keyframes start at
   opacity 1, so the hero is never black when the animation is not running.
   That case is not hypothetical: browsers suspend CSS animations in background
   tabs, and the reduced-motion block below collapses every animation to 0.01ms.
   With the old keyframes (which started at 0) both left all four frames
   transparent and the hero rendered as a black box with text on it. */
.kenburns { position: absolute; inset: 0; }
/* The slide is the <picture>, not the <img>. Targeting the img broke as soon as
   the portrait <source> was added: inside a <picture> the <img> is the SECOND
   child, so `img:first-child` matched nothing and every frame stayed at
   opacity 0 (black hero), while `img:nth-child(2)` matched all four at once and
   gave them the same delay. */
.kenburns picture {
  position: absolute; inset: 0; display: block;
  opacity: 0;
  animation: kb 32s var(--ease) infinite;
}
.kenburns picture img { width: 100%; height: 100%; object-fit: cover; }
.kenburns picture:first-child { opacity: 1; }
/* 8s apart over a 32s cycle: each frame holds ~8s with a ~2s crossfade, and
   frame 4 hands back to frame 1 exactly as the loop restarts. */
.kenburns picture:nth-child(2) { animation-delay: 8s; }
.kenburns picture:nth-child(3) { animation-delay: 16s; }
.kenburns picture:nth-child(4) { animation-delay: 24s; }
@keyframes kb {
  0%   { opacity: 1; transform: scale(1.0); }
  25%  { opacity: 1; }
  31%  { opacity: 0; transform: scale(1.09); }
  100% { opacity: 0; transform: scale(1.09); }
}

/* ---------- stats -------------------------------------------------------- */
/* auto-fit, not a fixed four: three of the four figures were invented and came
   off the site, and a hard 4-column grid left the survivor sitting in the
   left quarter with three empty cells beside it. auto-fit lets however many
   real numbers there are share the width evenly. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
.stat { padding: clamp(2rem, 4vw, 3rem) 1.5rem; text-align: center; }
.stat + .stat { border-left: 1px solid var(--edge); }
.stat__n {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(var(--t32), 5.4vw, var(--t64));
  line-height: 1.05; letter-spacing: -0.005em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.stat__l {
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey); margin-top: 0.75rem;
}

/* ---------- event cards -------------------------------------------------- */
.grid-events {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
  gap: clamp(1.5rem, 3vw, 2.25rem) clamp(1.25rem, 2.5vw, 1.75rem);
}
.ecard {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 8px;
  overflow: hidden;
  color: var(--body);
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.ecard:hover {
  transform: translateY(-4px);
  background: var(--raised);
  border-color: var(--hairline);
  color: var(--body);
}
.ecard__media {
  position: relative;
  aspect-ratio: 2 / 3;
  background: var(--raised);
  overflow: hidden;
}
.ecard__media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 400ms var(--ease);
}
.ecard:hover .ecard__media img { transform: scale(1.03); }
.ecard__body { padding: 1.25rem 1.25rem 1.5rem; }
.ecard__title {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--t22); line-height: 1.08; letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--white);
  margin-bottom: 0.625rem;
}
.ecard__meta {
  font-size: var(--t14); line-height: 1.5; color: var(--grey);
}
.ecard__meta b { display: block; font-weight: 400; color: var(--body); }

.badge {
  position: absolute; top: 0.875rem; left: 0.875rem; z-index: 1;
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.375rem 0.625rem;
  border-radius: 8px;
  background: rgba(10, 10, 11, 0.82);
  backdrop-filter: blur(6px);
  border: 1px solid var(--edge);
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--body);
}
.badge i {
  width: 5px; height: 5px; border-radius: 50%; background: var(--grey);
}
.badge--tickets { color: var(--accent); border-color: var(--hairline); }
.badge--tickets i { background: var(--accent); }
.badge--sold-out { color: var(--grey); }

/* ---------- case study --------------------------------------------------- */
.case { display: grid; gap: clamp(2.5rem, 6vw, 5.5rem); }
@media (min-width: 900px) {
  .case { grid-template-columns: 1.05fr 1fr; align-items: center; }
}
.case__photos {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: 0.875rem;
  /* Square overall, so the block fills its column instead of floating in the
     middle of it at whatever height three photos happen to add up to. */
  aspect-ratio: 1 / 1;
}
.case__photos figure { margin: 0; overflow: hidden; border-radius: 8px; }
.case__photos figure:first-child { grid-row: span 2; }
.case__photos img {
  width: 100%; height: 100%; object-fit: cover;
  will-change: transform;
}
.case__stats {
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 4vw, 3rem);
  margin: 2rem 0;
  padding-top: 2rem;
  border-top: 1px solid var(--hairline);
}
.case__stats div span {
  display: block;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--t44); line-height: 1; color: var(--accent);
}
.case__stats div small {
  display: block; margin-top: 0.5rem;
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey);
}
blockquote {
  margin: 0; padding-left: 1.5rem;
  border-left: 1px solid var(--hairline);
}
blockquote p {
  /* The one place the display face is wrong: a pull quote is a sentence
     someone said, and compressed extrabold caps turn a sentence into a poster
     you have to decode. It sets in the body face, one weight up from the
     surrounding text and full white. */
  font-family: var(--font-ui); font-weight: 300; font-style: normal;
  font-size: var(--t22); line-height: 1.45; letter-spacing: -0.005em;
  color: var(--white);
}
blockquote cite {
  display: block; margin-top: 0.875rem;
  font-family: var(--font-kicker); font-weight: 700; font-style: normal;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey);
}

/* ---------- about / team ------------------------------------------------- */
.about { display: grid; gap: clamp(3rem, 7vw, 6rem); }
@media (min-width: 900px) { .about { grid-template-columns: 0.85fr 1fr; } }

.team {
  display: grid; gap: clamp(1.25rem, 3vw, 2rem);
  /* auto-FIT, not auto-fill: four members in a grid wide enough for six left
     two empty tracks and the row read as left-aligned against a gap. auto-fit
     collapses the empty tracks so however many people there are, they span the
     full width. */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.team figure { margin: 0; }
/* Deliberately empty slot: a flat panel with a hairline, not a grey person
   icon. Reads as "photo pending" instead of as a broken image. */
.team__slot {
  aspect-ratio: 4 / 5;
  border-radius: 8px;
  background: var(--raised);
  border: 1px solid var(--edge);
  display: flex; align-items: center; justify-content: center;
}
.team__slot::after {
  content: ''; width: 28px; height: 1px; background: var(--hairline);
}
.team figcaption { margin-top: 0.875rem; }
.team figcaption b {
  display: block; font-weight: 400; font-size: var(--t16); color: var(--white);
}
.team figcaption span { font-size: var(--t14); color: var(--grey); }

/* ---------- partners ----------------------------------------------------- */
.venues {
  display: grid; gap: 0;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  border-top: 1px solid var(--edge);
}
.venue {
  padding: 1.75rem 0;
  border-bottom: 1px solid var(--edge);
}
.venue b {
  display: block;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--t22); line-height: 1.1; letter-spacing: 0.01em;
  text-transform: uppercase; color: var(--white);
}
.venue span { font-size: var(--t14); color: var(--grey); }
@media (min-width: 600px) {
  .venues { column-gap: clamp(1.5rem, 4vw, 3rem); }
}

.press { display: grid; gap: clamp(1.5rem, 4vw, 2.5rem); margin-top: clamp(2.5rem, 5vw, 4rem); }
@media (min-width: 760px) { .press { grid-template-columns: 1fr 1fr; } }

/* ---------- gallery ------------------------------------------------------ */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 0.875rem;
}
/* Uniform cells, deliberately. Feature cells at double scale were tried and
   dropped: any cell spanning 2x2 consumes four grid slots, so 16 photos stop
   being a whole number of rows and the wall ends on a visible hole in the
   bottom-right. Sixteen equal 3:2 cells tile exactly at the four-column
   desktop layout and at two columns, and the photography carries the variety
   on its own. */
.gallery figure {
  position: relative; margin: 0; overflow: hidden; border-radius: 8px;
  aspect-ratio: 3 / 2;
  background: var(--raised);
}
.gallery img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 400ms var(--ease), opacity var(--dur) var(--ease);
}
.gallery button {
  display: block; padding: 0; border: 0; background: none; cursor: zoom-in;
  width: 100%; height: 100%;
}
.gallery figcaption {
  position: absolute; inset: auto 0 0 0;
  padding: 2.5rem 1rem 0.875rem;
  background: linear-gradient(transparent, rgba(10, 10, 11, 0.85));
  font-size: var(--t12); color: var(--body);
  letter-spacing: 0.02em;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  pointer-events: none;
  text-align: left;
}
.gallery figure:hover figcaption, .gallery button:focus-visible + figcaption { opacity: 1; transform: none; }

.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: none; align-items: center; justify-content: center;
  padding: var(--gut);
  background: rgba(10, 10, 11, 0.96);
  backdrop-filter: blur(8px);
}
.lightbox[open], .lightbox.is-open { display: flex; }
.lightbox img { max-width: min(1100px, 100%); max-height: 84vh; object-fit: contain; border-radius: 8px; }
.lightbox__cap {
  position: absolute; left: 0; right: 0; bottom: 1.5rem;
  text-align: center; font-size: var(--t14); color: var(--grey);
}
.lightbox__x {
  position: absolute; top: 1.25rem; right: 1.25rem;
  width: 44px; height: 44px; border-radius: 8px;
  background: none; border: 1px solid var(--edge); color: var(--white);
  cursor: pointer; font-size: var(--t22); line-height: 1;
  transition: border-color var(--dur) var(--ease);
}
.lightbox__x:hover { border-color: var(--accent); }

/* ---------- contact / form ----------------------------------------------- */
.contact { display: grid; gap: clamp(2.5rem, 6vw, 4.5rem); }
@media (min-width: 900px) { .contact { grid-template-columns: 0.8fr 1fr; } }

.field { margin-bottom: 1.25rem; }
.field label {
  display: block; margin-bottom: 0.5rem;
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey);
}
.field input, .field select, .field textarea {
  width: 100%;
  padding: 0.875rem 1rem;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 8px;
  color: var(--white);
  font-family: var(--font-ui); font-weight: 300; font-size: var(--t16);
  line-height: 1.5;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.field textarea { resize: vertical; min-height: 148px; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--accent); background: var(--raised);
}
.field input::placeholder, .field textarea::placeholder { color: rgba(154, 154, 162, 0.7); }
.field select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--accent) 50%),
                    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% + 2px), calc(100% - 14px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 2.75rem;
}
.field select option { background: var(--panel); color: var(--white); }
/* ---------- styled select -------------------------------------------------
   A native <select> cannot be styled past its border: the option list is drawn
   by the OS, so on this site it opened as a small grey system menu in the
   middle of a black page. This replaces the *list* only. The native control
   stays in the DOM, keeps the name, keeps required, keeps submitting — it is
   simply moved out of sight once js/main.js has built the listbox, and it comes
   straight back if the script does not run. See buildSelects() in js/main.js.
   -------------------------------------------------------------------------- */
.sel { position: relative; }
/* The native control is still here and still submitting — just not visible and
   not a second tab stop. Not display:none: a hidden-but-present control is what
   keeps `required` and the form's own validation working against real state. */
.sel > select {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
  border: 0; padding: 0; margin: 0;
}
.sel__btn {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  width: 100%;
  padding: 0.875rem 1rem;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 8px;
  color: var(--white);
  font-family: var(--font-ui); font-weight: 300; font-size: var(--t16);
  line-height: 1.5;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.sel__btn:hover { background: var(--raised); }
.sel__btn:focus-visible { outline: none; border-color: var(--accent); background: var(--raised); }
.sel.is-open .sel__btn { border-color: var(--accent); background: var(--raised); }
/* The caret rotates rather than swapping glyph, so open and closed are the same
   mark in two positions. */
.sel__caret {
  flex: none; width: 9px; height: 9px;
  border-right: 1px solid var(--accent); border-bottom: 1px solid var(--accent);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 260ms var(--ease);
}
.sel.is-open .sel__caret { transform: translateY(2px) rotate(225deg); }

.sel__list {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 0.5rem);
  margin: 0; padding: 0.375rem;
  max-height: 17rem; overflow-y: auto;
  background: var(--raised);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.55);
  /* Closed state is scaled and lifted rather than display:none, so the list has
     something to animate from. pointer-events keeps it un-clickable while it is
     invisible; visibility keeps it out of the tab order. */
  opacity: 0; visibility: hidden;
  transform: translateY(-6px) scale(0.98);
  transform-origin: top center;
  transition: opacity 200ms var(--ease), transform 260ms var(--ease),
              visibility 0s linear 260ms;
}
.sel.is-open .sel__list {
  opacity: 1; visibility: visible;
  transform: none;
  transition: opacity 180ms var(--ease), transform 260ms var(--ease),
              visibility 0s;
}
.sel__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.6875rem 0.75rem;
  border-radius: 6px;
  color: var(--body);
  font-size: var(--t16); font-weight: 300; line-height: 1.4;
  cursor: pointer;
}
/* One highlight for hover and for keyboard, so arrowing through the list looks
   the same as running a mouse down it. */
.sel__opt.is-active { background: rgba(255, 255, 255, 0.08); color: var(--white); }
.sel__opt[aria-selected='true'] { color: var(--white); }
/* The tick is drawn, not a glyph — no font outside the two brand faces. */
.sel__opt[aria-selected='true']::after {
  content: ''; flex: none;
  width: 11px; height: 6px;
  border-left: 1.5px solid var(--accent); border-bottom: 1.5px solid var(--accent);
  transform: translateY(-2px) rotate(-45deg);
}
.field.is-invalid .sel__btn { border-color: var(--alert); }

@media (prefers-reduced-motion: reduce) {
  .sel__caret, .sel__list { transition: none; }
}

/* Contact-form honeypot. Not display:none — some bots skip fields that are. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.hp input, .hp label { color: var(--ink); background: var(--ink); border: 0; }

.field--half { display: grid; gap: 1.25rem; }
@media (min-width: 560px) { .field--half { grid-template-columns: 1fr 1fr; } }

.field.is-invalid input, .field.is-invalid select, .field.is-invalid textarea {
  border-color: var(--alert);
}
.field__err {
  display: none; margin-top: 0.5rem; font-size: var(--t14); color: var(--alert);
}
.field.is-invalid .field__err { display: block; }

.form-note {
  margin-top: 1.25rem; padding: 1rem 1.25rem;
  border-radius: 8px; border: 1px solid var(--edge);
  font-size: var(--t14);
  display: none;
}
.form-note.is-on { display: block; }
.form-note--ok { border-color: var(--hairline); color: var(--white); }
.form-note--err { border-color: var(--alert); color: var(--alert-text); }

.contact-list li { padding: 1.125rem 0; border-bottom: 1px solid var(--edge); }
.contact-list b {
  display: block; font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey); margin-bottom: 0.375rem;
}

.socials { display: flex; gap: 0.75rem; margin-top: 1.5rem; }
.socials a {
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 8px;
  border: 1px solid var(--edge); color: var(--body);
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.socials a:hover { border-color: var(--accent); color: var(--accent); }
.socials svg { width: 18px; height: 18px; }

/* ---------- footer ------------------------------------------------------- */
.ftr { background: var(--panel); border-top: 1px solid var(--edge); }
.ftr__in { padding-block: clamp(3rem, 6vw, 4.5rem) 2rem; }
.ftr__top {
  display: grid; gap: 2.5rem;
  padding-bottom: 2.5rem; border-bottom: 1px solid var(--edge);
}
@media (min-width: 760px) { .ftr__top { grid-template-columns: 1.2fr 1fr 1fr; } }
/* The lockup carries seven sub-brand marks under the wordmark, and those go
   illegible before the wordmark does — hence a floor of 260px rather than the
   190px the wordmark alone sat at. */
.ftr__logo img { width: clamp(260px, 26vw, 330px); }
.ftr-h {
  margin: 0 0 1rem;
  /* Rendered as an <h2> for heading order; sized as a small label. */
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}
.ftr li { margin-bottom: 0.625rem; }
.ftr li a { color: var(--body); font-size: var(--t14); }
.ftr li a:hover { color: var(--accent); }
.ftr__base {
  display: flex; flex-wrap: wrap; gap: 1rem;
  justify-content: space-between; align-items: center;
  padding-top: 2rem;
  font-size: var(--t12); color: var(--grey);
}

/* ---------- event detail page -------------------------------------------- */
.ev-hero { padding-top: 8.5rem; }
.ev-hero__in { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
@media (min-width: 860px) { .ev-hero__in { grid-template-columns: 0.72fr 1fr; } }
.ev-poster {
  border-radius: 8px; overflow: hidden; border: 1px solid var(--edge);
  background: var(--panel);
}
.ev-facts { margin: 2rem 0; border-top: 1px solid var(--hairline); }
.ev-facts div {
  display: grid; gap: 0.25rem 1.5rem; padding: 1rem 0;
  border-bottom: 1px solid var(--edge);
}
@media (min-width: 520px) { .ev-facts div { grid-template-columns: 160px 1fr; } }
.ev-facts dt {
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--grey);
  padding-top: 0.2rem;
}
.ev-facts dd { margin: 0; color: var(--white); }

.backlink {
  display: inline-flex; align-items: center; gap: 0.625rem;
  font-family: var(--font-ui); font-size: var(--t14); color: var(--grey);
  margin-bottom: 1.75rem;
}
.backlink:hover { color: var(--accent); }

/* ---------- 404 ---------------------------------------------------------- */
.err {
  min-height: 100svh; display: flex; align-items: center;
  text-align: center;
}
.err .display { margin-bottom: 1.5rem; }
.err p { margin-inline: auto; max-width: 46ch; }
.err .btn-row { justify-content: center; margin-top: 2.5rem; }

/* ---------- motion ------------------------------------------------------- */
/* Fade-up on scroll. .reveal is set by js/main.js only when IntersectionObserver
   is available, so with JS off everything is simply visible.

   The revealed state fades to var(--rest), not to a literal 1, because some
   revealed elements are meant to settle below full opacity — the brand marks
   sit at 0.62. Hard-coding 1 here put those two rules in a specificity fight
   that the dimmed state won, and the marks never animated in at all. */
.reveal {
  --rest: 1;
  --reveal-y: 18px;
  opacity: 0;
  transform: translate3d(0, var(--reveal-y), 0);
  transition: opacity 700ms var(--ease), transform 700ms var(--ease);
}
/* Note what this does NOT do: set `transform: none`. It used to, and that
   silently destroyed any resting transform the element wanted for itself —
   `.reveal.is-in` (0,2,0) outranks a component rule like `.gallery figure`
   (0,1,1), so the gallery's cursor drift computed to `none` with nothing in the
   console to say why. Moving the offset into a custom property means the
   revealed state changes a value instead of overwriting the property, and
   components compose their own transform on top. */
.reveal.is-in { opacity: var(--rest); --reveal-y: 0px; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Hold on frame one rather than flashing through all four in 0.01ms. */
  .kenburns picture { animation: none !important; }
  .kenburns picture:not(:first-child) { display: none; }
  .reveal { opacity: 1; transform: none; }
  .btn:hover, .ecard:hover, .marquee__item:hover { transform: none; }
  .ecard:hover .ecard__media img, .gallery figure:hover img { transform: none; }
}

/* ---------- responsive --------------------------------------------------- */
/* ---------- mobile nav panel ---------------------------------------------
   Full height rather than a dropdown. The old version covered about a third of
   the screen with body-size links while the page kept scrolling behind it,
   which read as a browser menu rather than as part of this site. This is the
   site's own voice: display type, the same 01/02 numerals the event carousel
   uses, and the contact details someone opening a menu on a phone is usually
   after.
   ------------------------------------------------------------------------- */
@media (max-width: 860px) {
  /* Both sit above the panel: the panel is fixed and so paints over its
     non-positioned siblings, which buried the wordmark. The brand and the
     close control belong together at the top of an open menu. */
  .nav-toggle { display: flex; position: relative; z-index: 2; }
  .hdr__logo { position: relative; z-index: 2; }

  /* Next to the burger, and above the panel — the panel is a fixed overlay, so
     without a z-index of its own the button would disappear underneath the
     thing it sits beside. */
  .lang { order: 1; position: relative; z-index: 2; margin-left: auto; }
  .nav-toggle { order: 2; }

  .nav {
    position: fixed;
    /* 100dvh, not 100vh: on mobile Safari the URL bar makes vh taller than the
       visible area, which pushes the contact block off the bottom of a panel
       that is supposedly full height. */
    inset: 0;
    height: 100dvh;
    display: flex; flex-direction: column; justify-content: space-between;
    align-items: stretch; gap: 0;
    padding: calc(76px + 2rem) var(--gut) calc(env(safe-area-inset-bottom) + 2.5rem);
    background: rgba(10, 10, 11, 0.94);
    backdrop-filter: blur(22px);
    -webkit-backdrop-filter: blur(22px);
    overflow-y: auto;
    opacity: 0; pointer-events: none;
    transition: opacity 320ms var(--ease);
  }
  .nav.is-open { opacity: 1; pointer-events: auto; }

  .nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav__list .nav__item:last-child { display: block; }

  .nav__item { border-bottom: 1px solid var(--edge); }
  .nav__item:first-child { border-top: 1px solid var(--edge); }
  .nav__item a {
    display: flex; align-items: baseline; gap: 1rem;
    padding: clamp(0.875rem, 2.6vh, 1.375rem) 0;
    font-family: var(--font-display); font-weight: 800;
    font-size: clamp(var(--t32), 9vw, var(--t44));
    line-height: 1.05; letter-spacing: 0.005em; text-transform: uppercase;
    color: var(--white);
    /* Each row slides up as the panel opens; the delay is set per item below. */
    transform: translateY(14px);
    opacity: 0;
    transition: transform 520ms var(--ease), opacity 520ms var(--ease);
  }
  .nav__n {
    display: block; flex: none;
    font-family: var(--font-kicker); font-weight: 700;
    font-size: 0.6875rem; letter-spacing: 0.3em;
    color: rgba(255, 255, 255, 0.42);
    transform: translateY(-0.35em);
  }
  .nav__item a[aria-current='page'] .nav__n { color: var(--white); }
  .nav__item a[aria-current='page'] .nav__t {
    /* The current page is marked with a rule rather than a colour, because
       there is no colour to mark it with. */
    box-shadow: inset 0 -1px 0 var(--white);
  }

  .nav.is-open .nav__item a { transform: none; opacity: 1; }
  .nav.is-open .nav__item:nth-child(1) a { transition-delay: 60ms; }
  .nav.is-open .nav__item:nth-child(2) a { transition-delay: 110ms; }
  .nav.is-open .nav__item:nth-child(3) a { transition-delay: 160ms; }
  .nav.is-open .nav__item:nth-child(4) a { transition-delay: 210ms; }

  .nav__foot {
    flex-direction: column; align-items: stretch; gap: 2rem;
    margin-top: 2.5rem;
    transform: translateY(14px); opacity: 0;
    transition: transform 520ms var(--ease), opacity 520ms var(--ease);
  }
  .nav.is-open .nav__foot { transform: none; opacity: 1; transition-delay: 260ms; }
  .nav__cta { justify-content: center; }

  .nav__meta { display: block; }
  .nav__meta a[href^='mailto'] {
    font-size: var(--t16); color: var(--white);
    text-decoration-color: rgba(255, 255, 255, 0.35);
  }
  .nav__meta p {
    margin: 0.5rem 0 0; font-size: var(--t14); color: var(--grey);
  }
  .nav__meta .socials { margin-top: 1.5rem; }

  /* Locked by js/main.js while the panel is open. position:fixed rather than
     overflow:hidden because iOS ignores the latter behind a fixed overlay; the
     JS keeps the scroll offset in `top` and restores it on close. */
  body.nav-open {
    position: fixed;
    left: 0; right: 0;
    width: 100%;
    overflow: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav__item a, .nav__foot { transform: none !important; opacity: 1 !important; }
}

@media (max-width: 700px) {
  .stats { grid-template-columns: 1fr 1fr; }
  .stat:nth-child(3) { border-left: 0; }
  .stat:nth-child(n + 3) { border-top: 1px solid var(--edge); }
  .hero__scroll { display: none; }
}

@media (max-width: 420px) {
  .grid-events { grid-template-columns: 1fr; }
  .hdr__logo img { width: 140px; }
}

/* ===========================================================================
   Ambient backdrop
   ---------------------------------------------------------------------------
   Fixed behind every page. Four layers, all drawn in CSS so they cost nothing
   to download: two very slow drifting radial washes that keep large flat areas
   of --ink from reading as dead space, a hairline column grid that gives the
   layout something to sit on, and a fine grain that stops the gradients
   banding on wide dark screens (8-bit gradients over near-black band badly).

   pointer-events:none throughout — none of it is ever in the way.
   =========================================================================== */
.backdrop {
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none; overflow: hidden;
  background: var(--ink);
}
.backdrop span { position: absolute; display: block; }

.backdrop__wash {
  width: 90vmax; height: 90vmax; border-radius: 50%;
  filter: blur(60px);
  opacity: 0.5;
  will-change: transform;
}
.backdrop__wash--a {
  top: -30vmax; left: -20vmax;
  background: radial-gradient(circle, rgba(255,255,255,0.07) 0%, transparent 62%);
  animation: drift-a 46s ease-in-out infinite alternate;
}
.backdrop__wash--b {
  bottom: -36vmax; right: -24vmax;
  background: radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 62%);
  animation: drift-b 62s ease-in-out infinite alternate;
}
@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(12vw, 8vh, 0) scale(1.15); }
}
@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-14vw, -6vh, 0) scale(1); }
}

/* Column grid, aligned to the content column so the hairlines land on the
   same rhythm as the layout rather than cutting across it. */
.backdrop__grid {
  inset: 0;
  max-width: var(--maxw);
  margin-inline: auto;
  background-image: repeating-linear-gradient(
    to right,
    rgba(255, 255, 255, 0.035) 0 1px,
    transparent 1px calc(100% / 6)
  );
  opacity: 0.9;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
}
@media (max-width: 760px) { .backdrop__grid { display: none; } }

/* Grain. An inline SVG feTurbulence, so it is a few hundred bytes and needs no
   image request. Kept very low so it reads as texture, not noise. */
.backdrop__grain {
  inset: -50%;
  width: 200%; height: 200%;
  opacity: 0.028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Sections sit on the backdrop, so the opaque ones need their own fill. */
.section--panel { position: relative; background: var(--panel); }
.section--tight { padding-block: clamp(3rem, 6vw, 5rem); }
.ftr { position: relative; }

/* ---------- scroll progress ---------------------------------------------- */
.progress {
  position: fixed; inset: 0 0 auto; z-index: 110;
  height: 2px; background: transparent; pointer-events: none;
}
.progress i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, rgba(255,255,255,0.25), var(--white));
  transform-origin: 0 50%;
}

/* ---------- page heads ---------------------------------------------------- */
.page-head { padding-top: clamp(8rem, 14vw, 11rem); }
.page-head .lede { max-width: 52ch; }

.eyebrow {
  margin: 0;
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  /* 0.50 is the floor, not a taste call: at 11px this is body text as far as
     WCAG is concerned, so it needs 4.5:1. White at 42% over --panel is 4.09:1
     and fails; 55% clears it on all three surface tokens with room to spare. */
  color: rgba(255, 255, 255, 0.55);
}

/* ---------- brand carousel ------------------------------------------------ */
.marquee {
  position: relative;
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  overflow: hidden;
  /* Fade both ends so marks enter and leave rather than being cut off by the
     viewport edge. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  cursor: grab;
}
.marquee.is-dragging { cursor: grabbing; }
.marquee__track {
  display: flex; width: max-content;
  will-change: transform;
}
.marquee__set {
  display: flex; align-items: center;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  padding-inline: clamp(1.25rem, 2.5vw, 2.25rem);
}
.marquee__item {
  display: flex; align-items: center; justify-content: center;
  height: 62px; flex: none;
  --rest: 0.55;
  opacity: var(--rest);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.marquee__item img { max-height: 62px; width: auto; object-fit: contain; }
.marquee:not(.is-dragging) .marquee__item:hover { --rest: 1; transform: translateY(-3px); }
.marquee__item:focus-visible { --rest: 1; }

/* ---------- split-line heading reveal ------------------------------------- */
/* js/main.js wraps each line of a [data-split] heading in .line > span. The
   span slides up from behind the .line's overflow, so the type appears to rise
   off the baseline instead of just fading. Falls back to a plain fade if the
   script never runs. */
[data-split] .line { display: block; overflow: hidden; }
[data-split] .line > span {
  display: block;
  transform: translateY(105%);
  transition: transform 900ms var(--ease);
}
[data-split].is-in .line > span { transform: none; }
[data-split] .line:nth-child(2) > span { transition-delay: 80ms; }
[data-split] .line:nth-child(3) > span { transition-delay: 160ms; }
[data-split] .line:nth-child(4) > span { transition-delay: 240ms; }

/* ---------- card affordance ----------------------------------------------- */
.ecard__go {
  display: inline-block; margin-top: 0.875rem;
  font-family: var(--font-ui); font-weight: 600;
  font-size: var(--t12); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--white);
  opacity: 0; transform: translateY(4px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.ecard:hover .ecard__go, .ecard:focus-visible .ecard__go { opacity: 1; transform: none; }
/* The label only appears on hover, so it must not add height when it does. */
.ecard__body { display: flex; flex-direction: column; }

@media (prefers-reduced-motion: reduce) {
  .backdrop__wash { animation: none !important; }
  .backdrop__grain { display: none; }
  [data-split] .line > span { transform: none !important; }
  .ecard__go { opacity: 1; transform: none; }
}

/* ===========================================================================
   Event carousel
   ---------------------------------------------------------------------------
   Native overflow scrolling with scroll-snap. No slider library, no transform
   maths: the browser already does momentum, touch, trackpad, keyboard arrows
   and focus-scrolling correctly, and every one of those is a thing a hand-rolled
   slider gets wrong. js/main.js only adds the arrows, the counter and the
   progress line on top.
   =========================================================================== */
.rail { position: relative; }

/* Full-bleed: the track escapes the content column so the last slide is cut by
   the viewport edge rather than stopping neatly inside it. A row that ends
   flush looks finished; one that runs off the edge reads as "there is more". */
.rail__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--rail-gut);
  /* Momentum on iOS, and no scrollbar chrome on any platform. */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  outline-offset: -2px;
}
.rail__viewport::-webkit-scrollbar { display: none; }

.rail {
  --rail-gut: max(var(--gut), calc((100vw - var(--maxw)) / 2 + var(--gut)));
}
.rail__track {
  display: flex;
  gap: clamp(1rem, 1.6vw, 1.5rem);
  padding-inline: var(--rail-gut);
  /* A flex row in a scroll container collapses its trailing padding in some
     engines; the pseudo-element guarantees the last slide can reach the snap
     position instead of jamming against the edge. */
  width: max-content;
}
.rail__track::after {
  content: '';
  flex: none;
  width: calc(var(--rail-gut) - clamp(1rem, 1.6vw, 1.5rem));
}
.rail__track > .ecard {
  flex: none;
  width: clamp(230px, 78vw, 340px);
  scroll-snap-align: start;
}
@media (min-width: 560px) { .rail__track > .ecard { width: clamp(280px, 42vw, 340px); } }

.rail__foot {
  display: flex; align-items: center; gap: clamp(1.5rem, 4vw, 3rem);
  max-width: var(--maxw);
  margin: clamp(1.75rem, 3vw, 2.5rem) auto 0;
  padding-inline: var(--gut);
}
.rail__progress {
  flex: 1; height: 1px; background: var(--edge); position: relative;
}
.rail__progress i {
  position: absolute; inset: 0 auto 0 0;
  display: block; width: 22%; background: var(--white);
  transform-origin: 0 50%;
  transition: transform 260ms var(--ease);
}
.rail__nav { display: flex; align-items: center; gap: 0.75rem; }
.rail__count {
  font-family: var(--font-kicker); font-weight: 700;
  font-size: 0.6875rem; letter-spacing: 0.3em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  font-variant-numeric: tabular-nums;
  margin-right: 0.5rem;
}
.rail__btn {
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; flex: none;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: transparent; color: var(--white); cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
              opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.rail__btn svg { width: 18px; height: 18px; }
.rail__btn:hover:not(:disabled) {
  border-color: var(--white); background: rgba(255, 255, 255, 0.08);
}
.rail__btn:active:not(:disabled) { transform: scale(0.94); }
.rail__btn:disabled { opacity: 0.28; cursor: default; }

/* Arrows are an enhancement — with JS off the row still scrolls, so showing
   dead buttons would be worse than not showing them. main.js unhides them. */
.rail__nav { visibility: hidden; }
.rail.is-live .rail__nav { visibility: visible; }

/* ---------- premium card pass -------------------------------------------- */
/* Slower, deeper hover: the poster drifts up behind a fixed frame while the
   card itself lifts, which reads as depth rather than as a jump. */
.ecard { transition: transform 420ms var(--ease), background 420ms var(--ease),
                     border-color 420ms var(--ease); }
.ecard__media img { transition: transform 900ms var(--ease); }
.ecard:hover .ecard__media img { transform: scale(1.05); }

/* Poster darkens from the bottom so the title beneath it feels attached to the
   image rather than floating under a hard edge. */
.ecard__media::after {
  content: '';
  position: absolute; inset: auto 0 0 0; height: 45%;
  background: linear-gradient(transparent, rgba(10, 10, 11, 0.55));
  opacity: 0; transition: opacity 420ms var(--ease);
  pointer-events: none;
}
.ecard:hover .ecard__media::after { opacity: 1; }

.ecard__num {
  position: absolute; top: 0.875rem; right: 1rem; z-index: 1;
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--t22); line-height: 1;
  color: var(--white);
  text-shadow: 0 1px 12px rgba(10, 10, 11, 0.9);
  opacity: 0.85;
}

.ecard__body {
  padding-top: 1.125rem;
  border-top: 1px solid var(--edge);
  margin-inline: 1.25rem;
  padding-inline: 0;
  transition: border-color 420ms var(--ease);
}
.ecard:hover .ecard__body { border-top-color: var(--hairline); }
.ecard__title { font-size: var(--t22); }
.ecard__meta b {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}

@media (prefers-reduced-motion: reduce) {
  .rail__viewport { scroll-behavior: auto; }
  .rail__progress i { transition: none; }
  .ecard:hover .ecard__media img { transform: none; }
}

/* Section head variant for the carousel: the supporting text and the "all
   events" link stack on the right so the heading keeps the full left column. */
.section-head--rail { align-items: flex-end; }
.section-head__aside {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 1rem; max-width: 46ch;
}
.section-head__aside p { margin: 0; }

/* Full-bleed section: the wrap inside still constrains the heading, but the
   rail is allowed to run to the viewport edge. */
.section--bleed > .wrap { margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.section--bleed .section-head { margin-bottom: 0; }

/* Equal-height cards in a flex row mean short cards get the tallest card's
   height. Rather than fight that (ragged tops look worse), the body grows to
   fill and the hover label is pushed to the bottom edge, so every card's
   baseline furniture lines up across the row. */
.ecard { display: flex; flex-direction: column; }
.ecard__body { flex: 1; display: flex; flex-direction: column; }
.ecard__go { margin-top: auto; padding-top: 1rem; }

/* Meta gets a little more air from the title now that it carries two lines on
   most cards. */
.ecard__meta { margin-bottom: 0; }
.ecard__meta b { margin-bottom: 0.25rem; }

/* ===========================================================================
   Motion layer
   ---------------------------------------------------------------------------
   Everything below is transform/opacity only, driven from a single rAF loop in
   js/main.js, and every piece is off under prefers-reduced-motion. The point is
   that the page should respond to the reader — to scroll position, to velocity,
   to where the cursor is — rather than playing one canned fade and stopping.
   =========================================================================== */

/* ---------- media reveal -------------------------------------------------- */
/* Photos scale down into their frame rather than fading in. The frame is
   already overflow:hidden, so the image starts overscaled and settles to fit —
   it reads as the image arriving, not as a div becoming opaque.

   The scale goes through a custom property so the reveal and the hover state
   can share one transform without overwriting each other. Setting `transform`
   directly in both places meant whichever rule won the cascade cancelled the
   other outright. */
[data-media] img { --media-scale: 1.16; }
[data-media].is-in img { --media-scale: 1; }
[data-media] img {
  transform: scale(var(--media-scale));
  transition: transform 1200ms var(--ease);
}
.ecard:hover [data-media].is-in img { --media-scale: 1.05; }
.gallery figure:hover img { --media-scale: 1.05; }

/* ---------- hero departure ------------------------------------------------ */
/* --hero-p runs 0 -> 1 as the hero scrolls out. The copy lifts and fades while
   the picture pushes in slightly, so the two layers separate on the way out
   instead of sliding as one flat sheet. */
.hero { --hero-p: 0; }
.hero__in {
  transform: translate3d(0, calc(var(--hero-p) * -70px), 0);
  opacity: calc(1 - var(--hero-p) * 1.25);
  will-change: transform, opacity;
}
.kenburns {
  transform: scale(calc(1 + var(--hero-p) * 0.09));
  transform-origin: 50% 35%;
}
.hero__scroll { opacity: calc(1 - var(--hero-p) * 2.4); }

/* ---------- magnetic controls --------------------------------------------- */
/* Buttons and the carousel arrows drift a few pixels toward the cursor while it
   is near them. Small enough to register as responsiveness rather than as a
   moving target — the offsets are capped in JS well below the hit area. */
/* Gated in JS, not here. js/main.js only attaches the pointer listeners when
   matchMedia('(hover: hover) and (pointer: fine)') passes, so --mx/--my simply
   never move on touch and these rules resolve to translate3d(0,0,0). Wrapping
   them in the same media query as well made the whole effect unverifiable —
   headless Chrome reports the query as true to matchMedia but does not apply
   the CSS block, so there was no way to confirm the transform was correct. */
.btn, .rail__btn {
  --mx: 0px; --my: 0px;
  transform: translate3d(var(--mx), var(--my), 0);
  transition: transform 400ms var(--ease), background var(--dur) var(--ease),
              color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn.is-magnetic, .rail__btn.is-magnetic { transition-duration: 120ms; }
/* The lift-on-hover composes with the magnet by folding into --my rather than
   being its own translate, which would replace it outright. */
.btn:hover { transform: translate3d(var(--mx), calc(var(--my) - 2px), 0); }
.btn:active { transform: translate3d(var(--mx), var(--my), 0); }

/* ---------- marquee velocity ---------------------------------------------- */
/* The brand strip leans into the scroll: fast scrolling adds speed and a slight
   skew, which decays back to the resting crawl. It ties the one element that
   moves on its own to the reader's own motion. */
.marquee__track { will-change: transform; }
.marquee__set { transition: none; }

/* ---------- gallery drift -------------------------------------------------- */
/* Tiles lean a few pixels toward the cursor — the same magnetic idea as the
   buttons, so the page has one interaction vocabulary rather than a different
   trick per section.

   This was a 3D perspective tilt first. It was cut because it could not be
   verified: the headless Chrome used for checks resolves any rotateX/rotateY to
   an identity matrix, so there was no way to confirm the angles were right
   rather than merely present. A 2D translate is measurable, cheaper to
   composite, and avoids stacking a perspective context on all sixteen tiles. */
/* js/main.js writes these as px, not as unitless factors. A
   calc(var(--tx, 0) * 4px) here computed to `none`: an unregistered custom
   property is substituted as raw tokens and validated late, and the whole
   transform is thrown away if that resolves badly — silently, with no console
   warning. Handing CSS a finished length removes the failure mode. */
.gallery figure {
  --tx: 0px; --ty: 0px;
  /* Folds in the reveal offset so both effects live in one transform. */
  transform: translate3d(var(--tx), calc(var(--ty) + var(--reveal-y, 0px)), 0);
  transition: transform 500ms var(--ease), opacity 700ms var(--ease);
}
.gallery figure:hover { transition-duration: 140ms; }

@media (prefers-reduced-motion: reduce) {
  [data-media] img { --media-scale: 1 !important; transition: none; }
  .hero__in, .kenburns, .hero__scroll { transform: none !important; opacity: 1 !important; }
  .btn, .rail__btn, .gallery figure { transform: none !important; }
}

/* ---------- hero video ---------------------------------------------------- */
/* Sits over the slideshow and fades in on `playing`, not on `canplay` — the
   latter fires while the first frame may still be blank, which showed as a
   black flash over a perfectly good photo. */
.hero__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  opacity: 0;
  transition: opacity 900ms var(--ease);
}
.hero.is-playing .hero__video { opacity: 1; }
.hero.is-playing .kenburns { opacity: 0; }
.kenburns { transition: opacity 900ms var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
  .hero.is-playing .kenburns { opacity: 1; }
}

/* ---------- odometer counters --------------------------------------------
   Each digit is a column of 0-9 that slides to its target, staggered left to
   right, inside a clipped cell. A plain count-up was the previous version; it
   reads as a number being computed, which is a loading state, not a headline.
   Digits rolling into place reads as a mechanism settling.

   Everything here is inert until js/main.js builds the columns — the figure is
   in the DOM as text, so with the script blocked it is just a number.
   ------------------------------------------------------------------------- */
.stat__n {
  display: flex; align-items: baseline; justify-content: center;
  font-variant-numeric: tabular-nums;
}
.stat__v { display: inline-flex; }
/* Bright enough to read as part of the figure — at 55% the "k" in "250k+"
   detached from the 250 and the number looked like it stopped at 250. */
.stat__suf { color: rgba(255, 255, 255, 0.78); }

.odo {
  display: inline-block;
  height: 1em;
  overflow: hidden;
  /* line-height must equal the cell height or the digits sit off-centre in
     their window and the whole row looks misaligned. */
  line-height: 1;
  vertical-align: baseline;
}
.odo__col {
  display: block;
  transform: translateY(0);
  transition: transform 1500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.odo__col i {
  display: block;
  height: 1em; line-height: 1;
  font-style: normal;
}

/* The rule under each figure draws in from the left as the stat arrives. */
.stat { position: relative; }
.stat::after {
  content: '';
  position: absolute; left: 1.5rem; right: 1.5rem; bottom: 0;
  height: 1px; background: var(--hairline);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 900ms var(--ease) 200ms;
}
.stat.is-in::after { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .odo__col { transition: none; }
  .stat::after { transform: scaleX(1); transition: none; }
}

/* ---------- recap clips ---------------------------------------------------
   Four 9:16 clips in a row on desktop, a snap-scrolling strip on phones.
   The pause control only shows on hover, on focus, or once a clip is
   paused — a wall of four permanent buttons reads as a player UI, and this
   is not one. Colour is the footage's own; the chrome stays monochrome. */
.reels {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.875rem;
  margin-top: clamp(2rem, 5vw, 3.5rem);
}
.reel {
  position: relative; margin: 0; overflow: hidden; border-radius: 8px;
  aspect-ratio: 9 / 16; background: var(--raised);
}
.reel video { width: 100%; height: 100%; object-fit: cover; }
.reel__toggle {
  position: absolute; right: 0.75rem; bottom: 0.75rem; width: 40px; height: 40px;
  display: grid; place-items: center; padding: 0; cursor: pointer;
  border: 1px solid var(--hairline); border-radius: 50%;
  background: rgba(10, 10, 11, 0.62); color: var(--white);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  opacity: 0; transition: opacity var(--dur) var(--ease), background var(--dur) var(--ease);
}
.reel:hover .reel__toggle, .reel.is-paused .reel__toggle, .reel__toggle:focus-visible { opacity: 1; }
.reel__toggle:hover { background: rgba(10, 10, 11, 0.85); }
.reel__toggle svg { width: 14px; height: 14px; fill: currentColor; }
.reel__toggle .i-play { display: none; }
.reel.is-paused .i-play { display: block; }
.reel.is-paused .i-pause { display: none; }
.reel__label {
  position: absolute; top: 0.75rem; left: 0.75rem; margin: 0; padding: 0.35rem 0.6rem; border-radius: 999px;
  background: rgba(10, 10, 11, 0.7); color: var(--white); font-family: var(--font-kicker);
  font-size: var(--t12); letter-spacing: 0.08em; text-transform: uppercase; pointer-events: none;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
/* Sound is the point, so this one is always visible — white while it is
   the clip you hear, one at a time. */
.reel__sound {
  position: absolute; left: 0.75rem; bottom: 0.75rem; display: flex; align-items: center; gap: 0.4rem;
  padding: 0.45rem 0.7rem 0.45rem 0.5rem; border-radius: 999px; border: 1px solid var(--hairline);
  background: rgba(10, 10, 11, 0.7); color: var(--white); cursor: pointer;
  font-family: var(--font-kicker); font-size: var(--t12); letter-spacing: 0.06em; text-transform: uppercase;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.reel__sound:hover { border-color: var(--white); }
.reel__sound svg { width: 18px; height: 18px; fill: currentColor; flex: none; }
.reel__sound .i-on { display: none; }
.reel.has-sound .reel__sound { background: var(--white); color: var(--ink); border-color: var(--white); }
.reel.has-sound .reel__sound .i-on { display: block; }
.reel.has-sound .reel__sound .i-off, .reel.has-sound .reel__sound span { display: none; }
@media (max-width: 860px) {
  .reels {
    display: flex; gap: 0.75rem; overflow-x: auto;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
    margin-inline: calc(-1 * var(--gut)); padding-inline: var(--gut);
    scrollbar-width: none;
  }
  .reels::-webkit-scrollbar { display: none; }
  .reel { flex: 0 0 min(64vw, 300px); scroll-snap-align: start; }
}
