/* ==========================================================================
   FX layer — premium micro-interactions (pairs with assets/js/fx.js).
   Load after style.css. Everything is opt-in through data attributes:
     data-tilt[="7"]  data-tilt-glare="off"  data-tilt-layer (child; depth via --fx-layer, default 8px)
     data-magnetic[="8"]  data-parallax="0.15"  data-spotlight  data-aurora  data-float[="10"]  data-depth="0.6"
   fx.js puts these classes on <html>:
     .fx       script is running            .fx-fine  mouse/trackpad and motion allowed
     .fx-prop  typed --fx-bob is available  .fx-sda   CSS scroll-driven parallax
   and injects aria-hidden layers it owns: div.fx-aurora ([data-aurora]) and div.fx-spot ([data-spotlight]),
   so the site's own ::before / ::after stay untouched.
   NOTE: after the first hover .fx-tilt / .fx-mag REPLACE the element's own `transition` list
   (see the lists below). If such an element transitions another property, add it to that list.
   Only transform / translate / opacity are animated: no layout shift, text stays readable.
   ========================================================================== */

/* ---------- [data-tilt]: 3D tilt toward the cursor + gold rim light and glare ---------- */
.fx-fine [data-tilt] { isolation: isolate; }
.fx-fine [data-tilt].fx-rel { position: relative; }

/* .fx-tilt stays after the first hover: smooth spring back on leave */
html.fx-fine [data-tilt].fx-tilt {
  transition: opacity .9s var(--ease), transform .8s cubic-bezier(.2, .8, .2, 1),
              box-shadow .5s var(--ease), border-color .5s;
  transition-delay: 0s;
}
html.fx-fine [data-tilt].fx-out { z-index: 1; }  /* released: stays above neighbours while it settles */
/* the edge under the cursor comes forward, so the box under the pointer grows instead of receding */
html.fx-fine [data-tilt].is-tilt {
  z-index: 2;
  transform: perspective(var(--fx-p, 1000px)) translate3d(0, var(--fx-lift, -4px), var(--fx-z, 12px))
             rotateX(calc(var(--fx-y, 0) * var(--fx-m, 7) * 1deg)) rotateY(calc(var(--fx-x, 0) * var(--fx-m, 7) * -1deg));
  transition: opacity .9s var(--ease), transform .22s cubic-bezier(.2, .6, .3, 1),
              box-shadow .5s var(--ease), border-color .5s;
  box-shadow: 0 44px 70px -46px rgba(15, 29, 54, .5), 0 22px 40px -30px rgba(150, 105, 35, .45);
}
/* hero items are revealed by a CSS animation whose fill would block the tilt */
.js :is(.hero, .cid-hero) .reveal[data-tilt].fx-tilt { animation: none; }

/* inner depth: children marked data-tilt-layer float above the card plane (no preserve-3d needed) */
html.fx-fine [data-tilt].fx-tilt [data-tilt-layer] { transition: translate .8s cubic-bezier(.2, .8, .2, 1); }
html.fx-fine [data-tilt].is-tilt [data-tilt-layer] {
  translate: calc(var(--fx-x, 0) * var(--fx-layer, 8px) * -1) calc(var(--fx-y, 0) * var(--fx-layer, 8px) * -1);
  transition: translate .22s cubic-bezier(.2, .6, .3, 1);
}

/* glare lives under the content (z-index:-1 inside the isolated card), so text is never washed out.
   The mask paints it at full strength on a 1px rim (on the inner border edge) and at 15% inside:
   a small cream hotspot under the cursor plus a wider gold sweep that mostly shows on the rim. */
