@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/vt323-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/vt323-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
    U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  color-scheme: dark;

  /* The palette: raw phosphor values, named for what they are.
     design/DESIGN.md, "The palette" — source of truth for every one of these. */
  --p-ground: #0a0704;
  --p-panel: #150f07;
  --p-panel-lit: #221709;
  --p-hairline: #3d2807;
  --p-rule: #7a5008;
  --p-dim: #b8790a;
  --p-soft: #f2bc62;
  --p-amber: #ffb000;
  --p-pale: #ffd694;
  --p-hot: #fff2da;
  --p-desc: #d8a45a;
  --p-cell: #100b06;

  --p-cyan: #6fdcf2;
  --p-jade: #5fd9a0;
  --p-lime: #d4e85c;
  --p-coral: #ff6a56;
  --p-rose: #ff7fcb;
  --p-violet: #b9a2ff;

  --bloom: 0 0 8px rgba(255, 176, 0, 0.3), 0 0 26px rgba(255, 176, 0, 0.16);
  --bloom-lg: 0 0 6px rgba(255, 176, 0, 0.42), 0 0 22px rgba(255, 176, 0, 0.34),
    0 0 46px rgba(255, 176, 0, 0.2), 0 0 96px rgba(255, 176, 0, 0.1);

  /* The contract: named for what each does, read by name from every game's
     style.css and from games/pong/script.js and games/flappy-bird/script.js
     at runtime. Change values freely; change a name only by updating those
     scripts' fallbacks in the same commit — see design/DESIGN.md, "How the
     tokens are layered". */
  --bg: var(--p-ground);
  --fg: var(--p-soft);
  --card-bg: var(--p-panel);
  --cell-bg: var(--p-cell);
  --cell-border: var(--p-hairline);
  --accent: var(--p-amber);
  --win: var(--p-jade);
  --lose: var(--p-coral);
  --muted: var(--p-dim);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--fg);
  font-family: "VT323", ui-monospace, "Courier New", monospace;
  font-size: 22px;
  line-height: 1.35;
}

@media (max-width: 760px) {
  body {
    font-size: 20px;
  }
}

h1 {
  margin: 0;
}

.status {
  margin: 0;
  font-size: 1.1rem;
  min-height: 1.5em;
  text-align: center;
}

button {
  font-family: inherit;
}

/* A terminal key, not a pill. Square corners, a hairline edge, and a label in
   letterspaced uppercase — the same language the hub uses for a selected tile,
   so a button on a game page belongs to the same machine as the one that sent
   you there. The rounded amber pill this replaces was the last thing on the
   site still wearing the pre-redesign default stylesheet. */
.btn {
  padding: 0.32rem 0.95rem;
  font-size: 1.05rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border: 1px solid var(--accent);
  border-radius: 0;
  background: var(--accent);
  color: var(--p-ground);
  cursor: pointer;
  box-shadow: 0 0 14px rgba(255, 176, 0, 0.35);
}

.btn:not(:disabled):hover {
  background: var(--p-pale);
  border-color: var(--p-pale);
  box-shadow: 0 0 18px rgba(255, 214, 148, 0.45);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

/* Unselected: outline only. The filled/outline pair is the whole "selected"
   idiom here — it carries the difference on its own, with no second signal. */
.btn.secondary {
  background: transparent;
  border: 1px solid var(--cell-border);
  color: var(--muted);
  box-shadow: none;
}

.btn.secondary:not(:disabled):hover {
  background: transparent;
  border-color: var(--p-rule);
  color: var(--p-pale);
  box-shadow: none;
}

/* Square rather than a pill, for a button whose label is a single glyph. */
.btn.icon {
  padding: 0.32rem 0.65rem;
  line-height: 1;
  letter-spacing: 0;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A row of buttons under the board. */
.controls {
  display: flex;
  gap: 0.6rem;
}



/* ---------------------------------------------------------------------------
   The wash behind every page: a bloom off the top of the tube, a second warm
   glow off the bottom, and a vignette into the corners, so the page has a light
   source instead of being flat black.

   One definition for both frames on purpose. The hub and the game pages each
   had their own, and the game one was missing the bottom glow - which read as
   the footer sitting on black on a game page and on warm ground on the hub.
   --------------------------------------------------------------------------- */
.deco,
.game-deco {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(85% 65% at 50% 2%, rgba(255, 176, 0, 0.16), transparent 74%),
    radial-gradient(65% 50% at 50% 100%, rgba(255, 140, 0, 0.06), transparent 70%),
    radial-gradient(125% 105% at 50% 45%, transparent 52%, rgba(0, 0, 0, 0.72));
}

/* ---------------------------------------------------------------------------
   The header every page wears: the wordmark on the left, and on a game page its
   own title on the right. It used to be the hub's alone and lived in `hub.css`;
   a game page wore a breadcrumb instead, which meant moving from the hub into a
   game changed the furniture as well as the content.

   The site nav is not up here. About and GitHub sit in the footer of every page,
   on a line of their own under whatever else that footer says, and there is no
   Home link at all: the wordmark is the way home on every page, so a Home link
   beside it was a second copy of the same thing. `tests/contract.test.js` holds
   each game page to having that link.
   --------------------------------------------------------------------------- */

.top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--p-hairline);
}

