/* === Boot noise on page load ===
   `--boot-noise-display` lets light themes (paper, brutalist) suppress
   the dark static flash entirely without touching the per-page <div>. */
.boot-noise {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 9999;
  display: var(--boot-noise-display);
  background:
    repeating-linear-gradient(0deg,
      rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 1px,
      rgba(0,0,0,0.0) 1px,    rgba(0,0,0,0.0) 2px),
    repeating-linear-gradient(90deg,
      rgba(255,255,255,0.06) 0px, rgba(255,255,255,0.06) 1px,
      rgba(0,0,0,0.0) 1px,    rgba(0,0,0,0.0) 2px),
    #050008;
  opacity: 1;
  animation: boot-fade 0.55s ease-out forwards;
}
@keyframes boot-fade {
  0%   { opacity: 1; }
  60%  { opacity: 0.6; }
  100% { opacity: 0; visibility: hidden; }
}

/* === Glitch title (RGB split) === */
.glitch-title {
  position: relative;
  display: inline-block;
  font-family: var(--font-display);
  font-size: 4rem;
  line-height: 1;
  letter-spacing: 0.04em;
  color: var(--fg);
  text-shadow: 0 0 12px rgba(var(--fg-rgb), 0.25);
  cursor: pointer;
  user-select: none;
}
.glitch-title::before,
.glitch-title::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.glitch-title::before {
  color: var(--glitch-1);
  text-shadow: 1px 0 var(--glitch-1);
  transform: translate(-1px, 0);
  mix-blend-mode: screen;
  /* `--glitch-opacity` is the theme-driven master fader on the RGB-split
     pseudo-elements; flat/print themes (paper, brutalist) set it to 0 to
     hide the split entirely without needing a per-theme override block. */
  opacity: calc(0.85 * var(--glitch-opacity));
  animation: glitch-shift-1 5.5s steps(8, end) infinite;
  clip-path: inset(0 0 60% 0);
}
.glitch-title::after {
  color: var(--glitch-2);
  text-shadow: -1px 0 var(--glitch-2);
  transform: translate(1px, 0);
  mix-blend-mode: screen;
  opacity: calc(0.9 * var(--glitch-opacity));
  animation: glitch-shift-2 4.3s steps(8, end) infinite;
  clip-path: inset(60% 0 0 0);
}
@keyframes glitch-shift-1 {
  0%, 100% { transform: translate(-1px, 0); clip-path: inset(0 0 60% 0); }
  10%      { transform: translate(-3px, 1px); clip-path: inset(10% 0 40% 0); }
  20%      { transform: translate(-1px, 0); clip-path: inset(0 0 60% 0); }
  60%      { transform: translate(-4px, -1px); clip-path: inset(30% 0 20% 0); }
  65%      { transform: translate(-1px, 0); clip-path: inset(0 0 60% 0); }
}
@keyframes glitch-shift-2 {
  0%, 100% { transform: translate(1px, 0); clip-path: inset(60% 0 0 0); }
  15%      { transform: translate(3px, -1px); clip-path: inset(40% 0 10% 0); }
  22%      { transform: translate(1px, 0); clip-path: inset(60% 0 0 0); }
  50%      { transform: translate(4px, 1px); clip-path: inset(20% 0 30% 0); }
  55%      { transform: translate(1px, 0); clip-path: inset(60% 0 0 0); }
}

.brand:hover .glitch-title,
.glitch-title:hover {
  text-shadow: 0 0 18px rgba(var(--hot-rgb), 0.35), 0 0 8px rgba(var(--cool-rgb), 0.25);
}
.brand:hover .glitch-title::before,
.glitch-title:hover::before {
  animation: glitch-shift-1 0.32s steps(8, end) infinite;
}
.brand:hover .glitch-title::after,
.glitch-title:hover::after {
  animation: glitch-shift-2 0.28s steps(8, end) infinite;
}

