/* ==========================================================================
   effects.css — Premium ambient effects: aurora, film grain, glass, spotlight
   --------------------------------------------------------------------------
   Depends on css/variables.css design tokens (--bg, --panel, --card, --text,
   --brand, --brand-accent, --ring, radii, --transition-med, easings) and works
   in BOTH themes (dark on :root, light on :root.light). Every value that needs
   to differ per theme is routed through an --fx-* token declared below, so the
   theme switch needs no duplicated rules.

   Design constraints honoured here (see .kiro/specs/site-redesign/design.md):
     - No build step, no CDN, no extra network request (the grain texture is an
       inline SVG data-URI, not a binary asset).
     - Animation is TRANSFORM/OPACITY only — never layout-affecting properties.
     - Every animation is disabled under `prefers-reduced-motion: reduce`.
     - Hover-driven effects are gated on `(hover: hover) and (pointer: fine)`
       so touch devices never get a stuck state.
     - WCAG AA text contrast is preserved: the aurora and grain paint BEHIND all
       content (negative z-index on the body pseudo-elements) and the card
       surfaces they sit under (`.card`) are opaque, so no text/background pair
       from variables.css is altered. The spotlight overlays are likewise
       painted below the card's content layer at a low tint strength.

   FILE MAP
   --------------------------------------------------------------------------
     1. Effect tokens (per-theme intensity)
     2. Aurora background        — body::before  (z-index -2)
     3. Film grain               — body::after   (z-index -1)
     4. Glass surfaces           — .glass utility
     5. Spotlight cards          — .spotlight + ::before glow + ::after edge
     6. Reduced motion / coarse pointer overrides
   ========================================================================== */

/* ==========================================================================
   1. Effect tokens
   --------------------------------------------------------------------------
   Dark theme carries the effects more strongly (a luminous wash reads well on
   a dark canvas); light theme is deliberately fainter so the ambient colour
   never lifts the page background toward the text colour.
   ========================================================================== */

:root {
  /* Aurora */
  --fx-aurora-opacity: 0.45;
  --fx-aurora-blur: 70px;
  --fx-aurora-duration: 32s;

  /* Film grain */
  --fx-grain-opacity: 0.045;
  --fx-grain-tile: 180px;
  --fx-grain-duration: 6s;

  /* Glass */
  --fx-glass-blur: 14px;
  --fx-glass-saturate: 140%;
  --fx-glass-tint: 72%;              /* --panel share of the translucent fill */
  --fx-glass-highlight: rgba(255, 255, 255, 0.12);
  --fx-glass-shadow: rgba(0, 0, 0, 0.5);

  /* Spotlight */
  /* Overlays are absolutely positioned against the PADDING box, while `.card`
     and `.pub` both carry a 1px border. Insetting by -1px pushes the overlays
     out onto the border box so the glow reaches the visual edge and the
     gradient hairline lands exactly on the existing --ring border instead of
     just inside it. Override per-surface if a border width ever differs. */
  --fx-spot-inset: -1px;
  --fx-spot-size: 340px;             /* radius of the pointer glow            */
  --fx-spot-edge-size: 260px;        /* radius of the brightened border arc   */
  --fx-spot-opacity: 1;              /* hover opacity of the glow overlay     */
  --fx-spot-edge-opacity: 0.9;
  --fx-spot-core: color-mix(in srgb, var(--brand) 26%, transparent);
  --fx-spot-mid: color-mix(in srgb, var(--brand-accent) 12%, transparent);
  --fx-spot-edge-core: color-mix(in srgb, var(--brand) 85%, transparent);
  --fx-spot-edge-mid: color-mix(in srgb, var(--brand) 22%, transparent);
}

:root.light {
  --fx-aurora-opacity: 0.22;
  --fx-aurora-blur: 80px;

  --fx-grain-opacity: 0.028;

  --fx-glass-tint: 82%;
  --fx-glass-highlight: rgba(255, 255, 255, 0.65);
  --fx-glass-shadow: rgba(76, 79, 105, 0.22);

  --fx-spot-opacity: 0.75;
  --fx-spot-edge-opacity: 0.8;
  --fx-spot-core: color-mix(in srgb, var(--brand) 16%, transparent);
  --fx-spot-mid: color-mix(in srgb, var(--brand-accent) 8%, transparent);
  --fx-spot-edge-core: color-mix(in srgb, var(--brand) 70%, transparent);
  --fx-spot-edge-mid: color-mix(in srgb, var(--brand) 16%, transparent);
}

