/* ==========================================================================
   RISE THE NATION — THE HALL · staging
   The crowd, the light boxes on the wall, and the titles. Everything is
   backlit: bodies are near-black with a cold rim, never front-lit.
   ========================================================================== */

.track { height: 1773vh; }

/* ══ HUD ════════════════════════════════════════════════════════════════ */
.hud {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 95;
  height: 72px;
  padding: 0 30px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  pointer-events: none;
}
.brand {
  pointer-events: auto;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text);
  font-size: .7rem;
  letter-spacing: .46em;
  text-transform: uppercase;
}
.hud-r { font-size: .62rem; font-weight: 500; letter-spacing: .18em; text-transform: uppercase; color: var(--smoke); }

.rail {
  position: fixed;
  right: 26px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 95;
  display: flex;
  flex-direction: column;
  gap: 15px;
}
.rail button {
  display: flex;
  align-items: center;
  gap: 11px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--smoke);
  font-size: .62rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  transition: color .35s var(--ease-film);
}
.rail button i { width: 18px; height: 1px; background: currentColor; transition: width .35s var(--ease-film); }
.rail button span { opacity: 0; transform: translateX(5px); transition: opacity .35s, transform .35s; }
.rail button:hover, .rail button.on { color: var(--beam); }
.rail button:hover span, .rail button.on span { opacity: 1; transform: none; }
.rail button.on i { width: 34px; box-shadow: 0 0 10px var(--beam); }

/* ══ TITLES ═════════════════════════════════════════════════════════════ */
.cards { position: relative; z-index: 20; }

.card {
  position: fixed;
  inset: 0;
  display: grid;
  padding: 0 7vw;
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s var(--ease-film);
}
.card.on { opacity: 1; }
.card.on .ci { pointer-events: auto; }

.ci { max-width: 620px; transform: translateY(var(--shift, 0px)); }

/* A card that holds still. initDirector() already skips writing --shift for
   these, and this makes sure a value left over from a previous frame cannot
   hold the type off-centre. */
.c-still .ci { transform: none; }

/* three positions in frame — the crowd owns the bottom centre */
.c-mid  { place-items: center; text-align: center; }
.c-mid .ci { max-width: 900px; }
.c-mid .lede { margin: 22px auto 0; }
.c-low  { align-items: end; justify-items: start; padding-bottom: 12vh; }
.c-top  { align-items: start; justify-items: center; padding-top: 12vh; text-align: center; }
.c-top .lede, .c-top .ci { margin: 0 auto; }

/* The line-up stands in the middle of this frame, so the copy that lands on it
   drops to the lower third rather than sitting across their faces. Centred
   horizontally still — only the vertical placement changes. */
.c-mid.c-drop { align-items: end; padding-bottom: 17vh; }

/* The flyer crowns the cast with the mark: SECOND / INSTINCT / the series
   line all sit in the dark above their heads. In this landscape frame the
   ceiling runs about 33vh before the first head and the block is about 27vh,
   so it goes high and tight rather than centred — .c-drop's opposite. */
.c-mid.c-crown {
  align-items: start;
  padding-top: 2.2vh;
}

/* The opening frame carries nothing but the scroll cue, and a cue belongs at
   the foot of the picture, not across the middle of it. The same move .c-drop
   makes, at the shallower depth a cue wants. The inset keeps it off the home
   indicator on a phone and is zero everywhere else. */
.c-mid.c-cue {
  align-items: end;
  padding-bottom: calc(7vh + env(safe-area-inset-bottom, 0px));
}
/* that margin existed to hold the cue off the copy above it — there is none */
.c-mid.c-cue .cue { margin-top: 0; }

/* the payoff line of the whole opening — it answers the shut door */
.big-line {
  font-family: var(--type-body);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 3.6vw, 2.9rem);
  letter-spacing: .06em;
  line-height: 1.18;
  color: var(--beam);
  margin-bottom: 22px;
  text-shadow: 0 2px 4px rgba(0,0,0,.85), 0 0 22px rgba(2,5,10,.85), 0 0 56px rgba(2,5,10,.6);
}

.card .heavy { margin: 10px 0 14px; }
.card .title + .lede { margin-top: 22px; }

.row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 24px; }

.reel-links { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; margin-top: 30px; }
.reel-links a {
  font-size: .64rem;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--smoke);
  text-decoration: none;
  padding-bottom: 5px;
  border-bottom: 1px solid rgba(214, 233, 250, .2);
}
.reel-links a:hover { color: var(--beam); border-bottom-color: var(--beam); }

.cue {
  margin-top: 40px;
  font-size: .6rem;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--smoke-dim);
  animation: cue 2.8s ease-in-out infinite;
}
@keyframes cue { 0%, 100% { opacity: .25; } 50% { opacity: .8; } }