/* === Blinking cursor === */
.blinker::after {
  content: "_";
  color: var(--hot);
  animation: blink 1s steps(2, start) infinite;
  margin-left: 0.15em;
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.6);
}
@keyframes blink {
  0%, 49%  { opacity: 1; }
  50%, 100%{ opacity: 0; }
}

/* === Hover tear on post titles === */
.post-title:hover {
  animation: tear 0.5s steps(8, end) 1;
}
@keyframes tear {
  0%   { text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.35); transform: translateX(0); }
  20%  { text-shadow: 2px 0 var(--glitch-1), -2px 0 var(--glitch-2); transform: translateX(-2px); }
  40%  { text-shadow: -3px 0 var(--glitch-1), 3px 0 var(--glitch-2); transform: translateX(2px); }
  60%  { text-shadow: 1px 0 var(--glitch-1), -1px 0 var(--glitch-2); transform: translateX(-1px); }
  80%  { text-shadow: 2px 0 var(--glitch-2), -2px 0 var(--glitch-1); transform: translateX(1px); }
  100% { text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.35); transform: translateX(0); }
}

/* === reduced motion === */
@media (prefers-reduced-motion: reduce) {
  .boot-noise,
  .glitch-title::before,
  .glitch-title::after,
  .blinker::after,
  .crt-rec::before,
  .post-title:hover {
    animation: none !important;
  }
  .boot-noise { display: none !important; }
  .glitch-title::before, .glitch-title::after { opacity: 0; }
}


/* === Inline FX codes ====================================================
 *
 * `#name [s<n>] [i<n>] {body}` markup parsed by frontend/js/fx.js. Wrapper
 * is always a `.fx.fx-<name>` span; leaf effects (scan, scramble, etc.)
 * also include an inner `.fx-out` that the JS rewrites every tick;
 * per-char effects (jitter, wave) split the body into `.fx-ch` atoms.
 *
 * Static effects (chroma, pulse, tear) are pure CSS and live entirely
 * in this file. Animated effects rely on JS but pick up baseline
 * rendering here.
 *
 * FX codes are author-authored obfuscation, applied on every theme --
 * they ignore `--glitch-opacity` (which only governs the site's own
 * `.glitch-title` / corner-tick chrome). The visitor can still dial
 * them down via the FX picker (`data-fx-mode="off"` on <html> short-
 * circuits parse() to plain text; `.fx-off` mirrors that for already-
 * mounted blocks if the visitor toggles mid-page) and the OS-level
 * prefers-reduced-motion preference (handled at the bottom of the file).
 * ====================================================================== */

.fx {
  display: inline-block;
  position: relative;
}
.fx-ch {
  display: inline-block;
  will-change: transform;
}
.fx-out {
  /* `.fx-out` is the leaf rewrite target. `tabular-nums` so row-by-row
     glyph swaps don't kerning-jitter the surrounding text; `pre-wrap`
     so leaf-effects that emit runs of " " (flicker, redact, scan) keep
     their positions but the body still wraps at column boundaries. */
  font-variant-numeric: tabular-nums;
  white-space: pre-wrap;
}

/* `data-fx-mode="off"` and `.fx-off` neutralize the wrapper so a turned-
   off block reads as plain text. Leaf effects keep their .fx-out span;
   we just stop animating it (the JS clears textContent back to .text). */
[data-fx-mode="off"] .fx,
.fx.fx-off {
  text-shadow: none !important;
  animation: none !important;
}
[data-fx-mode="off"] .fx::before,
[data-fx-mode="off"] .fx::after,
.fx.fx-off::before,
.fx.fx-off::after {
  display: none !important;
}
[data-fx-mode="off"] .fx-ch,
.fx.fx-off .fx-ch {
  transform: none !important;
}
/* #color + #gradient neutralization. parse() short-circuits to plain
   text on a cold load with mode=off, so these selectors only matter
   for the live "switched to off after the page mounted" path -- they
   match the .fx-off class that the fx.js mode-change handler adds
   alongside the data-fx-mode flip. Without these the gradient body
   would stay rainbow-painted and the colored text would keep its
   custom hue in off mode, which contradicts the picker's intent. */
