/* =========================================================
   AIMBOTTUK admin — dark glass theme after ghostlynex.fun
   Palette, type, radii and easings are taken from that site's
   own tokens (--bg #07050b, --acc #a88dff / --acc-2 #7c5cf0,
   Onest, radii 30/18/16, cubic-bezier(.22,1,.36,1) and the
   .34,1.56,.64,1 spring). Container 1280, base type 15px.
   ========================================================= */
:root {
  --bg: #07050b;
  --bg-darker: #050409;
  /* Translucent on purpose: the glass surfaces get their backdrop-filter from the layer at the
     bottom of this file. A solid value here would silently turn every one of them opaque. */
  --panel: rgba(22, 16, 37, .62);
  --panel-2: rgba(29, 21, 48, .68);
  --panel-hover: rgba(41, 30, 66, .72);
  --border: rgba(255, 255, 255, .08);
  --border-soft: rgba(255, 255, 255, .05);

  --text: #f4f1fa;
  --text-2: #bdb5d0;
  --muted: #847d98;

  --primary: #7c5cf0;
  --primary-strong: #6d4ce6;
  --primary-dark: #5b3fe0;
  --primary-soft: #a88dff;
  --primary-tint: rgba(124, 92, 240, .16);
  --primary-line: rgba(168, 141, 255, .3);

  --success: #8fe3c0;
  --warn: #ffd08a;
  --danger: #ff9aa9;
  --info: #9ec5ff;

  --ctl: rgba(255, 255, 255, .05);
  --ctl-hover: rgba(255, 255, 255, .09);
  --ctl-border: rgba(255, 255, 255, .1);
  --input-bg: rgba(8, 6, 14, .55);
  /* Opaque sibling of --ctl for the handful of surfaces the browser draws
     itself rather than compositing over the page - the <select> popup above
     all. A translucent value there shows the OS's white surface underneath. */
  --ctl-solid: #1b1626;

  --radius: 22px;
  --radius-sm: 14px;
  --radius-xs: 10px;
  /* the reference site's own scale, used by the glass layer below */
  --r-card: 30px;
  --r-in: 18px;
  --r-fld: 16px;
  --pill: 999px;
  --glass-blur: 18px;

  /* Raw accent channels. theme.js rewrites these from the chosen accent; the values here are the
     default violet and exist so the first paint - before theme.js has run - is not transparent.
     Everything translucent in the glass layer below is rgba(var(--acc-rgb), a), which is what makes
     the accent picker actually recolour the whole panel instead of just the buttons. */
  --acc-rgb: 124, 92, 240;
  --acc-strong-rgb: 112, 82, 226;
  --acc-deep-rgb: 84, 58, 173;
  --acc-soft-rgb: 178, 156, 255;
  --acc-pale-rgb: 220, 209, 255;
  --bg-rgb: 7, 5, 11;

  /* Floating sheets - popover, dialog, toast - are near-opaque on purpose. They sit above an
     animated backdrop, and the topbar's own backdrop-filter forms a "backdrop root" that stops a
     nested blur from sampling the page behind it: at panel-level translucency the background came
     straight through underneath the settings text. */
  --sheet: rgba(15, 11, 26, .93);

  --sh-2: 0 26px 60px -26px rgba(0, 0, 0, .92);
  --sh-accent: 0 10px 34px -10px rgba(124, 92, 240, .55);

  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-io: cubic-bezier(.65, 0, .35, 1);
  --spring: cubic-bezier(.34, 1.56, .64, 1);
  --t: .24s var(--ease);

  --font: 'Onest', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --container: 1280px;
  --pad: 32px;
  --topbar-h: 72px;
}

/* The reference site declares --spring twice and so does this: the cubic-bezier above is the
   fallback, and where the browser can parse linear() easing the spring becomes a real sampled
   spring curve instead of an approximation of one. The difference is not academic - a cubic-bezier
   with an overshoot above 1 can only bulge once and then settle monotonically, whereas the sampled
   curve below carries the actual damped oscillation (it peaks around 1.108 at 26% and is still
   moving at 41%). Everything in the panel that uses var(--spring) - the cards lifting on hover, the
   hour bars, the modal, the toasts, the settings glider - picks this up with no further change,
   which is the whole reason the reference puts it in a variable.

   Curve copied from the reference verbatim rather than re-derived: it is a sampled spring, and
   re-sampling "something similar" would produce a different feel for no benefit. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --spring: linear(0, .009, .035 2.1%, .141 4.4%, .723 12.9%, .938 16.7%, 1.017 19.4%, 1.067,
      1.099 24.3%, 1.108 26%, 1.1 27.9%, 1.019 34.8%, .991 38.8%, .986 41.4%, .993 50%, 1.001 64.1%, 1);
  }
}

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  overflow-x: hidden;
}

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }

/* Tell the browser this is a dark page. Without it the UA assumes a light document and paints
   every native control it draws itself - form fields, the password-reveal eye, scrollbars, date
   pickers, and above all the autofill highlight - in light colours that no amount of CSS on the
   element can reach. Declared here and as a <meta name="color-scheme"> in the head; the meta
   matters because it applies before this stylesheet finishes loading. */
:root { color-scheme: dark; }

/* Autofilled fields. Chrome ignores background and background-color on :-webkit-autofill and
   paints its own light box over the field, which on a dark panel reads as a white rectangle
   exactly where the login and password go. Two overrides are needed and neither is obvious:
     * an inset box-shadow the size of the field, because that is the only property the UA lets
       win over its own autofill background;
     * -webkit-text-fill-color, because `color` is also overridden and the text would be
       near-invisible dark-on-dark without it.
   The absurdly long transition is the standard companion trick: it defers the UA's own
   background-colour change far past any session, so it never gets a chance to repaint. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill {
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
  box-shadow: 0 0 0 1000px var(--input-bg) inset;
  border-color: var(--ctl-border);
  transition: background-color 99999s ease-in-out 0s;
}

/* Firefox has no :-webkit-autofill; it uses color-scheme plus its own preview state. */
input:autofill, textarea:autofill, select:autofill {
  background-color: var(--input-bg);
  color: var(--text);
}

