/* ---------------------------------------------------------------------
 * Retro CRT / synthwave theme, ported from design.html. Two parts:
 *
 *   1. The theme engine itself (variables, body typography, the #scene
 *      background, the #boot screen, the #swap cable-transition overlay,
 *      the scanline overlay) -- copied close to verbatim from design.html;
 *      see theme.js for the matching boot/toggle/swap script and
 *      index.html for the matching markup.
 *   2. A mapping of that theme onto Bootstrap 5.3's CSS custom properties
 *      (--bs-body-bg, --bs-primary, etc.), plus a handful of explicit
 *      component overrides for the pieces Bootstrap's shipped CSS bakes
 *      as literal hex rather than deriving from those variables at
 *      runtime (buttons, the form-select/navbar-toggler icons) -- same
 *      "the cascade alone isn't enough" situation the old Solarized
 *      version of this file already ran into for .btn-primary.
 * --------------------------------------------------------------------- */

/* Night: green phosphor CRT (default) */
:root {
  --bg: #000;
  --fg: #0f0;
  --glow: rgba(0, 255, 0, 0.8);
  --scanline: rgba(0, 0, 0, 0.3);
  --btn-bg: rgba(0, 255, 0, 0.08);

  /* synthwave scene */
  --sky-top: #14031f;
  --sky-bottom: #4a1152;
  --sun-top: #ffd36e;
  --sun-bottom: #ff2d95;
  --rock-top: #6b3b22;
  --rock-bottom: #24120a;
  --rock-line: #c9713f;
  --grid-line: rgba(255, 45, 149, 0.55);
  --horizon-glow: rgba(255, 45, 149, 0.65);
  --ground: #0a0110;
  --scene-opacity: 1;

  /* accents Bootstrap components (alerts, badges, "broadcast" highlights)
     need on top of the theme's core two-tone fg/bg -- reused from the
     scene palette above rather than inventing new hues. */
  --danger: #ff3b3b;
  --accent2: var(--sun-bottom);
  /* A third badge hue (see .bitsiphon-signal-path_scan) -- explicitly
     tuned per theme for contrast against --bg the way --danger already
     is, rather than reused from the scene palette above: --sun-top in
     particular is a near-white pale cream in the day theme, meant to
     glow softly behind the synthwave sun, not to stand alone as
     legible badge text. */
  --warn: #ffb000;
}

/* Day: amber ink on warm paper */
:root[data-theme="day"] {
  --bg: #d8d0b8;
  --fg: #3a2c10;
  --glow: rgba(180, 120, 20, 0.45);
  --scanline: rgba(60, 40, 10, 0.12);
  --btn-bg: rgba(58, 44, 16, 0.06);

  --sky-top: #f6e3c0;
  --sky-bottom: #f0b47a;
  --sun-top: #fff3c4;
  --sun-bottom: #ff8f5e;
  --rock-top: #b08256;
  --rock-bottom: #7d5334;
  --rock-line: #e0a978;
  --grid-line: rgba(150, 78, 40, 0.45);
  --horizon-glow: rgba(255, 143, 94, 0.6);
  --ground: #e3cfa8;
  --scene-opacity: 0.75;

  --danger: #b3261e;
  --accent2: var(--sun-bottom);
  --warn: #8a5a00;
}

/* Bootstrap's whole type scale (headings, buttons, form controls, spacing
   utilities) is defined in rem, relative to this -- bumping it, rather
   than body's px size alone, is what makes "bigger" apply consistently
   site-wide instead of just to plain paragraph text. */
html {
  font-size: 20px;
}

body {
  background-color: var(--bg);
  color: var(--fg);
  /* VT323 reads noticeably bolder/bigger than Courier New at the same
     size (its foundry recommends sizing it up to compensate for its
     small x-height), which is what actually fixes the CRT glow turning
     small Courier text into a blur. */
  font-family: 'VT323', 'Courier New', monospace;
  letter-spacing: 0.5px;
  text-shadow: 0 0 10px var(--glow);
  transition: background-color 0.3s, color 0.3s;
}

/* Press Start 2P: true pixel-art bitmap font, reserved for the brand mark
   and h1 -- its per-character width is roughly triple a normal font's, so
   it only stays legible for short, fixed chrome strings ("Weblog",
   "Admin: Users"). h2 and below are also used for dynamic, potentially
   long user content (topic titles in topic-list.html/topic-detail.html),
   so they keep the body's VT323 instead. */
h1, .navbar-brand {
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  letter-spacing: 0.5px;
  line-height: 1.6;
}

h2, h3, h4, h5, h6 {
  font-weight: normal;
  letter-spacing: 0.5px;
}

/* Two-tone service wordmarks (Bitsiphon, Synthdrop, Wirehead, ...):
   splits a compound service name across its two halves in the theme's two
   core hues -- --fg (the page's own foreground) and --accent2 (the
   synthwave accent already used for room codes, badges, etc.) -- reading
   as a small piece of corp-style branding rather than a single flat-
   colored heading. Deliberately NOT applied to the navbar's own
   ".navbar-brand" WEBLOG mark: theme.js periodically replaces that
   element's textContent wholesale for its glitch/hieroglyph effects,
   which would collapse any nested spans back into plain text the next
   time it fires.
   .brand-a inherits the heading's own color rather than repeating --fg
   explicitly, so this still respects a more specific color rule elsewhere
   (e.g. a title that's also wrapped in a themed link). */
.brand-b {
  color: var(--accent2);
  text-shadow: 0 0 10px var(--accent2);
}

/* ---------- synthwave background ---------- */

#scene {
  position: fixed;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  opacity: var(--scene-opacity);
  pointer-events: none;
}

#scene .sky {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 58%;
  background: linear-gradient(to bottom, var(--sky-top), var(--sky-bottom));
}

#scene .sun {
  position: absolute;
  left: 50%;
  top: 58%;
  width: 42vmin;
  height: 42vmin;
  margin-left: -21vmin;
  margin-top: -30vmin;
  border-radius: 50%;
  background:
    repeating-linear-gradient(
      to bottom,
      transparent 0 62%,
      rgba(0, 0, 0, 0.55) 62% 64%,
      transparent 64% 70%,
      rgba(0, 0, 0, 0.55) 70% 73%,
      transparent 73% 79%,
      rgba(0, 0, 0, 0.55) 79% 83%,
      transparent 83% 89%,
      rgba(0, 0, 0, 0.55) 89% 94%
    ),
    linear-gradient(to bottom, var(--sun-top), var(--sun-bottom));
  filter: blur(0.4px);
}

#scene .ridge {
  position: absolute;
  left: 0;
  width: 100%;
  bottom: 42%;
  height: 26vmin;
  display: block;
}

#scene .ground {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 42%;
  background: var(--ground);
  overflow: hidden;
  /* vh-relative (not a fixed px value) so this stays proportional to
     .ground's own height (42% of the viewport) on any screen -- see the
     .grid comment below for why the ratio between this and .grid's
     rotateX() angle matters, not just this value in isolation. */
  perspective: 65vh;
  perspective-origin: 50% 0%;
}

#scene .ground::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--horizon-glow);
  box-shadow: 0 0 24px 6px var(--horizon-glow);
  z-index: 1;
}

#scene .grid {
  position: absolute;
  top: 0;
  left: -50%;
  width: 200%;
  height: 100%;
  background-image:
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px);
  background-size: 4% 40px;
  /* 60deg, not the far more extreme angle this originally shipped with:
     Chromium renders a CSS 3D transform's far side past the perspective
     plane's near-clip boundary anyway (lenient/non-conformant), but
     WebKit (Safari/iOS) enforces that clip -- at rotateX(85deg) with a
     perspective this shallow, most of .grid's own height (it's 100% of
     .ground) projects past that boundary and never draws at all, which is
     exactly what "doesn't fit the screen" on an iPhone was: almost the
     entire floor grid silently missing, only a sliver near the horizon
     ever rendering, everywhere except Chromium. 60deg keeps the whole
     element inside the safe (pre-clip) range on every engine, verified
     directly against WebKit -- still a clearly receding perspective
     floor, just not one relying on a rendering discrepancy between
     browsers to appear at all.
     Both this and perspective's 65vh above scale off the same viewport
     height .ground itself does (42vh) -- what actually keeps this angle
     safe on any screen size is their ratio (needs perspective >
     .ground's height * sin(rotateX)), not either value alone, so both
     have to move together if either one is retuned. */
  transform: rotateX(60deg);
  transform-origin: 50% 0;
  animation: grid-scroll 1.2s linear infinite;
}

@keyframes grid-scroll {
  from { background-position: 0 0; }
  to   { background-position: 0 40px; }
}

@media (prefers-reduced-motion: reduce) {
  #scene .grid { animation: none; }
}

/* ---------- per-route themed scene layers ----------
   All three live in the DOM permanently (see index.html) and are switched
   purely by the scene-<name> class ShellController puts on #scene, so
   routing between them costs nothing but a class change -- no re-parse of
   a few hundred SVG nodes on every navigation. Each replaces the default
   .ridge (the generic synthwave mountains) while keeping the shared sky,
   sun and perspective grid, so every route still reads as the same world
   rather than an unrelated backdrop per page. */

#scene .scene-layer {
  position: absolute;
  display: none;
  pointer-events: none;
}

#scene.scene-wirehead .ridge,
#scene.scene-aegis .ridge,
#scene.scene-synthdrop .ridge {
  display: none;
}

#scene.scene-wirehead .scene-origami,
#scene.scene-aegis .scene-ziggurat,
#scene.scene-synthdrop .scene-street {
  display: block;
}

/* ---- WIREHEAD: Gaff's origami unicorn ---- */

/* Held back to the same degree as the other two themed layers, and for
   the same reason: the wirehead's message list sits directly over this. */
#scene .scene-origami {
  left: 50%;
  bottom: 44%;
  width: 38vmin;
  height: 29vmin;
  margin-left: -19vmin;
  opacity: 0.72;
  filter: drop-shadow(0 0 12px var(--horizon-glow));
}

/* The alternating fold opacities are the whole effect: one flat gradient
   read through three different facet brightnesses is what makes creased
   paper rather than a silhouette. */
#scene .scene-origami .fold-a { opacity: 1; }
#scene .scene-origami .fold-b { opacity: 0.88; }
#scene .scene-origami .fold-c { opacity: 0.76; }

#scene .scene-origami .origami-figure {
  transform-origin: 100px 75px;
  animation: origami-drift 11s ease-in-out infinite;
}

@keyframes origami-drift {
  0%, 100% { transform: translateY(0) rotate(-1.5deg); }
  50%      { transform: translateY(-8px) rotate(1.5deg); }
}

