/* Experimental "trendier" motion layer — grain texture, magnetic hover,
   and scroll-linked reveals. Kept in its own file so it's easy to compare
   against (or merge back into) the main branch. Every effect here is
   gated behind prefers-reduced-motion and, where relevant, @supports —
   nothing in this file should ever leave content stuck invisible. */

/* ---------------------------------------------------------------------
   Grain texture — adds tactile depth to flat backgrounds without adding
   visual noise to the content itself. Fixed, very low opacity, ignores
   pointer events, injected once by initGrainOverlay() in js/main.js. The
   fractalNoise SVG is generated at a small tile size and repeated, which
   is far cheaper than a raster image and never needs a network request.
   ------------------------------------------------------------------- */
.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

@media (prefers-reduced-motion: no-preference) {
  .grain-overlay {
    animation: grain-shift 9s steps(8) infinite;
  }
}

/* Animates background-position, not transform: a `transform` on a
   position:fixed, viewport-filling element still moves its own painted
   box away from the viewport at points in the animation cycle — which,
   confirmed on a real mobile layout, several engines (including at least
   one mobile Safari build) count toward the *page's* scrollable area,
   producing a genuine few-pixel horizontal (and vertical) overflow purely
   from this decorative layer, and that horizontal overflow is itself a
   known trigger for position:sticky breaking elsewhere on the page (the
   .site-header nav bar, in this case). background-position shifts only
   where the tiled noise texture sits *within* this element's own
   never-moving box, so it can't contribute overflow at all — same visual
   drift, none of the side effects. Percentages below are of the 180px
   tile itself (background-size above) rather than the viewport, which is
   also just more correct for a repeating pattern: the original
   viewport-relative transform amounts never had a principled reason to
   scale with viewport size for a texture that's supposed to read as a
   fixed grain density regardless of screen size. */
@keyframes grain-shift {
  0% { background-position: 0 0; }
  12.5% { background-position: -7.2px 5.4px; }
  25% { background-position: 5.4px -9px; }
  37.5% { background-position: -9px -5.4px; }
  50% { background-position: 7.2px 7.2px; }
  62.5% { background-position: -5.4px 9px; }
  75% { background-position: 9px -3.6px; }
  87.5% { background-position: -3.6px -7.2px; }
  100% { background-position: 0 0; }
}

/* ---------------------------------------------------------------------
   Magnetic hover — nav links and footer links pull subtly toward the
   cursor as it nears them (see initMagneticHover in js/main.js), rather
   than only reacting once the cursor is exactly on top. Desktop-only
   (mouse-driven by definition) and skipped under reduced motion.
   ------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .magnetic {
    display: inline-block;
    transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
    will-change: transform;
  }
}

/* ---------------------------------------------------------------------
   Scroll-linked reveal — ties an element's entrance directly to its own
   scroll position via the native view() timeline, rather than a fixed
   load-time delay. Chromium and Safari support this; Firefox doesn't yet
   (as of mid-2026), so the whole hidden-starting-state is scoped inside
   @supports — Firefox just sees the content in its normal, fully visible
   state with no animation at all, never a stuck-invisible element.
   ------------------------------------------------------------------- */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* animation-range is scoped to "entry" alone (the element's own
       transition from just-appearing to fully-visible) rather than
       mixing in "cover" — a cover-based end point needs scroll distance
       to exist *below* the element to finish traversing, which the very
       last item on a page doesn't have, leaving it stuck at partial
       opacity forever with no more scrolling able to complete it. entry
       is self-contained to the element's own size, so it always
       completes regardless of what's below it. */
    .scroll-reveal {
      animation: scroll-reveal-in linear both;
      animation-timeline: view();
      animation-range: entry;
    }

    @keyframes scroll-reveal-in {
      from {
        opacity: 0;
        transform: translateY(28px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    /* Applied structurally (no per-file HTML edits needed) to every
       standalone image/video and split-row on a project detail page, so
       the gallery reveals piece by piece as you scroll through the case
       study instead of all at once on load. */
    .project-images > * {
      animation: scroll-reveal-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
  }
}
