/* depth.css: the landing's depth-and-motion layer. Four effects, each
   opt-in by one class on the element that gets it, so wiring is a class
   on a tag and nothing else. depth.js drives the two pointer effects;
   the other two are pure CSS. Same-origin, as the CSP's style-src
   requires; no build step, this file is served as written.

   THE API

     .fx-tilt      The surface leans toward the pointer on a perspective
                   and eases back on leave. Fine pointers only (mouse,
                   pen), never touch. Needs depth.js.
                   --fx-tilt-max   the lean at the edge, default 2.5deg
                   --fx-tilt-depth the perspective, default 1200px

     .fx-spot      A soft disc of light follows the pointer across the
                   surface, painted UNDER the element's content and over
                   its own background, so text on it is never lightened.
                   The host becomes its own clip (overflow: clip) and
                   stacking context. Fine pointers only. Needs depth.js.
                   --fx-spot-size  the disc, default 560px
                   --fx-spot-color the light, default white at .6
                   --fx-spot-alpha the disc's strength 0..1, default 1
                                   (set 0 to switch it off on one host)

     .fx-reveal    Rises and fades in as it enters the viewport, driven
                   by the scroll itself (animation-timeline: view()), no
                   script. Finished state everywhere the timeline is not
                   supported or motion is reduced.
                   --fx-reveal-y   the rise, default 24px
                   --fx-reveal-end where in the entry it settles, default
                                   65% (of the element's own entry)

     .fx-parallax  Drifts against the scroll across its whole time on
                   screen: down by --fx-drift as it enters, up by the
                   same as it leaves, so it moves slower than the page.
                   Same timeline, same fallbacks as .fx-reveal.
                   --fx-drift      default 40px


   EASING: the four curves are legal.css's tokens (--ease-settle for the
   tilt's reset, --ease-exit for its smoothing, --ease-glide for the
   light's fade), each with its own
   value as the fallback so this sheet still moves correctly on a page
   that has not loaded legal.css.

   WHAT ANIMATES: transform, translate and opacity, nothing else. No
   layout property is ever touched, no filter, no backdrop, and the
   pointer effects write only custom properties that feed a transform.
   None of it reads or writes the scroll position; the reveals READ it
   through the timeline, which is the compositor's, not script's.

   REDUCED MOTION: the block at the foot switches every effect off with
   !important, on top of the pointer effects never being rigged by the
   script and the scroll effects being declared only under
   no-preference. What remains is the finished state: the surface flat,
   the light gone, the content in place, the field still.

   THE TWO TRAPS

   1. view() measures against the nearest SCROLL CONTAINER, and
      overflow: hidden makes one even though nothing can scroll it.
      Measured (Chrome 152, WebKit 26.5): a .fx-reveal inside an
      overflow: hidden box never animates and shows its finished state,
      and a .fx-parallax inside one is frozen at a fixed offset (3.5px
      in the harness), never moving with the page. Dead, not invisible,
      but dead. Put the classes on elements whose ancestors up to <html>
      are all overflow: visible or overflow: clip; clip is not a scroll
      container and clips the same, which is why .fx-spot uses it and
      why a tile that hosts a reveal should say overflow-clip
      rather than overflow-hidden.

   2. The reveal and the parallax animate the `translate` property, the
      tilt writes `transform`, and the phone shell scales with `scale`.
      Three separate properties, so one element can carry a tilt and a
      reveal at once with neither cancelling the other (an animation
      holding transform: none would pin a tilt flat for good). Keep it
      that way: never animate `transform` in a keyframe here. */

/* ---- .fx-tilt ------------------------------------------------------ */
/* The lean is two rotations from two custom properties the script sets
   from the pointer's place on the surface, on a perspective that sits
   on the element itself so the vanishing point is its centre. The long
   transition is the reset: on leave the script zeroes the angles and
   this eases them home. While the pointer is on it the script marks it
   .is-fx-hover and the transition shortens to a smoothing interval, so
   each frame's new angle is reached over a few frames rather than
   snapped to; that is what makes a 60Hz pointer stream read as one
   motion. will-change lives only in that state, never at rest, because
   a layer held for every tile on the page was one of this page's
   measured jank sources. Fine pointers only: on touch there is no hover
   and a lean on tap would be a surface flinching from a finger. */
@media (hover: hover) and (pointer: fine) {
  .fx-tilt {
    --fx-rx: 0deg; --fx-ry: 0deg;
    transform: perspective(var(--fx-tilt-depth, 1200px)) rotateX(var(--fx-rx)) rotateY(var(--fx-ry));
    transition: transform 560ms var(--ease-settle, cubic-bezier(.2, .7, .2, 1));
  }
  .fx-tilt.is-fx-hover {
    transition: transform 110ms var(--ease-exit, cubic-bezier(0, 0, .3, 1));
    will-change: transform;
  }
}