/* ══ THE GAP FORM ═══════════════════════════════════════════════════════ */
.c-form { place-items: center; text-align: center; }
.c-form .ci {
  max-width: 460px;
  background: rgba(5, 8, 13, .8);
  backdrop-filter: blur(14px);
  border: 1px solid rgba(214, 233, 250, .14);
  padding: 34px 32px 26px;
}
.c-form .lede { margin: 18px auto 26px; }
.demo-form { text-align: left; }
.demo-form .btn { width: 100%; justify-content: center; margin-top: 10px; }
.alt { margin-top: 14px; font-size: .62rem; letter-spacing: .2em; color: var(--smoke-dim); }
.alt a { color: var(--beam); text-decoration: none; }

.credits {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid rgba(214, 233, 250, .1);
  font-size: .55rem;
  letter-spacing: .12em;
  line-height: 1.9;
  color: var(--smoke-dim);
}
.credits a { color: var(--smoke); text-decoration: none; }
.credits .tiny { display: block; margin-top: 5px; opacity: .7; }

@media (max-width: 900px) {
  .rail { display: none; }
  .face { right: 50%; transform: translate(50%, -50%); width: 62vw; opacity: calc(var(--f, 0) * .5); }
  .c-low { justify-items: center; text-align: center; padding-bottom: 16vh; }
  .hud-r { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .gap-mark, .cue { animation: none; }
}

/* ══ THINGS THAT ARE ALWAYS MOVING ═══════════════════════════════════════
   None of this waits for scroll. The hall is a place with air in it, a
   lamp that is not perfectly stable, and people who cannot stand still. */

/* the camera is being held, not bolted down */
.hall {
  animation: handheld 19s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes handheld {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1.012); }
  30%  { transform: translate3d(-.35%, .22%, 0) rotate(-.12deg) scale(1.016); }
  62%  { transform: translate3d(.28%, -.18%, 0) rotate(.1deg) scale(1.013); }
  100% { transform: translate3d(.14%, .3%, 0) rotate(-.05deg) scale(1.018); }
}

/* the lamp is a working light, and working lights are never steady */
.shaft { animation: lamp 7.3s linear infinite; }
@keyframes lamp {
  0%, 100% { filter: blur(10px) brightness(1); }
  6%       { filter: blur(10px) brightness(.86); }
  8%       { filter: blur(10px) brightness(1.06); }
  9%       { filter: blur(10px) brightness(.94); }
  47%      { filter: blur(10px) brightness(1.03); }
  49%      { filter: blur(10px) brightness(.9); }
  51%      { filter: blur(10px) brightness(1); }
}

/* the wall it is hitting breathes with it */
.wall-glow { animation: wall-breathe 11s ease-in-out infinite alternate; }
@keyframes wall-breathe {
  from { filter: blur(18px) brightness(.94); }
  to   { filter: blur(22px) brightness(1.08); }
}

/* anamorphic streak — the flare a real lens throws across a hard source */
.hall::after {
  content: '';
  position: absolute;
  left: -10%;
  right: -10%;
  bottom: 46vh;
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg,
    transparent, rgba(180, 214, 245, .10) 22%,
    rgba(220, 238, 252, .55) 50%,
    rgba(180, 214, 245, .10) 78%, transparent);
  filter: blur(3px);
  opacity: calc(var(--glow, .8) * .55);
  animation: streak 9s ease-in-out infinite alternate;
}
@keyframes streak {
  from { transform: scaleX(.82); opacity: .22; }
  to   { transform: scaleX(1.06); opacity: .5; }
}

/* nobody stands perfectly still: a slow breath and a shift of weight */
.fig-body {
  height: 100%;
  animation-name: idle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  transform-origin: 50% 100%;
}
@keyframes idle {
  0%   { transform: translateY(0) rotate(-.45deg) scaleY(1); }
  50%  { transform: translateY(-.5%) rotate(.15deg) scaleY(1.006); }
  100% { transform: translateY(.2%) rotate(.5deg) scaleY(.997); }
}

