/* Entrance animations, keyframes, motion preferences */

/* Scroll reveal — elements start hidden, JS adds .is-visible.
   The entrance runs as an ANIMATION with fill-mode backwards, not a transition:
   a transition here would occupy the element's `transition` property and cancel
   the hover transitions its own component stylesheet defines. Once the animation
   finishes nothing is left applied, so hover transforms work normally. */
[data-reveal] { opacity: 0; }
[data-reveal].is-visible {
  opacity: 1;
  animation: reveal-up .85s var(--ease) var(--reveal-delay, 0ms) backwards;
}
[data-reveal="left"].is-visible  { animation-name: reveal-left; }
[data-reveal="right"].is-visible { animation-name: reveal-right; }
[data-reveal="zoom"].is-visible  { animation-name: reveal-zoom; }

/* The mask variant clips the image, not the observed box — IntersectionObserver
   treats a fully clipped target as not intersecting, so it would never reveal. */
[data-reveal="mask"] { opacity: 1; }
[data-reveal="mask"].is-visible { animation: none; }
[data-reveal="mask"] img { clip-path: inset(0 0 100% 0); transition: clip-path var(--dur-slow) var(--ease-in-out); transition-delay: var(--reveal-delay, 0ms); }
[data-reveal="mask"].is-visible img { clip-path: inset(0 0 0 0); }

@keyframes fade-in     { from { opacity: 0; } to { opacity: 1; } }
@keyframes reveal-up    { from { opacity: 0; transform: translateY(34px); } to { opacity: 1; transform: none; } }
@keyframes reveal-left  { from { opacity: 0; transform: translateX(-40px); } to { opacity: 1; transform: none; } }
@keyframes reveal-right { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: none; } }
@keyframes reveal-zoom  { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: none; } }

/* Hero entrance (plays immediately, no observer) */
.hero [data-enter] { animation: enter-up .95s var(--ease) var(--enter-delay, 0ms) backwards; }
.hero__media [data-enter] { animation-name: enter-scale; }

@keyframes enter-up { from { opacity: 0; transform: translateY(38px); } to { opacity: 1; transform: none; } }
@keyframes enter-scale { from { opacity: 0; transform: scale(.94) translateY(24px); } to { opacity: 1; transform: none; } }
@keyframes underline-in { to { transform: scaleX(1); } }
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes float-y { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-16px); } }
@keyframes sway { 0%, 100% { transform: rotate(var(--sway-from, -6deg)); } 50% { transform: rotate(var(--sway-to, 6deg)); } }
@keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 rgba(31, 154, 114, .45); } 70% { box-shadow: 0 0 0 18px rgba(31, 154, 114, 0); } 100% { box-shadow: 0 0 0 0 rgba(31, 154, 114, 0); } }
@keyframes spin-slow { to { transform: rotate(360deg); } }

.float { animation: float-y 6s var(--ease-in-out) infinite; }
.float--slow { animation-duration: 9s; }
.float--delay { animation-delay: -3s; }
.leaf--sway svg { animation: sway 11s var(--ease-in-out) infinite; transform-origin: top center; }
.pulse { animation: pulse-ring 2.6s ease-out infinite; }

/* Parallax targets get --parallax-y from JS; each element composes it into its own
   transform so a base rotation is not thrown away. */
[data-parallax] { will-change: transform; }

/* Image transitions live here and ONLY here.
   `transition` is a single property: whichever stylesheet declares it last replaces the
   whole list. This rule loads after the component sheets, so if it named only opacity it
   would silently delete every hover zoom on the page. Both belong in one declaration. */
.frame img,
.service__media img,
.gallery__item img,
.hero__card img {
  opacity: 0;
  transition: opacity .7s var(--ease), transform 1.1s var(--ease-in-out);
}
.frame img.is-loaded,
.service__media img.is-loaded,
.gallery__item img.is-loaded,
.hero__card img.is-loaded { opacity: 1; }

/* ---------------------------------------------------------------------------
   Reduced motion.

   The setting exists for people who get motion sick, and it is what Windows 11
   turns on when "Animation effects" is switched off. It means "stop moving
   things", not "make the page dead": drifting, looping, parallax and slide-in
   entrances all stop, while cross-fades and pointer feedback stay — a fade or a
   colour change does not trigger vestibular symptoms.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  /* Anything that moves by itself: off. */
  .marquee__track, .float, .leaf--sway svg, .pulse, .hero__title em::after { animation: none !important; }
  .hero__title em::after { transform: scaleX(1); }
  [data-parallax] { --parallax-y: 0px !important; }

  /* Entrances become a plain fade — no travel, page still reads as alive.
     Kept as an animation (not a transition) so component hover transitions survive. */
  [data-reveal].is-visible { animation: fade-in .5s linear var(--reveal-delay, 0ms) backwards !important; }
  .hero [data-enter], .hero__card--one, .hero__card--two { animation: fade-in .6s linear var(--enter-delay, 0ms) backwards !important; }
  [data-reveal="mask"] img { clip-path: none !important; transition: none !important; }

  /* Pointer feedback stays — it is small, user-initiated and ends when the pointer
     leaves — but it settles quickly so nothing drifts under the cursor. */
  :root { --dur-fast: .12s; --dur: .18s; --dur-slow: .24s; }

  /* Testimonials stop sliding and become a plain stack; the JS clones are hidden. */
  .carousel__viewport { overflow: visible; }
  .carousel__track { flex-wrap: wrap; transform: none !important; transition: none !important; }
  .carousel__slide { flex-basis: 50%; margin-block: .5rem; }
  .carousel__slide[aria-hidden="true"] { display: none; }
  .carousel__nav { display: none; }
}
