/* Two movements share this section, so it is twice a normal beat's height. */
.beat-focus { height: 600vh; }

.beat-focus__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  background: var(--c-aperture);
}

/* ---- The opening frame, before any JavaScript runs ----
   main.js is a deferred module, so everything below paints at least one frame before
   setProgress() has said anything. Left to the defaults that frame is: a full-viewport
   YELLOW rectangle (the Portalith is inset:0 with a gold background and no clip-path yet),
   the world film, the statement and both principle rails — all at full opacity. That is
   the glitch on load, and it is CSS's job to fix, not JS's: by the time JS could correct
   it, it has already been seen.

   So the stylesheet describes the opening frame itself. The trade is real and worth
   naming: if the module never runs at all, Focus stays on that frame instead of falling
   back to everything-visible. Every other beat is unaffected. */
.beat-focus .statement__soft,
.beat-focus .statement__sharp,
.beat-focus [data-principle] { --in: 0; }

/* The Portalith and the lockup are hidden until focus.js says the beat is ready, and the
   lockup already carries its opening --lift. Both matter for the same reason: without them
   the first painted frame is the pair centred and opaque, and the next one is the pair
   offset and animating in from zero — which reads as the page assembling itself twice.
   is-ready is added in both motion modes; is-arriving only adds the animation on top. */
.beat-focus .portalith,
.beat-focus .lockup { opacity: 0; }

.beat-focus.is-ready .portalith,
.beat-focus.is-ready .lockup { opacity: 1; }

.beat-focus .lockup { --lift: 22vh; }

/* openFrame(0) expressed in CSS: the hero 9:16 at 30vh, closed and centred. Same numbers as
   HEIGHTS_VH[0] and RATIOS["9:16"] in portalith.js — 30vh tall, 16.875vh wide. */
.beat-focus .portalith {
  clip-path: inset(35vh calc(50% - 8.4375vh) round 28px);
}

/* The page opens on pure black: nothing but the gold Portalith and the wordmark. The world,
   the statement and the rail all reach that state through --in, the arrival gate defined on
   .plane in base.css — depth alone cannot get there, because depth 0 still paints. */

/* The Portalith is `position: absolute` from portalith.css, so this stage clips it and it
   cannot outlive the section. Nothing to override here.

   Both rails already sit at the same absolute slot from principle-rail.css, so they stack
   and crossfade on depth alone. Nothing needed there either. */

.lockup {
  position: absolute;
  inset: 0;
  z-index: 10;
  pointer-events: none;
}

/* The wordmark is drawn twice — the <img> is its left half, the ::after its right — so the
   two can travel apart when the Portalith outgrows the lockup. Both are placed with
   IDENTICAL box metrics and the same centring, because any difference between them shows
   as a seam down the middle of the word. Do not centre one with grid and the other with
   offsets: they must be positioned the same way.

   The wordmark is now the supplied WHITE asset (assets/brand/), not the black one turned
   round with a filter. Same reason the nav chip carries its own ground: an inverted mark is
   a brand problem, and a filter is a guess at one. */
.lockup__wordmark,
.lockup::after {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(58vw, 780px);
  aspect-ratio: 2000 / 452;
  height: auto;
}

.statement {
  position: absolute;
  z-index: 15;
  left: var(--gutter);
  right: var(--gutter);
  /* 10vh, not 14. The statement block is ~310px tall, so at 14vh its top landed at y 550
     and the centred wordmark's baseline sits at 583 — a hundred-pixel collision once the
     lockup's opening lift is counted. At 10vh the block clears the wordmark entirely. */
  bottom: 10vh;
  display: grid;
  gap: 0.15em;
  font-family: var(--f-display);
  font-size: var(--t-statement);
  line-height: 1.04;
  color: var(--c-ground);
  pointer-events: none;
}

/* The two halves leave the way the wordmark does — one left, one right, on the same
   --split. Each composes the depth grammar's own scale channel into its transform rather
   than replacing it, or the plane loses a channel the moment it starts to travel. */
.statement__soft {
  max-width: 18ch;
  transform: translateX(calc(var(--split, 0) * -70vw)) scale(var(--p-scale, 1));
}
.statement__sharp {
  max-width: 18ch;
  justify-self: end;
  text-align: right;
  transform: translateX(calc(var(--split, 0) * 70vw)) scale(var(--p-scale, 1));
}

.cue {
  position: absolute;
  z-index: 20;
  left: 50%;
  bottom: 2.25rem;
  transform: translateX(-50%);
  color: var(--c-ground);
  font-size: var(--t-nav);
  letter-spacing: var(--tracking-nav);
  text-transform: uppercase;
  /* No opacity transition: focus.js writes this element's opacity every scrub frame. A
     CSS transition on top of a per-frame write makes it lag scroll and rubber-band
     instead of tracking it. */
}