a { color: inherit; text-decoration: none; }
svg { display: block; flex: none; }
[hidden] { display: none !important; }
::selection { background: rgba(139, 92, 246, .35); color: #fff; }
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 3px; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-darker); }
::-webkit-scrollbar-thumb { background: #333338; border-radius: 5px; border: 2px solid var(--bg-darker); }
::-webkit-scrollbar-thumb:hover { background: #45454c; }

/* ============ backdrop: animated, transform-only (composited, no repaints) ============ */
.bg {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none; overflow: hidden;
  background-color: var(--bg);
  contain: layout paint style;
  container-type: size;
}
.bg.paused * { animation-play-state: paused !important; }

.bg-aurora {
  position: absolute; border-radius: 50%;
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}
.bg-aurora-1 {
  top: -28vh; left: -10vw; width: 66vw; height: 54vh; opacity: .5;
  background: radial-gradient(circle at 50% 50%, var(--fx-aura-1, rgba(124,58,237,.34)), var(--fx-aura-1b, rgba(109,40,217,.1)) 46%, transparent 72%);
  animation: drift-1 17s ease-in-out infinite alternate;
}
.bg-aurora-2 {
  top: -18vh; right: -16vw; width: 54vw; height: 48vh; opacity: .38;
  background: radial-gradient(circle at 50% 50%, var(--fx-aura-2, rgba(99,102,241,.3)), var(--fx-aura-2b, rgba(76,29,149,.08)) 48%, transparent 74%);
  animation: drift-2 23s ease-in-out infinite alternate;
}
.bg-aurora-3 {
  bottom: -32vh; left: 18vw; width: 56vw; height: 46vh; opacity: .22;
  background: radial-gradient(circle at 50% 50%, var(--fx-aura-3, rgba(167,139,250,.2)), transparent 70%);
  animation: drift-3 29s ease-in-out infinite alternate;
}
/* Travel roughly doubled and periods cut by a third. At 7vw over 28s the bloom moved about three
   pixels a second on a 1600px screen - measurable with a ruler, invisible to a person, which is
   exactly the complaint ("тупо фиксированный"). These numbers are still slow enough to sit behind
   text without pulling the eye. */
@keyframes drift-1 {
  0% { transform: translate3d(0, 0, 0) scale(1); }
  100% { transform: translate3d(13vw, 10vh, 0) scale(1.3); }
}
@keyframes drift-2 {
  0% { transform: translate3d(0, 0, 0) scale(1.08); }
  100% { transform: translate3d(-15vw, 11vh, 0) scale(.9); }
}
@keyframes drift-3 {
  0% { transform: translate3d(0, 0, 0) scale(1); }
  100% { transform: translate3d(-16vw, -11vh, 0) scale(1.26); }
}

.bg-grid {
  position: absolute; inset: -64px;
  background-image:
    linear-gradient(to right, var(--fx-grid, rgba(255, 255, 255, .045)) 1px, transparent 1px),
    linear-gradient(to bottom, var(--fx-grid, rgba(255, 255, 255, .045)) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 82% 66% at 50% 16%, #000 10%, transparent 82%);
  -webkit-mask-image: radial-gradient(ellipse 82% 66% at 50% 16%, #000 10%, transparent 82%);
  will-change: transform;
  transform: translateZ(0);
  animation: grid-pan 13s linear infinite;
}
@keyframes grid-pan {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(64px, 64px, 0); }
}

.bg-vignette {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 124% 96% at 50% 34%, transparent 52%, rgba(8, 8, 10, .5) 100%);
}

/* bottom glow — accent coloured, slowly breathing. It animates opacity, which is why the effects
   that want it gone use display:none rather than opacity:0: an animation beats a declaration. */
.bg-base {
  position: absolute; left: -10%; right: -10%; bottom: -6vh; height: 40vh;
  background: radial-gradient(ellipse 60% 100% at 50% 100%, var(--fx-base, rgba(139, 92, 246, .22)), transparent 72%);
  opacity: .7;
  will-change: opacity;
  animation: base-breathe 7s ease-in-out infinite alternate;
}
@keyframes base-breathe {
  from { opacity: .3; }
  to { opacity: .95; }
}

/* «Сияние» had nothing that obviously travelled: three blooms drifting a few pixels a second and a
   grid panning under a mask read as a still image. The band layer already existed (a wide diagonal
   accent gradient, used by other effects) and was simply switched off here - giving it a slow sweep
   is one rule and gives the eye something to follow. */
.bg[data-fx="aurora"] .bg-band {
  opacity: .9;
  animation: band-sweep 19s var(--ease-io) infinite alternate;
}
@keyframes band-sweep {
  from { transform: translate3d(-9%, -4%, 0) rotate(-1.5deg) scaleY(.94); }
  to { transform: translate3d(10%, 5%, 0) rotate(1.5deg) scaleY(1.08); }
}

/* ---- background effects: shared by the page backdrop and the settings previews ----
   All motion is transform/opacity only. Sizes and travel use container units
   (cqw/cqh) so the same keyframes work full-screen and inside a small preview tile.
   Every colour comes from the accent (--fx-*), so the whole backdrop follows the theme. */
.bg-fx, .fx-layer { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.bg-fx i, .fx-layer i { position: absolute; display: block; }

/* ---- per-effect atmosphere layers (cross-fade on switch) ---- */
.bg-band, .bg-clouds, .bg-mist, .bg-scanlines {
  position: absolute; pointer-events: none;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.bg-band {
  top: -22%; left: -22%; width: 144%; height: 144%;
  background:
    linear-gradient(112deg, transparent 34%, var(--fx-band, rgba(167, 139, 250, .17)) 48%, transparent 64%),
    linear-gradient(112deg, transparent 43%, rgba(255, 255, 255, .05) 50%, transparent 58%);
}
.bg-clouds {
  left: -12%; right: -12%; top: -16vh; height: 34vh;
  background:
    radial-gradient(ellipse 42% 74% at 20% 42%, var(--fx-cloud, rgba(210, 225, 245, .07)), transparent 70%),
    radial-gradient(ellipse 48% 84% at 58% 28%, var(--fx-cloud, rgba(210, 225, 245, .06)), transparent 72%),
    radial-gradient(ellipse 40% 70% at 88% 46%, var(--fx-cloud, rgba(210, 225, 245, .05)), transparent 70%),
    linear-gradient(to bottom, rgba(6, 8, 12, .92), transparent 88%);
}
.bg-mist {
  left: 0; right: 0; bottom: 0; height: 30vh;
  background: linear-gradient(to top, var(--fx-mist, rgba(200, 225, 255, .13)) 0%, transparent 76%);
}
.bg-scanlines {
  inset: 0;
  background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, .4) 2px 4px);
}

/* ---- seasonal overlays: body[data-mode] --------------------------------
   These sit on top of whichever background is chosen, not instead of it: «Новый год» snows over
   «Звёзды» and over «Сияние» alike. All three are cheap on purpose - a mode is decoration the panel
   wears for a week, not a second theme, and it must not be the reason a machine that already had
   animation switched off in Windows starts dropping frames. One animated property each (opacity),
   no blur, no blend mode, no masks. */
.bg-garland, .bg-fog, .bg-neon {
  position: absolute; pointer-events: none; opacity: 0;
  transition: opacity .55s var(--ease);
}
/* A wire across the top with sixteen bulbs hanging off it. The bulbs are built by
   buildGarland() in theme.js so the markup stays one line; each gets its own negative delay, which
   is what makes it twinkle instead of breathing as one lamp. */
.bg-garland { left: 0; right: 0; top: 0; height: 46px; }
.bg-garland::before {
  content: ''; position: absolute; left: 0; right: 0; top: 9px; height: 2px;
  background: linear-gradient(to right, transparent, rgba(255, 255, 255, .18) 12%, rgba(255, 255, 255, .18) 88%, transparent);
}
.bg-garland i {
  position: absolute; top: 10px;
  width: 9px; height: 12px; margin-left: -4.5px;
  border-radius: 50% 50% 46% 46%;
  background: var(--fx-glow, rgba(255, 210, 140, .8));
  box-shadow: 0 0 14px 2px rgba(var(--acc-rgb), .55);
  will-change: opacity;
  animation: garland-blink 3.1s ease-in-out infinite;
}
/* Three colours, because a garland in one colour is a row of dots. All three are accent-derived so
   the strand follows the theme instead of fighting it. */
.bg-garland i:nth-child(3n+1) { background: rgba(var(--acc-pale-rgb), .95); }
.bg-garland i:nth-child(3n+2) { background: var(--success, #8fe3c0); }
.bg-garland i:nth-child(3n) { background: var(--warn, #ffd08a); }
body[data-mode="newyear"] .bg-garland { opacity: 1; }
@keyframes garland-blink {
  0%, 100% { opacity: .35; }
  42% { opacity: 1; }
  58% { opacity: .78; }
}

/* Low warm fog along the bottom, two layers drifting at different speeds so the edge between them
   moves. Reads as ground mist under a fire, which is what «Угли» is standing on. */
.bg-fog { left: -20%; right: -20%; bottom: -6vh; height: 34vh; will-change: transform, opacity; }
.bg-fog-1 {
  background: radial-gradient(ellipse 60% 100% at 28% 100%, rgba(var(--acc-soft-rgb), .16), transparent 72%);
}
.bg-fog-2 {
  height: 26vh;
  background: radial-gradient(ellipse 54% 100% at 72% 100%, rgba(var(--acc-pale-rgb), .12), transparent 70%);
}
body[data-mode="halloween"] .bg-fog { opacity: 1; }
body[data-mode="halloween"] .bg-fog-1 { animation: fog-1 37s var(--ease-io) infinite alternate; }
body[data-mode="halloween"] .bg-fog-2 { animation: fog-2 29s var(--ease-io) infinite alternate; }
@keyframes fog-1 {
  from { transform: translate3d(-4%, 0, 0) scaleY(.94); }
  to { transform: translate3d(5%, -2vh, 0) scaleY(1.08); }
}
@keyframes fog-2 {
  from { transform: translate3d(4%, 0, 0) scaleY(1.04); }
  to { transform: translate3d(-5%, -1.4vh, 0) scaleY(.92); }
}

/* Neon: the scanline layer the panel already had, lit up, plus a cold horizon glow at the bottom.
   No animation at all - «Киберпанк» gets its movement from the «Код» background the mode suggests. */
.bg-neon {
  left: 0; right: 0; bottom: 0; height: 30vh;
  background: linear-gradient(to top, rgba(var(--acc-rgb), .2), transparent 78%);
}
body[data-mode="cyber"] .bg-neon { opacity: 1; }
body[data-mode="cyber"] .bg-scanlines { opacity: .5; }

/* ---- per-effect backdrop recipe ----
   The base colour is always --bg (set on .bg above), so effects differ by atmosphere layers and
   particles rather than by hue: a background never fights the panels or the buttons.

   «Комета», «Дождь» and «Звёзды» want a clean field, so the blooms and the floor grid fade out
   under them. Faded, not display:none - switching background is meant to read as a cross-fade, and
   a layer that disappears in one frame looks like a flash. Both of those layers animate transform
   only, so an opacity transition on them is free. `.bg-base` is the exception and stays
   display:none: it animates opacity itself (base-breathe), and an animation beats a declaration, so
   the only way to actually turn it off is not to render it. */
.bg-aurora, .bg-grid { transition: opacity .55s var(--ease); }

/* The figure effects want a clean field: blooms and a floor grid behind falling drops or glyph
   columns just muddy them. «Снег» and «Угли» are the exception and keep the sky - they are
   weather, and weather with nothing behind it looks like a sticker on a flat panel. Their own bloom
   levels are set in the atmosphere block below. */
.bg[data-fx="rain"] .bg-aurora,
.bg[data-fx="stars"] .bg-aurora,
.bg[data-fx="code"] .bg-aurora,
.bg[data-fx="rain"] .bg-grid,
.bg[data-fx="stars"] .bg-grid { opacity: 0; }

.bg[data-fx="rain"] .bg-base,
.bg[data-fx="stars"] .bg-base,
.bg[data-fx="code"] .bg-base { display: none; }

/* atmosphere per effect. The rule of thumb these follow: a figure needs a clean field behind it, and
   weather needs a sky. Snow and embers are weather, so they get the layers that read as sky and
   ground - a cloud deck, low mist, and for embers a taller bottom glow, since a fire that does not
   light the ground under it looks pasted on. Code is a figure on a dark field, so the blooms come
   off: three drifting lights behind glyph columns turn them into noise. Petals and confetti keep a
   little sky because they are things falling *through* air, and air with nothing in it reads as a
   sticker rather than as a scene. */
.bg[data-fx="rain"] .bg-clouds { opacity: 1; }
.bg[data-fx="rain"] .bg-mist { opacity: .55; }
.bg[data-fx="snow"] .bg-clouds { opacity: 1; }
.bg[data-fx="snow"] .bg-mist { opacity: .7; }
.bg[data-fx="snow"] .bg-aurora-1 { opacity: .26; }
.bg[data-fx="snow"] .bg-aurora-3 { opacity: .16; }
.bg[data-fx="embers"] .bg-mist { opacity: .4; }
.bg[data-fx="embers"] .bg-base { height: 52vh; }
.bg[data-fx="petals"] .bg-mist { opacity: .4; }
.bg[data-fx="petals"] .bg-aurora-3 { opacity: .18; }
.bg[data-fx="confetti"] .bg-clouds { opacity: .7; }
.bg[data-fx="confetti"] .bg-mist { opacity: .3; }
/* «Молнии» keep a full storm sky on at all times: the bolt itself is lit about a twelfth of the cycle,
   so without the clouds and the lit-from-under bloom this background is an empty screen between
   flashes. That is the same trap «Салют» fell into, solved by leaving something on rather than by
   making the flash longer. */
.bg[data-fx="lightning"] .bg-clouds { opacity: 1; }
.bg[data-fx="lightning"] .bg-mist { opacity: .55; }
.bg[data-fx="lightning"] .bg-aurora-1 { opacity: .3; }
.bg[data-fx="lightning"] .bg-base { height: 46vh; opacity: .45; }
.bg[data-fx="frost"] .bg-clouds { opacity: .8; }
.bg[data-fx="frost"] .bg-mist { opacity: .6; }
.bg[data-fx="frost"] .bg-aurora-1 { opacity: .22; }
.bg[data-fx="frost"] .bg-aurora-3 { opacity: .14; }
/* «Роса» is on the near side of the glass, so what is behind it should be soft and close: mist up, the
   floor bloom off (a droplet field with a bright horizon under it reads as a spill), blooms dimmed. */
.bg[data-fx="dew"] .bg-mist { opacity: .75; }
.bg[data-fx="dew"] .bg-clouds { opacity: .6; }
.bg[data-fx="dew"] .bg-base { display: none; }
.bg[data-fx="dew"] .bg-aurora-1 { opacity: .3; }
.bg[data-fx="dew"] .bg-aurora-2 { opacity: .2; }
.bg[data-fx="streamers"] .bg-clouds { opacity: .8; }
.bg[data-fx="streamers"] .bg-mist { opacity: .45; }

/* vignette strength per effect */
.bg[data-fx="stars"] .bg-vignette { opacity: .6; }
.bg[data-fx="rain"] .bg-vignette { opacity: .8; }
.bg[data-fx="code"] .bg-vignette { opacity: .7; }
.bg[data-fx="confetti"] .bg-vignette { opacity: .6; }
.bg[data-fx="lightning"] .bg-vignette { opacity: .88; }
.bg[data-fx="frost"] .bg-vignette { opacity: .62; }
.bg[data-fx="dew"] .bg-vignette { opacity: .55; }
.bg[data-fx="streamers"] .bg-vignette { opacity: .7; }

/* ---- «Дождь»: a tilted curtain of accent streaks, bright tip first ----
   Two depth tiers are built in js/theme.js (thin/dim/slow against thick/bright/fast); this rule only
   paints one streak. `animation-direction: normal` matters: the shared `.fx-layer i` rule below sets
   `alternate` for the nebula motes, and a raindrop that falls and then rises back up is the one way
   this effect could look broken rather than merely busy. */
[data-fx="rain"] .fx-layer i {
  top: 0; width: 2px; border-radius: 1px; opacity: 0;
  background: linear-gradient(to bottom,
    var(--fx-rain-a, rgba(220, 235, 255, 0)) 0%,
    var(--fx-rain-b, rgba(220, 235, 255, .38)) 26%,
    var(--fx-rain-c, rgba(245, 250, 255, .98)) 100%);
  will-change: transform, opacity;
  animation-name: fx-rain; animation-timing-function: cubic-bezier(.25, .1, .6, .9);
  animation-iteration-count: infinite; animation-direction: normal;
}
/* About one near drop in six is a heavy one (js/theme.js decides): longer, brighter, and with a glow.
   A static box-shadow is painted once and travels with the layer, so this costs nothing per frame -
   animating a shadow is what would have. */
[data-fx="rain"] .fx-layer i.heavy {
  box-shadow: 0 0 14px -2px rgba(var(--acc-soft-rgb), .75);
}
@keyframes fx-rain {
  0% { transform: translate3d(0, -20cqh, 0) rotate(var(--tilt, 7deg)); opacity: 0; }
  10% { opacity: var(--o, .6); }
  90% { opacity: var(--o, .6); }
  100% { transform: translate3d(var(--dx, -3cqw), 120cqh, 0) rotate(var(--tilt, 7deg)); opacity: 0; }
}

/* ---- «Звёзды» in the preview tile: static field, staggered twinkle ----
   Full-screen the stars breathe instead (.bg[data-fx="stars"] .fx-layer i, further down, which wins
   on specificity); this recipe is what the settings tile shows. No animation-direction here on
   purpose: both keyframe sets start and end at the same opacity, so the `alternate` inherited from
   the shared rule is what makes them read as a slow breathe rather than as a blink. */
[data-fx="stars"] .fx-layer i {
  opacity: 0;
  background: radial-gradient(circle at 50% 50%,
    var(--fx-star, #fff) 0 38%, var(--fx-halo, rgba(255, 255, 255, .45)) 60%, transparent 80%);
  will-change: opacity, transform;
  animation-name: fx-twinkle; animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes fx-twinkle {
  0% { opacity: calc(var(--o, .9) * .06); transform: translate3d(0, 0, 0) scale(.65); }
  38% { opacity: var(--o, .9); transform: translate3d(0, -1.1cqh, 0) scale(1.08); }
  58% { opacity: calc(var(--o, .9) * .3); transform: translate3d(0, -1.7cqh, 0) scale(.88); }
  100% { opacity: calc(var(--o, .9) * .06); transform: translate3d(0, -3cqh, 0) scale(.65); }
}

/* ---- «Снег»: soft flakes that sway across their own fall ----
   The sway is written into the keyframe as offsets of one custom property (--dx) rather than as a
   second animation, because an element carries one. Snow is the one falling effect that is NOT the
   rain with round dots: rain goes straight down and reads as weather you are inside, snow crosses
   itself on the way and reads as weather you are watching. `animation-direction: normal` for the
   usual reason - the shared rule says `alternate`, and a flake that falls and rises is a yo-yo. */
[data-fx="snow"] .fx-layer i {
  top: 0; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle at 50% 50%,
    var(--fx-snow, #fff) 0 42%, var(--fx-snow-soft, rgba(255, 255, 255, .3)) 62%, transparent 80%);
  will-change: transform, opacity;
  animation-name: fx-snow; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
@keyframes fx-snow {
  0% { transform: translate3d(0, -10cqh, 0); opacity: 0; }
  8% { opacity: var(--o, .8); }
  30% { transform: translate3d(calc(var(--dx, 6cqw) * .5), 26cqh, 0); }
  55% { transform: translate3d(calc(var(--dx, 6cqw) * -.35), 52cqh, 0); }
  80% { transform: translate3d(calc(var(--dx, 6cqw) * .8), 78cqh, 0); }
  92% { opacity: var(--o, .8); }
  100% { transform: translate3d(calc(var(--dx, 6cqw) * .3), 112cqh, 0); opacity: 0; }
}

/* ---- «Угли»: sparks coming up off the bottom edge, flickering ----
   The flicker is opacity steps inside the same keyframe that carries the rise: a spark that climbs
   smoothly is a bubble, and a bubble is not what a bed of embers looks like. The shadow on the big
   ones is static, so it is painted once and travels with the layer. */
[data-fx="embers"] .fx-layer i {
  bottom: 0; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle at 50% 50%,
    var(--fx-ember, #ffb066) 0 38%, var(--fx-ember-soft, rgba(255, 150, 60, .45)) 60%, transparent 82%);
  will-change: transform, opacity;
  animation-name: fx-embers; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
[data-fx="embers"] .fx-layer i.big { box-shadow: 0 0 14px 2px rgba(var(--acc-rgb), .45); }
@keyframes fx-embers {
  0% { transform: translate3d(0, 4cqh, 0) scale(.6); opacity: 0; }
  9% { opacity: var(--o, .85); }
  24% { opacity: calc(var(--o, .85) * .45); }
  38% { opacity: var(--o, .85); }
  52% { transform: translate3d(var(--dx, 3cqw), -46cqh, 0) scale(1); }
  64% { opacity: calc(var(--o, .85) * .4); }
  78% { opacity: calc(var(--o, .85) * .85); }
  100% { transform: translate3d(calc(var(--dx, 3cqw) * -.6), -104cqh, 0) scale(.7); opacity: 0; }
}

/* ---- «Код»: segmented columns falling with a bright leading head ----
   The shape everybody reads as "matrix", built from a repeating gradient instead of glyphs, so it
   follows the accent like every other effect instead of hardcoding green. The bright dash sits at
   the BOTTOM of each segment and the head dot at the very end, because the column falls head-first -
   a gradient that is bright at the top reads as a column moving up. */
[data-fx="code"] .fx-layer i {
  top: 0; width: 3px; border-radius: 2px; opacity: 0;
  background-image: repeating-linear-gradient(to bottom,
    transparent 0 6px,
    var(--fx-code-a, rgba(180, 200, 255, .5)) 6px 9px,
    var(--fx-code-b, rgba(240, 250, 255, .95)) 9px 12px);
  will-change: transform, opacity;
  animation-name: fx-code; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
[data-fx="code"] .fx-layer i::after {
  content: ''; position: absolute; bottom: -2px; left: 50%;
  width: 5px; height: 5px; margin-left: -2.5px; border-radius: 50%;
  background: var(--fx-code-b, #fff);
  box-shadow: 0 0 12px 1px rgba(var(--acc-soft-rgb), .8);
}
@keyframes fx-code {
  0% { transform: translate3d(0, -32cqh, 0); opacity: 0; }
  7% { opacity: var(--o, .8); }
  93% { opacity: var(--o, .8); }
  100% { transform: translate3d(0, 128cqh, 0); opacity: 0; }
}

/* ---- «Лепестки»: soft shapes falling and turning ----
   Both falling effects that already exist go straight down: the rain is a line with a bright tip and
   the snow sways but stays round. A petal is longer than it is wide and turns over as it falls, and
   the turn is what separates it from the other two - so rotate() is in the same keyframe as the
   translate, since one element carries one animation and a second one would cost frames on a layer
   this crowded.

   The border-radius is the four-corner form with a slash, which is what makes an asymmetric leaf
   shape rather than an ellipse: the horizontal radii and the vertical ones are different, so the
   shape has a pointed end and a rounded one. Sizes stay small (6-15px) - a petal the size of a thumb
   reads as debris. */
[data-fx="petals"] .fx-layer i {
  top: -10cqh;
  border-radius: 62% 38% 55% 45% / 60% 55% 45% 40%;
  opacity: 0;
  background-image: linear-gradient(140deg,
    var(--fx-petal, #e9e2ff) 0 44%,
    var(--fx-petal-soft, rgba(233, 226, 255, .42)) 100%);
  box-shadow: none;
  will-change: transform, opacity;
  animation-name: fx-petal; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
@keyframes fx-petal {
  0% { transform: translate3d(0, -12cqh, 0) rotate(var(--rot, 0deg)) scale(.9); opacity: 0; }
  12% { opacity: var(--o, .55); }
  50% { transform: translate3d(var(--dx, 8cqw), 56cqh, 0)
                     rotate(calc(var(--rot, 0deg) + 200deg)) scale(1); }
  88% { opacity: calc(var(--o, .55) * .85); }
  100% { transform: translate3d(calc(var(--dx, 8cqw) * -.4), 122cqh, 0)
                      rotate(calc(var(--rot, 0deg) + 430deg)) scale(.94); opacity: 0; }
}

/* ---- «Конфетти»: the one effect that is not a single colour ----
   Every piece takes one of the four status colours, handed out by nth-child rather than chosen in
   js/theme.js. Two reasons: a builder called once per particle has no memory and would produce three
   reds in a row by pure luck, and colours that come from the palette the operator picked stay tied to
   the rest of the panel - a hardcoded rainbow is the fastest way to make a themed panel look broken.

   The flip is the motion that sells it. A rectangle that falls is a raindrop with corners; one that
   turns edge-on and comes back reads as paper, and edge-on is just scaleX(.16) at the halfway point.
   The sheen is a static background-image over the colour, so the pieces catch light as they turn
   without anything being repainted. */
[data-fx="confetti"] .fx-layer i {
  top: -8cqh;
  border-radius: 1.5px; opacity: 0;
  background-image: linear-gradient(178deg,
    rgba(255, 255, 255, .42) 0 34%, rgba(255, 255, 255, 0) 72%);
  background-color: var(--success, #8fe3c0);
  box-shadow: none;
  will-change: transform, opacity;
  animation-name: fx-confetti; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
[data-fx="confetti"] .fx-layer i:nth-child(4n+2) { background-color: var(--warn, #ffd08a); }
[data-fx="confetti"] .fx-layer i:nth-child(4n+3) { background-color: var(--danger, #ff9aa9); }
[data-fx="confetti"] .fx-layer i:nth-child(4n) { background-color: var(--info, #9ec5ff); }
[data-fx="confetti"] .fx-layer i.wide { border-radius: 2px; }
@keyframes fx-confetti {
  0% { transform: translate3d(0, -10cqh, 0) rotate(0deg) scaleX(1); opacity: 0; }
  10% { opacity: var(--o, .85); }
  50% { transform: translate3d(var(--dx, 5cqw), 54cqh, 0) rotate(190deg) scaleX(.16); }
  90% { opacity: calc(var(--o, .85) * .8); }
  100% { transform: translate3d(0, 120cqh, 0) rotate(384deg) scaleX(1); opacity: 0; }
}

/* ---- «Молнии»: a jagged bolt, and the storm sky it happens in ----
   The only broken line in the set - the rain is straight, the streamers curve, and nothing else here
   has a corner. Three parts: the element is the main channel, ::before continues it at an angle, and
   ::after is a short fork off the middle. A fork rather than a third chained segment on purpose: both
   pseudo-elements hang off the parent, so neither has to know where the other ended. Two segments read
   as a bent stick and four would need a second element per bolt.

   The bolt is lit ~12% of its 5s cycle, so this effect is dark most of the time by design - and that
   is why it gets a permanent storm sky underneath (clouds, mist and a dimmed bloom, in the atmosphere
   block). Without it the background is an empty screen between flashes, and any pixel measurement
   taken between two flashes measures nothing. Same reasoning as «Салют» used to need, but solved by
   leaving something on screen rather than by widening the flash. */
[data-fx="lightning"] .fx-layer i {
  border-radius: 2px; opacity: 0;
  background-image: linear-gradient(to bottom,
    rgba(255, 255, 255, .1) 0%, var(--fx-bolt, #fff) 30%, var(--fx-bolt, #fff) 100%);
  box-shadow: 0 0 16px 2px var(--fx-bolt-glow, rgba(167, 139, 250, .5));
  transform-origin: 50% 0;
  will-change: opacity;
  animation-name: fx-bolt; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
[data-fx="lightning"] .fx-layer i::before,
[data-fx="lightning"] .fx-layer i::after {
  content: ''; position: absolute; left: 0; width: 100%;
  border-radius: 2px;
  background-image: linear-gradient(to bottom,
    var(--fx-bolt, #fff), rgba(255, 255, 255, .28));
  transform-origin: 50% 0;
}
[data-fx="lightning"] .fx-layer i::before { top: 94%; height: 76%; transform: rotate(21deg); }
[data-fx="lightning"] .fx-layer i::after { top: 42%; height: 44%; width: 84%; transform: rotate(44deg); }
[data-fx="lightning"] .fx-layer i.bright { box-shadow: 0 0 26px 4px var(--fx-bolt-glow, rgba(167, 139, 250, .6)); }
@keyframes fx-bolt {
  0%, 82% { opacity: 0; }
  83% { opacity: var(--o, .8); }
  85% { opacity: calc(var(--o, .8) * .16); }
  86.5% { opacity: var(--o, .8); }
  90% { opacity: calc(var(--o, .8) * .38); }
  93% { opacity: calc(var(--o, .8) * .1); }
  95%, 100% { opacity: 0; }
}

/* ---- «Иней»: a six-armed crystal ----
   Three bars crossed at 60°, drawn as one background gradient on the element and repeated on both
   pseudo-elements with `inset: 0` and `background-image: inherit` - so the arms cannot drift out of
   registration with each other, which is the failure a hand-written second gradient runs into.
   `inherit` is the part that makes it cheap: one gradient definition, three arms.

   Six arms is what makes it ice rather than a spark. The star field's glint is four-pointed and a
   fraction of the size, and a three-armed shape reads as a mistake rather than as a crystal. */
[data-fx="frost"] .fx-layer i {
  opacity: 0;
  background-image: linear-gradient(to right,
    transparent 0 43%, var(--fx-frost, #e9e2ff) 43% 57%, transparent 57% 100%);
  box-shadow: none;
  will-change: transform, opacity;
  animation-name: fx-frost; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
[data-fx="frost"] .fx-layer i::before,
[data-fx="frost"] .fx-layer i::after {
  content: ''; position: absolute; inset: 0;
  background-image: inherit;
}
[data-fx="frost"] .fx-layer i::before { transform: rotate(60deg); }
[data-fx="frost"] .fx-layer i::after { transform: rotate(120deg); }
[data-fx="frost"] .fx-layer i.near { filter: drop-shadow(0 0 6px var(--fx-frost-soft, rgba(233, 226, 255, .34))); }
@keyframes fx-frost {
  0% { transform: translate3d(0, -16cqh, 0) rotate(var(--rot, 0deg)); opacity: 0; }
  12% { opacity: var(--o, .5); }
  50% { transform: translate3d(var(--dx, 6cqw), 52cqh, 0) rotate(calc(var(--rot, 0deg) + 90deg)); }
  88% { opacity: calc(var(--o, .5) * .8); }
  100% { transform: translate3d(0, 120cqh, 0) rotate(calc(var(--rot, 0deg) + 180deg)); opacity: 0; }
}

/* ---- «Роса»: droplets clinging to the glass ----
   Three stacked radial gradients, and the order is the whole effect: a small hard highlight at 34%/28%
   (upper left, because that is where light comes from), a bright arc near the bottom (where light
   exits the drop after passing through it - without this rim a droplet is a grey disc), and a soft
   body behind both. No `filter: blur()` anywhere, per the cost rules: the softness is painted into
   the gradient once and is free after that.

   `alternate` is correct here: the motion is a swell-and-fade, which has no direction to keep, and
   the same is true of the nebula motes and the star field. */
[data-fx="dew"] .fx-layer i {
  border-radius: 50%;
  background-image:
    radial-gradient(circle at 34% 28%, rgba(255, 255, 255, .62) 0 11%, transparent 29%),
    radial-gradient(circle at 50% 80%, var(--fx-dew-rim, rgba(233, 226, 255, .6)) 0 15%, transparent 33%),
    radial-gradient(circle at 50% 50%, var(--fx-dew, rgba(196, 181, 253, .3)) 0 60%, transparent 78%);
  box-shadow: none;
  will-change: opacity, transform;
  animation-name: fx-dew; animation-timing-function: var(--ease-io);
  animation-iteration-count: infinite; animation-direction: alternate;
}
[data-fx="dew"] .fx-layer i.big { box-shadow: none; }
@keyframes fx-dew {
  0% { opacity: calc(var(--o, .3) * .5); transform: scale(.93); }
  100% { opacity: var(--o, .3); transform: scale(1.07); }
}

/* ---- «Серпантин»: a tapered strip on a sinuous path ----
   Tapered by the gradient (transparent at both ends, colour in the middle) and rounded at the ends, so
   the element is a ribbon rather than a bar. The path is what separates it from the rain: two sideways
   swings on the way down, at 25% and 75%, in opposite directions. A raindrop is a line; this is a
   curve traced over time.

   Built from one element on a curved path rather than three chained segments, because a chain has to
   be assembled with each segment offset by the previous one's sine - geometry nothing in this repo can
   check except a screenshot. One transform cannot be assembled wrong. */
[data-fx="streamers"] .fx-layer i {
  border-radius: 999px; opacity: 0;
  background-image: linear-gradient(to bottom,
    var(--fx-curl-a, rgba(196, 181, 253, 0)) 0%,
    var(--fx-curl-b, rgba(233, 226, 255, .6)) 24% 76%,
    var(--fx-curl-a, rgba(196, 181, 253, 0)) 100%);
  box-shadow: none;
  will-change: transform, opacity;
  animation-name: fx-streamer; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-direction: normal;
}
@keyframes fx-streamer {
  0% { transform: translate3d(0, -22cqh, 0) rotate(-7deg); opacity: 0; }
  12% { opacity: var(--o, .45); }
  25% { transform: translate3d(var(--dx, 7cqw), 12cqh, 0) rotate(6deg); }
  50% { transform: translate3d(calc(var(--dx, 7cqw) * -.7), 46cqh, 0) rotate(-6deg); }
  75% { transform: translate3d(var(--dx, 7cqw), 80cqh, 0) rotate(5deg); }
  90% { opacity: calc(var(--o, .45) * .7); }
  100% { transform: translate3d(0, 122cqh, 0) rotate(-4deg); opacity: 0; }
}

/* stars - dense field, slow global drift, every near star gets a 4-point glint.
   150s was a drift you had to stare at to believe; 104s is still calm but reads as motion in the
   corner of the eye, which is the point of a star field behind a panel you are reading. */
[data-fx="stars"] .fx-layer { animation: star-drift 104s linear infinite; will-change: transform; }
@keyframes star-drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-3cqw, -2.4cqh, 0); }
}
[data-fx="stars"] .fx-layer i.big::before,
[data-fx="stars"] .fx-layer i.big::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  background: var(--fx-star, #fff);
  transform: translate(-50%, -50%);
}
[data-fx="stars"] .fx-layer i.big::before { width: 420%; height: 1px; }
[data-fx="stars"] .fx-layer i.big::after { width: 1px; height: 420%; }

/* inside the small settings previews keep the accents proportionate */
.fx-tile[data-fx="stars"] .fx-layer i.big::before { width: 300%; }
.fx-tile[data-fx="stars"] .fx-layer i.big::after { height: 300%; }

/* ============ primitives ============ */
.mark {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: 8px;
  color: var(--primary-soft);
  background-color: var(--panel-2);
  border: 1px solid var(--border);
}
.mark.sm { width: 26px; height: 26px; border-radius: var(--radius-sm); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 12px 22px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-size: 14.5px; font-weight: 550; letter-spacing: -.005em;
  white-space: nowrap;
  transition: background-color var(--t), border-color var(--t), color var(--t),
              transform .15s var(--ease), box-shadow var(--t);
}
.btn:active { transform: scale(.98); }
.btn[disabled] { opacity: .45; pointer-events: none; }

.btn-primary { background-color: var(--primary-strong); color: #fff; }
.btn-primary:hover { background-color: var(--primary-dark); box-shadow: var(--sh-accent); }

.btn-ghost { background-color: var(--ctl); border-color: var(--ctl-border); color: var(--text-2); }
.btn-ghost:hover { background-color: var(--ctl-hover); border-color: #43434a; color: var(--text); }
.btn-danger-ghost:hover { border-color: rgba(248, 113, 113, .45); color: var(--danger); background-color: rgba(248, 113, 113, .08); }
.btn-danger { background-color: #b91c1c; color: #fff; }
.btn-danger:hover { background-color: #dc2626; }

.btn-block { width: 100%; padding: 14px 22px; font-size: 15px; }
.btn-sm { padding: 9px 16px; font-size: 13.5px; gap: 7px; }

.icon-btn {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--ctl-border);
  background-color: var(--ctl);
  color: var(--text-2);
  transition: background-color var(--t), color var(--t), border-color var(--t), transform .15s var(--ease);
}
.icon-btn:hover { background-color: var(--ctl-hover); color: var(--text); border-color: #43434a; }
.icon-btn:active { transform: scale(.94); }
.icon-btn.sm { width: 32px; height: 32px; }
.icon-btn.danger:hover { color: var(--danger); border-color: rgba(248, 113, 113, .4); background-color: rgba(248, 113, 113, .08); }
.icon-btn.spin:hover svg { animation: spin .7s var(--ease); }
@keyframes spin { to { transform: rotate(360deg); } }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-xs);
  font-size: 12px; font-weight: 550; letter-spacing: .05em; text-transform: uppercase;
  background-color: var(--primary-tint); color: var(--primary-soft);
  border: 1px solid var(--primary-line);
}
.chip.muted { background-color: var(--ctl); color: var(--muted); border-color: var(--ctl-border); }

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-xs);
  font-size: 12px; font-weight: 550;
  background-color: var(--ctl); color: var(--muted);
  border: 1px solid var(--ctl-border);
  white-space: nowrap;
}
.badge .dotmark { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge.active { color: var(--primary-soft); background-color: var(--primary-tint); border-color: var(--primary-line); }
.badge.expired { color: var(--danger); background-color: rgba(248, 113, 113, .08); border-color: rgba(248, 113, 113, .24); }
.badge.unused { color: var(--info); background-color: rgba(96, 165, 250, .08); border-color: rgba(96, 165, 250, .24); }
.badge.revoked { color: var(--text-2); }
.badge.active .dotmark { animation: pulse 2.2s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.plat {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; font-weight: 600; letter-spacing: .06em;
  color: var(--text-2);
  padding: 4px 10px;
  border-radius: var(--radius-xs);
  background-color: var(--ctl); border: 1px solid var(--ctl-border);
}
.plat svg { color: var(--primary-soft); }

.mono { font-family: var(--mono); font-size: 13.5px; letter-spacing: -.01em; }
.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.hint { font-size: 13.5px; color: var(--muted); margin-top: 16px; line-height: 1.65; }
.section-note { font-size: 14px; color: var(--muted); margin-bottom: 28px; max-width: 62ch; line-height: 1.7; }
.block-title { font-size: 12px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin: 36px 0 16px; }
.divider { height: 1px; background: var(--border); margin: 28px 0; }

.spinner {
  width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid rgba(139, 92, 246, .22); border-top-color: var(--primary);
  animation: spin .7s linear infinite;
}

/* ============ forms ============ */
.field { display: block; margin-bottom: 22px; }
.field-label { display: block; margin-bottom: 9px; font-size: 13.5px; font-weight: 500; color: var(--text-2); }
/* Shows the key shape the server will actually generate. Monospace so the character count
   is legible, muted so it reads as secondary to the input. */
.field-hint {
  display: block; margin-top: 8px;
  font-family: var(--mono);
  font-size: 12px; line-height: 1.5; color: var(--muted);
  letter-spacing: -.01em;
}
.field-control {
  display: flex; align-items: center; gap: 10px;
  min-height: 48px; padding: 0 16px;
  background-color: var(--input-bg);
  border: 1px solid var(--ctl-border);
  border-radius: var(--radius-sm);
  transition: border-color var(--t), box-shadow var(--t), background-color var(--t);
}
.field-control:hover { border-color: #43434a; }
.field-control:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(139, 92, 246, .14); background-color: #15151a; }
.field-icon { color: var(--muted); display: flex; transition: color var(--t); }
.field-control:focus-within .field-icon { color: var(--primary-soft); }
.field-control input, .field-control textarea {
  flex: 1; min-width: 0; background: none; border: none; outline: none; font-size: 15px;
}
.field-control:not(.field-textarea) input { height: 46px; }
.field-control input::placeholder, .field-control textarea::placeholder { color: #6b6b74; }
.field-control.field-textarea { height: auto; padding: 13px 16px; align-items: flex-start; }
.field-control textarea { resize: vertical; line-height: 1.65; }
.field-eye {
  color: var(--muted); display: grid; place-items: center;
  width: 34px; height: 34px; margin-right: -10px; border-radius: var(--radius-xs);
  transition: color var(--t), background-color var(--t);
}
.field-eye:hover { color: var(--text); background-color: var(--ctl-hover); }

.select { position: relative; display: inline-flex; align-items: center; }
.select select {
  appearance: none; -webkit-appearance: none;
  height: 42px; padding: 0 34px 0 14px;
  background-color: var(--ctl); border: 1px solid var(--ctl-border);
  border-radius: var(--radius-sm);
  font-size: 14px; font-weight: 500; color: var(--text-2); cursor: pointer;
  transition: border-color var(--t), color var(--t), background-color var(--t);
}
.select select:hover { border-color: #43434a; background-color: var(--ctl-hover); color: var(--text); }
.select > span { position: absolute; right: 12px; color: var(--muted); pointer-events: none; display: flex; }

.search { position: relative; flex: 1; min-width: 220px; max-width: 460px; }
.search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--muted); display: flex; transition: color var(--t); z-index: 1; }
.search:focus-within .search-icon { color: var(--primary-soft); }
.search input {
  width: 100%; height: 42px; padding: 0 14px 0 38px;
  background-color: var(--input-bg); border: 1px solid var(--ctl-border); border-radius: var(--radius-sm);
  font-size: 14px; outline: none;
  transition: border-color var(--t), box-shadow var(--t), background-color var(--t);
}
.search input::placeholder { color: #6b6b74; }
.search input:hover { border-color: #43434a; }
.search input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(139, 92, 246, .14); background-color: #15151a; }
.search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.switch-row {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  padding: 18px 20px; margin-bottom: 24px;
  background-color: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
}
.switch-title { display: block; font-size: 15px; font-weight: 550; }
.switch-desc { display: block; font-size: 13px; color: var(--muted); margin-top: 3px; }
.switch {
  position: relative; flex: none; width: 46px; height: 26px;
  border-radius: 999px; background-color: #303036; border: 1px solid var(--ctl-border);
  transition: background-color .22s var(--ease), border-color .22s var(--ease);
}
.switch-knob {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: #7a7a83;
  transition: transform .3s var(--spring), background-color .22s var(--ease);
}
.switch[aria-checked="true"] { background-color: rgba(124, 58, 237, .45); border-color: var(--primary-line); }
.switch[aria-checked="true"] .switch-knob { transform: translateX(20px); background: #ede9fe; }

.segmented { display: inline-flex; gap: 10px; flex-wrap: wrap; }
.seg {
  padding: 10px 18px; border-radius: var(--radius-sm);
  border: 1px solid var(--ctl-border); background-color: var(--ctl);
  font-size: 14px; font-weight: 500; color: var(--text-2);
  transition: background-color var(--t), color var(--t), border-color var(--t), transform .15s var(--ease);
}
.seg:hover { color: var(--text); background-color: var(--ctl-hover); border-color: #43434a; }
.seg:active { transform: scale(.97); }
.seg.is-active { background-color: var(--primary-tint); border-color: var(--primary-line); color: var(--primary-soft); }

.form-actions { display: flex; gap: 10px; align-items: center; margin-top: 8px; flex-wrap: wrap; }

/* ============ gate ============ */
.gate { position: relative; z-index: 1; min-height: 100vh; min-height: 100dvh; display: grid; place-items: center; padding: 32px; }
.gate-card {
  width: 100%; max-width: 440px; min-width: 0; padding: 48px 40px 34px;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
  text-align: center;
  animation: card-in .45s var(--ease);
}
@keyframes card-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.gate-logo { display: flex; justify-content: center; margin-bottom: 24px; }
.gate-logo .mark { width: 60px; height: 60px; border-radius: 16px; color: var(--primary-soft); background-color: var(--primary-tint); border-color: var(--primary-line); }
.gate-title { font-size: 26px; font-weight: 700; letter-spacing: .08em; color: var(--text); }
.gate-sub { font-size: 14px; color: var(--muted); margin: 6px 0 34px; }
.gate-form { text-align: left; }
.gate-error {
  display: flex; align-items: center; gap: 8px;
  font-size: 13.5px; color: var(--danger);
  background-color: rgba(248, 113, 113, .08); border: 1px solid rgba(248, 113, 113, .24);
  border-radius: var(--radius-sm); padding: 11px 14px; margin: -8px 0 18px;
  animation: card-in .25s var(--ease);
}
.gate-foot {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 28px; padding-top: 22px; border-top: 1px solid var(--border);
  font-size: 13px; color: var(--muted);
}
.gate.shake .gate-card { animation: shake .42s var(--ease); }
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  30%, 70% { transform: translateX(5px); }
  50% { transform: translateX(-6px); }
}

/* ============ topbar ============ */
.app { position: relative; z-index: 1; min-height: 100vh; min-height: 100dvh; }
.topbar {
  position: sticky; top: 0; z-index: 120;
  background-color: rgba(14, 14, 16, .92);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.topbar-inner {
  max-width: var(--container); margin: 0 auto; height: var(--topbar-h); padding: 0 var(--pad);
  display: flex; align-items: center; gap: 18px;
}
.brand { display: flex; align-items: center; gap: 12px; flex: none; }
.brand .mark { width: 36px; height: 36px; }
.brand-text { display: flex; flex-direction: column; line-height: 1.3; }
.brand-name { font-size: 15.5px; font-weight: 650; letter-spacing: .08em; white-space: nowrap; }
.brand-accent { color: var(--primary-soft); }
.brand-sub { font-size: 11px; color: var(--muted); letter-spacing: .05em; white-space: nowrap; }

.topbar-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; flex: none; }
.tb-meta { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 500; color: var(--text-2); }
.tb-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background-color: var(--success);
  box-shadow: 0 0 0 3px rgba(74, 222, 128, .13);
  animation: pulse 2.6s ease-in-out infinite;
}
.status-pill.off { color: var(--danger); }
.status-pill.off .tb-dot { background-color: var(--danger); box-shadow: 0 0 0 3px rgba(248, 113, 113, .13); animation: none; }
.tb-sep { width: 1px; height: 22px; background-color: var(--border); flex: none; }
.clock {
  font-family: var(--mono); font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.user-wrap { position: relative; display: inline-flex; }
.user-block {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 4px 11px 4px 4px;
  border-radius: 8px;
  background-color: var(--panel-2); border: 1px solid var(--border);
  transition: border-color var(--t), background-color var(--t);
}
.user-block:hover, .user-block[aria-expanded="true"] { border-color: #3a3a40; background-color: var(--panel-hover); }
.user-avatar {
  width: 27px; height: 27px; border-radius: 7px;
  display: grid; place-items: center;
  background-color: var(--primary-tint); color: var(--primary-soft);
  font-size: 12.5px; font-weight: 700;
  transition: background-color var(--t), color var(--t);
}
.user-name { font-size: 13px; font-weight: 550; color: var(--text-2); }
.user-caret { display: flex; color: var(--muted); transition: transform .22s var(--ease); }
.user-block[aria-expanded="true"] .user-caret { transform: rotate(180deg); }
.icon-btn.ghost { background-color: transparent; border-color: transparent; color: var(--muted); }
.icon-btn.ghost:hover { background-color: var(--ctl); border-color: var(--ctl-border); color: var(--text); }

/* platform switch — centred */
.platform-row {
  max-width: var(--container); margin: 0 auto;
  padding: 0 var(--pad) 16px;
  display: flex; justify-content: center;
}
.platform-tabs {
  position: relative; display: inline-flex; align-items: center; gap: 5px;
  padding: 5px;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  max-width: 100%;
  overflow-x: auto; scrollbar-width: none;
}
.platform-tabs::-webkit-scrollbar { display: none; }
.tab-glider {
  position: absolute; top: 0; left: 0; width: 0; height: 0;
  background-color: var(--primary-strong);
  border-radius: 7px;
  transition: transform .34s var(--ease), width .34s var(--ease), height .34s var(--ease);
  pointer-events: none; z-index: 0;
}
.ptab {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  height: 42px; padding: 0 24px; border-radius: 7px;
  font-size: 14px; font-weight: 600; letter-spacing: .05em;
  color: var(--muted); white-space: nowrap;
  transition: color var(--t), background-color var(--t), transform .15s var(--ease);
}
.ptab:hover { color: var(--text-2); background-color: rgba(255, 255, 255, .04); }
.ptab:active { transform: scale(.98); }
.ptab.is-active { color: #fff; }
.ptab.is-active svg { color: #fff; }
.ptab .ptab-count {
  font-size: 12px; font-weight: 600; padding: 1px 6px; border-radius: var(--radius-xs);
  background-color: var(--ctl); color: var(--muted);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--t), color var(--t);
}
.ptab.is-active .ptab-count { background-color: rgba(255, 255, 255, .2); color: #fff; }

/* ============ shell ============ */
/* 96px of bottom padding left a gap under the last panel that read as a layout mistake - the
   operator's words were «дохуя мест снизу». 44px is enough to clear the fade at the bottom of the
   scroll area without looking empty; the bottom-nav variant below still gets its own, larger value
   because there the bar really is there. */
.shell { max-width: var(--container); margin: 0 auto; padding: 40px var(--pad) 44px; }
.shell .subtabs, .shell .panel, .shell .stat, .shell .stats { scroll-margin-top: calc(var(--topbar-h) + 130px); }

/* section tabs — boxed segmented control, same language as the platform switch */
.subtabs {
  position: relative; display: flex; gap: 4px; align-items: center;
  padding: 5px;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  margin-bottom: 28px;
  width: fit-content; max-width: 100%;
  overflow-x: auto; scrollbar-width: none;
}
.subtabs::-webkit-scrollbar { display: none; }
.subtab-glider {
  position: absolute; top: 0; left: 0; width: 0; height: 0;
  background-color: var(--primary-strong);
  border-radius: 7px;
  transition: transform .3s var(--ease), width .3s var(--ease), height .3s var(--ease);
  pointer-events: none;
}
.subtab {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: 9px;
  height: 38px; padding: 0 16px;
  border-radius: 7px;
  font-size: 14px; font-weight: 550; color: var(--muted); white-space: nowrap;
  transition: color var(--t), background-color var(--t), transform .15s var(--ease);
}
.subtab:hover { color: var(--text-2); background-color: rgba(255, 255, 255, .04); }
.subtab:active { transform: scale(.98); }
.subtab.is-active { color: #fff; }
.subtab.is-active svg { color: #fff; }
.subtab-count {
  font-size: 12px; font-weight: 600; padding: 1px 7px; height: 20px;
  display: inline-flex; align-items: center; border-radius: var(--radius-xs);
  background-color: var(--ctl); color: var(--muted); border: 1px solid var(--ctl-border);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--t), color var(--t), border-color var(--t);
}
.subtab.is-active .subtab-count { background-color: rgba(255, 255, 255, .2); color: #fff; border-color: transparent; }

.view { display: none; }
.view.is-active { display: block; }
.view.is-active > * { animation: view-in .4s var(--ease) both; }
.view.is-active > *:nth-child(2) { animation-delay: .07s; }
.view.is-active > *:nth-child(3) { animation-delay: .14s; }
@keyframes view-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* When the scroll reveal is live it owns the entrance, and this animation has to stand down. Not
   because the two look alike - they do not - but because `animation: ... both` holds its end state
   forever, and a running animation beats a declaration in the cascade. With view-in still applied,
   every direct child of an active view was pinned at opacity: 1 no matter what the reveal rule said,
   and the measured result was a panel that was blurred but fully opaque: filter survived (view-in
   does not animate it), opacity did not. Same trap the backdrop already documents for .bg-base, which
   is switched off with display:none rather than opacity:0 for exactly this reason.

   The stagger those nth-child delays provided is not lost: the reveal carries its own via --d, set on
   the top stat cards below and per-child here, so a view still arrives as a sequence rather than as
   one slab. */
html.reveal-on body:not([data-motion="off"]) .view.is-active > * { animation: none; }
html.reveal-on body:not([data-motion="off"]) .view.is-active > .panel:not(.revealed):nth-child(2),
html.reveal-on body:not([data-motion="off"]) .view.is-active > .stat:not(.revealed):nth-child(2) { --d: .07s; }
html.reveal-on body:not([data-motion="off"]) .view.is-active > .panel:not(.revealed):nth-child(3),
html.reveal-on body:not([data-motion="off"]) .view.is-active > .stat:not(.revealed):nth-child(3) { --d: .14s; }

/* ============ stats: plain plut .info-box, no icon chips ============ */
.stats { display: grid; gap: 20px; margin-bottom: 36px; }
.stats-top { grid-template-columns: repeat(4, 1fr); }
.stats-detail { grid-template-columns: repeat(4, 1fr); margin-bottom: 20px; }
.stats-sm { grid-template-columns: repeat(4, 1fr); }

.stat {
  position: relative;
  padding: 26px 26px 24px;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: background-color var(--t), border-color var(--t), transform var(--t);
  animation: stat-in .5s var(--ease) both;
}
.stat:nth-child(2) { animation-delay: .06s; }
.stat:nth-child(3) { animation-delay: .12s; }
.stat:nth-child(4) { animation-delay: .18s; }
.stat:nth-child(5) { animation-delay: .24s; }
.stat:nth-child(6) { animation-delay: .3s; }
.stat:nth-child(7) { animation-delay: .36s; }
.stat:nth-child(8) { animation-delay: .42s; }
@keyframes stat-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.stat:hover { background-color: var(--panel-2); border-color: #3a3a40; transform: translateY(-2px); }

.stat-main { min-width: 0; }
.stat-label {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; font-weight: 600; color: var(--muted);
  letter-spacing: .1em; text-transform: uppercase;
  margin-bottom: 12px;
}
.stat-ico-mini {
  display: grid; place-items: center;
  width: 26px; height: 26px; flex: none;
  border-radius: 7px;
  color: var(--primary-soft);
  background-color: var(--primary-tint);
  transition: transform .28s var(--spring), color var(--t), background-color var(--t);
}
.stat:hover .stat-ico-mini { transform: scale(1.08) rotate(-5deg); }
.stat-value {
  font-size: 40px; font-weight: 650; letter-spacing: -.035em; line-height: 1.05;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.stat-value.accent { color: var(--primary-soft); }
.stat-foot { font-size: 13.5px; color: var(--muted); margin-top: 10px; line-height: 1.55; }
.stat-foot b { color: var(--text-2); font-weight: 600; }

/* conversion bars */
.bars { display: flex; flex-direction: column; gap: 18px; }
.bar-row { display: grid; grid-template-columns: 140px 1fr 150px; align-items: center; gap: 18px; }
.bar-label { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 550; color: var(--text-2); letter-spacing: .04em; }
.bar-label svg { color: var(--primary-soft); }
.bar-track { height: 8px; background-color: var(--input-bg); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.bar-fill { height: 100%; width: 0; background-color: var(--primary-strong); border-radius: 999px; transition: width .85s var(--ease); }
.bar-meta { font-size: 13.5px; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.bar-meta b { color: var(--text); font-weight: 600; }

/* ============ analytics ============ */

.an-head { margin-bottom: 24px; }
.an-head .panel-head { border-bottom: none; padding: 20px 24px; }
.an-controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
.an-range-label {
  font-size: 11px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.an-updated { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.seg-sm { gap: 4px; }
.seg-sm .seg { padding: 7px 13px; font-size: 12.5px; }

/* metric toggles above the chart */
.an-metrics { display: flex; gap: 6px; flex-wrap: wrap; }
.an-metric {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: 999px;
  border: 1px solid var(--border); background-color: var(--ctl);
  color: var(--muted); font-size: 12.5px; font-weight: 550;
  font-family: var(--font); cursor: pointer;
  transition: color var(--t), border-color var(--t), background-color var(--t), transform .18s var(--spring);
}
.an-metric:hover { color: var(--text-2); border-color: var(--ctl-border); transform: translateY(-1px); }
.an-metric .dot { width: 8px; height: 8px; border-radius: 50%; background-color: currentColor; opacity: .45; flex: none; }
.an-metric[aria-pressed="true"] { color: var(--text); border-color: var(--primary-line); background-color: var(--primary-tint); }
.an-metric[aria-pressed="true"] .dot { opacity: 1; }
.an-metric[data-metric="created"][aria-pressed="true"] { color: var(--primary-soft); }
.an-metric[data-metric="activated"] .dot { color: var(--success); }
.an-metric[data-metric="expired"] .dot { color: var(--danger); }
.an-metric[data-metric="logs"] .dot { color: var(--info); }
.an-metric[data-metric="bans"] .dot { color: var(--warn); }

/* chart */
.an-chart { position: relative; width: 100%; height: 268px; }
.an-chart svg { display: block; width: 100%; height: 100%; overflow: visible; }
.an-grid-line { stroke: var(--border-soft); stroke-width: 1; }
.an-axis { fill: var(--muted); font-size: 10.5px; font-family: var(--mono); }
.an-axis-y { text-anchor: end; }
.an-axis-x { text-anchor: middle; }
.an-axis-x.weekend { fill: var(--text-2); }
.an-bar { transition: opacity var(--t); }
.an-bar:hover { opacity: .78; }
.an-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.an-area { stroke: none; }
.an-dot { stroke: var(--panel); stroke-width: 2; }
.an-crosshair { stroke: var(--primary-line); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0; transition: opacity .12s var(--ease); }
.an-chart.is-hover .an-crosshair { opacity: 1; }
.an-hit { fill: transparent; cursor: crosshair; }

.an-tip {
  position: absolute; z-index: 5; pointer-events: none;
  min-width: 168px; padding: 11px 13px;
  background-color: var(--panel-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--sh-2);
  opacity: 0; transform: translate(-50%, 8px) scale(.97);
  transition: opacity .13s var(--ease), transform .13s var(--ease);
}
.an-tip.on { opacity: 1; transform: translate(-50%, 8px) scale(1); }
.an-tip-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 12px; color: var(--muted); margin-bottom: 8px;
  padding-bottom: 7px; border-bottom: 1px solid var(--border-soft);
}
.an-tip-head b { color: var(--text); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.an-tip-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-2); padding: 2.5px 0; }
.an-tip-row .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.an-tip-row span:last-child { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text); font-weight: 550; }

.an-legend { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 16px; }
.an-legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); }
.an-legend-item .swatch { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.an-legend-item .swatch.line { height: 3px; border-radius: 999px; width: 16px; }

.an-summary { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
.an-pill {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 9px 14px; border-radius: var(--radius-sm);
  background-color: var(--panel-2); border: 1px solid var(--border);
  font-size: 12.5px; color: var(--muted);
}
.an-pill b { font-size: 15px; font-weight: 620; color: var(--text); font-variant-numeric: tabular-nums; }
.an-pill .trend { font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.an-pill .trend.up { color: var(--success); }
.an-pill .trend.down { color: var(--danger); }
.an-pill .trend.flat { color: var(--muted); }

/* two-column layout for the analytics panels. The gap alone separates rows: .panel also carries a
   32px bottom margin, and the two used to add up to 64px of nothing between «Воронка» and
   «Активность по часам». */
.an-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: start; }
.an-grid .panel { margin-bottom: 0; }
/* «Источники ключей» was removed, which left «Платформы» alone in its row: without this it would
   sit at half width next to an empty cell. */
.an-grid > .panel:only-child { grid-column: 1 / -1; }

/* analytics tables are denser than the data tables */
.an-table-wrap { border: 1px solid var(--border); border-radius: var(--radius-sm); }
/* Both analytics tables shed columns as the window narrows rather than scrolling sideways. The daily
   table always did: the operator asked for the horizontal slider under «Таблица по дням» to go. The
   platform table used to keep a 640px floor instead, on the theory that eight numeric columns genuinely
   do not fit a phone - true, but the measurement of what that costs was worse than the theory: 640px
   inside a 314px wrapper is two screens of sideways dragging to read three rows, and «reachable by
   scrolling» is not the same as usable. So it now sheds too (rules below): at 640px «Ожидают» and
   «Отозвано» go, at 480px «Истекли» and «Баны», leaving the four that answer "does this platform
   sell". The floor is lifted at exactly the widths where the columns were dropped - keeping it while
   shedding columns would have the table both narrower in content and wider in layout than its wrapper.
   Everything dropped is still on the desktop, and «Истекли»/«Отозвано»/«Баны» are all in the funnel
   and platform totals above. */
#an-platform-table { min-width: 640px; }
@media (max-width: 640px) { #an-platform-table { min-width: 0; } }
#an-daily-table { width: 100%; }
#an-daily-table thead th { padding: 10px 9px; font-size: 10.5px; letter-spacing: .05em; }
#an-daily-table tbody td { padding: 9px; font-size: 13px; }
#an-daily-table .c-day { padding-left: 12px; }
#an-daily-table .c-spark { width: 92px; padding-right: 12px; }
#an-daily-table .an-day-cell { gap: 6px; }
/* narrowest useful column first: the sparkline, then the two rarest events, then the raw event
   count. Дата / Создано / Активаций / Прирост / Активных survive to phone width. */
@media (max-width: 1420px) { #an-daily-table .c-spark { display: none; } }
@media (max-width: 1180px) { #an-daily-table .c-errors, #an-daily-table .c-bans { display: none; } }
@media (max-width: 980px) { #an-daily-table .c-logs, #an-daily-table .c-expired { display: none; } }
@media (max-width: 720px) { #an-daily-table .c-net { display: none; } }
/* Shedding columns is not enough on a phone, and the measurement says why. At 390px four columns are
   left, and the table still came out 399px wide inside a 314px wrapper: `.c-num` carries
   `width: 1%` with `white-space: nowrap` (the usual "shrink to content" trick), while `.c-day` is
   nowrap AND holds an uppercase «сегодня» tag - so the day column took 276px and each number was
   crushed to 24px. The wrapper scrolled, which is exactly the slider the operator asked to have
   removed, and the numbers were unreadable anyway.

   Fixed layout with explicit shares settles it: the day column gets 34% and each number 22%, so the
   table is the width of its wrapper by construction rather than by negotiation between two nowrap
   cells. The day cell is allowed to wrap and the tag shrinks, because «06.10 / сегодня» is two facts
   that stack happily where they cannot sit side by side. */
@media (max-width: 480px) {
  #an-daily-table { table-layout: fixed; }
  #an-daily-table .c-day { width: 34%; white-space: normal; padding: 8px 6px 8px 10px; }
  #an-daily-table .c-num { width: 22%; white-space: normal; padding: 8px 6px; }
  #an-daily-table thead th { padding: 8px 6px; font-size: 9.5px; letter-spacing: .03em; }
  #an-daily-table .an-day-cell { flex-wrap: wrap; gap: 1px 6px; }
  #an-daily-table .an-tag { font-size: 9px; padding: 0 4px; letter-spacing: .02em; }
}
/* «Платформы» sheds columns the same way «Таблица по дням» does, for the same reason: it was left as a
   horizontally scrolling table on the theory that eight numeric columns genuinely do not fit a phone.
   They do not - but the measurement says what that costs: the table came out 1223px wide inside a
   314px wrapper, which is three screens of sideways dragging to read three rows, on a panel whose
   operator asked for the horizontal slider under the daily table to be removed. Reachable is not the
   same as usable.

   Order of shedding is least actionable first, and everything dropped is still on the desktop: at
   640px «Ожидают» and «Отозвано» go, at 480px «Истекли» and «Баны» go too, leaving Платформа /
   Всего / Активны / Конверсия - the four that answer "does this platform sell". Targeted by class, not
   by nth-child, so adding a column later cannot silently hide a different one. */
@media (max-width: 640px) {
  #an-platform-table .p-unused, #an-platform-table .p-revoked { display: none; }
}
@media (max-width: 480px) {
  #an-platform-table .p-expired, #an-platform-table .p-banned { display: none; }
  #an-platform-table { table-layout: fixed; }
  #an-platform-table thead th, #an-platform-table tbody td { padding: 9px 6px; font-size: 12px; }
  #an-platform-table thead th { font-size: 9.5px; letter-spacing: .03em; }
  #an-platform-table .c-conv { width: auto; }
}
.an-table thead th { padding: 11px 14px; font-size: 11px; letter-spacing: .08em; }
.an-table tbody td { padding: 11px 14px; font-size: 13.5px; }
.an-table tbody tr { animation: none; }
.an-table tbody tr:hover { background-color: var(--panel-2); }
.an-table .c-num { width: 1%; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.an-table thead th.c-num { text-align: right; }
.an-table thead th[data-sort] { cursor: pointer; user-select: none; transition: color var(--t); }
.an-table thead th[data-sort]:hover { color: var(--text-2); }
.an-table thead th.is-sorted { color: var(--primary-soft); }
.an-table .sort-arrow { font-size: 10px; margin-left: 3px; }
.an-sub { color: var(--muted); font-size: 12px; }
.an-table .c-day { white-space: nowrap; }
.an-table .c-spark { width: 128px; }
.an-table .c-conv { width: 148px; }
.an-day-cell { display: flex; align-items: baseline; gap: 8px; }
.an-day-cell .d { font-weight: 550; color: var(--text); font-variant-numeric: tabular-nums; }
.an-day-cell .w { font-size: 11.5px; color: var(--muted); text-transform: lowercase; }
.an-table tr.is-today { background-color: var(--primary-tint); }
.an-table tr.is-today:hover { background-color: var(--primary-tint); }
.an-table tr.is-today .an-day-cell .d { color: var(--primary-soft); }
.an-table tr.is-weekend .an-day-cell .w { color: var(--text-2); }
.an-tag {
  display: inline-block; padding: 1px 6px; border-radius: var(--radius-xs);
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  background-color: var(--primary-tint); color: var(--primary-soft);
}

.delta { font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.delta.up { color: var(--success); }
.delta.down { color: var(--danger); }
.delta.flat { color: var(--muted); }
.mini-spark { display: flex; align-items: flex-end; gap: 2px; height: 22px; }
.mini-spark i {
  flex: 1 1 auto; min-width: 3px; border-radius: 1px;
  background-color: var(--primary-line);
  transition: height .5s var(--ease), background-color var(--t);
}
.mini-spark i.zero { background-color: var(--border); }
.mini-spark i.peak { background-color: var(--primary-soft); }

/* funnel */
.an-funnel { display: flex; flex-direction: column; gap: 13px; }
.an-funnel-row { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; align-items: center; }
.an-funnel-label { font-size: 13px; color: var(--text-2); font-weight: 500; }
.an-funnel-value { font-size: 13.5px; font-variant-numeric: tabular-nums; color: var(--text); font-weight: 600; }
.an-funnel-value small { color: var(--muted); font-weight: 500; font-size: 12px; margin-left: 2px; }
.an-funnel-value .v { color: var(--text); }
.an-funnel-track {
  grid-column: 1 / -1; height: 26px; position: relative;
  background-color: var(--input-bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); overflow: hidden;
}
.an-funnel-fill {
  position: absolute; inset: 0 auto 0 0; width: 0;
  background-color: var(--primary-tint);
  border-right: 2px solid var(--primary-strong);
  transition: width .8s var(--ease);
}
.an-funnel-row:nth-child(2) .an-funnel-fill { background-color: rgba(74, 222, 128, .13); border-right-color: var(--success); }
.an-funnel-row:nth-child(3) .an-funnel-fill { background-color: rgba(96, 165, 250, .13); border-right-color: var(--info); }
.an-funnel-row:nth-child(4) .an-funnel-fill { background-color: rgba(251, 191, 36, .13); border-right-color: var(--warn); }
.an-funnel-row:nth-child(5) .an-funnel-fill { background-color: rgba(248, 113, 113, .13); border-right-color: var(--danger); }
.an-funnel-row:nth-child(6) .an-funnel-fill { background-color: rgba(141, 141, 150, .13); border-right-color: var(--muted); }

/* hour-of-day heatmap */
/* «Активность по часам»: 24 столбца в треках фиксированной высоты.
   The track has a real height (188px) and the fill inside it is sized as a percentage, so the chart
   encodes value by length. The previous version sized each cell with `aspect-ratio: 1/1.35` and
   `min-height: 30px`, which at 24 columns across a half-width panel meant min-height always won and
   the whole chart was a 27px strip in a 172px panel standing next to a 428px neighbour - the empty
   space the operator complained about was missing content, not extra padding.

   262px is chosen against that neighbour, and it was arrived at by measuring rather than by eye: the
   first version of this chart put the panel at 325px beside a 428px funnel card, which still left a
   103px hole in the row. Head + body padding is ~96px and the axis and note take ~60px, so 262 lands
   the panel within a few pixels of its neighbour and the row stops having a gap in it. */
.an-hours { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: 4px; height: 262px; }
.an-hour {
  position: relative;
  display: flex; align-items: flex-end;
  border-radius: var(--radius-xs); border: 1px solid var(--border);
  background-color: var(--input-bg);
  overflow: hidden;
  transition: transform .18s var(--spring), border-color var(--t);
}
.an-hour:hover { transform: translateY(-2px); border-color: var(--primary-line); }
.an-hour.peak { border-color: var(--primary-line); box-shadow: 0 0 0 1px var(--primary-line) inset; }
/* The fill grows from zero on the frame after it is built, so a row of bars arrives as one motion
   rather than appearing already drawn. Staggered by index in CSS rather than by a timer in JS: the
   chart is re-rendered on every refresh, and 24 setTimeouts per refresh is 24 things to cancel. */
.an-hour-fill {
  display: block; width: 100%; height: 0;
  border-radius: 2px 2px 0 0;
  background-image: linear-gradient(to top, var(--primary), var(--primary-soft));
  transition: height .7s var(--ease);
  transition-delay: calc(var(--i, 0) * 22ms);
}
.an-hours.grown .an-hour-fill { height: var(--h, 0%); }
.an-hour-fill.zero { height: 3px !important; background-image: none; background-color: var(--border); }
.an-hours-axis { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: 4px; margin-top: 7px; }

/* «По дням недели»: seven columns, each a track with a fill plus three lines of text under it.
   The track is 186px so the panel lands next to its neighbour at a similar height - the same problem
   «Активность по часам» had, solved the same way, by giving the chart a real height instead of
   letting it collapse to whatever its content happened to need. Numbers and percentages sit under the
   bar rather than inside it because a 7-column chart has no room for text in the column itself. */
.an-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 10px; }
.an-week-col { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.an-week-track {
  position: relative; width: 100%; height: 186px;
  display: flex; align-items: flex-end;
  border-radius: var(--radius-xs); border: 1px solid var(--border);
  background-color: var(--input-bg);
  overflow: hidden;
  transition: transform .18s var(--spring), border-color var(--t);
}
.an-week-col:hover .an-week-track { transform: translateY(-2px); border-color: var(--primary-line); }
.an-week-col.peak .an-week-track { border-color: var(--primary-line); box-shadow: 0 0 0 1px var(--primary-line) inset; }
.an-week-col.weekend .an-week-label { color: var(--text-2); font-weight: 620; }
.an-week-fill {
  display: block; width: 100%; height: 0;
  border-radius: 2px 2px 0 0;
  background-image: linear-gradient(to top, var(--primary), var(--primary-soft));
  transition: height .7s var(--ease);
  transition-delay: calc(var(--i, 0) * 45ms);
}
.an-week.grown .an-week-fill { height: var(--h, 0%); }
.an-week-fill.zero { height: 3px !important; background-image: none; background-color: var(--border); }
.an-week-label { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.an-week-num { font-size: 17px; font-weight: 680; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.1; }
.an-week-conv { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.an-hours-axis span {
  font-size: 9.5px; color: var(--muted); text-align: center;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.an-note { margin-top: 14px; font-size: 12.5px; color: var(--muted); line-height: 1.6; }
.an-note b { color: var(--text-2); font-weight: 600; }

/* horizontal distribution bars */
.an-bars { display: flex; flex-direction: column; gap: 19px; }
.an-bar-row { display: grid; grid-template-columns: 84px 1fr 116px; gap: 12px; align-items: center; }
.an-bar-name { font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums; font-weight: 550; }
.an-bar-track { height: 10px; background-color: var(--input-bg); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.an-bar-fill {
  position: relative;
  height: 100%; width: 0; background-color: var(--primary-line); border-radius: 999px;
  transition: width .8s var(--ease);
}
.an-bar-fill.soft { background-color: rgba(var(--acc-soft-rgb), .18); }
/* The alive share inside the bar. It is a child of the fill and sized as a percentage of it, so it
   always reads as "this part of that term is still running" no matter how short the term's own bar
   is. Rounded on the left only: the right end is a cut through the middle of the outer bar, and
   rounding it there would suggest the bar ended. */
.an-bar-live {
  display: block; height: 100%; width: 0;
  background-image: linear-gradient(90deg, var(--primary), var(--primary-strong));
  border-radius: 999px 0 0 999px;
  box-shadow: 0 0 12px -4px rgba(var(--acc-rgb), 1);
  transition: width .8s var(--ease) .12s;
}
.an-bar-meta { font-size: 12px; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.an-bar-meta b { color: var(--text); font-weight: 600; }
/* What the two widths mean, stated where the chart is rather than in a tooltip: an unfamiliar
   stacked bar reads as decoration until somebody says which part is which. */
.an-bars-legend {
  display: flex; align-items: center; gap: 16px;
  margin-top: 16px; padding-top: 12px;
  border-top: 1px solid var(--border-soft);
  font-size: 11.5px; color: var(--muted);
}
.an-bars-legend i {
  display: inline-block; width: 16px; height: 7px; margin-right: 6px;
  border-radius: 999px; vertical-align: -1px;
}
.an-bars-legend i.total { background-color: var(--primary-line); }
.an-bars-legend i.live { background-image: linear-gradient(90deg, var(--primary), var(--primary-strong)); }

/* engagement */
.an-engagement { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.an-eng {
  padding: 15px 16px; border-radius: var(--radius-sm);
  background-color: var(--panel-2); border: 1px solid var(--border);
  transition: border-color var(--t), transform var(--t);
}
.an-eng:hover { border-color: var(--ctl-border); transform: translateY(-1px); }
.an-eng-label { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 7px; }
.an-eng-value { font-size: 26px; font-weight: 640; letter-spacing: -.03em; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.1; }
.an-eng-value.accent { color: var(--primary-soft); }
.an-eng-value.warn { color: var(--warn); }
.an-eng-sub { font-size: 12px; color: var(--muted); margin-top: 5px; }

/* The «expiry forecast» cards and the key-source dot went with their panels. */
.conv-cell { display: flex; align-items: center; gap: 10px; }
.conv-cell .track { flex: 1 1 auto; height: 6px; background-color: var(--input-bg); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.conv-cell .fill { height: 100%; width: 0; border-radius: 999px; background-color: var(--primary-strong); transition: width .8s var(--ease); }
.conv-cell .fill.good { background-color: var(--success); }
.conv-cell .fill.mid { background-color: var(--warn); }
.conv-cell .fill.bad { background-color: var(--danger); }
.conv-cell .val { font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums; min-width: 46px; text-align: right; }

.plat-cell { display: inline-flex; align-items: center; gap: 8px; font-weight: 550; }
.plat-cell svg { color: var(--primary-soft); }

@media (max-width: 1400px) {
  .an-grid { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .an-engagement { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .an-chart { height: 220px; }
  .an-controls { justify-content: flex-start; }
  .an-head .panel-head { flex-direction: column; align-items: flex-start; }
  .an-bar-row { grid-template-columns: 70px 1fr 96px; }
}
@media (max-width: 620px) {
  .an-engagement { grid-template-columns: minmax(0, 1fr); }
  .an-hours { gap: 2px; }
  /* aspect-ratio + min-height would force a ~16px minimum cell width, and 24 of those
     do not fit a phone — drop the ratio so the columns shrink with the grid */
  .an-hour { aspect-ratio: auto; height: 30px; min-height: 0; }
  .an-hours-axis { gap: 2px; }
  .an-hours-axis span { font-size: 8.5px; }
  /* every 3rd hour is labelled in JS; on a phone keep only 0/6/12/18 */
  .an-hours-axis span:not(:nth-child(6n+1)) { visibility: hidden; }
  .an-chart { height: 190px; }
  .an-summary { gap: 8px; }
}

/* ============ panel & table ============ */
.panel {
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 32px;
}
.panel-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 24px 28px; border-bottom: 1px solid var(--border);
}
.panel-title { display: flex; align-items: center; gap: 11px; font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.panel-title svg { color: var(--primary-soft); }
.panel-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.panel-body { padding: 28px; }
/* Analytics panels hold dense small stuff - a 24-cell heatmap, a funnel, a table of numbers - and at
   the data panels' 28px of air the «Активность по часам» card was more padding than content. Same
   reason the block title above «Таблица по дням» loses a third of its top margin here. */
#view-stats .panel-head { padding: 18px 22px; }
#view-stats .panel-body { padding: 20px 22px; }
#view-stats .panel { margin-bottom: 20px; }
#view-stats .block-title { margin: 24px 0 12px; }
#view-stats .an-note { margin-top: 12px; }
.panel-toolbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 20px 28px; border-bottom: 1px solid var(--border);
  background-color: rgba(0, 0, 0, .18);
}
.selects { display: flex; gap: 10px; margin-left: auto; flex-wrap: wrap; }

.table-wrap { position: relative; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table { width: 100%; border-collapse: collapse; }
.table thead th {
  text-align: left; padding: 15px 28px;
  font-size: 12px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap;
}
.table tbody td {
  padding: 17px 28px; border-bottom: 1px solid var(--border-soft);
  vertical-align: middle; font-size: 14.5px;
}
.table tbody tr { transition: background-color .18s var(--ease); animation: row-in .35s var(--ease) both; }
.table tbody tr:nth-child(1) { animation-delay: .02s; }
.table tbody tr:nth-child(2) { animation-delay: .04s; }
.table tbody tr:nth-child(3) { animation-delay: .06s; }
.table tbody tr:nth-child(4) { animation-delay: .08s; }
.table tbody tr:nth-child(5) { animation-delay: .1s; }
.table tbody tr:nth-child(6) { animation-delay: .12s; }
.table tbody tr:nth-child(7) { animation-delay: .14s; }
.table tbody tr:nth-child(8) { animation-delay: .16s; }
.table tbody tr:nth-child(n+9) { animation-delay: .18s; }
@keyframes row-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.table tbody tr:hover { background-color: var(--panel-2); }
.table tbody tr:last-child td { border-bottom: none; }

.c-key { min-width: 220px; }
.c-status { width: 160px; }
.c-exp { width: 200px; }
.c-hwid { width: 190px; }
.c-note { min-width: 160px; }
.c-act { width: 1%; text-align: right; white-space: nowrap; }
.c-time { width: 180px; }
.c-level { width: 110px; }
.c-plat { width: 150px; }
.c-ip { width: 200px; }

.keycell { display: flex; align-items: center; gap: 10px; }
.keycell .mono { color: var(--text); font-size: 14px; }
.copy-btn {
  /* `flex: none` is the whole fix for a button that measured 8px wide on a phone. It declares 30x30,
     but it sits in a flex row next to the key itself, and flex items shrink by default - so in a 79px
     cell the mono key took the space and the button was squeezed to a sliver. Unhittable with a mouse
     and impossible with a thumb. */
  flex: none;
  display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-xs);
  color: var(--muted); border: 1px solid transparent;
  transition: color var(--t), background-color var(--t), border-color var(--t), transform .2s var(--spring);
}
.copy-btn:hover { color: var(--primary-soft); background-color: var(--ctl-hover); border-color: var(--ctl-border); }
.copy-btn.done { color: var(--success); transform: scale(1.12); }

.expcell { display: flex; flex-direction: column; gap: 2px; }
.expcell .left { font-size: 14px; font-variant-numeric: tabular-nums; font-weight: 550; }
.expcell .left.warn { color: var(--warn); }
.expcell .left.bad { color: var(--danger); }
.expcell .left.ok { color: var(--success); }
.expcell .date { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

.row-actions { display: flex; gap: 6px; justify-content: flex-end; }

.lv {
  display: inline-flex; align-items: center;
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  padding: 3px 9px; border-radius: var(--radius-xs);
  background-color: var(--ctl); color: var(--muted); border: 1px solid var(--ctl-border);
}
.lv.info { color: var(--info); border-color: rgba(96, 165, 250, .24); background-color: rgba(96, 165, 250, .08); }
.lv.success { color: var(--success); border-color: rgba(74, 222, 128, .22); background-color: rgba(74, 222, 128, .07); }
.lv.warn { color: var(--warn); border-color: rgba(251, 191, 36, .24); background-color: rgba(251, 191, 36, .08); }
.lv.error { color: var(--danger); border-color: rgba(248, 113, 113, .24); background-color: rgba(248, 113, 113, .08); }
.lv.auth { color: var(--primary-soft); border-color: var(--primary-line); background-color: var(--primary-tint); }

.msg-cell { max-width: 560px; }
.msg-text { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.actor-line { display: block; font-size: 12px; color: var(--muted); font-family: var(--mono); margin-top: 2px; }

.table-empty {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 80px 24px; color: var(--muted); font-size: 15px;
  animation: view-in .3s var(--ease);
}
.table-empty svg { color: #3a3a40; }
.table-loading {
  position: absolute; inset: 0; z-index: 5;
  display: grid; place-items: center;
  background-color: rgba(14, 14, 16, .55); backdrop-filter: blur(2px);
}

.panel-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 18px 28px; border-top: 1px solid var(--border); flex-wrap: wrap;
}
.foot-info { font-size: 13.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.pager { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pager .pg {
  min-width: 36px; height: 36px; padding: 0 11px;
  display: inline-grid; place-items: center;
  border-radius: var(--radius-sm); border: 1px solid var(--ctl-border);
  background-color: var(--ctl); color: var(--text-2);
  font-size: 14px; font-weight: 500; font-variant-numeric: tabular-nums;
  transition: background-color var(--t), color var(--t), border-color var(--t), transform .15s var(--ease);
}
.pager .pg:hover:not([disabled]) { background-color: var(--ctl-hover); color: var(--text); border-color: #43434a; }
.pager .pg:active:not([disabled]) { transform: scale(.95); }
.pager .pg.is-active { background-color: var(--primary-tint); border-color: var(--primary-line); color: var(--primary-soft); }
.pager .pg[disabled] { opacity: .35; pointer-events: none; }
.pager .dots { color: var(--muted); padding: 0 8px; font-size: 13px; }

/* ============ logs: live terminal feed ============ */
.live {
  display: inline-flex; align-items: center; gap: 7px;
  height: 31px; padding: 0 12px; border-radius: 999px;
  background-color: rgba(74, 222, 128, .08); border: 1px solid rgba(74, 222, 128, .24);
  color: var(--success);
  font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  transition: background-color var(--t), border-color var(--t), color var(--t);
}
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: pulse 1.7s ease-in-out infinite; }
.live.paused { background-color: rgba(251, 191, 36, .08); border-color: rgba(251, 191, 36, .26); color: var(--warn); }
.live.paused .live-dot { animation: none; }

.log-vol { padding: 16px 20px 14px; border-bottom: 1px solid var(--border); background-color: rgba(0, 0, 0, .16); }
.log-vol-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.log-vol-title { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
.log-vol-title svg { color: var(--primary-soft); }
.log-vol-sum { margin-left: auto; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.log-vol-sum b { color: var(--text); font-weight: 650; }
.log-vol-bars { display: flex; align-items: flex-end; gap: 3px; height: 56px; }
.log-vol-bars i {
  flex: 1; min-width: 2px; height: 2px;
  border-radius: 2px 2px 0 0;
  background-color: var(--primary-tint);
  transition: height .7s var(--ease), background-color .2s var(--ease), transform .2s var(--ease);
  transform-origin: bottom;
}
.log-vol-bars i:hover { background-color: var(--primary); transform: scaleY(1.06); }
.log-vol-bars i.peak { background-color: var(--primary-strong); }
.log-vol-bars i.now { background-color: var(--primary-soft); }

.log-toolbar { gap: 12px; }
.lv-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.lv-chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 11px; border-radius: 999px;
  border: 1px solid var(--ctl-border); background-color: var(--ctl);
  color: var(--text-2);
  font-family: var(--mono); font-size: 11.5px; font-weight: 500;
  transition: background-color var(--t), border-color var(--t), color var(--t), transform .15s var(--ease);
}
.lv-chip:hover { background-color: var(--ctl-hover); color: var(--text); transform: translateY(-1px); }
.lv-chip:active { transform: scale(.96); }
.lv-chip .cnt { font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.lv-chip.is-active { color: var(--text); border-color: var(--primary-line); background-color: var(--primary-tint); }
.lv-chip.is-active .cnt { color: inherit; opacity: .75; }
.lv-chip[data-level="info"].is-active { color: var(--info); border-color: rgba(96, 165, 250, .4); background-color: rgba(96, 165, 250, .12); }
.lv-chip[data-level="success"].is-active { color: var(--success); border-color: rgba(74, 222, 128, .4); background-color: rgba(74, 222, 128, .12); }
.lv-chip[data-level="warn"].is-active { color: var(--warn); border-color: rgba(251, 191, 36, .4); background-color: rgba(251, 191, 36, .12); }
.lv-chip[data-level="error"].is-active { color: var(--danger); border-color: rgba(248, 113, 113, .4); background-color: rgba(248, 113, 113, .12); }
.lv-chip[data-level="auth"].is-active { color: var(--primary-soft); }
.log-search { max-width: 320px; }

.term-wrap { position: relative; background-color: #0a0a0d; }
.term-scan {
  position: absolute; left: 0; right: 0; top: 0; height: 46px;
  background: linear-gradient(to bottom, var(--primary-tint), transparent);
  opacity: 0; pointer-events: none; z-index: 3;
}
.term-scan.run { animation: term-scan 1.15s var(--ease); }
@keyframes term-scan {
  0% { opacity: .85; transform: translate3d(0, -46px, 0); }
  100% { opacity: 0; transform: translate3d(0, 520px, 0); }
}
.term-head {
  display: grid;
  grid-template-columns: 84px 86px 112px minmax(0, 1fr) 168px 32px;
  gap: 12px;
  padding: 9px 20px;
  border-bottom: 1px solid var(--border);
  font-size: 10.5px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.term-head span:last-child { text-align: right; }
.term {
  max-height: 540px; overflow-y: auto; overflow-x: hidden;
  padding: 4px 0 8px;
  font-family: var(--mono);
  scrollbar-width: thin;
}
.trow {
  display: grid;
  grid-template-columns: 84px 86px 112px minmax(0, 1fr) 168px 32px;
  gap: 12px; align-items: baseline;
  padding: 7px 20px;
  border-left: 2px solid transparent;
  font-size: 12.5px;
  transition: background-color .16s var(--ease), border-color .16s var(--ease);
  animation: trow-in .34s var(--ease) both;
}
@keyframes trow-in { from { opacity: 0; transform: translate3d(-10px, 0, 0); } to { opacity: 1; transform: none; } }
.trow.fresh { animation: trow-fresh .9s var(--ease); }
@keyframes trow-fresh {
  0% { opacity: 0; transform: translate3d(-18px, 0, 0); background-color: var(--primary-tint); }
  35% { opacity: 1; transform: none; background-color: var(--primary-tint); }
  100% { opacity: 1; transform: none; background-color: transparent; }
}
.trow:hover { background-color: rgba(255, 255, 255, .035); border-left-color: var(--primary-line); }
.t-time { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.t-level {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}
.t-level::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.trow[data-level="info"] .t-level { color: var(--info); }
.trow[data-level="success"] .t-level { color: var(--success); }
.trow[data-level="warn"] .t-level { color: var(--warn); }
.trow[data-level="error"] .t-level { color: var(--danger); }
.trow[data-level="auth"] .t-level { color: var(--primary-soft); }
.t-plat { display: flex; align-items: center; }
.t-msg { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trow[data-level="error"] .t-msg { color: #fecaca; }
.trow[data-level="warn"] .t-msg { color: #fde68a; }
.t-src { color: var(--muted); font-size: 11.5px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-actor { color: #6f6f78; }
.t-copy {
  display: grid; place-items: center; width: 26px; height: 24px;
  border-radius: var(--radius-xs); color: #55555e;
  opacity: 0; transition: opacity .16s var(--ease), color var(--t), background-color var(--t);
}
.trow:hover .t-copy { opacity: 1; }
.t-copy:hover { color: var(--primary-soft); background-color: var(--ctl); }
.t-copy.done { color: var(--success); opacity: 1; }
.term-more { display: none; }

/* ============ links ============ */
.links-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.link-card {
  min-width: 0;
  padding: 26px;
  background-color: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
  transition: background-color var(--t), border-color var(--t), transform var(--t);
  animation: stat-in .5s var(--ease) both;
}
.link-card:nth-child(2) { animation-delay: .06s; }
.link-card:nth-child(3) { animation-delay: .12s; }
.link-card:nth-child(4) { animation-delay: .18s; }
.link-card:nth-child(5) { animation-delay: .24s; }
.link-card:nth-child(6) { animation-delay: .3s; }
.link-card:hover { background-color: var(--panel-hover); border-color: var(--primary-line); transform: translateY(-2px); }
.link-name { display: flex; align-items: center; gap: 10px; font-size: 16px; font-weight: 600; margin-bottom: 10px; }
.link-name svg { color: var(--primary-soft); }
.link-url {
  min-width: 0; max-width: 100%;
  font-family: var(--mono); font-size: 12.5px; color: var(--primary-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 10px;
}
.link-desc { font-size: 14px; color: var(--text-2); margin-bottom: 20px; min-height: 22px; line-height: 1.6; }
.link-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.link-actions .btn { flex: 1 1 auto; min-width: 0; }
.link-actions .btn span { overflow: hidden; text-overflow: ellipsis; }

/* ============ layout helpers ============ */
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; align-items: start; }
.grid-2 .panel { margin-bottom: 0; }

/* ============ modal ============ */
.modal-root {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center; padding: 24px;
  background-color: rgba(8, 8, 10, .88);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  animation: overlay-in .25s var(--ease);
}
@keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } }
.modal-root[hidden] { display: none; }
.modal {
  width: 100%; max-width: 520px;
  background-color: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 30px 70px rgba(0, 0, 0, .8);
  overflow: hidden;
  animation: modal-in .25s var(--ease);
  max-height: calc(100dvh - 48px); display: flex; flex-direction: column;
}
@keyframes modal-in { from { opacity: 0; transform: scale(.95); } to { opacity: 1; transform: scale(1); } }
.modal.wide { max-width: 660px; }
.modal-head { display: flex; align-items: center; gap: 12px; padding: 24px 28px; border-bottom: 1px solid var(--border); }
.modal-title { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.modal-sub { font-size: 13px; color: var(--muted); font-family: var(--mono); margin-top: 2px; }
.modal-close { margin-left: auto; }
.modal-body { padding: 28px; overflow-y: auto; }
.modal-foot { display: flex; gap: 10px; justify-content: flex-end; padding: 20px 28px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.durations { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 24px; }
.dur {
  padding: 10px 18px; border-radius: var(--radius-sm);
  border: 1px solid var(--ctl-border); background-color: var(--ctl); color: var(--text-2);
  font-size: 14px; font-weight: 500;
  transition: background-color var(--t), border-color var(--t), color var(--t), transform .15s var(--ease);
}
.dur:hover { color: var(--text); background-color: var(--ctl-hover); border-color: #43434a; }
.dur:active { transform: scale(.97); }
.dur.is-active { background-color: var(--primary-tint); border-color: var(--primary-line); color: var(--primary-soft); }

.result-list { display: flex; flex-direction: column; gap: 8px; max-height: 320px; overflow-y: auto; margin-bottom: 18px; }
.result-item {
  display: flex; align-items: center; gap: 11px; padding: 12px 14px;
  background-color: var(--input-bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  animation: row-in .3s var(--ease) both;
}
.result-item .mono { flex: 1; font-size: 14px; }

/* ============ toasts ============ */
.toasts {
  position: fixed; z-index: 300; right: 24px; bottom: 24px;
  display: flex; flex-direction: column; gap: 10px; pointer-events: none;
  max-width: min(400px, calc(100vw - 40px));
}
.toast {
  position: relative; overflow: hidden;
  display: flex; align-items: flex-start; gap: 12px;
  padding: 16px 18px 18px;
  background-color: var(--panel-2); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: var(--sh-2);
  font-size: 14.5px; pointer-events: auto;
  animation: toast-in .3s var(--spring);
}
.toast::after {
  content: ''; position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
  background-color: var(--primary); transform-origin: left;
  animation: toast-bar 3.4s linear forwards;
}
.toast.out { animation: toast-out .22s var(--ease) forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateX(26px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateX(26px) scale(.97); } }
@keyframes toast-bar { from { transform: scaleX(1); } to { transform: scaleX(0); } }
.toast .t-ico { margin-top: 2px; color: var(--primary-soft); }
.toast.success .t-ico { color: var(--success); }
.toast.success::after { background-color: var(--success); }
.toast.error { border-color: rgba(248, 113, 113, .3); }
.toast.error .t-ico { color: var(--danger); }
.toast.error::after { background-color: var(--danger); animation-duration: 5.2s; }
.toast.warn .t-ico { color: var(--warn); }
.toast.warn::after { background-color: var(--warn); }
.toast-body { flex: 1; min-width: 0; }
.toast-title { font-weight: 600; font-size: 14.5px; }
.toast-text { color: var(--text-2); font-size: 13px; margin-top: 3px; word-break: break-word; }

/* ============ settings popover ============ */
.pop {
  position: absolute; top: calc(100% + 12px); right: 0; z-index: 140;
  width: 620px; max-width: calc(100vw - 32px);
  background-color: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .9);
  overflow: hidden;
  animation: pop-in .22s var(--spring);
  transform-origin: top right;
}
.pop[hidden] { display: none; }
@keyframes pop-in { from { opacity: 0; transform: translateY(-8px) scale(.97); } to { opacity: 1; transform: none; } }
.pop-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.pop-title { font-size: 14.5px; font-weight: 600; }
.pop-head .icon-btn { margin-left: auto; }
.pop-body { padding: 18px 16px; max-height: min(64vh, 540px); overflow-y: auto; }
.pop-sec { margin-bottom: 22px; }
.pop-sec:last-child { margin-bottom: 0; }
.pop-label {
  display: block; margin-bottom: 11px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.pop-row { margin-bottom: 10px; }
.pop-foot {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 16px; border-top: 1px solid var(--border);
  background-color: rgba(0, 0, 0, .18);
}
.pop-foot .btn { margin-left: auto; }
.pop-saved { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--muted); transition: color var(--t); }
.pop-saved.on { color: var(--success); }
.pop-note { font-size: 12.5px; color: var(--muted); margin-top: 10px; line-height: 1.55; }

/* background effect tiles with live preview */
.fx-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.fx-tile {
  position: relative; min-width: 0; padding: 0; overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--ctl-border); background-color: var(--ctl);
  text-align: left;
  transition: border-color var(--t), background-color var(--t), transform .16s var(--ease), box-shadow var(--t);
}
.fx-tile:hover { border-color: #4a4a52; background-color: var(--ctl-hover); transform: translateY(-2px); }
.fx-tile:active { transform: scale(.98); }
.fx-tile.is-active { border-color: var(--primary-line); box-shadow: 0 0 0 1px var(--primary-line) inset; }
.fx-prev {
  position: relative; display: block; height: 78px; overflow: hidden;
  background-color: #0b0b0e;
  container-type: size;
}
/* each preview tile shows the same backdrop recipe as the real background */
.fx-tile[data-fx="aurora"] .fx-prev { background-color: var(--bg); }
.fx-tile[data-fx="rain"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 60% 70% at 26% 0%, rgba(var(--acc-pale-rgb), .12), transparent 70%),
    radial-gradient(ellipse 56% 80% at 74% 0%, rgba(var(--acc-pale-rgb), .1), transparent 72%),
    linear-gradient(to top, var(--fx-mist, rgba(200, 225, 255, .14)), transparent 48%);
}
.fx-prev::after {
  content: ''; position: absolute; inset: 0;
  background: radial-gradient(ellipse 120% 100% at 50% 100%, rgba(8, 8, 10, .5), transparent 60%);
  pointer-events: none;
}
/* mini atmosphere for the aurora preview tile */
.fx-tile:not([data-fx="aurora"]) .fx-glow { display: none; }
.fx-glow {
  position: absolute; border-radius: 50%; pointer-events: none;
  will-change: transform;
}
.fx-glow.g1 {
  top: -60%; left: -20%; width: 90%; height: 130%; opacity: .55;
  background: radial-gradient(circle at 50% 50%, var(--fx-tint, rgba(167, 139, 250, .55)), transparent 68%);
  animation: tile-drift-1 12s ease-in-out infinite alternate;
}
.fx-glow.g2 {
  bottom: -70%; right: -25%; width: 85%; height: 130%; opacity: .35;
  background: radial-gradient(circle at 50% 50%, var(--primary, #8b5cf6), transparent 70%);
  animation: tile-drift-2 16s ease-in-out infinite alternate;
}
@keyframes tile-drift-1 { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(18%, 12%, 0) scale(1.12); } }
@keyframes tile-drift-2 { from { transform: translate3d(0, 0, 0) scale(1.06); } to { transform: translate3d(-16%, -10%, 0) scale(.94); } }

.fx-name {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 10px;
  font-size: 12.5px; font-weight: 550; color: var(--text-2);
  border-top: 1px solid var(--border);
}
.fx-tile.is-active .fx-name { color: var(--text); }
.fx-check { margin-left: auto; display: none; color: var(--primary-soft); }
.fx-tile.is-active .fx-check { display: flex; }

/* ---- seasonal mode tiles ------------------------------------------------
   Deliberately NOT built like the background tiles: a mode is four options with a sentence of
   explanation each, and a 78px live preview of "a garland" at thumbnail size would be a smudge. So
   each tile is a swatch (its own accent, drawn as the three lights of the garland / the ember glow /
   the neon line) plus the name and what it does. Two columns, because four tiles in one row would
   make the hints unreadable and in three rows would waste the window. */
.mode-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.mode-tile {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 12px;
  border-radius: var(--r-in);
  border: 1px solid var(--ctl-border); background-color: var(--ctl);
  text-align: left;
  transition: border-color var(--t), background-color var(--t), transform .16s var(--ease), box-shadow var(--t);
}
.mode-tile:hover { border-color: #4a4a52; background-color: var(--ctl-hover); transform: translateY(-2px); }
.mode-tile:active { transform: scale(.98); }
.mode-tile.is-active {
  border-color: var(--primary-line);
  box-shadow: 0 0 0 1px var(--primary-line) inset, 0 10px 26px -18px rgba(var(--acc-rgb), 1);
}
.mode-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mode-text b { font-size: 13px; font-weight: 620; color: var(--text-2); }
.mode-tile.is-active .mode-text b { color: var(--text); }
.mode-text span { font-size: 11.5px; line-height: 1.45; color: var(--muted); }
.mode-swatch {
  position: relative; flex: none;
  width: 34px; height: 34px; border-radius: 10px;
  background-color: #0b0b0e;
  border: 1px solid rgba(255, 255, 255, .09);
  overflow: hidden;
}
/* normal: the accent itself, nothing on top of it */
.mode-swatch.mode-normal { background-image: radial-gradient(circle at 50% 62%, #7c5cf0, transparent 72%); }
/* halloween: an ember glow at the bottom and fog over it */
.mode-swatch.mode-halloween {
  background-image:
    radial-gradient(ellipse 90% 60% at 50% 116%, #fb923c, transparent 72%),
    radial-gradient(ellipse 70% 40% at 30% 92%, rgba(255, 190, 130, .35), transparent 70%);
}
/* newyear: three garland bulbs on a wire */
.mode-swatch.mode-newyear {
  background-image:
    linear-gradient(to right, transparent 6%, rgba(255, 255, 255, .3) 12%, rgba(255, 255, 255, .3) 88%, transparent 94%),
    radial-gradient(circle at 26% 40%, #ef4444 0 3px, transparent 4px),
    radial-gradient(circle at 50% 40%, #8fe3c0 0 3px, transparent 4px),
    radial-gradient(circle at 74% 40%, #ffd08a 0 3px, transparent 4px);
  background-size: 100% 2px, 100% 100%, 100% 100%, 100% 100%;
  background-position: 0 11px, 0 0, 0 0, 0 0;
  background-repeat: no-repeat;
}
/* cyber: scanlines with a neon floor */
.mode-swatch.mode-cyber {
  background-image:
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, .5) 2px 4px),
    linear-gradient(to top, #22d3ee, transparent 62%);
}

/* The nav-position wireframe tiles (.nav-grid/.nav-tile/.nav-prev/.np-*) went with the picker: the
   sections are on top and there is nothing left to choose. */

/* ============ nav position: left rail (full-height sidebar) ============ */
body[data-nav="left"] { --rail-w: 248px; }

body[data-nav="left"] .subtabs {
  position: fixed; z-index: 70;
  top: var(--topbar-h); left: 0; bottom: 0;
  width: var(--rail-w);
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  padding: 16px 14px 20px;
  margin: 0;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-left: none; border-bottom: none;
  border-radius: 0;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: thin;
}

/* the platform switch moves into the rail (JS relocates the node) */
body[data-nav="left"] .platform-row { height: 0; padding: 0; margin: 0; overflow: visible; }
body[data-nav="left"] .rail-slot { display: block; width: 100%; margin-bottom: 14px; padding-bottom: 16px; border-bottom: 1px solid var(--border); }
body[data-nav="left"] .platform-tabs {
  width: 100%; flex-direction: column; align-items: stretch;
  gap: 4px; padding: 5px;
  background-color: var(--panel-2);
  overflow: visible;
}
body[data-nav="left"] .ptab {
  width: 100%; height: 38px; padding: 0 12px;
  justify-content: flex-start; font-size: 13px;
}
body[data-nav="left"] .ptab .ptab-count { margin-left: auto; }
body[data-nav="left"] .tab-glider { border-radius: 7px; }

body[data-nav="left"] .subtab {
  width: 100%; height: 42px; justify-content: flex-start;
  padding: 0 12px; border-radius: 7px; font-size: 14px;
}
body[data-nav="left"] .subtab-count { margin-left: auto; }
body[data-nav="left"] .subtab-glider { border-radius: 7px; }

/* content shifts right of the rail */
body[data-nav="left"] .topbar-inner,
body[data-nav="left"] .shell {
  max-width: none;
  margin-left: var(--rail-w);
  margin-right: 0;
  padding-left: 28px;
  padding-right: 28px;
}
body[data-nav="left"] .shell { padding-top: 28px; }
body[data-nav="left"] .modal-root { padding-left: calc(var(--rail-w) + 24px); }
body[data-nav="left"] .toasts { right: 24px; }

/* ============ nav position: bottom bar (iOS style) ============ */
body[data-nav="bottom"] .shell { padding-bottom: 118px; }
body[data-nav="bottom"] .subtabs {
  position: fixed; z-index: 90;
  left: 50%; bottom: 0;
  transform: translateX(-50%);
  width: min(780px, 100%);
  margin: 0;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
  display: flex; gap: 4px;
  background-color: rgba(23, 23, 26, .95);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--border); border-bottom: none;
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -12px 34px -18px rgba(0, 0, 0, .9);
  overflow: visible;
}
body[data-nav="bottom"] .subtab {
  position: relative; flex: 1;
  flex-direction: column; gap: 4px; justify-content: center;
  height: 54px; padding: 6px 2px;
  border-radius: 10px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .01em;
}
body[data-nav="bottom"] .subtab svg { width: 20px; height: 20px; }
body[data-nav="bottom"] .subtab-count {
  position: absolute; top: 4px; right: 10px;
  height: 16px; padding: 0 4px; font-size: 10px;
}
body[data-nav="bottom"] .subtab-glider { border-radius: 10px; }
body[data-nav="bottom"] .toasts { bottom: 92px; }
body[data-nav="bottom"] .modal-root { padding-bottom: 86px; }

@media (max-width: 900px) {
  body[data-nav="bottom"] .shell { padding-bottom: 104px; }
  body[data-nav="bottom"] .subtabs { width: 100%; border-radius: 14px 14px 0 0; }
  body[data-nav="bottom"] .subtab { height: 52px; font-size: 10px; }
  body[data-nav="bottom"] .subtab svg { width: 19px; height: 19px; }
  body[data-nav="bottom"] .subtab-count { right: 6px; }
  body[data-nav="bottom"] .toasts { bottom: 78px; }
}

@media (max-width: 420px) {
  body[data-nav="bottom"] .subtab span:not(.subtab-count) { font-size: 9.5px; }
  body[data-nav="bottom"] .subtab-count { display: none; }
}

.accent-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.color-wrap {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 5px 12px 5px 5px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--ctl-border); background-color: var(--ctl);
  transition: border-color var(--t), background-color var(--t);
}
.color-wrap:hover { border-color: #43434a; background-color: var(--ctl-hover); }
/* The native colour input is the value holder and the fallback for a browser without the custom
   picker, but it must never be the thing the operator clicks: its dialog is the OS one. Out of the
   tab order (tabindex="-1" in the markup), out of the pointer's way, and 1px so it cannot be hit by
   accident - not `display: none`, because a display:none input still has to hold its value for
   syncSettingsUi() to read and write. */
.color-native {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; border: 0;
  opacity: 0;
  pointer-events: none;
}
/* The swatch is a real button, so it gets the panel's focus ring, hover and press for free. Its
   background is set from the current value in syncSettingsUi(). */
.color-swatch {
  width: 30px; height: 30px; flex: none;
  padding: 0;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, .18);
  background-color: var(--primary);
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 6px 16px -10px rgba(var(--acc-rgb), 1);
  transition: transform .16s var(--spring), border-color var(--t), box-shadow var(--t);
}
.color-swatch:hover { transform: scale(1.07); border-color: rgba(var(--acc-soft-rgb), .7); }
.color-swatch:active { transform: scale(.96); }
.color-swatch[aria-expanded="true"] {
  border-color: rgba(var(--acc-soft-rgb), .85);
  box-shadow: 0 0 0 2px rgba(var(--acc-rgb), .35), inset 0 1px 0 rgba(255, 255, 255, .22);
}
.color-hex { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
.accent-dots { display: flex; gap: 7px; flex-wrap: wrap; }
.adot {
  width: 24px; height: 24px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .16);
  transition: transform .18s var(--spring), box-shadow .18s var(--ease);
}
.adot:hover { transform: scale(1.16); }
.adot.is-active { box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--primary-soft); }

.seg-full { display: flex; width: 100%; }
.seg-full .seg { flex: 1; justify-content: center; }

/* silent refresh: no re-animation on background updates */
.stats.no-anim .stat,
.table-wrap.no-anim .table tbody tr,
.links-grid.no-anim .link-card,
.bars.no-anim .bar-fill { animation: none !important; }

/* ============ responsive ============ */
@media (max-width: 1280px) {
  :root { --pad: 24px; }
  .stats-detail { grid-template-columns: repeat(2, 1fr); }
  .grid-2 { grid-template-columns: 1fr; gap: 24px; }
  .stat-value { font-size: 36px; }
}

@media (max-width: 1080px) {
  .stats-top, .stats-sm { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 900px) {
  /* blur on a sticky bar is the main scroll-jank source on phones — drop it, keep the bar opaque */
  .topbar { backdrop-filter: none; -webkit-backdrop-filter: none; background-color: rgba(14, 14, 16, .97); }
  .topbar-inner { padding: 0 20px; gap: 10px; height: 60px; }
  .brand { gap: 10px; }
  .brand .mark { width: 32px; height: 32px; }
  .brand-name { font-size: 14.5px; letter-spacing: .06em; }
  .brand-sub, .clock, .tb-sep, .user-name { display: none; }
  .user-block { padding: 4px; }
  .topbar-actions { gap: 8px; }
  .platform-row { padding: 0 20px 14px; }
  .shell { padding: 28px 20px 72px; }
  .panel-head, .panel-toolbar, .panel-body, .panel-foot { padding-left: 22px; padding-right: 22px; }
  .table thead th, .table tbody td { padding-left: 22px; padding-right: 22px; }
  .c-note, .c-hwid { display: none; }
  td.c-note, td.c-hwid { display: none; }
  .msg-cell { max-width: 42vw; }
  .bar-row { grid-template-columns: 110px 1fr 120px; gap: 12px; }
  .term-head { display: none; }
  .trow { grid-template-columns: 74px 82px minmax(0, 1fr) 30px; gap: 8px; padding: 8px 16px; }
  .trow .t-plat { display: none; }
  .t-src { grid-column: 2 / 4; text-align: left; }
  .log-search { max-width: none; }
}

@media (max-width: 720px) {
  :root { --pad: 16px; }
  .stats-top, .stats-sm, .stats-detail { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .stats { margin-bottom: 24px; }
  .stat { padding: 18px; }
  .stat-value { font-size: 30px; }
  .stat-label { font-size: 11px; margin-bottom: 8px; }
  .stat-foot { font-size: 12.5px; margin-top: 6px; }

  .panel { margin-bottom: 24px; }
  .panel-head { flex-direction: column; align-items: stretch; gap: 14px; padding: 18px 18px; }
  .panel-title { font-size: 16px; }
  .panel-actions { width: 100%; }
  .panel-actions .btn { flex: 1; }
  .panel-body, .panel-toolbar, .panel-foot { padding: 16px 18px; }
  .subtabs { width: 100%; margin-bottom: 20px; padding: 4px; gap: 3px; }
  .subtab { flex: 1 0 auto; justify-content: center; height: 36px; padding: 0 12px; font-size: 13.5px; gap: 7px; }

  .search { max-width: none; }
  .selects { margin-left: 0; width: 100%; }
  .selects .select { flex: 1; }
  .selects .select select { width: 100%; }

  /* table -> cards */
  .table thead { display: none; }
  .table, .table tbody, .table tr, .table td { display: block; width: 100%; }
  /* Two columns inside the card, not one. Stacked, a key card came out 269px tall - a third of a phone
     screen per row, seven rows to scroll past before anything else is visible, which is what "на
     телефонах ключи плохо отображаются" actually was. Status and remaining time are both short and
     belong on one line; the key, the HWID and the actions span both columns because they are not.
     Measured after the change: 269px -> ~170px per card. */
  .table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* `dense` is what keeps this from being taller than the table it replaces. The cells arrive in DOM
       order key, status, hwid, remaining, actions; hwid spans both columns, so without dense packing
       it cannot sit beside status and jumps to a new row, leaving a hole - and «remaining» then starts
       yet another row. Measured: 449px per card, worse than the 269px of the stacked version. With
       dense, «remaining» back-fills the hole next to «status» and the card is four rows.
       Dense reorders the visual grid only; the DOM order, and therefore the reading order for a screen
       reader and the tab order, is untouched. */
    grid-auto-flow: row dense;
    gap: 0 14px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border);
  }
  .table tbody td {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    /* Wrapping, and min-width:0 on the value, because a label and a value that do not fit used to
       overflow the card instead of rearranging: the ban list's «Дата блокировки» + a full timestamp
       stuck 9px past a 360px screen, where the panel's `overflow: hidden` quietly clipped it. A cell
       that can wrap degrades to two lines; one that cannot degrades to lost text. */
    flex-wrap: wrap;
    padding: 5px 0; border: none; text-align: right; font-size: 14px;
    min-width: 0;
  }
  .table tbody td > * { min-width: 0; }
  .table tbody td::before {
    content: attr(data-label);
    font-size: 11.5px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
    color: var(--muted); text-align: left; flex: none;
  }
  .table tbody td.c-key, .table tbody td.c-msg { justify-content: flex-start; }
  .table tbody td.c-key::before, .table tbody td.c-msg::before { display: none; }
  /* The two cells that share a row stack their label above their value instead of sitting side by
     side. In a 157px half-column «Осталось» plus a value like «меньше минуты» does not fit, and with
     `flex-wrap: wrap` on the cell it wrapped by accident - which cost 30px per card and looked like a
     mistake rather than a layout. Stacked and left-aligned it is deliberate, predictable, and shorter:
     measured on the live panel, that row went 89px -> 58px and the card 324px -> 293px. */
  .table tbody td.c-status, .table tbody td.c-exp {
    flex-direction: column; align-items: flex-start; justify-content: flex-start;
    gap: 3px; text-align: left;
  }
  .table tbody td.c-status .expcell, .table tbody td.c-exp .expcell { align-items: flex-start; }
  .table tbody td.c-key {
    grid-column: 1 / -1;
    padding-bottom: 10px; border-bottom: 1px solid var(--border-soft); margin-bottom: 4px;
  }
  .table tbody td.c-act { grid-column: 1 / -1; justify-content: flex-end; padding-top: 10px; }
  .table tbody td.c-act::before { display: none; }
  .table tbody td.c-note, .table tbody td.c-hwid { display: flex; grid-column: 1 / -1; }
  /* The key itself is the one thing that must not be truncated on a phone: it is what gets copied and
     pasted into the client, and a half-visible key is worse than a taller card. `overflow-wrap:
     anywhere` rather than `word-break: break-all` - the latter breaks between any two characters and
     will split a key in the middle of a block, where `anywhere` only breaks when the word genuinely
     does not fit, so `APK-1234-5678` keeps its dashes as break points. */
  .table tbody td.c-key .mono { white-space: normal; overflow-wrap: anywhere; line-height: 1.5; }
  .table tbody td.c-hwid .mono { white-space: normal; overflow-wrap: anywhere; }
  .row-actions { justify-content: flex-end; }
  .expcell { align-items: flex-end; }
  .msg-cell { max-width: none; }
  .msg-text { white-space: normal; text-align: right; }
  .table-logs tbody td.c-msg { justify-content: flex-end; grid-column: 1 / -1; }
  .table-logs tbody td.c-msg .msg-text { text-align: right; }

  /* analytics tables opt out of the card transformation: ten numeric columns turned into
     stacked blocks get squeezed into unreadable slivers, so they stay a real table and
     scroll horizontally inside .an-table-wrap instead */
  .an-table { display: table; width: 100%; }
  .an-table thead { display: table-header-group; }
  .an-table tbody { display: table-row-group; }
  .an-table tbody tr { display: table-row; width: auto; padding: 0; border-bottom: none; }
  .an-table tbody td {
    display: table-cell; width: auto;
    padding: 11px 14px; border: none; border-bottom: 1px solid var(--border-soft);
    text-align: left; font-size: 13.5px; justify-content: flex-start;
  }
  .an-table tbody td::before { display: none; }
  .an-table tbody td.c-num { text-align: right; }
  .an-table tbody tr:last-child td { border-bottom: none; }

  .links-grid { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .link-card { padding: 20px; }
  .log-vol { padding: 14px 16px 12px; }
  .log-vol-bars { height: 42px; gap: 2px; }
  .lv-chips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
  .lv-chips::-webkit-scrollbar { display: none; }
  .lv-chip { flex: none; }
  .term { max-height: 62vh; }
  .trow { grid-template-columns: 68px minmax(0, 1fr) 28px; gap: 8px; padding: 9px 14px; }
  .trow .t-level { grid-column: 2; grid-row: 1; justify-self: end; }
  .t-msg { grid-column: 2; grid-row: 2; white-space: normal; }
  .t-src { grid-column: 2; grid-row: 3; }
  .t-copy { grid-column: 3; grid-row: 1 / span 3; align-self: center; opacity: 1; }
  .log-toolbar .selects { width: auto; }
  .bar-row { grid-template-columns: 96px 1fr; gap: 10px; }
  .bar-meta { grid-column: 2; text-align: left; }
  .form-actions .btn { flex: 1 1 auto; }

  .platform-tabs { width: 100%; }
  .ptab { flex: 1; padding: 0 8px; height: 40px; font-size: 13px; }
  .modal { max-width: none; }
  .modal-head { padding: 20px; }
  .modal-body { padding: 20px; }
  .modal-foot { padding: 16px 20px; }
  .modal-foot .btn { flex: 1; }
  .field-row { grid-template-columns: 1fr; }
  .gate { padding: 20px; }
  .gate-card { padding: 34px 22px 26px; }
  .gate-logo { margin-bottom: 18px; }
  .gate-logo .mark { width: 52px; height: 52px; border-radius: 14px; }
  .gate-title { font-size: 22px; letter-spacing: .06em; }
  .gate-sub { margin: 5px 0 26px; }
  .btn-block { padding: 13px 16px; }
  .toasts { right: 12px; bottom: 12px; left: auto; max-width: calc(100vw - 24px); }

  .pop {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0;
    width: auto; max-height: 86dvh;
    display: flex; flex-direction: column;
    border-radius: 16px 16px 0 0; border-bottom: none;
    transform-origin: bottom center;
    animation: sheet-up .28s var(--ease);
  }
  .pop-body { max-height: none; flex: 1; padding-bottom: 26px; }
  .fx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .fx-prev { height: 66px; }
}
@keyframes sheet-up { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } }

/* --- touch targets -------------------------------------------------------
   Keyed on `pointer: coarse` rather than on width, because the thing that makes a 30px button
   unhittable is a thumb and not a narrow window: a phone in landscape and a tablet are both coarse
   and both need the same floor, while a desktop window dragged narrow does not want its controls
   inflated. 40px is the floor used here - Apple asks for 44 and Material for 48dp, but the row of a
   key card carries five actions and 5x44 plus gaps does not fit in a 330px card, whereas 5x40 does
   with room to spare. Everything below measured under 40 on a 390px phone before this block:
   the row actions and the copy button at 30, the topbar buttons at 34, both tab rows at 34-38. */
@media (pointer: coarse) {
  .icon-btn, .icon-btn.sm { min-width: 40px; min-height: 40px; }
  .copy-btn { width: 40px; height: 40px; }
  /* The brand is an <a href>, so it is a target whether or not anybody means to tap it: on a phone it
     measured 129x30, sitting right where a thumb reaches for the top of the screen. Giving it the same
     floor costs nothing - the topbar inner row is 58px tall here, so a 40px link fits inside it. */
  .brand { min-height: 40px; display: flex; align-items: center; }
  .subtab { min-height: 42px; }
  .ptab { min-height: 42px; }
  .user-block { min-height: 42px; }
  .set-tab { min-height: 42px; }
  .seg { min-height: 40px; }
  .cat-chip, .lv-chip { min-height: 38px; }
  .switch { min-height: 30px; }
  /* Row actions get real gaps as well: five 40px targets touching each other is one target, because
     a thumb lands between two of them and hits whichever it pleases. */
  .row-actions { gap: 6px; flex-wrap: wrap; }
  /* Hover states do not exist on touch, and a rule that only fires on hover leaves the control with
     no feedback at all. `:active` is what a phone can actually show. */
  .icon-btn:active, .copy-btn:active, .subtab:active, .ptab:active {
    background-color: var(--ctl-hover);
    transform: scale(.94);
  }
}

@media (max-width: 420px) {
  .stats-top, .stats-sm, .stats-detail { grid-template-columns: 1fr; }
  .topbar-inner { padding: 0 14px; gap: 8px; height: 58px; }
  .brand .mark { width: 32px; height: 32px; }
  .brand-name { font-size: 14px; letter-spacing: .04em; }
  .icon-btn { width: 34px; height: 34px; }
  .tb-meta span:not(.tb-dot) { display: none; }
  .platform-row { padding: 0 14px 12px; }
  .platform-tabs { padding: 4px; gap: 3px; }
  .ptab { height: 38px; padding: 0 4px; font-size: 12px; gap: 6px; letter-spacing: .02em; }
  .ptab svg { width: 15px; height: 15px; }
  .ptab .ptab-count { display: none; }
  .shell { padding: 20px 14px 56px; }
  .subtab-count { display: none; }
  .subtab { height: 34px; padding: 0 9px; font-size: 12.5px; gap: 5px; }
  .subtab svg { width: 14px; height: 14px; }
  .panel-head, .panel-body, .panel-toolbar, .panel-foot { padding-left: 16px; padding-right: 16px; }
  .table tbody tr { padding: 16px; }
  .btn { padding: 11px 16px; font-size: 14px; }
}

/* Reduced motion. Everything is damped - the backdrop too - UNLESS the operator has explicitly
   chosen «Полные» in Настройки → Внешний вид.

   That carve-out is narrow and deliberate. The system preference is inherited, and in practice it
   comes from an OS switch nobody remembers touching (Windows «Эффекты анимации», which Chrome
   forwards as prefers-reduced-motion: reduce). With the old blanket rule the result was a panel
    whose background stood perfectly still no matter which of the eight effects was picked, and no
   setting inside the panel could change it - which reads as "the feature is broken". A choice made
   inside the panel, about decoration that sits behind the content and never carries information,
   outranks an inherited hint. Everything that DOES carry information, and every interface
   transition, stays damped no matter what is chosen here. */
@media (prefers-reduced-motion: reduce) {
  *:not(.bg):not(.bg *), *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  body:not([data-motion="full"]) .bg,
  body:not([data-motion="full"]) .bg * {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
  .bg-fx i, .bg-particles i { opacity: 0 !important; }
  body[data-motion="full"] .bg-fx i { opacity: var(--o, .6) !important; }
}

/* =========================================================================
   GHOSTLYNEX LAYER
   Everything below restyles the panels above to ghostlynex.fun: glass
   surfaces, pill controls, a sheen that sweeps the primary button, a ripple
   on press, spring-and-blur entrances, and a glow that follows the accent.

   It is appended rather than woven in for one reason: the 950 rules above are
   the layout, and this is the skin. Keeping them apart means the skin can be
   read, tuned or removed in one piece without hunting through the file - and
   every rule here wins by source order, not by out-specifying the layout.

   No colour in this layer is a literal violet. Everything is rgba(var(--acc-rgb), a)
   or one of the --acc-*-rgb triplets that theme.js derives from the accent the
   operator picked, so changing the accent really does change the whole panel -
   glows, rings, ripples, scrollbars, ribbons - instead of leaving the default
   purple baked into forty places.
   ========================================================================= */

/* --- ambience ---------------------------------------------------------- */
body {
  background-color: var(--bg);
  background-image:
    radial-gradient(1200px 620px at 12% -8%, rgba(var(--acc-rgb), .2), transparent 62%),
    radial-gradient(900px 520px at 92% 4%, rgba(var(--acc-deep-rgb), .16), transparent 60%),
    radial-gradient(760px 480px at 50% 108%, rgba(var(--acc-soft-rgb), .1), transparent 62%);
  background-attachment: fixed;
}

::selection { background: rgba(var(--acc-rgb), .38); color: #fff; }

/* Thin accent scrollbars: the reference site's are nearly invisible, and the
   default grey ones are the most obvious "not this design" tell on a page
   this dark. */
* { scrollbar-width: thin; scrollbar-color: rgba(var(--acc-soft-rgb), .28) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: rgba(var(--acc-soft-rgb), .22);
  border-radius: var(--pill);
  border: 2px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: rgba(var(--acc-soft-rgb), .42); background-clip: content-box; }

/* --- glass surfaces ---------------------------------------------------- */
.panel,
.stat,
.link-card,
.gate-card,
.term-wrap,
.an-chart {
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .055),
    inset 0 -1px 0 rgba(0, 0, 0, .28),
    var(--sh-2);
}

/* Sheets that float ABOVE the page - popover, dialog, toast - are near-opaque
   rather than translucent. They used to share --panel with the cards, and
   because .topbar already has a backdrop-filter, it forms a "backdrop root":
   a nested backdrop-filter can only sample inside it, so the popover's blur
   did nothing and the animated background showed straight through at 38%
   opacity, underneath the settings text. Raising the alpha fixes the legibility
   without giving up the glass look of the panels below. */
.pop, .modal, .toast {
  background-color: var(--sheet);
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .07),
    0 34px 80px -30px rgba(0, 0, 0, .95),
    0 0 0 1px rgba(var(--acc-rgb), .12);
}

.panel, .link-card, .term-wrap { border-radius: var(--r-card); }
.stat { border-radius: var(--r-in); }
.gate-card, .modal, .pop { border-radius: var(--r-card); }
.toast { border-radius: var(--r-in); }

/* A hairline that catches the light at the top edge - the cheapest way to make
   a flat translucent rectangle read as a sheet of glass. */
.panel::before,
.stat::before,
.link-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  border-radius: inherit;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .22), transparent);
  opacity: .7;
  pointer-events: none;
  z-index: 2;
}
.panel, .link-card { position: relative; }

.topbar {
  background-color: rgba(var(--bg-rgb, 7, 5, 11), .78);
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  border-bottom: 1px solid var(--border);
}

/* --- the header gives room back once you start reading -------------------
   The reference site narrows its nav when the hero button scrolls out of view. This panel has no hero,
   so the trigger is simply "not at the top", and what it buys is 16px of table: the header is sticky
   and permanently covers that strip, and on a panel whose whole job is rows of numbers, a header that
   shrinks out of the way is worth more than one that stays imposing.

   Only the inner height and the subtitle change. Nothing is hidden that carries state - the status
   pill, the clock and the account button all stay, and the platform row underneath is untouched, so
   compacting cannot take away a control. The height is transitioned rather than switched because a
   sticky header that snaps on every scroll near the threshold is the most visible kind of jitter.

   --topbar-h is redeclared here rather than on :root so the new value applies to the header and its
   descendants only: html's scroll-padding-top keeps the tall value, which is the safe one to use when
   the browser scrolls something into view on its own. */
.topbar.compact { --topbar-h: 56px; }
.topbar-inner { transition: height .45s var(--ease); }
.brand-sub { transition: opacity .3s var(--ease); }
.topbar.compact .brand-sub { opacity: 0; }
.topbar.compact .brand-img { width: 28px; height: 28px; }
.brand-img { transition: width .45s var(--ease), height .45s var(--ease); }

/* --- controls: pills, sheen, press ------------------------------------- */
/* This list and RIPPLE_TARGET in app.js are the same list for the same reason: a ripple in an element
   that does not clip is a purple circle sliding out over the table. Both grew the controls that were
   clickable but silent - the settings tabs, the journal's level and category chips, the two copy
   buttons, the analytics metric pills and the sortable column headers. */
.btn, .icon-btn, .pg, .chip, .seg, .dur, .ptab, .subtab, .adot, .fx-tile, .cat-chip,
.set-tab, .lv-chip, .copy-btn, .t-copy, .an-metric, th[data-sort], .mode-tile {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.btn { border-radius: var(--pill); letter-spacing: .005em; }
.btn-sm { padding: 9px 16px; }
.icon-btn { border-radius: var(--pill); }

.btn-primary {
  background-image: linear-gradient(180deg, var(--primary-soft), var(--primary) 46%, var(--primary-dark));
  border-color: rgba(255, 255, 255, .16);
  color: #fff;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .38),
    0 12px 30px -14px rgba(var(--acc-rgb), 1),
    var(--sh-accent);
}

/* Sheen: a soft band that sweeps across on hover. ::after is free here -
   .btn has no other pseudo-element in use, and overflow is already clipped. */
.btn-primary::after {
  content: '';
  position: absolute;
  top: -20%; bottom: -20%; left: -60%;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .38), transparent);
  transform: translateX(-40%) skewX(-14deg);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
}
.btn-primary:hover::after { animation: sheen .78s var(--ease); }
.btn-primary:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .45),
    0 20px 46px -16px rgba(var(--acc-rgb), 1),
    0 0 0 1px rgba(var(--acc-soft-rgb), .22);
  transform: translateY(-1px);
}

.btn-ghost {
  background-color: var(--ctl);
  border-color: var(--ctl-border);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.btn-ghost:hover {
  background-color: var(--ctl-hover);
  border-color: rgba(var(--acc-soft-rgb), .34);
  color: var(--text);
  transform: translateY(-1px);
  box-shadow: 0 10px 26px -16px rgba(var(--acc-rgb), .9);
}

.btn:active, .icon-btn:active, .seg:active, .pg:active, .dur:active, .cat-chip:active {
  transform: scale(.965);
  transition: transform .09s var(--ease);
}

.icon-btn:hover {
  background-color: var(--ctl-hover);
  border-color: rgba(var(--acc-soft-rgb), .32);
  color: var(--text);
  transform: translateY(-1px);
}

/* Ripple. Injected by app.js on pointerdown and removed after the animation;
   the element is inert and never affects layout. */
.ripple {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  background: radial-gradient(circle, rgba(255, 255, 255, .42) 0%, rgba(var(--acc-soft-rgb), .22) 42%, transparent 70%);
  transform: scale(.3);
  opacity: .6;
  animation: ripple-out .62s var(--ease) forwards;
}

/* --- fields ------------------------------------------------------------ */
.field-control, .gate-form input, select, textarea {
  border-radius: var(--r-fld);
}
.field-control {
  background-color: var(--input-bg);
  border-color: var(--ctl-border);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.field-control:hover { border-color: rgba(var(--acc-soft-rgb), .28); }
.field-control:focus-within {
  border-color: rgba(var(--acc-soft-rgb), .62);
  box-shadow: 0 0 0 4px rgba(var(--acc-rgb), .16), 0 0 26px -8px rgba(var(--acc-rgb), .6);
  background-color: rgba(10, 7, 18, .7);
}
.field-label { letter-spacing: .02em; }

/* --- tabs -------------------------------------------------------------- */
.ptab, .subtab { border-radius: var(--pill); }
/* The containers become pills too, otherwise a pill button sits inside a 10px box and the
   mismatch is visible at every corner. body[data-nav="bottom"] .subtabs keeps its own
   top-only rounding: that rule is more specific and still wins. */
.subtabs, .platform-tabs, .segmented, .pager { border-radius: var(--pill); }
.ptab:hover:not(.is-locked), .subtab:hover { color: var(--text-2); }
.ptab.is-active, .subtab.is-active {
  color: #fff;
  text-shadow: 0 0 18px rgba(var(--acc-soft-rgb), .55);
}
.subtab-glider, .tab-glider {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .22),
    0 10px 26px -14px rgba(var(--acc-rgb), 1),
    var(--sh-accent);
}

/* A platform the shop does not sell: on screen, obviously unavailable, and it
   says why if you try. `disabled` is not used - a disabled button fires no
   click, so there would be nothing to explain itself with. */
.ptab.is-locked {
  color: var(--muted);
  opacity: .45;
  cursor: not-allowed;
  filter: saturate(.2);
}
.ptab.is-locked:hover { color: var(--muted); transform: none; }
.ptab.is-locked .ptab-lock {
  display: inline-flex;
  margin-left: 2px;
  opacity: .85;
}
.ptab-lock { display: none; }

/* --- tables ------------------------------------------------------------ */
.table thead th {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}
.table tbody tr {
  animation: casc .42s var(--ease) both;
  transition: background-color .18s var(--ease), box-shadow .18s var(--ease);
}
.table tbody tr:hover {
  background-color: rgba(var(--acc-rgb), .09);
  box-shadow: inset 2px 0 0 var(--primary-soft);
}
/* Staggered entrance, CSS-only: the rows are re-rendered wholesale on every
   load, so the delay has to come from position rather than from JS. */
.table tbody tr:nth-child(1) { animation-delay: .01s; }
.table tbody tr:nth-child(2) { animation-delay: .035s; }
.table tbody tr:nth-child(3) { animation-delay: .06s; }
.table tbody tr:nth-child(4) { animation-delay: .085s; }
.table tbody tr:nth-child(5) { animation-delay: .11s; }
.table tbody tr:nth-child(6) { animation-delay: .135s; }
.table tbody tr:nth-child(7) { animation-delay: .16s; }
.table tbody tr:nth-child(8) { animation-delay: .185s; }
.table tbody tr:nth-child(9) { animation-delay: .21s; }
.table tbody tr:nth-child(10) { animation-delay: .235s; }
.table tbody tr:nth-child(11) { animation-delay: .26s; }
.table tbody tr:nth-child(12) { animation-delay: .285s; }
.table tbody tr:nth-child(n+13) { animation-delay: .31s; }

/* --- stat cards -------------------------------------------------------- */
.stat { transition: transform .3s var(--spring), border-color var(--t), box-shadow var(--t); }
.stat:hover {
  transform: translateY(-3px);
  border-color: rgba(var(--acc-soft-rgb), .28);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .07),
    0 22px 46px -26px rgba(var(--acc-rgb), .95);
}
.stat-value {
  font-weight: 700;
  letter-spacing: -.03em;
  background-image: linear-gradient(180deg, #fff, var(--primary-soft));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stat-value.accent { background-image: linear-gradient(180deg, #fff, var(--primary)); }
.stat-ico-mini {
  background-image: radial-gradient(circle at 50% 35%, rgba(255, 255, 255, .22), var(--primary-tint));
  box-shadow: 0 0 22px -6px rgba(var(--acc-rgb), 1);
}
/* "this number moved on the last automatic pass" */
.stat.bumped { animation: stat-bump 1.1s var(--ease); }

/* Semantic tones. The order of the cards is now Active → Expired → Not activated → Total, and the
   first two carry their meaning in colour rather than in a caption the eye has to read: green for
   licences that work, red for licences that do not. The number keeps its gradient (white at the top
   so it stays legible on any accent) and only the lower stop changes. */
.stat.tone-ok .stat-value { background-image: linear-gradient(180deg, #fff, var(--success)); }
.stat.tone-bad .stat-value { background-image: linear-gradient(180deg, #fff, var(--danger)); }
.stat.tone-warn .stat-value { background-image: linear-gradient(180deg, #fff, var(--warn)); }
.stat.tone-info .stat-value { background-image: linear-gradient(180deg, #fff, var(--info)); }
.stat.tone-ok .stat-ico-mini { color: var(--success); box-shadow: 0 0 22px -8px var(--success); }
.stat.tone-bad .stat-ico-mini { color: var(--danger); box-shadow: 0 0 22px -8px var(--danger); }
.stat.tone-warn .stat-ico-mini { color: var(--warn); box-shadow: 0 0 22px -8px var(--warn); }
.stat.tone-info .stat-ico-mini { color: var(--info); box-shadow: 0 0 22px -8px var(--info); }
.stat.tone-ok:hover { border-color: rgba(143, 227, 192, .3); }
.stat.tone-bad:hover { border-color: rgba(255, 154, 169, .3); }

/* Cursor spotlight. Only painted at motion=full, and only on the card the
   pointer is actually over - app.js writes --mx/--my. */
.stat.spot::after, .link-card.spot::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(220px circle at var(--mx, 50%) var(--my, 50%), rgba(var(--acc-soft-rgb), .16), transparent 62%);
  opacity: 1;
  z-index: 1;
}

/* --- glass edge that turns toward the cursor -----------------------------
   The reference site's signature detail, and the reason its cards read as thick glass rather than as
   translucent rectangles: a 1.2px ring of light whose brightest point sits on the side the pointer is
   on. Two custom properties drive it, both written by app.js from the same pointermove that already
   feeds the spotlight - --la is the angle from the card's centre to the cursor, --hot is how near the
   edge the cursor is.

   The ring is a conic gradient punched out by a mask (padding + mask-composite: exclude), which is the
   only way to get a gradient that follows a border-radius. That is also why it is applied to the ONE
   card under the cursor and not to every card: the cost rules for the backdrop in this file measured
   masks on moving layers at 11-13fps against 20, and forty masked rings is forty of them. One ring,
   repainted on pointermove, is the same cost the spotlight already has.

   It replaces the top hairline while hovered rather than sitting under it - both are ::before, and a
   bright edge that turns is strictly more information than a static one. */
.stat.spot::before, .link-card.spot::before {
  content: '';
  position: absolute;
  inset: 0;
  height: auto;
  border-radius: inherit;
  padding: 1.2px;
  pointer-events: none;
  z-index: 2;
  background: conic-gradient(from var(--la, 220deg),
    rgba(255, 255, 255, .9), rgba(225, 210, 255, .3) 8%, rgba(255, 255, 255, .06) 24%,
    rgba(var(--acc-soft-rgb), .42) 50%, rgba(var(--acc-pale-rgb), .5) 66%,
    rgba(255, 255, 255, .06) 80%, rgba(255, 255, 255, .9));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: calc(.55 + var(--hot, 0) * .45);
  transition: opacity .5s var(--ease);
}
/* Not painted at all when decoration is off: a masked conic ring is the most expensive thing on this
   list and carries no information whatsoever. */
body[data-motion="off"] .stat.spot::before,
body[data-motion="off"] .link-card.spot::before { background: none; opacity: 0; }

.link-card { transition: transform .3s var(--spring), border-color var(--t), box-shadow var(--t); }
.link-card:hover {
  transform: translateY(-3px);
  border-color: rgba(var(--acc-soft-rgb), .3);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 24px 50px -28px rgba(var(--acc-rgb), 1);
}

/* --- log terminal ------------------------------------------------------ */
.trow {
  animation: casc .34s var(--ease) both;
  cursor: pointer;
}
.trow:hover {
  background-color: rgba(var(--acc-rgb), .1);
  box-shadow: inset 2px 0 0 var(--primary-soft);
}
.trow:focus-visible { outline-offset: -2px; }
.trow.fresh { animation: trow-in .5s var(--ease), flash-row 1.1s var(--ease); }
.term-wrap { background-color: rgba(8, 6, 14, .5); }
.live { text-shadow: 0 0 16px rgba(143, 227, 192, .5); }

/* the one-word answer to "what is this row about" */
.t-cat {
  display: inline-block;
  margin-left: 9px;
  padding: 1px 8px;
  border-radius: var(--pill);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: lowercase;
  white-space: nowrap;
  color: var(--text-2);
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .09);
}
.t-cat[data-cat="keys_fail"] { color: var(--danger); border-color: rgba(255, 154, 169, .3); background: rgba(255, 154, 169, .1); }
.t-cat[data-cat="keys_use"] { color: var(--success); border-color: rgba(143, 227, 192, .28); background: rgba(143, 227, 192, .1); }
.t-cat[data-cat="keys_ops"] { color: var(--primary-soft); border-color: rgba(var(--acc-soft-rgb), .34); background: rgba(var(--acc-rgb), .12); }
.t-cat[data-cat="admin"] { color: var(--info); border-color: rgba(158, 197, 255, .3); background: rgba(158, 197, 255, .1); }
.t-cat[data-cat="bot"] { color: var(--warn); border-color: rgba(255, 208, 138, .3); background: rgba(255, 208, 138, .1); }
.t-cat[data-cat="bans"] { color: #ffb3c0; border-color: rgba(255, 154, 169, .34); background: rgba(255, 154, 169, .12); }

/* category chips: what the journal is about, as opposed to how loud it is */
.cat-row {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 14px 24px 0;
}
.cat-chip {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 13px;
  border-radius: var(--pill);
  border: 1px solid var(--ctl-border);
  background: var(--ctl);
  color: var(--text-2);
  font-size: 12.5px; font-weight: 550;
  transition: background-color var(--t), border-color var(--t), color var(--t), transform .15s var(--ease), box-shadow var(--t);
}
.cat-chip .cnt {
  font-family: var(--mono);
  font-size: 11px;
  padding: 1px 7px;
  border-radius: var(--pill);
  background: rgba(255, 255, 255, .07);
  color: var(--muted);
}
.cat-chip:hover { color: var(--text); border-color: rgba(var(--acc-soft-rgb), .34); transform: translateY(-1px); }
.cat-chip.is-active {
  color: #fff;
  border-color: rgba(var(--acc-soft-rgb), .55);
  background-image: linear-gradient(180deg, rgba(var(--acc-rgb), .34), rgba(var(--acc-deep-rgb), .22));
  box-shadow: 0 8px 22px -12px rgba(var(--acc-rgb), 1);
}
.cat-chip.is-active .cnt { background: rgba(255, 255, 255, .16); color: #fff; }
.cat-chip.empty { opacity: .5; }

/* --- the detail sheet a log row opens ----------------------------------- */
.log-detail { display: flex; flex-direction: column; gap: 2px; }
.ld-row {
  display: grid; grid-template-columns: 132px 1fr; gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: 13.5px;
}
.ld-k { color: var(--muted); font-size: 12px; letter-spacing: .04em; text-transform: uppercase; padding-top: 2px; }
.ld-v { color: var(--text); word-break: break-word; }
.ld-v.mono { font-family: var(--mono); font-size: 12.5px; color: var(--primary-soft); }
.ld-msg { padding: 12px 0 4px; }
.ld-msg p {
  margin: 8px 0 0;
  padding: 12px 14px;
  border-radius: var(--r-in);
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text);
  word-break: break-word;
}

/* --- modal / popover / toast ------------------------------------------- */
.modal { animation: modal-in .34s var(--spring); }
.pop { animation: pop-in .3s var(--spring); }
.toast {
  animation: toast-in .42s var(--spring);
  border-color: rgba(var(--acc-soft-rgb), .18);
}
.toast.success { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 20px 44px -24px rgba(143, 227, 192, .8); }
.toast.error { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 20px 44px -24px rgba(255, 154, 169, .8); }

/* --- login gate -------------------------------------------------------- */
.gate-card {
  background-image: linear-gradient(180deg, rgba(var(--acc-deep-rgb), .3), rgba(14, 10, 24, .74));
  background-color: var(--sheet);
  border-color: rgba(var(--acc-soft-rgb), .18);
  animation: card-in .6s var(--ease);
}
.gate-logo .mark {
  filter: drop-shadow(0 0 18px rgba(var(--acc-soft-rgb), .65));
  animation: halo 4.6s var(--ease) infinite;
}
.gate-title {
  letter-spacing: .16em;
  background-image: linear-gradient(180deg, #fff, var(--primary-soft));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* --- the operator's own logo -------------------------------------------- */
/* Round with a hairline accent edge, so it reads as part of the glass rather
   than as a photo pasted on top. The topbar one tilts a couple of degrees on
   hover - the brand is the one element here where a little play is free. */
.brand-img {
  width: 34px; height: 34px;
  flex: none;
  display: block;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(var(--acc-soft-rgb), .35);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45), 0 6px 18px -8px rgba(var(--acc-rgb), .9);
  transition: transform .35s var(--spring), box-shadow .35s var(--ease);
}
.brand:hover .brand-img {
  transform: scale(1.07) rotate(-2.5deg);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45), 0 0 24px -4px rgba(var(--acc-rgb), .95);
}
.gate-logo-img {
  width: 76px; height: 76px;
  display: block;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(var(--acc-soft-rgb), .32);
  filter: drop-shadow(0 0 22px rgba(var(--acc-rgb), .55));
  animation: halo 4.6s var(--ease) infinite;
}
/* An avatar (settings → Аккаунт) uses the same treatment at a smaller size. */
.avatar-img {
  width: 100%; height: 100%;
  display: block;
  border-radius: 50%;
  object-fit: cover;
}
@media (max-width: 620px) {
  .brand-img { width: 30px; height: 30px; }
  .gate-logo-img { width: 62px; height: 62px; }
}

/* --- badges, chips, switches ------------------------------------------- */
.badge, .chip { border-radius: var(--pill); }
.switch[aria-checked="true"] {
  background-color: rgba(var(--acc-rgb), .5);
  border-color: var(--primary-line);
  box-shadow: 0 0 20px -6px rgba(var(--acc-rgb), 1);
}
.switch[aria-checked="true"] .switch-knob { background: #f2edff; }
.switch { transition: background-color .28s var(--ease), box-shadow .28s var(--ease), border-color var(--t); }

/* --- «Туманность»: ribbons and motes ------------------------------------ */
/* Two wide, soft bands of accent light that drift in opposite directions. The
   blooms alone (which is all «Сияние» has) read as a slow pulse; adding
   something that travels across the frame is what makes the backdrop feel
   like weather rather than like a screensaver. Transform-only, so they cost
   compositing and nothing else. */
.bg-ribbon {
  position: absolute;
  left: -30%; width: 160%; height: 42vh;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  transition: opacity .6s var(--ease);
  background: linear-gradient(100deg,
    transparent 8%,
    rgba(var(--acc-soft-rgb), .1) 34%,
    rgba(255, 255, 255, .055) 50%,
    rgba(var(--acc-rgb), .12) 66%,
    transparent 92%);
  mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, #000 20%, transparent 78%);
}
.bg-ribbon-1 { top: 4vh; transform: rotate(-7deg); }
.bg-ribbon-2 { top: 46vh; height: 34vh; transform: rotate(5deg); }
.bg[data-fx="nebula"] .bg-ribbon { opacity: 1; }
.bg[data-fx="nebula"] .bg-ribbon-1 { animation: ribbon-1 34s var(--ease-io) infinite alternate; }
.bg[data-fx="nebula"] .bg-ribbon-2 { animation: ribbon-2 46s var(--ease-io) infinite alternate; }
/* the blooms burn a little brighter under nebula, so the ribbons have
   something to sit on */
.bg[data-fx="nebula"] .bg-aurora-1 { opacity: .62; }
.bg[data-fx="nebula"] .bg-aurora-2 { opacity: .48; }
.bg[data-fx="nebula"] .bg-aurora-3 { opacity: .3; }

/* Motes: soft accent points. The softness is a radial gradient rather than
   filter: blur() - blurring dozens of animated elements costs frames, and the
   gradient is free after the first paint. */
.bg[data-fx="nebula"] .fx-layer i,
.fx-layer i {
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(var(--acc-pale-rgb), .95) 0%,
    rgba(var(--acc-soft-rgb), .5) 38%,
    rgba(var(--acc-rgb), .16) 62%,
    transparent 74%);
  opacity: var(--o, .6);
  animation-name: mote-drift;
  animation-timing-function: var(--ease-io);
  animation-iteration-count: infinite;
  animation-direction: alternate;
  will-change: transform, opacity;
}
.fx-layer i.big { box-shadow: 0 0 18px 4px rgba(var(--acc-soft-rgb), .3); }

/* the same ribbons, miniature, inside the settings preview tile */
.fx-ribbon {
  position: absolute; left: -40%; width: 180%; height: 46%;
  opacity: .85; pointer-events: none;
  background: linear-gradient(100deg, transparent 10%, rgba(var(--acc-soft-rgb), .3) 48%, transparent 88%);
}
.fx-ribbon.r1 { top: 8%; transform: rotate(-8deg); animation: ribbon-1 9s var(--ease-io) infinite alternate; }
.fx-ribbon.r2 { top: 52%; height: 34%; transform: rotate(6deg); animation: ribbon-2 12s var(--ease-io) infinite alternate; }

/* --- sound controls (settings popover) --------------------------------- */
.snd-row { display: flex; align-items: center; gap: 12px; }
.snd-state { font-size: 13px; color: var(--muted); min-width: 74px; }
.snd-state.on { color: var(--success); }
.snd-row .btn { margin-left: auto; }
.snd-vol { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
.snd-vol-val { font-size: 12.5px; color: var(--text-2); min-width: 40px; text-align: right; }

input[type="range"]#snd-volume {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 22px;
  background: transparent;
  cursor: pointer;
}
input[type="range"]#snd-volume::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--pill);
  background: linear-gradient(90deg, var(--primary) var(--snd-fill, 50%), rgba(255, 255, 255, .1) var(--snd-fill, 50%));
}
input[type="range"]#snd-volume::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px;
  margin-top: -5px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid rgba(var(--acc-soft-rgb), .8);
  box-shadow: 0 0 14px -2px rgba(var(--acc-rgb), 1);
  transition: transform .18s var(--spring);
}
input[type="range"]#snd-volume:active::-webkit-slider-thumb { transform: scale(1.18); }
input[type="range"]#snd-volume::-moz-range-track {
  height: 6px; border-radius: var(--pill); background: rgba(255, 255, 255, .1);
}
input[type="range"]#snd-volume::-moz-range-progress {
  height: 6px; border-radius: var(--pill); background: var(--primary);
}
input[type="range"]#snd-volume::-moz-range-thumb {
  width: 15px; height: 15px; border-radius: 50%;
  background: #fff; border: 1px solid rgba(var(--acc-soft-rgb), .8);
}
input[type="range"]#snd-volume[disabled] { opacity: .45; cursor: not-allowed; }

/* The topbar mute button reads as "on" with an accent glow, so its state is
   visible without opening the popover. */
#btn-sound[aria-pressed="true"] {
  color: var(--primary-soft);
  border-color: rgba(var(--acc-soft-rgb), .4);
  background-color: var(--primary-tint);
  box-shadow: 0 0 22px -8px rgba(var(--acc-rgb), 1);
}

/* --- keyboard focus ---------------------------------------------------- */
/* The reference site shows its focus state as an accent ring rather than the browser default,
   which on this background is nearly invisible. Scoped to :focus-visible so a mouse click does not
   leave rings on everything. Deliberately no border-radius here: setting it in a global selector
   would override the radius of every control it matches. */
:focus-visible {
  outline: 2px solid rgba(var(--acc-soft-rgb), .75);
  outline-offset: 2px;
}
.btn:focus-visible, .icon-btn:focus-visible, .ptab:focus-visible, .subtab:focus-visible,
.seg:focus-visible, .pg:focus-visible, .switch:focus-visible, .adot:focus-visible,
.fx-tile:focus-visible, .cat-chip:focus-visible { outline-offset: 3px; }

/* --- scroll reveal ------------------------------------------------------ */
/* The recipe is ghostlynex's, and its shape matters more than its numbers. A block does not simply
   fade and slide up: it rises out of the floor. `perspective()` with a negative Z, a few degrees of
   rotateX and an origin at the bottom centre is what turns a slide into depth - the top edge travels
   further than the bottom one, so the block is tipping up towards you rather than being carried in.
   The blur is the other half: it reads as coming into focus, not as arriving.

   Durations are long on purpose (1.2s opacity, 1.4s transform, .7s filter) and all on --ease, which
   is cubic-bezier(.22,1,.36,1) - it covers most of the distance in the first fifth and then settles.
   A short linear fade at these distances looks like a crossfade between two screenshots; this looks
   like the block has weight.

   Gated behind html.reveal-on, which app.js adds only once the IntersectionObserver exists, so a JS
   failure cannot leave the panel's content invisible - that is the one way this could do real damage.
   It used to be gated on body[data-motion="full"] as well, which meant the default «Обычно» setting
   showed no reveal at all; the preference that turns decoration off is «Выкл», and that is what this
   honours now (plus prefers-reduced-motion, which wins over the taste setting). */
html.reveal-on body:not([data-motion="off"]) .view .panel:not(.revealed),
html.reveal-on body:not([data-motion="off"]) .stats-top .stat:not(.revealed) {
  opacity: 0;
  transform: perspective(1200px) translate3d(0, 46px, -60px) rotateX(9deg) scale(.97);
  transform-origin: 50% 100%;
  filter: blur(7px);
}
html.reveal-on body:not([data-motion="off"]) .view .panel.revealed,
html.reveal-on body:not([data-motion="off"]) .stats-top .stat.revealed {
  opacity: 1;
  transform: none;
  filter: none;
  transition: opacity calc(1.2s * var(--speed, 1)) var(--ease),
              transform calc(1.4s * var(--speed, 1)) var(--ease),
              filter calc(.7s * var(--speed, 1)) var(--ease);
  transition-delay: var(--d, 0s);
}
/* The four cards across the top are one row, so they stagger instead of landing together: 70ms apart
   is ghostlynex's per-word step and it reads as one motion travelling across the row rather than as
   four. Panels below are left at 0s - they enter the viewport at different scroll positions anyway,
   so staggering them would only delay the ones you are already looking at. */
html.reveal-on body:not([data-motion="off"]) .stats-top .stat:nth-child(2) { --d: .07s; }
html.reveal-on body:not([data-motion="off"]) .stats-top .stat:nth-child(3) { --d: .14s; }
html.reveal-on body:not([data-motion="off"]) .stats-top .stat:nth-child(4) { --d: .21s; }

/* --- scroll progress ---------------------------------------------------- */
/* A 2px line along the bottom of the sticky topbar, driven by transform: scaleX from the rAF loop in
   app.js - never width, never top, so it costs compositing only. It is the one scroll affordance
   here that carries information rather than decoration, which is why it stays on at motion=«Выкл»:
   on a panel with five views and a table that can be two thousand rows long, "how much is left" is
   an answer, not an ornament. */
.topbar-prog {
  position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  transform-origin: left; transform: scaleX(0);
  background: linear-gradient(90deg, transparent, rgba(var(--acc-rgb), .9) 18%, rgba(var(--acc-pale-rgb), 1) 62%, #fff);
  box-shadow: 0 0 10px rgba(var(--acc-rgb), .75);
  opacity: .85;
  pointer-events: none;
  will-change: transform;
}

/* The topbar is sticky, so anything the browser scrolls into view on its own - find-in-page, a focused
   control, an anchor - would otherwise land underneath it. ghostlynex uses 96px for a taller bar; this
   one is --topbar-h plus its own padding. */
html { scroll-padding-top: calc(var(--topbar-h, 68px) + 16px); }

/* Background scroll is locked while a modal is open. Without it the wheel keeps turning the page
   behind the window, and on a panel whose tables are long that means closing a dialog to find the
   view has moved several hundred pixels. Also read by the wheel handler in app.js, which has to
   leave the page alone entirely while a sheet owns the scroll. */
body.locked { overflow: hidden; }

/* --- motion ceiling ----------------------------------------------------- */
/* prefs.motion = 'off' keeps whatever carries information (the numbers rolling,
   the live badge, the ring draining) and drops the decoration. This is a taste
   setting; prefers-reduced-motion below is the accessibility one and wins over
   everything.

   `.view.is-active > *` is in this list because of the scroll reveal, and the reason is not obvious.
   The reveal hides a block with `opacity: 0` under `body:not([data-motion="off"])`, and the view's
   entrance animation is switched off under the same selector. Turn motion off and BOTH stop
   applying - including the switch-off, so `animation: view-in .4s both` comes back, and an animation
   with `both` fill holds its from-state through the delay. The measured result was a panel at
   opacity 0 under «Выкл»: the one setting that is supposed to guarantee no decoration was the one
   that hid content. Stopping the animation here closes that, and it is the same category as the
   `.table tbody tr` and `.trow` rules already below - entrance animation, not information. */
body[data-motion="off"] .btn-primary::after,
body[data-motion="off"] .ripple,
body[data-motion="off"] .stat.spot::after,
body[data-motion="off"] .link-card.spot::after,
body[data-motion="off"] .bg-ribbon,
body[data-motion="off"] .bg-blob,
body[data-motion="off"] .bg-rays,
body[data-motion="off"] .bg-grid,
body[data-motion="off"] .fx-layer i,
body[data-motion="off"] .bg-garland i,
body[data-motion="off"] .bg-fog,
body[data-motion="off"] .fx-tile .fx-prev::before,
body[data-motion="off"] .view.is-active > *,
body[data-motion="off"] .gate-logo .mark { animation: none !important; }
body[data-motion="off"] .ripple { display: none; }
body[data-motion="off"] .table tbody tr,
body[data-motion="off"] .trow { animation: none; }
body[data-motion="off"] .stat:hover,
body[data-motion="off"] .link-card:hover,
body[data-motion="off"] .btn-ghost:hover,
body[data-motion="off"] .icon-btn:hover { transform: none; }
body[data-motion="off"] .bg-ribbon { opacity: 0 !important; }
/* Nothing else here needs a "hide it too" rule. Stopping an animation returns the layer to its
   unanimated state, and every one of these starts off-screen or at opacity 0: the radar group parks
   above the viewport (top: -34vh), the particles keep the opacity: 0 their recipes declare, and the
   curtains, caustics, shards and the eclipse simply stop moving. The ribbons are the exception
   because their unanimated state is *visible* - a static band of light that reads as a rendering
   artefact rather than as a paused effect. */
/* 'normal' (the default) gets everything except the two heaviest decorations */
body[data-motion="normal"] .stat.spot::after,
body[data-motion="normal"] .link-card.spot::after { display: none; }
body[data-motion="normal"] .bg-ribbon { opacity: .55; }
/* ...and 'full' is supposed to be MORE than that, which it was not: the ribbons had a rule for
   'normal' (.55) and one for 'off' (0), but none for 'full', so at the highest setting they fell
   back to their base opacity of 0 and vanished from every background except «Туманность», which
   switches them on itself. Found by a pixel measurement of «Дождь» that came back at 0.7% of pixels
   changed on the operator's machine and 89% on a fresh database - the difference was not the rain,
   it was that one machine ran motion=full and the other motion=normal. A ladder that is not
   monotonic is a bug even when every rung looks reasonable on its own. */
body[data-motion="full"] .bg-ribbon { opacity: 1; }

/* --- behaviour attributes (prefs.density, prefs.flash) ------------------- */
/* Compact rows: same type, less air. Applied to the three places rows are drawn - the keys and logs
   tables, the journal's terminal rows and the analytics tables - because "compact" that only tightens
   one of them reads as a bug in the other two. Roughly 1.5x the rows on screen, which is the whole
   point of the setting. */
body[data-density="compact"] .table tbody td { padding-top: 7px; padding-bottom: 7px; }
body[data-density="compact"] .table thead th { padding-top: 8px; padding-bottom: 8px; }
body[data-density="compact"] .an-table thead th,
body[data-density="compact"] .an-table tbody td { padding-top: 6px; padding-bottom: 6px; }
body[data-density="compact"] .trow { padding-top: 5px; padding-bottom: 5px; }
body[data-density="compact"] .expcell { gap: 0; }
body[data-density="compact"] .stat { padding: 14px 16px; }

/* The change-flash, off. The `.bumped` class is added by syncStatCards() whenever a number differs
   from the previous poll; suppressing the animation here rather than not adding the class keeps one
   code path and means the setting takes effect on the cards already on screen. */
body[data-flash="off"] .stat.bumped { animation: none; }

/* --- keyframes --------------------------------------------------------- */
@keyframes ripple-out {
  to { transform: scale(1); opacity: 0; }
}
@keyframes sheen {
  0% { opacity: 0; transform: translateX(-40%) skewX(-14deg); }
  22% { opacity: 1; }
  100% { opacity: 0; transform: translateX(320%) skewX(-14deg); }
}
/* The reference site's `casc`: content arrives from slightly below, slightly
   out of focus, and settles. Blur is what makes it feel like depth rather than
   like a slide. */
@keyframes casc {
  from { opacity: 0; transform: translateY(14px) scale(.985); filter: blur(6px); }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes flash-row {
  0% { background-color: rgba(var(--acc-soft-rgb), .22); }
  100% { background-color: transparent; }
}
@keyframes halo {
  0%, 100% { transform: scale(1); opacity: .92; }
  50% { transform: scale(1.07); opacity: 1; }
}
@keyframes stat-bump {
  0% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 0 0 0 rgba(var(--acc-rgb), .5); }
  35% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 0 34px -4px rgba(var(--acc-rgb), .75); }
  100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 0 0 0 rgba(var(--acc-rgb), 0); }
}
@keyframes ribbon-1 {
  0% { transform: translate3d(-6vw, 0, 0) rotate(-7deg) scaleY(1); }
  100% { transform: translate3d(9vw, 5vh, 0) rotate(-4deg) scaleY(1.18); }
}
@keyframes ribbon-2 {
  0% { transform: translate3d(7vw, 0, 0) rotate(5deg) scaleY(1.1); }
  100% { transform: translate3d(-8vw, -4vh, 0) rotate(2deg) scaleY(.92); }
}
@keyframes mote-drift {
  0% { transform: translate3d(0, 0, 0) scale(.9); opacity: calc(var(--o, .6) * .55); }
  50% { opacity: var(--o, .6); }
  100% { transform: translate3d(var(--dx, 4cqw), var(--dy, -6cqh), 0) scale(1.15); opacity: calc(var(--o, .6) * .5); }
}

/* --- leftovers that named a specific purple ----------------------------- */
/* The spinner, the field focus rings and the settings-tile hovers hardcoded
   rgba(139,92,246,...) or a neutral grey, so they kept the old violet whatever
   accent was chosen - the visible half of "the accent does not apply
   everywhere". Overridden here rather than edited in place, to keep the skin in
   one piece. */
.spinner {
  border-color: rgba(var(--acc-rgb), .22);
  border-top-color: var(--primary-soft);
}
.field-control:hover { border-color: rgba(var(--acc-soft-rgb), .3); }
.field-control:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--acc-rgb), .16);
  background-color: rgba(12, 9, 20, .72);
}
.search input:hover { border-color: rgba(var(--acc-soft-rgb), .3); }
.search input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--acc-rgb), .16);
  background-color: rgba(12, 9, 20, .72);
}
.fx-tile:hover {
  border-color: rgba(var(--acc-soft-rgb), .38);
  box-shadow: 0 12px 28px -18px rgba(var(--acc-rgb), 1);
}
.fx-tile.is-active {
  border-color: rgba(var(--acc-soft-rgb), .55);
  box-shadow: 0 0 0 1px rgba(var(--acc-soft-rgb), .35) inset, 0 12px 30px -18px rgba(var(--acc-rgb), 1);
}
.fx-tile { border-radius: var(--r-in); }

/* Fifteen backgrounds. Three up is the compromise: two columns would make eight rows of half-empty
   space in a window that already scrolls, and four columns make each preview too small to tell
   «Лепестки» from «Снег» at a glance - which is the pair that actually needs the room, since both are
   small pale shapes falling across a dark field. */
.fx-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.fx-tile[data-fx="nebula"] .fx-prev { background-color: var(--bg); }

/* --- native dropdowns: the popup is not the page ------------------------ */
/* The glass restyle made --ctl translucent, and a native <select> popup does
   not composite over the page: it is drawn by the browser on its own surface,
   so a 5%-opaque background landed on white. That is the "нажимаю — и всё
   белое" the operator saw on the status filter. Options are painted with an
   opaque colour instead, and each status keeps the colour its badge has in the
   table, so the list reads as the same language as the data. */
select, .select select, option {
  background-color: var(--ctl-solid);
  color: var(--text);
  color-scheme: dark;
}
option { padding: 9px 12px; }
option:disabled { color: var(--muted); opacity: .55; }
option.st-active { color: var(--success); }
option.st-expired { color: var(--danger); }
option.st-unused { color: var(--info); }
option.st-revoked { color: var(--muted); }
option.st-all { color: var(--text-2); }
/* the closed control echoes the colour of whatever is selected */
.select select[data-tone="active"] { color: var(--success); }
.select select[data-tone="expired"] { color: var(--danger); }
.select select[data-tone="unused"] { color: var(--info); }
.select select[data-tone="revoked"] { color: var(--muted); }

/* --- the keys table fits its panel -------------------------------------- */
/* Five columns of fixed pixel widths plus 28px of padding on every side added
   up to ~1230px inside a ~1216px panel, so the table overflowed by a handful
   of pixels and grew a horizontal scrollbar - the panel looked broken even
   though nothing was missing. Fixed layout with percentages means it can never
   be wider than its container; the values that can be long (HWID above all)
   truncate with an ellipsis and stay readable in full in the title tooltip. */
#view-keys .table { table-layout: fixed; }
#view-keys .table thead th,
#view-keys .table tbody td { padding-left: 15px; padding-right: 15px; }
#view-keys .c-key { width: 24%; min-width: 0; }
#view-keys .c-status { width: 15%; }
#view-keys .c-hwid { width: 24%; }
#view-keys .c-exp { width: 19%; }
#view-keys .c-act { width: 18%; }
#view-keys .table tbody td { overflow: hidden; }
#view-keys .keycell, #view-keys .c-hwid {
  min-width: 0;
  overflow: hidden;
}
#view-keys .keycell .mono, #view-keys .c-hwid .mono {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* ---- card mode takes all of the above back ----
   Those percentages are column shares for a five-column table. Below 720px the row stops being a
   table row and becomes a two-column grid, and the very same declarations are then applied to the grid
   ITEMS - so every cell shrinks to a slice of the card instead of filling its track. Measured at
   390px: the tracks were a correct 157px + 157px, and `.c-act` still computed to 59px because
   `width: 18%` won on specificity; five 40px action buttons wrapped into five rows inside it and the
   card came out 410px tall - worse than the 269px stacked layout this grid was meant to replace.

   The ellipsis rules above are wrong here for the same reason: truncating a key is correct when it has
   24% of a 1200px table and wrong when it has a whole card to itself, because the key is the thing
   being read out and copied. Specificity matches the rules it overrides and the block comes after
   them, which is the only thing that makes it win - `#view-keys` beats any class-only selector. */
@media (max-width: 720px) {
  #view-keys .table tbody td { padding-left: 0; padding-right: 0; overflow: visible; }
  #view-keys .table tbody td.c-key,
  #view-keys .table tbody td.c-status,
  #view-keys .table tbody td.c-hwid,
  #view-keys .table tbody td.c-exp,
  #view-keys .table tbody td.c-act { width: auto; }
  #view-keys .keycell, #view-keys .c-hwid { overflow: visible; }
  #view-keys .keycell .mono, #view-keys .c-hwid .mono {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    overflow-wrap: anywhere;
  }
}
#view-keys .row-actions { gap: 4px; }
#view-keys .row-actions .icon-btn { width: 30px; height: 30px; }
#view-keys .expcell .date { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- account menu ------------------------------------------------------- */
/* Narrow, and deliberately not the settings window: the click on the name
   answers "who am I and what can I do", the «Настройки» item inside it opens
   the window. Two levels instead of one 620px popover with everything in it. */
.user-menu { width: 320px; padding: 8px; }
.um-head {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px 14px;
  border-bottom: 1px solid var(--border-soft);
  margin-bottom: 6px;
}
.um-avatar {
  width: 42px; height: 42px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  font-weight: 700; font-size: 17px;
  color: #fff;
  background-image: radial-gradient(circle at 50% 32%, rgba(255, 255, 255, .28), var(--primary-tint));
  border: 1px solid rgba(var(--acc-soft-rgb), .35);
  box-shadow: 0 0 20px -8px rgba(var(--acc-rgb), 1);
  overflow: hidden;
}
.um-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.um-meta b { font-size: 14.5px; font-weight: 650; }
.um-meta span { font-size: 12px; color: var(--muted); }
.um-item {
  width: 100%;
  display: flex; align-items: center; gap: 11px;
  padding: 11px 12px;
  border-radius: var(--r-in);
  color: var(--text-2);
  font-size: 14px; font-weight: 550;
  text-align: left;
  transition: background-color var(--t), color var(--t), transform .16s var(--ease);
}
.um-item:hover { background-color: rgba(var(--acc-rgb), .12); color: var(--text); }
.um-item:active { transform: scale(.985); }
.um-item .um-ico { display: flex; color: var(--primary-soft); flex: none; }
.um-item .um-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.um-hint { font-size: 11.5px; font-weight: 450; color: var(--muted); }
.um-item.danger:hover { background-color: rgba(255, 154, 169, .14); color: var(--danger); }
.um-item.danger .um-ico { color: inherit; }

/* avatars: the same round treatment in all three places, sized by context */
.user-avatar, .acc-avatar { overflow: hidden; }
.user-avatar.has-img, .um-avatar.has-img, .acc-avatar.has-img {
  background-image: none;
  border: 1px solid rgba(var(--acc-soft-rgb), .4);
}
.acc-avatar-row { display: flex; align-items: center; gap: 18px; }
.acc-avatar {
  width: 84px; height: 84px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  font-size: 34px; font-weight: 700; color: #fff;
  background-image: radial-gradient(circle at 50% 32%, rgba(255, 255, 255, .26), var(--primary-tint));
  border: 1px solid rgba(var(--acc-soft-rgb), .35);
  box-shadow: 0 0 30px -10px rgba(var(--acc-rgb), 1);
}
.acc-avatar-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; min-width: 0; }
.acc-avatar-acts .set-note { flex-basis: 100%; margin: 2px 0 0; }

/* Account facts under the avatar. Key/value rows rather than a table: there are six of them, they are
   one line each, and a <table> would bring its own header row and borders into a card that is
   otherwise just an avatar and two buttons. The rule line is a hairline in --border-soft so the block
   reads as a list and not as a grid.

   This is what closed the hole under the card. It was 181px tall in a grid row whose other card is
   468px, and `align-items: start` meant the difference showed as 287px of empty shell below it - the
   «снизу слишком много места» complaint. Stretching the card would have moved the same emptiness
   inside it; giving the card something to say removed it. */
.acc-summary {
  display: flex; flex-direction: column;
  margin-top: 16px;
  border-top: 1px solid var(--border-soft);
}
.acc-sum-row {
  display: flex; align-items: baseline; gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border-soft);
}
.acc-sum-row:last-child { border-bottom: none; }
.acc-sum-k { flex: none; width: 128px; font-size: 12.5px; color: var(--muted); }
.acc-sum-v { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--text-2); font-weight: 550; }

/* --- the settings window ------------------------------------------------ */
/* Tabs in a row along the top. They were moved to a left rail on 05.10.2026 and moved back the same
   day at the operator's request («вкладки надо сверху а не слева») - the rail is gone, and so is the
   narrow-screen rule that turned it back into a row, because a row is now the only layout. What
   survived from that round is the glider being sized on both axes: it costs nothing and it is what
   keeps the pill correct if the tabs are ever laid out vertically again. */
.set-wrap { display: flex; flex-direction: column; min-height: 0; }
.set-tabs {
  position: relative;
  display: flex; gap: 4px;
  padding: 5px;
  margin-bottom: 18px;
  border-radius: var(--pill);
  background-color: rgba(255, 255, 255, .045);
  border: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}
.set-tabs::-webkit-scrollbar { display: none; }
.set-tab {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: 8px;
  flex: none;
  padding: 9px 16px;
  border-radius: var(--pill);
  font-size: 13.5px; font-weight: 600; white-space: nowrap;
  color: var(--muted);
  transition: color var(--t);
}
.set-tab:hover { color: var(--text-2); }
.set-tab.is-active { color: #fff; text-shadow: 0 0 16px rgba(var(--acc-soft-rgb), .5); }
/* The glider slides between tabs, so it is sized and moved on both axes at once: it takes the active
   tab's width AND height and translates to its offsetLeft/offsetTop. Sizing one axis only is how a
   pill ends up sitting over two tabs at once when the layout changes. */
.set-glider {
  position: absolute; top: 0; left: 0; z-index: 0;
  border-radius: var(--pill);
  background-image: linear-gradient(180deg, var(--primary-soft), var(--primary) 55%, var(--primary-dark));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 10px 24px -14px rgba(var(--acc-rgb), 1);
  transition: transform .38s var(--spring), width .38s var(--spring), height .38s var(--spring);
  pointer-events: none;
}
.set-panel { display: none; }
.set-panel.is-active { display: block; animation: casc .34s var(--ease) both; }
/* align-items: start, not the default stretch: the avatar card and the password card sit side by side
   and the password one is twice as tall, so stretching made the avatar card a tall box with the
   controls at the top and nothing but empty space under the note - «дохуя отступов снизу». */
.set-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: start; }
.set-card {
  position: relative;
  padding: 18px 18px 16px;
  margin-bottom: 14px;
  border-radius: var(--r-in);
  background-color: rgba(255, 255, 255, .035);
  border: 1px solid var(--border-soft);
}
.set-grid .set-card { margin-bottom: 0; }
/* A card that follows the two-column grid needs air above it: the grid's own cards have no bottom
   margin, so without this «Кто заходил» sat flush against them and read as part of the same block. */
.set-grid + .set-card { margin-top: 16px; }
.set-panel > .set-card:first-child { margin-top: 0; }
.set-card + .set-card { margin-top: 0; }
.set-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.set-title {
  display: block;
  font-size: 12px; font-weight: 650; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
}
.set-card-head .set-title { margin-bottom: 0; }
.set-note { margin: 10px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
/* The "your system asked for calm motion" explanation. Amber rather than red: nothing is broken,
   there is just a reason and a switch. */
.set-note.warn-note {
  margin-top: 12px;
  padding: 11px 13px;
  border-radius: var(--r-fld);
  color: var(--warn);
  background: rgba(255, 208, 138, .08);
  border: 1px solid rgba(255, 208, 138, .22);
}
.set-note.warn-note b { color: var(--warn); font-weight: 650; }
.set-inline { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 14px; }
.set-inline:first-of-type { margin-top: 12px; }
/* Inside «Акцент» the picker, the swatches and the per-colour grid are three separate controls and
   used to sit shoulder to shoulder; each gets its own line and its own gap. */
.set-card .segmented + .accent-row { margin-top: 16px; }
.accent-row { row-gap: 12px; }
.color-grid + .set-note { margin-top: 12px; }
.field + .form-actions { margin-top: 18px; }
.set-sub { font-size: 13px; color: var(--text-2); min-width: 84px; }
.set-list { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.75; color: var(--text-2); }
.set-list b { color: var(--text); font-weight: 600; }
.set-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  padding-top: 14px; margin-top: 4px;
  border-top: 1px solid var(--border-soft);
}
.set-foot .pop-saved { margin-right: auto; }

/* --- account lists ------------------------------------------------------ */
.acc-list { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.acc-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 13px;
  border-radius: var(--r-fld);
  background-color: rgba(255, 255, 255, .03);
  border: 1px solid var(--border-soft);
  text-align: left;
  transition: background-color var(--t), border-color var(--t), transform .16s var(--ease);
}
button.acc-row { width: 100%; cursor: pointer; color: inherit; font: inherit; }
.acc-row:hover { background-color: rgba(var(--acc-rgb), .1); border-color: rgba(var(--acc-soft-rgb), .3); }
button.acc-row:hover { transform: translateX(2px); }
.acc-row.current { border-color: rgba(var(--acc-soft-rgb), .45); background-color: rgba(var(--acc-rgb), .1); }
.acc-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.acc-main b { font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acc-main span { font-size: 12px; color: var(--muted); }
.acc-side { display: flex; align-items: center; gap: 8px; flex: none; }
.acc-side .mono { font-size: 12px; color: var(--text-2); }
.acc-badge, .acc-kind {
  padding: 2px 9px;
  border-radius: var(--pill);
  font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: lowercase;
  background: rgba(var(--acc-rgb), .18);
  border: 1px solid rgba(var(--acc-soft-rgb), .34);
  color: var(--primary-soft);
  white-space: nowrap;
}
.acc-kind.ok { background: rgba(143, 227, 192, .14); border-color: rgba(143, 227, 192, .32); color: var(--success); }
.acc-kind.bad { background: rgba(255, 154, 169, .14); border-color: rgba(255, 154, 169, .32); color: var(--danger); }
.acc-empty, .acc-note { margin: 10px 0 0; font-size: 12.5px; color: var(--muted); }

/* --- per-colour accent editing ------------------------------------------ */
.color-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
.color-cell {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 11px 12px;
  border-radius: var(--r-fld);
  background-color: rgba(255, 255, 255, .03);
  border: 1px solid var(--border-soft);
  font-size: 12px; color: var(--text-2);
  transition: border-color var(--t), background-color var(--t);
}
.color-cell:hover { border-color: rgba(var(--acc-soft-rgb), .34); background-color: rgba(255, 255, 255, .05); }
.color-cell .color-name { font-size: 12px; color: var(--text-2); }
/* full-width swatch here, unlike the 30px one in the accent row: these four sit in a grid and a
   swatch that does not fill its cell looks like a rendering mistake */
.color-cell .color-swatch { width: 100%; height: 30px; border-radius: 9px; }
.color-cell b { font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--muted); }

/* --- the colour picker ---------------------------------------------------
   Drawn by the panel instead of by the operating system, for the reason the native `<select>` was
   given an opaque background: a white system dialog in the middle of dark glass reads as a different
   application. Same vocabulary as the rest of the window - --sheet, --border, the pill radius, the
   accent for the focus ring - and no literal colours except inside the HSV square, where the whole
   point is colours.

   The square is three stacked backgrounds: the current hue at full saturation, white fading in from
   the left (saturation), black fading in from the bottom (value). That is the standard HSV layout and
   it needs no canvas, no image and no library. */
.cp-pop {
  /* Above .modal-root (200), because the picker is always opened from inside the settings window and
     that window paints a full-screen backdrop of rgba(8,8,10,.88). At z-index 120 - the topbar's - the
     popover opened correctly, was in the viewport, had the right background colour, and was invisible:
     the backdrop covered it. Geometry and computed style cannot see paint order, so the probe that
     passed on all of them now also hit-tests the centre of the popover with elementFromPoint. */
  position: fixed; z-index: 260;
  width: 268px;
  padding: 12px;
  border-radius: var(--r-in);
  background-color: var(--sheet);
  border: 1px solid var(--border);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .9), 0 0 0 1px rgba(var(--acc-rgb), .14);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  animation: cp-in .18s var(--ease) both;
}
.cp-pop[hidden] { display: none; }
@keyframes cp-in { from { opacity: 0; transform: translateY(-6px) scale(.98); } to { opacity: 1; transform: none; } }
.cp-sv {
  position: relative;
  height: 132px;
  border-radius: var(--r-fld);
  border: 1px solid var(--border);
  background-color: #7c5cf0;
  background-image:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
  cursor: crosshair;
  touch-action: none;
  overflow: hidden;
}
.cp-sv:focus-visible { outline: 2px solid rgba(var(--acc-soft-rgb), .8); outline-offset: 2px; }
.cp-cursor {
  position: absolute; top: 0; left: 0;
  width: 15px; height: 15px; margin: -7.5px 0 0 -7.5px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55), 0 2px 8px rgba(0, 0, 0, .5);
  pointer-events: none;
}
.cp-hue { display: flex; align-items: center; gap: 9px; margin-top: 11px; }
.cp-hue > span { font-size: 11.5px; color: var(--muted); flex: none; }
.cp-hue b { font-size: 11px; color: var(--text-2); min-width: 32px; text-align: right; }
.cp-hue input[type="range"] { flex: 1 1 auto; }
/* the hue strip is the one place a rainbow is the correct design */
.cp-hue input[type="range"]::-webkit-slider-runnable-track {
  height: 8px; border-radius: var(--pill); border: 1px solid var(--border);
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.cp-hue input[type="range"]::-moz-range-track {
  height: 8px; border-radius: var(--pill); border: 1px solid var(--border);
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.cp-row { display: flex; align-items: center; gap: 8px; margin-top: 11px; }
.cp-hex {
  flex: 1 1 auto; min-width: 0;
  height: 34px; padding: 0 10px;
  border-radius: var(--r-fld);
  border: 1px solid var(--ctl-border); background-color: var(--input-bg);
  color: var(--text);
  font-family: var(--mono); font-size: 12.5px;
  text-transform: lowercase;
}
.cp-hex:focus { outline: none; border-color: rgba(var(--acc-soft-rgb), .7); }
.cp-hex.is-bad { border-color: rgba(248, 113, 113, .7); }
.cp-preview {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-fld);
  border: 1px solid rgba(255, 255, 255, .18);
  background-color: var(--primary);
}
.cp-row .btn { flex: none; }
.cp-presets { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 11px; }
.cp-presets:empty { display: none; }
.cp-preset {
  width: 22px; height: 22px; padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .18);
  cursor: pointer;
  transition: transform .16s var(--spring), box-shadow .16s var(--ease);
}
.cp-preset:hover { transform: scale(1.14); }
.cp-preset.is-current { box-shadow: 0 0 0 2px rgba(var(--acc-soft-rgb), .85); }
@media (max-width: 620px) {
  .cp-pop { width: min(268px, calc(100vw - 24px)); }
}

/* --- ranges: one rule for every slider in the window --------------------- */
.rng-row { display: flex; align-items: center; gap: 14px; margin-top: 12px; }
.rng-label { font-size: 13px; color: var(--text-2); min-width: 210px; }
.rng-row b { font-size: 12.5px; color: var(--primary-soft); min-width: 46px; text-align: right; }
input[type="range"] {
  -webkit-appearance: none; appearance: none;
  flex: 1; height: 22px; background: transparent; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 6px; border-radius: var(--pill);
  background: rgba(255, 255, 255, .12);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px; margin-top: -5px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid rgba(var(--acc-soft-rgb), .8);
  box-shadow: 0 0 14px -2px rgba(var(--acc-rgb), 1);
  transition: transform .18s var(--spring);
}
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.18); }
input[type="range"]::-moz-range-track { height: 6px; border-radius: var(--pill); background: rgba(255, 255, 255, .12); }
input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px; border-radius: 50%;
  background: #fff; border: 1px solid rgba(var(--acc-soft-rgb), .8);
}
/* the volume slider keeps its filled-track look, which needs the custom property */
input[type="range"]#snd-volume::-webkit-slider-runnable-track {
  background: linear-gradient(90deg, var(--primary) var(--snd-fill, 50%), rgba(255, 255, 255, .12) var(--snd-fill, 50%));
}

/* --- tempo -------------------------------------------------------------- */
/* `motion` is the ceiling, this is the tempo. One multiplier, applied through
   calc() to the durations that are actually felt: transitions, entrances,
   ripple, sheen. Ambient background motion is left alone on purpose - slowing
   a 34-second ribbon drift is not something anybody perceives as "slower UI". */
body { --t: calc(.24s * var(--speed, 1)) var(--ease); }
body[data-speed="slow"] { --speed: 1.7; }
body[data-speed="normal"] { --speed: 1; }
body[data-speed="fast"] { --speed: .6; }
.table tbody tr { animation-duration: calc(.42s * var(--speed, 1)); }
.trow { animation-duration: calc(.34s * var(--speed, 1)); }
.modal { animation-duration: calc(.34s * var(--speed, 1)); }
.pop { animation-duration: calc(.3s * var(--speed, 1)); }
.toast { animation-duration: calc(.42s * var(--speed, 1)); }
.ripple { animation-duration: calc(.62s * var(--speed, 1)); }
.stat, .link-card { transition-duration: calc(.3s * var(--speed, 1)); }
.set-glider { transition-duration: calc(.38s * var(--speed, 1)); }
.set-panel.is-active { animation-duration: calc(.34s * var(--speed, 1)); }
.btn-primary:hover::after { animation-duration: calc(.78s * var(--speed, 1)); }

/* The settings window is the one dialog that needs room: four tabs, each with cards, and «Внешний вид»
   now carries fifteen background tiles. Wider than `.modal.wide` and with its own scroll area, so the
   tab row stays put while the body scrolls - the popover this replaced scrolled as one block, which is
   what made it feel like a list of unrelated controls. The width is capped by the viewport as well:
   900px is the size this window was accepted at, and the tiles are three-up whatever the width, so
   going wider only makes each tile bigger, not the window shorter. */
.modal.modal-settings { max-width: min(900px, calc(100vw - 32px)); }
.modal.modal-settings .modal-body {
  padding: 20px 24px 24px;
  max-height: min(70vh, 700px);
  overscroll-behavior: contain;
}
.modal.modal-settings .set-tabs {
  position: sticky; top: -20px; z-index: 3;
  background-color: var(--sheet);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

/* --- backgrounds -------------------------------------------------------- */
/* «Плазма» and «Лучи» live here, plus the atmosphere for Дождь/Звёзды, whose particle recipes are up
   with the other effect layers; «Сияние» and «Туманность» are further up with the layers they were
   built from, and the five newest effects (Боке, Пузыри, Лепестки, Конфетти, Нити) are particle
   recipes and sit up with the rest. All of them
   animate, all of them are driven by the accent triplets - a background that does not match the
   buttons is decoration fighting the interface. Every layer sits in the DOM permanently and is
   switched on by [data-fx], which is what makes changing background a cross-fade rather than a
   rebuild and lets the settings tiles preview the real recipe.

   Cost rules, because these run behind a panel that polls itself:
     * transform and opacity only - no animating width, top, box-shadow or filter (a *static*
       box-shadow is fine and is used for the heavy raindrops: it is painted
       once and travels with the layer, where animating one would repaint every frame);
     * radial/conic/repeating gradients instead of filter: blur(), which is free after the
       first paint where blurring a moving layer is not;
     * no mix-blend-mode and no mask on a layer that moves. Both were tried on «Занавес» and
       «Каустика» and measured: 13fps and 11fps against 20 for the particle effects in the same
       headless run. Blending forces the whole stacking context to recomposite every frame and a mask
       has to be re-applied on every transform;
     * one animation per layer, and for the gradient layers 12s or slower with no more than ~4
       animated elements. The particle effects (Дождь 44, Звёзды 170, Конфетти 34, Лепестки 30 at
       «обычно») are the deliberate exception: they are the effects that read as alive, they are
       composited-only, and the count is scaled by «Плотность» and again below 720px - see countFor()
       in theme.js;
     * `body[data-motion="off"]` stops all of it, and prefers-reduced-motion stops all of it except
       the backdrop under `body[data-motion="full"]` (an explicit in-app choice). */
.bg-blob, .bg-rays {
  position: absolute;
  pointer-events: none;
  opacity: 0;
  transition: opacity .55s var(--ease);
  will-change: transform, opacity;
}

/* ---- Плазма: three accent blobs drifting on different paths ---- */
.bg-blob { border-radius: 50%; mix-blend-mode: screen; }
.bg-blob-1 {
  top: -16vh; left: -8vw; width: 54vw; height: 48vh;
  background: radial-gradient(circle at 50% 50%, rgba(var(--acc-rgb), .34), rgba(var(--acc-deep-rgb), .12) 46%, transparent 72%);
}
.bg-blob-2 {
  top: 20vh; right: -14vw; width: 46vw; height: 44vh;
  background: radial-gradient(circle at 50% 50%, rgba(var(--acc-soft-rgb), .26), transparent 70%);
}
.bg-blob-3 {
  bottom: -22vh; left: 22vw; width: 46vw; height: 40vh;
  background: radial-gradient(circle at 50% 50%, rgba(var(--acc-pale-rgb), .18), transparent 72%);
}
.bg[data-fx="plasma"] .bg-blob { opacity: 1; }
.bg[data-fx="plasma"] .bg-blob-1 { animation: blob-1 26s var(--ease-io) infinite alternate; }
.bg[data-fx="plasma"] .bg-blob-2 { animation: blob-2 34s var(--ease-io) infinite alternate; }
.bg[data-fx="plasma"] .bg-blob-3 { animation: blob-3 42s var(--ease-io) infinite alternate; }
.bg[data-fx="plasma"] .bg-aurora { opacity: .18; }

@keyframes blob-1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(13vw, 9vh, 0) scale(1.24); }
}
@keyframes blob-2 {
  from { transform: translate3d(0, 0, 0) scale(1.08); }
  to { transform: translate3d(-15vw, 12vh, 0) scale(.9); }
}
@keyframes blob-3 {
  from { transform: translate3d(0, 0, 0) scale(.94); }
  to { transform: translate3d(-9vw, -13vh, 0) scale(1.2); }
}

/* ---- Лучи: a slow conic sweep, masked so it fades before the edges ---- */
.bg-rays {
  top: -70vh; right: -34vw;
  width: 130vw; height: 130vw;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0 5%, rgba(var(--acc-soft-rgb), .17) 8%, transparent 11% 17%,
    rgba(var(--acc-rgb), .13) 20%, transparent 23% 32%,
    rgba(var(--acc-pale-rgb), .11) 35%, transparent 38% 51%,
    rgba(var(--acc-soft-rgb), .15) 54%, transparent 57% 69%,
    rgba(var(--acc-rgb), .11) 72%, transparent 75% 87%,
    rgba(var(--acc-pale-rgb), .13) 90%, transparent 93%);
  mask-image: radial-gradient(circle at 50% 50%, transparent 16%, #000 44%, transparent 72%);
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 16%, #000 44%, transparent 72%);
}
.bg[data-fx="rays"] .bg-rays { opacity: .9; animation: rays-spin 96s linear infinite; }
.bg[data-fx="rays"] .bg-aurora-1 { opacity: .3; }

@keyframes rays-spin { to { transform: rotate(360deg); } }

/* Five gradient effects lived here for one day - «Занавес», «Каустика», «Радар», «Осколки» and
   «Затмение» - and the operator rejected all five in a single message. Their rules are gone rather
   than commented out: dead CSS that "might come back" is how the snow/embers/matrix layer that had
   to be dug out earlier got there. Four more went the same way a day later - «Комета», «Салют»,
   «Светлячки», «Поток» - and they are gone the same way, for the same reason: what survived every
   cut is a shape you can point at, and three of those four were a bright line with a gradient on it
   at different speeds and angles. */

/* «Сетка» was retired here: the neon floor looked like a wallpaper next to the others, and the faint
   grid it was built from stays as ambient texture under the effects that want it (.bg-grid above). */

/* ---- Звёзды: accent motes that breathe, over a faint band ---- */
.bg[data-fx="stars"] .bg-band { opacity: 1; }
.bg[data-fx="stars"] .bg-aurora-1 { opacity: .22; }
.bg[data-fx="stars"] .bg-aurora-3 { opacity: .14; }
.bg[data-fx="stars"] .fx-layer i { animation-name: star-breathe; }

@keyframes star-breathe {
  0%, 100% { transform: translate3d(0, 0, 0) scale(.82); opacity: calc(var(--o, .6) * .45); }
  50% { transform: translate3d(var(--dx, 1cqw), calc(var(--dy, -1cqh) / 2), 0) scale(1.12); opacity: var(--o, .6); }
}

/* --- preview tiles: a miniature of each recipe, cheap enough to run fifteen at once ---
   Only «Плазма» and «Лучи» need an animated ::before of their own. The other thirteen are animated by
   the same particles or layers the full-screen version uses, built at 0.42 scale and 0.34 tempo by
   buildPreviews() - which is also why the particle tiles below only set a background: the flakes,
   sparks, columns, discs and petals are already moving inside them. */
.fx-tile[data-fx="plasma"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(circle at 26% 34%, rgba(var(--acc-rgb), .5), transparent 58%),
    radial-gradient(circle at 74% 62%, rgba(var(--acc-soft-rgb), .38), transparent 60%),
    radial-gradient(circle at 52% 88%, rgba(var(--acc-pale-rgb), .24), transparent 62%);
}
.fx-tile[data-fx="rays"] .fx-prev {
  background-color: var(--bg);
  background-image: conic-gradient(from 210deg at 78% 22%,
    transparent 0 7%, rgba(var(--acc-soft-rgb), .4) 10%, transparent 13% 24%,
    rgba(var(--acc-rgb), .32) 27%, transparent 30% 46%,
    rgba(var(--acc-pale-rgb), .26) 49%, transparent 52% 70%,
    rgba(var(--acc-soft-rgb), .3) 73%, transparent 76%);
}
.fx-tile[data-fx="petals"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 72% 58% at 32% -12%, rgba(var(--acc-pale-rgb), .14), transparent 70%),
    linear-gradient(to top, var(--fx-mist, rgba(200, 225, 255, .14)), transparent 44%);
}
/* The confetti tile needs its four colours in the background too: buildPreviews() hands out six
   particles, and six pieces against a flat field do not say "multicoloured" at a glance the way six
   pieces against a tinted one do. */
.fx-tile[data-fx="confetti"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 120% 62% at 50% 118%, rgba(var(--acc-soft-rgb), .22), transparent 70%),
    linear-gradient(to top, rgba(var(--success, #8fe3c0), .1), transparent 40%);
}
.fx-tile[data-fx="snow"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 70% 60% at 30% -10%, rgba(var(--acc-pale-rgb), .16), transparent 70%),
    linear-gradient(to top, var(--fx-mist, rgba(200, 225, 255, .18)), transparent 46%);
}
/* «Молнии» keeps its storm sky in the tile for the same reason it keeps it full-screen: a bolt is lit
   a twelfth of the time, so a tile showing only the bolt would be blank most of the seconds somebody
   spends looking at it, and would lose to every other tile in the grid for that reason alone. */
.fx-tile[data-fx="lightning"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 110% 66% at 34% -14%, rgba(var(--acc-pale-rgb), .2), transparent 72%),
    linear-gradient(to top, rgba(var(--acc-rgb), .22), transparent 52%);
}
.fx-tile[data-fx="frost"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 74% 58% at 68% -10%, rgba(var(--acc-pale-rgb), .18), transparent 70%),
    linear-gradient(to top, var(--fx-mist, rgba(200, 225, 255, .2)), transparent 48%);
}
/* The dew tile gets the softest field of the lot, because the droplets sit on the near side of the
   glass and everything behind them should be out of focus. */
.fx-tile[data-fx="dew"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 120% 90% at 50% 8%, rgba(var(--acc-soft-rgb), .16), transparent 68%),
    linear-gradient(to top, rgba(var(--acc-rgb), .14), transparent 46%);
}
.fx-tile[data-fx="streamers"] .fx-prev {
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 90% 64% at 26% -12%, rgba(var(--acc-pale-rgb), .16), transparent 70%),
    linear-gradient(112deg, transparent 38%, rgba(var(--acc-soft-rgb), .14) 52%, transparent 66%);
}
.fx-tile[data-fx="embers"] .fx-prev {
  background-color: var(--bg);
  background-image: radial-gradient(ellipse 76% 100% at 50% 118%,
    rgba(var(--acc-rgb), .42), transparent 72%);
}
.fx-tile[data-fx="code"] .fx-prev {
  background-color: var(--bg);
  background-image: repeating-linear-gradient(to bottom,
    rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, .3) 2px 4px);
}
.fx-tile[data-fx="stars"] .fx-prev {
  background-color: var(--bg);
  background-image:
    linear-gradient(112deg, transparent 34%, rgba(var(--acc-soft-rgb), .2) 48%, transparent 64%),
    radial-gradient(circle at 22% 30%, rgba(var(--acc-pale-rgb), .9) 1px, transparent 2px),
    radial-gradient(circle at 68% 58%, rgba(var(--acc-pale-rgb), .8) 1px, transparent 2px),
    radial-gradient(circle at 44% 76%, rgba(var(--acc-pale-rgb), .7) 1px, transparent 2px);
}
/* the two gradient tiles keep one slow animated element each, so they are alive like the rest */
.fx-tile[data-fx="plasma"] .fx-prev::before,
.fx-tile[data-fx="rays"] .fx-prev::before {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
}
.fx-tile[data-fx="rays"] .fx-prev::before {
  background: conic-gradient(from 0deg at 78% 22%, transparent 0 12%, rgba(255, 255, 255, .16) 15%, transparent 18% 50%, rgba(255, 255, 255, .1) 53%, transparent 56%);
  animation: rays-spin 14s linear infinite;
}
.fx-tile[data-fx="plasma"] .fx-prev::before {
  background: radial-gradient(circle at 30% 40%, rgba(255, 255, 255, .12), transparent 60%);
  animation: blob-1 12s var(--ease-io) infinite alternate;
}
/* inside the small previews keep the accents proportionate: the code column head is a full-size
   element scaled by buildPreviews(), but a pseudo-element is not */
.fx-tile[data-fx="code"] .fx-layer i::after { width: 3px; height: 3px; margin-left: -1.5px; bottom: -1px; }
.fx-tile[data-fx="snow"] .fx-layer i { box-shadow: none; }

/* --- small screens ----------------------------------------------------- */

@media (max-width: 720px) {
  .panel, .gate-card, .modal, .pop { border-radius: var(--r-in); }
  .stat { border-radius: var(--r-in); }
  .btn { border-radius: var(--pill); }
  /* blur is the most expensive thing on a phone; the surfaces stay translucent
     and just lose the frost */
  .panel, .stat, .link-card, .term-wrap, .an-chart { backdrop-filter: none; -webkit-backdrop-filter: none; }
  .cat-row { padding: 12px 16px 0; }
  .ld-row { grid-template-columns: 1fr; gap: 3px; }
}

/* --- settings window and account lists on a narrow screen --------------- */
@media (max-width: 900px) {
  .set-grid { grid-template-columns: 1fr; }
  .color-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mode-grid { grid-template-columns: minmax(0, 1fr); }
  .rng-row { flex-wrap: wrap; }
  .rng-label { min-width: 0; flex-basis: 100%; }
  .modal.modal-settings { max-width: 100%; }
  .modal.modal-settings .modal-body { max-height: none; padding: 14px 14px 18px; }
  /* Sticky is pointless once the whole window fits on screen; the tab row itself already scrolls
     sideways (overflow-x on .set-tabs), so four tabs on a 390px phone stay reachable. */
  .modal.modal-settings .set-tabs { position: static; }
  .set-tab { padding: 9px 14px; }
  .user-menu { width: min(320px, calc(100vw - 24px)); }
  .acc-row { flex-wrap: wrap; }
  .acc-side { width: 100%; justify-content: space-between; }
  .acc-avatar-row { flex-direction: column; align-items: flex-start; }
}