/* ---- AEGIS: the 2049 ziggurat ---- */

/* Stands ON the horizon (bottom: 42% is where .ground's top edge is), the
   same place the default .ridge occupies -- that's the composition the
   base scene already proves works. Held well under full opacity because
   page content sits directly on top of it: a background that competes
   with the text in front of it is just noise. */
#scene .scene-ziggurat {
  left: 50%;
  bottom: 42%;
  width: 70vmin;
  height: 26vmin;
  margin-left: -35vmin;
  opacity: 0.7;
}

#scene .scene-ziggurat .ziggurat-haze {
  opacity: 0.35;
  filter: blur(6px);
}

#scene .scene-ziggurat .ziggurat-ledges {
  opacity: 0.8;
}

/* Window rows flicker as a group rather than individually -- a whole
   floor's lights dipping together reads as a building on a failing grid,
   which is the intent; per-window randomness would just look like noise. */
#scene .scene-ziggurat .ziggurat-windows {
  opacity: 0.75;
  animation: ziggurat-windows-flicker 7s steps(1) infinite;
}

@keyframes ziggurat-windows-flicker {
  0%, 40%, 44%, 70%, 74%, 100% { opacity: 0.75; }
  42%, 72% { opacity: 0.3; }
}

#scene .scene-ziggurat .ziggurat-beacon {
  animation: ziggurat-beacon-pulse 2.6s ease-in-out infinite;
}

@keyframes ziggurat-beacon-pulse {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 1; }
}

/* ---- SYNTHDROP: neon street skyline ---- */

/* Also anchored to the horizon, and deliberately NOT a full-height canyon
   wrapped around the viewport: towers cluster left and right and stay low
   through the middle, so the synthwave sun reads down the centre as the
   street itself -- and, just as importantly, the centred content column
   has nothing bright behind it. */
#scene .scene-street {
  left: 0;
  right: 0;
  bottom: 42%;
  width: 100%;
  height: 24vmin;
  opacity: 0.5;
}

#scene .scene-street text {
  font-family: 'VT323', 'Courier New', monospace;
  font-size: 6px;
  letter-spacing: 0.5px;
  text-anchor: middle;
  fill: var(--sun-bottom);
}

#scene .scene-street .street-text-h {
  font-size: 5px;
  letter-spacing: 0.3px;
}

#scene .scene-street .street-sign-militech text { fill: var(--grid-line); }
#scene .scene-street .street-sign-kanji text { fill: var(--sun-top); }

/* Each sign gets its own flicker cadence and phase so the street never
   pulses in unison -- the staggered negative delays are what keep three
   identical keyframes from reading as one synchronized blink. */
#scene .scene-street .street-sign-arasaka {
  animation: street-neon-flicker 5.5s steps(1) infinite;
}
#scene .scene-street .street-sign-militech {
  animation: street-neon-flicker 4.2s steps(1) infinite;
  animation-delay: -1.7s;
}
#scene .scene-street .street-sign-kanji {
  animation: street-neon-flicker 6.8s steps(1) infinite;
  animation-delay: -3.1s;
}

@keyframes street-neon-flicker {
  0%, 88%, 91%, 94%, 100% { opacity: 0.9; }
  89%, 92% { opacity: 0.3; }
}

#scene .scene-street .street-windows {
  opacity: 0.8;
  animation: ziggurat-windows-flicker 9s steps(1) infinite;
}

#scene .scene-street .street-rain {
  animation: street-rain-fall 0.9s linear infinite;
}

@keyframes street-rain-fall {
  from { transform: translateY(0); }
  to   { transform: translateY(40px); }
}

@media (prefers-reduced-motion: reduce) {
  #scene .scene-origami .origami-figure,
  #scene .scene-ziggurat .ziggurat-windows,
  #scene .scene-ziggurat .ziggurat-beacon,
  #scene .scene-street .street-sign,
  #scene .scene-street .street-windows,
  #scene .scene-street .street-rain {
    animation: none;
  }
}

/* Real page content (navbar + routed view) sits above #scene. */
.navbar,
.container.mt-4 {
  position: relative;
  z-index: 1;
}

#theme-toggle {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 10000;
  background: var(--btn-bg);
  color: var(--fg);
  border: 1px solid var(--fg);
  font-family: inherit;
  font-size: 14px;
  letter-spacing: 2px;
  text-transform: uppercase;
  padding: 8px 14px;
  cursor: pointer;
  text-shadow: 0 0 8px var(--glow);
  box-shadow: 0 0 8px var(--glow);
  backdrop-filter: blur(4px);
}

#theme-toggle:hover {
  background: var(--fg);
  color: var(--bg);
  text-shadow: none;
}

/* ---------- boot / loading screen ---------- */

#boot {
  position: fixed;
  inset: 0;
  z-index: 10001;
  background: var(--bg);
  color: var(--fg);
  padding: 6vh 6vw;
  font-family: inherit;
  font-size: 15px;
  line-height: 1.7;
  letter-spacing: 2px;
  text-shadow: 0 0 10px var(--glow);
  overflow: hidden;
  transition: opacity 0.7s ease;
}

#boot.done {
  opacity: 0;
  pointer-events: none;
}

#boot-log {
  font-family: inherit;
  white-space: pre-wrap;
  margin: 0;
}

#boot .cursor {
  display: inline-block;
  width: 0.6em;
  background: var(--fg);
  animation: blink 1s step-end infinite;
}

#boot .cursor::before { content: '\00a0'; }

@keyframes blink {
  50% { opacity: 0; }
}

html.booting body { overflow: hidden; }

#boot .crt-on {
  position: absolute;
  inset: 0;
  background: var(--bg);
  transform-origin: 50% 50%;
  animation: crt-on 0.7s ease-out forwards;
}

@keyframes crt-on {
  0%   { transform: scaleY(0.004) scaleX(1);   filter: brightness(6); }
  45%  { transform: scaleY(0.004) scaleX(1);   filter: brightness(6); }
  70%  { transform: scaleY(1) scaleX(1);       filter: brightness(2); }
  100% { transform: scaleY(1) scaleX(1);       filter: brightness(1); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  #boot .crt-on { animation: none; opacity: 0; }
  #boot .cursor { animation: none; }
}

/* ---------- cable-swap theme transition ---------- */

#swap {
  position: fixed;
  inset: 0;
  z-index: 10002;
  display: none;
  overflow: hidden;
  pointer-events: none;
}

#swap.active { display: block; }

#swap .bar {
  position: absolute;
  left: 0;
  right: 0;
  height: 51%;
  background: #04040a;
  transition: transform 170ms cubic-bezier(0.6, 0, 0.9, 0.4);
}

#swap .bar.top { top: 0; transform: translateY(-101%); }
#swap .bar.bot { bottom: 0; transform: translateY(101%); }

#swap.closed .bar.top,
#swap.closed .bar.bot { transform: translateY(0); }

#swap.opening .bar {
  transition: transform 190ms cubic-bezier(0.1, 0.7, 0.4, 1);
}

#swap .line {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 2px;
  margin-top: -1px;
  background: #fff;
  box-shadow: 0 0 20px 5px rgba(255, 255, 255, 0.9);
  opacity: 0;
  transform: scaleX(1);
  transition: opacity 130ms ease, transform 130ms ease-in;
}

#swap.closed .line { opacity: 1; }

#swap.dead .line {
  opacity: 0;
  transform: scaleX(0.015);
}

#swap .snow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  opacity: 0;
  transition: opacity 120ms ease;
}

#swap.dead .snow { opacity: 0.5; }

#swap .rollbar,
#swap .resync-band {
  position: absolute;
  left: 0;
  right: 0;
  height: 14%;
  background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.12), transparent);
  opacity: 0;
}

#swap.dead .rollbar {
  opacity: 1;
  animation: roll 0.85s linear infinite;
}

#swap.resync .resync-band {
  opacity: 1;
  animation: roll 0.3s linear 2;
}

@keyframes roll {
  from { top: -16%; }
  to   { top: 100%; }
}

#swap .osd {
  position: absolute;
  top: 14%;
  left: 9%;
  padding: 14px 22px;
  border: 2px solid rgba(255, 255, 255, 0.85);
  color: #fff;
  font-family: 'Courier New', monospace;
  font-size: 15px;
  letter-spacing: 4px;
  line-height: 1.9;
  text-shadow: 0 0 10px rgba(255, 255, 255, 0.7);
  opacity: 0;
  transition: opacity 120ms ease;
}

#swap.dead .osd {
  opacity: 1;
  animation: osd-blink 1s step-end infinite;
}

@keyframes osd-blink {
  50% { opacity: 0.3; }
}

#swap .flash {
  position: absolute;
  inset: 0;
  background: #fff;
  opacity: 0;
}

#swap.flashing .flash {
  animation: flash 280ms ease-out forwards;
}

@keyframes flash {
  from { opacity: 0.5; }
  to   { opacity: 0; }
}

/* Scan lines effect */
body::after {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: repeating-linear-gradient(
    0deg,
    var(--scanline),
    var(--scanline) 1px,
    transparent 1px,
    transparent 2px
  );
  pointer-events: none;
  z-index: 9999;
}

/* =======================================================================
 * Bootstrap 5.3 mapping -- lets the shipped CDN CSS pick up the theme
 * above without a Sass rebuild, same approach (and same caveat about a
 * few components baking literal hex instead of reading these variables)
 * the old Solarized version of this file used.
 * ===================================================================== */

:root {
  color-scheme: dark;

  --bs-body-bg: var(--bg);
  --bs-body-color: var(--fg);
  --bs-emphasis-color: var(--fg);
  --bs-secondary-color: color-mix(in srgb, var(--fg) 65%, transparent);
  --bs-secondary-bg: color-mix(in srgb, var(--fg) 10%, var(--bg));
  --bs-tertiary-color: color-mix(in srgb, var(--fg) 55%, transparent);
  --bs-tertiary-bg: color-mix(in srgb, var(--fg) 6%, var(--bg));
  --bs-border-color: color-mix(in srgb, var(--fg) 35%, transparent);
  --bs-link-color: var(--fg);
  --bs-link-color-rgb: 0, 255, 0;
  --bs-link-hover-color: var(--fg);

  --bs-primary: var(--fg);
  --bs-secondary: var(--fg);
  --bs-success: var(--fg);
  --bs-danger: var(--danger);
  --bs-warning: var(--accent2);
  --bs-info: var(--accent2);

  /* Bootstrap 5.3's opacity utilities (.text-danger, .bg-success, etc.)
     read the *-rgb variant directly, not the hex variable above -- these
     are what actually make those utility classes (used standalone, e.g.
     topic-create.html's slug-availability text) pick up the theme instead
     of Bootstrap's own built-in defaults. */
  --bs-primary-rgb: 0, 255, 0;
  --bs-secondary-rgb: 0, 255, 0;
  --bs-success-rgb: 0, 255, 0;
  --bs-danger-rgb: 255, 59, 59;
  --bs-warning-rgb: 255, 45, 149;
  --bs-info-rgb: 255, 45, 149;

  --bs-danger-bg-subtle: color-mix(in srgb, var(--danger) 15%, var(--bg));
  --bs-danger-border-subtle: var(--danger);
  --bs-danger-text-emphasis: var(--danger);

  --bs-dark: var(--bg);
  --bs-dark-rgb: 0, 0, 0;
  --bs-light: var(--fg);
  --bs-light-rgb: 0, 255, 0;
}

