/* ==========================================================================
   Showcase template - layout for the project pages build/showcase.py renders.
   ==========================================================================
   Rides on dwm-v6.css: tokens, type scale, .scene/.label/.lead/.cta/.rv, nav
   and footer all come from there. This file is only the page-specific layout,
   so the design system stays in one place.
   ========================================================================== */

/* ------------------------------------------------------------------- hero */

/* A very wide hero box crops the image band hard, so faces tuned for a
   16:9-ish window sit wrong on ultrawide monitors. Past ~2:1 the consumers
   switch to the wide focus (which falls back to the normal one). */
/* 1.7:1 catches a 16:9 desktop monitor (1.78) while leaving laptops (~1.5)
   and phones on the base focus - the 72svh hero box on a monitor runs about
   2.5:1, which crops the image band hard. */
.shero { --focus-r: var(--focus, 50% 50%); }
@media (min-aspect-ratio: 17 / 10) {
  .shero { --focus-r: var(--focus-wide, var(--focus, 50% 50%)); }
}
/* A 9:19.5 window shows about a third of a 16:9 frame, so the horizontal
   framing has to be chosen for the phone specifically. Every layer - plate,
   film, cutout, front - reads --focus, so re-framing moves them together and
   the freeze-frame match survives. */
@media (max-width: 699px) {
  .shero { --focus-r: var(--focus-phone, var(--focus, 50% 50%)); }
}

.shero { min-height: 72svh; display: grid; }
/* Big screens use the whole screen: at 16:9 the full-height hero shows the
   frame nearly uncropped, which is what fixes the endless crop-tuning there.
   Phones also run full height - the tall window takes the image's full
   height, so the whole figure fits. */
@media (min-aspect-ratio: 17 / 10) { .shero { min-height: 100svh; } }
/* Phones stop at 80svh. A full-height hero on a tall phone pushes everything
   below it off the fold entirely - you arrive and there is no sign the page
   continues. Wide screens keep the full height above. */
@media (max-width: 699px) { .shero { min-height: 80svh; } }
/* Undimmed by default - the picture is the point. Only the very bottom keeps
   a whisper of gradient so the standfirst and chips stay legible on any
   still. Pages whose hero is bright behind the type opt into the heavier
   wash with .shero--wash (see the Monk page). */
.shero .scene__bg::after {
  opacity: 1;
  background: var(--shero-veil);
}
.shero {
  /* Desktop: barely there. The type is large and the layout gives it room. */
  --shero-veil: linear-gradient(180deg, transparent 52%, rgba(0,0,0,.58) 100%);
}
@media (max-width: 699px) {
  /* A phone stacks the standfirst and chips over the busiest part of the
     frame, and a hero still is full of detail there - so the wash starts
     higher, and lands heavier. Three stops rather than two, so it ramps
     rather than appearing as an edge. */
  .shero {
    --shero-veil: linear-gradient(180deg,
      transparent 18%, rgba(0,0,0,.34) 46%, rgba(0,0,0,.72) 74%, rgba(0,0,0,.88) 100%);
  }
  /* ...but NOT on a page that builds its own depth. The concert hero is a
     layered scene - plate, subject cutout, front element - and the wash sits
     on the plate only, so a heavy one darkens the stage while the subject
     painted above it stays bright. That reads as a mistake rather than
     depth. It keeps the whisper it was designed with. */
  .shero--theatric {
    --shero-veil: linear-gradient(180deg, transparent 52%, rgba(0,0,0,.58) 100%);
  }
}
.shero--wash .scene__bg::after {
  background:
    linear-gradient(90deg, rgba(0,0,0,.62) 0%, rgba(0,0,0,.28) 38%, transparent 62%),
    linear-gradient(180deg, rgba(0,0,0,.28) 0%, rgba(0,0,0,.06) 42%, rgba(0,0,0,.78) 100%);
}
.shero.lit .scene__bg::after { --veil: 1; }
.shero .scene__bg img { width: 100%; height: 100%; object-fit: cover; object-position: var(--focus-r); }
.shero .scene__bg > img, .shero .scene__bg > picture > img, .shero__vid,
.shero__still, .shero__cutlayer, .shero__front { transform-origin: 50% 100%; }
/* Overscan exists so the concert page's sway has somewhere to travel, and it
   scales about the bottom - which quietly pushes the top of the frame off the
   screen on every other page, cutting heads. Only the page that needs it gets
   it; the rest sit at true scale. */
.shero--theatric .scene__bg > img, .shero--theatric .scene__bg > picture > img,
.shero--theatric .shero__vid { transform: scale(var(--overscan)); }
.shero__inner { align-self: end; padding-top: 22vh; }
.shero h1 {
  max-width: 13ch;
  /* 0.98 is set globally for display type and is fine for single lines, but
     across two it puts the descender of "Ryder" into the ascender of
     "Wembley". Just past 1 clears them without loosening the stack. */
  line-height: 1.06;
  /* Two shadows, not one: a wide soft pool lifts the type off blown-out stage
     lighting, and a tight dark one keeps the letter edges crisp where a lamp
     sits directly behind them. Both stay well under the type so it reads as
     separation rather than an effect. */
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.42), 0 1px 4px rgba(0, 0, 0, 0.32);
}
.shero .label { text-shadow: 0 1px 12px rgba(0, 0, 0, 0.5); }
.shero .lead { max-width: 52ch; margin-top: 1.4rem; }

.shero__facts {
  display: flex; flex-wrap: wrap; gap: .55rem .6rem;
  margin: 2.2rem 0 0; padding: 0; list-style: none;
}
.shero__facts li {
  font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--warm-2);
  padding: .5rem .85rem; border: 1px solid var(--line); border-radius: 2px;
  background: rgba(0,0,0,.35);
}

/* --------------------------------------------------------------- overview */

.overview { display: grid; gap: clamp(2rem, 5vw, 4rem); margin-top: 1.9rem; }
@media (min-width: 900px) {
  .overview { grid-template-columns: minmax(0, 1.6fr) minmax(260px, 1fr); }
}

/* One rule for paragraph rhythm, applied once. The Webflow pages spaced
   paragraphs with empty <p>&zwj;</p> lines, which is why the gaps were wrong. */
.overview__story p + p { margin-top: 1.05em; }
.overview__story p { margin: 0; color: #d9d6d2; line-height: 1.65; }

.overview__facts ul { margin: 0; padding: 0; list-style: none; }
.overview__facts li {
  padding: .85rem 0 .85rem 1.4rem; position: relative;
  border-bottom: 1px solid var(--line);
  color: var(--white); font-weight: 600; line-height: 1.4;
}
.overview__facts li:first-child { border-top: 1px solid var(--line); }
.overview__facts li::before {
  content: ""; position: absolute; left: 0; top: 1.32rem;
  width: 7px; height: 7px; background: var(--gold);
}

/* ------------------------------------------------------------------ films */

.films__tabs {
  display: flex; flex-wrap: wrap; gap: .25rem 1.6rem;
  margin: 1.9rem 0 1.6rem; border-bottom: 1px solid var(--line);
}
.films__tab {
  appearance: none; background: none; border: 0; cursor: pointer;
  padding: .65rem 0 .9rem; margin-bottom: -1px;
  font: inherit; font-weight: 700; font-size: .95rem;
  color: var(--warm-2); border-bottom: 2px solid transparent;
  transition: color .25s ease, border-color .25s ease;
}
.films__tab:hover { color: var(--white); }
.films__tab[aria-selected="true"] { color: var(--white); border-bottom-color: var(--gold); }
.films__tab:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* the facade - a poster and a play mark, the player only exists after a click */
.facade {
  position: relative; display: block; width: 100%; padding: 0;
  border: 1px solid rgba(230,193,122,.22); border-radius: 3px;
  background: #000; cursor: pointer; overflow: hidden;
  box-shadow: 0 18px 44px rgba(0,0,0,.5);
  transition: border-color .3s ease, transform .3s ease;
}
.facade img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: .92; }
.facade:hover, .facade:focus-visible { border-color: var(--gold); transform: translateY(-2px); }
.facade:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

