/* ==========================================================================
   The Canadian Tank Museum - flagship immersive homepage
   Built to direction.md. Tokens, type and spacing are exact.
   ========================================================================== */

/* ---------- 1. Fonts (self-hosted, latin subset) ------------------------- */

/* DISPLAY FACE: Archivo, client-directed swap from Saira Condensed. Google's
   latin subset ships one variable file covering the whole weight axis, so 800
   and 900 come out of the same 34 KB download rather than two static cuts, and
   the page carries one fewer font request than it did. font-display: swap is
   unchanged from the faces it replaces: both hero and assembly are pinned
   canvases whose copy is absolutely positioned in a fixed rail, so a swap
   changes glyph widths inside a box that is already sized by the rail and never
   reflows the runway. The Saira files stay on disk, unreferenced. */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 800 900;
  font-display: swap;
  src: url('../assets/fonts/archivo-var.woff2') format('woff2-variations'),
       url('../assets/fonts/archivo-var.woff2') format('woff2');
}

@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../assets/fonts/hanken-grotesk-var.woff2') format('woff2-variations'),
       url('../assets/fonts/hanken-grotesk-var.woff2') format('woff2');
}

/* ---------- 2. Tokens ---------------------------------------------------- */

:root {
  /* palette (direction.md, exact) */
  --ground: #141314;
  --ground-deep: #0F0E0E;
  --panel: #1C1B19;
  --bone: #F2EFE9;
  --ink: #141314;
  --text: #EDEAE4;
  --text-mute: #9C978E;
  --text-mute-ink: #5C574F;
  --hairline: #2A2825;
  --hairline-ink: #D8D3C9;
  --signal: #C8102E;

  /* type */
  --display: 'Archivo', 'Helvetica Neue', 'Segoe UI', Arial, sans-serif;
  /* the display face's own metrics, set once and read by every display block:
     Archivo is a normal-width grotesque where Saira Condensed was condensed, so
     the tracking tightens and the leading opens a step. */
  --display-track: -0.04em;
  --display-lead: 0.9;
  --sans: 'Hanken Grotesk', 'Avenir Next', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;

  /* The scale was authored around a CONDENSED face. Archivo sets about 1.5x
     wider per character, so the same numbers would put a phone headline five
     lines deep and a tablet band headline past its rail. Every ceiling and
     floor comes down by roughly a third, which keeps the RENDERED line lengths
     and the roughly 12x scale contrast the direction specifies rather than
     keeping numbers that no longer mean the same thing in a different face. */
  --size-xl: clamp(46px, 9vw, 140px);
  --size-l: clamp(38px, 6vw, 96px);
  --size-m: clamp(30px, 3.8vw, 58px);
  --size-lede: clamp(20px, 1.6vw, 26px);

  /* spacing ladder: 8 16 24 32 48 64 96 128 160 200 */
  --margin: 64px;
  --maxw: 1440px;
  --gutter: 32px;
  --pad-band: 160px;
  --pad-climax: 200px;

  --nav-h: 88px;

  /* width of the live content column, used to fit display lines to the grid */
  --content: min(calc(100vw - var(--margin) * 2), var(--maxw));

  /* THE RAIL. The single vertical line every band, every headline, every
     caption and every hero beat starts on: the page margin, or the edge of the
     centred content column once the viewport is wider than it. Everything that
     used to compute this inline now reads this one token, so nothing can drift
     off the line by a margin's worth at one breakpoint and not another. */
  --rail: max(var(--margin), calc((100vw - var(--maxw)) / 2));

  /* motion */
  --ease: cubic-bezier(0.33, 0, 0.15, 1);
  --dur: 0.8s;
}

@media (max-width: 1023px) {
  :root {
    --margin: 32px;
    --pad-band: 128px;
    --pad-climax: 128px;
  }
}

@media (max-width: 767px) {
  :root {
    --margin: 24px;
    --pad-band: 120px;
    --pad-climax: 120px;
    --nav-h: 72px;
  }
}

/* the smallest phones: the display floors are dropped so the longest single
   word of every headline still fits the measure and nothing can overflow */
@media (max-width: 359px) {
  :root {
    --size-xl: clamp(38px, 12vw, 140px);
    --size-l: clamp(32px, 9.5vw, 96px);
  }
}

/* ---------- 3. Reset ----------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: 96px;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  font-weight: 400;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, p, ul, figure, table { margin: 0; }
ul { padding: 0; list-style: none; }
img { display: block; max-width: 100%; height: auto; }

a { color: inherit; }

:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 4px;
}

.on-bone :focus-visible { outline-color: var(--ink); }

::selection { background: var(--text); color: var(--ground); }

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

.skip-link {
  position: absolute;
  left: var(--margin);
  top: 0;
  z-index: 200;
  transform: translateY(-140%);
  background: var(--bone);
  color: var(--ink);
  padding: 12px 24px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform 0.24s var(--ease);
}

.skip-link:focus { transform: translateY(16px); }

/* ---------- 4. Type primitives ------------------------------------------ */

.d-xl, .d-l, .d-m {
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: var(--display-track);
  line-height: var(--display-lead);
  font-weight: 900;
}

.d-xl { font-size: var(--size-xl); }
.d-l  { font-size: var(--size-l); }
.d-m  { font-size: var(--size-m); font-weight: 800; }

/* Line-fitting: the display face is condensed, so the longest specified line of
   each headline is measured in em and the size is capped so the art-directed
   line breaks hold instead of re-wrapping. Below 768px the lines are allowed to
   wrap naturally, because fitting a 29-character line to a phone would shrink
   the type past the point of impact. */
@media (min-width: 768px) {
  .fit { font-size: min(var(--cap, var(--size-xl)), calc(var(--content) / var(--em, 1))); }
}


.lede {
  font-size: var(--size-lede);
  line-height: 1.45;
  color: var(--text-mute);
  max-width: 34ch;
}

.body {
  font-size: 17px;
  line-height: 1.6;
  max-width: 62ch;
  color: var(--text-mute);
}

.label {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-mute);
}

/* ---------------------------------------------------------------------------
   THE EYEBROW. One treatment page-wide, on every band copy.md lists one for and
   on no other: the Label token exactly as it already exists, set as plain small
   caps on the rail directly above that band's headline, 24px on the ladder
   between the two. NO pill, NO dot, NO numbering, NO rule, no second size and
   no second tracking anywhere on the page. Not on the hero and not in the
   footer, which copy.md gives none.

   The one thing that varies is the tone, and it varies the way every other
   label-class block on this page already varies: --text-mute on flat ground,
   full --text over a bed (amendment 25, copy over media), --text-mute-ink on
   bone. Measured per surface in BUILD-NOTES.
   --------------------------------------------------------------------------- */
.eyebrow { margin-bottom: 24px; }

/* a single line carries its own reveal rather than sitting in a wrapper whose
   only child it would be; initReveals() observes it like any other .reveal */
.js .eyebrow.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}

.js .eyebrow.reveal.is-in { opacity: 1; transform: none; }

/* the eyebrow and its headline share one cell wherever the parent is a gap
   driven grid, so the 24px step between them is always the eyebrow's own margin
   and never the parent's row gap */
.head-stack { display: block; }

.eyebrow--media { color: var(--text); }
.on-bone .eyebrow { color: var(--text-mute-ink); }

.num { font-variant-numeric: tabular-nums; }

/* SI symbols keep their case inside an uppercased display line: M is mega,
   m is metre */
.unit { text-transform: none; }

/* holds a date or a figure together so a line cannot break inside it */
.nowrap { white-space: nowrap; }

.signal { color: var(--signal); }

.tag {
  display: inline-block;
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 8px 12px;
  background: var(--signal);
  color: var(--bone);
}

/* ---------- 5. Layout helpers ------------------------------------------- */

.wrap {
  width: 100%;
  max-width: calc(var(--maxw) + var(--margin) * 2);
  margin-inline: auto;
  padding-inline: var(--margin);
}

.band { padding-block: var(--pad-band); }

/* ---------------------------------------------------------------------------
   THE PAGE'S ONE GRID.

   Every band that pairs two columns runs the SAME 12-column grid with the 32px
   gutter direction.md specifies, and takes one of exactly two placements:

     7/5   primary on columns 1 to 7, secondary on columns 8 to 12
     5/7   primary on columns 1 to 5, secondary on columns 6 to 12

   The 5/7 mirror belongs to Plan Your Visit alone, which the direction
   specifies that way. Nothing else on the page invents a third split, and
   nothing uses an fr pair with a fluid gap any more: five bands were doing
   that with five different gaps, so no two of them landed on the same vertical
   line and the body read as if it had no grid at all. Screenshot any two bands
   now and the columns line up.
   --------------------------------------------------------------------------- */
.collection,
.p1918__intro,
.p1918__archive,
.duffy__stats,
.duffy__prose,
.aquino__after,
.visit__grid,
.join__content {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 48px;
  align-items: start;
}

/* ---------- 6. Buttons --------------------------------------------------- */

.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 28px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.4s var(--ease), color 0.4s var(--ease), border-color 0.4s var(--ease);
}

/* :active mirrors :hover so the press reads on touch, where hover never fires.
   The bone pill used to hover to --text, which is five units off --bone and did
   not register as a state at all. --hairline-ink is the palette's bone one step
   down, so the hover reads without adding a colour the page does not own.
   Colour only, no lift, per the motion spec. */
.pill--bone { background: var(--bone); color: var(--ink); }
.pill--bone:hover,
.pill--bone:active { background: var(--hairline-ink); }

.pill--ink { background: var(--ink); color: var(--bone); }
.pill--ink:hover,
.pill--ink:active { background: #000; }

.pill--ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--text);
}
.pill--ghost:hover,
.pill--ghost:active { background: var(--text); color: var(--ground); }