.brand {
  display: inline-block;
  font-size: 3.1rem;
  line-height: 1;
  letter-spacing: 0.1em;
  color: var(--p-hot);
  text-shadow: var(--bloom-lg);
  text-decoration: none;
}

.site-nav {
  display: flex;
  gap: 1.6rem;
}

.site-nav a {
  color: var(--p-dim);
  font-size: 1.15rem;
  letter-spacing: 0.06em;
  text-decoration: none;
  padding-bottom: 0.15rem;
  border-bottom: 1px solid transparent;
}

/* The nav takes a line of its own under whatever the footer already says. On a
   game page that footer is a two-column row of control hints, so the nav claims
   a whole row rather than becoming a third column in it. */
.foot .site-nav,
.game-foot .site-nav {
  flex-basis: 100%;
  margin-top: 0.55rem;
}

/* A character VT323 does not carry, drawn in its own pixels instead: one viewBox
   unit is one of the font's pixels, and the box is its cap height sat on the
   baseline, so the glyph lines up with the letters beside it. `.narrow` is a
   glyph one pixel narrower. See design/DESIGN.md, "Glyphs the typeface lacks". */
.px {
  display: inline-block;
  width: 0.4em;
  height: 0.56em;
  margin: 0 0.04em;
  vertical-align: baseline;
  fill: currentColor;
}

.px.narrow {
  width: 0.32em;
}

.site-nav a:hover,
.site-nav a:focus-visible {
  color: var(--p-pale);
  outline: none;
}

.brand:hover .wm,
.brand:focus-visible .wm {
  color: var(--p-hot);
  text-shadow: var(--bloom-lg), 0 0 34px rgba(255, 176, 0, 0.3);
}

@media (max-width: 760px) {
  .brand {
    font-size: 2.4rem;
  }
}

/* ---------------------------------------------------------------------------
   The wordmark, and the faults it wears.

   The name is on every page of the site, at the same size on all eight, so it
   lives here rather than in `hub.css` or `game.css`, which each own only one
   kind of page.

   The mark is plain text in the HTML and stays readable with no JavaScript at
   all. `wordmark.js` upgrades it in place: the word stays as `.base` in the
   normal flow and remains the one real copy of the name, while the copies an
   effect displaces are empty elements that draw themselves from `data-text`. The
   whole mark is then labelled as one image of the name, because nine
   one-character spans is something a screen reader may otherwise spell out.

   Every effect is `.wm.fx-<name>` - the effect class and `.wm` sit on the SAME
   element, so a descendant selector would never match. With only that class
   present the mark is clean; `.glitch` runs the fault once and is taken away
   again; `.peak` freezes the worst frame and is only used by the previews and
   the tests. Nothing loops.

   Offsets are in `em` on purpose. They were drawn against a 64px mark, and
   nothing holds every page to that size - the header is 3.1rem and 2.4rem on a
   narrow screen, and the mark was for a while a sixth of it inside a breadcrumb,
   where the same tear in px cut the word in half. Anything a fault moves scales
   with whatever type size it is dropped on.

   Shared trick throughout: a band that moves independently is drawn twice, once
   by `.base` and once by a copy clipped to that band, with a `mask-image` hole
   punched in `.base` while the fault runs so the two never double-print. A copy
   that only adds colour (screen blend, no displacement) needs no hole.
   --------------------------------------------------------------------------- */

