


.promise .ln { display: inline; }
@media (min-width: 901px) { .promise .ln { display: block; } }

@media (min-width: 901px) {
  /* Headline: mask reveal plus a 14px upward travel, 60ms per line,
     complete at 120 + 400 = 520ms. */
  .js.intro .promise .ln {
    animation: line-in 400ms var(--e-out) backwards;
    animation-delay: calc(var(--i, 0) * 60ms);
  }
  .promise .ln:nth-child(1) { --i: 0; }
  .promise .ln:nth-child(2) { --i: 1; }
  .promise .ln:nth-child(3) { --i: 2; }

  /* Eyebrow: the rule draws, then the label resolves — 300ms total. */
  .js.intro .eyebrow { animation: label-in 180ms var(--e-out) 120ms backwards; }
  .js.intro .eyebrow::before { animation: rule-draw 200ms var(--e-signal) backwards; transform-origin: left; }

  /* Lede and metadata resolve after the headline, inside 320ms. */
  .js.intro .hero__lede { animation: soft-up 320ms var(--e-out) 470ms backwards; }
  .js.intro .hero__meta { animation: soft-up 320ms var(--e-out) 540ms backwards; }

  /* The light world follows one beat later. */
  .js.intro .hero__statement { animation: soft-up 340ms var(--e-out) 260ms backwards; }
  .js.intro .hero__sub { animation: soft-up 320ms var(--e-out) 380ms backwards; }
  /* CTAs appear together and never move: opacity only. */
  .js.intro .hero__actions { animation: fade-only 300ms var(--e-out) 430ms backwards; }
  .js.intro .hero__stack { animation: fade-only 340ms var(--e-out) 520ms backwards; }
}

/* The gate must come after every `animation` shorthand above: the shorthand
   resets animation-play-state, so declaring the pause earlier would let the
   sequence run at parse time and finish before the loader hands over. */
.js.intro :is(.promise .ln, .eyebrow, .hero__lede, .hero__meta, .hero__actions,
               .hero__statement, .hero__sub, .hero__stack),
.js.intro .eyebrow::before { animation-play-state: paused; }
.js.intro.intro-go :is(.promise .ln, .eyebrow, .hero__lede, .hero__meta,
                        .hero__actions, .hero__statement, .hero__sub, .hero__stack),
.js.intro.intro-go .eyebrow::before { animation-play-state: running; }

@keyframes line-in {
  from { clip-path: inset(0 0 100% 0); translate: 0 14px; }
  to   { clip-path: inset(-.16em 0 -.24em 0); translate: 0 0; }
}
@keyframes rule-draw { from { scale: 0 1; } to { scale: 1 1; } }
@keyframes label-in { from { opacity: 0; translate: 0 4px; } to { opacity: 1; translate: 0 0; } }
@keyframes soft-up { from { opacity: 0; translate: 0 10px; } to { opacity: 1; translate: 0 0; } }
@keyframes fade-only { from { opacity: 0; } to { opacity: 1; } }

/* One controlled light sweep across the cyan phrase. Runs once, ends on solid
   brand cyan, and the colour is inherited if the animation never runs. */
@media (min-width: 901px) {
  .js.intro.intro-go .promise em {
    background-image: linear-gradient(100deg,
      var(--cyan-400) 0 42%, #eafcff 50%, var(--cyan-400) 58% 100%);
    background-size: 320% 100%;
    background-position: 100% 0;
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: sweep 900ms var(--e-out) 480ms 1 forwards;
  }
}
@keyframes sweep {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

/* ============================ 4 · SECTION TITLE TRANSITIONS ============================ */
/* Retuned into the authorized band: eyebrow leads by 70ms, the heading takes a
   380ms mask/up reveal, the paragraph follows and completes inside 300ms.
   The reveal observer unobserves on first entry, so scrolling back never
   re-animates anything. */
.js .stagger.is-in > * { transition: none; }
.js .stagger > *:nth-child(1) { opacity: 1; translate: none; }
.js .stagger.is-in > *:nth-child(1) { animation: label-in 200ms var(--e-out) backwards; }
.js .stagger.is-in > *:nth-child(2) { animation: title-in 380ms var(--e-out) 70ms backwards; }
.js .stagger.is-in > *:nth-child(3) { animation: soft-up 300ms var(--e-out) 300ms backwards; }
.js .stagger > *:nth-child(n + 2) { opacity: 0; }
.js .stagger.is-in > *:nth-child(n + 2) { opacity: 1; }

@keyframes title-in {
  from { clip-path: inset(0 0 100% 0); translate: 0 16px; }
  to   { clip-path: inset(-.16em 0 -.24em 0); translate: 0 0; }
}

/* ============================ 7.1 · FALLBACKS ============================ */
/* Mobile and tablet portrait: simplified, no pinning, and above all no content
   delay — the copy is present at first paint and only the eyebrow rule draws. */
@media (max-width: 900px) {
  .js.intro :is(.promise .ln, .hero__lede, .hero__meta, .hero__actions,
                 .hero__statement, .hero__sub, .hero__stack) { animation: none; opacity: 1; }
  .hero__planes { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js.intro :is(.promise .ln, .eyebrow, .hero__lede, .hero__meta, .hero__actions,
                 .hero__statement, .hero__sub, .hero__stack),
  .js.intro .eyebrow::before,
  .js .stagger.is-in > * { animation: none; opacity: 1; translate: none; clip-path: none; scale: 1; }
  .js.intro.intro-go .promise em { background-image: none; -webkit-text-fill-color: currentColor; animation: none; }
  .hero__planes .plane { transform: none; transition: none; }
  .cinema .drift .hero__planes .plane-shell,
  .cinema .drift .logic-pulse,
  .cinema .drift .logic-flow,
  .cinema .drift .logic-foci .logic-spark { animation: none; }
  .logic-pulse, .logic-flow { opacity: 0; }
  .logic-foci .logic-spark { opacity: .72; transform: none; }
  .js.intro .logic-code { animation: none; opacity: var(--code-o); transform: none; }
}