/* and each of them is standing on polished concrete */
.fig-refl {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 46%;
  transform: scaleY(-1);
  transform-origin: 50% 0;
  opacity: .16;
  filter: blur(3px);
  -webkit-mask-image: linear-gradient(to top, transparent 8%, #000 92%);
          mask-image: linear-gradient(to top, transparent 8%, #000 92%);
  pointer-events: none;
}
.fig-refl svg { height: 100%; width: auto; display: block; }

/* the smoke sits in layers, so it has depth rather than being one sheet */
.smoke::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(30% 20% at 34% 58%, rgba(190, 216, 240, .10), transparent 72%),
    radial-gradient(36% 18% at 68% 52%, rgba(170, 200, 228, .08), transparent 74%);
  animation: smoke-roll 27s ease-in-out infinite alternate-reverse;
}

@media (prefers-reduced-motion: reduce) {
  .hall, .shaft, .wall-glow, .hall::after, .fig-body, .smoke::after { animation: none; }
}

/* ══ GENERATED MEDIA, WHEN IT ARRIVES ════════════════════════════════════
   These rules do nothing until the files exist in assets/. The JS adds
   .has-plate only after an image has decoded, so a missing file is simply
   the drawn version staying put. */
.hall.has-plate {
  background-image: var(--plate);
  background-size: cover;
  background-position: 50% 58%;
  /* knocked back a little so the titles stay readable over it */
  background-color: rgba(4, 6, 10, .34);
  background-blend-mode: multiply;
}

/* The drawn room steps back: the plate is the room now. What stays is the
   part that has to react to scroll — the glow and the shaft still swing,
   brighten and narrow, just at lower strength on top of the photograph. */
.hall.has-plate .floor      { display: none; }
.hall.has-plate .wall-glow  { opacity: calc(var(--glow, .8) * .38); }
.hall.has-plate .shaft      { opacity: calc(var(--shaft-power, 1) * .45); }

/* The plate's own pool of light sits at 80% of its height, just left of
   centre — measured off the file, not guessed. The crowd stands in it. */
.hall.has-plate .crowd     { bottom: 19vh; }

/* Scale to the room in the photograph, not to the screen. That pool is
   roughly four metres across, so a person standing in it is about a quarter
   of the frame — measured off the plate's own geometry. */
.hall.has-plate .fig {
  height: calc((27 - var(--depth, 0) * 7) * 1vh);
  bottom: calc(var(--depth, 0) * 4vh);
}
.hall.has-plate .floor-pool {
  bottom: 15vh;
  left: 47%;
  opacity: calc(var(--pool, 1) * .42);
}
.hall.has-plate .gap-mark  { bottom: 20vh; }

.fig .plate-img {
  height: 100%;
  width: auto;
  display: block;
  /* these arrive already lit for a black room, so the grade only has to
     match them to each other — and lift whoever the light is on */
  filter: brightness(calc(.6 + var(--rim, 0) * .42)) contrast(1.06) saturate(.8);
  transition: filter .6s var(--ease-film);
}
/* the drawn silhouette underneath must not show through the photograph */
.fig.has-plate svg { display: none; }
/* a photographed figure carries its own rim, so it needs far less help */
.fig.has-plate { filter: blur(calc(var(--depth, 0) * 2px))
                        drop-shadow(0 0 calc(6px + var(--rim, 0) * 18px)
                                    rgba(190, 220, 245, calc(.06 + var(--rim, 0) * .22))); }
.fig.has-plate .fig-refl { opacity: .1; }

/* ══ ACT 1 · THE COLD OPEN ═══════════════════════════════════════════════
   The crouched figure holds the frame before the room exists. Same plate
   geometry as the hall, so when it fades the room is already behind it. */
.cold-open {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: 50% 58%;
  /* Driven straight from the scroll variable. It used to be gated behind a
     .ready class added when the fallback still loaded — so one missing file
     hid the whole layer, canvas and all. */
  opacity: var(--cold-open, 0);
  transition: opacity .2s linear;
  pointer-events: none;
  z-index: 3;
}

/* ══ ACT 3 · THE HOUSE'S FACTS ═══════════════════════════════════════════ */
.facts { list-style: none; margin-top: 24px; font-size: .78rem; font-weight: 400; letter-spacing: .04em; }
.facts li {
  display: flex;
  gap: 16px;
  padding: 10px 0;
  border-top: 1px solid rgba(214, 233, 250, .16);
  color: var(--text-soft);
  line-height: 1.65;
}
.facts li span { color: var(--beam-cold); min-width: 92px; letter-spacing: .16em; font-weight: 500; }
.facts li em { font-style: italic; color: var(--beam); }

/* ══ ACT 5 · THE RECORD'S CREDITS ════════════════════════════════════════
   Five small graded portraits — the people on one record, shown as evidence
   of what the house can put behind an artist. Not a roster. */
.credits-row {
  display: flex;
  gap: 14px;
  margin: 26px 0 4px;
  flex-wrap: wrap;
}
.credit { width: 88px; }
.credit img {
  width: 88px;
  height: 88px;
  object-fit: cover;
  display: block;
  filter: contrast(1.05) brightness(.95);
  -webkit-mask-image: radial-gradient(58% 58% at 50% 46%, #000 52%, transparent 82%);
          mask-image: radial-gradient(58% 58% at 50% 46%, #000 52%, transparent 82%);
}
.credit figcaption { margin-top: 4px; font-size: .54rem; letter-spacing: .14em; line-height: 1.5; }
.credit figcaption b { display: block; color: var(--text); font-weight: 500; text-transform: uppercase; }
.credit figcaption span { color: var(--smoke-dim); }

.also { margin-top: 22px; font-size: .62rem; letter-spacing: .16em; color: var(--smoke-dim); }
.also a { color: var(--smoke); text-decoration: none; border-bottom: 1px solid rgba(214,233,250,.2); }
.also a:hover { color: var(--beam); }

/* ══ ACT 6 · WHO IS OUT THERE ════════════════════════════════════════════ */
.who-list { list-style: none; margin-bottom: 26px; }
.who-list li {
  font-size: clamp(1rem, 1.7vw, 1.35rem);
  letter-spacing: .04em;
  padding: 7px 0;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .7s var(--ease-film), transform .7s var(--ease-film);
}
.card.on .who-list li { opacity: 1; transform: none; }
.who-list li b { font-weight: 400; color: var(--text); }
.who-list li span { color: var(--smoke); }

/* a card anchored right, for the beats that sit opposite the line */
.c-right { align-items: end; justify-items: end; padding-bottom: 12vh; text-align: right; }
.c-right .lede { margin-left: auto; }

@media (max-width: 900px) {
  .c-right { justify-items: center; text-align: center; }
  .c-right .lede { margin: 0 auto; }
  .credit, .credit img { width: 64px; }
  .credit img { height: 64px; }
}

/* the cold open draws into a canvas once the frame sequence exists */
#cold-canvas { width: 100%; height: 100%; display: block; }
.cold-open.scrubbing { background-image: none !important; }

/* ══ LEGIBILITY OVER THE FOOTAGE ═════════════════════════════════════════
   Type sat directly on the clip and lost its edges wherever the shaft or a
   pale jacket passed behind it. The first attempt put a soft radial behind
   each block, which read as a grey blob following the words around. This
   instead does what a film title does: a tight shadow on the glyphs, and a
   scrim that runs the full width of the frame so it reads as the room
   falling off rather than a shape behind the text. */
.card .title,
.card .heavy,
.card .lede,
.card .kicker,
.card .cue {
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
}

/* full-bleed, anchored to the edge the text sits against — never centred,
   because a centred scrim is exactly the blob we are avoiding */
.c-low::after,
.c-right::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 62%;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(2, 4, 7, .72) 0%, rgba(2, 4, 7, .3) 45%, transparent 100%);
}

.c-top::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 55%;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(2, 4, 7, .68) 0%, rgba(2, 4, 7, .28) 48%, transparent 100%);
}