/* ==========================================================================
   2. Aurora background — body::before
   --------------------------------------------------------------------------
   Three large, soft, blurred radial blobs in --brand / --brand-accent that
   drift forever on a transform-only loop. Implemented on `body::before` so no
   new HTML element is required.

   STACKING: <html> declares no background, so body's own background propagates
   to the canvas and is painted first, below everything. body is not a stacking
   context, so this `z-index: -2` pseudo-element joins the root stacking context
   and paints ABOVE that canvas background but BELOW every in-flow element —
   including all text and all card surfaces. `pointer-events: none` guarantees
   it can never intercept a click.

   The layer is inset NEGATIVELY (-25%) so it is substantially larger than the
   viewport: the drift transform can translate and scale it without ever
   exposing an uncovered edge.
   ========================================================================== */

body::before {
  content: '';
  position: fixed;
  inset: -25%;
  z-index: -2;
  pointer-events: none;
  background-color: transparent;
  background-repeat: no-repeat;
  background-image:
    radial-gradient(
      38% 40% at 24% 26%,
      color-mix(in srgb, var(--brand) 55%, transparent) 0%,
      transparent 68%
    ),
    radial-gradient(
      34% 36% at 78% 20%,
      color-mix(in srgb, var(--brand-accent) 50%, transparent) 0%,
      transparent 66%
    ),
    radial-gradient(
      44% 38% at 62% 86%,
      color-mix(in srgb, var(--brand) 40%, transparent) 0%,
      transparent 70%
    );
  opacity: var(--fx-aurora-opacity);
  filter: blur(var(--fx-aurora-blur));
  transform: translate3d(0, 0, 0) scale(1);
  will-change: transform;
  animation: fx-aurora-drift var(--fx-aurora-duration) ease-in-out infinite
    alternate;
}

/* Transform-only drift: translate + scale + a whisper of rotation. No
   background-position, no filter animation — the compositor handles it. */
@keyframes fx-aurora-drift {
  0% {
    transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
  }
  50% {
    transform: translate3d(3%, -2.5%, 0) scale(1.08) rotate(1.5deg);
  }
  100% {
    transform: translate3d(-3%, 2.5%, 0) scale(1.04) rotate(-1.5deg);
  }
}

/* ==========================================================================
   3. Film grain — body::after
   --------------------------------------------------------------------------
   A tiled monochrome noise texture whose only job is to break up the banding
   that large, low-contrast gradients produce on 8-bit displays.

   The texture is generated by an inline SVG `feTurbulence` encoded as a
   data-URI, so it costs zero extra requests and ships no binary asset. `#` is
   percent-encoded (%23) because a raw `#` would terminate the URL.

   Opacity is kept in the 0.028–0.045 range (per theme): enough to dither the
   gradient, far too faint to measurably change any text contrast ratio. It
   paints at `z-index: -1` — above the aurora, still behind all content — and
   never receives pointer events.
   ========================================================================== */

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  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.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: var(--fx-grain-tile) var(--fx-grain-tile);
  opacity: var(--fx-grain-opacity);
  /* Cheap, discrete shimmer: three steps of opacity, nothing per-frame. */
  animation: fx-grain-shimmer var(--fx-grain-duration) steps(3, end) infinite;
}

@keyframes fx-grain-shimmer {
  0% {
    opacity: var(--fx-grain-opacity);
  }
  50% {
    opacity: calc(var(--fx-grain-opacity) * 0.6);
  }
  100% {
    opacity: var(--fx-grain-opacity);
  }
}

/* ==========================================================================
   4. Glass surfaces — .glass
   --------------------------------------------------------------------------
   An opt-in utility: a translucent --panel fill that blurs and slightly
   saturates whatever sits behind it, a hairline --ring border, and a top-edge
   inset highlight that reads as a lit bevel.

   The `-webkit-` prefixed `backdrop-filter` is declared first for Safari.
   Where backdrop-filter is unsupported the @supports block below swaps the
   translucent fill for a SOLID --panel background, so text on the surface
   stays fully legible instead of compositing over page content.
   ========================================================================== */

.glass {
  background-color: color-mix(
    in srgb,
    var(--panel) var(--fx-glass-tint),
    transparent
  );
  -webkit-backdrop-filter: blur(var(--fx-glass-blur))
    saturate(var(--fx-glass-saturate));
  backdrop-filter: blur(var(--fx-glass-blur))
    saturate(var(--fx-glass-saturate));
  border: 1px solid var(--ring);
  border-radius: var(--radius-lg);
  box-shadow:
    inset 0 1px 0 var(--fx-glass-highlight),
    0 18px 40px -28px var(--fx-glass-shadow);
}

