/* Motion layer ---------------------------------------------------------------
   Everything here is an enhancement on top of base.css, components.css and sections.css, and every rule that
   hides something is gated on `html.m-js:not(.m-still)`:

     .m-js     set synchronously by the inline head script, so the opening
               states are in place before the first paint — never after it.
     .m-still  set alongside it when the visitor asks for reduced motion, which
               switches this whole sheet back to the static page.

   The same inline script drops .m-js again if motion.js has not reported in,
   so a blocked or broken script can never leave the page hidden.

   Where the section sheets already own an element's `transform`, this sheet either
   composes with it explicitly or moves its own part onto the independent
   `scale` / `translate` properties, so nothing below is silently lost.
   -------------------------------------------------------------------------- */

:root {
  --m-ease: cubic-bezier(.22, 1, .36, 1);
  --m-ease-io: cubic-bezier(.76, 0, .24, 1);
}

/* Opening curtain ------------------------------------------------------------
   Shown only when the page opens at the top with motion allowed; the markup is
   inert otherwise, so no-JS and reduced-motion visitors land on the hero.
   -------------------------------------------------------------------------- */
.intro { display: none; }

.m-js:not(.m-still) .intro {
  position: fixed;
  z-index: 300;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 24px;
  background: var(--charcoal-2);
  color: var(--paper);
  clip-path: inset(0 0 0 0);
  transition: clip-path 1.05s var(--m-ease-io);
}

.intro-mark {
  display: block;
  /* The wordmark is masked by its own line and rides up into it. */
  overflow: hidden;
  padding-bottom: .12em;
  margin-bottom: -.12em;
  font-family: var(--font-display);
  font-size: clamp(58px, 12vw, 150px);
  font-weight: var(--w-light);
  letter-spacing: .04em;
  line-height: 1;
  font-variant-numeric: lining-nums;
}

.intro-mark i {
  display: inline-block;
  font-style: normal;
  transform: translateY(110%);
  animation: intro-rise 1s var(--m-ease) .05s forwards;
}

.intro-sub {
  font-size: 10px;
  letter-spacing: .32em;
  text-transform: uppercase;
  color: rgba(var(--paper-rgb), .55);
  opacity: 0;
  animation: intro-fade .8s ease .4s forwards;
}

.intro-bar {
  width: min(240px, 42vw);
  height: 1px;
  margin-top: 10px;
  background: rgba(var(--paper-rgb), .2);
  overflow: hidden;
}

.intro-bar i {
  display: block;
  height: 100%;
  background: var(--paper);
  transform: scaleX(var(--load, 0));
  transform-origin: left;
  transition: transform .3s linear;
}

.intro-pct {
  font-size: 10px;
  letter-spacing: .2em;
  font-variant-numeric: tabular-nums;
  color: rgba(var(--paper-rgb), .5);
}

body.m-lock { overflow: hidden; }

/* Both of these have to out-specify the .m-js:not(.m-still) .intro rule
   above, or the wipe loses to it and the curtain simply vanishes. */
.m-js:not(.m-still) .intro.is-done { clip-path: inset(0 0 100% 0); pointer-events: none; }
.m-js:not(.m-still) .intro.is-done > * { opacity: 0; transition: opacity .4s ease; }

/* The home page rises the white lockup instead of a set name, and opens on
   its X alone. The mark is clipped to the X's box (x 5–196, y 5–197 of the
   900×303 image), pivoted on the X's centre and shifted so that centre sits
   in the middle of the screen, a size up. The type metrics above are zeroed
   so the mask is exactly the image's box and the 110% translate hides all of
   it. .is-full lets go of the clip, shift and size in one move, so the X
   settles into place while H E K wipe out from behind it and CONSTRUCTION
   drops in beneath. */
.intro-lockup { --intro-logo-w: clamp(180px, 34vw, 440px); }

.intro-mark-logo { padding-bottom: 0; margin-bottom: 0; font-size: 0; line-height: 0; }
.intro-mark-logo i { display: block; }
.intro-mark-logo img { display: block; width: var(--intro-logo-w, clamp(180px, 34vw, 440px)); height: auto; }

.intro-lockup .intro-mark-logo {
  transform-origin: 11.17% 33.27%;
  clip-path: inset(0 76.2% 30.4% 0);
  translate: 38.83% 16.73%;
  scale: 1.73;
  transition: clip-path 1.9s var(--m-ease-io), translate 1.9s var(--m-ease-io), scale 1.9s var(--m-ease-io);
}
.intro-lockup.is-full .intro-mark-logo { clip-path: inset(0); translate: 0 0; scale: 1; }

/* The lockup bottoms out at 180px on phones, where the X inside it would be
   a dot; it gets a bigger head start there. */
@media (max-width: 720px) {
  .intro-lockup .intro-mark-logo { scale: 2.28; }
}

