/* Motion library.
   docs/LANDING-DESIGN-LANGUAGE.md §5.5 and §8.

   The first build of this page had one entrance (fade-up) and one loop. That
   reads as a template. Measuring the reference sites' stylesheets showed why:
   Rex declares 44 keyframes and runs 12 infinite loops, Avoca 34 keyframes,
   Gravy 11 keyframes with 8 mask-faded scrolling regions. The ones that are
   quiet in CSS — Skymerse, RealPact, Zaplar — are carrying the page with video
   and photography instead. Every site does one or the other, hard.

   We cannot shoot footage, so this file is the other route: many small loops
   living inside the synthetic product mocks, plus a varied entrance set so no
   two bands arrive the same way. */

/* ══════════════════════════════════════════════════ ambient loops ═══ */

/* A status dot that breathes. Goes on anything reporting live state. */
@keyframes pulse-dot {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }
  50% { opacity: 0.55; box-shadow: 0 0 0 3px rgba(0, 0, 0, 0); }
}

/* Gauge fills drawing to their value when a maintenance panel opens, the way a
   real dashboard paints on first render. The width is already correct in the
   markup; this scales it in, so nothing reflows. */
@keyframes gauge-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* Infinite marquee. Track holds two identical halves; -50% loops seamlessly. */
@keyframes marquee {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

/* Slow drift for atmospheric layers. Gravy runs four of these at 95s–200s with
   negative delays so they never sync up. */
@keyframes drift {
  from { transform: translate3d(-4%, 0, 0); }
  to { transform: translate3d(4%, 0, 0); }
}

/* A light sweeping across a surface, once every long while. */
@keyframes sweep {
  0% { transform: translateX(-120%) skewX(-14deg); }
  100% { transform: translateX(320%) skewX(-14deg); }
}

@keyframes blink {
  0%, 45% { opacity: 1; }
  46%, 100% { opacity: 0; }
}

/* ═════════════════════════════════════════════════════ entrances ═══ */

@keyframes rise {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: none; }
}

/* Card deals in with a touch of rotation, so a grid of cards does not arrive
   as one flat sheet. */
@keyframes deal {
  from { opacity: 0; transform: translateY(26px) rotate(-1.2deg) scale(0.985); }
  to { opacity: 1; transform: none; }
}

@keyframes pop {
  from { opacity: 0; transform: scale(0.94); }
  60% { opacity: 1; transform: scale(1.012); }
  to { opacity: 1; transform: none; }
}

/* Wipe: the text is revealed by a moving edge rather than faded. Used on
   display headings, one line at a time. */
@keyframes wipe {
  from { clip-path: inset(0 0 108% 0); transform: translateY(0.14em); }
  to { clip-path: inset(0 0 -12% 0); transform: none; }
}

@keyframes slide-left {
  from { opacity: 0; transform: translateX(28px); }
  to { opacity: 1; transform: none; }
}

/* ═══════════════════════════════════════════════ applied classes ═══ */

.is-in .anim,
.anim.is-in {
  animation-duration: 720ms;
  animation-timing-function: var(--ease-out);
  animation-fill-mode: both;
  animation-delay: var(--d, 0ms);
}

.anim--rise.is-in { animation-name: rise; }
.anim--deal.is-in { animation-name: deal; }
.anim--pop.is-in { animation-name: pop; }
.anim--slide.is-in { animation-name: slide-left; }

.anim:not(.is-in) { opacity: 0; }

/* Line-by-line wipe on display headings. The span wrapper is added by
   scripts/reveal.js so the markup stays plain text. */
.wipe-line {
  display: block;
  overflow: hidden;
}

.wipe-line > i {
  display: block;
  font-style: inherit;
  clip-path: inset(0 0 108% 0);
  transform: translateY(0.14em);
}

.is-in .wipe-line > i {
  animation: wipe 760ms var(--ease-out) both;
  animation-delay: calc(var(--line, 0) * 90ms + var(--d, 0ms));
}

/* ── loop utilities ─────────────────────────────────────────────────── */

.loop-dot {
  animation: pulse-dot 1.9s var(--ease-io) infinite;
}

.loop-blink {
  animation: blink 1.05s steps(1) infinite;
}

/* The gauges draw in when their feature band reveals. Scaled over the width
   already in the markup, so nothing reflows and the values stay correct if the
   animation never runs. */
.feat__vis.is-in .gauge__fill {
  transform-origin: left;
  animation: gauge-fill 720ms var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 90ms);
}

.loop-sweep {
  position: relative;
  overflow: hidden;
}

/* A slow specular pass over a panel. Long interval, low opacity: it should
   register as light in the room, not as a highlight effect. */
.loop-sweep::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, 0.5) 48%,
    transparent 72%
  );
  width: 42%;
  animation: sweep 7.5s var(--ease-io) infinite;
  opacity: 0.5;
}

[data-surface="night"] .loop-sweep::after {
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, 0.075) 48%,
    transparent 72%
  );
  opacity: 1;
}

/* ── marquee ────────────────────────────────────────────────────────── */

.marquee {
  position: relative;
  overflow: hidden;
  /* Gravy masks the edges of every scrolling region rather than letting items
     collide with a hard boundary. Eight of them on that page. */
  mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee var(--speed, 46s) linear infinite;
}

.marquee:hover .marquee__track {
  animation-play-state: paused;
}

.marquee__half {
  display: flex;
  align-items: center;
  gap: clamp(28px, 4vw, 56px);
  padding-right: clamp(28px, 4vw, 56px);
}

/* ── atmosphere ─────────────────────────────────────────────────────── */

/* Contour lines, the way Skymerse lays faint isobars across its black bands.
   Drawn, not imported, so they inherit the surface. */
.contours {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: 0.5;
}

.contours svg {
  position: absolute;
  width: 160%;
  height: 100%;
  left: -30%;
}

.contours svg:nth-child(1) { animation: drift 74s var(--ease-io) infinite alternate; }
.contours svg:nth-child(2) { animation: drift 112s var(--ease-io) -30s infinite alternate-reverse; }
.contours svg path {
  fill: none;
  stroke: var(--line);
  stroke-width: 1;
}

/* ── scroll-linked ──────────────────────────────────────────────────── */

/* --p is written by scripts/parallax.js: 0 at the top of the element's travel,
   1 at the bottom. Everything scroll-driven reads from it, so there is one
   rAF loop for the whole page. */
.par {
  will-change: transform;
  transform: translate3d(0, calc(var(--p, 0) * var(--par-y, 0px)), 0)
    scale(calc(1 + var(--p, 0) * var(--par-s, 0)));
}

@media (prefers-reduced-motion: reduce) {
  .loop-dot,
  .loop-blink,
  .loop-sweep::after,
  .feat__vis.is-in .gauge__fill,
  .marquee__track,
  .contours svg,
  .anim,
  .wipe-line > i {
    animation: none !important;
  }

  .anim:not(.is-in),
  .anim {
    opacity: 1 !important;
    transform: none !important;
  }

  .wipe-line > i {
    clip-path: none !important;
    transform: none !important;
  }

  .par {
    transform: none !important;
  }

  .loop-sweep::after {
    display: none;
  }
}