/* Legibility fallback: no backdrop-filter (prefixed or not) -> solid panel. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .glass {
    background-color: var(--panel);
  }
}

/* ==========================================================================
   5. Spotlight cards — .spotlight
   --------------------------------------------------------------------------
   The signature effect. js/spotlight.js writes `--mx` / `--my` (percentages,
   relative to the element's own box) on whichever `.spotlight` element the
   pointer is over; two pseudo-element overlays read those values:

     ::before  a soft radial glow tinted with --brand, centred on the pointer.
     ::after   a 1px gradient border whose arc nearest the pointer brightens.

   STACKING — why text stays readable and clickable:
     `isolation: isolate` turns each `.spotlight` element into its own stacking
     context, and both overlays sit at `z-index: -1` INSIDE it. Per the CSS
     painting order that places them above the element's own background/border
     but BELOW every descendant (block backgrounds, floats, inline text,
     positioned children). So the glow can never wash over a heading or a
     publication title, and because `isolation` scopes the stacking context the
     -1 never escapes behind the page. Both overlays are additionally
     `pointer-events: none`, so links and buttons inside the card keep every
     click. No existing child needs a z-index, which keeps the absolutely
     positioned decorations already inside `.card` (e.g. the news timeline)
     unaffected.

   Both overlays rest at `opacity: 0` and ease in over 300ms, so nothing is
   visible — and nothing composites over the card's background — until hover.
   ========================================================================== */

.spotlight {
  position: relative;
  isolation: isolate;
}

/* --- 5a. Pointer glow ------------------------------------------------------ */
.spotlight::before {
  content: '';
  position: absolute;
  inset: var(--fx-spot-inset, 0);
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  background-image: radial-gradient(
    circle var(--fx-spot-size) at var(--mx, 50%) var(--my, 50%),
    var(--fx-spot-core) 0%,
    var(--fx-spot-mid) 38%,
    transparent 72%
  );
  opacity: 0;
  transition: opacity var(--transition-med, 300ms) var(--ease-out, ease-out);
}

/* --- 5b. Pointer-tracking gradient hairline ------------------------------- */
/* Standard "gradient border" trick: paint the gradient across the whole box,
   then mask out everything except a 1px inset ring. `padding: 1px` defines the
   ring thickness; the two-layer mask keeps the border box and subtracts the
   content box. Prefixed properties are listed for WebKit. */
.spotlight::after {
  content: '';
  position: absolute;
  inset: var(--fx-spot-inset, 0);
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  padding: 1px;
  background-image: radial-gradient(
    circle var(--fx-spot-edge-size) at var(--mx, 50%) var(--my, 50%),
    var(--fx-spot-edge-core) 0%,
    var(--fx-spot-edge-mid) 45%,
    transparent 78%
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity var(--transition-med, 300ms) var(--ease-out, ease-out);
}

/* --- 5c. Hover activation (fine pointers only) ---------------------------- */
/* Gated so a touch device never latches a glow it cannot move or dismiss.
   `:focus-within` is included so keyboard users get the same affordance; with
   no pointer the glow falls back to the 50%/50% centre defaults. */
@media (hover: hover) and (pointer: fine) {
  .spotlight:hover::before,
  .spotlight:focus-within::before {
    opacity: var(--fx-spot-opacity);
  }

  .spotlight:hover::after,
  .spotlight:focus-within::after {
    opacity: var(--fx-spot-edge-opacity);
  }
}

/* ==========================================================================
   6. Reduced motion & coarse pointer
   --------------------------------------------------------------------------
   Reduced motion: the aurora stops drifting and the grain stops shimmering
   (both remain visible, simply static), and the spotlight overlays lose their
   transition so hover is an instant state change rather than an animation.
   js/spotlight.js additionally declines to attach at all under reduced motion,
   so in practice --mx/--my keep their 50%/50% defaults; these rules are the
   CSS-side guarantee.

   Nothing here changes any colour or opacity that text sits on, so contrast is
   identical with motion on or off.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  body::before,
  body::after {
    animation: none;
  }

  /* Freeze the aurora at its neutral, untransformed position. */
  body::before {
    transform: none;
    will-change: auto;
  }

  .spotlight::before,
  .spotlight::after {
    transition: none;
  }
}

/* Small viewports are the throttled-mobile Lighthouse case: shrink the blur
   radius (the gradients are already soft, so the visual delta is small) and
   drop the grain shimmer, keeping the static grain. Purely a cost reduction —
   no colour, opacity-at-rest or layout value changes. */
@media (max-width: 640px) {
  :root {
    --fx-aurora-blur: 45px;
  }

  body::after {
    animation: none;
  }
}

/* Touch / coarse pointer: the hover block above never matches, so the overlays
   stay at opacity 0. Drop the transitions and the compositor hint as well so
   no work is scheduled for an effect that can never run. */
@media (hover: none) and (pointer: coarse) {
  .spotlight::before,
  .spotlight::after {
    transition: none;
  }

  body::before {
    will-change: auto;
  }
}