.facade__go {
  position: absolute; inset: 50% auto auto 50%; translate: -50% -50%;
  width: 62px; height: 62px; border-radius: 50%; background: var(--gold);
  display: grid; place-items: center;
  transition: scale .3s cubic-bezier(.2,.8,.26,1);
}
.facade__go::after {
  content: ""; width: 0; height: 0; margin-left: 4px;
  border-left: 17px solid #0a0a0c;
  border-top: 11px solid transparent; border-bottom: 11px solid transparent;
}
.facade:hover .facade__go { scale: 1.08; }

/* portrait films (the after film) should not fill the row */
.facade[style*="4 / 5"] { max-width: 420px; margin-inline: auto; }

.films__frame { display: block; width: 100%; border: 0; border-radius: 3px; background: #000; }

/* Switching films: the incoming pane rises in. Panes are toggled with [hidden],
   so the animation runs each time one is revealed. */
@keyframes film-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}
.films__pane:not([hidden]) { animation: film-in .45s cubic-bezier(.2,.8,.26,1) both; }
@media (prefers-reduced-motion: reduce) { .films__pane:not([hidden]) { animation: none; } }

.films__lower { margin-top: 1.6rem; }
/* description left, the polaroid pair centred in a right column - the same
   two-column treatment as the crew band */
/* Descriptions with no polaroid pair beside them run the full width - Dan's
   call for the film-only pages. With a pair, the 62ch column returns. */
.films__lower:not(:has(.polscatter)) .films__about { max-width: none; }

/* Two columns only when there is actually a polaroid pair to hold - pages
   without BTS media would otherwise reserve an empty right column. */
@media (min-width: 1000px) {
  .films__lower:has(.polscatter) { display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(240px, 1fr);
    gap: clamp(2rem, 4vw, 3.5rem); align-items: center; }
}
.films__about { max-width: 62ch; }
.films__about h3 { margin-bottom: .7rem; }
.films__about h3 span {
  display: block; margin-top: .25rem;
  font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--gold);
}
.films__about p { margin: 0; color: #d9d6d2; line-height: 1.65; }
.films__about p + p { margin-top: 1.05em; }

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

/* ---------------------------------------------------------------- credits */

.credits-band .label { margin-bottom: 1.6rem; }
.credits { display: grid; gap: 0; margin: 0; max-width: 560px; }
.credit {
  display: grid; grid-template-columns: minmax(150px, 1fr) 1.4fr; gap: 1rem;
  padding: .6rem 0; border-bottom: 1px solid var(--line);
}
.credit:first-child { border-top: 1px solid var(--line); }
.credit dt {
  font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--warm-2); padding-top: .15rem;
}
.credit dd { margin: 0; color: var(--white); font-weight: 600; }

/* -------------------------------------------------------------- more work */
/* Big buttons, the Webflow way: the whole card is the thumbnail, the title
   sits on it, and hovering swaps the still for the film itself. Two-up on a
   desktop so each card actually reads as a destination, one-up on a phone.
   The grid breaks out of the reading column to near-full-bleed - these are
   films, they get the screen. */

.morework .wrap { max-width: none; }

.morework__grid {
  display: grid; gap: clamp(1.1rem, 2.5vw, 1.8rem);
  grid-template-columns: 1fr;
  margin-top: 1.9rem;
}
@media (min-width: 760px) { .morework__grid { grid-template-columns: 1fr 1fr; } }

.mcard {
  position: relative; display: block; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: 3px; border: 1px solid var(--line); background: #000;
  text-decoration: none;
}
.mcard img, .mcard video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
.mcard video { opacity: 0; transition: opacity .4s ease; }

.mcard::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 45%, rgba(0,0,0,.82) 100%);
}

.mcard__caption { position: absolute; inset: auto auto 0 0; right: 0; z-index: 1;
  padding: clamp(1rem, 2.5vw, 1.6rem); }
.mcard__title {
  display: block; color: var(--white); font-weight: 800;
  font-size: clamp(1.25rem, 2.6vw, 1.9rem); letter-spacing: -.02em; line-height: 1.1;
}
.mcard__sub { display: block; margin-top: .3rem; font-size: .82rem; color: var(--warm-2); }

.mcard:hover video, .mcard:focus-visible video { opacity: 1; }
.mcard:hover { border-color: rgba(230,193,122,.5); }
.mcard:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }

/* No hover on touch - the loops just play, as the Webflow version did. */
@media (hover: none) { .mcard video { opacity: 1; } }

/* --------------------------------------------------------------- enquire */

.enq-band { text-align: left; }
.enq-band .lead { max-width: 52ch; margin: 1.3rem 0 2rem; }
.enq-band .form { max-width: 640px; }

/* ------------------------------------------------------- BTS polaroid stack */
/* Behind-the-scenes phone media next to the crew list - the people in the
   credits, at work. Same physical language as the homepage polaroids and
   prints: white frames, a tilt each, landing when the section lights. Videos
   are muted loops, lazy like everything else. */

/* The print pile is always taller than a crew list, which left a hole under
   the names. Centring the list against the pile splits that space above and
   below it, so it reads as a deliberate margin rather than a gap. */
.credits-wrap { display: grid; gap: clamp(2rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 1000px) {
  .credits-wrap { grid-template-columns: minmax(260px, .78fr) minmax(0, 1.22fr); }
}

/* A hand-placed collage: every slot has a position, width and tilt, and the
   polaroids overlap on purpose - a pile, not a grid with gaps. Slot 0 is the
   lead. Featuring a shot swaps slot attributes (dwm-showcase.js), so these
   rules are the single source of the arrangement, per breakpoint. */
.btsstack { position: relative; aspect-ratio: 1 / 1.02; }

.pol {
  position: absolute; left: var(--x); top: var(--y); width: var(--w);
  margin: 0; cursor: pointer; z-index: 1;
  background: #f4f2ef; padding: 6px 6px 18px;
  box-shadow: 0 14px 32px rgba(0,0,0,.55);
  opacity: 0;
  transform: translateY(-26px) rotate(0deg) scale(.96);
  transition: opacity .55s ease var(--d, 0ms),
              transform .8s cubic-bezier(.2,.8,.26,1) var(--d, 0ms),
              filter .45s ease;
}
.pol.is-big { z-index: 2; cursor: default; }
.pol:hover { z-index: 3; }
.pol img, .pol video { display: block; width: 100%; height: auto; }
.pol:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

.scene.lit .pol { opacity: 1; transform: rotate(var(--r)); }
/* Dim with filter only, never opacity - these overlap, and a transparent
   polaroid shows the one beneath it through itself. */
.scene.lit .pol:not(.is-big) { filter: saturate(.75) brightness(.72); }
.scene.lit .pol:not(.is-big):hover { filter: none; }

/* The slots form a loose ring with overlaps, so whichever one the featured
   shot vacates never leaves a readable hole. */
.pol[data-slot="0"] { --x: 0%;  --y: 4%;  --w: 30%; --r: -5deg; }
.pol[data-slot="1"] { --x: 34%; --y: 0%;  --w: 27%; --r: 3deg; }
.pol[data-slot="2"] { --x: 66%; --y: 3%;  --w: 32%; --r: -4deg; }
.pol[data-slot="3"] { --x: 70%; --y: 40%; --w: 26%; --r: -3deg; }
.pol[data-slot="4"] { --x: 60%; --y: 66%; --w: 26%; --r: 5deg; }
.pol[data-slot="5"] { --x: 26%; --y: 72%; --w: 26%; --r: -6deg; }
.pol[data-slot="6"] { --x: 0%;  --y: 48%; --w: 26%; --r: 4deg; }

/* Featured: lifted out of the pile into the middle, on top of the others.
   Position is the override, so no slot swapping - demote and it drops back
   into its own gap. */
/* Geometry differs by shape - one set of numbers cannot centre a 9:16 frame
   and a 4:3 frame both; the landscape shot sat high and small when featured. */
.pol--tall.is-big { --x: 27%; --y: 9%;  --w: 46%; --r: -2deg; }
.pol--port.is-big { --x: 24%; --y: 16%; --w: 52%; --r: -2deg; }
.pol--land.is-big { --x: 17%; --y: 26%; --w: 66%; --r: -2deg; }

/* phone: taller canvas, same ring idea */
@media (max-width: 699px) {
  /* NO phone-specific sizing for the subject. Dan's rule: the cutout must
     match the film exactly, and the film is cover-cropped like every other
     layer - so the cutout has to be too. Any custom width, scale or fit
     breaks the freeze-frame match at the swap. The consequence is that a
     9:19.5 window shows about a third of a 16:9 frame's width, so Sam is
     cropped at the sides here - but identically in the film and the still,
     which reads as framing rather than a fault.
     (An earlier attempt at custom widths could not have worked anyway: the
     global img rule caps max-width at 100%, silently clamping it.) */
  .shero { --subj-scale: 1; }
  /* The cast shadow is sized for a figure a third of the frame wide. Filling
     a phone he is three times that, and the shadow reads as a dark smear
     rather than depth - so it goes soft and close. */
  .shero__cutwrap { filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.32)); }

  .btsstack { aspect-ratio: 1 / 1.6; }
  .pol { padding: 5px 5px 14px; }
  .pol[data-slot="0"] { --x: 0%;  --y: 0%;  --w: 44%; --r: -5deg; }
  .pol[data-slot="1"] { --x: 52%; --y: 2%;  --w: 44%; --r: 3deg; }
  .pol[data-slot="2"] { --x: 50%; --y: 28%; --w: 46%; --r: -4deg; }
  .pol[data-slot="3"] { --x: 58%; --y: 50%; --w: 38%; --r: -3deg; }
  .pol[data-slot="4"] { --x: 20%; --y: 56%; --w: 38%; --r: 5deg; }
  .pol[data-slot="5"] { --x: 0%;  --y: 30%; --w: 36%; --r: -6deg; }
  .pol[data-slot="6"] { --x: 30%; --y: 10%; --w: 34%; --r: 4deg; }
  .pol--tall.is-big { --x: 23%; --y: 18%; --w: 54%; --r: -2deg; }
  .pol--port.is-big { --x: 20%; --y: 24%; --w: 60%; --r: -2deg; }
  .pol--land.is-big { --x: 13%; --y: 32%; --w: 74%; --r: -2deg; }
}