:root[data-theme="day"] {
  color-scheme: light;
  --bs-link-color-rgb: 58, 44, 16;
  --bs-light-rgb: 58, 44, 16;

  --bs-primary-rgb: 58, 44, 16;
  --bs-secondary-rgb: 58, 44, 16;
  --bs-success-rgb: 58, 44, 16;
  --bs-danger-rgb: 179, 38, 30;
  --bs-warning-rgb: 255, 143, 94;
  --bs-info-rgb: 255, 143, 94;

  --bs-dark-rgb: 216, 208, 184;
}

/* Bootstrap's reboot sets a plain sans-serif body font; the theme's
   monospace/letter-spacing above needs to win everywhere, including
   inside components (buttons, inputs, tables) that otherwise reset
   font-family back to inherit-from-body anyway, but headings/labels set
   their own font-weight that's worth keeping legible against the glow. */
* {
  font-family: inherit;
}

a {
  text-shadow: 0 0 6px var(--glow);
}

a:hover {
  text-shadow: 0 0 12px var(--glow);
}

/* ---------- navbar ---------- */

.navbar {
  background-color: var(--bg) !important;
  border-bottom: 1px solid var(--fg);
  box-shadow: 0 2px 16px 0 var(--horizon-glow);
}

.navbar .navbar-brand,
.navbar .nav-link {
  color: var(--fg) !important;
  text-shadow: 0 0 8px var(--glow);
  text-transform: uppercase;
  letter-spacing: 1px;
}

.navbar .navbar-brand {
  font-size: 1rem;
}

/* ---------- ambient brand-mark RGB-desync glitch ----------
   .rgb-glitch is toggled on/off by theme.js's scheduleRgbGlitch at random
   intervals, entirely independent of the hieroglyph text-swap (also
   theme.js) -- pure decoration, never gates anything. The base element's
   own `color` is set !important two rules up, so this animates
   text-shadow/filter on it instead (an !important declaration always
   wins over a running animation on the same property, so `color` itself
   is left alone) and gets its red/blue fringes from two ::before/::after
   copies, which aren't subject to that !important at all since it's
   scoped to .navbar-brand itself, not its pseudo-elements. The base
   text's own --fg (green in the night theme) reads as the third,
   unshifted channel. */
.navbar-brand {
  position: relative;
  display: inline-block;
}

.navbar-brand.rgb-glitch {
  animation: brand-rgb-desync 900ms ease-in-out;
}

.navbar-brand.rgb-glitch::before,
.navbar-brand.rgb-glitch::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: screen;
}

.navbar-brand.rgb-glitch::before {
  color: #ff2fd0;
  animation: brand-rgb-desync-r 900ms ease-in-out;
}

.navbar-brand.rgb-glitch::after {
  color: #2fd0ff;
  animation: brand-rgb-desync-b 900ms ease-in-out;
}

@keyframes brand-rgb-desync {
  0%   { filter: blur(0); text-shadow: 0 0 8px var(--glow); }
  15%  { filter: blur(2px); text-shadow: 0 0 14px #ff2fd0; }
  40%  { filter: blur(1px); text-shadow: 0 0 10px #2fd0ff; }
  65%  { filter: blur(1.5px); text-shadow: 0 0 10px #ff2fd0; }
  100% { filter: blur(0); text-shadow: 0 0 8px var(--glow); }
}

@keyframes brand-rgb-desync-r {
  0%   { opacity: 0; filter: blur(0); transform: translate(0, 0); }
  15%  { opacity: 0.85; filter: blur(2px); transform: translate(0, 0); }
  40%  { opacity: 0.9; filter: blur(0.5px); transform: translate(-3px, 1px); }
  65%  { opacity: 0.8; filter: blur(1px); transform: translate(2px, -1px); }
  85%  { opacity: 0.4; transform: translate(-1px, 0); }
  100% { opacity: 0; filter: blur(0); transform: translate(0, 0); }
}

@keyframes brand-rgb-desync-b {
  0%   { opacity: 0; filter: blur(0); transform: translate(0, 0); }
  15%  { opacity: 0.85; filter: blur(2px); transform: translate(0, 0); }
  40%  { opacity: 0.9; filter: blur(0.5px); transform: translate(3px, -1px); }
  65%  { opacity: 0.8; filter: blur(1px); transform: translate(-2px, 1px); }
  85%  { opacity: 0.4; transform: translate(1px, 0); }
  100% { opacity: 0; filter: blur(0); transform: translate(0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .navbar-brand.rgb-glitch,
  .navbar-brand.rgb-glitch::before,
  .navbar-brand.rgb-glitch::after {
    animation: none;
  }
}

.navbar .nav-link {
  font-size: 1.15rem;
  /* VT323 has no bold weight of its own; a hair of stroke on top of the
     fill is what actually reads as "bold" for a single-weight font,
     without falling back to a synthetic-bold render that looks mushy at
     this size. */
  -webkit-text-stroke: 0.4px currentColor;
}

/* Inline icon glyphs (login key, logout power, search magnifier, etc.):
   a CSS mask keeps a single source SVG in sync with whichever theme is
   active via background-color: currentColor, instead of needing a
   separate hex-baked copy per theme the way .navbar-toggler-icon and
   .form-select below have to (mask-image, unlike background-image, is
   just alpha -- the SVG's own stroke color is irrelevant). */
.nav-icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-right: 0.35em;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* icon-only: the label text is still there for screen readers
   (.visually-hidden, from Bootstrap) and as a title-attribute tooltip,
   just not painted -- so the icon alone needs to stand on its own
   without a trailing label to lean on, hence no margin and a bit more
   size than the inline (icon + text) nav-icon default above. */
.icon-only .nav-icon {
  margin-right: 0;
  width: 1.3em;
  height: 1.3em;
}

.icon-key {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4'/%3E%3C/svg%3E");
}

.icon-power {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18.36 6.64a9 9 0 1 1-12.73 0'/%3E%3Cline x1='12' y1='2' x2='12' y2='12'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18.36 6.64a9 9 0 1 1-12.73 0'/%3E%3Cline x1='12' y1='2' x2='12' y2='12'/%3E%3C/svg%3E");
}

.icon-search {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}

.icon-plus {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E");
}

/* password-toggle (controllers.js): icon-eye is "currently hidden, click
   to reveal", icon-eye-off is the same eye shape with a slash added --
   built as the eye plus a plain diagonal line rather than a separate
   hand-drawn "crossed-out eye" path, since the two need to read as
   obviously the same glyph with one thing added, not two unrelated
   icons. */
.icon-eye {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
}

.icon-eye-off {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cline x1='2' y1='2' x2='22' y2='22'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cline x1='2' y1='2' x2='22' y2='22'/%3E%3C/svg%3E");
}

.password-toggle-btn .nav-icon {
  margin-right: 0;
  width: 1.1em;
  height: 1.1em;
}

.navbar .nav-link.disabled {
  opacity: 0.6;
  text-shadow: none;
}

.navbar .nav-link:hover,
.navbar .nav-link:focus {
  text-shadow: 0 0 16px var(--glow);
}

/* #theme-toggle (see below) is fixed top-right over everything, including
   the navbar -- design.html never had this problem since it had no
   navbar of its own. Reserve enough right-side clearance that neither
   the expanded nav-links row (>=992px, navbar-expand-lg's breakpoint) nor
   the collapsed hamburger toggler (below it) ever renders underneath it. */
@media (min-width: 992px) {
  .navbar > .container {
    padding-right: 150px;
  }
}

@media (max-width: 991.98px) {
  .navbar > .container {
    padding-right: 110px;
  }
}

/* #backend-status: OPERATIONAL/FLATLINED indicator, shown in the navbar on
   every screen -- see ShellController's pollBackendStatus (controllers.js)
   and StatusService (services.js). Three states (status-checking is the
   brief window before the first poll resolves), plus a one-shot
   status-flash class applied only on an actual transition (never on a
   poll that just reconfirms the same state), matching the "animation when
   it goes offline/online" ask without animating on every poll tick. The
   label text itself (shell.backendStatusLabel) runs its own katakana
   glitch/decrypt transition on the same trigger -- see
   scrambleBackendStatusLabel in controllers.js. */
#backend-status {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 3px 10px;
  border: 1px solid currentColor;
  font-size: 11px;
  letter-spacing: 1px;
  white-space: nowrap;
}

#backend-status .status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 6px currentColor;
  flex: 0 0 auto;
}

#backend-status.status-checking {
  color: color-mix(in srgb, var(--fg) 60%, transparent);
}

#backend-status.status-operational {
  color: var(--fg);
  text-shadow: 0 0 8px var(--glow);
}

#backend-status.status-flatlined {
  color: var(--danger);
  text-shadow: 0 0 8px color-mix(in srgb, var(--danger) 70%, transparent);
  animation: status-flatlined-pulse 1.2s ease-in-out infinite;
}

@keyframes status-flatlined-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* Plays once, on top of whatever the state's own steady-state rule above
   is already doing, exactly when shell.backendStatusFlash is briefly true
   -- a bright flash reads as "something just happened" regardless of
   which direction the transition went. */
#backend-status.status-flash {
  animation: status-flash 0.7s ease-out;
}

@keyframes status-flash {
  0% { filter: brightness(3.5); }
  100% { filter: brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  #backend-status.status-flatlined,
  #backend-status.status-flash {
    animation: none;
  }
}

.navbar-toggler {
  border-color: var(--fg);
}

.navbar-toggler:hover {
  box-shadow: 0 0 8px var(--glow);
}

.navbar-toggler:focus {
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fg) 35%, transparent);
}

/* Bootstrap bakes the hamburger icon as a fixed-color SVG (independent of
   --bs-body-color), so it needs an explicit per-theme copy of the same
   icon in each theme's own --fg hex -- can't reference a CSS variable
   from inside a background-image data URI. */