.wm {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  user-select: none;
}

/* .base stays in the normal flow: it is what gives .wm its box, and it is the
   only copy that is real text. Every other copy is drawn from a `data-text`
   attribute through generated content, so the word appears once in the document
   and once only - thirteen text nodes saying ARCADE404 is what a screen reader,
   a crawler and a copy-paste would all have had to wade through. */
.wm .layer::before,
.wm .fringe::before {
  content: attr(data-text);
}

.wm .ch {
  display: inline;
  position: relative;
}

.wm .layer,
.wm .fringe {
  position: absolute;
  inset: 0;
  display: block;
  margin: 0;
  color: inherit;
  opacity: 0;
  pointer-events: none;
}

.wm .scan {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  pointer-events: none;
}



/* ===================================================================== */
/* fx-quiet-tear — port of lot-e v1. One thin band across the middle slips
   sideways once and snaps back. Reads clean almost always. */
/* ===================================================================== */

.wm.fx-quiet-tear.glitch .base,
.wm.fx-quiet-tear.peak .base {
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 38%, transparent 38%, transparent 76%, black 76%, black 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 38%, transparent 38%, transparent 76%, black 76%, black 100%);
}
.wm.fx-quiet-tear.glitch .l1,
.wm.fx-quiet-tear.peak .l1 {
  clip-path: inset(38% 0 24% 0);
  opacity: 1;
}
.wm.fx-quiet-tear.glitch .l1 { animation: wm-quiet-tear 420ms steps(1, end) 1; }
.wm.fx-quiet-tear.peak .l1 { animation: none; transform: translateX(0.1406em); }
/* Every step is a jump: steps(1, end) holds each value until the next
   keyframe, so nothing travels. The band is already displaced on frame one —
   a glitch does not arrive, it is simply there. */
@keyframes wm-quiet-tear {
  0%   { transform: translateX(0.1406em); }
  33%  { transform: translateX(-0.0625em); }
  55%  { transform: translateX(0); }
  100% { transform: translateX(0); }
}

/* ===================================================================== */
/* fx-six-thin-slices — port of lot-e v2. Six hairline bands; four of them
   fire in a quick staggered burst, two stay put. */
/* ===================================================================== */

.wm.fx-six-thin-slices.glitch .base,
.wm.fx-six-thin-slices.peak .base {
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, transparent 16.6667%,
    black 16.6667%, black 33.3333%,
    transparent 33.3333%, transparent 66.6667%,
    black 66.6667%, black 83.3333%,
    transparent 83.3333%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, transparent 16.6667%,
    black 16.6667%, black 33.3333%,
    transparent 33.3333%, transparent 66.6667%,
    black 66.6667%, black 83.3333%,
    transparent 83.3333%, transparent 100%);
}
.wm.fx-six-thin-slices.glitch .l1, .wm.fx-six-thin-slices.peak .l1 { clip-path: inset(0% 0 83.3333% 0); opacity: 1; }
.wm.fx-six-thin-slices.glitch .l2, .wm.fx-six-thin-slices.peak .l2 { clip-path: inset(33.3333% 0 50% 0); opacity: 1; }
.wm.fx-six-thin-slices.glitch .l3, .wm.fx-six-thin-slices.peak .l3 { clip-path: inset(50% 0 33.3333% 0); opacity: 1; }
.wm.fx-six-thin-slices.glitch .l4, .wm.fx-six-thin-slices.peak .l4 { clip-path: inset(83.3333% 0 0% 0); opacity: 1; }
.wm.fx-six-thin-slices.glitch .l1 { animation: wm-six-a 700ms steps(1, end) 1; }
.wm.fx-six-thin-slices.glitch .l2 { animation: wm-six-b 700ms steps(1, end) 1; }
.wm.fx-six-thin-slices.glitch .l3 { animation: wm-six-c 700ms steps(1, end) 1; }
.wm.fx-six-thin-slices.glitch .l4 { animation: wm-six-d 700ms steps(1, end) 1; }
.wm.fx-six-thin-slices.peak .l1 { animation: none; transform: translateX(0.1094em); }
.wm.fx-six-thin-slices.peak .l2 { animation: none; transform: translateX(-0.1562em); }
.wm.fx-six-thin-slices.peak .l3 { animation: none; transform: translateX(0.0938em); }
.wm.fx-six-thin-slices.peak .l4 { animation: none; transform: translateX(-0.1094em); }
@keyframes wm-six-a { 0% { transform: translateX(0); } 10% { transform: translateX(0.1094em); } 25% { transform: translateX(0.1094em); } 35%, 100% { transform: translateX(0); } }
@keyframes wm-six-b { 0%, 25% { transform: translateX(0); } 35% { transform: translateX(-0.1562em); } 50% { transform: translateX(-0.1562em); } 60%, 100% { transform: translateX(0); } }
@keyframes wm-six-c { 0%, 45% { transform: translateX(0); } 55% { transform: translateX(0.0938em); } 70% { transform: translateX(0.0938em); } 80%, 100% { transform: translateX(0); } }
@keyframes wm-six-d { 0% { transform: translateX(0); } 10% { transform: translateX(-0.1094em); } 25% { transform: translateX(-0.1094em); } 35%, 100% { transform: translateX(0); } }

