/* effects.css — the effects kit: scanlines, holo shimmer, glitch, and the frame the Matrix rain
   canvas sits in.

   Every public page loads it. At ~2.5 KB gzipped that is cheaper than deciding per page which
   effects a page is allowed, and it means a skin can turn an effect on anywhere without a handler
   change. The canvas is the exception: `fx.js` is real work, so a page opts into it with
   `"FX": true` in its page data and most pages never load it.

   Four rules the whole kit obeys, from PLAN.md D8:

   1. **Only transform, opacity and filter animate.** Nothing here animates a colour, a size, a
      position or a background-position: those run on the main thread and cost the frame budget
      that keeps Lighthouse above 95. A sweep is a translated gradient, not a moving background.
   2. **Nothing an effect draws can move the page.** Every layer is absolutely positioned inside a
      `position: relative` host, so the kit contributes exactly zero to CLS.
   3. **Nothing here is ever the only way to see something.** An effect decorates text that is
      already there and already readable with the effect off. Turn the whole file off and the page
      still says everything it said before — which is also what `prefers-reduced-motion` gets.
   4. **A skin retunes, never rewrites.** Every value comes from a `--fx-*` token in tokens.css.
      Setting `--fx-scanline-opacity: 0` turns scanlines off for a skin with no JS involved.

   `prefers-reduced-motion: reduce` stops the animation and keeps the static look — a still
   shimmer is a highlight, a still scanline is a texture. Neither is a reason to blank the element.
   The rain is a special case: it is motion by definition, so fx.js never starts it at all. */

/* ── Scanlines ────────────────────────────────────────────────────────────────────────────────
   A CRT texture over a surface. Static by default; `.fx-scanlines-roll` adds a slow drift, and
   because the layer is twice the height of its host the drift is one transform, looping. */
.fx-scanlines { position: relative; isolation: isolate; }
.fx-scanlines > * { position: relative; z-index: 1; }
/* `rgba(var(--x), var(--y))` and not `rgb(var(--x) / var(--y))`: the colour tokens hold a
   comma-separated list, because fx.js concatenates one into a string too. Substituted into the
   slash form that becomes `rgb(0, 0, 0 / 0.07)` — comma and slash syntax mixed, which is invalid,
   and an invalid value takes the whole declaration with it. It looked right and drew nothing. */
.fx-scanlines::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  background: repeating-linear-gradient(
    to bottom,
    rgba(var(--fx-scanline-color), var(--fx-scanline-opacity)) 0,
    rgba(var(--fx-scanline-color), var(--fx-scanline-opacity)) 1px,
    transparent 1px,
    transparent var(--fx-scanline-size)
  );
}
.fx-scanlines-roll::after {
  height: 200%;
  inset-block: 0 auto;
  animation: fx-roll 8s linear infinite;
  will-change: transform;
}
@keyframes fx-roll {
  to { transform: translateY(calc(var(--fx-scanline-size) * -1)); }
}

/* ── Holo shimmer ─────────────────────────────────────────────────────────────────────────────
   A band of light that crosses the element and leaves. The band is a gradient on a layer wider
   than its host, translated across — so the animation is one composited transform, and the
   element underneath is untouched.

   The band is 60% of the host wide and starts 60% off its leading edge, so carrying it fully past
   the trailing edge is 160% of the host — which is 266.7% of the band's own width, and transform
   percentages are of the element itself. That is where the odd number in the keyframe comes from;
   it is not a viewport measurement, so the sweep takes the same time on a narrow card as a wide
   one. */
.fx-shimmer { position: relative; overflow: hidden; isolation: isolate; }
.fx-shimmer::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -60%;
  width: 60%;
  z-index: 2;
  pointer-events: none;
  opacity: var(--fx-shimmer-opacity);
  background: linear-gradient(
    105deg,
    transparent 0%,
    var(--glow) 40%,
    var(--glow-strong) 50%,
    var(--glow) 60%,
    transparent 100%
  );
  animation: fx-sweep var(--fx-shimmer-dur) var(--ease-in-out) infinite;
  will-change: transform;
}
@keyframes fx-sweep {
  0%        { transform: translateX(0); }
  55%, 100% { transform: translateX(266.7%); }
}