:root:not([data-theme="day"]) .navbar-toggler-icon {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%230f0' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

:root[data-theme="day"] .navbar-toggler-icon {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%233a2c10' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

/* ---------- buttons ---------- */
/* Same reason the old Solarized version of this file overrode
   .btn-primary: Bootstrap's shipped CSS precomputes each variant's
   colors as literal hex, not as var(--bs-primary)/var(--bs-danger), so
   the cascade above alone never reaches them. */

.btn {
  --bs-btn-border-radius: 0;
  --bs-btn-focus-box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fg) 35%, transparent);
  text-shadow: 0 0 6px var(--glow);
  letter-spacing: 1px;
}

.btn-primary {
  --bs-btn-color: var(--bg);
  --bs-btn-bg: var(--fg);
  --bs-btn-border-color: var(--fg);
  --bs-btn-hover-color: var(--bg);
  --bs-btn-hover-bg: var(--fg);
  --bs-btn-hover-border-color: var(--fg);
  --bs-btn-active-color: var(--bg);
  --bs-btn-active-bg: var(--fg);
  --bs-btn-active-border-color: var(--fg);
  --bs-btn-disabled-color: var(--bg);
  --bs-btn-disabled-bg: var(--fg);
  --bs-btn-disabled-border-color: var(--fg);
  box-shadow: 0 0 10px var(--glow);
  text-shadow: none;
}

.btn-primary:hover,
.btn-primary:active {
  filter: brightness(1.15);
}

.btn-outline-primary,
.btn-outline-secondary {
  --bs-btn-color: var(--fg);
  --bs-btn-bg: var(--btn-bg);
  --bs-btn-border-color: var(--fg);
  --bs-btn-hover-color: var(--bg);
  --bs-btn-hover-bg: var(--fg);
  --bs-btn-hover-border-color: var(--fg);
  --bs-btn-active-color: var(--bg);
  --bs-btn-active-bg: var(--fg);
  --bs-btn-active-border-color: var(--fg);
  --bs-btn-disabled-color: var(--fg);
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--fg);
  box-shadow: 0 0 8px var(--glow);
}

/* topic-list.html's collapsible search toggle -- the ">" caret rotates to
   point down when expanded, purely via the [aria-expanded] Bootstrap's
   collapse plugin already keeps in sync on the trigger button itself (no
   extra JS/Angular state to maintain here). */
.topic-search-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
}

/* The gap above already spaces the icon from the caret -- without this,
   .nav-icon's own margin-right (needed elsewhere, for icon+label nav
   links) would stack on top of it. */
.topic-search-toggle .nav-icon {
  margin-right: 0;
}

.topic-search-caret {
  display: inline-block;
  transition: transform 0.2s ease;
}

.topic-search-toggle[aria-expanded="true"] .topic-search-caret {
  transform: rotate(90deg);
}

.btn-outline-danger {
  --bs-btn-color: var(--danger);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: var(--danger);
  --bs-btn-hover-color: var(--bg);
  --bs-btn-hover-bg: var(--danger);
  --bs-btn-hover-border-color: var(--danger);
  --bs-btn-active-color: var(--bg);
  --bs-btn-active-bg: var(--danger);
  --bs-btn-active-border-color: var(--danger);
  --bs-btn-disabled-color: var(--danger);
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--danger);
  box-shadow: 0 0 8px color-mix(in srgb, var(--danger) 60%, transparent);
}

:root:not([data-theme="day"]) .btn-close {
  filter: invert(1) brightness(1.8) drop-shadow(0 0 4px var(--glow));
}

/* ---------- forms ---------- */

.form-control,
.form-select {
  background-color: color-mix(in srgb, var(--fg) 5%, var(--bg));
  color: var(--fg);
  border: 1px solid color-mix(in srgb, var(--fg) 45%, transparent);
  border-radius: 0;
}

.form-control:focus,
.form-select:focus {
  background-color: color-mix(in srgb, var(--fg) 5%, var(--bg));
  color: var(--fg);
  border-color: var(--fg);
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fg) 30%, transparent);
}

.form-control::placeholder {
  color: color-mix(in srgb, var(--fg) 45%, transparent);
  opacity: 1;
}

.form-control:disabled,
.form-select:disabled {
  background-color: color-mix(in srgb, var(--fg) 3%, var(--bg));
  color: color-mix(in srgb, var(--fg) 50%, transparent);
}

/* ---------- command-highlight (wirehead message input) ---------- */
/* commandHighlight directive (controllers.js) wraps the input in
   .command-highlight-wrap and layers .command-highlight-overlay on top of
   it, sharing .form-control so padding/border-width/font/line-height stay
   pixel-identical to the real input beneath -- overridden below to be
   otherwise invisible (no border/background/shadow of its own) so only
   the real input's box is ever what's actually seen; the overlay
   contributes only the (optionally highlighted) text pixels, plus a
   terminal-style block cursor (see .command-highlight-cursor), on top of
   it. The real input's own text AND native caret are both made
   invisible -- there's never a double-rendered/misaligned text layer, and
   a thin native line caret would otherwise show through alongside the
   overlay's own solid block one; native ::placeholder is unaffected
   (color/caret-color don't touch it). */
.command-highlight-wrap {
  position: relative;
  flex: 1 1 auto;
}

.command-highlight-input {
  color: transparent;
  /* color: transparent alone still lets Chrome's subpixel-antialiased
     glyph rendering show a faint ghost of the real text underneath the
     overlay -- invisible in isolation, but enough to visibly misalign
     with the overlay's own text the moment the two diverge in layout
     (e.g. the block cursor span occupying very slightly different width
     than the character it replaces). -webkit-text-fill-color is what
     actually needs to be transparent to fully suppress that. */
  -webkit-text-fill-color: transparent;
  caret-color: transparent;
  width: 100%;
}

.form-control.command-highlight-overlay {
  position: absolute;
  inset: 0;
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
  overflow: hidden;
  white-space: pre;
  pointer-events: none;
}

.command-highlight-token {
  color: var(--accent2);
  text-shadow: 0 0 6px var(--accent2);
  background: color-mix(in srgb, var(--accent2) 22%, transparent);
  border-radius: 2px;
}

/* Terminal-style block cursor -- a solid --fg rectangle with the one
   character it's "covering" shown in reverse video (--bg on --fg) inside
   it, same reverse-video-block shape as a real terminal's insert-mode
   cursor, rather than a thin line beside the character. Only ever
   present in the overlay while the input has focus (see the directive's
   `focused` flag) -- there's nothing to hide/show on blur beyond that,
   since render() simply never emits this span at all when unfocused.
   `span` in the selector (not just .command-highlight-cursor) bumps
   specificity above .command-highlight-token alone, so the cursor's own
   colors always win on the rare character where the two coincide (the
   caret sitting inside the highlighted command token). Reuses #boot
   .cursor's exact blink timing/step for the same "real terminal" feel. */
span.command-highlight-cursor {
  background: var(--fg);
  color: var(--bg);
  text-shadow: none;
  animation: blink 1s step-end infinite;
}

@media (prefers-reduced-motion: reduce) {
  span.command-highlight-cursor {
    animation: none;
  }
}

/* decrypt-on-type (controllers.js's trackInsertions/render): a freshly-
   typed character's brief random-glyph phase, tinted --accent2 so it
   visibly reads as "still resolving" rather than looking like a typo the
   moment before it corrects itself. Never actually delays the real
   character reaching the input's value -- purely the overlay's own
   momentary display of it. */
.command-highlight-decrypting {
  color: var(--accent2);
  text-shadow: 0 0 6px var(--accent2);
}

/* Toggled by commandHighlight's render() whenever a recognized command is
   currently matched (not just while the token itself is visible on
   screen) -- tints the real input's own border/glow to match the token
   color. Specificity bumped via the ancestor class (not just
   .command-highlight-input.command-active) so this reliably wins over
   .form-control:focus, which is the same specificity level on its own
   and would otherwise depend on source order to lose. */
.command-highlight-wrap .command-highlight-input.command-active {
  border-color: var(--accent2);
  box-shadow: 0 0 8px var(--accent2);
}

.form-label,
.form-check-label {
  color: var(--fg);
}

.form-text {
  color: color-mix(in srgb, var(--fg) 65%, transparent);
}

.form-check-input {
  background-color: transparent;
  border: 1px solid var(--fg);
}

.form-check-input:checked {
  background-color: var(--fg);
  border-color: var(--fg);
}

.form-check-input:focus {
  border-color: var(--fg);
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fg) 30%, transparent);
}

.input-group-text {
  background-color: color-mix(in srgb, var(--fg) 8%, var(--bg));
  color: var(--fg);
  border: 1px solid color-mix(in srgb, var(--fg) 45%, transparent);
  border-radius: 0;
}

/* Same "baked SVG, not a variable" situation as .navbar-toggler-icon. */
:root:not([data-theme="day"]) .form-select {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%230f0' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
}

:root[data-theme="day"] .form-select {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%233a2c10' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
}

/* ---------- tables ---------- */

.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--fg);
  --bs-table-border-color: color-mix(in srgb, var(--fg) 30%, transparent);
  --bs-table-striped-bg: color-mix(in srgb, var(--fg) 4%, transparent);
  --bs-table-striped-color: var(--fg);
  --bs-table-hover-bg: color-mix(in srgb, var(--fg) 8%, transparent);
  --bs-table-hover-color: var(--fg);
}

.table thead th {
  text-transform: uppercase;
  letter-spacing: 1px;
  border-bottom: 2px solid var(--fg);
}

/* ---------- cards, alerts, badges, list groups ---------- */

.card {
  background-color: color-mix(in srgb, var(--fg) 4%, var(--bg));
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  border-radius: 0;
  color: var(--fg);
}

.alert {
  border-radius: 0;
}

.alert-danger {
  color: var(--danger);
  background-color: color-mix(in srgb, var(--danger) 12%, var(--bg));
  border: 1px solid var(--danger);
  text-shadow: 0 0 6px color-mix(in srgb, var(--danger) 70%, transparent);
}

.alert-success {
  color: var(--fg);
  background-color: color-mix(in srgb, var(--fg) 10%, var(--bg));
  border: 1px solid var(--fg);
}

.badge {
  border-radius: 0;
  font-weight: normal;
  letter-spacing: 0.5px;
}

.badge.bg-light {
  background-color: color-mix(in srgb, var(--fg) 12%, var(--bg)) !important;
  color: var(--fg) !important;
  border: 1px solid color-mix(in srgb, var(--fg) 40%, transparent);
}

.badge.bg-secondary {
  background-color: transparent !important;
  color: var(--fg) !important;
  border: 1px solid var(--fg);
}

.badge.bg-primary {
  background-color: var(--fg) !important;
  color: var(--bg) !important;
}