/* ===================================================================== */
/* fx-big-tear — port of lot-e v4. Two fat halves, one hard 0.4688em snap that
   briefly destroys the word entirely. */
/* ===================================================================== */

.wm.fx-big-tear.glitch .base,
.wm.fx-big-tear.peak .base { opacity: 0; }
.wm.fx-big-tear.glitch .l1, .wm.fx-big-tear.peak .l1 { clip-path: inset(0 0 50% 0); opacity: 1; }
.wm.fx-big-tear.glitch .l2, .wm.fx-big-tear.peak .l2 { clip-path: inset(50% 0 0 0); opacity: 1; }
.wm.fx-big-tear.glitch .l1 { animation: wm-big-tear-top 520ms steps(1, end) 1; }
.wm.fx-big-tear.glitch .l2 { animation: wm-big-tear-bot 520ms steps(1, end) 1; }
.wm.fx-big-tear.peak .l1 { animation: none; transform: translateX(-0.4688em); }
.wm.fx-big-tear.peak .l2 { animation: none; transform: translateX(0.4688em); }
@keyframes wm-big-tear-top { 0%, 60% { transform: translateX(0); } 62%, 78% { transform: translateX(-0.4688em); } 80%, 100% { transform: translateX(0); } }
@keyframes wm-big-tear-bot { 0%, 60% { transform: translateX(0); } 62%, 78% { transform: translateX(0.4688em); } 80%, 100% { transform: translateX(0); } }

/* ===================================================================== */
/* fx-smear-fill — port of lot-e v5. Same middle-band tear as quiet-tear,
   but the gap it opens fills with a dim trailing smear. */
/* ===================================================================== */

.wm.fx-smear-fill.glitch .base,
.wm.fx-smear-fill.peak .base {
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 34%, transparent 34%, transparent 68%, black 68%, black 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 34%, transparent 34%, transparent 68%, black 68%, black 100%);
}
.wm.fx-smear-fill.glitch .l1, .wm.fx-smear-fill.peak .l1 { clip-path: inset(34% 0 32% 0); opacity: 1; }
.wm.fx-smear-fill.glitch .fr1, .wm.fx-smear-fill.peak .fr1 { clip-path: inset(34% 0 32% 0); color: var(--p-dim); }
.wm.fx-smear-fill.glitch .l1 { animation: wm-smear-band 700ms steps(1, end) 1; }
.wm.fx-smear-fill.glitch .fr1 { animation: wm-smear-ghost 700ms steps(1, end) 1; }
.wm.fx-smear-fill.peak .l1 { animation: none; transform: translateX(0.2188em); }
.wm.fx-smear-fill.peak .fr1 { animation: none; opacity: 0.6; transform: translateX(0.0312em); }
/* The smear decays in discrete drops rather than fading — a trail of held
   frames, the way a slow phosphor actually reads. */
