/* ============================================================
   Sprinturf Redesign - Motion utilities
   Garner Sites demo build, 2026-04-29

   Scroll-reveal start states, marquee timing, micro-interaction
   classes (lift, tilt-3d, magnetic), page-load fade-in, shimmer
   skeleton.

   Editorial restraint: motion has weight and intent. No bounce,
   no elastic, no consumer-app springiness. ease-out and
   ease-out-expo carry the load.

   The full prefers-reduced-motion short-circuit at the bottom
   reduces every keyframe and transform to zero - users with
   the OS preference set will see no motion.
   ============================================================ */

@import url("./tokens.css");

/* ============================================================
   Page-load fade-in
   Plays once on initial paint. The body adds .has-loaded after
   first paint (in JS); until then everything is visible by
   default but the .page-fade utility opts elements in.
   ============================================================ */
.page-fade {
  opacity: 0;
  transform: translateY(8px);
  animation: page-fade-in 200ms var(--ease-out) forwards;
  animation-delay: 80ms;
}
@keyframes page-fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   Scroll-reveal utilities
   Start state lives in CSS. The motion.js IntersectionObserver
   adds the class .is-visible to trigger the end state.
   Default duration: --t-reveal (800ms ease-out-expo).
   ============================================================ */

/* fade-up: rise from below + fade in */
.scroll-fade-up {
  opacity: 0;
  transform: translate3d(0, 32px, 0);
  transition:
    opacity var(--t-reveal),
    transform var(--t-reveal);
  will-change: opacity, transform;
}
.scroll-fade-up.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* fade-in: opacity only (for stationary text blocks) */
.scroll-fade-in {
  opacity: 0;
  transition: opacity var(--t-reveal);
}
.scroll-fade-in.is-visible { opacity: 1; }

/* fade-left / fade-right: directional reveal for adjacent grids */
.scroll-fade-left {
  opacity: 0;
  transform: translate3d(-32px, 0, 0);
  transition:
    opacity var(--t-reveal),
    transform var(--t-reveal);
}
.scroll-fade-left.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
.scroll-fade-right {
  opacity: 0;
  transform: translate3d(32px, 0, 0);
  transition:
    opacity var(--t-reveal),
    transform var(--t-reveal);
}
.scroll-fade-right.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* zoom-in: subtle 0.97 -> 1 scale + opacity */
.scroll-zoom-in {
  opacity: 0;
  transform: scale(0.97);
  transition:
    opacity var(--t-reveal),
    transform var(--t-reveal);
  will-change: opacity, transform;
}
.scroll-zoom-in.is-visible {
  opacity: 1;
  transform: scale(1);
}

/* clip-reveal: image-curtain wipe, useful for hero photography */
.scroll-clip-reveal {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1100ms var(--ease-out-expo);
  will-change: clip-path;
}
.scroll-clip-reveal.is-visible { clip-path: inset(0 0 0 0); }

/* ============================================================
   Stagger children
   Apply to a parent with .scroll-stagger-children. Direct
   children inherit the start state. JS triggers the parent's
   .is-visible class and each child uses transition-delay
   based on its --i custom property.
   ============================================================ */