/* A soft light behind the lone X, gone again as the lockup unfolds. */
.m-js:not(.m-still) .intro-lockup::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--intro-logo-w) * .9);
  aspect-ratio: 1;
  translate: -50% -50%;
  background: radial-gradient(circle, rgba(var(--paper-rgb), .14), transparent 62%);
  opacity: 0;
  animation: intro-fade .8s ease .2s forwards;
}
.m-js:not(.m-still) .intro-lockup.is-full::before { animation: intro-glow-out .8s ease forwards; }

/* The bar and count hang under the finished lockup instead of sharing the
   grid with it, so the X really is centred while it is on its own, and they
   only arrive with the full logo. */
.intro-load {
  position: absolute;
  left: 50%;
  top: 50%;
  display: grid;
  justify-items: center;
  gap: 24px;
  translate: -50% calc(var(--intro-logo-w) * .168 + 24px);
  opacity: 0;
  transition: opacity .5s ease .25s;
}
.intro-lockup.is-full .intro-load { opacity: 1; }

@keyframes intro-rise { to { transform: translateY(0); } }
@keyframes intro-fade { to { opacity: 1; } }
@keyframes intro-glow-out { from { opacity: 1; } to { opacity: 0; } }

/* Hero choreography ----------------------------------------------------------
   One sequence ordered by its own delays rather than by DOM position, so the
   labels settle while the type is still arriving.
   -------------------------------------------------------------------------- */
.m-js:not(.m-still) .hero-lead > p,
.m-js:not(.m-still) .hero-actions,
.m-js:not(.m-still) .scroll-cue {
  opacity: 0;
  transform: translateY(var(--m-from, 20px));
}

.m-js:not(.m-still) .hero-in .hero-lead > p,
.m-js:not(.m-still) .hero-in .hero-actions,
.m-js:not(.m-still) .hero-in .scroll-cue {
  opacity: 1;
  transform: none;
  transition:
    opacity .9s ease var(--m-d, 0ms),
    transform 1.1s var(--m-ease) var(--m-d, 0ms);
}

.m-js:not(.m-still) .hero-lead > p { --m-d: 780ms; --m-from: 30px; }
.m-js:not(.m-still) .hero-actions { --m-d: 900ms; --m-from: 24px; }
.m-js:not(.m-still) .scroll-cue { --m-d: 980ms; --m-from: 20px; }

/* The headline is held back until the sequence starts, so its lines can be
   measured behind the curtain rather than flashing un-split type. */
.m-js:not(.m-still) .hero h1 { opacity: 0; }
.m-js:not(.m-still) .hero-in .hero h1 { opacity: 1; }

/* The hero media settles out of a slow push-in. This rides the independent
   `scale` property because script.js owns `transform` here for the parallax —
   transitioning that would make the parallax lag by the whole 2.4s. */
.m-js:not(.m-still) .hero-video,
.m-js:not(.m-still) .hero-image { scale: 1.13; transition: scale 2.4s var(--m-ease); }
.m-js:not(.m-still) .hero-in .hero-video,
.m-js:not(.m-still) .hero-in .hero-image { scale: 1; }

/* Kinetic type ---------------------------------------------------------------
   motion.js rewrites a [data-split] heading into measured lines of words: the
   line is a block that clips at its own box, and each word rides up from under
   that edge. The padding / negative-margin pair gives descenders room to live
   inside the clip without changing the block's height by a pixel.
   -------------------------------------------------------------------------- */
.m-line {
  display: block;
  overflow: hidden;
  padding-bottom: .2em;
  margin-bottom: -.2em;
  white-space: nowrap;
}

.m-w { display: inline-block; }

.m-js:not(.m-still) [data-split].is-split .m-w {
  transform: translate3d(0, calc(100% + .24em), 0);
  will-change: transform;
}

.m-js:not(.m-still) [data-split].is-split.is-in .m-w {
  transform: none;
  transition: transform 1s var(--m-ease) var(--m-wd, 0ms);
  will-change: auto;
}

/* Drawn rules ----------------------------------------------------------------
   The section-label rules draw themselves instead of appearing.
   -------------------------------------------------------------------------- */
.m-js:not(.m-still) .section-label.reveal::after,
.m-js:not(.m-still) .reveal .section-label::after {
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .9s var(--m-ease) .12s;
}
.m-js:not(.m-still) .section-label.reveal.is-visible::after,
.m-js:not(.m-still) .reveal.is-visible .section-label::after { transform: scaleX(1); }

/* Counters are tabular already; the run-up starts at the target's own digit
   count, so nothing reflows while it counts. */
.m-js [data-count] { font-variant-numeric: lining-nums tabular-nums; }

/* The dialog arrives rather than appearing. */
.m-js:not(.m-still) .project-dialog[open] { animation: dialog-in .55s var(--m-ease); }
.m-js:not(.m-still) .project-dialog[open]::backdrop { animation: backdrop-in .5s ease; }
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(28px) scale(.985); }
  to { opacity: 1; transform: none; }
}
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }

/* A visitor can switch the preference on mid-visit; .m-still only reflects
   what was true at load, so the query itself has the final word. */
@media (prefers-reduced-motion: reduce) {
  .intro { display: none !important; }
  .m-line { overflow: visible; }
  [data-split] .m-w { transform: none !important; }
}