@keyframes wm-smear-band {
  0%   { transform: translateX(0.2188em); }
  30%  { transform: translateX(0.2188em); }
  45%  { transform: translateX(0.0781em); }
  60%, 100% { transform: translateX(0); }
}
@keyframes wm-smear-ghost {
  0%   { opacity: 0.85; transform: translateX(0); }
  30%  { opacity: 0.5;  transform: translateX(0.0625em); }
  45%  { opacity: 0.25; transform: translateX(0.125em); }
  60%, 100% { opacity: 0; transform: translateX(0); }
}

/* ===================================================================== */
/* fx-hue-shift-tear — port of lot-e v8. Same middle-band tear again, but
   the displaced band jumps to a guest hue for the instant it's out. */
/* ===================================================================== */

.wm.fx-hue-shift-tear.glitch .base,
.wm.fx-hue-shift-tear.peak .base {
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 36%, transparent 36%, transparent 70%, black 70%, black 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 36%, transparent 36%, transparent 70%, black 70%, black 100%);
}
.wm.fx-hue-shift-tear.glitch .l1, .wm.fx-hue-shift-tear.peak .l1 { clip-path: inset(36% 0 30% 0); opacity: 1; }
.wm.fx-hue-shift-tear.glitch .l1 { animation: wm-hue-tear 420ms steps(1, end) 1; }
.wm.fx-hue-shift-tear.peak .l1 { animation: none; transform: translateX(0.1719em); color: var(--p-coral); text-shadow: none; }
/* The colour has to snap. Interpolated, amber crosses through orange on its
   way to coral and the eye reads a tint change instead of a wrong signal. */
@keyframes wm-hue-tear {
  0%   { transform: translateX(0.1719em); color: var(--p-coral); text-shadow: none; }
  38%  { transform: translateX(-0.0938em); color: var(--p-coral); text-shadow: none; }
  58%  { transform: translateX(0); color: var(--p-amber); text-shadow: var(--bloom); }
  100% { transform: translateX(0); color: var(--p-amber); text-shadow: var(--bloom); }
}

/* ===================================================================== */
/* fx-band-split-guns — port of lot-h lh-v1. Three bands, three guns: coral
   top, amber middle, cyan bottom, screen-blended over the whole word. */
/* ===================================================================== */

.wm.fx-band-split-guns.glitch .l1, .wm.fx-band-split-guns.peak .l1 { clip-path: inset(0% 0 66% 0); color: var(--p-coral); mix-blend-mode: screen; }
.wm.fx-band-split-guns.glitch .l2, .wm.fx-band-split-guns.peak .l2 { clip-path: inset(33% 0 33% 0); color: var(--p-amber); mix-blend-mode: screen; }
.wm.fx-band-split-guns.glitch .l3, .wm.fx-band-split-guns.peak .l3 { clip-path: inset(66% 0 0% 0); color: var(--p-cyan); mix-blend-mode: screen; }
.wm.fx-band-split-guns.glitch .l1 { animation: wm-guns-top 620ms steps(1, end) 1; }
.wm.fx-band-split-guns.glitch .l2 { animation: wm-guns-mid 620ms steps(1, end) 1; }
.wm.fx-band-split-guns.glitch .l3 { animation: wm-guns-bot 620ms steps(1, end) 1; }
.wm.fx-band-split-guns.peak .l1 { animation: none; opacity: 1; transform: translateX(-0.1562em); }
.wm.fx-band-split-guns.peak .l2 { animation: none; opacity: 0.55; transform: translateX(0); }
.wm.fx-band-split-guns.peak .l3 { animation: none; opacity: 1; transform: translateX(0.1406em); }
@keyframes wm-guns-top { 0% { opacity: 0; transform: translateX(0); } 20% { opacity: 1; transform: translateX(-0.1562em); } 60% { opacity: 1; transform: translateX(0.0938em); } 80%, 100% { opacity: 0; transform: translateX(0); } }
@keyframes wm-guns-mid { 0% { opacity: 0; } 20%, 70% { opacity: 0.55; } 85%, 100% { opacity: 0; } }
@keyframes wm-guns-bot { 0% { opacity: 0; transform: translateX(0); } 20% { opacity: 1; transform: translateX(0.1406em); } 60% { opacity: 1; transform: translateX(-0.1094em); } 80%, 100% { opacity: 0; transform: translateX(0); } }