.pill--ghost-ink {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.pill--ghost-ink:hover,
.pill--ghost-ink:active { background: var(--ink); color: var(--bone); }

.pill--sm { min-height: 44px; padding: 0 22px; font-size: 12px; }

/* ---------- 7. Header ---------------------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--nav-h);
  pointer-events: none;
}

/* the fill is a scalable layer, so the bar shrinks on transform and opacity
   only, never on height */
.header__bg {
  position: absolute;
  inset: 0;
  background: var(--ground);
  transform-origin: top center;
  opacity: 0;
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}

.header.is-filled .header__bg { opacity: 1; pointer-events: auto; }

/* with scripting off there is no scroll state to read, so the bar is solid */
.no-js .header__bg { opacity: 1; pointer-events: auto; }
.no-js .nav__link { color: var(--text-mute); }
.header.is-solid .header__bg { transform: scaleY(0.727); }

.header__inner {
  position: relative;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  transition: transform 0.4s var(--ease);
}

.header.is-solid .header__inner { transform: translateY(-12px); }

.header__inner > * { pointer-events: auto; }

/* The museum's own logo, knockout lockup. The white badge plate it replaces was
   a bright rectangle sitting on a near-black cinematic page and on the hero
   frames; the knockout is bone type and a bone tank on transparency, so the
   header carries the mark and nothing else. It needs no fill, no border and no
   treatment behind it in any header state. Height is the only thing the page
   sets: 32px in the 88px bar, 26px in the 72px mobile bar. That is smaller than
   the badge was, and it can be, because a knockout lockup reads at a size a
   boxed badge cannot. */
.wordmark {
  display: block;
  text-decoration: none;
  line-height: 0;
}

.wordmark picture { display: block; }

.wordmark img {
  display: block;
  height: 32px;
  width: auto;
}

@media (max-width: 767px) {
  .wordmark img { height: 26px; }
}

.nav { display: flex; align-items: center; gap: 32px; }

.nav__list { display: flex; align-items: center; gap: 32px; }

/* over the transparent header the links carry full --text; they only drop to
   the muted tone once the solid ground fill is behind them */
.nav__link {
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  transition: color 0.3s var(--ease);
}

.header.is-filled .nav__link { color: var(--text-mute); }
.nav__link:hover,
.header.is-filled .nav__link:hover { color: var(--text); }

.nav-toggle {
  display: none;
  align-items: center;
  gap: 12px;
  background: none;
  border: 0;
  padding: 8px 0;
  cursor: pointer;
  color: var(--text);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transition: color 0.3s var(--ease);
}

.nav-toggle:hover,
.nav-toggle:active { color: var(--text-mute); }

.nav-toggle__bars {
  display: grid;
  gap: 5px;
  width: 26px;
}

/* currentColor so the bars follow the button's own hover state */
.nav-toggle__bars span {
  display: block;
  height: 2px;
  background: currentColor;
  transition: transform 0.4s var(--ease);
}

/* open state: the two bars cross, matching the Menu / Close label swap */
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:first-child {
  transform: translateY(3.5px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

@media (max-width: 899px) {
  .nav { display: none; }
  .nav-toggle { display: inline-flex; }
}

/* overlay menu */
.menu {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: var(--ground);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 32px;
  padding: calc(var(--nav-h) + 32px) var(--margin) 48px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s var(--ease), visibility 0s linear 0.5s;
}

.menu.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.5s var(--ease), visibility 0s linear 0s;
}

.menu__list { display: grid; gap: 16px; }

.menu__link {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(38px, 11vw, 76px);
  line-height: 0.94;
  letter-spacing: var(--display-track);
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
  display: block;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
}

.menu.is-open .menu__link {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--i) * 0.06s + 0.1s);
}

/* hover is colour and underline only, the same language as the footer links */
.menu__link:hover,
.menu__link:active {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 10px;
}

.menu__foot { margin-top: 16px; }

/* ==========================================================================
   8. HERO - pinned Sherman orbit
   ========================================================================== */

/* The runway sets the pace. At 900vh the scrollable span is 800vh, TWO viewports
   of that are the exit tail, and the remaining 600vh is spread over five beats:
   120vh a beat, an 84vh hold, and 4.1vh of scroll for every frame of the orbit
   against the 2.8vh it used to get. The phone keeps the same shape at 660vh.
   The extra 100vh over the last pass is the second viewport of tail and nothing
   else: the scrub still owns 600vh, and the beat windows are computed from
   progress and the number of live beats, so the frames each beat is measured
   against are the same at any runway height. */
.hero {
  position: relative;
  z-index: 0;
  height: 900vh;
}

@media (max-width: 767px) {
  .hero { height: 660vh; }
}

/* 1px marker: once it leaves the viewport the header takes its ground fill */
.hero__top {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.hero__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--ground);
}

.hero__poster,
.hero__poster img,
.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.hero__poster img { object-fit: cover; }

.hero__canvas { display: block; }

/* The rail is the containing block for the beats. It starts below the fixed
   header and holds the same gutter every other band holds, so hero copy lands
   on the page's own vertical alignment lines instead of running to the edge.
   Absolutely positioned children resolve against this box, so every beat
   clears the header at every width without magic offsets. */
.hero__beats {
  position: absolute;
  top: calc(var(--nav-h) + 24px);
  bottom: 24px;
  left: var(--rail);
  right: var(--rail);
}

/* JS drives opacity continuously so each beat can hold at full strength across
   the middle 70% of its segment; the mask reveal below still runs off the
   is-in class. Opacity alone owns the fade: nothing here may hide a beat from
   the accessibility tree, because this block carries the page's only h1.
   Hit testing and focus are handled per beat in JS (pointer-events and inert),
   so a faded beat never swallows a click or takes focus. */
.beat {
  position: absolute;
  opacity: 0;
}

/* masked line reveal */
.mask { display: block; overflow: hidden; padding-bottom: 0.02em; }

/* the offset only exists where JS will animate it back, so the copy is fully
   visible with scripting off */
.mask > span { display: block; }

.js :is(.beat, .ex) .mask > span {
  transform: translateY(105%);
  transition: transform var(--dur) var(--ease);
  transition-delay: calc(var(--i, 0) * 0.08s);
}

.js :is(.beat, .ex).is-in .mask > span { transform: none; }
.js :is(.beat, .ex).is-out .mask > span { transform: translateY(-105%); }

/* ---------------------------------------------------------------------------
   V9 footage, re-placed on ONE RAIL.

   The client's read of the hero was that the copy was all over the place, and
   the measurement is why it was: each beat had been given the best position
   anywhere inside the rail box, so the five of them landed on five different
   left offsets (0%, 0%, 28.65%, 3.2%, 0%) and the climax was centred inside its
   own block on top of that. Five beats, four horizontal starts, and the eye had
   nothing to hold on to between them.

   The horizontal is now a constraint rather than a variable: EVERY beat starts
   on --bx 0%, which is the page rail, the same line every band opener below the
   hero starts on, and every beat is left aligned. Only the vertical is searched.
   Where a beat could not reach the build's roughly 1% bar on the rail at its
   shipped size, the SIZE was stepped down one notch rather than the beat
   leaving the rail: the trade is recorded in BUILD-NOTES.md.

   Method, unchanged: each frame of a beat's hold window is rendered at full
   resolution in screen space exactly as the canvas paints it, focal keyframes
   and clamps included, a block fails a pixel where the background drops that
   block's own shipping colour under its target ratio, a position is scored by
   its WORST FRAME, and the search is exhaustive over every integer vertical
   position in the rail with the sizes swept alongside it.

   Measured at 1280x800, worst frame in each hold window. Three columns: the
   off-rail Saira Condensed placement that shipped, the same placement on the
   rail, and what ships now, which is on the rail in Archivo. Same sampler for
   all three, so they are comparable.

     Beat / block                 off-rail   on-rail   ARCHIVO, shipped
     A  label                       0.43       0.43       0.60   58px
     A  4,000+ ARTEFACTS            0.70       0.67       0.47
     A  1,560 m2 MVCC               0.41       0.41       0.78
     B  OSHAWA, ONTARIO             0.00       0.00       0.00   96px
     B  HISTORY                     0.36       0.36       0.55
     B  LIVES HERE.                 1.28       1.28       1.82
     C  AND IT STILL RUNS.          0.11       1.00       0.74   58px
     C  M4 SHERMAN                  0.12       0.00       0.72
     D  STILL, and the full stop    0.00       0.00       0.00   76px
     E  bone pill / ghost pill      0.00       0.00       0.00 / 0.00
     D  the accent word            33.74      37.08      31.72

   Two things paid for the display swap. Archivo sets about 1.5x wider per
   character, so every beat's block covers materially more of the frame at the
   same point size and the sizes step down with it: the h1 from 112px to 96px
   (112px measures 6.36%, past this build's bar and past the 3% gate; 104px
   measures 2.64%; 96px measures 1.82%), the data stack and the tagline to the
   Display M token, and the climax from 100px to 76px. The climax is the one
   size chosen against the accent rather than against the bar: its --text line
   is 0.00% at every size in the sweep, while the accent word reads 68.19% at
   100px and 31.72% at 76px, which is the best that word has measured on this
   footage in any face at any position.

   The one block that cannot clear its ratio is the accent word inside the
   climax: #C8102E sits inside this hangar's mid tones and no position in the
   frame does better, on the rail or off it. See BUILD-NOTES.md and
   CLIENT-QUESTIONS.md. No scrim was added; the ban stands.
   --------------------------------------------------------------------------- */

/* width is the content's own width, capped per beat, rather than whatever the
   left offset happens to leave. Two things follow: the art-directed line breaks
   hold at any position, and the measured block geometry is the same wherever
   the block is placed, so the placement search below is exact.

   A beat placed in the lower half of the rail is anchored from the BOTTOM
   (--bb) rather than from the top (--by). The measured position is identical at
   the 1280x800 the search runs at, but a percentage top plus a fixed block
   height walks off the bottom of a short viewport: at 1024x600 the tagline used
   to hang 38px below the fold. Anchored from the bottom it cannot, at any
   height. Each beat sets exactly one of the two. */
/* Under the condensed face each beat carried a hand-set max-width, which is
   how a beat ends up with a measure nothing else on the page shares. In a
   normal-width grotesque a beat box is simply its own longest line, capped by
   the rail: width max-content, max-width 100%. One rule, five beats, and the
   size sweep below is what keeps every line unwrapped. */
.beat {
  left: var(--bx, 0);
  top: var(--by, auto);
  bottom: var(--bb, auto);
  width: max-content;
  max-width: 100%;
}