@media (prefers-reduced-motion: reduce) {
  .pol { transition: none; opacity: 1; transform: rotate(var(--r)); }
}

/* --------------------------------------------------- scattered polaroids (v7) */
/* The alternative to the pile: small clusters through the page. Best three
   beside the hero title, pairs at the films heading and the crew list.
   Decorative - no lightbox, no rotation, pointer-events off. */

.polscatter {
  display: flex; align-items: flex-start; gap: 1rem;
  pointer-events: none; z-index: 2;
}
.spol {
  margin: 0; margin-top: var(--oy, 0);
  background: #f4f2ef; padding: 6px 6px 10px;
  box-shadow: 0 14px 32px rgba(0,0,0,.55);
  opacity: 0;
  transform: translateY(-24px) rotate(0deg) scale(.96);
  transition: opacity .55s ease var(--d, 0ms),
              transform .8s cubic-bezier(.2,.8,.26,1) var(--d, 0ms);
}
.spol img, .spol video { display: block; width: 100%; height: auto; }
.scene.lit .spol { opacity: 1; transform: rotate(var(--r)); }

/* hero: low right, sitting on the border with the next section. Biggest on
   the far left stepping down to the right, overlapped and splayed like a fan
   of prints in a hand - shared baseline, rotation pivoting near the bottom.

   ONE scale variable drives the whole fan. Independent clamps hit their
   floors and ceilings at different viewport widths, so the three prints (and
   the headline, on its own clamp) drifted out of proportion on any screen
   width they were not hand-tuned against. */
.polscatter--hero { --fw: clamp(150px, 13.5vw, 230px); position: absolute; bottom: 4%; right: 3.5vw; align-items: flex-end; gap: 0; }
.polscatter--hero .spol { transform-origin: 50% 85%; }
.polscatter--hero .spol:nth-child(1) { width: var(--fw);              --fan: -8deg; }
.polscatter--hero .spol:nth-child(2) { width: calc(var(--fw) * .83); --fan: -1deg; margin-left: calc(var(--fw) * -.14); }
.polscatter--hero .spol:nth-child(3) { width: calc(var(--fw) * .70); --fan: 6deg;  margin-left: calc(var(--fw) * -.14); }
.scene.lit .polscatter--hero .spol { transform: rotate(var(--fan)); }

/* the mobile fan lives in the films section; desktop never shows it,
   and neither of the mobile-only clusters exists on desktop */
.polscatter--mfan, .polscatter--mcrew { display: none; }

/* films: the video pair beside each film's description - it was absolutely
   parked at the section's top right, which sat it on top of the player */
.films { position: relative; }
.polscatter--films { --fw: clamp(120px, 11.5vw, 185px); justify-content: center; align-self: center; gap: calc(var(--fw) * .12); }
.polscatter--films .spol { width: var(--fw); }

/* credits: a proper second column - the crew list left, the polaroids
   centred in the right cell, level with the list */
.polscatter--credits { --fw: clamp(112px, 13.2vw, 200px); flex-wrap: wrap;
  /* centred to each other - the prints vary in height, and hanging them from
     a shared top edge made the row look dropped rather than arranged */
  justify-content: center; align-self: center; align-items: center;
  /* Rows used to tuck into each other for a pile look, but a print now ends
     in a caption and the tuck buried it. A small positive gap keeps the
     scatter without hiding anything written on them. */
  gap: calc(var(--fw) * .07) calc(var(--fw) * .1); }
.polscatter--credits .spol { width: var(--fw); }

/* Three on show, one clean row - five wrapped into a ragged second row that
   read as spill rather than a pile. The other two stay in the DOM, so the
   lightbox still walks through the whole set; they are simply not on the
   wall. Captions come off down here too: at this size seven lines of them
   was more label than photograph. */
@media (min-width: 700px) {
  .polscatter--credits .spol:nth-child(n+4) { display: none; }
}
/* two classes deep, so it outranks the base .spol figcaption rule that sits
   later in this file at equal specificity */
.polscatter--credits .spol figcaption { display: none; }
.polscatter--credits .spol { padding-bottom: 1.5em; }