/* the centred beats carry no scrim at all — the shadow alone holds them,
   and the frame stays clean */
.c-mid .ci { position: relative; }


/* ══ THE FILM ════════════════════════════════════════════════════════════
   Eight frames stacked. Opacity is driven by scroll; the blackout sits on
   top and fills whatever the scenes are not covering, so every cut is a dip
   to black rather than a dissolve between two lit rooms. */
.film {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: #04060a;
}

canvas.scene { object-fit: unset; }
.scene {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 58%;
  opacity: 0;
  will-change: opacity;
  transition: opacity .12s linear;
  /* every frame is pinned identically — same fit, same anchor, no transform.
     The pillars must land on the same pixels in all eight. */
  transform: none;
}

/* The vignette drops below the cards.

   It is a treatment for the PICTURE, not for the type, and at z-index 80 it
   was sitting over both. That cost nothing while every card lived in the
   clear centre of the frame — but .c-crown puts the mark at the top, where
   the gradient reaches 34% black. It was laying 34% over SECOND, 18% over
   INSTINCT and nothing at all over the series line: a gradient across the
   mark, which is why the three stopped agreeing with each other and none of
   them matched the flyer. Below the cards it dims the room and leaves the
   type alone, the same way .hud and .rail already sit above it at 95.
   Overridden here rather than edited in core.css, so the treatment's own
   definition stays intact. */
.vignette { z-index: 10; }

/* the blackout is the top layer — everything cuts through it */
.scene-black { z-index: 5; }

.dust { position: absolute; inset: 0; z-index: 6; pointer-events: none; }

.track { height: 1773vh; }

/* ══ WHERE THIS GOES ═════════════════════════════════════════════════════ */
.roadmap { list-style: none; margin-top: 26px; text-align: left; max-width: 620px; }
.roadmap li {
  display: grid;
  grid-template-columns: 120px 1fr;   /* month names need more room than Today/Next/Then */
  gap: 20px;
  padding: 14px 0;
  border-top: 1px solid rgba(214, 233, 250, .12);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .7s var(--ease-film), transform .7s var(--ease-film);
}
.card.on .roadmap li { opacity: 1; transform: none; }
.card.on .roadmap li:nth-child(2) { transition-delay: .12s; }
.card.on .roadmap li:nth-child(3) { transition-delay: .24s; }
.roadmap li b {
  font-family: var(--type-body);
  font-weight: 400;
  font-size: .72rem;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--beam);
}
.roadmap li span { font-size: .96rem; line-height: 1.65; color: var(--text-soft); font-weight: 400; }