[data-fx-mode="off"] .fx-color,
.fx.fx-off.fx-color {
  color: inherit !important;
}
[data-fx-mode="off"] .fx-gradient,
.fx.fx-off.fx-gradient,
[data-fx-mode="off"] .fx-rainbow,
.fx.fx-off.fx-rainbow {
  background-image: none !important;
  color: inherit !important;
  -webkit-text-fill-color: inherit !important;
  animation: none !important;
}
/* The marquee track is a non-.fx child of the wrapper, so the generic
   `.fx { animation: none }` off-rule above doesn't reach it. Halt it
   explicitly and drop its slide transform so off mode reads as plain
   text. (#blink / #shake animate the .fx element itself, so they're
   already covered by the generic rule.) */
[data-fx-mode="off"] .fx-marquee-track,
.fx.fx-off .fx-marquee-track {
  animation: none !important;
  transform: none !important;
}
[data-fx-mode="off"] .fx-blink,
.fx.fx-off.fx-blink { opacity: 1 !important; }
[data-fx-mode="off"] .fx-shake,
.fx.fx-off.fx-shake { transform: none !important; }

/* ---- chroma ----------------------------------------------------------
   Scaled-down `.glitch-title` treatment for inline text. The pseudo-
   element clones use `attr(data-fx-text)` -- the literal stripped body
   that fx.js stamped on the wrapper at parse time -- so they keep
   showing the original word even when the wrapper's flow content is a
   live ticker (scramble) or a per-char container (jitter). That makes
   nesting safe: the RGB-split clones drift over the natural-flow body,
   the nested fx animates inside the body, both run together. */