/* ---- .fx-spot ------------------------------------------------------ */
/* One disc, drawn as the host's ::before at z-index -1 inside the host's
   own stacking context: over the host's background, under everything
   it contains. That ordering is the point. A sheen painted OVER a tile
   lightens its type as well as its ground (muted-on-wash drops from 5.3
   to 3.3:1 under white at .22), so the light here falls on the surface
   and the ink stays ink. The disc is a fixed circle whose centre sits at
   the host's origin, moved by translate3d from two custom properties in
   px; moving a composited layer costs the compositor a matrix, whereas
   moving a gradient's position would repaint the tile every frame. The
   host clips it (overflow: clip, which respects the host's own corners
   and is not a scroll container, see trap 1). Entry and exit are an
   opacity fade, so the light comes up where the pointer is and dies
   where it was, never sliding in from the last hover's corner. The
   clip margin lets a keyboard focus ring (2px, offset 2px, legal.css)
   on a link flush with the host's edge survive the clip; the disc's
   own light is faded to nothing long before it reaches a corner, so
   the four pixels of margin never show it. Browsers without the margin
   property (Safari) clip at the box, which is where they were. */
@media (hover: hover) and (pointer: fine) {
  .fx-spot {
    --fx-mx: 0px; --fx-my: 0px;
    position: relative; isolation: isolate; overflow: clip; overflow-clip-margin: 4px;
  }
  .fx-spot::before {
    content: ""; position: absolute; z-index: -1; pointer-events: none;
    top: 0; left: 0;
    width: var(--fx-spot-size, 560px); height: var(--fx-spot-size, 560px);
    margin: calc(var(--fx-spot-size, 560px) / -2) 0 0 calc(var(--fx-spot-size, 560px) / -2);
    border-radius: 50%;
    background: radial-gradient(closest-side, var(--fx-spot-color, rgba(255, 255, 255, .6)), transparent);
    opacity: 0; transition: opacity 320ms var(--ease-glide, cubic-bezier(.25, 0, .3, 1));
    transform: translate3d(var(--fx-mx), var(--fx-my), 0);
  }
  .fx-spot.is-fx-hover::before {
    opacity: var(--fx-spot-alpha, 1);
    will-change: transform;
  }
}

/* ---- .fx-reveal and .fx-parallax ---------------------------------- */
/* Scroll-driven, so the scroll IS the clock: no listener, no rAF, no
   script at all, and the animation runs on the compositor thread with
   the scroll it follows. Both are declared only where the timeline is
   supported AND motion is not reduced, and the base state carries no
   opacity or offset of its own: a browser that never runs the animation
   shows the element exactly where it ends up, which is the rule the
   hero's own entrance set. Support (measured 2026-09-08 in the scratch
   harness): Chromium from 115 (Chrome 152 here) and Safari 26 (WebKit
   26.5 here) run them; Firefox 153 carries them behind
   layout.css.scroll-driven-animations.enabled, off by default in the
   build measured, and with it off every reveal sat at opacity 1 with no
   offset and no animation, above the fold, below it and at the foot of
   the page. That is the whole fallback: the finished state, because the
   base state IS the finished state.

   The reveal's range is measured on the element's own entry, so it is
   always completable: at the foot of the page the viewport's bottom is
   the document's bottom, and every element's entry is done by then. A
   range measured on `cover` is not (an element near the foot can never
   reach cover 35%) and would leave content half-faded for good. The
   parallax has no such hazard because it changes no opacity: whatever
   point of its cover it is left at is a position, not a fade. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .fx-reveal {
      animation: fx-reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% entry var(--fx-reveal-end, 65%);
    }
    .fx-parallax {
      animation: fx-parallax linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes fx-reveal {
  from { opacity: 0; translate: 0 var(--fx-reveal-y, 24px); }
  to { opacity: 1; translate: 0 0; }
}
@keyframes fx-parallax {
  from { translate: 0 var(--fx-drift, 40px); }
  to { translate: 0 calc(var(--fx-drift, 40px) * -1); }
}

/* ---- reduced motion ------------------------------------------------ */
/* The whole layer, off. !important so that no inline property the
   script may already have written (a preference that flips mid-visit)
   and no later rule can move anything. The script also watches the
   query and unrigs itself, so the pointer effects stop at their source
   as well as here. */
@media (prefers-reduced-motion: reduce) {
  .fx-tilt { transform: none !important; transition: none !important; will-change: auto !important; }
  .fx-spot::before { display: none !important; }
  .fx-reveal, .fx-parallax { animation: none !important; opacity: 1 !important; translate: none !important; }
}