.badge.bg-danger {
  background-color: transparent !important;
  color: var(--danger) !important;
  border: 1px solid var(--danger);
}

.badge.bg-success {
  background-color: transparent !important;
  color: var(--fg) !important;
  border: 1px solid var(--fg);
}

.badge.bg-warning,
.badge.bg-info {
  background-color: color-mix(in srgb, var(--accent2) 25%, var(--bg)) !important;
  border: 1px solid var(--accent2);
}

/* topic-detail.html's media upload progress bar. */
.progress {
  background-color: color-mix(in srgb, var(--fg) 10%, var(--bg));
  border-radius: 0;
}

.progress-bar {
  background-color: var(--fg);
}

.list-group-item {
  background-color: transparent;
  color: var(--fg);
  border-color: color-mix(in srgb, var(--fg) 30%, transparent);
}

.list-group-item-action:hover,
.list-group-item-action:focus {
  background-color: color-mix(in srgb, var(--fg) 8%, transparent);
  color: var(--fg);
}

/* ---------- modal ---------- */

.modal-content {
  background-color: var(--bg);
  color: var(--fg);
  border: 1px solid var(--fg);
  border-radius: 0;
  box-shadow: 0 0 24px var(--glow);
}

/* ---------------------------------------------------------------------
 * App-specific components below -- unchanged in structure/behavior from
 * before, just re-pointed from the old Solarized (--sol-*) variables onto
 * the CRT theme's --fg/--bg/--danger/--accent2.
 * --------------------------------------------------------------------- */

.tag-badge {
  margin-right: 0.25rem;
}

.namespace-badge {
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* ---------- JSON document surface (Aegis console, account page) ----------
   The premise the old .json-form had, kept: an editable record should LOOK
   like the JSON document it actually is server-side, so what you're
   changing and what gets sent are obviously the same thing. What's new is
   that it now reads like a real editor pane -- a titled document with a
   filename and a live saved/unsaved badge, syntax-colored keys and values,
   per-line dirty marking, and inline controls that sit in the value
   position instead of floating beside it as generic form widgets. */

.jsondoc {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  margin-bottom: 1.25rem;
}

.jsondoc-danger {
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

.jsondoc-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.75rem;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 25%, transparent);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
}

.jsondoc-filename {
  font-size: 0.85rem;
  letter-spacing: 0.5px;
  color: color-mix(in srgb, var(--fg) 85%, transparent);
}

.jsondoc-badge {
  margin-left: auto;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  padding: 0.1rem 0.45rem;
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  color: color-mix(in srgb, var(--fg) 65%, transparent);
}

.jsondoc-badge-dirty {
  border-color: var(--accent2);
  color: var(--accent2);
  animation: jsondoc-dirty-pulse 1.6s ease-in-out infinite;
}

.jsondoc-badge-danger {
  border-color: var(--danger);
  color: var(--danger);
}

.jsondoc-badge-new {
  border-color: var(--fg);
  color: var(--fg);
}

@keyframes jsondoc-dirty-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* Line numbers come from a CSS counter rather than markup: the document's
   line count changes with ng-if'd rows, and a counter renumbers itself
   automatically where hand-written numbers would silently go stale. */
.jsondoc-body {
  counter-reset: jsondoc-line;
  padding: 0.6rem 0;
  overflow-x: auto;
}

.jsondoc-line {
  counter-increment: jsondoc-line;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.12rem 0.75rem 0.12rem 0;
  white-space: nowrap;
}

.jsondoc-line::before {
  content: counter(jsondoc-line);
  flex: 0 0 2.5rem;
  text-align: right;
  padding-right: 0.75rem;
  color: color-mix(in srgb, var(--fg) 30%, transparent);
  font-size: 0.8rem;
  user-select: none;
}

.jsondoc-indent { padding-left: 0; }
.jsondoc-indent .jsondoc-key { margin-left: 1.5rem; }

.jsondoc-line-dirty {
  background: color-mix(in srgb, var(--accent2) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--accent2);
}

.jsondoc-key { color: var(--accent2); }
.jsondoc-string { color: var(--fg); }
.jsondoc-number { color: var(--warn); }
.jsondoc-punct { color: color-mix(in srgb, var(--fg) 45%, transparent); }

/* Read-only lines are dimmed rather than hidden: seeing id/version/
   created_at is exactly what makes this feel like the real record, and
   version in particular is the value the CAS writes actually send. */
.jsondoc-readonly .jsondoc-string,
.jsondoc-readonly .jsondoc-number {
  color: color-mix(in srgb, var(--fg) 60%, transparent);
}

.jsondoc-lock,
.jsondoc-was {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.jsondoc-lock { color: color-mix(in srgb, var(--fg) 45%, transparent); }
.jsondoc-was { color: var(--accent2); }

/* Inline controls: styled to sit in the value position as if they were
   the literal, rather than reading as a form dropped into a code block. */
.jsondoc-input,
.jsondoc-select {
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--fg);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--fg) 30%, transparent);
  border-radius: 0;
  padding: 0.05rem 0.35rem;
  min-width: 8rem;
}

.jsondoc-select { min-width: 7rem; }

.jsondoc-input:focus,
.jsondoc-select:focus {
  outline: none;
  border-color: var(--accent2);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent2) 30%, transparent);
}

.jsondoc-input:disabled,
.jsondoc-select:disabled {
  opacity: 0.5;
}

.jsondoc-note,
.jsondoc-error {
  font-size: 0.8rem;
  padding: 0 0.75rem 0.5rem 3.25rem;
  margin: 0;
}

.jsondoc-note { color: color-mix(in srgb, var(--fg) 65%, transparent); }
.jsondoc-error { color: var(--danger); }

.jsondoc-actions {
  display: flex;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border-top: 1px solid color-mix(in srgb, var(--fg) 20%, transparent);
}

/* ---------- Aegis console layout ---------- */

/* Two panes side by side on wide screens, stacked on narrow ones. The
   directory is sticky so the editor can be long (three documents) without
   scrolling the list you're navigating with the arrow keys out of view. */
.aegis-console {
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
}

@media (max-width: 900px) {
  .aegis-console { grid-template-columns: minmax(0, 1fr); }
}

.aegis-directory {
  position: sticky;
  top: 1rem;
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
}

.aegis-toolbar {
  display: flex;
  gap: 0.4rem;
  padding: 0.5rem;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 20%, transparent);
}

.aegis-search {
  flex: 1 1 auto;
  border-radius: 0;
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  border-color: color-mix(in srgb, var(--fg) 30%, transparent);
  color: var(--fg);
}

.aegis-search:focus {
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  color: var(--fg);
  border-color: var(--accent2);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent2) 25%, transparent);
}

.aegis-list {
  max-height: 26rem;
  overflow-y: auto;
}

/* The list is one focusable widget (tabindex on the container), so the
   focus ring belongs on the container, not on individual rows. */
.aegis-list:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent2) 45%, transparent);
}

.aegis-list-head,
.aegis-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 4.5rem minmax(0, 6rem) 1.5rem;
  gap: 0.4rem;
  align-items: center;
  padding: 0.25rem 0.6rem;
  font-size: 0.85rem;
}

.aegis-list-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: color-mix(in srgb, var(--fg) 10%, var(--bg));
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 25%, transparent);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-size: 0.7rem;
  color: color-mix(in srgb, var(--fg) 65%, transparent);
}

.aegis-list-head span { cursor: pointer; }
.aegis-list-head span:hover { color: var(--fg); }

.aegis-row {
  cursor: pointer;
  border-left: 3px solid transparent;
}

.aegis-row:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }

.aegis-row-selected {
  background: color-mix(in srgb, var(--accent2) 14%, transparent);
  border-left-color: var(--accent2);
}

/* The keyboard cursor is shown separately from the selection: they're
   normally the same row, but the distinction matters the moment the two
   diverge (clicking one row while the cursor sits elsewhere). */
.aegis-row-active {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent2) 55%, transparent);
}

.aegis-row-banned .aegis-col-user {
  text-decoration: line-through;
  opacity: 0.65;
}

.aegis-col-user,
.aegis-col-ns {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.aegis-col-role {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.aegis-role-admin { color: var(--danger); }
.aegis-role-author { color: var(--accent2); }
.aegis-role-regular { color: color-mix(in srgb, var(--fg) 55%, transparent); }

.aegis-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.aegis-dot-active { background: var(--fg); box-shadow: 0 0 6px var(--glow); }
.aegis-dot-banned { background: var(--danger); box-shadow: 0 0 6px var(--danger); }

.aegis-empty {
  padding: 1rem 0.6rem;
  margin: 0;
  color: color-mix(in srgb, var(--fg) 55%, transparent);
  font-size: 0.85rem;
}

.aegis-hint {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.6rem;
  border-top: 1px solid color-mix(in srgb, var(--fg) 20%, transparent);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: color-mix(in srgb, var(--fg) 55%, transparent);
}

.aegis-key {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  padding: 0 0.3rem;
  color: var(--fg);
}

.aegis-count { margin-left: auto; }

.aegis-placeholder {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  padding: 2rem 1rem;
  text-align: center;
  color: color-mix(in srgb, var(--fg) 50%, transparent);
  border: 1px dashed color-mix(in srgb, var(--fg) 25%, transparent);
}

/* media-drop-zone (topic-detail.html): dashed target for the dropZone
   directive (controllers.js) -- drop-zone-active is toggled by the
   directive itself while a drag carrying files is directly over it, and
   page-drag-highlight by pageFileDragDetector as soon as a file drag
   enters the page at all (see page-drag-overlay below), purely visual
   affordances -- the actual drop handling works regardless of whether
   either class is present. position: relative is what lets z-index below
   lift this element above page-drag-overlay's dim layer once
   page-drag-highlight is active. */
.media-drop-zone {
  position: relative;
  border: 2px dashed var(--bs-border-color);
  transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.media-drop-zone.drop-zone-active {
  border-color: var(--fg);
  background-color: var(--bs-tertiary-bg);
}

.media-drop-zone.page-drag-highlight {
  z-index: 1041;
  border-color: var(--fg);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--fg) 25%, transparent);
}

/* page-drag-overlay: dims the rest of the page the moment a file drag
   enters anywhere on it (see pageFileDragDetector), so the one real drop
   target reads as "the highlighted part" without needing an actual
   cutout -- media-drop-zone.page-drag-highlight just paints above this
   layer via z-index. pointer-events: none keeps this purely visual: drag
   events must keep reaching whatever element is actually under the
   cursor, most importantly the drop zone itself. */
.page-drag-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1040;
  pointer-events: none;
}