@media (max-width: 899px) {
  /* phone: the hero cluster shrinks into the free top-right corner; the films
     pair goes - it would sit on the tabs; the credits pair flows statically */
  /* phone: the hero cannot lend its fan across the section border (overflow
     hidden), so it hides and the same three fan out beside "The films."
     heading - the empty right column Dan circled. */
  .polscatter--hero { display: none; }
  .polscatter--films { display: none; }
  /* The fan sits IN the layout, not floated over it - absolutely positioned
     it kept landing on the tabs, because --band and text wrapping differ per
     device. The heading area becomes a grid: label and h2 in the left column,
     the fan in the right, tabs spanning below both. Whatever the fan's height,
     the tabs start after it. */
  .films .scene__inner { display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: start; column-gap: .8rem; }
  .films .scene__inner > .label { grid-column: 1; align-self: end; }
  .films .scene__inner > h2 { grid-column: 1; }
  /* Stamped across the seam: a transform, not a margin, lifts the fan so its
     top half rides over the hero image. Transforms move pixels without moving
     layout, so the tab clearance the grid guarantees is untouched - and the
     films scene must not clip what pokes above it. */
  .films { overflow: visible; }
  .polscatter--mfan {
    --stamp: calc(var(--band) + 3.8rem);
    display: flex; grid-column: 2; grid-row: 1 / span 2; justify-self: end;
    align-items: flex-end; gap: 0;
    /* the transform slides the fan up over the seam; the negative bottom
       margin hands back exactly that much layout space, so the heading and
       tabs close up instead of leaving a fan-sized hole where it used to be */
    transform: translateY(calc(-1 * var(--stamp)));
    margin-bottom: calc(-1 * var(--stamp));
  }
  .films__tabs, .films__pane { grid-column: 1 / -1; }
  .polscatter--mfan .spol { transform-origin: 50% 85%; }
  /* two prints only - three crowded the heading and sat on the tabs */
  .polscatter--mfan .spol:nth-child(1) { width: 104px; --fan: -7deg; }
  .polscatter--mfan .spol:nth-child(2) { width: 86px;  --fan: 4deg; margin-left: -.9rem; }
  .polscatter--mfan .spol:nth-child(3) { display: none; }
  .scene.lit .polscatter--mfan .spol { transform: rotate(var(--fan)); }

  /* the rest of the set lives by the crew: the third photo and the four
     loops, a loose wrapped row of tilted prints */
  /* A phone shows three, side by side, no scrolling - the same edit as the
     desktop wall. Tapping any of them opens the lightbox, which still holds
     the whole set. The films pair does not render at this width, so the
     stage-walk shot would otherwise never appear on a phone at all: it is
     repeated at the end of the crew list and ordered into the middle here,
     taking the drum-cam rigging's place. */
  .polscatter--credits {
    --fw: calc((100% - 1.5rem) / 3);
    display: flex; flex-wrap: nowrap;
    justify-content: center; align-items: center;
    overflow: visible;
    gap: .75rem;
    padding: 1.1rem 0 .3rem;
    margin-inline: 0;
  }
  .polscatter--credits .spol {
    flex: 0 0 var(--fw); width: var(--fw); margin-top: 0;
  }
  /* indices follow the JSON crew order: 1 p2, 2 v3, 3 v2, 4 v1, 5 v4, 6 p3 */
  .polscatter--credits .spol:nth-child(2),
  .polscatter--credits .spol:nth-child(4),
  .polscatter--credits .spol:nth-child(5) { display: none; }
  .polscatter--credits .spol:nth-child(1) { order: 1; }
  /* the middle one leans the other way, so the three read as a spread rather
     than a stack. !important because the tilt arrives as an inline style from
     the template, which a stylesheet cannot otherwise beat. */
  .polscatter--credits .spol:nth-child(6) { order: 2; --r: 5deg !important; }
  .polscatter--credits .spol:nth-child(3) { order: 3; }
  /* No grow on a phone: there is no hover to undo it with, and a tap already
     has a job - it opens the lightbox. */
  .spol { cursor: pointer; }
  .spol:hover, .spol:focus-visible, .spol:active {
    transform: rotate(var(--r));
    box-shadow: 0 14px 32px rgba(0, 0, 0, 0.55);
  }
  .scene:has(.spol:hover), .scene:has(.spol:focus-visible) { z-index: auto; }
  .polscatter--credits .spol {
    flex: 0 0 var(--fw); width: var(--fw); margin-top: 0;
    scroll-snap-align: center;
  }
  .polscatter--mcrew {
    display: flex; flex-wrap: wrap; gap: .9rem; justify-content: center;
    margin-top: .4rem;
  }
  .polscatter--mcrew .spol:nth-child(1) { width: 56%; }
  .polscatter--mcrew .spol:nth-child(n+2) { width: calc(30% - .9rem); }
}

@media (prefers-reduced-motion: reduce) {
  .spol { transition: none; opacity: 1; transform: rotate(var(--r)); }
}

/* --------------------------------------------------------- banner loops */
/* Muted autoplay loops under the player, as on the Webflow original. */
.films__banners {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: clamp(.6rem, 1.5vw, 1rem); margin-top: clamp(.8rem, 2vw, 1.2rem);
}
@media (min-width: 900px) { .films__banners { grid-template-columns: repeat(4, 1fr); } }
.films__banner {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: 3px; border: 1px solid var(--line); background: #000;
}
.films__banner iframe, .films__banner video {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0; object-fit: cover;
}

/* -------------------------------------------------------- hero intro video */
/* Plays over the still for a few seconds, then fades and hands back. */
.shero__vid {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: var(--focus-r);
  transition: opacity 1.2s ease;      /* plain pages dissolve to the still */
  transform: scale(var(--overscan));
}
.shero--theatric .shero__vid { transition: none; }   /* the flash covers it */
.shero__vid.done { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .shero__vid { display: none; } }

/* On a hero with an intro film, the polaroid fan holds until the film has
   dissolved into the still - the prints land on the photograph, not on
   moving footage. .handed arrives from dwm-showcase.js after the fade. */
.shero:has(.shero__vid):not(.handed) .polscatter--hero .spol {
  opacity: 0;
  transform: translateY(-24px) rotate(0deg) scale(.96);
}

/* ------------------------------------------------- subject cutout + strokes */
/* The subject layers over the headline; a stroke-only copy of the text,
   masked to the cutout's silhouette, sits over the subject - so letters
   crossing them become outlines. Layer order: still, veil, text, cutout,
   strokes, polaroids. */

/* The cutout is a masked wrapper, not a bare image: inside it, the same
   photo AND a copy of the hero veil, all clipped to the silhouette. Sam gets
   exactly the darkening the background gets, so nothing betrays the seam -
   the only tell is type passing behind him. All layers inherit --focus from
   the section, which is also what fixed the cutout rendering at a different
   crop to the still. */
/* The title AND its label play the depth game - Sam covers both. The
   standfirst and fact chips are information: above every scene layer. */
.shero .shero__inner > .lead,
.shero .shero__inner > .shero__facts { position: relative; z-index: 3; }

/* The baked still fronts the hero while layers load - the clean plate never
   shows bare. It dissolves at handover, covered by the identical composite. */
.shero__still {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: var(--focus-r);
  transform: scale(var(--overscan));
}
.shero.handed .shero__still { opacity: 0; }

/* The flashbang: the swap from film to scene - and the title jumping to its
   high position - happens at peak white. Nothing animates in view; the flash
   IS the transition. */