.fx-fine [data-tilt]:not([data-tilt-glare="off"])::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  padding: 1px; border-radius: inherit;
  background:
    radial-gradient(circle 220px at var(--fx-gx, 50%) var(--fx-gy, 0%), #FFF6DD, rgba(236, 207, 146, .9) 42%, rgba(233, 204, 141, 0) 100%),
    radial-gradient(circle 520px at var(--fx-gx, 50%) var(--fx-gy, 0%), rgba(214, 174, 98, .6), rgba(214, 174, 98, 0) 72%);
  -webkit-mask: linear-gradient(rgba(0, 0, 0, .85) 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(rgba(0, 0, 0, .85) 0 0) content-box exclude, linear-gradient(#000 0 0);
  opacity: 0; transition: opacity .6s var(--ease);
}
.fx-fine [data-tilt].is-tilt::after { opacity: 1; }

/* ---------- [data-magnetic]: drift toward the cursor (ramps in over a 24px band), spring back ---------- */
html.fx-fine [data-magnetic].fx-mag {
  transition: background-position .6s var(--ease), transform .75s cubic-bezier(.3, 1.7, .45, 1),
              box-shadow .3s var(--ease), border-color .3s, color .3s, background-color .3s;
}
html.fx-fine [data-magnetic].is-mag {
  transform: translate3d(var(--fx-mx, 0px), var(--fx-my, 0px), 0);
  transition: background-position .6s var(--ease), transform .25s cubic-bezier(.2, .6, .3, 1),
              box-shadow .3s var(--ease), border-color .3s, color .3s, background-color .3s;
}

/* ---------- [data-spotlight]: soft golden light under the cursor ---------- */
.fx-fine [data-spotlight] { isolation: isolate; }
.fx-fine [data-spotlight].fx-rel { position: relative; }
.fx-spot {
  position: absolute; inset: 0; z-index: -1; overflow: hidden;
  pointer-events: none; border-radius: inherit;
  /* feathered top and bottom: the section edge never shows a straight cut */
  -webkit-mask-image: linear-gradient(transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(transparent, #000 12%, #000 88%, transparent);
}
.fx-spot::before {
  content: ""; position: absolute; left: 0; top: 0;
  width: clamp(320px, 38vw, 560px); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(250, 233, 190, .35), rgba(238, 212, 156, .12) 50%, rgba(238, 212, 156, 0));
  translate: -50% -50%;
  transform: translate3d(var(--fx-sx, 50%), var(--fx-sy, 50%), 0);
  opacity: 0;
  transition: opacity .9s var(--ease), transform .55s cubic-bezier(.2, .7, .2, 1);
}
.is-spot > .fx-spot::before { opacity: 1; }

/* ---------- [data-aurora]: slow bright gradient blobs (CSS only, its own injected layer) ----------
   z-index:-2 keeps it under the section's own decorations (dot grid, drift glow).
   Blobs fade out between 11% and 83% of the layer height, so neither a section edge nor the capped
   layer bottom ever cuts one. Gold and sky blobs never overlap, and the sky tints are near-white
   "cool light" (blue at low alpha on ivory or a gold glow turns grey; a light tint stays clean). */
:where([data-aurora]) { position: relative; isolation: isolate; overflow: hidden; overflow: clip; }
.fx-aurora {
  position: absolute; z-index: -2; pointer-events: none;
  top: max(-10%, -90px); left: -12%; right: -12%; height: min(130%, 160vh);
  background:
    radial-gradient(30% 30% at 28% 36%, rgba(247, 227, 174, .62), rgba(247, 227, 174, 0) 70%),
    radial-gradient(24% 26% at 80% 30%, rgba(226, 237, 255, .85), rgba(226, 237, 255, 0) 70%),
    radial-gradient(30% 24% at 60% 66%, rgba(233, 204, 141, .42), rgba(233, 204, 141, 0) 70%),
    radial-gradient(17% 18% at 16% 70%, rgba(218, 232, 255, .8), rgba(218, 232, 255, 0) 70%),
    radial-gradient(24% 24% at 46% 42%, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0) 70%);
  animation: fx-au-in 1.4s var(--ease) both, fx-au-move 26s ease-in-out -7s infinite alternate;
}
.fx-idle > .fx-aurora { animation-play-state: paused; }
@keyframes fx-au-in { from { opacity: 0; } }
@keyframes fx-au-move {  /* translate only: the GPU layer is never re-rastered */
  0%   { translate: -3% -2%; }
  50%  { translate: 2% 1.5%; }
  100% { translate: -1% 3%; }
}
/* phones: a still image, nothing to composite or wake the GPU for */
@media (hover: none), (max-width: 760px) { .fx-aurora { animation: none; } }

/* ---------- [data-float] bobbing + [data-depth] / [data-parallax] offsets ----------
   All three share the `translate` property, so they compose with the element's own transform. */
.fx [data-float] { animation: fx-bob var(--fx-dur, 7s) ease-in-out var(--fx-delay, 0s) infinite; }
.fx [data-float].fx-idle { animation: none !important; }  /* paused off-screen (beats the depth variant) */
@keyframes fx-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 calc(var(--fx-amp, 10px) * -1); }
}
.fx [data-depth], .fx [data-parallax] {
  translate: var(--fx-dx, 0px) calc(var(--fx-py, 0px) + var(--fx-dy, 0px) + var(--fx-bob, 0px));
}
/* floating + depth: bob through a typed custom property so both offsets add up */
.fx-fine.fx-prop [data-float][data-depth] { animation-name: fx-bob-v; }
.fx-fine:not(.fx-prop) [data-float][data-depth] { animation: none; }
@keyframes fx-bob-v {
  0%, 100% { --fx-bob: 0px; }
  50%      { --fx-bob: calc(var(--fx-amp, 10px) * -1); }
}
/* scroll parallax on the compositor where supported (fx.js measures --fx-from/--fx-to from the
   frame's real overscan and falls back to rAF on desktop, or stays still on touch) */