/* drag-drop-hint: "or drag and drop files here" is dead copy on a
   touchscreen -- there's no OS-level drag-a-file-in gesture on
   Android/iOS, so dropZone/pageFileDragDetector (controllers.js) never
   fire there and the "Add media" button is the only real path anyway.
   Hidden by default and only shown when the device actually has
   hover+a precise pointer (i.e. a mouse/trackpad), rather than sniffing
   user agent or screen width -- a touch laptop with a mouse attached
   still gets the hint, a touch-only tablet doesn't. */
.drag-drop-hint {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  .drag-drop-hint {
    display: block;
  }
}

/* lightbox-trigger (topic-detail.html): every clickable image that opens
   #imageLightbox -- cursor affordance only, the actual open/close is
   Bootstrap's modal JS via data-bs-toggle/data-bs-target on the element. */
.lightbox-trigger {
  cursor: zoom-in;
}

/* lightbox-image: the full-size picture inside #imageLightbox itself.
   img-fluid already caps width at 100% of the modal, but on a short/wide
   viewport (a landscape phone, or a portrait image on a short desktop
   window) width alone isn't enough to keep it from spilling past the
   bottom of the screen -- max-height plus object-fit: contain caps the
   other dimension too, the same "shrink to fit, never crop, never
   overflow" behavior img-fluid already gives on the width axis. */
.lightbox-image {
  max-height: 85vh;
  object-fit: contain;
}

/* wirehead-messages (wirehead.html): fixed-height, independently
   scrolling message list -- without a cap, a long-running namespace would
   keep pushing the send form further down the page every time a message
   arrived. */
.wirehead-messages {
  max-height: 60vh;
  overflow-y: auto;
}

/* tabindex="0" (wirehead.html) makes this panel itself focusable, so
   arrows/PgUp/PgDn/Home/End scroll it via plain native browser behavior
   for a focused scrollable region -- no JS needed for that path at all
   (contrast WireheadController.onInputKeydown's explicit overrides,
   which only cover the "still composing a message" case). Swaps the
   browser's default blue focus ring for the theme's own glow, same
   treatment .form-control:focus already gets. */
.wirehead-messages:focus {
  outline: none;
  border-color: var(--fg);
  box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fg) 30%, transparent);
}

/* wirehead-hint-bar: a fixed legend of this view's keyboard shortcuts,
   styled as reverse video (--bg text on a solid --fg bar) -- the same
   "highlighted strip along the bottom of the screen" convention old
   terminal editors (nano, WordStar, DOS EDIT.COM) use for their own
   shortcut hints. */
.wirehead-hint-bar {
  background: var(--fg);
  color: var(--bg);
  font-size: 0.8rem;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 4px 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.25em;
}

.wirehead-hint-key {
  background: var(--bg);
  color: var(--fg);
  padding: 0 0.4em;
  margin-right: 0.4em;
  font-weight: bold;
}

/* wirehead-own / wirehead-broadcast (WireheadController.highlightClass):
   left border + tint is enough to draw the eye down a list of otherwise
   plain rows without needing a different layout per kind of message.
   --fg for "this is you", --accent2 (the scene's synthwave accent hue)
   for "this reached you from every namespace, not just this one." */
.wirehead-message.wirehead-own {
  border-left: 3px solid var(--fg);
  background-color: var(--bs-tertiary-bg);
  padding-left: 0.5rem;
}

.wirehead-message.wirehead-broadcast {
  border-left: 3px solid var(--accent2);
  background-color: var(--bs-tertiary-bg);
  padding-left: 0.5rem;
}

/* Plays once when a message's DOM node is first created -- ng-repeat only
   ever creates a new node for a genuinely new array entry (existing rows
   aren't re-created on each digest), so this naturally fires exactly once
   per arriving message, sent or received, with no ngAnimate dependency or
   manual "just arrived" flag needed. */
.wirehead-message {
  animation: wirehead-message-pop 0.28s ease-out;
}

@keyframes wirehead-message-pop {
  0% { opacity: 0; transform: scale(0.9) translateY(4px); filter: brightness(1.8); }
  60% { opacity: 1; transform: scale(1.02) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); filter: brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  .wirehead-message {
    animation: none;
  }
}

/* wirehead-typing-indicator (WireheadController.typingUsers): sits below
   the message list, same muted-text treatment as the "no messages yet"
   placeholder -- an ellipsis that visibly pulses reads as "still going"
   without needing to animate the whole line. */
.wirehead-typing-indicator .wirehead-typing-dots {
  display: inline-block;
  animation: wirehead-typing-pulse 1.2s ease-in-out infinite;
}

@keyframes wirehead-typing-pulse {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .wirehead-typing-indicator .wirehead-typing-dots {
    animation: none;
  }
}

/* ---------- Bitsiphon admin page ---------- */

.bitsiphon-panel {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  padding: 1rem 1.25rem;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
}

.bitsiphon-panel h2 {
  text-transform: uppercase;
  letter-spacing: 1px;
}

/* One row per dimension value: label | proportional bar | count | avg
   response time. Grid (not flex) so all four columns line up across
   every row regardless of how long a given value's name is. */
.bitsiphon-bar-row {
  display: grid;
  grid-template-columns: minmax(90px, 14ch) 1fr auto auto;
  align-items: center;
  gap: 0.6em;
  margin-bottom: 0.4em;
  font-size: 0.9rem;
}

.bitsiphon-bar-label {
  text-transform: uppercase;
  letter-spacing: 0.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bitsiphon-bar-track {
  display: block;
  height: 0.9em;
  background: color-mix(in srgb, var(--fg) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
}

.bitsiphon-bar-fill {
  display: block;
  height: 100%;
  background: var(--fg);
  box-shadow: 0 0 8px var(--glow);
}

.bitsiphon-bar-count,
.bitsiphon-bar-avg {
  white-space: nowrap;
  color: color-mix(in srgb, var(--fg) 75%, transparent);
  font-size: 0.85em;
}

.bitsiphon-trend-table {
  color: var(--fg);
}

.bitsiphon-trend-table tbody tr {
  cursor: pointer;
}

.bitsiphon-trend-table tbody tr:hover {
  background: color-mix(in srgb, var(--fg) 8%, transparent);
}

.bitsiphon-trend-row-selected {
  background: color-mix(in srgb, var(--accent2) 15%, transparent) !important;
  box-shadow: inset 2px 0 0 var(--accent2);
}

/* ---- endpoint latency/error-budget panel ---- */

.bitsiphon-stat-tile {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  padding: 0.6rem 0.9rem;
  height: 100%;
}

.bitsiphon-stat-tile-danger {
  border-color: var(--danger);
  box-shadow: 0 0 10px color-mix(in srgb, var(--danger) 40%, transparent);
}

.bitsiphon-stat-tile-danger .bitsiphon-stat-value {
  color: var(--danger);
}

.bitsiphon-stat-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: color-mix(in srgb, var(--fg) 65%, transparent);
}

.bitsiphon-stat-value {
  font-size: 1.5rem;
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
}

.bitsiphon-latency-card {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  padding: 0.75rem 1rem;
  height: 100%;
}

.bitsiphon-latency-card-error {
  border-color: var(--danger);
}

.bitsiphon-percentile-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1em 1.5em;
  font-size: 0.9rem;
}

/* One badge color per bitsiphon.SignalKind -- rogue_host reuses the scene
   accent (magenta/pink), error_storm the theme's danger red, path_scan
   the dedicated --warn amber, so all three read as distinct at a glance
   in a mixed list. */
.bitsiphon-signal-badge {
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-size: 0.75em;
}

.bitsiphon-signal-rogue_host {
  background-color: var(--accent2) !important;
  color: var(--bg) !important;
}

.bitsiphon-signal-error_storm {
  background-color: var(--danger) !important;
  color: var(--bg) !important;
}

.bitsiphon-signal-path_scan {
  background-color: var(--warn) !important;
  color: var(--bg) !important;
}

/* The GeoIP update form's drop target (admin-bitsiphon.html). Reuses
   .media-drop-zone for the dashed border and the drop-zone-active /
   page-drag-highlight states the shared dropZone directive toggles, and
   only adds what's specific to being a <label> wrapping a hidden file
   input: the pointer affordance, and a disabled look while an install is
   already in flight. */
.geodb-drop-zone {
  cursor: pointer;
}

.geodb-drop-zone-disabled {
  cursor: progress;
  opacity: 0.6;
}

.geodb-drop-icon {
  font-size: 1.6rem;
  line-height: 1;
  margin-bottom: 0.35rem;
  color: color-mix(in srgb, var(--fg) 70%, transparent);
}

.geodb-staged {
  border: 1px solid color-mix(in srgb, var(--fg) 30%, transparent);
  background: color-mix(in srgb, var(--fg) 4%, transparent);
  padding: 0.5rem 0.75rem;
}

.geodb-staged-name {
  word-break: break-all;
}

.bitsiphon-geodb-status dt {
  color: color-mix(in srgb, var(--fg) 70%, transparent);
  text-transform: uppercase;
  font-size: 0.85em;
  letter-spacing: 0.5px;
}

/* ---------- Synthdrop (synthwave falling-block, line-clearing puzzle) ---------- */

/* Piece colors are fixed hex, not theme variables -- the board is meant
   to read as its own little arcade-cabinet screen (always dark, always
   neon) regardless of whether the surrounding site is in the night or
   day theme, the same reasoning the synthwave #scene background already
   uses fixed colors (just at reduced --scene-opacity in the day theme)
   rather than remapping itself per theme. */
:root {
  --synthdrop-cell: 24px;
  /* Preview cells are smaller than playfield cells so the widest piece (a
     4-cell I) still fits the side panel without pushing the board around. */
  --synthdrop-preview-cell: 16px;
  --synthdrop-i: #33fff0;
  --synthdrop-o: #ffe135;
  --synthdrop-t: #ff2fd0;
  --synthdrop-s: #39ff14;
  --synthdrop-z: #ff3b3b;
  --synthdrop-j: #2fd0ff;
  --synthdrop-l: #ff8a3d;
  --synthdrop-garbage: #5a5a66;
}

.synthdrop-panel {
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  padding: 1rem;
}

.synthdrop-code-input {
  max-width: 8ch;
  text-transform: uppercase;
  letter-spacing: 2px;
  text-align: center;
}

.synthdrop-rankings-title {
  text-transform: uppercase;
  letter-spacing: 1px;
}

.synthdrop-rankings-table {
  color: var(--fg);
}

.synthdrop-room-code {
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  font-size: 2.4rem;
  letter-spacing: 6px;
  color: var(--accent2);
  text-shadow: 0 0 16px var(--accent2);
  margin: 1rem 0;
}

.synthdrop-waiting {
  animation: synthdrop-waiting-pulse 1.6s ease-in-out infinite;
}