/* ---------------------------------------------------------------------------
   THE HERO HALO.  (client-directed, this pass: "still very weak")

   The hero display type had been sized DOWN, twice, to a per-pixel contrast
   sampler that measures the raw frame under each glyph. That sampler cannot
   model a text shadow, so every size decision it drove assumed the type had no
   separation of its own and the only lever was point size. The result reads as
   a caption on a film still rather than as a poster.

   This is the separation a cinema poster prints with: two soft dark shadows on
   the TYPE, one tight and one wide, and nothing else. It is not a scrim, not a
   gradient, not a plate and not a box: nothing is painted between the frame and
   the type, the frame is untouched, and the ban on all four stands. It is set
   ONLY on hero beat display text, where copy sits on live footage; no other
   block on the page carries it and no boxed element anywhere gets a shadow.

     tight   0 1px 2px  rgba(0,0,0,0.32)     kills the halation at the edge
     wide    0 2px 30px rgba(0,0,0,0.26)     lifts the block off the field
     total alpha 0.58, inside the 0.5 to 0.6 the direction for this pass sets.

   With it, the sizes go back up past anything this hero has shipped: the h1 to
   124px against 96px, the climax to 104px against 76px, the tagline and the
   data stack up one notch to 62px against 58px. Evidence is screenshot crops of
   the worst frame in each hold window, in BUILD-NOTES.md, because the numeric
   sampler is blind to the shadow by construction.
   --------------------------------------------------------------------------- */
.beat .d-xl,
.beat .d-m,
.beat .label {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.32), 0 2px 30px rgba(0, 0, 0, 0.26);
}

/* the pills are solid fills, not type on footage, so they take none of it */
.beat .pill { text-shadow: none; }

/* A: collection data stack. On the rail, at the vertical the search returns.
   The pocket is stable rather than a spike: the worst reading anywhere within
   10px of this top is 0.99%. */
.beat-a {
  --bx: 0%;
  --by: 55.4%;
  display: grid;
  gap: 16px;
}

/* one notch up on the halo: the Display M token is 48.6px at 1280, this is 62px */
.beat-a .d-m { font-size: clamp(32px, 4.85vw, 62px); }
.beat-a__label { max-width: 34ch; line-height: 1.35; }

/* Over the frames the muted tone bottoms out under 1:1, so every label inside a
   beat takes full --text. --text-mute stays the body colour on flat ground. */
.beat .label { color: var(--text); }

/* B: the h1, flush in the rail's top left corner. Left aligned was already the
   measured winner here (3.9% right aligned against 1.18% left at 112px, because
   the ragged edge pushes the long line into the lit hull), so the rail costs
   this beat nothing. The locator keeps the line above the headline. */
.beat-b {
  --bx: 0%;
  --by: 0.5%;
}

/* the h1 back to poster scale: 124px at 1280 against the 96px the sampler-only
   build shipped, and it holds its two art-directed lines to 1920 */
.beat-b .d-xl { font-size: clamp(44px, 9.7vw, 132px); }
.beat-b__label { margin-bottom: 16px; }

/* C: the tagline. Its window is the closest part of the orbit, where the hull
   carries the light across the middle of the frame. It used to buy its way out
   of that by sitting at 28.65% of the rail; on the rail it takes the vertical
   the search returns and, if that will not hold, one step down in size. The
   label sits under its left edge. */
.beat-c {
  --bx: 0%;
  --by: 0%;
}

.beat-c .d-m { font-size: clamp(32px, 4.85vw, 62px); }
.beat-c__label { margin-top: 16px; }

/* D: the climax. It used to sit at 3.2% of the rail and centre its own line
   inside a 56% block, which put the one line the page is built around on a
   vertical nothing else on the page uses. It is now on the rail and left
   aligned like every other beat. */
.beat-d {
  --bx: 0%;
  --by: 59.0%;
  text-align: left;
}

/* the climax, up from 76px to 104px at 1280. The accent word is the one block
   on this footage that cannot clear 3:1 at any size or position (see
   BUILD-NOTES); the halo is the first thing this build has been able to put
   under it, and the size is now chosen for the poster rather than as the least
   bad reading of an unwinnable measurement. */
.beat-d .d-xl { --cap: clamp(40px, 8.13vw, 112px); --em: 8.55; }

/* E: the CTA bar, low on the left where the floor falls into shadow through
   the whole closing window. Both pills measure 0.00% there. */
.beat-e {
  --bx: 0%;
  --bb: 17.2%;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

/* Portrait runs its own placement: the crop is a different composition, the
   blocks are full width and taller, and only B, D and E are live, so the
   vertical positions are measured against the portrait crop separately. */
@media (max-width: 767px) {
  .beat-a, .beat-b, .beat-c, .beat-d, .beat-e {
    --bx: 0;
    max-width: 100%;
    width: auto;
    left: 0;
    right: 0;
  }
  .beat-b { text-align: left; --by: auto; --bb: 5.7%; }
  /* the desktop clamp bottoms out at 40px on a phone, which is well under the
     Display XL token and was never asked for. Measured against the portrait
     crop the token size is the better block: the two headline lines read
     0.05% and 1.48% against 0.00% and 6.12% at 40px, for a headline 59%
     larger. Only the 12px locator above them is worse, at 9.28%. */
  .beat-b .d-xl { font-size: var(--size-xl); }

  /* The climax is the one block on the phone that measures better SMALLER, and
     by a wide margin. At the Display XL floor it is 46px, "STILL RUNNING." is
     8.41em in this face so it breaks onto two lines inside a 327px rail, and the
     accent word lands on 75.08% of its area failing 3:1 wherever it is put. At
     38px the line is 319px, so it holds on ONE line, and the top of the portrait
     crop through beat D's window is dark: the accent word measures 9.68%. That
     is the best this word has measured on a phone across every footage version,
     against 76.21% on the build that shipped. The --text line is 0.22%. */
  /* the phone keeps ONE line for STILL RUNNING., which is what took the accent
     word from 75% to under 10%, and the halo buys the notch back inside that
     constraint: 44px is the largest size the line holds at inside a 327px rail. */
  .beat-d { --by: 4.9%; --bb: auto; }
  .beat-d .d-xl { font-size: min(44px, calc((100vw - var(--margin) * 2) / 8.55)); }

  .beat-e { --bb: 12.3%; flex-direction: column; align-items: stretch; }
  .beat-e .pill { width: 100%; }
}

/* mobile collapses to beats B, D, E (scrub mode only) */
@media (max-width: 767px) {
  .js:not(.reduced) .beat-a,
  .js:not(.reduced) .beat-c { display: none; }
}

/* ==========================================================================
   9. Bands
   ========================================================================== */

/* ---------------------------------------------------------------------------
   THE PAINTED FLOOR.  (critical fix, this pass)

   THE DEFECT. Scrolled below the hero, a strip of the raw hero frame painted
   across the full width of the page, behind the top of The Collection band:
   the band's headline, body and plate sat on top of live canvas. Reproduced at
   1280x800 and captured: a 40px band of the Sherman's track run at scroll
   6800, between The Claim and The Collection.

   THE CAUSE, exactly. The hero pins a 100svh stage inside a 900vh runway and
   The Claim rides up over it as the cover. That cover only works while the
   cover is at least as tall as the stage it is covering, and it no longer is:
   in the current type system .claim measures 760px against an 800px viewport.
   So the moment the pin releases, the last 40px of the still-painting stage
   sits BELOW the cover's bottom edge, inside the hero's own box, for the whole
   remaining 800px of that box. Every band under the cover was transparent
   (.collection carried position: relative for paint order but NO background),
   and .hero is a z-index: 0 stacking context, so the canvas painted straight
   through them. The vacated-ground ::after does not help: it is zero tall once
   the cover has landed, which is exactly when the strip appears.

   THE FIX, and why it is this one rather than a taller cover. Forcing the two
   covering bands to 100svh would put dead ground back into two bands this same
   pass is tightening. Instead every band on the page now paints its own ground
   and sits above the pinned stages, so no pinned frame can ever show through
   page content at any offset, whatever the cover's height does at any future
   viewport or type change. The runways themselves stay at z-index 0, which is
   what keeps their canvases visible inside their own boxes.
   --------------------------------------------------------------------------- */
.claim,
.collection,
.lineage,
.fleet,
.p1918,
.duffy,
.run,
.events,
.aquino,
.visit,
.join,
.strip,
.footer {
  position: relative;
  z-index: 1;
  background: var(--ground);
}

/* the two pinned runways stay under the floor; their own stages paint inside
   their own boxes and nowhere else */
.hero,
.explode { z-index: 0; }

/* -- reveal primitive ----------------------------------------------------- */
.js .reveal > * {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
  transition-delay: calc(var(--i, 0) * 0.08s);
}

.js .reveal.is-in > * { opacity: 1; transform: none; }

/* line reveals animate the inner span only, so the mask box never shifts */
.js .reveal-line > *,
.js .reveal > .reveal-line { opacity: 1; transform: none; }

.js .reveal-line .mask > span {
  transform: translateY(105%);
  transition: transform var(--dur) var(--ease);
  transition-delay: calc(var(--i, 0) * 0.08s);
}

.js .reveal-line.is-in .mask > span { transform: none; }

/* -- Band 1: The Claim ---------------------------------------------------- */

/* The statement band. It kept the direction's 200px, and with nothing in it but
   three display lines and a lede that read as a black field with type at the
   top of it. It now carries the page's one ambient loop and the geometry is
   tightened to its content: the padding comes to the 160px band step, the
   headline and the lede are the whole band, and there is no minimum height, so
   the band is exactly as tall as what is in it. */
.claim { padding-block: var(--pad-band); }

/* The ambient hall loop. Its own clipping box rather than overflow on the band,
   because .claim::after paints the ground the band vacates during the hero exit
   and lives OUTSIDE the band's box: clipping the band would clip that fill and
   put the sandwich back. */
.claim__film {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
  background: var(--ground);
}

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

/* the still is the reduced-motion and no-JS frame; the loop paints on top of it
   and is the same footage, so there is nothing to swap and no flash */
.claim__video { z-index: 1; }

:is(.no-js, .reduced) .claim__video { display: none; }

.claim > .wrap { position: relative; z-index: 1; }

/* Smooth exit: the next band rides up over the still-pinned canvas across the
   runway's tail, so the pin is covered rather than released. Scoped to the
   scrubbing path, because the static and no-JS heroes are in normal flow.

   The layout overlap stays one viewport, which is the distance this band has to
   travel from the bottom edge to the top edge. The tail it travels over is now
   two, so layout alone would leave the first screen of it dead and then ride the
   band up at full speed through the second. The engine authors the rendered
   position instead and carries the difference here, on --cover: the band leaves
   the bottom edge at rest, eases in, and lands flush with no gap at either end.
   --cover is only ever negative, it is zero at the landing and stays zero, and
   nothing but the engine writes it, so the reduced-motion and no-JS heroes are
   untouched. */
.js:not(.reduced) .claim,
.js:not(.reduced) .p1918__after {
  position: relative;
  z-index: 1;
  background: var(--ground);
  margin-top: -100vh;
  margin-top: -100svh;
  transform: translate3d(0, var(--cover, 0px), 0);
}

/* The ground the band vacates while it is held back. It is exactly -1 * --cover
   tall, so it starts at the band's own bottom edge and ends on the pixel the
   next band starts: it can never overlap that band and can never leave a strip
   of live canvas between the two. Zero tall at rest, so it does not exist on any
   other pass of the page. */
.js:not(.reduced) .claim::after,
.js:not(.reduced) .p1918__after::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: calc(var(--cover, 0px) * -1);
  background: var(--ground);
  pointer-events: none;
}