/* ===================================================================== */
/* fx-standing-fringe — port of lot-h lh-v2. The source keeps a permanent
   faint mis-registration; rule 1 forbids that here, so this ports it as the
   event instead — the fringe opens out of nothing and closes again, and it
   opens slower than the other faults so it reads as convergence drifting,
   not tearing. */
/* ===================================================================== */

.wm.fx-standing-fringe.glitch .fr1, .wm.fx-standing-fringe.peak .fr1 { color: var(--p-coral); mix-blend-mode: screen; }
.wm.fx-standing-fringe.glitch .fr2, .wm.fx-standing-fringe.peak .fr2 { color: var(--p-cyan); mix-blend-mode: screen; }
.wm.fx-standing-fringe.glitch .fr1 { animation: wm-fringe-cr 700ms steps(1, end) 1; }
.wm.fx-standing-fringe.glitch .fr2 { animation: wm-fringe-cy 700ms steps(1, end) 1; }
.wm.fx-standing-fringe.peak .fr1 { animation: none; opacity: 0.9; transform: translateX(0.1719em); }
.wm.fx-standing-fringe.peak .fr2 { animation: none; opacity: 0.9; transform: translateX(-0.1875em); }
/* The guns snap apart, hold, jump again, then snap shut. The two hues break
   at different moments so the pair never looks choreographed. */
@keyframes wm-fringe-cr {
  0%   { opacity: 0.9; transform: translateX(0.1719em); }
  26%  { opacity: 0.9; transform: translateX(0.2188em); }
  48%  { opacity: 0.9; transform: translateX(0.0938em); }
  64%, 100% { opacity: 0; transform: translateX(0); }
}
@keyframes wm-fringe-cy {
  0%   { opacity: 0.9; transform: translateX(-0.1875em); }
  32%  { opacity: 0.9; transform: translateX(-0.0938em); }
  52%  { opacity: 0.9; transform: translateX(-0.2344em); }
  64%, 100% { opacity: 0; transform: translateX(0); }
}

/* ===================================================================== */
/* fx-damage-only-colour — port of lot-h lh-v3. Clean amber everywhere;
   colour only shows up inside the torn band itself. */
/* ===================================================================== */

.wm.fx-damage-only-colour.glitch .base,
.wm.fx-damage-only-colour.peak .base {
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 38%, transparent 38%, transparent 62%, black 62%, black 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 38%, transparent 38%, transparent 62%, black 62%, black 100%);
}
.wm.fx-damage-only-colour.glitch .fr1, .wm.fx-damage-only-colour.peak .fr1 { clip-path: inset(38% 0 38% 0); color: var(--p-coral); mix-blend-mode: screen; }
.wm.fx-damage-only-colour.glitch .fr2, .wm.fx-damage-only-colour.peak .fr2 { clip-path: inset(38% 0 38% 0); color: var(--p-cyan); mix-blend-mode: screen; }
.wm.fx-damage-only-colour.glitch .fr1 { animation: wm-damage-cr 520ms steps(1, end) 1; }
.wm.fx-damage-only-colour.glitch .fr2 { animation: wm-damage-cy 520ms steps(1, end) 1; }
.wm.fx-damage-only-colour.peak .fr1 { animation: none; opacity: 1; transform: translateX(-0.2031em); }
.wm.fx-damage-only-colour.peak .fr2 { animation: none; opacity: 1; transform: translateX(0.2031em); }
@keyframes wm-damage-cr { 0%, 30% { opacity: 0; transform: translateX(0); } 40% { opacity: 1; transform: translateX(-0.2031em); } 85% { opacity: 1; transform: translateX(-0.2031em); } 95%, 100% { opacity: 0; transform: translateX(0); } }
@keyframes wm-damage-cy { 0%, 30% { opacity: 0; transform: translateX(0); } 40% { opacity: 1; transform: translateX(0.2031em); } 85% { opacity: 1; transform: translateX(0.2031em); } 95%, 100% { opacity: 0; transform: translateX(0); } }

/* ===================================================================== */
/* fx-crossed-axes — port of lot-h lh-v4. The tear splits the word up/down
   while the colour separation runs sideways across the whole thing. */
/* ===================================================================== */