@supports (animation-timeline: view()) {
  .fx-sda [data-parallax]:not(.fx-pjs) { animation: fx-par linear both; animation-timeline: view(); }
}
@keyframes fx-par {
  from { translate: 0 var(--fx-from, 0px); }
  to   { translate: 0 var(--fx-to, 0px); }
}

/* ---------- .btn click ripple ---------- */
.fx .btn { overflow: hidden; isolation: isolate; }
.fx-ripple {
  position: absolute; z-index: -1; border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, rgba(255, 255, 255, .85), rgba(255, 249, 232, .45) 55%, rgba(255, 255, 255, 0));
  animation: fx-ripple .8s cubic-bezier(.2, .7, .2, 1) forwards;
}
.btn--outline .fx-ripple { background: radial-gradient(closest-side, rgba(233, 204, 141, .75), rgba(247, 227, 174, .35) 55%, rgba(247, 227, 174, 0)); }
.btn--blue .fx-ripple { background: radial-gradient(closest-side, rgba(247, 227, 174, .7), rgba(255, 255, 255, .25) 55%, rgba(255, 255, 255, 0)); }
@keyframes fx-ripple {
  from { transform: scale(.08); opacity: 1; }
  55%  { opacity: .6; }
  to   { transform: scale(1); opacity: 0; }
}

/* ---------- heading reveal: feathered mask wipe + soft focus (text is never split) ----------
   Only .h2 and the hero titles. Runs once when main.js adds .is-visible (titles inside .hero /
   .cid-hero run on load together with style.css "rise"). Fill "backwards" only: no mask or filter
   is left behind. Touch screens skip the blur (a full-heading filter on DPR 3 while WebGL boots). */
:root { --fx-wipe: fx-wipe; }
@media (hover: none) { :root { --fx-wipe: fx-wipe-m; } }
@media (prefers-reduced-motion: no-preference) {
  .js .reveal.is-visible :is(.h2, .page-hero__title, .ev-hero__title, .bio-hero__name),
  .js :is(.h2, .page-hero__title, .ev-hero__title, .bio-hero__name).reveal.is-visible,
  .js :is(.hero, .cid-hero) .reveal :is(.hero__title, .cid-hero__title) {
    animation: var(--fx-wipe) 1s var(--ease) backwards;
    animation-delay: calc(var(--d, 0s) + .05s);
  }
  .js :is(.hero, .cid-hero) :is(.hero__title, .cid-hero__title).reveal {
    animation: rise 1s var(--ease) both, var(--fx-wipe) 1s var(--ease) backwards;
    animation-delay: var(--d, 0s), calc(var(--d, 0s) + .05s);
  }
  .js .reveal.is-visible .h2::after,
  .js .h2.reveal.is-visible::after {
    transform-origin: 0 50%;
    animation: fx-bar .9s var(--ease) backwards;
    animation-delay: calc(var(--d, 0s) + .35s);
  }
  .js .section-head--center.is-visible .h2::after,
  .js .section-head--center .reveal.is-visible .h2::after { transform-origin: 50% 50%; }
}
/* the mask is 2.6x the heading: a soft 26%-wide edge sweeps from left to right */
@keyframes fx-wipe {
  from {
    -webkit-mask-image: linear-gradient(90deg, #000 45%, transparent 55%); mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
    -webkit-mask-size: 260% 100%; mask-size: 260% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 90% 0; mask-position: 90% 0;
    filter: blur(4px);
  }
  50% { filter: blur(0); }
  to {
    -webkit-mask-image: linear-gradient(90deg, #000 45%, transparent 55%); mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
    -webkit-mask-size: 260% 100%; mask-size: 260% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 0 0; mask-position: 0 0;
    filter: blur(0);
  }
}
@keyframes fx-wipe-m {
  from {
    -webkit-mask-image: linear-gradient(90deg, #000 45%, transparent 55%); mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
    -webkit-mask-size: 260% 100%; mask-size: 260% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 90% 0; mask-position: 90% 0;
  }
  to {
    -webkit-mask-image: linear-gradient(90deg, #000 45%, transparent 55%); mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
    -webkit-mask-size: 260% 100%; mask-size: 260% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 0 0; mask-position: 0 0;
  }
}
@keyframes fx-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ---------- reduced motion: everything stands still ---------- */
@media (prefers-reduced-motion: reduce) {
  .fx [data-float], .fx-ripple, .fx-aurora { animation: none !important; }
  .fx [data-depth], .fx [data-parallax] { translate: none !important; }
}