/* The band under each covering band paints above the pinned stage rather than
   under it, so the moment the cover has landed the screen carries page content
   from edge to edge. .duffy already had this; .collection is the hero's. */
.js:not(.reduced) .collection { position: relative; }

.claim__head { --cap: var(--size-l); --em: 17.95; }

.claim__lede {
  margin-top: 64px;
  margin-left: 0;
  max-width: 46ch;
}

/* the lede starts on column 4 of the 12-column grid, not at a round 25%, so it
   hangs off the same rail the rest of the page is built on */
@media (min-width: 1024px) {
  .claim__lede {
    margin-left: calc((100% - 11 * var(--gutter)) / 12 * 3 + 3 * var(--gutter));
  }
}

/* -- Band 2: The Collection ---------------------------------------------- */

/* 7/5 on the page grid. The plate still bleeds off the left page edge, but the
   band itself is now the same 12-column box every other band is, so the body
   column starts on column 8, which is the line the See Them Run panel, the
   Duffy caption and the Join body all start on. */
.collection {
  width: 100%;
  max-width: calc(var(--maxw) + var(--margin) * 2);
  margin-inline: auto;
  padding-inline: var(--margin);
  padding-block: var(--pad-band);
  /* THE BAND'S HEIGHT IS ITS CONTENT'S HEIGHT. The plate used to carry a fixed
     4:5 aspect, which made it 900px tall beside a 730px copy column and left a
     black field about 200px deep down the right half of the band, growing to
     over 400px at 1920 where the copy column is narrower still. The media box
     now stretches to the row instead of setting it, so the two columns end on
     the same line at every width and neither side can leave a vacant field. */
  align-items: stretch;
}

.collection__media {
  grid-column: 1 / span 7;
  min-width: 0;
  margin-left: calc(var(--rail) * -1);
  display: flex;
}

/* a plate whose media box is driven by the row rather than by an aspect ratio.
   The master is 3:2 so that a box running from about 1.0 to 1.8 in aspect
   cover-crops out of it without losing the case. */
.plate--fill {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.plate--fill picture {
  flex: 1;
  min-height: 420px;
}

.plate--fill img { object-position: 62% 50%; }

.collection__body {
  grid-column: 8 / span 5;
  min-width: 0;
  display: grid;
  align-content: start;
  gap: 40px;
}

.stat__figure {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(38px, 4.4vw, 68px);
  line-height: var(--display-lead);
  letter-spacing: var(--display-track);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* Stat labels are the one place a 12px tracked label runs long enough to wrap,
   so they take the same 1.5 leading the slot captions use rather than the
   single-line label's line-height of 1, where two lines collided. Balanced
   wrapping stops the last line falling to a one-word widow. */
.stat__label {
  margin-top: 12px;
  line-height: 1.5;
  text-wrap: balance;
}

@media (max-width: 899px) {
  .collection { grid-template-columns: minmax(0, 1fr); }
  .collection__media,
  .collection__body { grid-column: 1; }
  /* stacked, the plate has no facing column to take its height from, so it
     takes a portrait box of its own: on a phone the case reads at 4:5 and the
     3:2 master crops to it around the same anchor */
  .plate--fill picture { flex: none; aspect-ratio: 4 / 5; min-height: 0; }
}

/* -- Band 3: The Lineage -------------------------------------------------- */

/* The one band on the page carrying no media at all, sitting between two bands
   that now carry photographs. It takes --panel as its ground so the timeline
   reads as its own block instead of dissolving into the run of near-black
   above and below it. A full-width fill and a tonal step, which is the
   separation device the direction sanctions; no rule, no border, no box. */
.band--panel { background: var(--panel); }

/* ---------------------------------------------------------------------------
   THE LINEAGE, recomposed.

   The client's read, twice: "no idea what you are trying to achieve here. poor
   layout design, structure and spacing." The band was a four-column sequence
   with a 120px drop on the first column and a 3:2 photograph placeholder
   dropped into the middle of the third. Four year numerals landed on three
   different baselines, four copy blocks on four, and the placeholder pushed the
   band roughly 400px taller than anything in it, so the band ended in a field
   of empty panel. It read as broken rather than as stepped.

   It is now flat and disciplined. FOUR EQUAL COLUMNS on the page's own grid.
   EVERY YEAR NUMERAL ON ONE BASELINE at the top of the band, every copy block
   on one baseline under them, one measure across all four. No stagger, no
   placeholder inside the sequence, and no connecting line, no spine and no rule
   of any kind: the shared baseline is what makes it a timeline. The band's
   height is its content plus the 160px band step, so there is no dead field
   under it. The --panel tonal step stays.
   --------------------------------------------------------------------------- */
.lineage__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 64px;
  /* start, not stretch: with every numeral one line deep and one type size, the
     four numerals share a baseline and the four bodies share the next one */
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.lineage__grid .year { min-width: 0; }

/* the year numerals are this band's only focal element, so they take the full
   Display M token the direction assigns to year numerals rather than sitting a
   step under it */
.year__num {
  font-family: var(--display);
  font-weight: 800;
  font-size: var(--size-m);
  line-height: var(--display-lead);
  letter-spacing: var(--display-track);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* 24px under the numeral at every column, so the four copy blocks start on one
   line. text-wrap: pretty keeps the measure even rather than leaving one column
   with a one-word last line beside three that are full. */
.year__body {
  margin-top: 24px;
  font-size: 16px;
  line-height: 1.6;
  color: var(--text-mute);
  text-wrap: pretty;
}

/* 2x2 on a tablet and stacked on a phone, both with the same discipline: the
   numerals in a row share a baseline and the copy under them starts on one
   line, because align-items: start and the 24px step are the same rules. */
@media (max-width: 1023px) {
  .lineage__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 64px; }
}

@media (max-width: 639px) {
  .lineage__grid { grid-template-columns: minmax(0, 1fr); row-gap: 48px; }
}

/* -- Band 3.25: The Fleet ------------------------------------------------- */

/* ---------------------------------------------------------------------------
   THE FLEET, eight cards on the page's own 12 column grid.

   Four across on desktop (span 3), two on a tablet (span 6), two on a phone,
   one on the smallest phones where a two column card leaves an eleven character
   measure. The row gap is the grid's own 32px gutter, so the set reads as one
   block rather than as eight floating tiles.

   Every card is a FULL FOUR-SIDED STRUCTURE: a --panel fill, picture flush to
   its top and side edges, name and factual line padded beneath. No single sided
   border on any edge of any card, and no rule under any name; the fill is the
   structure. The cards carry NO hover or focus state, because they carry no
   destination: copy.md gives this band no CTA and no per-vehicle URL exists, so
   painting a hover response onto a block that cannot be clicked would be an
   affordance that lies. Recorded in BUILD-NOTES.

   The band sits between The Lineage on --panel and Project 1918 on --ground, so
   it takes --ground and its cards take --panel: the seam above it is a ground
   flip and the cards read against the band rather than dissolving into it.
   --------------------------------------------------------------------------- */
.fleet__head { --cap: var(--size-l); --em: 16.20; margin-bottom: 64px; }

.fleet__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--gutter);
  margin: 0;
  padding: 0;
  list-style: none;
}

.card {
  grid-column: span 3;
  min-width: 0;
  background: var(--panel);
  display: flex;
  flex-direction: column;
}

.card__media {
  position: relative;
  margin: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--ground);
}

.card__media picture { display: block; height: 100%; }

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The one card with no render behind it. Same box, same aspect, same card fill,
   and a FULL FOUR-SIDED hairline on the media box so the reserved slot reads as
   a frame rather than as a hole: at --ground inside a --panel card it is two
   units of luminance, which disappears next to seven lit photographs. Four
   sides, never one, per the ban list. */
.card__media--supply {
  display: flex;
  align-items: flex-end;
  padding: 24px;
  border: 1px solid var(--hairline);
}

.card__supply { max-width: none; }

.card__name {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(18px, 1.55vw, 22px);
  line-height: var(--display-lead);
  letter-spacing: var(--display-track);
  text-transform: uppercase;
  color: var(--text);
  padding: 24px 24px 0;
}

.card__line {
  margin-top: 12px;
  padding: 0 24px 24px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-mute);
  text-wrap: pretty;
}

.fleet__note { margin-top: 32px; max-width: none; }
.fleet__note .media-cap__note { max-width: 78ch; }

@media (max-width: 1023px) {
  .card { grid-column: span 6; }
}

@media (max-width: 639px) {
  .fleet__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 16px;
    row-gap: 16px;
  }
  .card { grid-column: span 1; }
  .card__name { padding: 16px 16px 0; }
  .card__line { margin-top: 8px; padding: 0 16px 16px; font-size: 14px; }
  .card__media--supply { padding: 16px; }
}

/* the smallest phones: two columns leaves an eleven character measure under the
   picture, so the set runs one card wide, the same width at which the Lineage
   sequence already collapses to one column */
@media (max-width: 359px) {
  .fleet__grid { grid-template-columns: minmax(0, 1fr); }
}

/* -- Band 3.5: Project 1918 ----------------------------------------------- */

.p1918 { padding-block: var(--pad-band) 0; }