@keyframes synthdrop-waiting-pulse {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}

.synthdrop-stat-line {
  display: flex;
  gap: 1.5em;
  font-size: 1.2rem;
}

.synthdrop-stat-label {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: color-mix(in srgb, var(--fg) 65%, transparent);
}

.synthdrop-stage {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
}

/* The board itself: a fixed COLS x ROWS pixel grid (see
   synthdrop-engine.js's COLS/ROWS) rendered dark regardless of theme --
   see the doc comment on the color variables above. position: relative
   so the flash/pause overlays below can cover it exactly. */
.synthdrop-board {
  position: relative;
  width: calc(var(--synthdrop-cell) * 10);
  background: #05030a;
  border: 2px solid color-mix(in srgb, var(--accent2) 60%, transparent);
  box-shadow: 0 0 20px color-mix(in srgb, var(--accent2) 35%, transparent);
  transition: background 0.6s ease, border-color 0.6s ease, box-shadow 0.6s ease;
}

.synthdrop-board-paused {
  filter: brightness(0.5);
}

/* The WebGL board (synthdrop-renderer.js). Sized in the same cell units
   the DOM board uses, so the surrounding layout is identical whichever
   renderer is active -- the canvas is what gives .synthdrop-board its
   height in WebGL mode, exactly as the stack of .synthdrop-row divs does
   in the fallback. The backing-store resolution is set separately, in JS
   (see Renderer.resize), and is not tied to these CSS pixels. */
.synthdrop-canvas {
  display: block;
  width: calc(var(--synthdrop-cell) * 10);
  height: calc(var(--synthdrop-cell) * 20);
}

/* ---- Tiers (SynthdropController's TIERS/tierFor): every 10 lines
   cleared re-skins the board's ambient look, on top of the one-shot
   .synthdrop-transition-<theme> burst played the instant the tier is
   reached (see synthdrop.html's synthdrop-tier-transition element).
   Piece colors (--synthdrop-i/o/t/... above) stay constant across every
   tier -- only the board's own background/border/glow shifts -- so a
   piece is always identifiable by color regardless of how far a run has
   progressed. ---- */

.synthdrop-tier-neon {
  background: #05030a;
  border-color: color-mix(in srgb, var(--accent2) 60%, transparent);
  box-shadow: 0 0 20px color-mix(in srgb, var(--accent2) 35%, transparent);
}

.synthdrop-tier-chrome {
  background: linear-gradient(160deg, #1c1f26, #3a4048 55%, #1c1f26);
  border-color: #cfd8e3;
  box-shadow: 0 0 22px rgba(207, 216, 227, 0.45), inset 0 0 30px rgba(255, 255, 255, 0.06);
}

.synthdrop-tier-static {
  background: #101010;
  border-color: #8a8a8a;
  box-shadow: 0 0 16px rgba(180, 180, 180, 0.35);
  animation: synthdrop-static-flicker 2.4s steps(2) infinite;
}

@keyframes synthdrop-static-flicker {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.12) contrast(1.08); }
}

.synthdrop-tier-overdrive {
  background: radial-gradient(circle at 50% 30%, #3a0d02, #150200 75%);
  border-color: #ff5a1f;
  box-shadow: 0 0 26px rgba(255, 90, 31, 0.55);
}

.synthdrop-tier-black-ice {
  background: linear-gradient(180deg, #020814, #041226);
  border-color: #29e0ff;
  box-shadow: 0 0 28px rgba(41, 224, 255, 0.5), inset 0 0 20px rgba(41, 224, 255, 0.15);
}

.synthdrop-tier-ghost {
  background: linear-gradient(180deg, rgba(10, 15, 20, 0.92), rgba(160, 210, 225, 0.12));
  border-color: rgba(220, 245, 255, 0.65);
  box-shadow: 0 0 24px rgba(220, 245, 255, 0.4);
  animation: synthdrop-ghost-breathe 3s ease-in-out infinite;
}

@keyframes synthdrop-ghost-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.82; }
}

.synthdrop-tier-singularity {
  background: radial-gradient(circle, #2a0033, #000 80%);
  border-color: #fff;
  box-shadow: 0 0 32px rgba(255, 255, 255, 0.6);
  animation: synthdrop-singularity-shift 4s linear infinite;
}

@keyframes synthdrop-singularity-shift {
  0% { filter: hue-rotate(0deg) brightness(1); }
  100% { filter: hue-rotate(360deg) brightness(1.15); }
}

/* Per-tier line-clear "ghost" animations (SynthdropController's
   spawnClearingRows, synthdrop.html's synthdrop-clearing-row element):
   one absolutely-positioned copy of a just-cleared row's actual cell
   colors, held at its old position and burned away with the treatment
   matching whichever tier it cleared under, while the real board (a
   sibling, already reflowed) sits underneath. z-index 2 -- above the
   plain board cells (z-index: auto, effectively 0) but below the
   tier-transition burst and flash text (z-index 5 and above) so neither
   of those ever gets obscured by a clear happening at the same moment. */
.synthdrop-clearing-row {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--synthdrop-cell);
  display: flex;
  pointer-events: none;
  z-index: 2;
}

/* NEON: "purges cause bright neon blurring of lines before purging" --
   the row overexposes into its own glow, brighter and blurrier each step,
   until it's pure light. */
@keyframes synthdrop-clear-neon {
  0%   { filter: brightness(1) saturate(1) blur(0); opacity: 1; }
  35%  { filter: brightness(2.2) saturate(1.6) blur(1px); opacity: 1; }
  65%  { filter: brightness(3) saturate(2) blur(3px); opacity: 1; }
  100% { filter: brightness(3.2) blur(7px); opacity: 0; }
}
.synthdrop-clearing-row-neon { animation: synthdrop-clear-neon 600ms ease-in forwards; }

/* CHROME: "burning chrome effect" -- the row runs hot (sepia/contrast
   pushing it toward molten white-orange) and visibly melts, collapsing
   vertically as it burns through. */
@keyframes synthdrop-clear-chrome {
  0%   { filter: brightness(1) contrast(1) sepia(0); transform: scaleY(1); opacity: 1; }
  40%  { filter: brightness(2.4) contrast(1.3) sepia(0.45) hue-rotate(-8deg); transform: scaleY(1); opacity: 1; }
  70%  { filter: brightness(3.4) sepia(0.65); transform: scaleY(0.6); opacity: 0.9; }
  100% { filter: brightness(4) blur(2px); transform: scaleY(0.05); opacity: 0; }
}
.synthdrop-clearing-row-chrome { animation: synthdrop-clear-chrome 600ms ease-in forwards; transform-origin: center; }

/* STATIC: signal interference -- choppy horizontal jitter plus a
   chromatic-aberration-style red/cyan fringe, like a dying broadcast. */
@keyframes synthdrop-clear-static {
  0%   { transform: translateX(0); opacity: 1; filter: none; }
  12%  { transform: translateX(-3px); opacity: 0.7; }
  24%  { transform: translateX(4px); opacity: 1; }
  36%  { transform: translateX(-2px); opacity: 0.5; }
  48%  { transform: translateX(3px); opacity: 1; filter: drop-shadow(2px 0 #33fff0) drop-shadow(-2px 0 #ff2fd0); }
  62%  { transform: translateX(-4px); opacity: 0.35; }
  78%  { transform: translateX(2px); opacity: 0.75; }
  100% { transform: translateX(0); opacity: 0; }
}
.synthdrop-clearing-row-static { animation: synthdrop-clear-static 600ms steps(2, jump-none) forwards; }

/* OVERDRIVE: the row redlines off to the right in a streak of motion
   blur, like it's been launched rather than cleared. */
@keyframes synthdrop-clear-overdrive {
  0%   { transform: scaleX(1) translateX(0); filter: blur(0) brightness(1); opacity: 1; }
  30%  { transform: scaleX(1.15) translateX(4px); filter: blur(1px) brightness(1.4); opacity: 1; }
  100% { transform: scaleX(1.6) translateX(70px); filter: blur(9px) brightness(1.9); opacity: 0; }
}
.synthdrop-clearing-row-overdrive { animation: synthdrop-clear-overdrive 600ms cubic-bezier(0.6, 0, 0.9, 0.3) forwards; transform-origin: left center; }

/* BLACK ICE: a hard flash (light catching the ice) then it fractures --
   a jittery rotate/scale wobble simulating cracking apart before it
   collapses to nothing. */
@keyframes synthdrop-clear-black-ice {
  0%   { transform: rotate(0deg) scale(1); filter: brightness(1); opacity: 1; }
  20%  { filter: brightness(2.3); opacity: 1; }
  38%  { transform: rotate(-1.5deg) scale(1.02); }
  54%  { transform: rotate(2deg) scale(0.97); }
  70%  { transform: rotate(-2.5deg) scale(0.88); opacity: 0.85; }
  100% { transform: rotate(6deg) scale(0.15); filter: brightness(2.6); opacity: 0; }
}
.synthdrop-clearing-row-black-ice { animation: synthdrop-clear-black-ice 600ms ease-in forwards; }

/* GHOST: no violence at all -- it just lets go, drifting upward while
   desaturating and softening into nothing, matching the tier's own
   gentler, ethereal ambient theme. */
@keyframes synthdrop-clear-ghost {
  0%   { transform: translateY(0); filter: grayscale(0) blur(0); opacity: 1; }
  100% { transform: translateY(-16px); filter: grayscale(1) blur(3px); opacity: 0; }
}
.synthdrop-clearing-row-ghost { animation: synthdrop-clear-ghost 600ms ease-out forwards; }

/* SINGULARITY: implodes toward its own center -- a gravitational
   collapse into a bright point, echoing the tier's WHITEOUT tier-
   transition burst instead of competing with it. */
@keyframes synthdrop-clear-singularity {
  0%   { transform: scaleX(1) rotate(0deg); filter: brightness(1); opacity: 1; }
  50%  { transform: scaleX(0.4) rotate(1deg); filter: brightness(2.6); opacity: 1; }
  75%  { transform: scaleX(0.05) rotate(3deg); filter: brightness(4); opacity: 1; }
  100% { transform: scaleX(0) rotate(0deg); filter: brightness(4); opacity: 0; }
}
.synthdrop-clearing-row-singularity { animation: synthdrop-clear-singularity 600ms cubic-bezier(0.7, 0, 0.84, 0) forwards; transform-origin: center; }

@media (prefers-reduced-motion: reduce) {
  @keyframes synthdrop-clear-fade-reduced {
    from { opacity: 1; }
    to { opacity: 0; }
  }
  .synthdrop-clearing-row-neon,
  .synthdrop-clearing-row-chrome,
  .synthdrop-clearing-row-static,
  .synthdrop-clearing-row-overdrive,
  .synthdrop-clearing-row-black-ice,
  .synthdrop-clearing-row-ghost,
  .synthdrop-clearing-row-singularity {
    animation: synthdrop-clear-fade-reduced 250ms ease-out forwards;
  }
}

/* One-shot overlays (synthdrop.html's synthdrop-tier-transition), each
   named after the tier it announces -- "BURNING CHROME" nods to Gibson,
   the rest follow the same "genre touchstone as effect name" idea. Fully
   opaque at their own z-index, above the board cells but below the flash
   text layer so the transition text still reads clearly through it. */
.synthdrop-tier-transition {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

.synthdrop-transition-neon {
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--synthdrop-t) 55%, transparent), color-mix(in srgb, var(--synthdrop-j) 55%, transparent), transparent);
  filter: blur(2px);
  animation: synthdrop-transition-neon 1.2s ease-out forwards;
}