/* ── Glitch ───────────────────────────────────────────────────────────────────────────────────
   Two copies of the text, offset in opposite directions and shown in brief flashes. The copies
   come from `data-text`, so the glitch needs no extra markup.

   **Generated content is read by screen readers**, and `opacity: 0` does not take it out of the
   accessibility tree — a naive glitch announces its word three times. Hence the doubled `content`
   declaration: the second gives the generated text an empty alt string (`content: … / ""`), which
   removes it from the tree, and a browser that does not understand that syntax discards the second
   declaration and keeps the first. Worst case is the old behaviour; best case is correct.

   `display: inline-block` is why this belongs on short, single-line text — a wordmark, a label. An
   inline-block cannot break across lines, so glitching a sentence would push it off the screen at
   phone width.

   Cheap on purpose: two transforms and two opacities, no filters, no clip-path animation. */
.fx-glitch { position: relative; display: inline-block; }
.fx-glitch::before,
.fx-glitch::after {
  content: attr(data-text);
  content: attr(data-text) / "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}
.fx-glitch::before { color: var(--accent); }
.fx-glitch::after  { color: var(--accent-2); }
/* Triggered by the host, or by whatever wraps it — a wordmark inside a link should glitch when
   you hover or keyboard-focus the link, and a <span> cannot take focus itself. */
.fx-glitch:hover::before,
:hover > .fx-glitch::before,
:focus-visible > .fx-glitch::before { animation: fx-glitch-a 0.45s steps(2, end) 2; }
.fx-glitch:hover::after,
:hover > .fx-glitch::after,
:focus-visible > .fx-glitch::after  { animation: fx-glitch-b 0.45s steps(2, end) 2; }
@keyframes fx-glitch-a {
  0%, 100% { opacity: 0; transform: translate(0); }
  20% { opacity: 0.75; transform: translate(calc(var(--fx-glitch-offset) * -1), 1px); }
  40% { opacity: 0; transform: translate(0); }
  60% { opacity: 0.6; transform: translate(calc(var(--fx-glitch-offset) * -0.5), -1px); }
}
@keyframes fx-glitch-b {
  0%, 100% { opacity: 0; transform: translate(0); }
  30% { opacity: 0.7; transform: translate(var(--fx-glitch-offset), -1px); }
  50% { opacity: 0; transform: translate(0); }
  70% { opacity: 0.5; transform: translate(calc(var(--fx-glitch-offset) * 0.5), 1px); }
}

/* ── Matrix rain ──────────────────────────────────────────────────────────────────────────────
   The host reserves the space; fx.js inserts the canvas into it and sizes it to the host. The
   canvas is behind everything and cannot be clicked. If the script never runs — no JS, an idle
   callback that never fires, reduced motion — the host is an empty box the page already laid out,
   so nothing moves and nothing is missing. */
.fx-rain { position: relative; isolation: isolate; }
.fx-rain > * { position: relative; z-index: 1; }
.fx-rain > canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: var(--fx-rain-opacity);
  /* Fades out at the bottom so the rain never competes with the text below it. */
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
}

@media (prefers-reduced-motion: reduce) {
  .fx-scanlines-roll::after,
  .fx-shimmer::before,
  .fx-glitch::before,
  .fx-glitch::after {
    animation: none !important;
  }
  /* The sweep still reads as a highlight when it is standing still; park it over the element
     rather than off the side of it, where it would look like nothing at all. */
  .fx-shimmer::before { inset-inline-start: 20%; opacity: calc(var(--fx-shimmer-opacity) * 0.5); }
}