@media (max-width: 900px) {
  .roadmap li { grid-template-columns: 1fr; gap: 6px; }
}


/* ══ ALIGNMENT ═══════════════════════════════════════════════════════════
   Nothing here. Deliberately.

   The files in assets/scenes/aligned/ are already resampled to one geometry:
   measured off the pixels, every frame carries its front pillars at
   0.182 / 0.819 (±0.002, about two pixels), and the door clip's last frame
   lands on the same pair. There is nothing left for a transform to correct.

   A CSS transform could not do this job anyway, even if it were needed.
   Pillar positions are measured in the source frame, but a transform runs in
   element space, and object-fit:cover crops the sides before the transform
   sees the frame — 12.3% each side at 16:9, 18.2% at 3:2. The scale factor
   survives that change of coordinates; the shift does not, so any fixed
   percentage is only correct at one window shape and drifts as you resize.
   Alignment belongs in the pixels, where it holds at every viewport.

   If a future frame arrives misaligned, resample the file. Do not add a
   transform here. */
.scene { transform: none; }


/* ══ THE TITLES ARRIVE, THEY DO NOT APPEAR ═══════════════════════════════
   Every block inside a card lifts in on its own beat. A card that fades in
   whole reads as a slide; one that assembles reads as something being said. */
.card .ci > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .75s var(--ease-film), transform .75s var(--ease-film);
}
.card.on .ci > * { opacity: 1; transform: none; }
.card.on .ci > *:nth-child(1) { transition-delay: .06s; }
.card.on .ci > *:nth-child(2) { transition-delay: .20s; }
.card.on .ci > *:nth-child(3) { transition-delay: .32s; }
.card.on .ci > *:nth-child(4) { transition-delay: .42s; }
.card.on .ci > *:nth-child(5) { transition-delay: .50s; }
.card.on .ci > *:nth-child(6) { transition-delay: .56s; }

.card.on .facts li { animation: fact-in .7s var(--ease-film) both; }
.card.on .facts li:nth-child(1) { animation-delay: .40s; }
.card.on .facts li:nth-child(2) { animation-delay: .50s; }
.card.on .facts li:nth-child(3) { animation-delay: .60s; }
.card.on .facts li:nth-child(4) { animation-delay: .70s; }
@keyframes fact-in { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .card .ci > * { opacity: 1; transform: none; transition: none; }
  .card.on .facts li { animation: none; }
}


/* one line that carries the weight of the paragraph above it */
.lede-strong {
  margin-top: 14px;
  font-weight: 600;
  font-size: clamp(1.1rem, 1.5vw, 1.32rem);
  letter-spacing: .01em;
  color: var(--beam);
}


/* the manifesto line sits on a shut door — it has to carry the frame alone */
.title-strong {
  font-weight: 800;
  letter-spacing: .07em;
  text-indent: .07em;
  font-size: clamp(1.8rem, 5vw, 4rem);
  line-height: 1.14;
  color: var(--beam);
}

/* Text arrives on a stagger but leaves at once. A line that lingers while the
   door is already moving reads as a mistake, so the exit is quick and has no
   per-child delay. */
.card:not(.on) .ci > * {
  transition-duration: .22s;
  transition-delay: 0s;
}


/* The door has bright metalwork behind this line — dark overall, but 9% of
   the pixels under the text are near-white, and that is what breaks the
   letters up. A flat colour cannot fix that; the type needs to carry its own
   darkness with it. Tight shadow for edge definition, then two soft blooms
   that knock the highlights back only where the glyphs actually are. */
.card .title.title-strong {
  text-shadow:
    0 2px 4px rgba(0, 0, 0, .9),
    0 0 20px rgba(2, 5, 10, .92),
    0 0 52px rgba(2, 5, 10, .8),
    0 0 90px rgba(2, 5, 10, .55);
}


/* the two names in the manifesto lift off the sentence without shouting */
.lede b {
  font-weight: 600;
  color: var(--text);
}


/* Body copy on a card has the same problem the kickers do, and worse on the
   line-up once the light is struck: the lower third of that frame used to
   run about 74 at the 95th percentile and now runs 197, because the pool is
   exactly where this copy sits. A pale 400-weight blue cannot hold against a
   background swinging from black to near-white inside one paragraph.
   Heavier, brighter, and carrying the same halo the kickers and the lockup
   use — a scrim bonded to the glyphs rather than a box drawn behind them. */