.shero__flash {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: #fff; opacity: 0;
}
.shero.flashing .shero__flash { animation: flashbang 1.1s cubic-bezier(.2,.6,.3,1) both; }
@keyframes flashbang {
  0% { opacity: 0; }
  13% { opacity: 1; }
  30% { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .shero.flashing .shero__flash { animation: none; }
}

/* Only the label and title relocate - and both live BEHIND Sam. The
   standfirst and chips hold the bottom, above every scene layer. The jump is
   instant: it happens under the flash. */
.shero--theatric.handed .shero__inner > .label,
.shero--theatric.handed .shero__inner > h1 { transform: translateY(var(--title-rise, -14svh)); }
/* Rise tiers by WIDTH, not aspect - a fullscreen MacBook browser is ~1.8:1,
   the same aspect as a 16:9 monitor, so aspect cannot tell them apart. The
   full-height hero stays aspect-keyed; only the title travel differs. */
/* ------------------------------------------------------------------ tiers
   Every hero tuning variable, in one place, in cascade order. They used to be
   scattered - the phone block sits ~230 lines earlier in this file, so at
   equal specificity the base rule below silently overrode it and the phone
   had been running desktop numbers. Put new tiers HERE, never in the layout
   sections above.

   --title-rise    how far label and title jump under the flashbang
   --overscan      shared by every media layer: keeps them registered with
                   each other and with the film, and buys room for the sway
   --sway          subject travel; must stay inside the overscan's spare
   --subj-settle   multiplied into the subject's scale; eases to its handed
                   value over 5s, so Sam settles back after the flash */
.shero {
  --title-rise: -14svh;
  --overscan: 1.12;
  --sway: 80px; --sway-front: 88px;
  --subj-final: 0.95;          /* desktop: Sam sits well inside the frame */
}

/* The rise is in svh, but the collision with the nav is an absolute distance
   - so a wide BUT SHORT window (1800x1000) drove the label into the logo
   while a 2K panel had room to spare. The big rise now needs the height too. */
/* Wide screens get a longer swing - Dan: his arm does not quite clear the
   title at the left extreme. There is room for it: on an 1800px hero Sam
   spans roughly 423-1395 inside a painted box of 43-1757, so even a 150px
   throw keeps his silhouette well clear of the box edge and no cut appears. */
@media (min-width: 1800px) {
  .shero { --title-rise: -22svh; --sway: 150px; --sway-front: 165px; }
}
@media (min-width: 1800px) and (min-height: 1200px) { .shero { --title-rise: -32svh; } }

@media (max-width: 699px) {
  /* The title clears Sam's face by rising a full title-height further than
     the desktop tiers. Overscan runs higher here because the settle eats into
     it: 1.18 x 0.9 leaves 1.06, still enough to cover the box plus the 8px of
     travel. Sam fills a phone frame edge to edge, so if the subject ever
     scaled below 1.0 his silhouette would part from the screen edge and show
     a cut - on desktop he sits well inside the frame and cannot. */
  /* Sam's shrink used to be capped by coverage: his layer is cover-cropped to
     its own box, so shrinking or nudging it slid a cut in from the screen
     edge, where a phone frames him flush.

     --subj-pad widens that box 120px beyond the screen on each side, which
     moves the cut off-screen entirely and frees both moves. Widening does not
     rescale the image - cover here is height-driven, so the image is the same
     size in a wider box - it only shifts it, by exactly 0.26 x the padding
     (independent of the hero's height). --subj-align pays that back and then
     carries the leftward nudge on top: 31px compensation minus 20px of nudge.

     With the cap lifted he drops to 0.92 against a 1.18 background - about a
     22% step down when the flash clears. */
  /* The rise was tuned against a full-height hero. At 80svh the text block
     already starts 20svh higher, so the old figure drove the title into the
     logo - it only needs a nudge now. */
  .shero { --title-rise: -5svh; --overscan: 1.18; --sway: 6px; --sway-front: 8px;
    --subj-final: 0.92; --subj-pad: 120px; --subj-align: 11px; }
}

/* Camera flash: the white goes off and Sam is simply smaller on the other
   side of it - no travel to watch, which is what a flash actually does, and
   it hands the title back a chunk of room. He rides his own layer so the
   shrink and the sway compose by nesting rather than fighting over transform.

   --subj-shrink is bounded by the overscan: the layer's painted box is
   cover-cropped, so once it shrinks below the viewport a cut appears wherever
   Sam is flush with the edge. Desktop has him well inside the frame and can
   take the full 15%; a phone has him overflowing both edges, so it shrinks
   only slightly and leans on a bigger overscan. */
/* The back copy of the subject. Same geometry as the front one, but at
   z-index 0 it paints BELOW the title while the front copy paints above it.
   Desktop has no use for it - the subject sits wholly in front there. */
.shero__cutback { display: none; }
.shero__cutback, .shero__cutlayer {
  position: absolute; inset: 0 calc(-1 * var(--subj-pad, 0px));
  pointer-events: none;
  transform-origin: 50% 100%;
  transform: scale(var(--overscan));
}
/* --subj-final is the subject's scale AFTER the flash, set outright rather
   than derived. It can differ freely from --overscan because the cutout is
   hidden until the flash clears - only the still has to match the film at
   the moment of the swap. Its floor is coverage, not taste: the layer's
   painted box is cover-cropped, so below ~1.0 (plus the sway) a cut appears
   wherever the subject is flush with the screen edge. */
.shero__cutlayer { z-index: 1; }
.shero__cutback  { z-index: 0; }
.shero--theatric.handed .shero__cutlayer,
.shero--theatric.handed .shero__cutback {
  transform: translateX(var(--subj-align, 0px)) scale(var(--subj-final));
}

/* The nearest plane - the mic stand. Above Sam, below the information. */
.shero__front {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  width: 100%; height: 100%; object-fit: cover;
  object-position: var(--focus-r);
  transform: scale(var(--overscan));
}
.shero:has(.shero__vid):not(.handed) .shero__front { opacity: 0; }

/* -------------------------------------------------------- ambient sway */
/* Not interactive: the planes sway left and right on their own, far enough
   that the title is periodically revealed from behind the subject. Nearer
   planes travel further and slightly out of phase; each cycle passes through
   rest so the start never jumps. Overscan keeps the plate's edges hidden. */
/* EVERY hero media layer carries the SAME --overscan, so they register
   exactly with each other and with the film - Dan's rule. The overscan is
   what makes the sway possible: each layer is cover-cropped to its own box,
   so translating a layer used to slide a hard cut into view (the edge Dan saw
   43px in from the right). Scaled up together, the spare pixels absorb the
   travel and no edge can appear. Sway amplitude is therefore capped by the
   overscan - hence the smaller figures on a phone, where 12% of 390px is
   only 23px of room each side.
   Before this, the plate alone carried a 1.1 overscan while the cutout sat at
   1.0 - so Sam was 10% smaller than the frame he was cut from, which is the
   mismatch Dan saw as a hard crop. */

/* Sway is translate only now - the scale lives on the parent layer. */
@keyframes sway-subj {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(calc(var(--sway) * -1)); }
  75%      { transform: translateX(var(--sway)); }
}
@keyframes sway-front {
  0%, 100% { transform: translateX(0) scale(var(--overscan)); }
  25%      { transform: translateX(calc(var(--sway-front) * -1)) scale(var(--overscan)); }
  75%      { transform: translateX(var(--sway-front)) scale(var(--overscan)); }
}

/* Theatrics are the concert page's own: sway, flash and the title jump stay
   behind the shero--theatric flag. CMS pages keep the plain dissolve. */
.shero--theatric.handed .shero__cutwrap  { animation: sway-subj 16s ease-in-out infinite; }
.shero--theatric.handed .shero__front    { animation: sway-front 16s ease-in-out infinite; }

.shero--theatric:not(:has(.shero__vid)) .shero__cutwrap  { animation: sway-subj 16s ease-in-out infinite; }
.shero--theatric:not(:has(.shero__vid)) .shero__front    { animation: sway-front 16s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .shero__cutwrap, .shero__front { animation: none !important; }
}

.shero__cutwrap {
  position: absolute; inset: 0; pointer-events: none;
  /* phones scale the subject plane down - he is his own layer, so he can sit
     smaller against the same stage and keep his arm and guitar in the narrow
     window. Feet stay planted. */
  /* the shadow follows the masked silhouette, falling on the letters and
     scene behind him - the depth cue that makes the occlusion read as real */
  filter: drop-shadow(0 22px 44px rgba(0, 0, 0, 0.55));
  -webkit-mask-image: var(--cut); mask-image: var(--cut);
  -webkit-mask-size: cover; mask-size: cover;
  -webkit-mask-position: var(--focus-r); mask-position: var(--focus-r);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.shero__cutwrap img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: var(--focus-r);
}
.shero__cutveil {
  position: absolute; inset: 0;
  /* matches whatever the background is doing - see .shero .scene__bg::after */
  background: linear-gradient(180deg, transparent 55%, rgba(0,0,0,.55) 100%);
}

.polaroids-above { z-index: 3; }
.shero .polscatter--hero { z-index: 3; }

/* With an intro film, the cutout and strokes arrive with the still - not
   floating over moving footage. */
.shero:has(.shero__vid):not(.handed) .shero__cutlayer,
.shero:has(.shero__vid):not(.handed) .shero__cutback { opacity: 0; }
.shero__cutlayer, .shero__cutback { transition: opacity 1.2s ease; }

/* ------------------------------------------------------- prints: hover + lightbox */
/* The scattered prints are clickable now, so the cluster stops swallowing
   pointer events. Each print is a real button in the tab order. */
.polscatter { pointer-events: auto; }
.spol { cursor: zoom-in; }
.spol:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }

/* Hovering a print drops the lights on everything else. The dim is a vast
   spread shadow on the print ITSELF, not an overlay: a shadow paints behind
   its own element, so the hovered print can never be covered by it, and
   there is no stacking context to fight. The previous overlay had to raise
   its whole section above the nav to cover the page - which painted the
   section's opaque background over the nav and wiped it out.
   Both print sections stop clipping so neither the growth nor the shadow is
   cut at their edges. */
.films, .scene:has(.credits-band) { overflow: visible; }
.spol { position: relative; }
.spol:hover, .spol:focus-visible { z-index: 20; }

/* A grown print overflows its section, and the next section paints later, so
   it swallowed the overflow. Lifting the hovered section clears that - at 50,
   which sits above its sibling sections but BELOW the fixed nav at 200. The
   earlier attempt used 250 and painted the section's opaque background over
   the nav, wiping it out. */