/* 7/5, the page pair. The headline and the museum's own photograph of the
   vehicle hold columns 1 to 7; the body and the one CTA hold 8 to 12. The CTA
   moved out of the headline column to get here, which is the page's rule: a
   CTA sits at the end of the text column it belongs to, never in the facing
   one. Duffy, Join, Plan Your Visit and See Them Run all read the same way. */
.p1918__left { grid-column: 1 / span 7; display: grid; gap: 48px; }
.p1918__right { grid-column: 8 / span 5; display: grid; gap: 32px; justify-items: start; }

.p1918__head { --cap: var(--size-l); --em: 7.60; }

@media (min-width: 900px) {
  /* "PROJECT 1918" is 4.60em wide and the column is 7 of 12 (0.5741 of --content) */
  .p1918__head { --em: 13.40; }
}

.p1918__ctas { display: flex; flex-wrap: wrap; gap: 16px; }

@media (max-width: 899px) {
  .p1918__intro { grid-template-columns: minmax(0, 1fr); }
  .p1918__left, .p1918__right { grid-column: 1; }
  .p1918__left { gap: 32px; }
}

/* The pinned exploded view: second scrub instance, own runway. 700vh against
   the last pass's 600vh, and the whole of the extra 100vh is the second viewport
   of exit tail: span 600vh, tail 200vh, scrub 400vh over five beats, so the
   80vh segment and the 56vh hold are exactly what they were. */
.explode {
  position: relative;
  z-index: 0;
  height: 700vh;
  margin-top: var(--pad-band);
}

.explode__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--ground);
}

/* poster and canvas always overlay inside this box; only the box changes shape
   between the pinned desktop stage and the stacked mobile one */
.explode__media {
  position: absolute;
  inset: 0;
}

.explode__poster,
.explode__poster img,
.explode__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* the draw anchors the crop to the top, so the poster matches it */
.explode__poster img { object-fit: cover; object-position: 50% 0; }
.explode__canvas { display: block; }

.explode__beats { position: absolute; inset: 0; }

/* Callouts live upper left. The new assembly footage is a three-quarter front
   composition, so the field that was clean on the old side view is not clean
   here. Re-measured per pixel across all 105 frames, worst case across all
   five ranges: this is the block where every pixel clears 4.5:1. Hung into the
   left margin like the hero's data stack. */
.ex {
  position: absolute;
  /* on the rail, not at half a margin. This block sat 32px from the viewport
     edge while every other block on the page sat at 64px or on the centred
     content edge, which is exactly the kind of near-miss that reads as
     "text all over the place". Re-measured on the rail: see BUILD-NOTES.md. */
  left: var(--rail);
  top: 13.5%;
  width: min(470px, 42%);
  text-align: left;
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}

/* Opacity owns the fade at both ends. A visibility flip keyed off is-in clipped
   the first callout while it was held at full opacity on the pin's first pixel,
   and cut the last one off mid-fade across the exit tail. */
.ex.is-in { opacity: 1; }

.ex__part {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(28px, 3.4vw, 44px);
  line-height: var(--display-lead);
  letter-spacing: var(--display-track);
  text-transform: uppercase;
  color: var(--text);
}

/* full --text, not the muted tone: over the frames the muted tone drops to
   3.9:1 at best and 0.6:1 at worst, which is not a body colour over media.
   16px under the part name is the ladder step beat B uses under its own
   right-aligned display line; 20px was off the ladder. The 1.6 leading matches
   the captions, which set at the same size. */
.ex__line {
  margin-top: 16px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text);
}

/* This slot is no longer a placeholder: the museum supplied the actual 1940
   Oshawa photograph its caption describes, and a second photograph of the same
   vehicle as it stands today. The two run as the page's 7/5 pair, and the right
   plate's crop was chosen so that it is exactly as tall as the 3:2 plate beside
   it: the two images share a top edge AND a bottom edge, so the two captions
   under them start on one line. The 21:9 letterbox the empty box used is gone
   with it, because at 21:9 the crop takes the commander's cap off the top of
   the photograph and the lower hull off the bottom. The plate's shape moved to
   the photograph, not the other way round. */
.p1918__archive { padding-block: var(--pad-band); }

.p1918__archive > .plate:first-child { grid-column: 1 / span 7; }
.p1918__archive > .plate:last-child { grid-column: 8 / span 5; }

@media (max-width: 899px) {
  .p1918__archive { grid-template-columns: minmax(0, 1fr); }
  .p1918__archive > .plate:first-child,
  .p1918__archive > .plate:last-child { grid-column: 1; }
}

/* One caption voice across the page: 15px on 1.6 sets 24px lines, on the
   ladder, and leaves a readable step under the 17px body; at 16px the caption
   read as a second body paragraph rather than as a caption. */
.duffy__caption {
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-mute);
  max-width: 62ch;
}

@media (max-width: 767px) {
  .explode { height: 560vh; }

  /* on a phone the stage stacks: the frame keeps its own 16:9 box so no part
     of the exploded vehicle is ever cropped, and the callout sits beneath it.
     The stack is centred in the pinned stage: top-anchored it left roughly
     340px of dead ground under the callout, which read as a broken band rather
     than as air. Nothing was added to fill it, the geometry was rebalanced. */
  .explode__stage {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-block: calc(var(--nav-h) + 24px);
  }

  /* the media box takes its own 16:9 shape in flow; poster and canvas stay
     stacked on top of each other inside it, never side by side */
  .explode__media {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 16 / 9;
  }

  /* the callouts overlay each other, so the box reserves the tallest of the
     five (113px at 375px wide) plus headroom for the extra line the narrowest
     phones wrap to. 260px reserved more than twice what any callout uses. */
  .explode__beats {
    position: relative;
    inset: auto;
    min-height: 152px;
    margin-top: 40px;
  }

  .ex {
    left: var(--rail);
    right: var(--rail);
    top: 0;
    width: auto;
  }
}

/* -- Band 3.75: The Duffy Collection -------------------------------------- */

.duffy { position: relative; }

.duffy__bed {
  position: relative;
  min-height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: flex-start;
}

/* The bed's media layer. The -6% inset started life as parallax headroom; with
   the parallax gone it stays as the CROP, because it is the framing the
   headline was measured against. Flush at inset 0 the frame sits 12% wider and
   lower, which walks the gun barrel straight through the upper left where the
   headline sits: measured over the traverse, flush reads 3.97% failing on
   desktop and 8.06% on the phone against 0.35% and 0.00% at this inset. Both
   the still and the canvas take it, so they frame identically. */
.duffy__media {
  position: absolute;
  inset: -6% 0;
  z-index: 0;
}

.duffy__media picture,
.duffy__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.duffy__headwrap {
  position: relative;
  z-index: 1;
  padding-top: clamp(88px, 11vh, 150px);
}

/* headline sits in the measured dark upper left of the render */
.duffy__head {
  --cap: var(--size-l);
  --em: 18.20;         /* longest line 9.34em inside a 0.52 column */
  max-width: 52%;
}

/* The gun traverse paints over the approved still. The still stays underneath
   as the poster, the reduced-motion frame and the no-JS frame, and it is the
   same composition as the sequence's last frame, so the canvas can simply sit
   on top of it: no flash on handover, nothing to hide or swap.

   The bed's parallax was REMOVED with this feature rather than composed with
   it. Two scroll-driven motions on the same layer, a vertical drift and a
   horizontal traverse, read as busy against each other, and the traverse is the
   one the band is about. One authored moment per band. */
.duffy__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

/* reduced motion and no-JS never build the canvas, so the still carries the bed */
:is(.no-js, .reduced) .duffy__canvas,
.duffy--static .duffy__canvas { display: none; }

/* the stat row takes the page's 7/5 pair too, so the second figure starts on
   column 8, the same line the caption in the prose pair below it starts on */
.duffy__stats { padding-block: var(--pad-band) 64px; }
.duffy__stats > .stat:first-child { grid-column: 1 / span 7; }
.duffy__stats > .stat:last-child { grid-column: 8 / span 5; }

.duffy__prose {
  gap: 32px;
  justify-items: start;
  max-width: 68ch;
  padding-bottom: var(--pad-band);
  grid-template-columns: minmax(0, 1fr);
}

/* .duffy__caption is set in the Project 1918 block, alongside .media-cap, so
   every caption on the page carries one voice. */

/* the global lede measure is 34ch, which set this line at 390px beside a 590px
   body in the same left-hung column and read as an unfinished second measure.
   46ch is the measure The Claim's lede already uses. */
.duffy__prose .lede { max-width: 46ch; }

/* The prose sub-band was four stacked text blocks in one column with the right
   third empty. The image caption now hangs in the facing column, top aligned to
   the body, so the sub-band reads as a 7/5 pair under the bed instead of a
   single stack. Same copy, same order in the DOM. */
@media (min-width: 900px) {
  .duffy__prose {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    row-gap: 32px;
    max-width: none;          /* the 68ch cap belongs to the single-column stack */
    justify-items: stretch;
  }
  .duffy__prose > .body { grid-column: 1 / span 7; }
  .duffy__prose > .duffy__caption { grid-column: 8 / span 5; grid-row: 1; margin-top: 0; }
  .duffy__prose > .lede { grid-column: 1 / span 7; }
  .duffy__prose > :last-child { grid-column: 1 / span 7; }
}


@media (max-width: 767px) {
  .duffy__bed { min-height: 78vh; }
  .duffy__headwrap { padding-top: calc(var(--nav-h) + 48px); }

  /* the .fit cap is released below 768px, which broke this headline onto three
     lines with WHOLE. alone on the last. "DONATED WHOLE." is 6.21em, so the
     size is capped to the measure and the art-directed two-line break holds.
     Above 404px the token floor still wins and nothing changes. */
  .duffy__head {
    max-width: 100%;
    font-size: min(var(--size-l), calc((100vw - var(--margin) * 2) / 9.50));
  }

  .duffy__stats { grid-template-columns: minmax(0, 1fr); row-gap: 48px; }
  .duffy__stats > .stat:first-child,
  .duffy__stats > .stat:last-child { grid-column: 1; }
}

/* -- Band 4: See Them Run ------------------------------------------------- */