@keyframes synthdrop-transition-neon {
  0% { opacity: 0; transform: translateX(-100%); }
  40% { opacity: 1; }
  100% { opacity: 0; transform: translateX(100%); }
}

.synthdrop-transition-chrome {
  background: radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 140, 60, 0.5) 40%, transparent 72%);
  animation: synthdrop-transition-chrome 1.3s ease-out forwards;
}

@keyframes synthdrop-transition-chrome {
  0% { opacity: 0; transform: scale(0.3); }
  25% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.6); }
}

.synthdrop-transition-static {
  background: repeating-linear-gradient(0deg, #fff 0 2px, #000 2px 4px);
  mix-blend-mode: difference;
  animation: synthdrop-transition-static 0.9s steps(6) forwards;
}

@keyframes synthdrop-transition-static {
  0% { opacity: 0.9; }
  50% { opacity: 0.5; }
  100% { opacity: 0; }
}

.synthdrop-transition-overdrive {
  background: rgba(255, 40, 0, 0.5);
  animation: synthdrop-transition-overdrive 0.8s ease-out forwards;
}

@keyframes synthdrop-transition-overdrive {
  0% { opacity: 0; transform: translateX(0); }
  15% { opacity: 1; transform: translateX(-4px); }
  30% { transform: translateX(4px); }
  45% { transform: translateX(-3px); }
  60% { transform: translateX(3px); }
  100% { opacity: 0; transform: translateX(0); }
}

.synthdrop-transition-black-ice {
  background: radial-gradient(circle, rgba(255, 255, 255, 0.85), rgba(41, 224, 255, 0.5) 50%, transparent 75%);
  animation: synthdrop-transition-blackice 1.1s ease-out forwards;
}

@keyframes synthdrop-transition-blackice {
  0% { opacity: 0; transform: scale(0.5); }
  20% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.3); }
}

.synthdrop-transition-ghost {
  background: linear-gradient(90deg, transparent, rgba(220, 245, 255, 0.55), transparent);
  filter: blur(3px);
  animation: synthdrop-transition-ghost 1.4s ease-in-out forwards;
}

@keyframes synthdrop-transition-ghost {
  0% { opacity: 0; transform: translateX(-60%); }
  50% { opacity: 0.9; }
  100% { opacity: 0; transform: translateX(60%); }
}

.synthdrop-transition-singularity {
  background: #fff;
  animation: synthdrop-transition-singularity 1.2s ease-in forwards;
}

@keyframes synthdrop-transition-singularity {
  0% { opacity: 0; }
  30% { opacity: 1; }
  60% { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .synthdrop-tier-static, .synthdrop-tier-ghost, .synthdrop-tier-singularity,
  .synthdrop-tier-transition {
    animation: none;
  }
}

.synthdrop-row {
  display: flex;
}

.synthdrop-cell {
  width: var(--synthdrop-cell);
  height: var(--synthdrop-cell);
  box-sizing: border-box;
  border: 1px solid rgba(255, 255, 255, 0.04);
}

.synthdrop-cell-I { background: var(--synthdrop-i); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-i); }
.synthdrop-cell-O { background: var(--synthdrop-o); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-o); }
.synthdrop-cell-T { background: var(--synthdrop-t); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-t); }
.synthdrop-cell-S { background: var(--synthdrop-s); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-s); }
.synthdrop-cell-Z { background: var(--synthdrop-z); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-z); }
.synthdrop-cell-J { background: var(--synthdrop-j); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-j); }
.synthdrop-cell-L { background: var(--synthdrop-l); box-shadow: inset 0 0 4px rgba(0,0,0,0.4), 0 0 6px var(--synthdrop-l); }
.synthdrop-cell-garbage { background: var(--synthdrop-garbage); }

/* Ghost (SynthdropEngine.ghostRowOffset): an outline-only preview of
   where the falling piece would land on a hard drop -- drawn UNDER the
   real piece (see displayGrid's draw order) so it's only ever visible on
   the rows the real piece hasn't reached yet. */
.synthdrop-cell-ghost {
  background: transparent;
  box-shadow: none;
  border: 1px solid currentColor;
  opacity: 0.35;
}
.synthdrop-cell-I.synthdrop-cell-ghost { color: var(--synthdrop-i); }
.synthdrop-cell-O.synthdrop-cell-ghost { color: var(--synthdrop-o); }
.synthdrop-cell-T.synthdrop-cell-ghost { color: var(--synthdrop-t); }
.synthdrop-cell-S.synthdrop-cell-ghost { color: var(--synthdrop-s); }
.synthdrop-cell-Z.synthdrop-cell-ghost { color: var(--synthdrop-z); }
.synthdrop-cell-J.synthdrop-cell-ghost { color: var(--synthdrop-j); }
.synthdrop-cell-L.synthdrop-cell-ghost { color: var(--synthdrop-l); }

.synthdrop-side {
  text-align: center;
}

.synthdrop-next-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: color-mix(in srgb, var(--fg) 65%, transparent);
  margin-bottom: 0.4em;
}

/* The "next" panel holds a miniature of the upcoming piece, built from the
   same .synthdrop-cell-<name> classes the board uses (see
   SynthdropEngine.previewGrid). The colors live on the CELLS inside it,
   never on this container: when this element carried the piece class
   itself, its own `background` silently beat .synthdrop-cell-<name>'s --
   identical specificity (both single-class), and this rule sits later in
   the file -- so the preview always painted as an empty dark square.
   Sized by its content rather than a fixed 3x3, since pieces differ in
   footprint (I is 4x1, O is 2x2) and a trimmed grid shouldn't be letterboxed
   inside a fixed square. */
.synthdrop-next-piece {
  min-width: calc(var(--synthdrop-preview-cell) * 4 + 12px);
  min-height: calc(var(--synthdrop-preview-cell) * 2 + 12px);
  border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
  background: #05030a;
  margin: 0 auto;
  padding: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.synthdrop-next-row {
  display: flex;
}

/* Overrides .synthdrop-cell's board sizing -- a preview cell is smaller
   than a playfield cell so a 4-wide I piece still fits the side panel. */
.synthdrop-next-cell {
  width: var(--synthdrop-preview-cell);
  height: var(--synthdrop-preview-cell);
}

/* A trimmed bounding box still has holes (the T piece's top row is
   [_, X, _]), and those must be invisible rather than showing
   .synthdrop-cell's faint grid border. */
.synthdrop-next-cell-empty {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* Flash overlay (SynthdropController's vm.flashes): transient, glowing,
   centered text -- level-ups, line-clear labels, "SYSTEM CRASH" -- laid
   over the board rather than pushed into the document flow, since it's
   meant to interrupt the eye for a moment without shoving the board
   around underneath it. */
.synthdrop-flash-layer {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  text-align: center;
  padding: 0 0.5em;
}

.synthdrop-flash {
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  font-size: 1rem;
  color: var(--fg);
  text-shadow: 0 0 10px var(--glow);
  animation: synthdrop-flash-pop 1.4s ease-out forwards;
}

.synthdrop-flash-tier {
  font-size: 1.15rem;
}

/* Per-tier flash color (SynthdropController's flash(tier.transition, ...)
   -- see the .synthdrop-tier-<theme>/.synthdrop-transition-<theme> block
   below for the matching ambient board reskin and one-shot overlay). */
.synthdrop-flash-tier-neon { color: var(--synthdrop-t); text-shadow: 0 0 14px var(--synthdrop-t); }
.synthdrop-flash-tier-chrome { color: #e8eef4; text-shadow: 0 0 14px #ffffff; }
.synthdrop-flash-tier-static { color: #cfcfcf; text-shadow: 0 0 10px #ffffff; }
.synthdrop-flash-tier-overdrive { color: #ff5a1f; text-shadow: 0 0 14px #ff5a1f; }
.synthdrop-flash-tier-black-ice { color: #29e0ff; text-shadow: 0 0 14px #29e0ff; }
.synthdrop-flash-tier-ghost { color: #dcf5ff; text-shadow: 0 0 14px #dcf5ff; }
.synthdrop-flash-tier-singularity { color: #fff; text-shadow: 0 0 18px #d9a4ff; }

.synthdrop-flash-quad {
  color: var(--accent2);
  text-shadow: 0 0 16px var(--accent2);
  font-size: 1.3rem;
}

.synthdrop-flash-crash {
  color: var(--danger);
  text-shadow: 0 0 16px var(--danger);
  font-size: 1.5rem;
}

@keyframes synthdrop-flash-pop {
  0% { opacity: 0; transform: scale(0.6); }
  15% { opacity: 1; transform: scale(1.15); }
  30% { transform: scale(1); }
  75% { opacity: 1; }
  100% { opacity: 0; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .synthdrop-flash, .synthdrop-waiting {
    animation: none;
  }
}

.synthdrop-paused-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  letter-spacing: 2px;
  color: var(--fg);
  background: rgba(0, 0, 0, 0.4);
  animation: blink 1s step-start infinite;
}

.synthdrop-hint-bar {
  background: var(--fg);
  color: var(--bg);
  font-size: 0.8rem;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 4px 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.25em;
}

.synthdrop-hint-key {
  background: var(--bg);
  color: var(--fg);
  padding: 0 0.4em;
  margin-right: 0.4em;
  font-weight: bold;
}

.synthdrop-crash-title {
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  color: var(--danger);
  text-shadow: 0 0 16px var(--danger);
}

.synthdrop-win-title {
  font-family: 'Press Start 2P', 'VT323', 'Courier New', monospace;
  color: var(--synthdrop-s);
  text-shadow: 0 0 16px var(--synthdrop-s);
}

.synthdrop-outcome-label {
  text-transform: uppercase;
  letter-spacing: 2px;
  font-size: 1.3rem;
}

.synthdrop-outcome-lose {
  color: var(--danger);
}