.card .lede {
  font-weight: 500;
  color: var(--text);
  text-shadow:
    0 1px 3px rgba(0, 0, 0, .92),
    0 0 12px rgba(2, 5, 10, .9),
    0 0 30px rgba(2, 5, 10, .72),
    0 0 64px rgba(2, 5, 10, .5);
}
/* the bolded names inside it lift a step further */
.card .lede b {
  font-weight: 700;
  color: #ffffff;
}

/* The discipline labels sit over lit scenes — warm stage light behind the
   singers, coloured beams behind the DJ. At .26em tracking in a thin blue
   they dissolved into the picture. Weight and a real shadow instead. */
.card .kicker {
  text-shadow:
    0 1px 3px rgba(0, 0, 0, .9),
    0 0 14px rgba(2, 5, 10, .85),
    0 0 34px rgba(2, 5, 10, .6);
}


/* ══ THE LOCKUP — SECOND INSTINCT ════════════════════════════════════════
   Two lines, one stamped mark. INSTINCT is set in the display face and is
   what decides the width of the block; SECOND is then justified across
   exactly that width by distributing its letters edge to edge, so the pair
   locks up however the type reflows. Nothing here is a hand-tuned
   letter-spacing value — the spacing is whatever it takes to meet the edges,
   which is the only way the two lines stay flush at every viewport.

   Both sizes hang off one font-size on the block, so the ratio between them
   is fixed and the whole mark scales as a unit. */
/* Shrink-wraps the title, so 100% inside it is the width of INSTINCT and
   nothing else. Both the light line and the series line size themselves off
   that, which is what makes all three end flush. The two squeeze factors
   live here so the series line — a sibling of the lockup, not a child —
   can see them too. */
.lockup-block {
  display: inline-block;
  --condense: .88;               /* Anton, squeezed toward the poster's cut */
  --condense-sub: .433;          /* the light line, squeezed harder — see below */
  --condense-tag: .46;           /* and the series line, harder still */
  /* How wide the series row runs, as a share of the wrapper. It should be
     --condense (.88), the same as INSTINCT's own scale, and by my arithmetic
     that lands the two flush. On screen it still ran over, so this is its own
     dial: lower it until the ends sit inside INSTINCT. Every 0.01 pulls each
     end in by about 2.4px at a 1920px viewport. */
  --tag-width: .84;
}

.lockup {
  display: block;
  /* Eased down from 12.6vw/11rem: at that size the block cleared the cast's
     heads by only about 38px, which read as crowding.
     It takes a vh term as well, because the room it has to fit in is the
     ceiling above the cast and that is set by the viewport's HEIGHT. Sized
     on width alone it fitted at 1920x1080 and collided on a 1366x768 laptop,
     where the frame is the same width and far shorter. */
  font-size: clamp(2.05rem, min(10.7vw, 16vh), 9.35rem);
  /* sampled off the flyer: the deep purple of INSTINCT and the lavender of
     SECOND. The rest of the film stays cold blue — this mark is the one place
     the series' own colour lives. */
  --ink-main: #603d73;
  --ink-sub:  #c3a3ce;
  line-height: 1;
  letter-spacing: normal;
  text-indent: 0;
}
.lockup-main {
  display: block;
  font-family: var(--type-display);
  font-weight: 400;              /* Anton ships one weight */
  font-size: 1em;
  letter-spacing: .006em;
  line-height: .86;
  color: var(--ink-main);
  /* Anton is condensed, the poster's face is more so. Squeezing the display
     line horizontally is what makes the letters read tall — and because a
     transform costs no layout, the block keeps its natural width and SECOND
     just takes the same fraction of it. Only this line is squeezed: the light
     line keeps its true letterforms. */
  transform: scaleX(var(--condense));
  transform-origin: 50% 50%;
}
/* space-between is what does the work: the first and last letter sit on the
   edges of INSTINCT and the gaps take up the slack */
.lockup-sub {
  display: flex;
  justify-content: space-between;
  font-family: var(--type-body);
  font-weight: 300;
  /* Anton's cap height is .87em and Archivo's is .70em, so .62em here puts
     SECOND at exactly half the cap height of INSTINCT. */
  font-size: .62em;
  line-height: 1;
  width: calc(var(--condense) * 100%);
  /* On the flyer the two lines almost touch: the gap is .077 of INSTINCT's
     cap height. Here it measured .192 — two and a half times too open, which
     is what stopped the two words reading as one mark. The line box below
     SECOND already carries its own descender space, so closing it needs a
     negative margin, not a smaller one. */
  margin: 0 auto -.03em;
  color: var(--ink-sub);
}
/* At half the cap height SECOND is almost as wide as INSTINCT on its own, and
   the gaps between the letters would close up to nothing. Squeezing each
   letter gives that width back to the spacing — the letters read tall and thin
   and still reach the edges. The outer two anchor to the edge they sit on so
   the line stays flush with INSTINCT; the rest squeeze about their centres,
   which spaces them optically rather than evenly. */
