/* =====================================================================
   Suri & Suri - global animation system
   Pairs with assets/js/animations.js. Tokens live in variables.css
   (--motion-*). Loaded after components.css so it can extend it.

   HOW IT WORKS
   - Motion is opt-in. Add a class to an element and it animates the first
     time it scrolls into view; nothing else on the site moves.
   - Only opacity, transform and clip-path are ever animated, so nothing
     reflows and there is no layout shift.
   - Content is hidden only while the page has confirmed that JavaScript and
     IntersectionObserver work (the "js" class set in <head>). Without them,
     or after a 3.5s failsafe ("reveal-fallback"), everything simply shows.
   - Visitors who ask for reduced motion get none of it: every rule that moves
     something sits inside (prefers-reduced-motion: no-preference).
   - When a reveal finishes JS adds .is-done, which stops these rules
     applying, so each component's own transition (e.g. card hover) takes over.

   CLASSES
     .reveal / .reveal-up   fade + rise            (headings, text blocks)
     .reveal-left / -right  fade + slide in        (two-column layouts; phones rise)
     .reveal-scale          fade + gentle scale-up
     .image-reveal          soft wipe + fade; an <img> child settles from 1.07x to 1x
     .stagger-group         its direct children reveal one after another
     .stagger-item          (added automatically to a group's children)
   Optional per-element delay:  style="--reveal-delay: 120ms"
   Hero intro (load, not scroll):  add .hero--intro to a .hero section.
   ===================================================================== */

/* Horizontal overflow: nothing here can cause it. Phones get no sideways
   movement at all (see variables.css), and from 768px the 28px sideways travel
   is smaller than the 40px page gutter. (A blanket overflow-x clip was tried and
   removed: it changed how some photos rasterize at rest.) */

/* ---------------------------------------------------------------------
   MOTION ALLOWED
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {

  /* ---------- 1. Scroll reveals: the hidden (pre-reveal) state ---------- */

  html.js:not(.reveal-fallback) :is(.reveal, .reveal-up, .stagger-item, .stagger-group > *):not(.is-visible) {
    opacity: 0;
    transform: translate3d(0, var(--motion-distance), 0);
  }

  html.js:not(.reveal-fallback) .reveal-left:not(.is-visible) {
    opacity: 0;
    transform: translate3d(calc(var(--motion-shift-x) * -1), var(--motion-shift-y), 0);
  }

  html.js:not(.reveal-fallback) .reveal-right:not(.is-visible) {
    opacity: 0;
    transform: translate3d(var(--motion-shift-x), var(--motion-shift-y), 0);
  }

  html.js:not(.reveal-fallback) .reveal-scale:not(.is-visible) {
    opacity: 0;
    transform: scale(0.96);
  }

  html.js:not(.reveal-fallback) .image-reveal:not(.is-visible) {
    opacity: 0;
    clip-path: inset(0 0 14% 0);
  }

  html.js:not(.reveal-fallback) .image-reveal:not(.is-visible) :is(img, .image-reveal__media) {
    transform: scale(1.07);
  }

  /* ---------- 2. The reveal itself (from when it is triggered until done) ---------- */

  html.js :is(.reveal, .reveal-up, .reveal-left, .reveal-right, .reveal-scale, .image-reveal, .stagger-item).is-visible:not(.is-done) {
    transition:
      opacity var(--motion-duration) var(--motion-ease),
      transform var(--motion-duration) var(--motion-ease),
      clip-path var(--motion-duration-image) var(--motion-ease);
    /* per-element delay + position in a staggered batch (capped so a long
       list never makes the sequence drag) */
    transition-delay: calc(var(--reveal-delay, 0ms) + min(var(--stagger-i, 0), 5) * var(--motion-stagger));
    will-change: opacity, transform;
  }

  /* clip-path cannot interpolate to `none`, so the wipe ends on an explicit inset */
  html.js .image-reveal.is-visible:not(.is-done) {
    clip-path: inset(0 0 0 0);
  }

  html.js .image-reveal.is-visible:not(.is-done) :is(img, .image-reveal__media) {
    transition: transform var(--motion-duration-image) var(--motion-ease);
    transition-delay: var(--reveal-delay, 0ms);
  }

  /* ---------- 3. Hero intro (plays on load; opt-in with .hero--intro) ---------- */

  /* The photo emerges from a dark veil while the text rises in, top to bottom.
     The veil sits under the text (which gets z-index 1) and its resting state
     is fully transparent. Total sequence is about one second.
     The animations use fill-mode "backwards" (hold the start state during the
     delay) rather than "both": once they finish the elements are released to
     their normal styles instead of staying pinned by a finished animation. */
  .hero--intro::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background: var(--color-ink);
    opacity: 0;
    pointer-events: none;
    animation: hero-veil 1200ms var(--motion-ease) 40ms backwards;
  }

  .hero--intro .hero__inner {
    z-index: 1;
  }

  .hero--intro .hero__inner > :not(.hero__meta) > * {
    animation: hero-rise 800ms var(--motion-ease) backwards;
    animation-delay: calc(120ms + var(--hero-i, 0) * 80ms);
  }

  .hero--intro .hero__inner > :not(.hero__meta) > :nth-child(2) { --hero-i: 1; }
  .hero--intro .hero__inner > :not(.hero__meta) > :nth-child(3) { --hero-i: 2; }
  .hero--intro .hero__inner > :not(.hero__meta) > :nth-child(4) { --hero-i: 3; }
  .hero--intro .hero__inner > :not(.hero__meta) > :nth-child(n + 5) { --hero-i: 4; }

  .hero--intro .hero__meta {
    animation: hero-fade 900ms ease-out 600ms backwards;
  }

  /* ---------- 4. Navigation: underline slides in from the left ---------- */

  @media (min-width: 960px) {
    .main-nav a {
      position: relative;
    }

    .main-nav a::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: -2px;
      height: 2px;
      background: var(--color-ink);
      transform: scaleX(0);
      transform-origin: left center;
      transition: transform var(--motion-hover) var(--motion-ease);
    }

    .main-nav a:hover::after,
    .main-nav a[aria-current="page"]::after {
      transform: scaleX(1);
    }

    /* the underline is now drawn by ::after, not the border */
    .main-nav a:hover,
    .main-nav a[aria-current="page"] {
      border-color: transparent;
    }
  }

  /* ---------- 5. Buttons ---------- */

  .btn {
    transition:
      background var(--transition-fast),
      color var(--transition-fast),
      border-color var(--transition-fast),
      transform var(--transition-fast);
  }
}