.scene:has(.spol:hover), .scene:has(.spol:focus-visible) { z-index: 50; }

.spol {
  transition: transform .45s cubic-bezier(.2,.8,.26,1), box-shadow .45s ease;
}
.spol:hover, .spol:focus-visible { z-index: 20; }

/* Beside the films they are small by necessity, so they blow up hard enough
   to actually read. In the crew pile they are already sizeable - a nudge is
   enough there. */
.polscatter--films .spol:hover,
.polscatter--films .spol:focus-visible {
  transform: rotate(var(--r)) scale(2.1);
  box-shadow: 0 40px 90px rgba(0,0,0,.7), 0 0 0 100vmax rgba(4, 4, 6, 0.66);
}
.polscatter--credits .spol:hover,
.polscatter--credits .spol:focus-visible {
  transform: rotate(var(--r)) scale(1.35);
  box-shadow: 0 28px 60px rgba(0,0,0,.65), 0 0 0 100vmax rgba(4, 4, 6, 0.6);
}
@media (hover: none) {
  .spol:hover { transform: rotate(var(--r)); box-shadow: 0 14px 32px rgba(0,0,0,.55); }
}

/* ----------------------------------------------------------------- lightbox */
.lbx {
  position: fixed; inset: 0; z-index: 400;
  display: grid; place-items: center;
  background: rgba(6, 6, 8, .88);
  backdrop-filter: blur(6px);
  animation: lbx-in .25s ease both;
}
.lbx[hidden] { display: none; }
@keyframes lbx-in { from { opacity: 0; } to { opacity: 1; } }

.lbx__stage {
  width: min(92vw, 1100px); max-height: 84svh;
  display: grid; place-items: center;
}
/* Bound to the VIEWPORT, not to the stage. The stage is a grid item, and a
   percentage max-height against a grid-sized parent let tall portrait prints
   (900x1600) run off the bottom of the screen. */
.lbx__stage img, .lbx__stage video {
  max-width: min(92vw, 1100px); max-height: 84svh;
  width: auto; height: auto;
  background: #f4f2ef; padding: 10px 10px 26px;
  box-shadow: 0 30px 80px rgba(0,0,0,.7);
  animation: lbx-pop .3s cubic-bezier(.2,.8,.26,1) both;
}
@keyframes lbx-pop { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }

.lbx__nav, .lbx__close {
  position: absolute; appearance: none; cursor: pointer;
  background: rgba(255,255,255,.1); border: 1px solid var(--line);
  width: 52px; height: 52px; border-radius: 50%;
  transition: background .2s ease, border-color .2s ease;
}
.lbx__nav:hover, .lbx__close:hover { background: rgba(255,255,255,.2); border-color: var(--gold); }
.lbx__nav:focus-visible, .lbx__close:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* chevrons and cross drawn from borders - no icon font, no SVG */
.lbx__nav::before {
  content: ""; position: absolute; inset: 50% auto auto 50%;
  width: 13px; height: 13px; border: 2px solid #fff; border-width: 2px 2px 0 0;
}
.lbx__prev { left: max(3vw, 18px); }
.lbx__prev::before { translate: -30% -50%; rotate: -135deg; }
.lbx__next { right: max(3vw, 18px); }
.lbx__next::before { translate: -70% -50%; rotate: 45deg; }

.lbx__close { top: max(3vh, 18px); right: max(3vw, 18px); }
.lbx__close::before, .lbx__close::after {
  content: ""; position: absolute; inset: 50% auto auto 50%;
  width: 18px; height: 2px; background: #fff; translate: -50% -50%;
}
.lbx__close::before { rotate: 45deg; }
.lbx__close::after { rotate: -45deg; }

.lbx__count {
  position: absolute; inset: auto auto max(3vh, 16px) 50%; translate: -50% 0;
  margin: 0; font-size: .78rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--warm-2);
}

@media (max-width: 699px) {
  .lbx__stage { width: 94vw; max-height: 76svh; }
  .lbx__stage img, .lbx__stage video { max-width: 94vw; max-height: 76svh; }
  .lbx__nav { width: 44px; height: 44px; }
  .lbx__prev { left: 10px; } .lbx__next { right: 10px; }
}
@media (prefers-reduced-motion: reduce) {
  .lbx, .lbx__stage img, .lbx__stage video { animation: none; }
  .polscatter--films .spol { transition: none; }
}

/* ------------------------------------------------------------ print captions */
/* The white lip a real polaroid leaves for a caption - so it wants a hand,
   not a UI face. Caps keep it disciplined; Architects Daughter keeps it from
   reading as a system label. Caveat was tried first and was too playful, then
   Manrope, which was too digital. Sized against --fw so it scales with the
   print when one is blown up. */