.lockup-sub span {
  transform: scaleX(var(--condense-sub));
  transform-origin: 50% 50%;
}
.lockup-sub span:first-child { transform-origin: 0 50%; }
.lockup-sub span:last-child  { transform-origin: 100% 50%; }

/* No shadow on the mark.

   It carried one because it used to sit mid-frame over the glow behind the
   cast. Two things changed: it moved up to the ceiling, and SECOND's letters
   were squeezed to .433 — only a few pixels wide. At that stroke width a
   dark glow covers a far larger share of the letter than it does of a thick
   one, so it bled inward and pulled the purple toward grey. INSTINCT had the
   same problem from the other side: it is a dark fill (#603d73), so a dark
   halo does not separate it from anything, it just thickens the dark mass
   and muddies the colour.

   The flyer has no halo either — the letters sit clean on the ground. The
   fills are sampled straight from it, so with nothing laid over them they
   should now read as the same purple. */
.card .title.lockup {
  text-shadow: none;
}


/* ══ THE SERIES LINE ═════════════════════════════════════════════════════
   The flyer's third line, under the lockup, in the cream sampled off it.
   It is a sibling of the lockup rather than part of it, so it arrives on its
   own beat in the card's stagger instead of with the title. */
.series-tag {
  font-family: var(--type-body);
  font-weight: 500;
  text-transform: uppercase;
  /* The third line of the mark, not a caption under it: on the flyer it is
     exactly as wide as INSTINCT. Tracking could only approximate that and
     overshot, so it is built the way SECOND is — letters on a flex row,
     space-between, at the title's own width. The gaps then fall wherever
     they must to land flush, at any viewport.
     Its letters are tall and narrow on the flyer — cap 2.07x the width of one
     — where a grotesque set plainly comes out at 0.95, near square. So it is
     squeezed like SECOND is, and sized up to match the flyer's proportion:
     its cap is .171 of INSTINCT's, which is 1.59vw here, about twice what it
     was. Taller letters, narrower, and the width they give up goes to the
     gaps, so the row still lands flush.

     Size is otherwise set by WIDTH, not by cap height. Sizing it from the flyer's cap
     ratio gave 1.92vw, at which the 21 letters measure 118% of the row: they
     overflow, .lockup-block shrink-wraps to fit THEM instead of the title,
     and the line ends up longer than the mark it belongs to. The flyer runs
     about 53% ink and 47% gap, which is 1vw here — and the 1.15rem ceiling
     holds that true past 1840px, where the lockup stops growing. */
  display: flex;
  justify-content: space-between;
  width: calc(var(--tag-width) * 100%);
  margin-left: auto;
  margin-right: auto;
  font-size: clamp(.85rem, 1.59vw, 1.91rem);
  letter-spacing: 0;
  /* On the flyer this line sits 0.43 of its own cap height below INSTINCT.
     Here it was landing at about 1.4 of it — and only part of that was the
     margin: line-height 1.4 puts a third of the font size of leading above
     the caps before the margin is even counted. Both pulled in, so the two
     close up the way they do on the poster. */
  line-height: 1.05;
  margin-top: .14em;
  color: #f8e6d2;
  text-shadow: none;
}
/* The letters live inside one aria-hidden span, so that span has to be the
   flex row rather than .series-tag itself — otherwise the row has a single
   child and space-between has nothing to distribute. */
.series-tag > span {
  display: flex;
  justify-content: space-between;
  width: 100%;
}
/* Squeezed the way SECOND is, but each letter also gets a narrow layout box.

   scaleX is a transform: it changes what is drawn, not what the row measures.
   Left to their natural widths the 21 letters measured 423px against a 367px
   row, so they overflowed, and .lockup-block — which shrink-wraps its widest
   child — grew to fit them. The series line then ran past INSTINCT, which is
   the one thing it must never do.

   The box is .34em wide, about half the natural advance, and the glyph is
   squeezed into it. Now the row measures narrower than the mark at every
   size, space-between has real slack to distribute, and nothing can push the
   wrapper wider.

   The outer two anchor to the edge they sit on, so the row stays flush —
   scaling about their centres would pull both ends inward. */
.series-tag > span > span {
  display: inline-block;
  width: .34em;
  text-align: center;
  transform: scaleX(var(--condense-tag));
  transform-origin: 50% 50%;
}
/* The outer two also align their glyph to the edge they anchor on. The box
   is narrower than the glyph, so a centred glyph already starts outside the
   box — scaling it toward that edge then left it hanging past the row, which
   is what made the line run wider than INSTINCT above it. Aligned left and
   right instead, the ink starts and ends on the row's own edges. */