.run {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: flex-end;
  /* the bottom step is 48 rather than 96 because the headline is measured, not
     placed by eye: the new line is nine characters longer and runs further
     across the lit centre of the formation, and 48px lower it sits in the dark
     floor band. 3.69% failing at 96, 1.18% at 64, 0.44% at 48. */
  padding-block: 96px 48px;
  overflow: hidden;
}

.run__media { position: absolute; inset: 0; }

.run__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 64px;
  align-items: end;
  width: 100%;
}

/* the panel rides high and right over the media, the headline runs the full
   width beneath it, so the two never collide at any width */
/* The one 35 character line on the page, and the only headline the display
   swap changed the SHAPE of. In Archivo it measures 22.49em, so holding it to
   one line across the full content width sets it at 39px at 1024 and 58px at
   1440: not a Display XL headline by any reading, and it stepped down hard from
   81px at 900. It therefore sets at a real poster size and takes a balanced
   wrap instead, which gives the same three line block from 768 up. The
   art-directed break between the two SENTENCES still holds, because each
   sentence is its own masked line; what wraps is inside the first sentence. */
/* the eyebrow and the headline share one cell, so the 24px step between them is
   the same step every other eyebrow on the page takes rather than one of this
   grid's 64px rows. The band is bottom aligned, so the headline does not move:
   the eyebrow and the caption above it ride up by their own height. */
.run__stack {
  grid-column: 1 / span 12;
  grid-row: 3;
}

.run__head {
  text-wrap: balance;
  --cap: clamp(38px, 5.6vw, 84px);
}

/* the bed caption, in the band's own bottom stack rather than over the top of
   the frame. 48px on the ladder under it, then the eyebrow, then the headline:
   the step opened from 24px when the eyebrow landed, because two 12px tracked
   label lines 24px apart read as one block of four lines rather than as a
   caption and a section label. */
.run__cap {
  grid-column: 1 / span 8;
  grid-row: 2;
  margin: 0 0 -16px;
  max-width: none;
}

.run__cap .media-cap__note { color: var(--text); max-width: 78ch; }

.panel-card {
  grid-column: 8 / span 5;
  grid-row: 1;
  background: var(--bone);
  color: var(--ink);
  padding: 48px;
  display: grid;
  gap: 16px;
  justify-items: start;
  transform: translateY(48px);
}

.panel-card__title { color: var(--ink); }
.panel-card__date { font-size: 17px; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.panel-card__body { color: var(--text-mute-ink); max-width: 34ch; font-size: 16px; }
.panel-card .pill { margin-top: 16px; }

/* stacked, the band pads on the same ladder as every other band (128 / 120),
   and the grid's own 64px row gap is the only seam between head and panel */
@media (max-width: 1023px) {
  .run { display: block; padding-block: var(--pad-band); }
  .run__grid { grid-template-columns: minmax(0, 1fr); row-gap: 48px; }
  .run__stack, .panel-card, .run__cap { grid-column: 1; grid-row: auto; }
  .panel-card { transform: none; padding: 32px; }
  /* stacked, the bed is in flow and the caption is simply under the frame it
     names, which is the page's first caption position */
  .run__cap { margin: 0; order: -1; }
  .run__media { position: relative; inset: auto; height: 62vh; margin-bottom: 24px; }
}

/* -- Band 4.5: The 2026 season -------------------------------------------- */

/* ---------------------------------------------------------------------------
   THE SEASON, as a ruled table on the page's own table pattern.

   It sits between two full-bleed media bands and paints its own --ground, so it
   is the breath between them as well as the schedule. Three columns: the event
   name in the row header, the date and its own detail, and the action column,
   which is empty on the four rows that have happened and carries the one live
   ticket link on the row that has not. Structural cell rules only, tabular
   numerals inherited from .data-table, right aligned action column above 768px
   and the same labelled restack below it that every other table on the page
   takes. No red anywhere on this band.
   --------------------------------------------------------------------------- */
.events__head { --cap: var(--size-l); --em: 9.75; margin-bottom: 64px; }

/* the name column is short and the date column carries the detail, so they are
   sized rather than left to the browser's content-driven guess: at 1280 the
   longest date line then sets on one line instead of wrapping under itself */
.events__list thead th:first-child { width: 26%; }
.events__list thead th:nth-child(2) { width: 58%; }

.events__list tbody th { font-size: 17px; }

@media (min-width: 768px) {
  .events__list thead th:last-child,
  .events__list tbody td:last-child { text-align: right; }
  .events__list tbody td:last-child { white-space: nowrap; }
  /* the pill sets its own 44px box, so the cell keeps the row rhythm rather
     than the 20px text padding stacking on top of it */
  .events__list tbody td:last-child { padding-top: 12px; padding-bottom: 12px; }
}

/* restacked, an empty action cell would print its own column label with nothing
   under it, so the four ended rows simply do not draw one */
@media (max-width: 767px) {
  .events__list tbody td:empty { display: none; }
}

/* -- Band 5: Aquino ------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   AQUINO, recomposed.

   The band was 88vh of empty labelled bed with a centred stack on it: the
   largest black field on the page. It now carries the page's FIFTH scrub, the
   show line forming up on the south field, and the climax composes over it.

   The bed's height comes down from 88vh to a content-driven box with a floor,
   so the band is as tall as the picture and the climax need and no taller, and
   the centred climax keeps the direction's second centred moment.
   --------------------------------------------------------------------------- */
.aquino { position: relative; }

.aquino__bed {
  position: relative;
  height: 72vh;
  min-height: 460px;
  overflow: hidden;
  background: var(--ground-deep);
}

.aquino__media { position: absolute; inset: 0; z-index: 0; }

/* The climax, off the picture and on the deepest ground. It stays the page's
   second centred moment, which is what the direction names; what moved is the
   surface it is read against, and the reason is measured (see the markup). */
.aquino__climax {
  background: var(--ground-deep);
  /* the climax belongs to the picture above it, so the air over it is the band
     step and the air under it is carried by the ticket zone's own top step.
     Padded on both sides it left a 200px black field under the lede that read
     as the void this pass is removing everywhere else. */
  padding: var(--pad-band) var(--margin) 0;
  display: grid;
  place-items: center;
}

.aquino__stack {
  position: relative;
  z-index: 1;
  text-align: center;
  display: grid;
  gap: 24px;
  justify-items: center;
  width: 100%;
  max-width: var(--maxw);
}

/* the centred stack's own gap is already the eyebrow's 24px step, so the
   eyebrow drops its margin here rather than setting 48px on this one band */
.aquino__stack .eyebrow { margin-bottom: 0; }

.aquino__head { --cap: var(--size-xl); --em: 12.80; }

.aquino__date { font-variant-numeric: tabular-nums; }
.aquino__lede { max-width: 30ch; }

/* the naming note reads as the band's context rather than as a footnote, so in
   the ticket pair's facing column it sets at the lede size and carries the
   column. It is the same string, moved, at a size the band already owns. */
.aquino__note {
  color: var(--text-mute);
  font-size: var(--size-lede);
  line-height: 1.45;
  max-width: 34ch;
}

/* THE TICKET ZONE. The client's read: "this is pretty bad. no context or copy
   or anything." It was a table on columns 1 to 7 with a single Label-sized
   partner line alone on 8 to 12 and nothing else in that column: an orphan in a
   vacant field. It is now a real 7/5 pair. The table keeps 1 to 7 with its own
   footnote under it, where a footnote belongs. The facing column carries the
   band's context: the naming note, which used to sit at the bottom of the
   centred climax and reads better as context than as part of a poster stack,
   and the partner line under it, both on the column's own rail. No copy was
   rewritten and none was invented; one paragraph moved. */
.aquino__tickets {
  grid-column: 1 / span 7;
  background: var(--panel);
  border: 1px solid var(--hairline);
  padding: 48px;
  min-width: 0;
}

/* Context at the head of the column, the partner credit at its foot, on the
   table's own bottom line. Two blocks holding the column between them reads as
   a composed facing page; the same two blocks clumped at the top read as an
   orphan over a hole, which is what the client saw. */
.aquino__context {
  grid-column: 8 / span 5;
  min-width: 0;
  align-self: stretch;
  display: grid;
  align-content: space-between;
  gap: 48px;
  justify-items: start;
}

@media (max-width: 899px) {
  .aquino__context { align-content: start; gap: 32px; }
}

/* the ticket table belongs to the climax above it: the seam inside the band is
   one step tighter than the seam between bands */
.aquino > .band { padding-block: 128px var(--pad-band); }

.aquino__foot { margin-top: 24px; font-size: 14px; color: var(--text-mute); }

/* prices align to the panel's right edge instead of ragging out mid-column, and
   a price holds on one line: right aligned, "$40 to $110" was breaking after
   "to" and leaving a stray figure on its own line. Same string. */
@media (min-width: 768px) {
  .aquino .data-table thead th:last-child,
  .aquino .data-table tbody td:last-child { text-align: right; }
  .aquino .data-table tbody td { white-space: nowrap; }
}

@media (max-width: 899px) {
  .aquino__after { grid-template-columns: minmax(0, 1fr); }
  .aquino__tickets,
  .aquino__context { grid-column: 1; }
}

@media (max-width: 767px) {
  .aquino__tickets { padding: 24px; }
  .aquino__bed { height: 56vh; }
}

/* -- Tables --------------------------------------------------------------- */

/* min-width:0 is load-bearing: without it a grid item keeps its automatic
   minimum size, the scroller never clips, and the whole page pans sideways */
.table-scroll {
  overflow-x: auto;
  min-width: 0;
  -webkit-overflow-scrolling: touch;
}

.data-table {
  width: 100%;
  min-width: 420px;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  text-align: left;
}

.data-table th,
.data-table td {
  padding: 20px 24px 20px 0;
  vertical-align: top;
  font-size: 16px;
  line-height: 1.5;
  border-bottom: 1px solid var(--hairline);
}

.data-table thead th {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-mute);
  padding-bottom: 16px;
}

.data-table tbody th { font-weight: 500; color: var(--text); }
.data-table tbody td { color: var(--text-mute); }
.data-table tbody tr:last-child th,
.data-table tbody tr:last-child td { border-bottom: 0; }

.data-table--ink th,
.data-table--ink td { border-bottom-color: var(--hairline-ink); }
.data-table--ink thead th { color: var(--text-mute-ink); }
.data-table--ink tbody th { color: var(--ink); }
.data-table--ink tbody td { color: var(--text-mute-ink); }