.spol figcaption {
  font-family: "Hand", "Segoe Print", "Bradley Hand", cursive;
  font-size: clamp(0.58rem, calc(var(--fw, 160px) * 0.062), 0.82rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.35;
  color: #8b8781;   /* grey enough to sit back - the photo leads, not the note */
  text-align: center;
  padding: 0.7em 0.4em 0.15em;
  min-height: 3.2em;
  display: flex; align-items: center; justify-content: center;
}

/* ------------------------------------------------------------ phone: no grow */
/* Last in the file on purpose. The grow rules are two classes deep and sit
   above this, so a reset earlier in the sheet lost to them at equal
   specificity - which is why the zoom kept surviving on a phone. There is no
   hover on touch to undo a zoom with, and a tap already has a job: the
   lightbox. */
@media (max-width: 699px) {
  .polscatter--films .spol:hover,
  .polscatter--films .spol:focus-visible,
  .polscatter--films .spol:active,
  .polscatter--credits .spol:hover,
  .polscatter--credits .spol:focus-visible,
  .polscatter--credits .spol:active {
    transform: rotate(var(--r));
    box-shadow: 0 14px 32px rgba(0, 0, 0, 0.55);
  }
  .scene:has(.spol:hover), .scene:has(.spol:focus-visible) { z-index: auto; }
}

/* ------------------------------------ phone: the title threads through him */
/* Last in the file on purpose - the base .shero__cutwrap sets mask-image on
   its own several hundred lines above and wins at equal specificity wherever
   else this is placed.

   Two copies of the subject, masked to complementary halves and painted on
   either side of the title. The FRONT copy keeps his head and guitar, so they
   still cross over the type. The BACK copy carries his raised arm, so the arm
   is still there but the words run over it. The two gradients are exact
   inverses, so their alphas sum to one and he reassembles seamlessly.

   The stops are placed by measurement, not by eye: the cutwrap box is
   --subj-pad wider than the screen on each side and then scaled by
   --subj-final and shifted by --subj-align, so 18-31% lands at roughly
   20-96px on a 390px screen. That finishes the ramp well before his head at
   ~125px - the previous 28-46% ran the ramp THROUGH his head, which is why
   the end of "Wembley" showed through it. */
@media (max-width: 699px) {
  .shero--theatric .shero__cutback { display: block; }

  .shero--theatric .shero__cutlayer .shero__cutwrap,
  .shero--theatric .shero__cutback .shero__cutwrap {
    -webkit-mask-size: cover, 100% 100%;
    mask-size: cover, 100% 100%;
    -webkit-mask-position: var(--focus-r), 0 0;
    mask-position: var(--focus-r), 0 0;
    -webkit-mask-repeat: no-repeat, no-repeat;
    mask-repeat: no-repeat, no-repeat;
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
  }
  /* in front of the title: him from the shoulders right */
  .shero--theatric .shero__cutlayer .shero__cutwrap {
    -webkit-mask-image: var(--cut),
      linear-gradient(90deg, transparent 0, transparent 18%, #000 31%, #000 100%);
    mask-image: var(--cut),
      linear-gradient(90deg, transparent 0, transparent 18%, #000 31%, #000 100%);
  }
  /* behind the title: the arm */
  .shero--theatric .shero__cutback .shero__cutwrap {
    -webkit-mask-image: var(--cut),
      linear-gradient(90deg, #000 0, #000 18%, transparent 31%, transparent 100%);
    mask-image: var(--cut),
      linear-gradient(90deg, #000 0, #000 18%, transparent 31%, transparent 100%);
  }
}

/* ------------------------------------------------------------- index page */
/* A page with no hero image: the type carries it and the cards below hold all
   the pictures, so nothing competes. Shorter than a photographic hero. */
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)) {
  min-height: 0;
  background: #0a0a0c;
}
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)) .shero__inner {
  padding-top: calc(var(--bar) + 8rem);
  padding-bottom: clamp(2.5rem, 6vw, 4.5rem);
}

/* The featured first card runs full width - five cards otherwise wrap 2+2+1
   and leave an orphan on the last row. */
@media (min-width: 760px) {
  .morework__grid--feature > .mcard:first-child { grid-column: 1 / -1; aspect-ratio: 21 / 8; }
}

/* The picker: an index is for choosing, not for admiring, so the cards come
   down in size and the grid stops running full bleed - it sits inside the
   reading width like the rest of the page. Flex rather than grid so a row of
   five wraps to 3 + 2 CENTRED, instead of leaving a hole on the last row. */
.morework--picker .wrap { max-width: var(--max); }
@media (min-width: 760px) {
  .morework--picker .morework__grid {
    --g: clamp(1.1rem, 2.5vw, 1.8rem);
    display: flex; flex-wrap: wrap; justify-content: center; gap: var(--g);
  }
  /* the gap has to come out of the basis or three never fit - a flat 33.333%
     silently fell back to two per row */
  .morework--picker .mcard { flex: 0 0 calc((100% - 2 * var(--g)) / 3); }
}
.morework--picker .mcard__title { font-size: clamp(1.05rem, 1.7vw, 1.3rem); }
.morework--picker .mcard__sub { font-size: .76rem; }

/* ==========================================================================
   About  (build/about.py)
   ========================================================================== */

/* The hero is one image and three lines - no facts row - so it does not need
   the full-screen treatment the project pages use. */
.shero--about { min-height: 66svh; }
@media (min-aspect-ratio: 17 / 10) { .shero--about { min-height: 78svh; } }
.shero--about .lead { max-width: 58ch; }

/* --- The team ----------------------------------------------------------- */
.crew__list { display: grid; gap: clamp(2.6rem, 6vw, 4.5rem); margin-top: clamp(2.4rem, 5vw, 3.6rem); }
.person { display: grid; gap: clamp(1.2rem, 3vw, 2.4rem); align-items: start; }
@media (min-width: 760px) {
  .person { grid-template-columns: minmax(220px, 300px) 1fr; }
  /* Alternate the side so a column of three does not read as a form. The
     photo keeps its DOM order for screen readers; only the visual side moves.
     The track sizes have to flip with it - moving the shot into column 2 while
     the 300px cap stays on column 1 squeezes the bio into the narrow track. */
  .person:nth-child(even) { grid-template-columns: 1fr minmax(220px, 300px); }
  .person:nth-child(even) > .person__shot { order: 2; }
}
.person__shot { position: relative; overflow: hidden; border-radius: 3px; background: #0b0b0b; }
.person__shot img {
  display: block; width: 100%; height: auto;
  /* Three headshots lit in three different rooms - one moody, one grey, one
     on an orange backdrop. Pulling the saturation back unifies them into the
     same page without draining them to grey. */
  filter: saturate(.62) contrast(1.04);
}
.person__body h3 { font-size: clamp(1.5rem, 3.2vw, 2.1rem); margin: 0; }
.person__role {
  font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase;
  color: var(--gold); margin: .5rem 0 1.1rem;
}
.person__body p + p { margin-top: .9rem; }
.person__body p { max-width: 62ch; opacity: .82; }

/* --- The process rail --------------------------------------------------- */
/* Numbered because it genuinely is a sequence: you cannot edit before you
   shoot. The rail is the through-line, and each stop lights as it arrives. */
.arc {
  --arc-gap: clamp(2rem, 4.5vw, 3.2rem);
  list-style: none; margin: clamp(2.4rem, 5vw, 3.6rem) 0 0; padding: 0;
  display: grid; gap: var(--arc-gap);
}
.arc__item { display: grid; grid-template-columns: auto 1fr; gap: clamp(1rem, 3vw, 2.2rem); align-items: start; }
.arc__no {
  font-size: clamp(.8rem, 1.4vw, .95rem); letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
  color: rgba(255,255,255,.34);
  padding-top: .45rem;
  /* Stretch the number cell to the full height of its stage so the rail can
     run the whole way down and cross the gap into the next number - a line
     that stops short reads as a mistake rather than a through-line. */
  align-self: stretch;
  display: flex; flex-direction: column; align-items: center; gap: .7rem;
}
.arc__no::after {
  content: ""; flex: 1 1 auto; width: 1px;
  background: rgba(255,255,255,.14);
  margin-bottom: calc(-1 * var(--arc-gap));
}
.arc__item:last-child .arc__no::after { display: none; }
.arc__item.on .arc__no { color: var(--gold); transition: color .5s ease .1s; }
.arc__stage {
  font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase;
  color: rgba(255,255,255,.5); margin: 0 0 .35rem;
}
.arc__body h3 { font-size: clamp(1.35rem, 2.8vw, 1.9rem); margin: 0 0 .8rem; }
.arc__body p { max-width: 68ch; opacity: .82; }

/* --- Pull quote --------------------------------------------------------- */
.pquote {
  margin: clamp(3rem, 7vw, 5rem) 0 0; padding: clamp(1.6rem, 4vw, 2.6rem);
  border: 1px solid var(--line); border-radius: 4px;
  background: rgba(255,255,255,.02);
  display: grid; gap: 1.2rem; justify-items: start;
  max-width: 62rem;
}
.pquote img { width: 62px; height: 62px; border-radius: 50%; object-fit: cover; filter: saturate(.7); }
.pquote blockquote {
  margin: 0; font-size: clamp(1.05rem, 2vw, 1.3rem); line-height: 1.5;
  max-width: 60ch;
}
.pquote blockquote::before { content: "\201C"; }
.pquote blockquote::after { content: "\201D"; }
.pquote figcaption { display: grid; gap: .2rem; font-size: .84rem; }
.pquote figcaption b { color: var(--white); }
.pquote figcaption span { color: rgba(255,255,255,.5); }

/* --- Services ----------------------------------------------------------- */
.svcs {
  margin-top: clamp(2.4rem, 5vw, 3.6rem);
  display: grid; gap: 1px; background: var(--line);
  border: 1px solid var(--line); border-radius: 4px; overflow: hidden;
}
@media (min-width: 620px) { .svcs { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .svcs { grid-template-columns: repeat(3, 1fr); } }
.svc { background: var(--black); padding: clamp(1.4rem, 3vw, 2.1rem); }
.svc h3 { font-size: clamp(1.05rem, 1.9vw, 1.25rem); margin: 0 0 .6rem; }
.svc p { font-size: .95rem; line-height: 1.6; opacity: .7; margin: 0; }

/* ==========================================================================
   Contact  (build/contact.py)
   ========================================================================== */

/* Shorter than a project hero - the page's job is the form below it, so the
   picture sets the tone and then gets out of the way. */
.shero--contact { min-height: 58svh; }
@media (min-aspect-ratio: 17 / 10) { .shero--contact { min-height: 68svh; } }
@media (max-width: 699px) { .shero--contact { min-height: 62svh; } }
.shero--contact .lead { max-width: 46ch; }
/* The type sits over the operator on the left of an already dark studio, so
   this wash is lighter than the generic one, not heavier - enough to separate
   white type from a mid-grey cyc without crushing him into the background.
   It clears well before the lantern so the lit side stays lit. */
.shero--contact .scene__bg::after {
  background:
    linear-gradient(90deg, rgba(0,0,0,.6) 0%, rgba(0,0,0,.34) 38%, rgba(0,0,0,.06) 66%, transparent 100%),
    /* the lantern's hotspot sits directly under the nav on this frame, and
       "What you get" was reading as grey-on-white up there */
    linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.18) 12%, transparent 26%),
    linear-gradient(180deg, transparent 40%, rgba(0,0,0,.42) 100%);
}
.cgrid { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: start; }
@media (min-width: 900px) {
  /* The form gets the wider track - it is the thing being asked for. */
  .cgrid { grid-template-columns: minmax(0, 1.35fr) minmax(260px, 1fr); gap: clamp(2.5rem, 6vw, 5rem); }
}
.cpanel h2, .cside h2 { font-size: clamp(1.3rem, 2.6vw, 1.75rem); margin: 0; }
.cpanel__note { margin: .7rem 0 0; opacity: .66; max-width: 46ch; font-size: .95rem; }
.cpanel .form { margin-top: 1.6rem; max-width: none; }

.cinfo { margin: 1.6rem 0 0; display: grid; gap: 0; }
.cinfo__row {
  display: grid; gap: .25rem;
  padding: 1rem 0; border-top: 1px solid var(--line);
}
.cinfo__row:last-child { border-bottom: 1px solid var(--line); }
.cinfo dt {
  font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}
.cinfo dd { margin: 0; font-size: 1.02rem; }
.cinfo dd a { color: var(--gold); text-decoration: none; border-bottom: 1px solid rgba(230, 193, 122, 0.35); }
.cinfo dd a:hover { border-bottom-color: var(--gold); }

/* --- Booking facade ----------------------------------------------------- */
.book__stage {
  margin-top: clamp(1.6rem, 4vw, 2.4rem);
  border: 1px solid var(--line); border-radius: 4px;
  background: #fff; overflow: hidden;
}
/* The widget renders on a white ground of its own, so the box becomes a
   frame around it rather than a panel behind it.
   The height is fixed because GHL's auto-resize needs their embed script,
   which we do not load - so the frame scrolls internally instead, and
   scrolling must NOT be disabled or the later slots are unreachable. */
.book__stage iframe { display: block; width: 100%; height: 780px; border: 0; }
@media (max-width: 699px) { .book__stage iframe { height: 940px; } }

/* ==========================================================================
   404  (build/notfound.py)
   ========================================================================== */

/* Fills the first screen; the footer sits just below it. Someone who has hit a
   dead end wants a way out, not a page to read. */
.nf {
  position: relative;
  min-height: 100svh;
  display: grid; align-items: center;
  padding-block: clamp(6rem, 14vh, 9rem) clamp(3rem, 8vh, 5rem);
  overflow: hidden;
}
.nf__inner {
  display: grid; gap: clamp(2.5rem, 6vw, 5rem);
  justify-items: center; text-align: center;
}
@media (min-width: 900px) {
  /* Disc left, copy right. The disc is the thesis, so it leads. */
  .nf__inner {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center; justify-items: start; text-align: left;
  }
}

/* --- the leader disc ---------------------------------------------------- */
.nf__leader {
  width: clamp(250px, 38vmin, 440px); height: auto;
  overflow: visible;
  /* A leader is projected light, not ink. The glow sells that in one line. */
  filter: drop-shadow(0 0 60px rgba(255, 255, 255, .07));
}
.nf__ring { fill: none; stroke: rgba(255, 255, 255, .3); stroke-width: 1; vector-effect: non-scaling-stroke; }
.nf__ring--outer { stroke: rgba(255, 255, 255, .55); stroke-width: 1.5; }
.nf__ring--faint { stroke: rgba(255, 255, 255, .16); }
.nf__cross { stroke: rgba(255, 255, 255, .22); stroke-width: 1; vector-effect: non-scaling-stroke; }
.nf__hand { stroke: rgba(255, 255, 255, .5); stroke-width: 1; vector-effect: non-scaling-stroke; }

.nf__sweep { transform-origin: 100px 100px; animation: nf-sweep 2s linear infinite; }
@keyframes nf-sweep { to { transform: rotate(360deg); } }

.nf__num {
  text-anchor: middle; dominant-baseline: central;
  fill: var(--white);
  font-family: var(--font); font-weight: 800;
  font-size: 62px; letter-spacing: -0.045em;
  /* Tabular so the two 4s sit symmetrically either side of the centre mark -
     proportional digits put the group very slightly off the crosshair. */
  font-variant-numeric: tabular-nums;
}

/* --- copy --------------------------------------------------------------- */
.nf__copy h1 { font-size: clamp(2.4rem, 6.5vw, 4.6rem); line-height: 1.02; }
.nf__copy .lead { margin-top: 1.3rem; max-width: 42ch; }
.nf__copy .ctas { margin-top: 2.2rem; display: flex; flex-wrap: wrap; gap: .7rem; }
@media (max-width: 899px) { .nf__copy .ctas { justify-content: center; } }

/* --- grain -------------------------------------------------------------- */
/* Real grain, not a gradient: feTurbulence as a data URI. Costs no request and
   stops a very dark, very empty screen reading as a rendering failure. */
.nf__grain {
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
  opacity: .30; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* A vignette pulls the eye to the disc and the type. */
.nf::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 90% at 50% 45%, transparent 40%, rgba(0,0,0,.55) 100%);
}
.nf__inner { position: relative; z-index: 2; }

