/* ============================================================
   Tochene — motion layer
   Loaded on every page. Everything here is progressive enhancement:
   it only applies once JS has added `.js` to <html>, and the whole
   file collapses to "final state" under prefers-reduced-motion.

   Deliberately NO `overflow-x: hidden` anywhere: every reveal offset
   stays inside the gutter, so the layout is genuinely free of
   horizontal overflow rather than hiding one. build/diag.mjs proves it.
   ============================================================ */

/* Anchor offsets so the sticky header never covers a jumped-to heading */
html {
  scroll-padding-top: calc(var(--header-h-scrolled) + 1rem);
}

/* ============================================================
   Scroll progress bar
   ============================================================ */
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: var(--z-progress);
  pointer-events: none;
  background: transparent;
}

.scroll-progress__bar {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--brass-600), var(--brass-300));
  transform-origin: left;
}

/* ============================================================
   Scroll reveals
   ============================================================ */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--d, 0ms);
  will-change: opacity, transform;
}

/* Directional variants drift inward by 14px. The horizontal component is
   deliberately smaller than the smallest gutter (20px), so a pre-reveal
   element can never reach past the viewport edge and create a horizontal
   scrollbar — no overflow-x band-aid required. */
.js [data-reveal='left'] {
  transform: translate3d(-14px, 22px, 0);
}

.js [data-reveal='right'] {
  transform: translate3d(14px, 22px, 0);
}

.js [data-reveal='zoom'] {
  transform: scale(0.94);
}

.js [data-reveal='blur'] {
  filter: blur(8px);
  transform: translateY(18px);
}

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

/* ============================================================
   Masked headline reveal
   Each .line is clipped and its inner span rises into place.
   ============================================================ */
.js .reveal-lines .line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.06em;
}

.js .reveal-lines .line > span {
  display: block;
  transform: translateY(105%);
  transition: transform 1000ms var(--ease-out);
  transition-delay: var(--d, 0ms);
}

.js .reveal-lines.is-in .line > span {
  transform: none;
}

/* ============================================================
   Hero parallax
   JS writes --parallax (px) from the scroll position.
   ============================================================ */
.js .hero__media {
  transform: translate3d(0, var(--parallax, 0px), 0);
}

/* ============================================================
   Counters
   ============================================================ */
.stat__value [data-count] {
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   Card tilt — JS writes --tilt-x / --tilt-y in degrees
   ============================================================ */
@media (pointer: fine) {
  .js .tilt {
    transform: perspective(900px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
    transition: transform var(--dur-slow) var(--ease-out);
  }

  .js .tilt:hover {
    transition-duration: var(--dur-fast);
  }
}

/* ============================================================
   Magnetic buttons — JS writes --mag-x / --mag-y in px
   ============================================================ */
@media (pointer: fine) {
  .js .magnetic {
    transform: translate3d(var(--mag-x, 0px), var(--mag-y, 0px), 0);
  }

  .js .magnetic:hover {
    transform: translate3d(var(--mag-x, 0px), calc(var(--mag-y, 0px) - 2px), 0);
  }
}

/* ============================================================
   Cursor spotlight — a soft brass light that follows the pointer
   across dark hero sections. Desktop pointers only.
   ============================================================ */
.spotlight {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    340px circle at var(--spot-x, 50%) var(--spot-y, 50%),
    rgba(227, 201, 138, 0.16),
    transparent 62%
  );
  transition: opacity var(--dur-slow) var(--ease-out);
}

@media (pointer: fine) {
  .js .spotlight {
    opacity: 1;
  }
}

/* ============================================================
   Page transition — a brief fade-up on first paint
   ============================================================ */
.js body {
  opacity: 0;
  transition: opacity 300ms var(--ease-out);
}

.js body.is-ready {
  opacity: 1;
}

/* ============================================================
   Image blur-up placeholders
   .img-lqip gets a tiny inline background; the real image fades in
   over it once decoded, so nothing pops in from empty space.

   The fade is an ANIMATION, not a transition, and deliberately so.
   This rule matches nearly every image on the site; declaring
   `transition: opacity ...` here silently replaced the transform
   transition that hover zoom depends on (same specificity, and this
   file loads last), so cards snapped to their zoomed scale instead of
   gliding. An animation composes with transitions instead of
   overwriting them.
   ============================================================ */
.img-lqip {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.img-lqip > img {
  opacity: 0;
}

.img-lqip.is-loaded > img {
  opacity: 1;
  animation: lqip-fade 700ms var(--ease-out) both;
}

@keyframes lqip-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ============================================================
   Header link underline sweep already lives in layout.css;
   here we add the reveal for the mobile panel backdrop.
   ============================================================ */
.nav-panel__scrim {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-overlay) - 1);
  background: rgba(7, 15, 24, 0.4);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s linear var(--dur-base);
}

.nav-panel__scrim.is-open {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* ============================================================
   Reduced motion — everything lands in its final state
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js [data-reveal='left'],
  .js [data-reveal='right'],
  .js [data-reveal='zoom'],
  .js [data-reveal='blur'] {
    opacity: 1;
    transform: none;
    filter: none;
  }

  .js .reveal-lines .line > span {
    transform: none;
  }

  .js .hero__media {
    transform: none;
  }

  .hero__media.is-kenburns img {
    animation: none;
  }

  .marquee__track {
    animation: none;
  }

  .js body {
    opacity: 1;
  }

  .js .spotlight {
    display: none;
  }

  .img-lqip > img {
    opacity: 1;
  }

  .img-lqip.is-loaded > img {
    animation: none;
  }
}