.fx-chroma {
  /* `inherit` (not `--fg`) so an enclosing `#color`/`#gradient` can
     repaint the natural-flow body. The two RGB-split ::before / ::after
     clones below pin explicit --glitch-1 / --glitch-2 colors so the
     split chrome stays themed regardless of any outer color override. */
  color: inherit;
  /* Perf: scope the screen-blend pseudos to a private stacking context
     so their per-frame compositor work doesn't drag the scanlines /
     vignette / fixed body-bg layers above into the dirty region. */
  isolation: isolate;
}
.fx-chroma::before,
.fx-chroma::after {
  content: attr(data-fx-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Author-intent obfuscation -- runs on every theme. We pin a
     fixed opacity rather than reading `--glitch-opacity` so paper /
     brutalist (which zero that var to kill the wordmark RGB-split)
     don't quietly suppress an FX code the writer chose to use. */
  opacity: 0.85;
  mix-blend-mode: screen;
}
/* `--fx-amp` is the chroma offset scaler. fx.js writes `--fx-intensity:N`
   (0..10) onto the wrapper inline; we map that to amp 0..5 so the default
   `i=2` produces amp=1 -- which is the literal pre-fix behavior (1px..3px
   keyframe translates). The two pseudos use `calc(var(--fx-period, 4.7s)
   * <ratio>)` for animation-duration so the original 4.7s/3.7s out-of-
   sync split is preserved when the author doesn't pass an explicit `s`. */
.fx-chroma::before {
  --fx-amp: calc(var(--fx-intensity, 2) / 2);
  color: var(--glitch-1);
  text-shadow: 1px 0 var(--glitch-1);
  transform: translate(calc(-1px * var(--fx-amp)), 0);
  animation: fx-chroma-1 var(--fx-period, 4.7s) steps(8, end) infinite;
  clip-path: inset(0 0 55% 0);
}
.fx-chroma::after {
  --fx-amp: calc(var(--fx-intensity, 2) / 2);
  color: var(--glitch-2);
  text-shadow: -1px 0 var(--glitch-2);
  transform: translate(calc(1px * var(--fx-amp)), 0);
  animation: fx-chroma-2 calc(var(--fx-period, 4.7s) * 0.787) steps(8, end) infinite;
  clip-path: inset(55% 0 0 0);
}
@keyframes fx-chroma-1 {
  0%, 100% { transform: translate(calc(-1px * var(--fx-amp)), 0);          clip-path: inset(0 0 55% 0); }
  10%      { transform: translate(calc(-2px * var(--fx-amp)), 1px);        clip-path: inset(8% 0 40% 0); }
  20%      { transform: translate(calc(-1px * var(--fx-amp)), 0);          clip-path: inset(0 0 55% 0); }
  60%      { transform: translate(calc(-3px * var(--fx-amp)), -1px);       clip-path: inset(28% 0 22% 0); }
  65%      { transform: translate(calc(-1px * var(--fx-amp)), 0);          clip-path: inset(0 0 55% 0); }
}
@keyframes fx-chroma-2 {
  0%, 100% { transform: translate(calc(1px * var(--fx-amp)), 0);           clip-path: inset(55% 0 0 0); }
  15%      { transform: translate(calc(2px * var(--fx-amp)), -1px);        clip-path: inset(40% 0 8% 0); }
  22%      { transform: translate(calc(1px * var(--fx-amp)), 0);           clip-path: inset(55% 0 0 0); }
  50%      { transform: translate(calc(3px * var(--fx-amp)), 1px);         clip-path: inset(20% 0 28% 0); }
  55%      { transform: translate(calc(1px * var(--fx-amp)), 0);           clip-path: inset(55% 0 0 0); }
}

/* ---- pulse -----------------------------------------------------------
   Rhythmic glow + opacity breathe driven by the active theme's --hot.
   Period parameterised via `--fx-period` set inline by fx.js, intensity
   via `--fx-intensity` (0..10). The default emitted intensity is 3,
   calibrated so the mid-cycle blur lands at the original 12px and the
   opacity dip at the original 0.65 -- existing #pulse{...} posts that
   never set `i` keep the same look. */
.fx-pulse {
  animation: fx-pulse-glow var(--fx-period, 2s) ease-in-out infinite;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.45);
}
@keyframes fx-pulse-glow {
  0%, 100% {
    opacity: 1;
    text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.35);
  }
  50% {
    /* i=0 -> opacity 1.0 (no dip); i=3 -> 0.65 (legacy); i=10 -> ~-0.17
       which CSS clamps to 0 (vanishes briefly at peak intensity). */
    opacity: calc(1 - var(--fx-intensity, 3) * 0.117);
    text-shadow:
      /* i=0 -> 4px (no swell); i=3 -> ~12px (legacy); i=10 -> ~31px halo. */
      0 0 calc(4px + var(--fx-intensity, 3) * 2.7px) rgba(var(--hot-rgb), 0.65),
      0 0 4px rgba(var(--hot-rgb), 0.45);
  }
}

/* ---- tear ------------------------------------------------------------
   Animated VHS slice. Two pseudo-element clones of the body (`data-fx-text`
   feeds them) get clip-path bands that snap left/right at random-feeling
   keyframes. `--fx-period` is total cycle length. The visible band only
   fires in the last ~8% of the cycle (the rest of the cycle is `inset(100%)`
   which hides the clone), so the timing function has to sample finely
   enough to hit those late keyframes -- `steps(50, end)` lands the
   discrete sample points on whole 2% boundaries, which means the 92/94/96/
   98% zone gets multiple distinct samples per cycle (the previous
   `steps(10, end)` only sampled at 10% multiples and skipped the entire
   visible band, which is why the tear effect looked dead). The clones
   keep showing the literal body text on top of whatever the natural flow
   is doing, so nesting an animated child inside #tear{...} still gets
   the slice -- both effects run together. */
