/* Reveal primitives. JS only toggles .is-in; everything else is CSS so the
   compositor handles it and the main thread stays free. */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  transition-delay: var(--reveal-delay, 0s);
  will-change: opacity, transform;
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
}

[data-reveal="fade"] { transform: none; }

[data-reveal="mask"] {
  opacity: 1;
  transform: translate3d(0, 100%, 0);
}

.mask {
  overflow: hidden;
  display: block;
}

[data-split] .char {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, .5em, 0);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
  transition-delay: calc(var(--i, 0) * 8ms);
}

[data-split].is-in .char {
  opacity: 1;
  transform: none;
}

[data-reveal].is-in,
[data-split].is-in .char {
  will-change: auto;
}

/* Populated by js/animations.js only — a [data-kinetic] element is plain
   visible text until that script runs, so there's no CSS-only hidden state
   here to leave stuck if the anime.js CDN import ever fails. */
.kinetic__mask { display: inline-block; overflow: hidden; vertical-align: top; }
.kinetic__word { display: inline-block; will-change: transform, opacity; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  [data-reveal],
  [data-split] .char,
  .nav-menu .nav-link {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}