.wm.fx-crossed-axes.glitch .base,
.wm.fx-crossed-axes.peak .base { opacity: 0; }
.wm.fx-crossed-axes.glitch .l1, .wm.fx-crossed-axes.peak .l1 { clip-path: inset(0 50% 0 0); opacity: 1; }
.wm.fx-crossed-axes.glitch .l2, .wm.fx-crossed-axes.peak .l2 { clip-path: inset(0 0 0 50%); opacity: 1; }
.wm.fx-crossed-axes.glitch .fr1, .wm.fx-crossed-axes.peak .fr1 { color: var(--p-coral); mix-blend-mode: screen; }
.wm.fx-crossed-axes.glitch .fr2, .wm.fx-crossed-axes.peak .fr2 { color: var(--p-cyan); mix-blend-mode: screen; }
.wm.fx-crossed-axes.glitch .l1 { animation: wm-cross-left 620ms steps(1, end) 1; }
.wm.fx-crossed-axes.glitch .l2 { animation: wm-cross-right 620ms steps(1, end) 1; }
.wm.fx-crossed-axes.glitch .fr1 { animation: wm-cross-cr 620ms steps(1, end) 1; }
.wm.fx-crossed-axes.glitch .fr2 { animation: wm-cross-cy 620ms steps(1, end) 1; }
.wm.fx-crossed-axes.peak .l1 { animation: none; transform: translateY(-0.1094em); }
.wm.fx-crossed-axes.peak .l2 { animation: none; transform: translateY(0.1094em); }
.wm.fx-crossed-axes.peak .fr1 { animation: none; opacity: 0.85; transform: translateX(-0.125em); }
.wm.fx-crossed-axes.peak .fr2 { animation: none; opacity: 0.85; transform: translateX(0.125em); }
@keyframes wm-cross-left  { 0%, 55% { transform: translateY(0); } 65%, 80% { transform: translateY(-0.1094em); } 90%, 100% { transform: translateY(0); } }
@keyframes wm-cross-right { 0%, 55% { transform: translateY(0); } 65%, 80% { transform: translateY(0.1094em); } 90%, 100% { transform: translateY(0); } }
@keyframes wm-cross-cr { 0%, 55% { opacity: 0; transform: translateX(0); } 65%, 80% { opacity: 0.85; transform: translateX(-0.125em); } 90%, 100% { opacity: 0; transform: translateX(0); } }
@keyframes wm-cross-cy { 0%, 55% { opacity: 0; transform: translateX(0); } 65%, 80% { opacity: 0.85; transform: translateX(0.125em); } 90%, 100% { opacity: 0; transform: translateX(0); } }

/* ===================================================================== */
/* fx-shredded — port of lot-h lh-v6. Eight thin bands, eight guest hues,
   each kicking sideways by its own amount, screen-blended over the word. */
/* ===================================================================== */