.series-tag > span > span:first-child {
  transform-origin: 0 50%;
  text-align: left;
}
.series-tag > span > span:last-child {
  transform-origin: 100% 50%;
  text-align: right;
}

/* the space between words: wider than a letter gap, or the two words run
   together once the letters are spread */
.series-tag .gap { width: .5em; flex: 0 0 auto; }

/* ══ THE OVERHEAD LIGHT — PARKED ═════════════════════════════════════════
   The light is baked into scene01_lineup_lit.png now, which cuts in on the
   scroll the title lands — see 'studiolit' in label.js. What follows drew it
   in CSS instead: a source burning at the ceiling, a cone that died at 42%
   of the frame, and the pool where it landed.

   Parked by renaming rather than commenting out: CSS comments cannot nest
   and this block is full of them. Nothing in the document is called
   .beam-parked, so none of these rules can match. To bring it back, rename
   .beam-parked to .beam and restore the <div class="beam"> in index.html —
   the hook that feeds it a scene's opacity is still live in initDirector().
   ══════════════════════════════════════════════════════════════════════

   ── what it was, verbatim ──
   The pool the flyer's light lays on the floor in front of the cast.

   There is no shaft. A visible beam needs atmosphere to scatter in, and the
   air in the front of this room is clear — drawn anyway it just washes the
   ceiling grey and reads as a gradient laid over the picture, which is
   exactly what it looked like. Light landing on a surface needs no such
   excuse, so the pool carries the whole idea on its own.

   The shape is measured off the room: the still's own floor fixtures
   photograph at height/width 0.20, so that is what a circle on this floor
   looks like from this camera. Three layers, because real light is not one
   soft blob — a small hot core, a wide dim spill falling off around it, and
   a vertical smear standing up out of it where the polished concrete
   reflects the source.

   Screen blend, so it adds to what is there: the floor keeps its
   reflections and the blacks stay black. initDirector() hands it the studio
   still's own opacity, so it arrives and leaves with the scene.
   ────────────────────────────────────────────────────────────────────────── */
.beam-parked {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  /* the fixture itself, burning at the ceiling */
  background: radial-gradient(ellipse 3.4% 3.6% at 50% 1%,
    rgba(240, 248, 255, .55) 0%,
    rgba(228, 240, 253, .18) 46%,
    rgba(216, 232, 248, 0)   100%);
  /* no z-index on purpose: it sits between the studio still and the scenes
     that follow it in the markup, so a later scene covers it as it cuts in.
     Lifting it above them would show the light over the singers during the
     handover, where the two scenes briefly overlap. */
}

/* The cone under the fixture. It dies at 42% of the frame — the room is
   hazy up near the ceiling and clear lower down, so the light only has
   something to scatter in for the first stretch of its fall. Running it
   further is what made the first attempt read as a wash laid over the
   picture. The stops follow an inverse-square curve, so it is bright at the
   source and almost gone a third of the way down. */
.beam-parked::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 42%;
  clip-path: polygon(47.8% 0%, 52.2% 0%, 58.5% 100%, 41.5% 100%);
  background: linear-gradient(to bottom,
    rgba(232, 242, 255, .30) 0%,
    rgba(224, 236, 252, .17) 20%,
    rgba(214, 230, 248, .08) 40%,
    rgba(206, 222, 242, .03) 60%,
    rgba(200, 216, 238, .01) 80%,
    rgba(196, 212, 236, 0)   100%);
  filter: blur(7px);
}

/* Where it lands. Three layers, because real light is not one soft blob: a
   small hot core, a wide dim spill around it, and a vertical smear standing
   up out of it where the polished concrete reflects the source. The ellipse
   is measured off the room — the still's own floor fixtures photograph at
   height/width 0.20, so that is what a circle on this floor looks like from
   this camera. */
.beam-parked::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 82%;
  width: 54%;
  height: 18%;
  transform: translate(-50%, -50%);
  background:
    radial-gradient(ellipse 8% 42% at 50% 42%,
      rgba(228, 238, 252, .20) 0%,
      rgba(222, 234, 250, .06) 48%,
      rgba(214, 228, 246, 0)   100%),
    radial-gradient(ellipse 21% 16% at 50% 64%,
      rgba(238, 246, 255, .80) 0%,
      rgba(228, 240, 253, .34) 42%,
      rgba(218, 232, 248, 0)   100%),
    radial-gradient(ellipse 48% 35% at 50% 64%,
      rgba(204, 220, 242, .30) 0%,
      rgba(198, 214, 238, .09) 46%,
      rgba(190, 208, 232, 0)   100%);
  filter: blur(6px);
}

@media (prefers-reduced-motion: reduce) { .beam-parked { filter: none; } }