.scroll-stagger-children > * {
  opacity: 0;
  transform: translate3d(0, 24px, 0);
  transition:
    opacity 700ms var(--ease-out-expo),
    transform 700ms var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 90ms);
  will-change: opacity, transform;
}
.scroll-stagger-children.is-visible > * {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* ============================================================
   Marquee keyframes
   The .marquee component selector lives in components.css.
   This file owns the animation keyframes and reduced-motion
   short-circuit.
   ============================================================ */
@keyframes motion-marquee {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}
.motion-marquee-fast .marquee-track { animation-duration: 24s; }
.motion-marquee-slow .marquee-track { animation-duration: 60s; }

/* ============================================================
   .lift - subtle hover lift with shadow upgrade
   Use on cards, tiles, anything that should feel "pickable".
   ============================================================ */
.lift {
  transition:
    transform var(--t-slow) var(--ease-out),
    box-shadow var(--t-slow) var(--ease-out);
  will-change: transform;
}
.lift:hover {
  transform: translate3d(0, -2px, 0);
  box-shadow: var(--shadow-card-hover);
}

/* ============================================================
   .tilt-3d - perspective-ready container for JS tilt
   The motion.js mousemove handler writes rotateX/rotateY to
   the --tilt-x and --tilt-y custom properties.
   ============================================================ */
.tilt-3d {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  perspective: 1200px;
  transform-style: preserve-3d;
  transition: transform 480ms var(--ease-out);
}
.tilt-3d > .tilt-3d-inner {
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 100ms linear;
  transform-style: preserve-3d;
  will-change: transform;
}

/* ============================================================
   .magnetic - cursor-magnetic hover utility
   The motion.js mousemove handler writes --magnet-x and
   --magnet-y. Translation capped at 4px for restraint.
   ============================================================ */
.magnetic {
  --magnet-x: 0;
  --magnet-y: 0;
  display: inline-block;
  transform: translate3d(
    calc(var(--magnet-x) * 4px),
    calc(var(--magnet-y) * 4px),
    0
  );
  transition: transform 240ms var(--ease-out);
  will-change: transform;
}

/* ============================================================
   .shimmer - skeleton loader image placeholder
   Lightweight gradient sweep, no JS required. The element
   renders the shimmer behind whatever child content (or none).
   ============================================================ */
.shimmer {
  position: relative;
  overflow: hidden;
  background: linear-gradient(
    90deg,
    var(--paper-100) 0%,
    var(--paper-200) 40%,
    var(--paper-100) 80%
  );
  background-size: 200% 100%;
  animation: shimmer-sweep 1600ms linear infinite;
}
@keyframes shimmer-sweep {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

/* ============================================================
   FLIP animation classes
   For JS-orchestrated FLIP transitions. .flip-target marks
   the element as a measurement target; JS reads its bounding
   rect, applies an inverse transform with .flip-inverted, then
   removes the class to play the animation back.
   ============================================================ */
.flip-target {
  transition:
    transform 460ms var(--ease-out-expo),
    opacity 460ms var(--ease-out-expo);
  will-change: transform;
}
.flip-inverted {
  transition: none !important;
}

/* ============================================================
   Hover utilities
   Small composable hover behaviors.
   ============================================================ */

/* .hover-zoom-img - scale image inside a fixed container */
.hover-zoom-img {
  overflow: hidden;
}
.hover-zoom-img img {
  transition: transform 700ms var(--ease-out-expo);
  will-change: transform;
}
.hover-zoom-img:hover img { transform: scale(1.05); }

/* .hover-underline-grow - link with growing underline */
.hover-underline-grow {
  position: relative;
  text-decoration: none;
}
.hover-underline-grow::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t) var(--ease-out);
}
.hover-underline-grow:hover::after { transform: scaleX(1); }

/* ============================================================
   prefers-reduced-motion: reduce
   Hard short-circuit. Any user who has set this OS preference
   gets zero motion. Animations still complete (so JS-triggered
   class changes still resolve to end state) but with no
   transition or transform interpolation.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* Reveal utilities resolve immediately to end state */
  .scroll-fade-up,
  .scroll-fade-in,
  .scroll-fade-left,
  .scroll-fade-right,
  .scroll-zoom-in,
  .scroll-clip-reveal,
  .scroll-stagger-children > *,
  .page-fade {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }

  /* Marquee stops */
  .marquee-track,
  .motion-marquee-fast .marquee-track,
  .motion-marquee-slow .marquee-track {
    animation: none !important;
  }

  /* Hover transforms cancel */
  .lift:hover,
  .tilt-3d > .tilt-3d-inner,
  .magnetic,
  .hover-zoom-img:hover img,
  .card:hover,
  .card-product:hover,
  .card-product:hover .card-product-img,
  .card-project:hover,
  .card-project:hover img {
    transform: none !important;
  }

  /* Shimmer stops to a static neutral */
  .shimmer { animation: none !important; background: var(--paper-100) !important; }
}