.wm.fx-shredded.glitch .l1, .wm.fx-shredded.peak .l1 { clip-path: inset(0%     0 87.5%  0); color: var(--p-coral);  mix-blend-mode: screen; --dx: -0.2031em; }
.wm.fx-shredded.glitch .l2, .wm.fx-shredded.peak .l2 { clip-path: inset(12.5%  0 75%    0); color: var(--p-lime);   mix-blend-mode: screen; --dx: 0.1406em; }
.wm.fx-shredded.glitch .l3, .wm.fx-shredded.peak .l3 { clip-path: inset(25%    0 62.5%  0); color: var(--p-jade);   mix-blend-mode: screen; --dx: -0.0938em; }
.wm.fx-shredded.glitch .l4, .wm.fx-shredded.peak .l4 { clip-path: inset(37.5%  0 50%    0); color: var(--p-cyan);   mix-blend-mode: screen; --dx: 0.2344em; }
.wm.fx-shredded.glitch .l5, .wm.fx-shredded.peak .l5 { clip-path: inset(50%    0 37.5%  0); color: var(--p-violet); mix-blend-mode: screen; --dx: -0.1719em; }
.wm.fx-shredded.glitch .l6, .wm.fx-shredded.peak .l6 { clip-path: inset(62.5%  0 25%    0); color: var(--p-rose);   mix-blend-mode: screen; --dx: 0.1094em; }
.wm.fx-shredded.glitch .l7, .wm.fx-shredded.peak .l7 { clip-path: inset(75%    0 12.5%  0); color: var(--p-coral);  mix-blend-mode: screen; --dx: -0.25em; }
.wm.fx-shredded.glitch .l8, .wm.fx-shredded.peak .l8 { clip-path: inset(87.5%  0 0%     0); color: var(--p-cyan);   mix-blend-mode: screen; --dx: 0.1562em; }
.wm.fx-shredded.glitch .l1, .wm.fx-shredded.glitch .l2, .wm.fx-shredded.glitch .l3, .wm.fx-shredded.glitch .l4,
.wm.fx-shredded.glitch .l5, .wm.fx-shredded.glitch .l6, .wm.fx-shredded.glitch .l7, .wm.fx-shredded.glitch .l8 {
  animation: wm-shred 700ms steps(1, end) 1;
}
.wm.fx-shredded.peak .l1, .wm.fx-shredded.peak .l2, .wm.fx-shredded.peak .l3, .wm.fx-shredded.peak .l4,
.wm.fx-shredded.peak .l5, .wm.fx-shredded.peak .l6, .wm.fx-shredded.peak .l7, .wm.fx-shredded.peak .l8 {
  animation: none; opacity: 0.95; transform: translateX(var(--dx));
}
@keyframes wm-shred {
  0%, 55%  { opacity: 0; transform: translateX(0); }
  65%      { opacity: 0.95; transform: translateX(var(--dx)); }
  85%      { opacity: 0.95; transform: translateX(calc(var(--dx) * -0.4)); }
  95%, 100% { opacity: 0; transform: translateX(0); }
}

/* ===================================================================== */
/* fx-interlace — port of lot-f v-interlace. Alternate scanlines drop for a
   frame, using the shared .scan overlay. */
/* ===================================================================== */

.wm.fx-interlace.glitch .scan,
.wm.fx-interlace.peak .scan {
  background: repeating-linear-gradient(to bottom, var(--p-panel) 0 0.0156em, transparent 0.0156em 0.0469em);
}
.wm.fx-interlace.glitch .scan { animation: wm-interlace 520ms steps(1, end) 1; }
.wm.fx-interlace.peak .scan { animation: none; opacity: 0.85; background-position: 0 0.0312em; }
@keyframes wm-interlace {
  0%   { opacity: 0; background-position: 0 0; }
  40%  { opacity: 0.85; background-position: 0 0; }
  55%  { opacity: 0.85; background-position: 0 0.0312em; }
  80%  { opacity: 0.85; background-position: 0 0; }
  100% { opacity: 0; background-position: 0 0; }
}

/* ===================================================================== */
/* fx-dead-cells — port of lot-f v-deadpix. Two characters, picked
   positionally since the markup carries no per-cell classes here: the 3rd
   (C) goes dark, the 7th (the first 4) shows a wrong glyph over it, and
   both heal before the window closes. */
/* ===================================================================== */

.wm.fx-dead-cells.glitch .ch:nth-child(3) { animation: wm-dead-blank 1100ms steps(1, end) 1; }
.wm.fx-dead-cells.peak .ch:nth-child(3) { color: var(--p-panel); text-shadow: none; }
@keyframes wm-dead-blank {
  0%, 10%  { color: var(--p-amber); text-shadow: var(--bloom); }
  20%, 65% { color: var(--p-panel); text-shadow: none; }
  80%, 100% { color: var(--p-amber); text-shadow: var(--bloom); }
}

.wm.fx-dead-cells.glitch .ch:nth-child(7)::after,
.wm.fx-dead-cells.peak .ch:nth-child(7)::after {
  content: "X";
  position: absolute;
  inset: 0;
  background: var(--p-panel);
  color: var(--p-pale);
  opacity: 0;
}
.wm.fx-dead-cells.glitch .ch:nth-child(7)::after { animation: wm-dead-wrong 1100ms steps(1, end) 1; }
.wm.fx-dead-cells.peak .ch:nth-child(7)::after { opacity: 1; }
@keyframes wm-dead-wrong {
  0%, 30%  { opacity: 0; }
  40%, 75% { opacity: 1; }
  90%, 100% { opacity: 0; }
}