/* The entrance carries its own from-state and is removed on animationend, so the resting
   state below is what the page falls back to — always visible. */
/* The arrival, on black. The Portalith lands first and the wordmark follows a beat later,
   so the mark is read as an object before it is read as a lockup. The class is removed on
   the LOCKUP's animationend because its delay makes it the later of the two — remove it on
   the Portalith's and the wordmark is cut off mid-entrance. */
.beat-focus.is-arriving .portalith { animation: portalith-arrive 1.1s var(--ease-out) 0s both; }
.beat-focus.is-arriving .lockup { animation: lockup-arrive 1.1s var(--ease-out) 0.25s both; }

@keyframes portalith-arrive {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes lockup-arrive {
  from { opacity: 0; transform: scale(1.04); }
  to   { opacity: 1; transform: scale(1); }
}

/* The split. --split runs 0..1 and is written by focus.js. Each half keeps the shared
   centring translate and adds its own travel on top, so at --split:0 they sit exactly
   on top of each other and the word reads as one image. */
/* --lift drops the wordmark below the Portalith on the opening frame so the two read as a
   vertical lockup rather than a collision — the gold 9:16 is only ~170px wide and the
   wordmark is 780px, so centred they always cross. It returns to 0 as the aperture fills,
   which is also when the word needs to be centred for the split. Both halves take it, or
   they part at different heights. */
.lockup__wordmark {
  transform: translate(-50%, -50%) translateY(var(--lift, 0)) translateX(calc(var(--split, 0) * -62vw));
  clip-path: inset(0 50% 0 0);
}

.lockup::after {
  content: "";
  background: url("../assets/brand/wordmark-white.png") center / contain no-repeat;
  transform: translate(-50%, -50%) translateY(var(--lift, 0)) translateX(calc(var(--split, 0) * 62vw));
  clip-path: inset(0 0 0 50%);
}

/* Reduced motion has no scrub, so a 600vh runway would become six viewport-heights of an
   unchanging frame. Collapse it to a single screen.

   Collapse to 100vh, never `auto`: every child of this stage is absolutely positioned and
   contributes no height, so `auto` flattens the whole composition into its padding. One
   viewport keeps resolve()'s composed still frame intact. */
:root[data-motion="reduced"] .beat-focus { height: 100vh; }
:root[data-motion="reduced"] .beat-focus__stage {
  position: relative;
  height: 100vh;
}

/* The motion-off frame, composed. All three rules below are scoped to the reduced state on
   purpose: the scrubbed path composes its own end frame and must not inherit any of this.
   Scoping to the attribute rather than doing it in resolve() also makes them symmetric for
   free — the on-page motion toggle rewrites data-motion, so turning motion back on undoes
   them without any cleanup code that could fail to run.

   1. The ghosted wordmark is dropped. At depth 0 it is a 14px-blurred ivory smear, and this
      frame is a bright, busy, full-bleed film: it does not read as depth anywhere on it, and
      raising it clear of the statement only loses it against the title card instead. resolve()
      still gives it a definite depth, per the rule that every element setProgress() touches
      gets one — this hides that value rather than replacing it. */
:root[data-motion="reduced"] .beat-focus .lockup { display: none; }

/* 2. The statement drops into the calmer lower band. */
:root[data-motion="reduced"] .beat-focus .statement { bottom: 7vh; }

/* 3. A scrim, so the soft half of the statement reads AS soft rather than as absent. Ivory
      text at ~6px blur has nowhere to land on this film's bright dancers, and "We take brands
      out of the blur" is the half of the client's line that has to be legible for the sharp
      half to mean anything. z-index 12 puts it over the Portalith (z 1) and the world (z 0)
      but under the statement (z 15) and the principle rail (z 20), so only the film dims. */
:root[data-motion="reduced"] .beat-focus__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 12;
  pointer-events: none;
  /* Re-weighted 2026-08-27, when this surface went back to carrying the launch films.
     The stops were tuned against `FG_TV Static`, which is near-monochrome and dark, so the
     23% of it left showing through at the 40% stop was almost nothing. The films are
     high-contrast illustration and photography, and the same 23% of a Hendrick's frame is
     a candelabra directly behind the word "blur".

     Measured: the soft half spans 19.7%-40.5% up from the bottom of the stage, so 40% is
     exactly its top edge — the knee of the old ramp sat on the line it was there to
     protect. It now holds 0.88 through the whole of that band and breaks above it, which
     keeps §0.A's trade intact: the bottom dims, the full-colour payoff lives in the top
     half, and the sharp half is untouched. */
  background: linear-gradient(
    to top,
    rgb(17 17 17 / 0.94) 0%,
    rgb(17 17 17 / 0.88) 42%,
    rgb(17 17 17 / 0.12) 66%,
    rgb(17 17 17 / 0) 76%
  );
}