.data-table td:last-child,
.data-table th:last-child { padding-right: 0; }

/* Under 768px the tables restack: every row becomes a labelled block, so no
   column is ever pushed out of the viewport and nothing scrolls sideways.
   The row rule stays, which is the structural line direction.md permits. */
@media (max-width: 767px) {
  .data-table { min-width: 0; }

  .data-table,
  .data-table tbody,
  .data-table tr,
  .data-table tbody th,
  .data-table tbody td { display: block; width: 100%; }

  .data-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .data-table tbody tr {
    padding-block: 20px;
    border-bottom: 1px solid var(--hairline);
  }

  .data-table--ink tbody tr { border-bottom-color: var(--hairline-ink); }

  .data-table tbody tr:last-child { border-bottom: 0; padding-bottom: 0; }

  .data-table tbody th,
  .data-table tbody td {
    border-bottom: 0;
    padding: 0;
  }

  .data-table tbody th { font-size: 17px; }
  .data-table tbody td { margin-top: 12px; }

  .data-table tbody td::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 4px;
    font-size: 12px;
    line-height: 1;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--text-mute);
  }

  .data-table--ink tbody td::before { color: var(--text-mute-ink); }
}

/* -- Band 6: Plan Your Visit ---------------------------------------------- */

.visit {
  background: var(--bone);
  color: var(--ink);
  padding-block: var(--pad-band);
}

/* the 5/7 mirror, and the only band on the page that takes it: the direction
   specifies Plan Your Visit that way. Same 12 columns, same 32px gutter, so the
   left column still starts on the rail and the hours table still gets the seven
   columns it needs to set three columns of data without a scroller. */
.visit__grid { row-gap: 64px; }

.visit__left { grid-column: 1 / span 5; display: grid; gap: 32px; align-content: start; }
.visit__right { grid-column: 6 / span 7; display: grid; gap: 48px; align-content: start; }
.visit__left > *,
.visit__right > * { min-width: 0; }

.visit h2 { color: var(--ink); }

.visit__block h3 {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-mute-ink);
  margin-bottom: 12px;
}

.visit__block p { font-size: 16px; line-height: 1.6; color: var(--text-mute-ink); max-width: 46ch; }
.visit__block p.visit__address {
  font-size: 17px;
  font-weight: 500;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.visit__media { margin-top: 16px; }

/* the entrance photograph hangs into the left gutter, per the band map. It is
   pulled by exactly the rail, so its left edge lands on the viewport edge the
   way the Collection plate does, rather than at a fixed margin that stops
   short of the page edge above 1568px. */
@media (min-width: 1024px) {
  .visit__media { margin-left: calc(var(--rail) * -1); }
}

.visit__note { font-size: 14px; color: var(--text-mute-ink); margin-top: 16px; }

.audio-box {
  border: 1px solid var(--hairline-ink);
  padding: 32px;
  display: grid;
  gap: 12px;
}

.audio-box h3 {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-mute-ink);
}

.audio-box p { font-size: 16px; line-height: 1.6; color: var(--ink); max-width: 52ch; }

.visit__ctas { display: flex; flex-wrap: wrap; gap: 16px; }

@media (max-width: 899px) {
  .visit__grid { grid-template-columns: minmax(0, 1fr); }
  .visit__left, .visit__right { grid-column: 1; }
}

/* -- Band 7: Join and Support --------------------------------------------- */

/* The band's shape moved with its footage, and the measurement is the reason.
   The M1917 plate was a dark studio frame with an empty left third, which is
   where the copy sat. The Leopard drive is the page's one daylight moment: the
   top 45% of every frame is sky and the trail under it is sunlit gravel and
   dust. Measured over the drive by the sampler, the best position anywhere in
   the frame reads 68.79% of a single headline line failing 3:1, 77.83% for the
   pair and 74.32% for the body, at every size and every integer position. There
   is no field on this bed that carries bone copy, so the bed keeps the full band
   width and the copy moves under it onto --ground, which is the arrangement this
   band already used under 1024px. */
.join {
  position: relative;
  padding-block: var(--pad-band);
  overflow: hidden;
}

.join__media { position: relative; margin-bottom: 96px; }

.join__media > .bed {
  position: relative;
  inset: auto;
  height: 72vh;
}

.join__content {
  position: relative;
  grid-template-columns: minmax(0, 1fr);
  row-gap: 32px;
  justify-items: start;
}

/* the fit cap outside the pair's range too: between 768 and 1023 the headline
   is a full width stack, and with no --em it fell back to the Display L token
   and broke both lines in half. Declared BEFORE the pair's media query, so the
   pair's own narrower cap wins on source order inside its range. */
.join__head { --cap: var(--size-l); --em: 13.35; }

/* Under the bed the copy is the page's 7/5 pair: the headline on columns 1 to
   7, the body and its two CTAs on 8 to 12, top aligned to it. It used to be an
   fr pair with a fluid gap, which put its second column on a line no other band
   used; on the real columns the longest line (8.56em) against a seven column
   track gives a cap of 15.20. The track is a smaller fraction of --content at
   1024 (0.5684) than at 1440 (0.5741), because the gutter is fixed and the
   columns are not, so the cap is derived at the narrow end of the pair's range
   and the headline still grew.
   Checked as rendered at 1024, 1100, 1280, 1440 and 1920: two lines at every
   one of them, the art-directed break intact. */
@media (min-width: 1024px) {
  .join__content {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
    row-gap: 32px;
    align-items: start;
  }

  /* the eyebrow rides in the headline's own cell, so the pair's placement moves
     to the stack and the headline keeps only its fit cap */
  .join__stack { grid-column: 1 / span 7; grid-row: 1 / span 2; }
  .join__head { --cap: var(--size-l); --em: 23.50; }
  .join__content .body { grid-column: 8 / span 5; grid-row: 1; max-width: 42ch; }
  .join__ctas { grid-column: 8 / span 5; grid-row: 2; }
}

/* the fit cap belongs outside the pair's media query too: between 768 and 1023
   the headline is a full width stack, and with no --cap it fell back to the
   Display XL token (117px at 900) and broke both lines in half. Capped at
   Display L it sets one line per mask at every width in that range. */
.join__ctas { display: flex; flex-wrap: wrap; gap: 16px; }

@media (max-width: 1023px) {
  .join__media { margin-bottom: 48px; }
  .join__media > .bed { height: 56vh; }
}

/* The still has to match the crop the canvas draws at the END of the drive, so
   the handover on that last frame is invisible. On desktop the bed leaves the
   cover fit no horizontal slack, so both are the plain centre crop. On a phone
   the window is 0.46 of the frame and the draw centres it on the focal point,
   which is 0.33 at frame 097: that resolves to 18% of the available offset. */
@media (max-width: 767px) {
  .join__media .bed img { object-position: 18% 50%; }
}

/* -- Band 7.5: More from the museum --------------------------------------- */

/* ---------------------------------------------------------------------------
   THE MEDIA STRIP. Three plain items, four columns each on the page's own grid,
   text forward: no cards, no boxes, no fills, no icons. Whitespace and the grid
   carry it, which is the separation device the direction sanctions and the
   reason this band can sit on the same --ground as the one above it.

   ONE of the three carries a link, because one of the three has a URL that is
   verified in research/facts.md. The other two state what the thing is and stop
   there; a link label pointing at a flag is worse than no link, which is the
   call Band 3.5 already makes. The link takes the footer's own treatment,
   muted with an underline, brightening to --text on hover and focus, never red.
   --------------------------------------------------------------------------- */
.strip__head { --cap: var(--size-l); --em: 17.05; margin-bottom: 64px; }

.strip__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 64px;
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.strip__item { grid-column: span 4; min-width: 0; }

.strip__name {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(22px, 2.2vw, 30px);
  line-height: var(--display-lead);
  letter-spacing: var(--display-track);
  text-transform: uppercase;
  color: var(--text);
  text-wrap: balance;
}

.strip__line {
  margin-top: 20px;
  font-size: 16px;
  line-height: 1.6;
  color: var(--text-mute);
  max-width: 42ch;
}

.strip__act { margin-top: 24px; }

.strip__link {
  font-size: 16px;
  font-weight: 500;
  color: var(--text-mute);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color 0.3s var(--ease);
}

.strip__link:hover,
.strip__link:active { color: var(--text); }

/* three four-column items set a 220px measure at the top of the tablet range,
   which is narrower than the name needs, so the strip stacks there rather than
   holding a row it cannot carry */
@media (max-width: 899px) {
  .strip__grid { grid-template-columns: minmax(0, 1fr); row-gap: 48px; }
  .strip__item { grid-column: 1; }
}

/* -- Footer --------------------------------------------------------------- */

.footer {
  background: var(--ground-deep);
  padding-block: 128px 96px;
}

.footer__mark {
  font-family: var(--display);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: var(--display-track);
  line-height: var(--display-lead);
  /* stamped like a hull number: always exactly one line, always full width */
  font-size: min(128px, calc(var(--content) / 16.10));
  color: var(--text);
}

.footer__cols {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gutter);
  margin-top: 128px;
}

.footer__col h2 { margin-bottom: 24px; }

.footer__col li + li { margin-top: 12px; }

.footer__link {
  font-size: 16px;
  color: var(--text-mute);
  text-decoration: none;
  transition: color 0.3s var(--ease);
}

.footer__link:hover { color: var(--text); text-decoration: underline; text-underline-offset: 4px; }

.footer__meta {
  margin-top: 96px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px;
}

.footer__meta p { font-size: 15px; color: var(--text-mute); max-width: 52ch; }

/* the badge sits on the rail at the head of the meta block, one grid cell wide,
   at the same restrained height the header gives it */
.footer__badge { grid-column: 1 / -1; margin-bottom: 32px; }
.footer__badge picture { display: block; }
.footer__badge img { display: block; height: 40px; width: auto; }

.footer__meta a {
  color: var(--text-mute);
  text-underline-offset: 4px;
  transition: color 0.3s var(--ease);
}

.footer__meta a:hover,
.footer__meta a:active { color: var(--text); }