.fx-tear {
  /* Same `inherit` story as .fx-chroma above -- the slice clones below
     keep their explicit --glitch-1 / --glitch-2 text-shadows so the
     tear chrome stays themed when an outer `#color` overrides body color. */
  color: inherit;
}
.fx-tear::before,
.fx-tear::after {
  content: attr(data-fx-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
  color: inherit;
  /* Pinned, theme-agnostic -- see the chroma block above. */
  opacity: 0.9;
}
/* `--fx-amp` is the tear translate scaler; default `i=3` from fx.js maps
   to amp=1.0 (the legacy ±2..3px slice translates). Scales linearly,
   so `i=10` lands around ±10px slices for a much harder rip. */
.fx-tear::before {
  --fx-amp: calc(var(--fx-intensity, 3) / 3);
  animation: fx-tear-1 var(--fx-period, 4s) steps(50, end) infinite;
  text-shadow: 1px 0 var(--glitch-1);
}
.fx-tear::after {
  --fx-amp: calc(var(--fx-intensity, 3) / 3);
  animation: fx-tear-2 var(--fx-period, 4s) steps(50, end) infinite;
  animation-delay: calc(var(--fx-period, 4s) * -0.37);
  text-shadow: -1px 0 var(--glitch-2);
}
@keyframes fx-tear-1 {
  0%, 92%, 100% { clip-path: inset(100% 0 0 0); transform: translateX(0); }
  94%           { clip-path: inset(20% 0 65% 0); transform: translateX(calc(-3px * var(--fx-amp))); }
  96%           { clip-path: inset(45% 0 35% 0); transform: translateX(calc(2px * var(--fx-amp))); }
  98%           { clip-path: inset(70% 0 10% 0); transform: translateX(calc(-2px * var(--fx-amp))); }
}
@keyframes fx-tear-2 {
  0%, 88%, 100% { clip-path: inset(100% 0 0 0); transform: translateX(0); }
  90%           { clip-path: inset(60% 0 25% 0); transform: translateX(calc(3px * var(--fx-amp))); }
  94%           { clip-path: inset(15% 0 70% 0); transform: translateX(calc(-2px * var(--fx-amp))); }
  98%           { clip-path: inset(40% 0 40% 0); transform: translateX(calc(2px * var(--fx-amp))); }
}

/* ---- rainbow ---------------------------------------------------------
   Animated full-spectrum text: a static multi-hue gradient clipped to the
   glyphs, with the whole element hue-rotated over --fx-period so the
   colors flow. Distinct from #gradient (which is a spatial two/N-stop
   sweep); rainbow cycles hue over TIME. */
.fx-rainbow {
  background-image: linear-gradient(
    90deg,
    #ff0000, #ff8800, #ffee00, #00cc44, #0099ff, #6633ff, #ff33cc, #ff0000
  );
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: fx-rainbow-hue var(--fx-period, 6s) linear infinite;
}
@keyframes fx-rainbow-hue {
  to { filter: hue-rotate(360deg); }
}

/* ---- neon ------------------------------------------------------------
   Static layered glow outline in the theme accent colors. The static
   cousin of #pulse -- no animation, just a steady halo whose size scales
   with --fx-intensity (default 4). */
.fx-neon {
  color: var(--fg);
  text-shadow:
    0 0 calc(2px + var(--fx-intensity, 4) * 0.6px) rgba(var(--hot-rgb), 0.9),
    0 0 calc(4px + var(--fx-intensity, 4) * 1.4px) rgba(var(--hot-rgb), 0.7),
    0 0 calc(8px + var(--fx-intensity, 4) * 2.4px) rgba(var(--cool-rgb), 0.5);
}

/* ---- blink -----------------------------------------------------------
   Unapologetic retro on/off blink. --fx-period is the full cycle;
   --fx-intensity is the blink DEPTH (higher = dims further): the "off"
   half drops to opacity 1 - i/10, so i=10 -> 0 (full off), i=0 -> 1 (no
   blink), i=5 -> 0.5 (half-dim). steps(1) keeps the transition hard. */
.fx-blink {
  animation: fx-blink-step var(--fx-period, 1s) steps(1, end) infinite;
}
@keyframes fx-blink-step {
  0%,  50% { opacity: 1; }
  50.01%, 100% { opacity: calc(1 - var(--fx-intensity, 10) / 10); }
}

/* ---- shake -----------------------------------------------------------
   Emphatic continuous shake. --fx-intensity scales the translate
   amplitude; the keyframes oscillate several times per --fx-period so
   it reads as a rapid rattle rather than a single nudge. */
.fx-shake {
  display: inline-block;
  --fx-amp: calc(var(--fx-intensity, 3) * 0.6px);
  animation: fx-shake-rattle var(--fx-period, 1s) linear infinite;
}
@keyframes fx-shake-rattle {
  0%, 100% { transform: translate(0, 0); }
  10% { transform: translate(calc(-1 * var(--fx-amp)), var(--fx-amp)); }
  20% { transform: translate(var(--fx-amp), calc(-1 * var(--fx-amp))); }
  30% { transform: translate(calc(-1 * var(--fx-amp)), calc(-1 * var(--fx-amp))); }
  40% { transform: translate(var(--fx-amp), var(--fx-amp)); }
  50% { transform: translate(calc(-1 * var(--fx-amp)), 0); }
  60% { transform: translate(var(--fx-amp), calc(-1 * var(--fx-amp))); }
  70% { transform: translate(calc(-1 * var(--fx-amp)), var(--fx-amp)); }
  80% { transform: translate(var(--fx-amp), 0); }
  90% { transform: translate(calc(-1 * var(--fx-amp)), calc(-1 * var(--fx-amp))); }
}

/* ---- marquee ---------------------------------------------------------
   Horizontal scrolling band. The outer .fx-marquee is the clip viewport
   (overflow:hidden); emit() wraps the body in an inner .fx-marquee-track
   that actually slides right-to-left, so the overshoot is clipped against
   the viewport rather than overlapping neighboring text. Reads best on
   its own line. */
.fx-marquee {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  vertical-align: bottom;
  white-space: nowrap;
}
.fx-marquee-track {
  display: inline-block;
  white-space: nowrap;
  will-change: transform;
  animation: fx-marquee-slide var(--fx-period, 8s) linear infinite;
}
@keyframes fx-marquee-slide {
  0%   { transform: translateX(100%); }
  100% { transform: translateX(-100%); }
}

/* ---- jitter / wave ---------------------------------------------------
   Per-char atoms; transforms driven by JS. Baseline styling here keeps
   the line rhythm stable when no JS has run yet (e.g. fx-mode=off). */
.fx-jitter,
.fx-wave,
.fx-wiggle {
  white-space: pre-wrap;
}

/* ---- leaf base -------------------------------------------------------
   Leaf effects share the same wrapper styling. Color/text-shadow tweaks
   per effect can layer on top. */
.fx-scan,
.fx-scramble,
.fx-redact,
.fx-decay,
.fx-flicker {
  letter-spacing: 0.02em;
}
.fx-redact .fx-out { color: var(--fg); }
.fx-scan   .fx-out { color: var(--fg-dim); }
.fx-scan   .fx-out { /* the revealed char inherits --fg via parent */ }

/* ---- color / gradient ------------------------------------------------
   Pure visual treatments. Both are containers so their bodies are parsed
   recursively (nested fx animate as real spans inside). #color sets a
   single inline --fx-color and lets the cascade do the work; #gradient
   exposes --fx-from / --fx-to / --fx-angle and uses background-clip:text
   to paint glyphs from the gradient image.

   Author-intent, theme-agnostic -- same posture as the other fx codes.
   The visitor's FX picker still gates them via [data-fx-mode="off"]
   (handled in the off-mode block above, extended below) and
   prefers-reduced-motion halts the optional shimmer animation. */
.fx-color {
  color: var(--fx-color, inherit);
}

.fx-gradient {
  /* Extended gradients (mid colors / repeat count) emit a fully-built
     --fx-image from emit(); the simple two-stop case leaves it unset and
     falls back to the from/to/angle var path below. */
  background-image: var(--fx-image, linear-gradient(
    var(--fx-angle, 90deg),
    var(--fx-from, var(--hot)),
    var(--fx-to, var(--cool))
  ));
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-size: 100% 100%;
}
/* Shimmer: emit() adds the fx-gradient-anim class whenever the author
   passed an s<sec> param. background-position cycles 0% -> 200% across
   one --fx-period so the gradient sweeps the body left-to-right
   continuously. background-size: 200% gives the slide somewhere to
   actually travel. */
.fx-gradient.fx-gradient-anim {
  background-size: 200% 100%;
  animation: fx-gradient-slide var(--fx-period, 4s) linear infinite;
}
@keyframes fx-gradient-slide {
  0%   { background-position:   0% 50%; }
  100% { background-position: 200% 50%; }
}

/* Nesting: cascade #color through nested leaves and per-char wrappers
   so a `#color cred {#scan{x}}` actually colors the .fx-out band and
   `#color chot {#jitter{x}}` colors the per-char atoms. Without these
   overrides the existing `.fx-scan .fx-out` / `.fx-redact .fx-out`
   color rules above would win on the inner span. */
.fx-color .fx-out,
.fx-color .fx-ch {
  color: inherit;
}

/* Nesting (gradient + ANY nested fx).
 *
 * Every .fx span is display:inline-block (set at the top of this file)
 * which means each one creates its own painting context. The outer
 * .fx-gradient's `background-clip: text` can therefore only clip to
 * glyphs in its OWN direct text -- it cannot reach across an
 * inline-block child boundary into a nested .fx-scramble / .fx-jitter
 * / .fx-chroma / etc. Without help, the intermediate wrapper has no
 * background-image of its own, the glyphs under it inherit
 * `color: transparent` from .fx-gradient, and the whole nested run
 * paints nothing -- the bug that motivated this block. (Selection
 * highlights still render because the browser measures the glyph
 * boxes; that's why the text was invisible-when-deselected but
 * pink-rectangle-when-selected in the report.)
 *
 * Fix: every intermediate .fx descendant inherits the parent's
 * background-image and re-applies the text-clip + transparent fill,
 * so the gradient walks down the tree until it hits a layer with
 * actual text glyphs. :not(.fx-gradient) leaves nested gradients
 * alone so their own --fx-from / --fx-to / --fx-angle overrides take
 * effect -- a `#gradient {#gradient {inner}}` should paint two
 * different gradients, not one inherited everywhere.
 */
.fx-gradient .fx:not(.fx-gradient) {
  background-image: inherit;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* The inner .fx-out is inline (no display set), so the parent .fx
 * leaf wrapper's clip:text reaches into it natively. We still pin
 * transparent on it because the existing `.fx-scan .fx-out` /
 * `.fx-redact .fx-out` rules above explicitly color the rewritten
 * frame, and we want the gradient to win when one wraps a leaf.
 * Author-visible result: a #scan inside #gradient still sweeps the
 * reveal cursor, but every glyph (revealed and ░-blocked alike) is
 * painted from the gradient instead of var(--fg-dim) / var(--fg). */
.fx-gradient .fx-out {
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Per-char atoms (.fx-ch from jitter/wave) are themselves
 * inline-block, so each one needs its own copy of the gradient
 * image. That's exactly the "per-char rainbow" the showcase docs
 * promise: each glyph paints its own slice of the from->to gradient
 * because every .fx-ch is a separate painting context. */
.fx-gradient .fx-ch {
  background-image: inherit;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Tear's slice clones (::before / ::after) use `color: inherit` so
 * they normally pick up --fg from the body. Inside a gradient wrapper
 * the propagation rule above turns the .fx-tear element transparent,
 * which would inherit transparent into the pseudos and erase the
 * slices. Pin the glitch colors explicitly here so a nested
 * `#gradient {#tear{x}}` still shows the slice clones (the body
 * itself stays gradient-painted; the clones add the slice motion on
 * top in theme-tied glitch colors). Chroma's pseudos already set
 * `color: var(--glitch-1/2)` explicitly so they don't need this. */
.fx-gradient .fx-tear::before { color: var(--glitch-1); }
.fx-gradient .fx-tear::after  { color: var(--glitch-2); }

/* ---- prefers-reduced-motion -----------------------------------------
   Pure-CSS effects halt; ticker-driven effects are downgraded by JS at
   mount time (renderStaticFrame). */
@media (prefers-reduced-motion: reduce) {
  .fx-chroma::before,
  .fx-chroma::after,
  .fx-pulse,
  .fx-tear::before,
  .fx-tear::after,
  .fx-gradient.fx-gradient-anim,
  .fx-rainbow,
  .fx-blink,
  .fx-shake,
  .fx-marquee-track {
    animation: none !important;
  }
  .fx-pulse {
    text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.35);
    opacity: 1;
  }
  /* Halted CSS effects settle into a readable static frame: blink stays
     visible, shake/marquee stop translated. Static gradient + rainbow
     gradient + neon glow still render (no motion to halt). */
  .fx-blink { opacity: 1; }
  .fx-shake, .fx-marquee-track { transform: none; }
}


/* === Composer FX preview ============================================== *
 *  Small panel that slides in below a textarea / input when its contents
 *  contain an FX code. Same render pipeline as the live page. Kept here
 *  rather than components.css because it visually IS the FX layer.
 * ====================================================================== */
.fx-preview {
  margin-top: 0.5em;
  padding: 0.5em 0.65em;
  border: 1px dashed var(--border);
  background: rgba(var(--fg-rgb), 0.02);
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.4;
  color: var(--fg);
}
.fx-preview-label {
  display: block;
  font-size: 0.78rem;
  color: var(--fg-dim);
  margin-bottom: 0.35em;
  letter-spacing: 0.05em;
}
.fx-preview-body {
  /* Same reset as a standalone `<p>` so the rendered body sits flush. */
  white-space: pre-wrap;
  word-break: break-word;
}
.fx-preview-body p {
  margin: 0 0 0.4em 0;
}
.fx-preview-body p:last-child { margin-bottom: 0; }
.fx-preview.empty { display: none; }


/* === Composer FX toolbar ============================================== *
 *  Row of buttons above the body textarea that wrap the current selection
 *  in an inline FX code. Built by fx-toolbar.js from the effect + preset
 *  registry. Styled to read as a compact, terminal-flavored chip rack.
 * ====================================================================== */
.fx-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3em;
  align-items: center;
  margin: 0.35em 0 0.5em 0;
}
.fx-tool-btn {
  font-family: var(--font-body);
  font-size: 0.78rem;
  line-height: 1;
  padding: 0.32em 0.5em;
  color: var(--fg-dim);
  background: rgba(var(--fg-rgb), 0.03);
  border: 1px solid var(--border);
  border-radius: var(--radius, 2px);
  cursor: pointer;
  letter-spacing: 0.03em;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.fx-tool-btn:hover,
.fx-tool-btn:focus-visible {
  color: var(--hot);
  border-color: var(--hot);
  background: rgba(var(--hot-rgb), 0.08);
  outline: none;
}
.fx-tool-preset {
  color: var(--cool);
  border-style: dashed;
}
.fx-tool-preset:hover,
.fx-tool-preset:focus-visible {
  color: var(--cool);
  border-color: var(--cool);
  background: rgba(var(--cool-rgb), 0.08);
}
.fx-tool-sep {
  width: 1px;
  align-self: stretch;
  margin: 0 0.2em;
  background: var(--border);
}
/* Color / gradient popovers: a <details> whose summary is itself a chip. */
.fx-tool-menu { position: relative; display: inline-block; }
.fx-tool-menu > summary { list-style: none; }
.fx-tool-menu > summary::-webkit-details-marker { display: none; }
.fx-tool-menu[open] > summary { color: var(--hot); border-color: var(--hot); }
.fx-tool-tray {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.25em);
  left: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25em;
  width: 13em;
  padding: 0.4em;
  background: var(--bg, #000);
  border: 1px solid var(--border);
  border-radius: var(--radius, 2px);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}
