/* ==========================================================================
   IGNC — Effects: keyframes, reveal utilities, text animation primitives
   Paired with effects/reveal.js and effects/tilt.js
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Keyframes
   -------------------------------------------------------------------------- */

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(26px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes float-soft {
  0%,
  100% {
    transform: translateY(0) rotate(0deg);
  }
  50% {
    transform: translateY(-16px) rotate(1.4deg);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes pulse-glow {
  0%,
  100% {
    opacity: 0.55;
    transform: scale(1);
  }
  50% {
    opacity: 0.95;
    transform: scale(1.07);
  }
}

@keyframes ping {
  0% {
    box-shadow: 0 0 0 0 rgba(245, 166, 35, 0.65);
  }
  70% {
    box-shadow: 0 0 0 9px rgba(245, 166, 35, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(245, 166, 35, 0);
  }
}

@keyframes gradient-pan {
  to {
    background-position: 200% 50%;
  }
}

@keyframes grid-pan {
  to {
    background-position: 64px 64px;
  }
}

@keyframes marquee {
  to {
    transform: translateX(-100%);
  }
}

@keyframes scroll-bead {
  0% {
    transform: translateY(-100%);
  }
  100% {
    transform: translateY(340%);
  }
}

@keyframes flip-in {
  0% {
    opacity: 0.25;
    transform: translateY(-45%) scale(0.9);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes orb-drift-a {
  0% {
    transform: translate(0, 0) scale(1);
  }
  100% {
    transform: translate(16%, 14%) scale(1.18);
  }
}

@keyframes orb-drift-b {
  0% {
    transform: translate(0, 0) scale(1.1);
  }
  100% {
    transform: translate(-14%, -12%) scale(0.92);
  }
}

@keyframes orb-drift-c {
  0% {
    transform: translate(0, 0) scale(0.9);
  }
  100% {
    transform: translate(-18%, 16%) scale(1.2);
  }
}

@keyframes twinkle {
  0%,
  100% {
    opacity: 0.12;
    transform: scale(0.7);
  }
  50% {
    opacity: 0.9;
    transform: scale(1);
  }
}

@keyframes word-rise {
  from {
    opacity: 0;
    transform: translateY(100%) rotate(4deg);
  }
  to {
    opacity: 1;
    transform: translateY(0) rotate(0deg);
  }
}

@keyframes word-rise-soft {
  from {
    opacity: 0;
    transform: translateY(40%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes char-pop {
  0% {
    opacity: 0;
    transform: translateY(28px) scale(0.86);
    filter: blur(6px);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
    filter: blur(0);
  }
}

@keyframes underline-grow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/* --------------------------------------------------------------------------
   2. Scroll reveal — elements start hidden, .is-in is added by reveal.js
   -------------------------------------------------------------------------- */

[data-reveal] {
  opacity: 0;
  transition: opacity 820ms var(--ease-out), transform 820ms var(--ease-out),
    clip-path 900ms var(--ease-out), filter 820ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

[data-reveal="up"] {
  transform: translateY(38px);
}

[data-reveal="down"] {
  transform: translateY(-38px);
}

[data-reveal="left"] {
  transform: translateX(-46px);
}

[data-reveal="right"] {
  transform: translateX(46px);
}

[data-reveal="zoom"] {
  transform: scale(0.9);
}

[data-reveal="blur"] {
  filter: blur(14px);
  transform: translateY(22px);
}

[data-reveal="flip"] {
  transform: perspective(1200px) rotateX(-18deg) translateY(30px);
  transform-origin: top center;
}

/* Image wipe — the frame unmasks, the picture inside settles back to 1:1.
   The clip is applied to the CHILD, never to the observed element itself:
   IntersectionObserver intersects against the target's own clip region, so a
   fully clipped target reports a ratio of 0, never crosses the threshold,
   and would stay invisible forever. */
[data-reveal="wipe"],
[data-reveal="curtain"] {
  opacity: 1;
}

[data-reveal="wipe"] > * {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 900ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal="wipe"].is-in > * {
  clip-path: inset(0 0 0 0);
}

[data-reveal="wipe"] img {
  transform: scale(1.22);
  transition: transform 1200ms var(--ease-out);
}

[data-reveal="wipe"].is-in img {
  transform: scale(1);
}

/* Curtain from the left */
[data-reveal="curtain"] > * {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 900ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal="curtain"].is-in > * {
  clip-path: inset(0 0 0 0);
}

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

/* Stagger helper — set on a parent, children inherit an index-based delay. */
[data-stagger] > * {
  --reveal-delay: calc(var(--i, 0) * var(--stagger-step, 90ms));
}

/* --------------------------------------------------------------------------
   3. Text animations
   -------------------------------------------------------------------------- */

/* split.js wraps each word in .word > .word__inner */
.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}

/* Wrappers around background-clip:text content must not clip — see
   split-text.js. The rise still reads without the mask. */
.word--noclip {
  overflow: visible;
}

.word__inner {
  display: inline-block;
  opacity: 0;
  transform: translateY(100%) rotate(4deg);
}

.word--noclip > .word__inner {
  transform: translateY(40%) rotate(0deg);
}

.is-split-in .word__inner {
  animation: word-rise 900ms var(--ease-out) forwards;
  animation-delay: var(--word-delay, 0ms);
}

/* Unclipped words rise a shorter distance, since there is no mask edge to
   hide the travel. */
.is-split-in .word--noclip > .word__inner {
  animation-name: word-rise-soft;
}

/* Character-level pop, used sparingly on short headings. */
.char {
  display: inline-block;
  opacity: 0;
  white-space: pre;
}

.is-split-in .char {
  animation: char-pop 620ms var(--ease-out) forwards;
  animation-delay: var(--char-delay, 0ms);
}

/* Continuously drifting gradient on emphasised words. */
.grad-text--anim {
  animation: gradient-pan 7s linear infinite alternate;
}

/* Hand-drawn style underline that draws itself when revealed. */
.mark-under {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}

.mark-under::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.12em;
  height: 0.32em;
  border-radius: 0.16em;
  background: linear-gradient(
    90deg,
    rgba(245, 166, 35, 0.55),
    rgba(138, 43, 226, 0.45)
  );
  transform: scaleX(0);
  transform-origin: left center;
  z-index: -1;
}

/* .is-in comes from reveal.js, .is-split-in from split-text.js — every
   .mark-under currently sits inside a [data-split] heading, so without the
   second selector the underline would stay at scaleX(0) forever. */
.mark-under.is-in::after,
.is-in .mark-under::after,
.is-split-in .mark-under::after {
  animation: underline-grow 760ms var(--ease-out) 260ms forwards;
}

/* --------------------------------------------------------------------------
   4. Decorative bits
   -------------------------------------------------------------------------- */

.star {
  position: absolute;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #fff;
  animation: twinkle 4s var(--ease-in-out) infinite;
}

/* Applies a slow parallax offset driven by parallax.js */
[data-parallax] {
  will-change: transform;
}

/* Magnetic buttons (magnetic.js sets --mag-x / --mag-y) */
[data-magnetic] {
  transform: translate(var(--mag-x, 0), var(--mag-y, 0));
}

/* --------------------------------------------------------------------------
   5. Reduced motion — strip everything decorative
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  [data-reveal],
  [data-reveal="wipe"],
  [data-reveal="curtain"],
  [data-reveal="wipe"] > *,
  [data-reveal="curtain"] > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }

  .word__inner,
  .char {
    opacity: 1 !important;
    transform: none !important;
  }

  .mark-under::after {
    transform: scaleX(1);
  }

  [data-magnetic] {
    transform: none !important;
  }
}