@media (max-width: 899px) {
  .footer__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; }
  .footer__meta { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   10. Media
   ========================================================================== */

/* THE EMPTY LABELLED BOX IS GONE FROM THE PAGE. Every designed media slot now
   carries something real: the museum's own photographs in Project 1918, graded
   demo renders in The Collection and See Them Run, four scrubbing sequences,
   and a live map on Plan Your Visit. The .slot component and its three aspect
   variants went with the last of them, rather than being left as dead rules.
   The three photographs the museum still owes are recorded in
   CLIENT-QUESTIONS.md and named in the captions of the blocks standing in for
   them, which is where a demo should carry them. */

/* ---------------------------------------------------------------------------
   PLATES, BEDS AND CAPTIONS. One component, two positions, page-wide.

   Three of the seven designed slots now carry THE MUSEUM'S OWN PHOTOGRAPHS,
   supplied by the client and graded to the page's plate curve: the 1940 Oshawa
   demonstration (the exact photograph its caption describes), the M1917 before
   restoration, and the M1917 in the hall. Three more carry demo imagery whose
   caption still names the photograph the museum owes. Three stay labelled
   placeholders because nothing honest stands in for them: the MVCC exterior,
   the Aquino re-enactment and the museum entrance.

   THE CAPTION RULE. A caption sits UNDER the media it names, left aligned to
   that media's own left edge, wherever the media is in flow: the Collection
   plate, all three Project 1918 photographs, the Join bed. It sits at the TOP
   LEFT OF THE RAIL only where the media is a full-bleed bed whose lower half
   carries the band's copy, because under a full-bleed bed there is no "under":
   See Them Run and the Aquino placeholder. Those are the only two positions on
   the page, and both start on the rail.

   A real photograph carries a sentence-case caption line and a credit line
   beneath it. A demo plate carries the label naming the photograph still to
   come. Same block, same geometry, same rhythm, either way.
   --------------------------------------------------------------------------- */

.plate { display: block; margin: 0; }

/* the aspect ratio lives on the MEDIA BOX, not on the figure. With it on the
   figure, and the figure clipping its overflow, the caption fell outside the
   ratio box and was clipped away: the Collection plate shipped with no visible
   caption at all. */
.plate picture {
  position: relative;
  display: block;
  width: 100%;
  overflow: hidden;
  background: var(--panel);
}

.plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.plate--4x5 picture { aspect-ratio: 4 / 5; max-height: 84vh; }
.plate--3x2 picture { aspect-ratio: 3 / 2; }

/* the pre-restoration plate: the crop was chosen so that on five columns it is
   exactly as tall as the 3:2 plate on the seven beside it */
.plate--pair picture { aspect-ratio: 1.055; }

.media-cap { display: block; margin-top: 16px; max-width: 46ch; }

.media-cap__text {
  display: block;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-mute);
}

.media-cap__note {
  display: block;
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--text-mute);
  max-width: 34ch;
}

/* a credit under a caption is one short line, so it takes the caption's measure
   rather than the 34ch a standing placeholder label wants: at 34ch the credit
   broke after "THE CANADIAN" in a 752px column, which is a wrap nothing in the
   layout asked for. It still wraps inside the rail on a phone. */
.media-cap__text + .media-cap__note { margin-top: 8px; max-width: 46ch; }

/* the one plate that bleeds off the left page edge pulls its caption back to
   the rail, so the caption still starts on the page's own vertical line. It
   also takes the plate's own measure rather than the standing 34ch label
   measure: under a 723px plate a 34ch note set five lines deep and read as a
   paragraph rather than as a caption. */
.media-cap--rail { padding-left: var(--rail); }
.media-cap--rail .media-cap__note { max-width: 64ch; }

/* a filled full-bleed bed: the photograph is the band's ground. The caption
   keeps the same corner the empty bed used, the one the band's copy does not */
.bed {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--ground-deep);
}

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

/* The Join bed's drive: fourth scrub, second unpinned one. The canvas sits on
   the still, both cover fitted to the same box, and the still is the last frame
   of the sequence, so the handover at the end of the drive is invisible. */
.bed__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

/* reduced motion and no-JS never build the canvas, so the still carries the bed */
:is(.no-js, .reduced) .bed__canvas,
.join--static .bed__canvas,
.aquino--static .bed__canvas { display: none; }

/* THE SECOND CAPTION POSITION, re-derived this pass.

   It used to be the TOP left of the rail, which was right while every bed on the
   page was a dark studio or hangar frame. Three of the four beds are now golden
   hour, client-directed and client-approved, and on a golden-hour frame the top
   of the rail is open sky: measured on the Aquino show line the caption's own
   rect runs 150 to 205 per channel, where bone type reads under 1.6:1. Their
   dark field is the bottom: grass, shadow and dirt.

   So the caption takes the BOTTOM left of the rail on a bed whose own copy is
   not there, and the band's own bottom stack where it is (See Them Run, whose
   headline owns the floor: its caption sits on the rail directly above the
   headline, in the same measured dark band). Both are still one component, both
   still start on the rail, and a caption is still never anywhere but on the
   media it names. Over a photograph the note takes full --text, which is the
   same call the hero beats make; --text-mute stays the body colour on flat
   ground. */
.media-cap--foot {
  position: absolute;
  /* over a scrubbing bed the caption has to clear the canvas, which paints at 1 */
  z-index: 2;
  margin-top: 0;
  bottom: 40px;
  left: var(--rail);
  right: var(--rail);
  max-width: none;
}

.media-cap--foot .media-cap__note { color: var(--text); max-width: 78ch; }

/* the Join bed is in flow, so its caption takes the page's first position:
   under the frame it names, on that frame's own left edge, which is the rail */
.join .media-cap { padding-inline: var(--rail); max-width: none; }
.join .media-cap__note { max-width: 90ch; }


@media (max-width: 767px) {
  .media-cap--foot { bottom: 24px; }
}

/* ---------------------------------------------------------------------------
   PLAN YOUR VISIT: the map.

   This block reserved a box for a photograph of the entrance. On a band whose
   whole job is getting a visitor to the door, the live map of the approach is
   what the block was for, so the placeholder is replaced by one. The keyless
   share-embed form is used, so nothing has to be provisioned for the demo, and
   the page's ONE external request lives here. The bone fill behind the frame is
   the fallback: blocked, offline or still loading, the block reads as a surface
   and never as a void, with the address and the directions directly above it.
   Recorded in CLIENT-QUESTIONS.md: production should confirm embed against a
   keyed map before this ships for real.
   --------------------------------------------------------------------------- */
/* 3:2, not the 1:1 the photograph placeholder used and not the 4:5 a portrait
   plate would take. Measured in place: at 4:5 the left column runs 1144px
   against 681px on the right, which is the same column imbalance this pass is
   removing everywhere else; at 3:2 the two columns land within one step of the
   spacing ladder of each other. A landscape frame is also the right shape for
   what the map has to show, the 401 and the airport south field together. */
.map {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  /* the ceiling only bites above about 1440, where the map is 821px wide and a
     free 3:2 would set it 547px tall and run the left column 363px past the
     right. Capped, the two columns land 176px apart at 1920, which is the same
     relationship they hold at 1280 and 1024. A map is happy in a wide strip. */
  max-height: 360px;
  background: var(--hairline-ink);
  overflow: hidden;
}

.map iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* ==========================================================================
   11. Reduced motion + no-JS fallbacks
   ========================================================================== */

/* The static hero: same DOM, unpinned. The frame becomes a banner and the five
   beats collapse into one left-aligned block beneath it. */

:is(.no-js .hero, .hero--static) { height: auto; }

:is(.no-js .hero, .hero--static) .hero__stage {
  position: static;
  height: auto;
  overflow: visible;
}

:is(.no-js .hero, .hero--static) .hero__poster {
  position: relative;
  inset: auto;
  display: block;
  height: 62vh;
  min-height: 340px;
}

:is(.no-js .hero, .hero--static) .hero__poster img {
  position: static;
  height: 100%;
}

:is(.no-js .hero, .hero--static) .hero__canvas { display: none; }

:is(.no-js .hero, .hero--static) .hero__beats {
  position: static;
  padding: 96px var(--margin);
  max-width: calc(var(--maxw) + var(--margin) * 2);
  margin-inline: auto;
  display: grid;
  gap: 64px;
  justify-items: start;
}

:is(.no-js .hero, .hero--static) .beat {
  position: static;
  opacity: 1;
  visibility: visible;
  transform: none;
  max-width: 100%;
  text-align: left;
}

:is(.no-js .hero, .hero--static) .beat-a { display: grid; }
:is(.no-js .hero, .hero--static) .mask > span { transform: none; }

/* The static exploded view: same DOM, unpinned. The assembled M1917 becomes a
   banner and the five callouts become a plain stacked list beneath it. */

:is(.no-js .explode, .explode--static) { height: auto; margin-top: var(--pad-band); }

:is(.no-js .explode, .explode--static) .explode__stage {
  position: static;
  height: auto;
  overflow: visible;
  display: block;
  padding-block: 0;
}

:is(.no-js .explode, .explode--static) .explode__media {
  position: relative;
  inset: auto;
  width: 100%;
  aspect-ratio: auto;
  height: 56vh;
  min-height: 300px;
}

:is(.no-js .explode, .explode--static) .explode__canvas { display: none; }

:is(.no-js .explode, .explode--static) .explode__beats {
  position: static;
  inset: auto;
  min-height: 0;
  margin-top: 0;
  padding: 96px var(--margin);
  max-width: calc(var(--maxw) + var(--margin) * 2);
  margin-inline: auto;
  display: grid;
  gap: 48px;
}

:is(.no-js .explode, .explode--static) .ex {
  position: static;
  opacity: 1;
  visibility: visible;
  width: auto;
  max-width: 62ch;
  text-align: left;
}

.no-js .menu { display: none; }
.no-js .nav-toggle { display: none; }

@media (max-width: 899px) {
  .no-js .nav { display: flex; flex-wrap: wrap; gap: 16px; }
}

/* smallest phones: hero headline floor drops with the rest of the display scale */
@media (max-width: 359px) {
  .beat-b .d-xl { font-size: clamp(40px, 10vw, 84px); }
}

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

  .js .reveal > * { opacity: 1; transform: none; }
  .js .eyebrow.reveal { opacity: 1; transform: none; }
  .js .reveal-line .mask > span { transform: none; }
  .beat { opacity: 1; visibility: visible; }
}