/* --- projector strike ---------------------------------------------------- */
/* One orchestrated moment on load: the page comes up the way a projector lamp
   does, in two stutters rather than a clean fade. Runs once, ~700ms, and never
   blocks the content - the page is fully readable throughout. */
.nf__inner, .nf__grain { animation: nf-strike .72s ease-out both; }
.nf__leader { animation: nf-strike .72s ease-out both, nf-settle .9s cubic-bezier(.2,.8,.25,1) both; }
@keyframes nf-strike {
  0%   { opacity: 0; }
  18%  { opacity: .55; }
  30%  { opacity: .12; }
  55%  { opacity: .92; }
  70%  { opacity: .6; }
  100% { opacity: 1; }
}
@keyframes nf-settle {
  from { transform: scale(.965); }
  to   { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .nf__sweep { animation: none; }
  .nf__inner, .nf__grain, .nf__leader { animation: none; opacity: 1; transform: none; }
}

/* --- About: the middle card stands taller ---------------------------------- */
/* Dan's card gets a deeper frame than the two either side - 1:1.28 against
   their 1:1.06 - so it reads as the centre of the group rather than one of
   three identical prints. .team__row is align-items:start, so the extra height
   hangs downwards and the three tops stay on one line.
   Scoped to .crew so the homepage set stays as it is. */
.crew .card--mid .card__shot { aspect-ratio: 1 / 1.28; }

/* --- Picker page: close the gap under the title ---------------------------- */
/* On /projects the heading and the cards are the whole page, so the space
   between them is pure friction. The adjacency selector is the point: it only
   matches when a text-only hero is immediately followed by the card grid,
   which is exactly the picker and nothing else. About's picker sits after the
   services section, so it keeps its normal spacing. */
/* Spacing around the boundary line between the hero and the cards.
   Two mistakes were made here in order. First the section padding was zeroed
   but the padding actually lives on .scene__inner, so 207px stayed. Then the
   air was moved onto the hero's padding-bottom, which put it ABOVE the visible
   edge between the two sections - so the line ran flush against the tops of the
   cards while the space sat where nobody reads it.
   It belongs below the line, and it has to match what sits under the grid. */
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)) + .morework {
  padding-top: 0;
}
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)) + .morework .scene__inner {
  padding-top: clamp(2.6rem, 5vw, 4rem);
  padding-bottom: clamp(2.6rem, 5vw, 4rem);
}
/* .morework__grid carries its own margin-top, which stacked on the padding and
   made the gap above the cards a constant 31px bigger than the one below. With
   no heading between them it has nothing to space away from. */
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)) + .morework .morework__grid {
  margin-top: 0;
}
/* The heading's own breathing room, above the line rather than below it. */
.shero:not(:has(.scene__bg img)):not(:has(.scene__bg picture)):has(+ .morework) .shero__inner {
  padding-bottom: clamp(2rem, 3.6vw, 2.9rem);
}

/* --- Cycling headline word ------------------------------------------------- */
/* No width, no min-width, no tabular figures: the point of the effect is that
   the full stop moves as the word changes. inline-block only so the colour
   transition has a box to apply to. */
.cyc { display: inline-block; transition: color .18s ease; }
.cyc--live { color: var(--gold); }