@keyframes hero-veil {
  from { opacity: 0.88; }
  to   { opacity: 0; }
}

@keyframes hero-rise {
  from { opacity: 0; transform: translate3d(0, 22px, 0); }
  to   { opacity: 1; transform: none; }
}

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

/* ---------------------------------------------------------------------
   HOVER (mouse / trackpad only, and only when motion is allowed).
   Touch screens skip these, so nothing sticks in a hovered state.
   --------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {

  /* Buttons: a 1px lift on top of the arrow nudge and colour change they already have */
  .btn:hover {
    transform: translateY(-1px);
  }

  .btn:active {
    transform: none;
  }

  /* Service cards: lift, soft shadow, photo eases in about 3% */
  .service-card {
    transition:
      transform var(--motion-hover) var(--motion-ease),
      box-shadow var(--motion-hover) var(--motion-ease);
  }

  .service-card:hover,
  .service-card:focus-visible {
    transform: translateY(-4px);
    box-shadow: 0 14px 32px rgba(23, 24, 26, 0.09);
  }

  .service-card__media {
    transition: transform 900ms var(--motion-ease);
  }

  .service-card:hover .service-card__media,
  .service-card:focus-visible .service-card__media {
    transform: scale(1.035);
  }

  /* Project cards: the photo is the element's own background, so the zoom is
     done on a duplicate of it (background: inherit) clipped by the card's
     existing overflow: hidden. The duplicate is fully transparent (not painted)
     at rest, so the resting photo is byte-for-byte the original. On hover it
     appears instantly at scale 1 (looks identical) and then eases in; on
     un-hover it stays until it has eased back, then disappears. */
  .project-card {
    transition: transform var(--motion-hover) var(--motion-ease);
  }

  .project-card__media {
    position: relative;
    isolation: isolate;
  }

  .project-card__media::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: inherit;
    opacity: 0;
    transition: transform 900ms var(--motion-ease), opacity 0s linear 900ms;
  }

  .project-card:hover {
    transform: translateY(-4px);
  }

  .project-card:hover .project-card__media::before {
    opacity: 1;
    transform: scale(1.04);
    transition: transform 900ms var(--motion-ease), opacity 0s;
  }

  /* Data-driven project cards (real <img>) and the project gallery: the same
     lift and slow ~4% photo zoom, clipped by the frame's overflow: hidden */
  .project-card__img,
  .project-gallery__item img {
    transition: transform 900ms var(--motion-ease);
  }

  a.project-card:focus-visible {
    transform: translateY(-4px);
  }

  .project-card:hover .project-card__img,
  a.project-card:focus-visible .project-card__img,
  .project-gallery__item:hover img,
  .project-gallery__item:focus-visible img {
    transform: scale(1.04);
  }

  /* Home split tiles: same technique, under the tile's gradient overlay and text */
  .tile {
    overflow: hidden;
    isolation: isolate;
  }

  .tile::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: inherit;
    opacity: 0;
    transition: transform 1100ms var(--motion-ease), opacity 0s linear 1100ms;
  }

  .tile:hover::after {
    opacity: 1;
    transform: scale(1.03);
    transition: transform 1100ms var(--motion-ease), opacity 0s;
  }

  /* Footer links nudge right */
  .footer-links a {
    transition: color var(--transition-fast), transform var(--motion-hover) var(--motion-ease);
  }

  .footer-links a:hover {
    transform: translateX(3px);
  }
}

/* ---------------------------------------------------------------------
   REDUCED MOTION: also calm the small movements that already existed
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .btn:hover svg,
  .link-arrow:hover svg,
  .mobile-nav__cta:hover::after {
    transform: none;
  }
}

/* ---------------------------------------------------------------------
   PRINT: never print a page with invisible not-yet-revealed content
   --------------------------------------------------------------------- */
@media print {
  html.js :is(.reveal, .reveal-up, .reveal-left, .reveal-right, .reveal-scale, .image-reveal, .stagger-item, .stagger-group > *) {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
}
